Browse Source

Merge branch 'develop' into css_transitions

css_transitions
jos 10 years ago
parent
commit
5c87ddebb8
112 changed files with 40174 additions and 33341 deletions
  1. +0
    -208
      Jakefile.js
  2. +12
    -8
      README.md
  3. +1
    -1
      dist/vis.css
  4. +32953
    -25864
      dist/vis.js
  5. +1
    -0
      dist/vis.map
  6. +0
    -0
      dist/vis.min.css
  7. +17
    -13
      dist/vis.min.js
  8. +1
    -1
      examples/network/07_selections.html
  9. +139
    -0
      gulpfile.js
  10. +56
    -0
      index.js
  11. +10
    -14
      lib/DOMutil.js
  12. +4
    -0
      lib/DataSet.js
  13. +5
    -0
      lib/DataView.js
  14. +218
    -0
      lib/graph3d/Filter.js
  15. +10
    -466
      lib/graph3d/Graph3d.js
  16. +11
    -0
      lib/graph3d/Point2d.js
  17. +85
    -0
      lib/graph3d/Point3d.js
  18. +140
    -0
      lib/graph3d/StepNumber.js
  19. +0
    -0
      lib/header.js
  20. +10
    -0
      lib/module/hammer.js
  21. +3
    -0
      lib/module/moment.js
  22. +10
    -6
      lib/network/Edge.js
  23. +4
    -0
      lib/network/Groups.js
  24. +2
    -0
      lib/network/Images.js
  25. +24
    -14
      lib/network/Network.js
  26. +3
    -0
      lib/network/Node.js
  27. +2
    -0
      lib/network/Popup.js
  28. +0
    -0
      lib/network/css/network-manipulation.css
  29. +0
    -0
      lib/network/css/network-navigation.css
  30. +826
    -0
      lib/network/dotparser.js
  31. +0
    -0
      lib/network/img/acceptDeleteIcon.png
  32. +0
    -0
      lib/network/img/addNodeIcon.png
  33. +0
    -0
      lib/network/img/backIcon.png
  34. +0
    -0
      lib/network/img/connectIcon.png
  35. +0
    -0
      lib/network/img/cross.png
  36. +0
    -0
      lib/network/img/cross2.png
  37. +0
    -0
      lib/network/img/deleteIcon.png
  38. +0
    -0
      lib/network/img/downArrow.png
  39. +0
    -0
      lib/network/img/editIcon.png
  40. +0
    -0
      lib/network/img/leftArrow.png
  41. +0
    -0
      lib/network/img/minus.png
  42. +0
    -0
      lib/network/img/plus.png
  43. +0
    -0
      lib/network/img/rightArrow.png
  44. +0
    -0
      lib/network/img/upArrow.png
  45. +0
    -0
      lib/network/img/zoomExtends.png
  46. +1137
    -0
      lib/network/mixins/ClusterMixin.js
  47. +304
    -0
      lib/network/mixins/HierarchicalLayoutMixin.js
  48. +571
    -0
      lib/network/mixins/ManipulationMixin.js
  49. +198
    -0
      lib/network/mixins/MixinLoader.js
  50. +196
    -0
      lib/network/mixins/NavigationMixin.js
  51. +548
    -0
      lib/network/mixins/SectorsMixin.js
  52. +705
    -0
      lib/network/mixins/SelectionMixin.js
  53. +393
    -0
      lib/network/mixins/physics/BarnesHutMixin.js
  54. +125
    -0
      lib/network/mixins/physics/HierarchialRepulsionMixin.js
  55. +700
    -0
      lib/network/mixins/physics/PhysicsMixin.js
  56. +58
    -0
      lib/network/mixins/physics/RepulsionMixin.js
  57. +0
    -0
      lib/network/shapes.js
  58. +3
    -2
      lib/timeline/DataStep.js
  59. +13
    -0
      lib/timeline/Graph2d.js
  60. +8
    -2
      lib/timeline/Range.js
  61. +7
    -10
      lib/timeline/Stack.js
  62. +4
    -0
      lib/timeline/TimeStep.js
  63. +13
    -0
      lib/timeline/Timeline.js
  64. +2
    -0
      lib/timeline/component/Component.js
  65. +5
    -1
      lib/timeline/component/CurrentTime.js
  66. +6
    -0
      lib/timeline/component/CustomTime.js
  67. +7
    -0
      lib/timeline/component/DataAxis.js
  68. +8
    -3
      lib/timeline/component/GraphGroup.js
  69. +6
    -0
      lib/timeline/component/Group.js
  70. +13
    -1
      lib/timeline/component/ItemSet.js
  71. +14
    -8
      lib/timeline/component/Legend.js
  72. +10
    -4
      lib/timeline/component/LineGraph.js
  73. +6
    -0
      lib/timeline/component/TimeAxis.js
  74. +0
    -0
      lib/timeline/component/css/animation.css
  75. +0
    -0
      lib/timeline/component/css/currenttime.css
  76. +0
    -0
      lib/timeline/component/css/customtime.css
  77. +0
    -0
      lib/timeline/component/css/dataaxis.css
  78. +0
    -0
      lib/timeline/component/css/item.css
  79. +0
    -0
      lib/timeline/component/css/itemset.css
  80. +0
    -0
      lib/timeline/component/css/labelset.css
  81. +0
    -0
      lib/timeline/component/css/panel.css
  82. +0
    -0
      lib/timeline/component/css/pathStyles.css
  83. +0
    -0
      lib/timeline/component/css/timeaxis.css
  84. +0
    -0
      lib/timeline/component/css/timeline.css
  85. +4
    -0
      lib/timeline/component/item/Item.js
  86. +4
    -0
      lib/timeline/component/item/ItemBox.js
  87. +4
    -0
      lib/timeline/component/item/ItemPoint.js
  88. +5
    -0
      lib/timeline/component/item/ItemRange.js
  89. +0
    -0
      lib/timeline/img/delete.png
  90. +96
    -94
      lib/util.js
  91. +22
    -12
      package.json
  92. +0
    -84
      src/module/exports.js
  93. +0
    -31
      src/module/imports.js
  94. +0
    -829
      src/network/dotparser.js
  95. +0
    -1143
      src/network/networkMixins/ClusterMixin.js
  96. +0
    -311
      src/network/networkMixins/HierarchicalLayoutMixin.js
  97. +0
    -576
      src/network/networkMixins/ManipulationMixin.js
  98. +0
    -199
      src/network/networkMixins/MixinLoader.js
  99. +0
    -205
      src/network/networkMixins/NavigationMixin.js
  100. +0
    -552
      src/network/networkMixins/SectorsMixin.js

+ 0
- 208
Jakefile.js View File

@ -1,208 +0,0 @@
/**
* Jake build script
*/
var jake = require('jake'),
browserify = require('browserify'),
wrench = require('wrench'),
CleanCSS = require('clean-css'),
fs = require('fs');
require('jake-utils');
// constants
var DIST = './dist';
var VIS = DIST + '/vis.js';
var VIS_CSS = DIST + '/vis.css';
var VIS_TMP = DIST + '/vis.js.tmp';
var VIS_MIN = DIST + '/vis.min.js';
var VIS_MIN_CSS = DIST + '/vis.min.css';
/**
* default task
*/
desc('Default task: build all libraries');
task('default', ['build', 'minify'], function () {
console.log('done');
});
/**
* build the visualization library vis.js
*/
desc('Build the visualization library vis.js');
task('build', {async: true}, function () {
jake.mkdirP(DIST);
jake.mkdirP(DIST + '/img');
// concatenate and stringify the css files
concat({
src: [
'./src/timeline/component/css/timeline.css',
'./src/timeline/component/css/panel.css',
'./src/timeline/component/css/labelset.css',
'./src/timeline/component/css/itemset.css',
'./src/timeline/component/css/item.css',
'./src/timeline/component/css/timeaxis.css',
'./src/timeline/component/css/currenttime.css',
'./src/timeline/component/css/customtime.css',
'./src/timeline/component/css/animation.css',
'./src/timeline/component/css/dataaxis.css',
'./src/timeline/component/css/pathStyles.css',
'./src/network/css/network-manipulation.css',
'./src/network/css/network-navigation.css'
],
dest: VIS_CSS,
separator: '\n'
});
console.log('created ' + VIS_CSS);
// concatenate the script files
concat({
dest: VIS_TMP,
src: [
'./src/module/imports.js',
'./src/shim.js',
'./src/util.js',
'./src/DOMutil.js',
'./src/DataSet.js',
'./src/DataView.js',
'./src/timeline/component/GraphGroup.js',
'./src/timeline/component/Legend.js',
'./src/timeline/component/DataAxis.js',
'./src/timeline/component/LineGraph.js',
'./src/timeline/DataStep.js',
'./src/timeline/Stack.js',
'./src/timeline/TimeStep.js',
'./src/timeline/Range.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/TimeAxis.js',
'./src/timeline/component/CurrentTime.js',
'./src/timeline/component/CustomTime.js',
'./src/timeline/component/ItemSet.js',
'./src/timeline/component/item/*.js',
'./src/timeline/component/Group.js',
'./src/timeline/Timeline.js',
'./src/timeline/Graph2d.js',
'./src/network/dotparser.js',
'./src/network/shapes.js',
'./src/network/Node.js',
'./src/network/Edge.js',
'./src/network/Popup.js',
'./src/network/Groups.js',
'./src/network/Images.js',
'./src/network/networkMixins/physics/PhysicsMixin.js',
'./src/network/networkMixins/physics/HierarchialRepulsion.js',
'./src/network/networkMixins/physics/BarnesHut.js',
'./src/network/networkMixins/physics/Repulsion.js',
'./src/network/networkMixins/HierarchicalLayoutMixin.js',
'./src/network/networkMixins/ManipulationMixin.js',
'./src/network/networkMixins/SectorsMixin.js',
'./src/network/networkMixins/ClusterMixin.js',
'./src/network/networkMixins/SelectionMixin.js',
'./src/network/networkMixins/NavigationMixin.js',
'./src/network/networkMixins/MixinLoader.js',
'./src/network/Network.js',
'./src/graph3d/Graph3d.js',
'./src/module/exports.js'
],
separator: '\n'
});
// copy images
wrench.copyDirSyncRecursive('./src/network/img', DIST + '/img/network', {
forceDelete: true
});
wrench.copyDirSyncRecursive('./src/timeline/img', DIST + '/img/timeline', {
forceDelete: true
});
var timeStart = Date.now();
// bundle the concatenated script and dependencies into one file
var b = browserify();
b.add(VIS_TMP);
b.bundle({
standalone: 'vis'
}, function (err, code) {
if(err) {
throw err;
}
console.log("browserify",Date.now() - timeStart); timeStart = Date.now();
// add header and footer
var lib = read('./src/module/header.js') + code;
// write bundled file
write(VIS, lib);
console.log('created js' + VIS);
// remove temporary file
fs.unlinkSync(VIS_TMP);
// update version number and stuff in the javascript files
replacePlaceholders(VIS);
complete();
});
});
/**
* minify the visualization library vis.js
*/
desc('Minify the visualization library vis.js');
task('minify', {async: true}, function () {
// minify javascript
minify({
src: VIS,
dest: VIS_MIN,
header: read('./src/module/header.js')
});
// update version number and stuff in the javascript files
replacePlaceholders(VIS_MIN);
console.log('created minified ' + VIS_MIN);
var minified = new CleanCSS().minify(read(VIS_CSS));
write(VIS_MIN_CSS, minified);
console.log('created minified ' + VIS_MIN_CSS);
});
/**
* test task
*/
desc('Test the library');
task('test', function () {
// TODO: use a testing suite for testing: nodeunit, mocha, tap, ...
var filelist = new jake.FileList();
filelist.include([
'./test/**/*.js'
]);
var files = filelist.toArray();
files.forEach(function (file) {
require('./' + file);
});
console.log('Executed ' + files.length + ' test files successfully');
});
/**
* replace version, date, and name placeholders in the provided file
* @param {String} filename
*/
var replacePlaceholders = function (filename) {
replace({
replacements: [
{pattern: '@@date', replacement: today()},
{pattern: '@@version', replacement: version()}
],
src: filename
});
};

+ 12
- 8
README.md View File

@ -124,15 +124,9 @@ To build the library from source, clone the project from github
git clone git://github.com/almende/vis.git git clone git://github.com/almende/vis.git
The project uses [jake](https://github.com/mde/jake) as build tool.
The build script uses [Browserify](http://browserify.org/) to
bundle the source code into a library,
and uses [UglifyJS](http://lisperator.net/uglifyjs/) to minify the code.
The source code uses the module style of node (require and module.exports) to The source code uses the module style of node (require and module.exports) to
organize dependencies.
To install all dependencies and build the library, run `npm install` in the
root of the project.
organize dependencies. To install all dependencies and build the library,
run `npm install` in the root of the project.
cd vis cd vis
npm install npm install
@ -141,6 +135,16 @@ Then, the project can be build running:
npm run build npm run build
To automatically rebuild on changes in the source files, once can use
npm run watch
This will both build and minify the library on changes. Minifying is relatively
slow, so when only the non-minified library is needed, one can use the
`watch-dev` script instead:
npm run watch-dev
## Test ## Test

+ 1
- 1
dist/vis.css View File

@ -703,4 +703,4 @@ div.network-navigation.zoomExtends {
background-image: url("img/network/zoomExtends.png"); background-image: url("img/network/zoomExtends.png");
bottom:50px; bottom:50px;
right:15px; right:15px;
}
}

+ 32953
- 25864
dist/vis.js
File diff suppressed because it is too large
View File


+ 1
- 0
dist/vis.map
File diff suppressed because it is too large
View File


+ 0
- 0
dist/vis.min.css View File


+ 17
- 13
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
examples/network/07_selections.html View File

@ -56,7 +56,7 @@
}); });
// set initial selection (id's of some nodes) // set initial selection (id's of some nodes)
network.setSelection([3, 4, 5]);
network.selectNodes([3, 4, 5]);
</script> </script>
</body> </body>

+ 139
- 0
gulpfile.js View File

@ -0,0 +1,139 @@
var fs = require('fs');
var gulp = require('gulp');
var gutil = require('gulp-util');
var concat = require('gulp-concat');
var minifyCSS = require('gulp-minify-css');
var rename = require("gulp-rename");
var webpack = require('webpack');
var uglify = require('uglify-js');
var rimraf = require('rimraf');
var merge = require('merge-stream');
var ENTRY = './index.js';
var HEADER = './lib/header.js';
var DIST = './dist';
var VIS_JS = 'vis.js';
var VIS_MAP = 'vis.map';
var VIS_CSS = 'vis.css';
var VIS_MIN_CSS = 'vis.min.css';
var DIST_VIS_MIN_JS = DIST + '/vis.min.js';
var DIST_VIS_MAP = DIST + '/' + VIS_MAP;
// generate banner with today's date and correct version
function createBanner() {
var today = gutil.date(new Date(), 'yyyy-mm-dd'); // today, formatted as yyyy-mm-dd
var version = require('./package.json').version;
return String(fs.readFileSync(HEADER))
.replace('@@date', today)
.replace('@@version', version);
}
var bannerPlugin = new webpack.BannerPlugin(createBanner(), {
entryOnly: true,
raw: true
});
// TODO: the moment.js language files should be excluded by default (they are quite big)
var webpackConfig = {
entry: ENTRY,
output: {
library: 'vis',
libraryTarget: 'umd',
path: DIST,
filename: VIS_JS,
sourcePrefix: ' '
},
plugins: [ bannerPlugin ],
cache: true
};
var uglifyConfig = {
outSourceMap: VIS_MAP,
output: {
comments: /@license/
}
};
// create a single instance of the compiler to allow caching
var compiler = webpack(webpackConfig);
// clean the dist directory
gulp.task('clean', function (cb) {
rimraf(DIST, cb);
});
gulp.task('bundle-js', ['clean'], function (cb) {
// update the banner contents (has a date in it which should stay up to date)
bannerPlugin.banner = createBanner();
compiler.run(function (err, stats) {
if (err) gutil.log(err);
cb();
});
});
// bundle and minify css
gulp.task('bundle-css', ['clean'], function () {
var files = [
'./lib/timeline/component/css/timeline.css',
'./lib/timeline/component/css/panel.css',
'./lib/timeline/component/css/labelset.css',
'./lib/timeline/component/css/itemset.css',
'./lib/timeline/component/css/item.css',
'./lib/timeline/component/css/timeaxis.css',
'./lib/timeline/component/css/currenttime.css',
'./lib/timeline/component/css/customtime.css',
'./lib/timeline/component/css/animation.css',
'./lib/timeline/component/css/dataaxis.css',
'./lib/timeline/component/css/pathStyles.css',
'./lib/network/css/network-manipulation.css',
'./lib/network/css/network-navigation.css'
];
return gulp.src(files)
.pipe(concat(VIS_CSS))
.pipe(gulp.dest(DIST))
// TODO: nicer to put minifying css in a separate task?
.pipe(minifyCSS())
.pipe(rename(VIS_MIN_CSS))
.pipe(gulp.dest(DIST));
});
gulp.task('copy-img', ['clean'], function () {
var network = gulp.src('./lib/network/img/**/*')
.pipe(gulp.dest(DIST + '/img/network'));
var timeline = gulp.src('./lib/timeline/img/**/*')
.pipe(gulp.dest(DIST + '/img/timeline'));
return merge(network, timeline);
});
gulp.task('minify', ['bundle-js'], function (cb) {
var result = uglify.minify([DIST + '/' + VIS_JS], uglifyConfig);
fs.writeFileSync(DIST_VIS_MIN_JS, result.code);
fs.writeFileSync(DIST_VIS_MAP, result.map);
cb();
});
gulp.task('bundle', ['bundle-js', 'bundle-css', 'copy-img']);
// The watch task (to automatically rebuild when the source code changes)
gulp.task('watch', ['bundle', 'minify'], function () {
gulp.watch(['index.js', 'lib/**/*'], ['bundle', 'minify']);
});
// The watch task (to automatically rebuild when the source code changes)
// this watch only rebuilds vis.js, not vis.min.js
gulp.task('watch-dev', ['bundle'], function () {
gulp.watch(['index.js', 'lib/**/*'], ['bundle']);
});
// The default task (called when you run `gulp`)
gulp.task('default', ['clean', 'bundle', 'minify']);

+ 56
- 0
index.js View File

@ -0,0 +1,56 @@
// utils
exports.util = require('./lib/util');
exports.DOMutil = require('./lib/DOMutil');
// data
exports.DataSet = require('./lib/DataSet');
exports.DataView = require('./lib/DataView');
// Graph3d
exports.Graph3d = require('./lib/graph3d/Graph3d');
// Timeline
exports.Timeline = require('./lib/timeline/Timeline');
exports.Graph2d = require('./lib/timeline/Graph2d');
exports.timeline= {
DataStep: require('./lib/timeline/DataStep'),
Range: require('./lib/timeline/Range'),
stack: require('./lib/timeline/Stack'),
TimeStep: require('./lib/timeline/TimeStep'),
components: {
items: {
Item: require('./lib/timeline/component/item/Item'),
ItemBox: require('./lib/timeline/component/item/ItemBox'),
ItemPoint: require('./lib/timeline/component/item/ItemPoint'),
ItemRange: require('./lib/timeline/component/item/ItemRange')
},
Component: require('./lib/timeline/component/Component'),
CurrentTime: require('./lib/timeline/component/CurrentTime'),
CustomTime: require('./lib/timeline/component/CustomTime'),
DataAxis: require('./lib/timeline/component/DataAxis'),
GraphGroup: require('./lib/timeline/component/GraphGroup'),
Group: require('./lib/timeline/component/Group'),
ItemSet: require('./lib/timeline/component/ItemSet'),
Legend: require('./lib/timeline/component/Legend'),
LineGraph: require('./lib/timeline/component/LineGraph'),
TimeAxis: require('./lib/timeline/component/TimeAxis')
}
};
// Network
exports.Network = require('./lib/network/Network');
exports.network = {
Edge: require('./lib/network/Edge'),
Groups: require('./lib/network/Groups'),
Images: require('./lib/network/Images'),
Node: require('./lib/network/Node'),
Popup: require('./lib/network/Popup'),
dotparser: require('./lib/network/dotparser')
};
// Deprecated since v3.0.0
exports.Graph = function () {
throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
};

src/DOMutil.js → lib/DOMutil.js View File

@ -1,15 +1,11 @@
/**
* Created by Alex on 6/20/14.
*/
var DOMutil = {};
// DOM utility methods
/** /**
* this prepares the JSON container for allocating SVG elements * this prepares the JSON container for allocating SVG elements
* @param JSONcontainer * @param JSONcontainer
* @private * @private
*/ */
DOMutil.prepareElements = function(JSONcontainer) {
exports.prepareElements = function(JSONcontainer) {
// cleanup the redundant svgElements; // cleanup the redundant svgElements;
for (var elementType in JSONcontainer) { for (var elementType in JSONcontainer) {
if (JSONcontainer.hasOwnProperty(elementType)) { if (JSONcontainer.hasOwnProperty(elementType)) {
@ -26,7 +22,7 @@ DOMutil.prepareElements = function(JSONcontainer) {
* @param JSONcontainer * @param JSONcontainer
* @private * @private
*/ */
DOMutil.cleanupElements = function(JSONcontainer) {
exports.cleanupElements = function(JSONcontainer) {
// cleanup the redundant svgElements; // cleanup the redundant svgElements;
for (var elementType in JSONcontainer) { for (var elementType in JSONcontainer) {
if (JSONcontainer.hasOwnProperty(elementType)) { if (JSONcontainer.hasOwnProperty(elementType)) {
@ -50,7 +46,7 @@ DOMutil.cleanupElements = function(JSONcontainer) {
* @returns {*} * @returns {*}
* @private * @private
*/ */
DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
var element; var element;
// allocate SVG element, if it doesnt yet exist, create one. // allocate SVG element, if it doesnt yet exist, create one.
if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
@ -86,7 +82,7 @@ DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
* @returns {*} * @returns {*}
* @private * @private
*/ */
DOMutil.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
var element; var element;
// allocate SVG element, if it doesnt yet exist, create one. // allocate SVG element, if it doesnt yet exist, create one.
if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
@ -126,17 +122,17 @@ DOMutil.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
* @param svgContainer * @param svgContainer
* @returns {*} * @returns {*}
*/ */
DOMutil.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
var point; var point;
if (group.options.drawPoints.style == 'circle') { if (group.options.drawPoints.style == 'circle') {
point = DOMutil.getSVGElement('circle',JSONcontainer,svgContainer);
point = exports.getSVGElement('circle',JSONcontainer,svgContainer);
point.setAttributeNS(null, "cx", x); point.setAttributeNS(null, "cx", x);
point.setAttributeNS(null, "cy", y); point.setAttributeNS(null, "cy", y);
point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size); point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size);
point.setAttributeNS(null, "class", group.className + " point"); point.setAttributeNS(null, "class", group.className + " point");
} }
else { else {
point = DOMutil.getSVGElement('rect',JSONcontainer,svgContainer);
point = exports.getSVGElement('rect',JSONcontainer,svgContainer);
point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size); point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size);
point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size); point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size);
point.setAttributeNS(null, "width", group.options.drawPoints.size); point.setAttributeNS(null, "width", group.options.drawPoints.size);
@ -153,8 +149,8 @@ DOMutil.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
* @param y * @param y
* @param className * @param className
*/ */
DOMutil.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
var rect = DOMutil.getSVGElement('rect',JSONcontainer, svgContainer);
exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
var rect = exports.getSVGElement('rect',JSONcontainer, svgContainer);
rect.setAttributeNS(null, "x", x - 0.5 * width); rect.setAttributeNS(null, "x", x - 0.5 * width);
rect.setAttributeNS(null, "y", y); rect.setAttributeNS(null, "y", y);
rect.setAttributeNS(null, "width", width); rect.setAttributeNS(null, "width", width);

src/DataSet.js → lib/DataSet.js View File

@ -1,3 +1,5 @@
var util = require('./util');
/** /**
* DataSet * DataSet
* *
@ -932,3 +934,5 @@ DataSet.prototype._appendRow = function (dataTable, columns, item) {
dataTable.setValue(row, col, item[field]); dataTable.setValue(row, col, item[field]);
} }
}; };
module.exports = DataSet;

src/DataView.js → lib/DataView.js View File

@ -1,3 +1,6 @@
var util = require('./util');
var DataSet = require('./DataSet');
/** /**
* DataView * DataView
* *
@ -294,3 +297,5 @@ DataView.prototype._trigger = DataSet.prototype._trigger;
// TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5) // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
DataView.prototype.subscribe = DataView.prototype.on; DataView.prototype.subscribe = DataView.prototype.on;
DataView.prototype.unsubscribe = DataView.prototype.off; DataView.prototype.unsubscribe = DataView.prototype.off;
module.exports = DataView;

+ 218
- 0
lib/graph3d/Filter.js View File

@ -0,0 +1,218 @@
var DataView = require('../DataView');
/**
* @class Filter
*
* @param {DataSet} data The google data table
* @param {Number} column The index of the column to be filtered
* @param {Graph} graph The graph
*/
function Filter (data, column, graph) {
this.data = data;
this.column = column;
this.graph = graph; // the parent graph
this.index = undefined;
this.value = undefined;
// read all distinct values and select the first one
this.values = graph.getDistinctValues(data.get(), this.column);
// sort both numeric and string values correctly
this.values.sort(function (a, b) {
return a > b ? 1 : a < b ? -1 : 0;
});
if (this.values.length > 0) {
this.selectValue(0);
}
// create an array with the filtered datapoints. this will be loaded afterwards
this.dataPoints = [];
this.loaded = false;
this.onLoadCallback = undefined;
if (graph.animationPreload) {
this.loaded = false;
this.loadInBackground();
}
else {
this.loaded = true;
}
};
/**
* Return the label
* @return {string} label
*/
Filter.prototype.isLoaded = function() {
return this.loaded;
};
/**
* Return the loaded progress
* @return {Number} percentage between 0 and 100
*/
Filter.prototype.getLoadedProgress = function() {
var len = this.values.length;
var i = 0;
while (this.dataPoints[i]) {
i++;
}
return Math.round(i / len * 100);
};
/**
* Return the label
* @return {string} label
*/
Filter.prototype.getLabel = function() {
return this.graph.filterLabel;
};
/**
* Return the columnIndex of the filter
* @return {Number} columnIndex
*/
Filter.prototype.getColumn = function() {
return this.column;
};
/**
* Return the currently selected value. Returns undefined if there is no selection
* @return {*} value
*/
Filter.prototype.getSelectedValue = function() {
if (this.index === undefined)
return undefined;
return this.values[this.index];
};
/**
* Retrieve all values of the filter
* @return {Array} values
*/
Filter.prototype.getValues = function() {
return this.values;
};
/**
* Retrieve one value of the filter
* @param {Number} index
* @return {*} value
*/
Filter.prototype.getValue = function(index) {
if (index >= this.values.length)
throw 'Error: index out of range';
return this.values[index];
};
/**
* Retrieve the (filtered) dataPoints for the currently selected filter index
* @param {Number} [index] (optional)
* @return {Array} dataPoints
*/
Filter.prototype._getDataPoints = function(index) {
if (index === undefined)
index = this.index;
if (index === undefined)
return [];
var dataPoints;
if (this.dataPoints[index]) {
dataPoints = this.dataPoints[index];
}
else {
var f = {};
f.column = this.column;
f.value = this.values[index];
var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
dataPoints = this.graph._getDataPoints(dataView);
this.dataPoints[index] = dataPoints;
}
return dataPoints;
};
/**
* Set a callback function when the filter is fully loaded.
*/
Filter.prototype.setOnLoadCallback = function(callback) {
this.onLoadCallback = callback;
};
/**
* Add a value to the list with available values for this filter
* No double entries will be created.
* @param {Number} index
*/
Filter.prototype.selectValue = function(index) {
if (index >= this.values.length)
throw 'Error: index out of range';
this.index = index;
this.value = this.values[index];
};
/**
* Load all filtered rows in the background one by one
* Start this method without providing an index!
*/
Filter.prototype.loadInBackground = function(index) {
if (index === undefined)
index = 0;
var frame = this.graph.frame;
if (index < this.values.length) {
var dataPointsTemp = this._getDataPoints(index);
//this.graph.redrawInfo(); // TODO: not neat
// create a progress box
if (frame.progress === undefined) {
frame.progress = document.createElement('DIV');
frame.progress.style.position = 'absolute';
frame.progress.style.color = 'gray';
frame.appendChild(frame.progress);
}
var progress = this.getLoadedProgress();
frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
// TODO: this is no nice solution...
frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider
frame.progress.style.left = 10 + 'px';
var me = this;
setTimeout(function() {me.loadInBackground(index+1);}, 10);
this.loaded = false;
}
else {
this.loaded = true;
// remove the progress box
if (frame.progress !== undefined) {
frame.removeChild(frame.progress);
frame.progress = undefined;
}
if (this.onLoadCallback)
this.onLoadCallback();
}
};
module.exports = Filter;

src/graph3d/Graph3d.js → lib/graph3d/Graph3d.js View File

@ -1,3 +1,11 @@
var Emitter = require('emitter-component');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Point3d = require('./Point3d');
var Point2d = require('./Point2d');
var Filter = require('./Filter');
var StepNumber = require('./StepNumber');
/** /**
* @constructor Graph3d * @constructor Graph3d
* Graph3d displays data in 3d. * Graph3d displays data in 3d.
@ -715,19 +723,6 @@ Graph3d.prototype._getDataPoints = function (data) {
return dataPoints; return dataPoints;
}; };
/**
* Append suffix 'px' to provided value x
* @param {int} x An integer value
* @return {string} the string value of x, followed by the suffix 'px'
*/
Graph3d.px = function(x) {
return x + 'px';
};
/** /**
* Create the main frame for the Graph3d. * Create the main frame for the Graph3d.
* This function is executed once when a Graph3d object is created. The frame * This function is executed once when a Graph3d object is created. The frame
@ -2455,458 +2450,6 @@ G3DpreventDefault = function (event) {
} }
}; };
/**
* @prototype Point3d
* @param {Number} x
* @param {Number} y
* @param {Number} z
*/
function Point3d(x, y, z) {
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : 0;
this.z = z !== undefined ? z : 0;
};
/**
* Subtract the two provided points, returns a-b
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} a-b
*/
Point3d.subtract = function(a, b) {
var sub = new Point3d();
sub.x = a.x - b.x;
sub.y = a.y - b.y;
sub.z = a.z - b.z;
return sub;
};
/**
* Add the two provided points, returns a+b
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} a+b
*/
Point3d.add = function(a, b) {
var sum = new Point3d();
sum.x = a.x + b.x;
sum.y = a.y + b.y;
sum.z = a.z + b.z;
return sum;
};
/**
* Calculate the average of two 3d points
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} The average, (a+b)/2
*/
Point3d.avg = function(a, b) {
return new Point3d(
(a.x + b.x) / 2,
(a.y + b.y) / 2,
(a.z + b.z) / 2
);
};
/**
* Calculate the cross product of the two provided points, returns axb
* Documentation: http://en.wikipedia.org/wiki/Cross_product
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} cross product axb
*/
Point3d.crossProduct = function(a, b) {
var crossproduct = new Point3d();
crossproduct.x = a.y * b.z - a.z * b.y;
crossproduct.y = a.z * b.x - a.x * b.z;
crossproduct.z = a.x * b.y - a.y * b.x;
return crossproduct;
};
/**
* Rtrieve the length of the vector (or the distance from this point to the origin
* @return {Number} length
*/
Point3d.prototype.length = function() {
return Math.sqrt(
this.x * this.x +
this.y * this.y +
this.z * this.z
);
};
/**
* @prototype Point2d
*/
Point2d = function (x, y) {
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : 0;
};
/**
* @class Filter
*
* @param {DataSet} data The google data table
* @param {Number} column The index of the column to be filtered
* @param {Graph} graph The graph
*/
function Filter (data, column, graph) {
this.data = data;
this.column = column;
this.graph = graph; // the parent graph
this.index = undefined;
this.value = undefined;
// read all distinct values and select the first one
this.values = graph.getDistinctValues(data.get(), this.column);
// sort both numeric and string values correctly
this.values.sort(function (a, b) {
return a > b ? 1 : a < b ? -1 : 0;
});
if (this.values.length > 0) {
this.selectValue(0);
}
// create an array with the filtered datapoints. this will be loaded afterwards
this.dataPoints = [];
this.loaded = false;
this.onLoadCallback = undefined;
if (graph.animationPreload) {
this.loaded = false;
this.loadInBackground();
}
else {
this.loaded = true;
}
};
/**
* Return the label
* @return {string} label
*/
Filter.prototype.isLoaded = function() {
return this.loaded;
};
/**
* Return the loaded progress
* @return {Number} percentage between 0 and 100
*/
Filter.prototype.getLoadedProgress = function() {
var len = this.values.length;
var i = 0;
while (this.dataPoints[i]) {
i++;
}
return Math.round(i / len * 100);
};
/**
* Return the label
* @return {string} label
*/
Filter.prototype.getLabel = function() {
return this.graph.filterLabel;
};
/**
* Return the columnIndex of the filter
* @return {Number} columnIndex
*/
Filter.prototype.getColumn = function() {
return this.column;
};
/**
* Return the currently selected value. Returns undefined if there is no selection
* @return {*} value
*/
Filter.prototype.getSelectedValue = function() {
if (this.index === undefined)
return undefined;
return this.values[this.index];
};
/**
* Retrieve all values of the filter
* @return {Array} values
*/
Filter.prototype.getValues = function() {
return this.values;
};
/**
* Retrieve one value of the filter
* @param {Number} index
* @return {*} value
*/
Filter.prototype.getValue = function(index) {
if (index >= this.values.length)
throw 'Error: index out of range';
return this.values[index];
};
/**
* Retrieve the (filtered) dataPoints for the currently selected filter index
* @param {Number} [index] (optional)
* @return {Array} dataPoints
*/
Filter.prototype._getDataPoints = function(index) {
if (index === undefined)
index = this.index;
if (index === undefined)
return [];
var dataPoints;
if (this.dataPoints[index]) {
dataPoints = this.dataPoints[index];
}
else {
var f = {};
f.column = this.column;
f.value = this.values[index];
var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
dataPoints = this.graph._getDataPoints(dataView);
this.dataPoints[index] = dataPoints;
}
return dataPoints;
};
/**
* Set a callback function when the filter is fully loaded.
*/
Filter.prototype.setOnLoadCallback = function(callback) {
this.onLoadCallback = callback;
};
/**
* Add a value to the list with available values for this filter
* No double entries will be created.
* @param {Number} index
*/
Filter.prototype.selectValue = function(index) {
if (index >= this.values.length)
throw 'Error: index out of range';
this.index = index;
this.value = this.values[index];
};
/**
* Load all filtered rows in the background one by one
* Start this method without providing an index!
*/
Filter.prototype.loadInBackground = function(index) {
if (index === undefined)
index = 0;
var frame = this.graph.frame;
if (index < this.values.length) {
var dataPointsTemp = this._getDataPoints(index);
//this.graph.redrawInfo(); // TODO: not neat
// create a progress box
if (frame.progress === undefined) {
frame.progress = document.createElement('DIV');
frame.progress.style.position = 'absolute';
frame.progress.style.color = 'gray';
frame.appendChild(frame.progress);
}
var progress = this.getLoadedProgress();
frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
// TODO: this is no nice solution...
frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
frame.progress.style.left = Graph3d.px(10);
var me = this;
setTimeout(function() {me.loadInBackground(index+1);}, 10);
this.loaded = false;
}
else {
this.loaded = true;
// remove the progress box
if (frame.progress !== undefined) {
frame.removeChild(frame.progress);
frame.progress = undefined;
}
if (this.onLoadCallback)
this.onLoadCallback();
}
};
/**
* @prototype StepNumber
* The class StepNumber is an iterator for Numbers. You provide a start and end
* value, and a best step size. StepNumber itself rounds to fixed values and
* a finds the step that best fits the provided step.
*
* If prettyStep is true, the step size is chosen as close as possible to the
* provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
*
* Example usage:
* var step = new StepNumber(0, 10, 2.5, true);
* step.start();
* while (!step.end()) {
* alert(step.getCurrent());
* step.next();
* }
*
* Version: 1.0
*
* @param {Number} start The start value
* @param {Number} end The end value
* @param {Number} step Optional. Step size. Must be a positive value.
* @param {boolean} prettyStep Optional. If true, the step size is rounded
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
StepNumber = function (start, end, step, prettyStep) {
// set default values
this._start = 0;
this._end = 0;
this._step = 1;
this.prettyStep = true;
this.precision = 5;
this._current = 0;
this.setRange(start, end, step, prettyStep);
};
/**
* Set a new range: start, end and step.
*
* @param {Number} start The start value
* @param {Number} end The end value
* @param {Number} step Optional. Step size. Must be a positive value.
* @param {boolean} prettyStep Optional. If true, the step size is rounded
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
this._start = start ? start : 0;
this._end = end ? end : 0;
this.setStep(step, prettyStep);
};
/**
* Set a new step size
* @param {Number} step New step size. Must be a positive value
* @param {boolean} prettyStep Optional. If true, the provided step is rounded
* to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
StepNumber.prototype.setStep = function(step, prettyStep) {
if (step === undefined || step <= 0)
return;
if (prettyStep !== undefined)
this.prettyStep = prettyStep;
if (this.prettyStep === true)
this._step = StepNumber.calculatePrettyStep(step);
else
this._step = step;
};
/**
* Calculate a nice step size, closest to the desired step size.
* Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
* integer Number. For example 1, 2, 5, 10, 20, 50, etc...
* @param {Number} step Desired step size
* @return {Number} Nice step size
*/
StepNumber.calculatePrettyStep = function (step) {
var log10 = function (x) {return Math.log(x) / Math.LN10;};
// try three steps (multiple of 1, 2, or 5
var step1 = Math.pow(10, Math.round(log10(step))),
step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
// choose the best step (closest to minimum step)
var prettyStep = step1;
if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
// for safety
if (prettyStep <= 0) {
prettyStep = 1;
}
return prettyStep;
};
/**
* returns the current value of the step
* @return {Number} current value
*/
StepNumber.prototype.getCurrent = function () {
return parseFloat(this._current.toPrecision(this.precision));
};
/**
* returns the current step size
* @return {Number} current step size
*/
StepNumber.prototype.getStep = function () {
return this._step;
};
/**
* Set the current value to the largest value smaller than start, which
* is a multiple of the step size
*/
StepNumber.prototype.start = function() {
this._current = this._start - this._start % this._step;
};
/**
* Do a step, add the step size to the current value
*/
StepNumber.prototype.next = function () {
this._current += this._step;
};
/**
* Returns true whether the end is reached
* @return {boolean} True if the current value has passed the end value.
*/
StepNumber.prototype.end = function () {
return (this._current > this._end);
};
/** /**
* @constructor Slider * @constructor Slider
* *
@ -2981,7 +2524,7 @@ function Slider(container, options) {
this.playTimeout = undefined; this.playTimeout = undefined;
this.playInterval = 1000; // milliseconds this.playInterval = 1000; // milliseconds
this.playLoop = true; this.playLoop = true;
};
}
/** /**
* Select the previous index * Select the previous index
@ -3308,3 +2851,4 @@ getMouseY = function(event) {
return event.targetTouches[0] && event.targetTouches[0].clientY || 0; return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
}; };
module.exports = Graph3d;

+ 11
- 0
lib/graph3d/Point2d.js View File

@ -0,0 +1,11 @@
/**
* @prototype Point2d
* @param {Number} [x]
* @param {Number} [y]
*/
Point2d = function (x, y) {
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : 0;
};
module.exports = Point2d;

+ 85
- 0
lib/graph3d/Point3d.js View File

@ -0,0 +1,85 @@
/**
* @prototype Point3d
* @param {Number} [x]
* @param {Number} [y]
* @param {Number} [z]
*/
function Point3d(x, y, z) {
this.x = x !== undefined ? x : 0;
this.y = y !== undefined ? y : 0;
this.z = z !== undefined ? z : 0;
};
/**
* Subtract the two provided points, returns a-b
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} a-b
*/
Point3d.subtract = function(a, b) {
var sub = new Point3d();
sub.x = a.x - b.x;
sub.y = a.y - b.y;
sub.z = a.z - b.z;
return sub;
};
/**
* Add the two provided points, returns a+b
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} a+b
*/
Point3d.add = function(a, b) {
var sum = new Point3d();
sum.x = a.x + b.x;
sum.y = a.y + b.y;
sum.z = a.z + b.z;
return sum;
};
/**
* Calculate the average of two 3d points
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} The average, (a+b)/2
*/
Point3d.avg = function(a, b) {
return new Point3d(
(a.x + b.x) / 2,
(a.y + b.y) / 2,
(a.z + b.z) / 2
);
};
/**
* Calculate the cross product of the two provided points, returns axb
* Documentation: http://en.wikipedia.org/wiki/Cross_product
* @param {Point3d} a
* @param {Point3d} b
* @return {Point3d} cross product axb
*/
Point3d.crossProduct = function(a, b) {
var crossproduct = new Point3d();
crossproduct.x = a.y * b.z - a.z * b.y;
crossproduct.y = a.z * b.x - a.x * b.z;
crossproduct.z = a.x * b.y - a.y * b.x;
return crossproduct;
};
/**
* Rtrieve the length of the vector (or the distance from this point to the origin
* @return {Number} length
*/
Point3d.prototype.length = function() {
return Math.sqrt(
this.x * this.x +
this.y * this.y +
this.z * this.z
);
};
module.exports = Point3d;

+ 140
- 0
lib/graph3d/StepNumber.js View File

@ -0,0 +1,140 @@
/**
* @prototype StepNumber
* The class StepNumber is an iterator for Numbers. You provide a start and end
* value, and a best step size. StepNumber itself rounds to fixed values and
* a finds the step that best fits the provided step.
*
* If prettyStep is true, the step size is chosen as close as possible to the
* provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
*
* Example usage:
* var step = new StepNumber(0, 10, 2.5, true);
* step.start();
* while (!step.end()) {
* alert(step.getCurrent());
* step.next();
* }
*
* Version: 1.0
*
* @param {Number} start The start value
* @param {Number} end The end value
* @param {Number} step Optional. Step size. Must be a positive value.
* @param {boolean} prettyStep Optional. If true, the step size is rounded
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
function StepNumber(start, end, step, prettyStep) {
// set default values
this._start = 0;
this._end = 0;
this._step = 1;
this.prettyStep = true;
this.precision = 5;
this._current = 0;
this.setRange(start, end, step, prettyStep);
};
/**
* Set a new range: start, end and step.
*
* @param {Number} start The start value
* @param {Number} end The end value
* @param {Number} step Optional. Step size. Must be a positive value.
* @param {boolean} prettyStep Optional. If true, the step size is rounded
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
this._start = start ? start : 0;
this._end = end ? end : 0;
this.setStep(step, prettyStep);
};
/**
* Set a new step size
* @param {Number} step New step size. Must be a positive value
* @param {boolean} prettyStep Optional. If true, the provided step is rounded
* to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
*/
StepNumber.prototype.setStep = function(step, prettyStep) {
if (step === undefined || step <= 0)
return;
if (prettyStep !== undefined)
this.prettyStep = prettyStep;
if (this.prettyStep === true)
this._step = StepNumber.calculatePrettyStep(step);
else
this._step = step;
};
/**
* Calculate a nice step size, closest to the desired step size.
* Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
* integer Number. For example 1, 2, 5, 10, 20, 50, etc...
* @param {Number} step Desired step size
* @return {Number} Nice step size
*/
StepNumber.calculatePrettyStep = function (step) {
var log10 = function (x) {return Math.log(x) / Math.LN10;};
// try three steps (multiple of 1, 2, or 5
var step1 = Math.pow(10, Math.round(log10(step))),
step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
// choose the best step (closest to minimum step)
var prettyStep = step1;
if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
// for safety
if (prettyStep <= 0) {
prettyStep = 1;
}
return prettyStep;
};
/**
* returns the current value of the step
* @return {Number} current value
*/
StepNumber.prototype.getCurrent = function () {
return parseFloat(this._current.toPrecision(this.precision));
};
/**
* returns the current step size
* @return {Number} current step size
*/
StepNumber.prototype.getStep = function () {
return this._step;
};
/**
* Set the current value to the largest value smaller than start, which
* is a multiple of the step size
*/
StepNumber.prototype.start = function() {
this._current = this._start - this._start % this._step;
};
/**
* Do a step, add the step size to the current value
*/
StepNumber.prototype.next = function () {
this._current += this._step;
};
/**
* Returns true whether the end is reached
* @return {boolean} True if the current value has passed the end value.
*/
StepNumber.prototype.end = function () {
return (this._current > this._end);
};
module.exports = StepNumber;

src/module/header.js → lib/header.js View File


+ 10
- 0
lib/module/hammer.js View File

@ -0,0 +1,10 @@
// Only load hammer.js when in a browser environment
// (loading hammer.js in a node.js environment gives errors)
if (typeof window !== 'undefined') {
module.exports = require('hammerjs');
}
else {
module.exports = function () {
throw Error('hammer.js is only available in a browser, not in node.js.');
}
}

+ 3
- 0
lib/module/moment.js View File

@ -0,0 +1,3 @@
// first check if moment.js is already loaded in the browser window, if so,
// use this instance. Else, load via commonjs.
module.exports = (typeof window !== 'undefined') && window['moment'] || require('moment');

src/network/Edge.js → lib/network/Edge.js View File

@ -1,3 +1,5 @@
var util = require('../util');
/** /**
* @class Edge * @class Edge
* *
@ -861,7 +863,7 @@ Edge.prototype._drawControlNodes = function(ctx) {
else { else {
this.controlNodes = {from:null, to:null, positions:{}}; this.controlNodes = {from:null, to:null, positions:{}};
} }
}
};
/** /**
* Enable control nodes. * Enable control nodes.
@ -869,7 +871,7 @@ Edge.prototype._drawControlNodes = function(ctx) {
*/ */
Edge.prototype._enableControlNodes = function() { Edge.prototype._enableControlNodes = function() {
this.controlNodesEnabled = true; this.controlNodesEnabled = true;
}
};
/** /**
* disable control nodes * disable control nodes
@ -877,7 +879,7 @@ Edge.prototype._enableControlNodes = function() {
*/ */
Edge.prototype._disableControlNodes = function() { Edge.prototype._disableControlNodes = function() {
this.controlNodesEnabled = false; this.controlNodesEnabled = false;
}
};
/** /**
* This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
@ -904,7 +906,7 @@ Edge.prototype._getSelectedControlNode = function(x,y) {
else { else {
return null; return null;
} }
}
};
/** /**
@ -922,7 +924,7 @@ Edge.prototype._restoreControlNodes = function() {
this.connectedNode = null; this.connectedNode = null;
this.controlNodes.to.unselect(); this.controlNodes.to.unselect();
} }
}
};
/** /**
* this calculates the position of the control nodes on the edges of the parent nodes. * this calculates the position of the control nodes on the edges of the parent nodes.
@ -961,4 +963,6 @@ Edge.prototype.getControlNodePositions = function(ctx) {
} }
return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}}; return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
}
};
module.exports = Edge;

src/network/Groups.js → lib/network/Groups.js View File

@ -1,3 +1,5 @@
var util = require('../util');
/** /**
* @class Groups * @class Groups
* This class can store groups and properties specific for groups. * This class can store groups and properties specific for groups.
@ -78,3 +80,5 @@ Groups.prototype.add = function (groupname, style) {
} }
return style; return style;
}; };
module.exports = Groups;

src/network/Images.js → lib/network/Images.js View File

@ -39,3 +39,5 @@ Images.prototype.load = function(url) {
return img; return img;
}; };
module.exports = Images;

src/network/Network.js → lib/network/Network.js View File

@ -1,3 +1,20 @@
var Emitter = require('emitter-component');
var Hammer = require('hammerjs');
var mousetrap = require('mousetrap');
var util = require('../util');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var dotparser = require('./dotparser');
var Groups = require('./Groups');
var Images = require('./Images');
var Node = require('./Node');
var Edge = require('./Edge');
var Popup = require('./Popup');
var MixinLoader = require('./mixins/MixinLoader');
// Load custom shapes into CanvasRenderingContext2D
require('./shapes');
/** /**
* @constructor Network * @constructor Network
* Create a network visualization, displaying nodes and edges. * Create a network visualization, displaying nodes and edges.
@ -495,7 +512,7 @@ Network.prototype.setData = function(data, disableStart) {
if (data && data.dot) { if (data && data.dot) {
// parse DOT file // parse DOT file
if(data && data.dot) { if(data && data.dot) {
var dotData = vis.util.DOTToGraph(data.dot);
var dotData = dotparser.DOTToGraph(data.dot);
this.setData(dotData); this.setData(dotData);
return; return;
} }
@ -874,8 +891,8 @@ Network.prototype._createKeyBinds = function() {
*/ */
Network.prototype._getPointer = function (touch) { Network.prototype._getPointer = function (touch) {
return { return {
x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas),
y: touch.pageY - util.getAbsoluteTop(this.frame.canvas)
}; };
}; };
@ -2232,9 +2249,9 @@ Network.prototype._createBezierNodes = function() {
* @private * @private
*/ */
Network.prototype._initializeMixinLoaders = function () { Network.prototype._initializeMixinLoaders = function () {
for (var mixinFunction in networkMixinLoaders) {
if (networkMixinLoaders.hasOwnProperty(mixinFunction)) {
Network.prototype[mixinFunction] = networkMixinLoaders[mixinFunction];
for (var mixin in MixinLoader) {
if (MixinLoader.hasOwnProperty(mixin)) {
Network.prototype[mixin] = MixinLoader[mixin];
} }
} }
}; };
@ -2289,11 +2306,4 @@ Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
} }
}; };
module.exports = Network;

src/network/Node.js → lib/network/Node.js View File

@ -1,3 +1,5 @@
var util = require('../util');
/** /**
* @class Node * @class Node
* A node. A node can be connected to other nodes via one or multiple edges. * A node. A node can be connected to other nodes via one or multiple edges.
@ -967,3 +969,4 @@ Node.prototype.updateVelocity = function(massBeforeClustering) {
this.vy = Math.sqrt(energyBefore/this.mass); this.vy = Math.sqrt(energyBefore/this.mass);
}; };
module.exports = Node;

src/network/Popup.js → lib/network/Popup.js View File

@ -130,3 +130,5 @@ Popup.prototype.show = function (show) {
Popup.prototype.hide = function () { Popup.prototype.hide = function () {
this.frame.style.visibility = "hidden"; this.frame.style.visibility = "hidden";
}; };
module.exports = Popup;

src/network/css/network-manipulation.css → lib/network/css/network-manipulation.css View File


src/network/css/network-navigation.css → lib/network/css/network-navigation.css View File


+ 826
- 0
lib/network/dotparser.js View File

@ -0,0 +1,826 @@
/**
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
*
* DOT language reference: http://www.graphviz.org/doc/info/lang.html
*
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
function parseDOT (data) {
dot = data;
return parseGraph();
}
// token types enumeration
var TOKENTYPE = {
NULL : 0,
DELIMITER : 1,
IDENTIFIER: 2,
UNKNOWN : 3
};
// map with all delimiters
var DELIMITERS = {
'{': true,
'}': true,
'[': true,
']': true,
';': true,
'=': true,
',': true,
'->': true,
'--': true
};
var dot = ''; // current dot file
var index = 0; // current index in dot file
var c = ''; // current token character in expr
var token = ''; // current token
var tokenType = TOKENTYPE.NULL; // type of the token
/**
* Get the first character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function first() {
index = 0;
c = dot.charAt(0);
}
/**
* Get the next character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function next() {
index++;
c = dot.charAt(index);
}
/**
* Preview the next character from the dot file.
* @return {String} cNext
*/
function nextPreview() {
return dot.charAt(index + 1);
}
/**
* Test whether given character is alphabetic or numeric
* @param {String} c
* @return {Boolean} isAlphaNumeric
*/
var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
function isAlphaNumeric(c) {
return regexAlphaNumeric.test(c);
}
/**
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
* @return {Object} a
*/
function merge (a, b) {
if (!a) {
a = {};
}
if (b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
}
return a;
}
/**
* Set a value in an object, where the provided parameter name can be a
* path with nested parameters. For example:
*
* var obj = {a: 2};
* setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
*
* @param {Object} obj
* @param {String} path A parameter name or dot-separated parameter path,
* like "color.highlight.border".
* @param {*} value
*/
function setValue(obj, path, value) {
var keys = path.split('.');
var o = obj;
while (keys.length) {
var key = keys.shift();
if (keys.length) {
// this isn't the end point
if (!o[key]) {
o[key] = {};
}
o = o[key];
}
else {
// this is the end point
o[key] = value;
}
}
}
/**
* Add a node to a graph object. If there is already a node with
* the same id, their attributes will be merged.
* @param {Object} graph
* @param {Object} node
*/
function addNode(graph, node) {
var i, len;
var current = null;
// find root graph (in case of subgraph)
var graphs = [graph]; // list with all graphs from current graph to root graph
var root = graph;
while (root.parent) {
graphs.push(root.parent);
root = root.parent;
}
// find existing node (at root level) by its id
if (root.nodes) {
for (i = 0, len = root.nodes.length; i < len; i++) {
if (node.id === root.nodes[i].id) {
current = root.nodes[i];
break;
}
}
}
if (!current) {
// this is a new node
current = {
id: node.id
};
if (graph.node) {
// clone default attributes
current.attr = merge(current.attr, graph.node);
}
}
// add node to this (sub)graph and all its parent graphs
for (i = graphs.length - 1; i >= 0; i--) {
var g = graphs[i];
if (!g.nodes) {
g.nodes = [];
}
if (g.nodes.indexOf(current) == -1) {
g.nodes.push(current);
}
}
// merge attributes
if (node.attr) {
current.attr = merge(current.attr, node.attr);
}
}
/**
* Add an edge to a graph object
* @param {Object} graph
* @param {Object} edge
*/
function addEdge(graph, edge) {
if (!graph.edges) {
graph.edges = [];
}
graph.edges.push(edge);
if (graph.edge) {
var attr = merge({}, graph.edge); // clone default attributes
edge.attr = merge(attr, edge.attr); // merge attributes
}
}
/**
* Create an edge to a graph object
* @param {Object} graph
* @param {String | Number | Object} from
* @param {String | Number | Object} to
* @param {String} type
* @param {Object | null} attr
* @return {Object} edge
*/
function createEdge(graph, from, to, type, attr) {
var edge = {
from: from,
to: to,
type: type
};
if (graph.edge) {
edge.attr = merge({}, graph.edge); // clone default attributes
}
edge.attr = merge(edge.attr || {}, attr); // merge attributes
return edge;
}
/**
* Get next token in the current dot file.
* The token and token type are available as token and tokenType
*/
function getToken() {
tokenType = TOKENTYPE.NULL;
token = '';
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
next();
}
do {
var isComment = false;
// skip comment
if (c == '#') {
// find the previous non-space character
var i = index - 1;
while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
i--;
}
if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
// the # is at the start of a line, this is indeed a line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
}
if (c == '/' && nextPreview() == '/') {
// skip line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
if (c == '/' && nextPreview() == '*') {
// skip block comment
while (c != '') {
if (c == '*' && nextPreview() == '/') {
// end of block comment found. skip these last two characters
next();
next();
break;
}
else {
next();
}
}
isComment = true;
}
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
next();
}
}
while (isComment);
// check for end of dot file
if (c == '') {
// token is still empty
tokenType = TOKENTYPE.DELIMITER;
return;
}
// check for delimiters consisting of 2 characters
var c2 = c + nextPreview();
if (DELIMITERS[c2]) {
tokenType = TOKENTYPE.DELIMITER;
token = c2;
next();
next();
return;
}
// check for delimiters consisting of 1 character
if (DELIMITERS[c]) {
tokenType = TOKENTYPE.DELIMITER;
token = c;
next();
return;
}
// check for an identifier (number or string)
// TODO: more precise parsing of numbers/strings (and the port separator ':')
if (isAlphaNumeric(c) || c == '-') {
token += c;
next();
while (isAlphaNumeric(c)) {
token += c;
next();
}
if (token == 'false') {
token = false; // convert to boolean
}
else if (token == 'true') {
token = true; // convert to boolean
}
else if (!isNaN(Number(token))) {
token = Number(token); // convert to number
}
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// check for a string enclosed by double quotes
if (c == '"') {
next();
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
token += c;
if (c == '"') { // skip the escape character
next();
}
next();
}
if (c != '"') {
throw newSyntaxError('End of string " expected');
}
next();
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// something unknown is found, wrong characters, a syntax error
tokenType = TOKENTYPE.UNKNOWN;
while (c != '') {
token += c;
next();
}
throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
}
/**
* Parse a graph.
* @returns {Object} graph
*/
function parseGraph() {
var graph = {};
first();
getToken();
// optional strict keyword
if (token == 'strict') {
graph.strict = true;
getToken();
}
// graph or digraph keyword
if (token == 'graph' || token == 'digraph') {
graph.type = token;
getToken();
}
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
graph.id = token;
getToken();
}
// open angle bracket
if (token != '{') {
throw newSyntaxError('Angle bracket { expected');
}
getToken();
// statements
parseStatements(graph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// end of file
if (token !== '') {
throw newSyntaxError('End of file expected');
}
getToken();
// remove temporary default properties
delete graph.node;
delete graph.edge;
delete graph.graph;
return graph;
}
/**
* Parse a list with statements.
* @param {Object} graph
*/
function parseStatements (graph) {
while (token !== '' && token != '}') {
parseStatement(graph);
if (token == ';') {
getToken();
}
}
}
/**
* Parse a single statement. Can be a an attribute statement, node
* statement, a series of node statements and edge statements, or a
* parameter.
* @param {Object} graph
*/
function parseStatement(graph) {
// parse subgraph
var subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement
var attr = parseAttributeStatement(graph);
if (attr) {
return;
}
// parse node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
var id = token; // id can be a string or a number
getToken();
if (token == '=') {
// id statement
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
graph[id] = token;
getToken();
// TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
}
else {
parseNodeStatement(graph, id);
}
}
/**
* Parse a subgraph
* @param {Object} graph parent graph object
* @return {Object | null} subgraph
*/
function parseSubgraph (graph) {
var subgraph = null;
// optional subgraph keyword
if (token == 'subgraph') {
subgraph = {};
subgraph.type = 'subgraph';
getToken();
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
subgraph.id = token;
getToken();
}
}
// open angle bracket
if (token == '{') {
getToken();
if (!subgraph) {
subgraph = {};
}
subgraph.parent = graph;
subgraph.node = graph.node;
subgraph.edge = graph.edge;
subgraph.graph = graph.graph;
// statements
parseStatements(subgraph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// remove temporary default properties
delete subgraph.node;
delete subgraph.edge;
delete subgraph.graph;
delete subgraph.parent;
// register at the parent graph
if (!graph.subgraphs) {
graph.subgraphs = [];
}
graph.subgraphs.push(subgraph);
}
return subgraph;
}
/**
* parse an attribute statement like "node [shape=circle fontSize=16]".
* Available keywords are 'node', 'edge', 'graph'.
* The previous list with default attributes will be replaced
* @param {Object} graph
* @returns {String | null} keyword Returns the name of the parsed attribute
* (node, edge, graph), or null if nothing
* is parsed.
*/
function parseAttributeStatement (graph) {
// attribute statements
if (token == 'node') {
getToken();
// node attributes
graph.node = parseAttributeList();
return 'node';
}
else if (token == 'edge') {
getToken();
// edge attributes
graph.edge = parseAttributeList();
return 'edge';
}
else if (token == 'graph') {
getToken();
// graph attributes
graph.graph = parseAttributeList();
return 'graph';
}
return null;
}
/**
* parse a node statement
* @param {Object} graph
* @param {String | Number} id
*/
function parseNodeStatement(graph, id) {
// node statement
var node = {
id: id
};
var attr = parseAttributeList();
if (attr) {
node.attr = attr;
}
addNode(graph, node);
// edge statements
parseEdge(graph, id);
}
/**
* Parse an edge or a series of edges
* @param {Object} graph
* @param {String | Number} from Id of the from node
*/
function parseEdge(graph, from) {
while (token == '->' || token == '--') {
var to;
var type = token;
getToken();
var subgraph = parseSubgraph(graph);
if (subgraph) {
to = subgraph;
}
else {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier or subgraph expected');
}
to = token;
addNode(graph, {
id: to
});
getToken();
}
// parse edge attributes
var attr = parseAttributeList();
// create edge
var edge = createEdge(graph, from, to, type, attr);
addEdge(graph, edge);
from = to;
}
}
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | null} attr
*/
function parseAttributeList() {
var attr = null;
while (token == '[') {
getToken();
attr = {};
while (token !== '' && token != ']') {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Attribute name expected');
}
var name = token;
getToken();
if (token != '=') {
throw newSyntaxError('Equal sign = expected');
}
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Attribute value expected');
}
var value = token;
setValue(attr, name, value); // name can be a path
getToken();
if (token ==',') {
getToken();
}
}
if (token != ']') {
throw newSyntaxError('Bracket ] expected');
}
getToken();
}
return attr;
}
/**
* Create a syntax error with extra information on current token and index.
* @param {String} message
* @returns {SyntaxError} err
*/
function newSyntaxError(message) {
return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
}
/**
* Chop off text after a maximum length
* @param {String} text
* @param {Number} maxLength
* @returns {String}
*/
function chop (text, maxLength) {
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
}
/**
* Execute a function fn for each pair of elements in two arrays
* @param {Array | *} array1
* @param {Array | *} array2
* @param {function} fn
*/
function forEach2(array1, array2, fn) {
if (array1 instanceof Array) {
array1.forEach(function (elem1) {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(elem1, elem2);
});
}
else {
fn(elem1, array2);
}
});
}
else {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(array1, elem2);
});
}
else {
fn(array1, array2);
}
}
}
/**
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
function DOTToGraph (data) {
// parse the DOT file
var dotData = parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {}
};
// copy the nodes
if (dotData.nodes) {
dotData.nodes.forEach(function (dotNode) {
var graphNode = {
id: dotNode.id,
label: String(dotNode.label || dotNode.id)
};
merge(graphNode, dotNode.attr);
if (graphNode.image) {
graphNode.shape = 'image';
}
graphData.nodes.push(graphNode);
});
}
// copy the edges
if (dotData.edges) {
/**
* Convert an edge in DOT format to an edge with VisGraph format
* @param {Object} dotEdge
* @returns {Object} graphEdge
*/
function convertEdge(dotEdge) {
var graphEdge = {
from: dotEdge.from,
to: dotEdge.to
};
merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
return graphEdge;
}
dotData.edges.forEach(function (dotEdge) {
var from, to;
if (dotEdge.from instanceof Object) {
from = dotEdge.from.nodes;
}
else {
from = {
id: dotEdge.from
}
}
if (dotEdge.to instanceof Object) {
to = dotEdge.to.nodes;
}
else {
to = {
id: dotEdge.to
}
}
if (dotEdge.from instanceof Object && dotEdge.from.edges) {
dotEdge.from.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
});
}
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
return graphData;
}
// exports
exports.parseDOT = parseDOT;
exports.DOTToGraph = DOTToGraph;

src/network/img/acceptDeleteIcon.png → lib/network/img/acceptDeleteIcon.png View File


src/network/img/addNodeIcon.png → lib/network/img/addNodeIcon.png View File


src/network/img/backIcon.png → lib/network/img/backIcon.png View File


src/network/img/connectIcon.png → lib/network/img/connectIcon.png View File


src/network/img/cross.png → lib/network/img/cross.png View File


src/network/img/cross2.png → lib/network/img/cross2.png View File


src/network/img/deleteIcon.png → lib/network/img/deleteIcon.png View File


src/network/img/downArrow.png → lib/network/img/downArrow.png View File


src/network/img/editIcon.png → lib/network/img/editIcon.png View File


src/network/img/leftArrow.png → lib/network/img/leftArrow.png View File


src/network/img/minus.png → lib/network/img/minus.png View File


src/network/img/plus.png → lib/network/img/plus.png View File


src/network/img/rightArrow.png → lib/network/img/rightArrow.png View File


src/network/img/upArrow.png → lib/network/img/upArrow.png View File


src/network/img/zoomExtends.png → lib/network/img/zoomExtends.png View File


+ 1137
- 0
lib/network/mixins/ClusterMixin.js
File diff suppressed because it is too large
View File


+ 304
- 0
lib/network/mixins/HierarchicalLayoutMixin.js View File

@ -0,0 +1,304 @@
exports._resetLevels = function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
}
}
}
};
/**
* This is the main function to layout the nodes in a hierarchical way.
* It checks if the node details are supplied correctly
*
* @private
*/
exports._setupHierarchicalLayout = function() {
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
this.constants.hierarchicalLayout.levelSeparation *= -1;
}
else {
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
}
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
}
}
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) {
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent(true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
this._determineLevels(hubsize);
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system.
this._placeNodesByHierarchy(distribution);
// start the simulation.
this.start();
}
}
};
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
* @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
exports._placeNodesByHierarchy = function(distribution) {
var nodeId, node;
// start placing all the level 0 nodes first. Then recursively position their branches.
for (nodeId in distribution[0].nodes) {
if (distribution[0].nodes.hasOwnProperty(nodeId)) {
node = distribution[0].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[0].minPos;
node.xFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
else {
if (node.yFixed) {
node.y = distribution[0].minPos;
node.yFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
};
/**
* This function get the distribution of levels based on hubsize
*
* @returns {Object}
* @private
*/
exports._getDistribution = function() {
var distribution = {};
var nodeId, node, level;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
if (!distribution.hasOwnProperty(node.level)) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[node.id] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
};
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
exports._determineLevels = function(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
}
}
// branch from hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
};
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
*
* @private
*/
exports._changeConstants = function() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
this.constants.smoothCurves = false;
this._configureSmoothCurves();
};
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
*
* @param edges
* @param parentId
* @param distribution
* @param parentLevel
* @private
*/
exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
};
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
exports._setLevel = function(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
};
/**
* Unfix nodes
*
* @private
*/
exports._restoreNodes = function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId].xFixed = false;
this.nodes[nodeId].yFixed = false;
}
}
};

+ 571
- 0
lib/network/mixins/ManipulationMixin.js View File

@ -0,0 +1,571 @@
var util = require('../../util');
/**
* clears the toolbar div element of children
*
* @private
*/
exports._clearManipulatorBar = function() {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
};
/**
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
* these functions to their original functionality, we saved them in this.cachedFunctions.
* This function restores these functions to their original function.
*
* @private
*/
exports._restoreOverloadedFunctions = function() {
for (var functionName in this.cachedFunctions) {
if (this.cachedFunctions.hasOwnProperty(functionName)) {
this[functionName] = this.cachedFunctions[functionName];
}
}
};
/**
* Enable or disable edit-mode.
*
* @private
*/
exports._toggleEditMode = function() {
this.editMode = !this.editMode;
var toolbar = document.getElementById("network-manipulationDiv");
var closeDiv = document.getElementById("network-manipulation-closeDiv");
var editModeDiv = document.getElementById("network-manipulation-editMode");
if (this.editMode == true) {
toolbar.style.display="block";
closeDiv.style.display="block";
editModeDiv.style.display="none";
closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
toolbar.style.display="none";
closeDiv.style.display="none";
editModeDiv.style.display="block";
closeDiv.onclick = null;
}
this._createManipulatorBar()
};
/**
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
* @private
*/
exports._createManipulatorBar = function() {
// remove bound functions
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
if (this.edgeBeingEdited !== undefined) {
this.edgeBeingEdited._disableControlNodes();
this.edgeBeingEdited = undefined;
this.selectedControlNode = null;
}
// restore overloaded functions
this._restoreOverloadedFunctions();
// resume calculation
this.freezeSimulation = false;
// reset global variables
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false;
if (this.editMode == true) {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
}
// bind the icons
var addNodeButton = document.getElementById("network-manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
var addEdgeButton = document.getElementById("network-manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
var editButton = document.getElementById("network-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
var editButton = document.getElementById("network-manipulate-editEdge");
editButton.onclick = this._createEditEdgeToolbar.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("network-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
var closeDiv = document.getElementById("network-manipulation-closeDiv");
closeDiv.onclick = this._toggleEditMode.bind(this);
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
}
else {
this.editModeDiv.innerHTML = "" +
"<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
var editModeButton = document.getElementById("network-manipulate-editModeButton");
editModeButton.onclick = this._toggleEditMode.bind(this);
}
};
/**
* Create the toolbar for adding Nodes
*
* @private
*/
exports._createAddNodeToolbar = function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._addNode.bind(this);
this.on('select', this.boundFunction);
};
/**
* create the toolbar to connect nodes
*
* @private
*/
exports._createAddEdgeToolbar = function() {
// clear the toolbar
this._clearManipulatorBar();
this._unselectAll(true);
this.freezeSimulation = true;
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this._unselectAll();
this.forceAppendSelection = false;
this.blockConnectingEdgeSelection = true;
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._handleConnect.bind(this);
this.on('select', this.boundFunction);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this._handleTouch = this._handleConnect;
this._handleOnRelease = this._finishConnect;
// redraw to show the unselect
this._redraw();
};
/**
* create the toolbar to edit edges
*
* @private
*/
exports._createEditEdgeToolbar = function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this.edgeBeingEdited = this._getSelectedEdge();
this.edgeBeingEdited._enableControlNodes();
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this.cachedFunctions["_handleTap"] = this._handleTap;
this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleTouch = this._selectControlNode;
this._handleTap = function () {};
this._handleOnDrag = this._controlNodeDrag;
this._handleDragStart = function () {}
this._handleOnRelease = this._releaseControlNode;
// redraw to show the unselect
this._redraw();
};
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
exports._selectControlNode = function(pointer) {
this.edgeBeingEdited.controlNodes.from.unselect();
this.edgeBeingEdited.controlNodes.to.unselect();
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
if (this.selectedControlNode !== null) {
this.selectedControlNode.select();
this.freezeSimulation = true;
}
this._redraw();
};
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
exports._controlNodeDrag = function(event) {
var pointer = this._getPointer(event.gesture.center);
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
}
this._redraw();
};
exports._releaseControlNode = function(pointer) {
var newNode = this._getNodeAt(pointer);
if (newNode != null) {
if (this.edgeBeingEdited.controlNodes.from.selected == true) {
this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
this.edgeBeingEdited.controlNodes.from.unselect();
}
if (this.edgeBeingEdited.controlNodes.to.selected == true) {
this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
this.edgeBeingEdited.controlNodes.to.unselect();
}
}
else {
this.edgeBeingEdited._restoreControlNodes();
}
this.freezeSimulation = false;
this._redraw();
};
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
exports._handleConnect = function(pointer) {
if (this._getSelectedNodeCount() == 0) {
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._selectObject(node,false);
// create a node the temporary line can look at
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetNode'].x = node.x;
this.sectors['support']['nodes']['targetNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetViaNode'].x = node.x;
this.sectors['support']['nodes']['targetViaNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
// create a temporary edge
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
this.edges['connectionEdge'].from = node;
this.edges['connectionEdge'].connected = true;
this.edges['connectionEdge'].smooth = true;
this.edges['connectionEdge'].selected = true;
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.gesture.center);
this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
};
this.moving = true;
this.start();
}
}
}
};
exports._finishConnect = function(pointer) {
if (this._getSelectedNodeCount() == 1) {
// restore the drag function
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
delete this.cachedFunctions["_handleOnDrag"];
// remember the edge id
var connectFromId = this.edges['connectionEdge'].fromId;
// remove the temporary nodes and edge
delete this.edges['connectionEdge'];
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._createEdge(connectFromId,node.id);
this._createManipulatorBar();
}
}
this._unselectAll();
}
};
/**
* Adds a node on the specified location
*/
exports._addNode = function() {
if (this._selectionIsEmpty() && this.editMode == true) {
var positionObject = this._pointerToPositionObject(this.pointerPosition);
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
if (this.triggerFunctions.add) {
if (this.triggerFunctions.add.length == 2) {
var me = this;
this.triggerFunctions.add(defaultData, function(finalizedData) {
me.nodesData.add(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels['addError']);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
else {
this.nodesData.add(defaultData);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
};
/**
* connect two nodes with a new edge.
*
* @private
*/
exports._createEdge = function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.connect) {
if (this.triggerFunctions.connect.length == 2) {
var me = this;
this.triggerFunctions.connect(defaultData, function(finalizedData) {
me.edgesData.add(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.add(defaultData);
this.moving = true;
this.start();
}
}
};
/**
* connect two nodes with a new edge.
*
* @private
*/
exports._editEdge = function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.editEdge) {
if (this.triggerFunctions.editEdge.length == 2) {
var me = this;
this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
me.edgesData.update(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.update(defaultData);
this.moving = true;
this.start();
}
}
};
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
*
* @private
*/
exports._editNode = function() {
if (this.triggerFunctions.edit && this.editMode == true) {
var node = this._getSelectedNode();
var data = {id:node.id,
label: node.label,
group: node.group,
shape: node.shape,
color: {
background:node.color.background,
border:node.color.border,
highlight: {
background:node.color.highlight.background,
border:node.color.highlight.border
}
}};
if (this.triggerFunctions.edit.length == 2) {
var me = this;
this.triggerFunctions.edit(data, function (finalizedData) {
me.nodesData.update(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["editError"]);
}
}
else {
alert(this.constants.labels["editBoundError"]);
}
};
/**
* delete everything in the selection
*
* @private
*/
exports._deleteSelected = function() {
if (!this._selectionIsEmpty() && this.editMode == true) {
if (!this._clusterInSelection()) {
var selectedNodes = this.getSelectedNodes();
var selectedEdges = this.getSelectedEdges();
if (this.triggerFunctions.del) {
var me = this;
var data = {nodes: selectedNodes, edges: selectedEdges};
if (this.triggerFunctions.del.length = 2) {
this.triggerFunctions.del(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
me._unselectAll();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["deleteError"])
}
}
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this._unselectAll();
this.moving = true;
this.start();
}
}
else {
alert(this.constants.labels["deleteClusterError"]);
}
}
};

+ 198
- 0
lib/network/mixins/MixinLoader.js View File

@ -0,0 +1,198 @@
var PhysicsMixin = require('./physics/PhysicsMixin');
var ClusterMixin = require('./ClusterMixin');
var SectorsMixin = require('./SectorsMixin');
var SelectionMixin = require('./SelectionMixin');
var ManipulationMixin = require('./ManipulationMixin');
var NavigationMixin = require('./NavigationMixin');
var HierarchicalLayoutMixin = require('./HierarchicalLayoutMixin');
/**
* Load a mixin into the network object
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
exports._loadMixin = function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
this[mixinFunction] = sourceVariable[mixinFunction];
}
}
};
/**
* removes a mixin from the network object.
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
exports._clearMixin = function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
this[mixinFunction] = undefined;
}
}
};
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
exports._loadPhysicsSystem = function () {
this._loadMixin(PhysicsMixin);
this._loadSelectedForceSolver();
if (this.constants.configurePhysics == true) {
this._loadPhysicsConfiguration();
}
};
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
exports._loadClusterSystem = function () {
this.clusterSession = 0;
this.hubThreshold = 5;
this._loadMixin(ClusterMixin);
};
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
exports._loadSectorSystem = function () {
this.sectors = {};
this.activeSector = ["default"];
this.sectors["active"] = {};
this.sectors["active"]["default"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = {};
this.sectors["support"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
this._loadMixin(SectorsMixin);
};
/**
* Mixin the selection system and initialize the parameters required
*
* @private
*/
exports._loadSelectionSystem = function () {
this.selectionObj = {nodes: {}, edges: {}};
this._loadMixin(SelectionMixin);
};
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
exports._loadManipulationSystem = function () {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false;
if (this.constants.dataManipulation.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'network-manipulationDiv';
this.manipulationDiv.id = 'network-manipulationDiv';
if (this.editMode == true) {
this.manipulationDiv.style.display = "block";
}
else {
this.manipulationDiv.style.display = "none";
}
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
if (this.editModeDiv === undefined) {
this.editModeDiv = document.createElement('div');
this.editModeDiv.className = 'network-manipulation-editMode';
this.editModeDiv.id = 'network-manipulation-editMode';
if (this.editMode == true) {
this.editModeDiv.style.display = "none";
}
else {
this.editModeDiv.style.display = "block";
}
this.containerElement.insertBefore(this.editModeDiv, this.frame);
}
if (this.closeDiv === undefined) {
this.closeDiv = document.createElement('div');
this.closeDiv.className = 'network-manipulation-closeDiv';
this.closeDiv.id = 'network-manipulation-closeDiv';
this.closeDiv.style.display = this.manipulationDiv.style.display;
this.containerElement.insertBefore(this.closeDiv, this.frame);
}
// load the manipulation functions
this._loadMixin(ManipulationMixin);
// create the manipulator toolbar
this._createManipulatorBar();
}
else {
if (this.manipulationDiv !== undefined) {
// removes all the bindings and overloads
this._createManipulatorBar();
// remove the manipulation divs
this.containerElement.removeChild(this.manipulationDiv);
this.containerElement.removeChild(this.editModeDiv);
this.containerElement.removeChild(this.closeDiv);
this.manipulationDiv = undefined;
this.editModeDiv = undefined;
this.closeDiv = undefined;
// remove the mixin functions
this._clearMixin(ManipulationMixin);
}
}
};
/**
* Mixin the navigation (User Interface) system and initialize the parameters required
*
* @private
*/
exports._loadNavigationControls = function () {
this._loadMixin(NavigationMixin);
// the clean function removes the button divs, this is done to remove the bindings.
this._cleanNavigation();
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
};
/**
* Mixin the hierarchical layout system.
*
* @private
*/
exports._loadHierarchySystem = function () {
this._loadMixin(HierarchicalLayoutMixin);
};

+ 196
- 0
lib/network/mixins/NavigationMixin.js View File

@ -0,0 +1,196 @@
exports._cleanNavigation = function() {
// clean up previous navigation items
var wrapper = document.getElementById('network-navigation_wrapper');
if (wrapper != null) {
this.containerElement.removeChild(wrapper);
}
document.onmouseup = null;
};
/**
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
*
* @private
*/
exports._loadNavigationElements = function() {
this._cleanNavigation();
this.navigationDivs = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
this.navigationDivs['wrapper'] = document.createElement('div');
this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
this.navigationDivs['wrapper'].style.position = "absolute";
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDivs[navigationDivs[i]] = document.createElement('div');
this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
document.onmouseup = this._stopMovement.bind(this);
};
/**
* this stops all movement induced by the navigation buttons
*
* @private
*/
exports._stopMovement = function() {
this._xStopMoving();
this._yStopMoving();
this._stopZoom();
};
/**
* stops the actions performed by page up and down etc.
*
* @param event
* @private
*/
exports._preventDefault = function(event) {
if (event !== undefined) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
/**
* move the screen up
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
* To avoid this behaviour, we do the translation in the start loop.
*
* @private
*/
exports._moveUp = function(event) {
this.yIncrement = this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['up'].className += " active";
}
};
/**
* move the screen down
* @private
*/
exports._moveDown = function(event) {
this.yIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['down'].className += " active";
}
};
/**
* move the screen left
* @private
*/
exports._moveLeft = function(event) {
this.xIncrement = this.constants.keyboard.speed.x;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['left'].className += " active";
}
};
/**
* move the screen right
* @private
*/
exports._moveRight = function(event) {
this.xIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['right'].className += " active";
}
};
/**
* Zoom in, using the same method as the movement.
* @private
*/
exports._zoomIn = function(event) {
this.zoomIncrement = this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className += " active";
}
};
/**
* Zoom out
* @private
*/
exports._zoomOut = function() {
this.zoomIncrement = -this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomOut'].className += " active";
}
};
/**
* Stop zooming and unhighlight the zoom controls
* @private
*/
exports._stopZoom = function() {
this.zoomIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
}
};
/**
* Stop moving in the Y direction and unHighlight the up and down
* @private
*/
exports._yStopMoving = function() {
this.yIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
}
};
/**
* Stop moving in the X direction and unHighlight left and right.
* @private
*/
exports._xStopMoving = function() {
this.xIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
}
};

+ 548
- 0
lib/network/mixins/SectorsMixin.js View File

@ -0,0 +1,548 @@
var util = require('../../util');
/**
* Creation of the SectorMixin var.
*
* This contains all the functions the Network object can use to employ the sector system.
* The sector system is always used by Network, though the benefits only apply to the use of clustering.
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
*/
/**
* This function is only called by the setData function of the Network object.
* This loads the global references into the active sector. This initializes the sector.
*
* @private
*/
exports._putDataInSector = function() {
this.sectors["active"][this._sector()].nodes = this.nodes;
this.sectors["active"][this._sector()].edges = this.edges;
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
};
/**
* /**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied (active) sector. If a type is defined, do the specific type
*
* @param {String} sectorId
* @param {String} [sectorType] | "active" or "frozen"
* @private
*/
exports._switchToSector = function(sectorId, sectorType) {
if (sectorType === undefined || sectorType == "active") {
this._switchToActiveSector(sectorId);
}
else {
this._switchToFrozenSector(sectorId);
}
};
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @param sectorId
* @private
*/
exports._switchToActiveSector = function(sectorId) {
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
this.nodes = this.sectors["active"][sectorId]["nodes"];
this.edges = this.sectors["active"][sectorId]["edges"];
};
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @private
*/
exports._switchToSupportSector = function() {
this.nodeIndices = this.sectors["support"]["nodeIndices"];
this.nodes = this.sectors["support"]["nodes"];
this.edges = this.sectors["support"]["edges"];
};
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
*
* @param sectorId
* @private
*/
exports._switchToFrozenSector = function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
};
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the currently active sector.
*
* @private
*/
exports._loadLatestSector = function() {
this._switchToSector(this._sector());
};
/**
* This function returns the currently active sector Id
*
* @returns {String}
* @private
*/
exports._sector = function() {
return this.activeSector[this.activeSector.length-1];
};
/**
* This function returns the previously active sector Id
*
* @returns {String}
* @private
*/
exports._previousSector = function() {
if (this.activeSector.length > 1) {
return this.activeSector[this.activeSector.length-2];
}
else {
throw new TypeError('there are not enough sectors in the this.activeSector array.');
}
};
/**
* We add the active sector at the end of the this.activeSector array
* This ensures it is the currently active sector returned by _sector() and it reaches the top
* of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
*
* @param newId
* @private
*/
exports._setActiveSector = function(newId) {
this.activeSector.push(newId);
};
/**
* We remove the currently active sector id from the active sector stack. This happens when
* we reactivate the previously active sector
*
* @private
*/
exports._forgetLastSector = function() {
this.activeSector.pop();
};
/**
* This function creates a new active sector with the supplied newId. This newId
* is the expanding node id.
*
* @param {String} newId | Id of the new active sector
* @private
*/
exports._createNewSector = function(newId) {
// create the new sector
this.sectors["active"][newId] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": this.scale,
"drawingNode": undefined};
// create the new sector render node. This gives visual feedback that you are in a new sector.
this.sectors["active"][newId]['drawingNode'] = new Node(
{id:newId,
color: {
background: "#eaefef",
border: "495c5e"
}
},{},{},this.constants);
this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
};
/**
* This function removes the currently active sector. This is called when we create a new
* active sector.
*
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
exports._deleteActiveSector = function(sectorId) {
delete this.sectors["active"][sectorId];
};
/**
* This function removes the currently active sector. This is called when we reactivate
* the previously active sector.
*
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
exports._deleteFrozenSector = function(sectorId) {
delete this.sectors["frozen"][sectorId];
};
/**
* Freezing an active sector means moving it from the "active" object to the "frozen" object.
* We copy the references, then delete the active entree.
*
* @param sectorId
* @private
*/
exports._freezeSector = function(sectorId) {
// we move the set references from the active to the frozen stack.
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
// we have moved the sector data into the frozen set, we now remove it from the active set
this._deleteActiveSector(sectorId);
};
/**
* This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
* object to the "active" object.
*
* @param sectorId
* @private
*/
exports._activateSector = function(sectorId) {
// we move the set references from the frozen to the active stack.
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
// we have moved the sector data into the active set, we now remove it from the frozen stack
this._deleteFrozenSector(sectorId);
};
/**
* This function merges the data from the currently active sector with a frozen sector. This is used
* in the process of reverting back to the previously active sector.
* The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
* upon the creation of a new active sector.
*
* @param sectorId
* @private
*/
exports._mergeThisWithFrozen = function(sectorId) {
// copy all nodes
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
}
}
// copy all edges (if not fully clustered, else there are no edges)
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
}
}
// merge the nodeIndices
for (var i = 0; i < this.nodeIndices.length; i++) {
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
}
};
/**
* This clusters the sector to one cluster. It was a single cluster before this process started so
* we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
*
* @private
*/
exports._collapseThisToSingleCluster = function() {
this.clusterToFit(1,false);
};
/**
* We create a new active sector from the node that we want to open.
*
* @param node
* @private
*/
exports._addSector = function(node) {
// this is the currently active sector
var sector = this._sector();
// // this should allow me to select nodes from a frozen set.
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
// console.log("the node is part of the active sector");
// }
// else {
// console.log("I dont know what the fuck happened!!");
// }
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
delete this.nodes[node.id];
var unqiueIdentifier = util.randomUUID();
// we fully freeze the currently active sector
this._freezeSector(sector);
// we create a new active sector. This sector has the Id of the node to ensure uniqueness
this._createNewSector(unqiueIdentifier);
// we add the active sector to the sectors array to be able to revert these steps later on
this._setActiveSector(unqiueIdentifier);
// we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
this._switchToSector(this._sector());
// finally we add the node we removed from our previous active sector to the new active sector
this.nodes[node.id] = node;
};
/**
* We close the sector that is currently open and revert back to the one before.
* If the active sector is the "default" sector, nothing happens.
*
* @private
*/
exports._collapseSector = function() {
// the currently active sector
var sector = this._sector();
// we cannot collapse the default sector
if (sector != "default") {
if ((this.nodeIndices.length == 1) ||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
var previousSector = this._previousSector();
// we collapse the sector back to a single cluster
this._collapseThisToSingleCluster();
// we move the remaining nodes, edges and nodeIndices to the previous sector.
// This previous sector is the one we will reactivate
this._mergeThisWithFrozen(previousSector);
// the previously active (frozen) sector now has all the data from the currently active sector.
// we can now delete the active sector.
this._deleteActiveSector(sector);
// we activate the previously active (and currently frozen) sector.
this._activateSector(previousSector);
// we load the references from the newly active sector into the global references
this._switchToSector(previousSector);
// we forget the previously active sector because we reverted to the one before
this._forgetLastSector();
// finally, we update the node index list.
this._updateNodeIndexList();
// we refresh the list with calulation nodes and calculation node indices.
this._updateCalculationNodes();
}
}
};
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
exports._doInAllActiveSectors = function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
};
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
exports._doInSupportSector = function(runFunction,argument) {
if (argument === undefined) {
this._switchToSupportSector();
this[runFunction]();
}
else {
this._switchToSupportSector();
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
};
/**
* This runs a function in all frozen sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
exports._doInAllFrozenSectors = function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
this._loadLatestSector();
};
/**
* This runs a function in all sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
exports._doInAllSectors = function(runFunction,argument) {
var args = Array.prototype.splice.call(arguments, 1);
if (argument === undefined) {
this._doInAllActiveSectors(runFunction);
this._doInAllFrozenSectors(runFunction);
}
else {
if (args.length > 1) {
this._doInAllActiveSectors(runFunction,args[0],args[1]);
this._doInAllFrozenSectors(runFunction,args[0],args[1]);
}
else {
this._doInAllActiveSectors(runFunction,argument);
this._doInAllFrozenSectors(runFunction,argument);
}
}
};
/**
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
*
* @private
*/
exports._clearNodeIndexList = function() {
var sector = this._sector();
this.sectors["active"][sector]["nodeIndices"] = [];
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
};
/**
* Draw the encompassing sector node
*
* @param ctx
* @param sectorType
* @private
*/
exports._drawSectorNodes = function(ctx,sectorType) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var sector in this.sectors[sectorType]) {
if (this.sectors[sectorType].hasOwnProperty(sector)) {
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
this._switchToSector(sector,sectorType);
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.resize(ctx);
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
}
}
node = this.sectors[sectorType][sector]["drawingNode"];
node.x = 0.5 * (maxX + minX);
node.y = 0.5 * (maxY + minY);
node.width = 2 * (node.x - minX);
node.height = 2 * (node.y - minY);
node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
node.setScale(this.scale);
node._drawCircle(ctx);
}
}
}
};
exports._drawAllSectorNodes = function(ctx) {
this._drawSectorNodes(ctx,"frozen");
this._drawSectorNodes(ctx,"active");
this._loadLatestSector();
};

+ 705
- 0
lib/network/mixins/SelectionMixin.js View File

@ -0,0 +1,705 @@
var Node = require('../Node');
/**
* This function can be called from the _doInAllSectors function
*
* @param object
* @param overlappingNodes
* @private
*/
exports._getNodesOverlappingWith = function(object, overlappingNodes) {
var nodes = this.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
}
};
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getAllNodesOverlappingWith = function (object) {
var overlappingNodes = [];
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes;
};
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
exports._pointerToPositionObject = function(pointer) {
var x = this._XconvertDOMtoCanvas(pointer.x);
var y = this._YconvertDOMtoCanvas(pointer.y);
return {
left: x,
top: y,
right: x,
bottom: y
};
};
/**
* Get the top node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
exports._getNodeAt = function (pointer) {
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
};
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
var edges = this.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
};
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
exports._getAllEdgesOverlappingWith = function (object) {
var overlappingEdges = [];
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
return overlappingEdges;
};
/**
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
* @param pointer
* @returns {null}
* @private
*/
exports._getEdgeAt = function(pointer) {
var positionObject = this._pointerToPositionObject(pointer);
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
}
};
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
exports._addToSelection = function(obj) {
if (obj instanceof Node) {
this.selectionObj.nodes[obj.id] = obj;
}
else {
this.selectionObj.edges[obj.id] = obj;
}
};
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
exports._addToHover = function(obj) {
if (obj instanceof Node) {
this.hoverObj.nodes[obj.id] = obj;
}
else {
this.hoverObj.edges[obj.id] = obj;
}
};
/**
* Remove a single option from selection.
*
* @param {Object} obj
* @private
*/
exports._removeFromSelection = function(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
}
else {
delete this.selectionObj.edges[obj.id];
}
};
/**
* Unselect all. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._unselectAll = function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
this.selectionObj.nodes[nodeId].unselect();
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();
}
}
this.selectionObj = {nodes:{},edges:{}};
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* Unselect all clusters. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._unselectClusters = function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
this.selectionObj.nodes[nodeId].unselect();
this._removeFromSelection(this.selectionObj.nodes[nodeId]);
}
}
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
exports._getSelectedNodeCount = function() {
var count = 0;
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
return count;
};
/**
* return the selected node
*
* @returns {number}
* @private
*/
exports._getSelectedNode = function() {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return this.selectionObj.nodes[nodeId];
}
}
return null;
};
/**
* return the selected edge
*
* @returns {number}
* @private
*/
exports._getSelectedEdge = function() {
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
return this.selectionObj.edges[edgeId];
}
}
return null;
};
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
exports._getSelectedEdgeCount = function() {
var count = 0;
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
};
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
exports._getSelectedObjectCount = function() {
var count = 0;
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
};
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
exports._selectionIsEmpty = function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return false;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
return false;
}
}
return true;
};
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
exports._clusterInSelection = function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
return true;
}
}
}
return false;
};
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._selectConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.select();
this._addToSelection(edge);
}
};
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._hoverConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.hover = true;
this._addToHover(edge);
}
};
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
exports._unselectConnectedEdges = function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.unselect();
this._removeFromSelection(edge);
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @param {Boolean} append
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
exports._selectObject = function(object, append, doNotTrigger, highlightEdges) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
if (highlightEdges === undefined) {
highlightEdges = true;
}
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
}
if (object.selected == false) {
object.select();
this._addToSelection(object);
if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
this._selectConnectedEdges(object);
}
}
else {
object.unselect();
this._removeFromSelection(object);
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
exports._blurObject = function(object) {
if (object.hover == true) {
object.hover = false;
this.emit("blurNode",{node:object.id});
}
};
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @private
*/
exports._hoverObject = function(object) {
if (object.hover == false) {
object.hover = true;
this._addToHover(object);
if (object instanceof Node) {
this.emit("hoverNode",{node:object.id});
}
}
if (object instanceof Node) {
this._hoverConnectedEdges(object);
}
};
/**
* handles the selection part of the touch, only for navigation controls elements;
* Touch is triggered before tap, also before hold. Hold triggers after a while.
* This is the most responsive solution
*
* @param {Object} pointer
* @private
*/
exports._handleTouch = function(pointer) {
};
/**
* handles the selection part of the tap;
*
* @param {Object} pointer
* @private
*/
exports._handleTap = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,false);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,false);
}
else {
this._unselectAll();
}
}
this.emit("click", this.getSelection());
this._redraw();
};
/**
* handles the selection part of the double tap and opens a cluster if needed
*
* @param {Object} pointer
* @private
*/
exports._handleDoubleTap = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null && node !== undefined) {
// we reset the areaCenter here so the opening of the node will occur
this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
"y" : this._YconvertDOMtoCanvas(pointer.y)};
this.openCluster(node);
}
this.emit("doubleClick", this.getSelection());
};
/**
* Handle the onHold selection part
*
* @param pointer
* @private
*/
exports._handleOnHold = function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,true);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,true);
}
}
this._redraw();
};
/**
* handle the onRelease event. These functions are here for the navigation controls module.
*
* @private
*/
exports._handleOnRelease = function(pointer) {
};
/**
*
* retrieve the currently selected objects
* @return {{nodes: Array.<String>, edges: Array.<String>}} selection
*/
exports.getSelection = function() {
var nodeIds = this.getSelectedNodes();
var edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
};
/**
*
* retrieve the currently selected nodes
* @return {String[]} selection An array with the ids of the
* selected nodes.
*/
exports.getSelectedNodes = function() {
var idArray = [];
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(nodeId);
}
}
return idArray
};
/**
*
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
exports.getSelectedEdges = function() {
var idArray = [];
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(edgeId);
}
}
return idArray;
};
/**
* select zero or more nodes
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
exports.setSelection = function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true);
}
console.log("setSelection is deprecated. Please use selectNodes instead.")
this.redraw();
};
/**
* select zero or more nodes with the option to highlight edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
* @param {boolean} [highlightEdges]
*/
exports.selectNodes = function(selection, highlightEdges) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true,highlightEdges);
}
this.redraw();
};
/**
* select zero or more edges
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
exports.selectEdges = function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var edge = this.edges[id];
if (!edge) {
throw new RangeError('Edge with id "' + id + '" not found');
}
this._selectObject(edge,true,true,highlightEdges);
}
this.redraw();
};
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
exports._updateSelection = function () {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}
}
};

+ 393
- 0
lib/network/mixins/physics/BarnesHutMixin.js View File

@ -0,0 +1,393 @@
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
exports._calculateNodeForces = function() {
if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
var node;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
var nodeCount = nodeIndices.length;
this._formBarnesHutTree(nodes,nodeIndices);
var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
// starting with root is irrelevant, it never passes the BarnesHut condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
this._getForceContribution(barnesHutTree.root.children.SE,node);
}
}
};
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param parentBranch
* @param node
* @private
*/
exports._getForceContribution = function(parentBranch,node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
var dx,dy,distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHut condition
// original condition : s/d < theta = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
}
}
}
};
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
exports._formBarnesHutTree = function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
var minX = Number.MAX_VALUE,
minY = Number.MAX_VALUE,
maxX =-Number.MAX_VALUE,
maxY =-Number.MAX_VALUE;
// get the range of the nodes
for (var i = 0; i < nodeCount; i++) {
var x = nodes[nodeIndices[i]].x;
var y = nodes[nodeIndices[i]].y;
if (x < minX) { minX = x; }
if (x > maxX) { maxX = x; }
if (y < minY) { minY = y; }
if (y > maxY) { maxY = y; }
}
// make the range a square
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
var minimumTreeSize = 1e-5;
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
var halfRootSize = 0.5 * rootSize;
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
var barnesHutTree = {
root:{
centerOfMass: {x:0, y:0},
mass:0,
range: {
minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
minY: centerY-halfRootSize,maxY:centerY+halfRootSize
},
size: rootSize,
calcSize: 1 / rootSize,
children: { data:null},
maxWidth: 0,
level: 0,
childrenCount: 4
}
};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
this._placeInTree(barnesHutTree.root,node);
}
// make global
this.barnesHutTree = barnesHutTree
};
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param parentBranch
* @param node
* @private
*/
exports._updateBranchMass = function(parentBranch, node) {
var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
parentBranch.centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
};
/**
* determine in which branch the node will be placed.
*
* @param parentBranch
* @param node
* @param skipMassUpdate
* @private
*/
exports._placeInTree = function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW");
}
else { // in SW
this._placeInRegion(parentBranch,node,"SW");
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
this._placeInRegion(parentBranch,node,"SE");
}
}
};
/**
* actually place the node in a region (or branch)
*
* @param parentBranch
* @param node
* @param region
* @private
*/
exports._placeInRegion = function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region],node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += Math.random();
node.y += Math.random();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
break;
}
};
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param parentBranch
* @private
*/
exports._splitBranch = function(parentBranch) {
// if the branch is shaded with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch,"NW");
this._insertRegion(parentBranch,"NE");
this._insertRegion(parentBranch,"SW");
this._insertRegion(parentBranch,"SE");
if (containedNode != null) {
this._placeInTree(parentBranch,containedNode);
}
};
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param parentBranch
* @param region
* @param parentRange
* @private
*/
exports._insertRegion = function(parentBranch, region) {
var minX,maxX,minY,maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass:{x:0,y:0},
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data:null},
maxWidth: 0,
level: parentBranch.level+1,
childrenCount: 0
};
};
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
exports._drawTree = function(ctx,color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root,ctx,color);
}
};
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
exports._drawBranch = function(branch,ctx,color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount == 4) {
this._drawBranch(branch.children.NW,ctx);
this._drawBranch(branch.children.NE,ctx);
this._drawBranch(branch.children.SE,ctx);
this._drawBranch(branch.children.SW,ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
};

+ 125
- 0
lib/network/mixins/physics/HierarchialRepulsionMixin.js View File

@ -0,0 +1,125 @@
/**
* Calculate the forces the nodes apply on eachother based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
exports._calculateNodeForces = function () {
var dx, dy, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// approximation constants
var b = 5;
var a_base = 0.5 * -b;
// repulsing forces between nodes
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
var minimumDistance = nodeDistance;
var a = a_base / minimumDistance;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
if (node1.level == node2.level) {
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2 * minimumDistance) {
repulsingForce = a * distance + b;
var c = 0.05;
var d = 2 * minimumDistance * 2 * c;
repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c);
// normalize force with
if (distance == 0) {
distance = 0.01;
}
else {
repulsingForce = repulsingForce / distance;
}
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
}
};
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
exports._calculateHierarchicalSpringForces = function () {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, distance;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance));
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
edge.to.fx -= fx;
edge.to.fy -= fy;
edge.from.fx += fx;
edge.from.fy += fy;
var factor = 5;
if (distance > edgeLength) {
factor = 25;
}
if (edge.from.level > edge.to.level) {
edge.to.fx -= factor*fx;
edge.to.fy -= factor*fy;
}
else if (edge.from.level < edge.to.level) {
edge.from.fx += factor*fx;
edge.from.fy += factor*fy;
}
}
}
}
}
};

+ 700
- 0
lib/network/mixins/physics/PhysicsMixin.js View File

@ -0,0 +1,700 @@
var util = require('../../../util');
var RepulsionMixin = require('./RepulsionMixin');
var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin');
var BarnesHutMixin = require('./BarnesHutMixin');
/**
* Toggling barnes Hut calculation on and off.
*
* @private
*/
exports._toggleBarnesHut = function () {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver();
this.moving = true;
this.start();
};
/**
* This loads the node force solver based on the barnes hut or repulsion algorithm
*
* @private
*/
exports._loadSelectedForceSolver = function () {
// this overloads the this._calculateNodeForces
if (this.constants.physics.barnesHut.enabled == true) {
this._clearMixin(RepulsionMixin);
this._clearMixin(HierarchialRepulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
this.constants.physics.damping = this.constants.physics.barnesHut.damping;
this._loadMixin(BarnesHutMixin);
}
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
this._clearMixin(BarnesHutMixin);
this._clearMixin(RepulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
this._loadMixin(HierarchialRepulsionMixin);
}
else {
this._clearMixin(BarnesHutMixin);
this._clearMixin(HierarchialRepulsionMixin);
this.barnesHutTree = undefined;
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
this.constants.physics.damping = this.constants.physics.repulsion.damping;
this._loadMixin(RepulsionMixin);
}
};
/**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check
* if there is more than one node. If it is just one node, we dont calculate anything.
*
* @private
*/
exports._initializeForceCalculation = function () {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0, 0);
}
else {
// if there are too many nodes on screen, we cluster without repositioning
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToNodes, false);
}
// we now start the force calculation
this._calculateForces();
}
};
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
exports._calculateForces = function () {
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
this._calculateGravitationalForces();
this._calculateNodeForces();
if (this.constants.smoothCurves == true) {
this._calculateSpringForcesWithSupport();
}
else {
if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
this._calculateHierarchicalSpringForces();
}
else {
this._calculateSpringForces();
}
}
};
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
* This function joins the datanodes and invisible (called support) nodes into one object.
* We do this so we do not contaminate this.nodes with the support nodes.
*
* @private
*/
exports._updateCalculationNodes = function () {
if (this.constants.smoothCurves == true) {
this.calculationNodes = {};
this.calculationNodeIndices = [];
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId] = this.nodes[nodeId];
}
}
var supportNodes = this.sectors['support']['nodes'];
for (var supportNodeId in supportNodes) {
if (supportNodes.hasOwnProperty(supportNodeId)) {
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
supportNodes[supportNodeId]._setForce(0, 0);
}
}
}
for (var idx in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(idx)) {
this.calculationNodeIndices.push(idx);
}
}
}
else {
this.calculationNodes = this.nodes;
this.calculationNodeIndices = this.nodeIndices;
}
};
/**
* this function applies the central gravity effect to keep groups from floating off
*
* @private
*/
exports._calculateGravitationalForces = function () {
var dx, dy, distance, node, i;
var nodes = this.calculationNodes;
var gravity = this.constants.physics.centralGravity;
var gravityForce = 0;
for (i = 0; i < this.calculationNodeIndices.length; i++) {
node = nodes[this.calculationNodeIndices[i]];
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default" && gravity != 0) {
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
gravityForce = (distance == 0) ? 0 : (gravity / distance);
node.fx = dx * gravityForce;
node.fy = dy * gravityForce;
}
else {
node.fx = 0;
node.fy = 0;
}
}
};
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
exports._calculateSpringForces = function () {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, distance;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
edge.from.fx += fx;
edge.from.fy += fy;
edge.to.fx -= fx;
edge.to.fy -= fy;
}
}
}
}
};
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
exports._calculateSpringForcesWithSupport = function () {
var edgeLength, edge, edgeId, combinedClusterSize;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
// this implies that the edges between big clusters are longer
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
}
}
}
}
};
/**
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
*
* @param node1
* @param node2
* @param edgeLength
* @private
*/
exports._calculateSpringForce = function (node1, node2, edgeLength) {
var dx, dy, fx, fy, springForce, distance;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
distance = Math.sqrt(dx * dx + dy * dy);
if (distance == 0) {
distance = 0.01;
}
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
node1.fx += fx;
node1.fy += fy;
node2.fx -= fx;
node2.fy -= fy;
};
/**
* Load the HTML for the physics config and bind it
* @private
*/
exports._loadPhysicsConfiguration = function () {
if (this.physicsConfiguration === undefined) {
this.backupConstants = {};
util.deepExtend(this.backupConstants,this.constants);
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
this.physicsConfiguration = document.createElement('div');
this.physicsConfiguration.className = "PhysicsConfiguration";
this.physicsConfiguration.innerHTML = '' +
'<table><tr><td><b>Simulation Mode:</b></td></tr>' +
'<tr>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
'</tr>' +
'</table>' +
'<table id="graph_BH_table" style="display:none">' +
'<tr><td><b>Barnes Hut</b></td></tr>' +
'<tr>' +
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_R_table" style="display:none">' +
'<tr><td><b>Repulsion</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_H_table" style="display:none">' +
'<tr><td width="150"><b>Hierarchical</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table><tr><td><b>Options:</b></td></tr>' +
'<tr>' +
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
'</tr>' +
'</table>'
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
this.optionsDiv = document.createElement("div");
this.optionsDiv.style.fontSize = "14px";
this.optionsDiv.style.fontFamily = "verdana";
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
var rangeElement;
rangeElement = document.getElementById('graph_BH_gc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
rangeElement = document.getElementById('graph_BH_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_BH_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_BH_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_BH_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_R_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
rangeElement = document.getElementById('graph_R_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_R_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_R_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_R_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
rangeElement = document.getElementById('graph_H_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_H_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_H_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_H_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_direction');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
rangeElement = document.getElementById('graph_H_levsep');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
rangeElement = document.getElementById('graph_H_nspac');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
var radioButton3 = document.getElementById("graph_physicsMethod3");
radioButton2.checked = true;
if (this.constants.physics.barnesHut.enabled) {
radioButton1.checked = true;
}
if (this.constants.hierarchicalLayout.enabled) {
radioButton3.checked = true;
}
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
var graph_repositionNodes = document.getElementById("graph_repositionNodes");
var graph_generateOptions = document.getElementById("graph_generateOptions");
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
graph_generateOptions.onclick = graphGenerateOptions.bind(this);
if (this.constants.smoothCurves == true) {
graph_toggleSmooth.style.background = "#A4FF56";
}
else {
graph_toggleSmooth.style.background = "#FF8532";
}
switchConfigurations.apply(this);
radioButton1.onchange = switchConfigurations.bind(this);
radioButton2.onchange = switchConfigurations.bind(this);
radioButton3.onchange = switchConfigurations.bind(this);
}
};
/**
* This overwrites the this.constants.
*
* @param constantsVariableName
* @param value
* @private
*/
exports._overWriteGraphConstants = function (constantsVariableName, value) {
var nameArray = constantsVariableName.split("_");
if (nameArray.length == 1) {
this.constants[nameArray[0]] = value;
}
else if (nameArray.length == 2) {
this.constants[nameArray[0]][nameArray[1]] = value;
}
else if (nameArray.length == 3) {
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
}
};
/**
* this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
*/
function graphToggleSmoothCurves () {
this.constants.smoothCurves = !this.constants.smoothCurves;
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this._configureSmoothCurves(false);
}
/**
* this function is used to scramble the nodes
*
*/
function graphRepositionNodes () {
for (var nodeId in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
}
}
if (this.constants.hierarchicalLayout.enabled == true) {
this._setupHierarchicalLayout();
}
else {
this.repositionNodes();
}
this.moving = true;
this.start();
}
/**
* this is used to generate an options file from the playing with physics system.
*/
function graphGenerateOptions () {
var options = "No options are required, default values used.";
var optionsSpecific = [];
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
if (radioButton1.checked == true) {
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options = "var options = {";
options += "physics: {barnesHut: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
if (optionsSpecific.length == 0) {options = "var options = {";}
else {options += ", "}
options += "smoothCurves: " + this.constants.smoothCurves;
}
if (options != "No options are required, default values used.") {
options += '};'
}
}
else if (radioButton2.checked == true) {
options = "var options = {";
options += "physics: {barnesHut: {enabled: false}";
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += ", repulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (optionsSpecific.length == 0) {options += "}"}
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
options += ", smoothCurves: " + this.constants.smoothCurves;
}
options += '};'
}
else {
options = "var options = {";
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += "physics: {hierarchicalRepulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", ";
}
}
options += '}},';
}
options += 'hierarchicalLayout: {';
optionsSpecific = [];
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
if (optionsSpecific.length != 0) {
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}'
}
else {
options += "enabled:true}";
}
options += '};'
}
this.optionsDiv.innerHTML = options;
}
/**
* this is used to switch between barnesHut, repulsion and hierarchical.
*
*/
function switchConfigurations () {
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
var tableId = "graph_" + radioButton + "_table";
var table = document.getElementById(tableId);
table.style.display = "block";
for (var i = 0; i < ids.length; i++) {
if (ids[i] != tableId) {
table = document.getElementById(ids[i]);
table.style.display = "none";
}
}
this._restoreNodes();
if (radioButton == "R") {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = false;
}
else if (radioButton == "H") {
if (this.constants.hierarchicalLayout.enabled == false) {
this.constants.hierarchicalLayout.enabled = true;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this.constants.physics.barnesHut.enabled = false;
this._setupHierarchicalLayout();
}
}
else {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = true;
}
this._loadSelectedForceSolver();
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this.moving = true;
this.start();
}
/**
* this generates the ranges depending on the iniital values.
*
* @param id
* @param map
* @param constantsVariableName
*/
function showValueOfRange (id,map,constantsVariableName) {
var valueId = id + "_value";
var rangeValue = document.getElementById(id).value;
if (map instanceof Array) {
document.getElementById(valueId).value = map[parseInt(rangeValue)];
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
}
else {
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
}
if (constantsVariableName == "hierarchicalLayout_direction" ||
constantsVariableName == "hierarchicalLayout_levelSeparation" ||
constantsVariableName == "hierarchicalLayout_nodeSpacing") {
this._setupHierarchicalLayout();
}
this.moving = true;
this.start();
}

+ 58
- 0
lib/network/mixins/physics/RepulsionMixin.js View File

@ -0,0 +1,58 @@
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
exports._calculateNodeForces = function () {
var dx, dy, angle, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// approximation constants
var a_base = -2 / 3;
var b = 4 / 3;
// repulsing forces between nodes
var nodeDistance = this.constants.physics.repulsion.nodeDistance;
var minimumDistance = nodeDistance;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2 * minimumDistance) {
if (distance < 0.5 * minimumDistance) {
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
// amplify the repulsion for clusters.
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
repulsingForce = repulsingForce / distance;
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
};

src/network/shapes.js → lib/network/shapes.js View File


src/timeline/DataStep.js → lib/timeline/DataStep.js View File

@ -172,7 +172,7 @@ DataStep.prototype.previous = function() {
/** /**
* Get the current datetime * Get the current datetime
* @return {Number} current The current date
* @return {String} current The current date
*/ */
DataStep.prototype.getCurrent = function() { DataStep.prototype.getCurrent = function() {
var toPrecision = '' + Number(this.current).toPrecision(5); var toPrecision = '' + Number(this.current).toPrecision(5);
@ -189,7 +189,6 @@ DataStep.prototype.getCurrent = function() {
} }
} }
return toPrecision; return toPrecision;
}; };
@ -213,3 +212,5 @@ DataStep.prototype.snap = function(date) {
DataStep.prototype.isMajor = function() { DataStep.prototype.isMajor = function() {
return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0); return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
}; };
module.exports = DataStep;

src/timeline/Graph2d.js → lib/timeline/Graph2d.js View File

@ -1,3 +1,14 @@
var Emitter = require('emitter-component');
var Hammer = require('hammerjs');
var util = require('../util');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Range = require('./Range');
var TimeAxis = require('./component/TimeAxis');
var CurrentTime = require('./component/CurrentTime');
var CustomTime = require('./component/CustomTime');
var LineGraph = require('./component/LineGraph');
/** /**
* Create a timeline visualization * Create a timeline visualization
* @param {HTMLElement} container * @param {HTMLElement} container
@ -868,3 +879,5 @@ Graph2d.prototype._updateScrollTop = function () {
Graph2d.prototype._getScrollTop = function () { Graph2d.prototype._getScrollTop = function () {
return this.props.scrollTop; return this.props.scrollTop;
}; };
module.exports = Graph2d;

src/timeline/Range.js → lib/timeline/Range.js View File

@ -1,3 +1,7 @@
var util = require('../util');
var moment = require('../module/moment');
var Component = require('./component/Component');
/** /**
* @constructor Range * @constructor Range
* A Range controls a numeric range with a start and end value. * A Range controls a numeric range with a start and end value.
@ -462,8 +466,8 @@ Range.prototype._pointerToDate = function (pointer) {
*/ */
function getPointer (touch, element) { function getPointer (touch, element) {
return { return {
x: touch.pageX - vis.util.getAbsoluteLeft(element),
y: touch.pageY - vis.util.getAbsoluteTop(element)
x: touch.pageX - util.getAbsoluteLeft(element),
y: touch.pageY - util.getAbsoluteTop(element)
}; };
} }
@ -525,3 +529,5 @@ Range.prototype.moveTo = function(moveTo) {
this.setRange(newStart, newEnd); this.setRange(newStart, newEnd);
}; };
module.exports = Range;

src/timeline/Stack.js → lib/timeline/Stack.js View File

@ -1,13 +1,10 @@
/**
* Utility functions for ordering and stacking of items
*/
var stack = {};
// Utility functions for ordering and stacking of items
/** /**
* Order items by their start data * Order items by their start data
* @param {Item[]} items * @param {Item[]} items
*/ */
stack.orderByStart = function(items) {
exports.orderByStart = function(items) {
items.sort(function (a, b) { items.sort(function (a, b) {
return a.data.start - b.data.start; return a.data.start - b.data.start;
}); });
@ -18,7 +15,7 @@ stack.orderByStart = function(items) {
* is used. * is used.
* @param {Item[]} items * @param {Item[]} items
*/ */
stack.orderByEnd = function(items) {
exports.orderByEnd = function(items) {
items.sort(function (a, b) { items.sort(function (a, b) {
var aTime = ('end' in a.data) ? a.data.end : a.data.start, var aTime = ('end' in a.data) ? a.data.end : a.data.start,
bTime = ('end' in b.data) ? b.data.end : b.data.start; bTime = ('end' in b.data) ? b.data.end : b.data.start;
@ -38,7 +35,7 @@ stack.orderByEnd = function(items) {
* If true, all items will be repositioned. If false (default), only * If true, all items will be repositioned. If false (default), only
* items having a top===null will be re-stacked * items having a top===null will be re-stacked
*/ */
stack.stack = function(items, margin, force) {
exports.stack = function(items, margin, force) {
var i, iMax; var i, iMax;
if (force) { if (force) {
@ -61,7 +58,7 @@ stack.stack = function(items, margin, force) {
var collidingItem = null; var collidingItem = null;
for (var j = 0, jj = items.length; j < jj; j++) { for (var j = 0, jj = items.length; j < jj; j++) {
var other = items[j]; var other = items[j];
if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
if (other.top !== null && other !== item && exports.collision(item, other, margin.item)) {
collidingItem = other; collidingItem = other;
break; break;
} }
@ -83,7 +80,7 @@ stack.stack = function(items, margin, force) {
* @param {{item: number, axis: number}} margin * @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis. * Margins between items and between items and the axis.
*/ */
stack.nostack = function(items, margin) {
exports.nostack = function(items, margin) {
var i, iMax; var i, iMax;
// reset top position of all items // reset top position of all items
@ -104,7 +101,7 @@ stack.nostack = function(items, margin) {
* the requested margin. * the requested margin.
* @return {boolean} true if a and b collide, else false * @return {boolean} true if a and b collide, else false
*/ */
stack.collision = function(a, b, margin) {
exports.collision = function(a, b, margin) {
return ((a.left - margin) < (b.left + b.width) && return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left && (a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) && (a.top - margin) < (b.top + b.height) &&

src/timeline/TimeStep.js → lib/timeline/TimeStep.js View File

@ -1,3 +1,5 @@
var moment = require('../module/moment');
/** /**
* @constructor TimeStep * @constructor TimeStep
* The class TimeStep is an iterator for dates. You provide a start date and an * The class TimeStep is an iterator for dates. You provide a start date and an
@ -464,3 +466,5 @@ TimeStep.prototype.getLabelMajor = function(date) {
default: return ''; default: return '';
} }
}; };
module.exports = TimeStep;

src/timeline/Timeline.js → lib/timeline/Timeline.js View File

@ -1,3 +1,14 @@
var Emitter = require('emitter-component');
var Hammer = require('hammerjs');
var util = require('../util');
var DataSet = require('../DataSet');
var DataView = require('../DataView');
var Range = require('./Range');
var TimeAxis = require('./component/TimeAxis');
var CurrentTime = require('./component/CurrentTime');
var CustomTime = require('./component/CustomTime');
var ItemSet = require('./component/ItemSet');
/** /**
* Create a timeline visualization * Create a timeline visualization
* @param {HTMLElement} container * @param {HTMLElement} container
@ -885,3 +896,5 @@ Timeline.prototype._updateScrollTop = function () {
Timeline.prototype._getScrollTop = function () { Timeline.prototype._getScrollTop = function () {
return this.props.scrollTop; return this.props.scrollTop;
}; };
module.exports = Timeline;

src/timeline/component/Component.js → lib/timeline/component/Component.js View File

@ -50,3 +50,5 @@ Component.prototype._isResized = function() {
return resized; return resized;
}; };
module.exports = Component;

src/timeline/component/CurrentTime.js → lib/timeline/component/CurrentTime.js View File

@ -1,3 +1,6 @@
var util = require('../../util');
var Component = require('./Component');
/** /**
* A current time bar * A current time bar
* @param {{range: Range, dom: Object, domProps: Object}} body * @param {{range: Range, dom: Object, domProps: Object}} body
@ -6,7 +9,6 @@
* @constructor CurrentTime * @constructor CurrentTime
* @extends Component * @extends Component
*/ */
function CurrentTime (body, options) { function CurrentTime (body, options) {
this.body = body; this.body = body;
@ -126,3 +128,5 @@ CurrentTime.prototype.stop = function() {
delete this.currentTimeTimer; delete this.currentTimeTimer;
} }
}; };
module.exports = CurrentTime;

src/timeline/component/CustomTime.js → lib/timeline/component/CustomTime.js View File

@ -1,3 +1,7 @@
var Hammer = require('hammerjs');
var util = require('../../util');
var Component = require('./Component');
/** /**
* A custom time bar * A custom time bar
* @param {{range: Range, dom: Object}} body * @param {{range: Range, dom: Object}} body
@ -180,3 +184,5 @@ CustomTime.prototype._onDragEnd = function (event) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}; };
module.exports = CustomTime;

src/timeline/component/DataAxis.js → lib/timeline/component/DataAxis.js View File

@ -1,3 +1,8 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var Component = require('./Component');
var DataStep = require('../DataStep');
/** /**
* A horizontal time axis * A horizontal time axis
* @param {Object} [options] See DataAxis.setOptions for the available * @param {Object} [options] See DataAxis.setOptions for the available
@ -466,3 +471,5 @@ DataAxis.prototype._calculateCharSize = function () {
DataAxis.prototype.snap = function(date) { DataAxis.prototype.snap = function(date) {
return this.step.snap(date); return this.step.snap(date);
}; };
module.exports = DataAxis;

src/timeline/component/GraphGroup.js → lib/timeline/component/GraphGroup.js View File

@ -1,3 +1,6 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
/** /**
* @constructor Group * @constructor Group
* @param {Number | String} groupId * @param {Number | String} groupId
@ -28,11 +31,11 @@ GraphGroup.prototype.setItems = function(items) {
else { else {
this.itemsData = []; this.itemsData = [];
} }
}
};
GraphGroup.prototype.setZeroPosition = function(pos) { GraphGroup.prototype.setZeroPosition = function(pos) {
this.zeroPosition = pos; this.zeroPosition = pos;
}
};
GraphGroup.prototype.setOptions = function(options) { GraphGroup.prototype.setOptions = function(options) {
if (options !== undefined) { if (options !== undefined) {
@ -113,4 +116,6 @@ GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, icon
DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer); DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer); DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
} }
}
};
module.exports = GraphGroup;

src/timeline/component/Group.js → lib/timeline/component/Group.js View File

@ -1,3 +1,7 @@
var util = require('../../util');
var stack = require('../Stack');
var ItemRange = require('./item/ItemRange');
/** /**
* @constructor Group * @constructor Group
* @param {Number | String} groupId * @param {Number | String} groupId
@ -418,3 +422,5 @@ Group.prototype._checkIfVisible = function(item, visibleItems, range) {
if (item.displayed) item.hide(); if (item.displayed) item.hide();
} }
}; };
module.exports = Group;

src/timeline/component/ItemSet.js → lib/timeline/component/ItemSet.js View File

@ -1,3 +1,14 @@
var Hammer = require('hammerjs');
var util = require('../../util');
var DataSet = require('../../DataSet');
var DataView = require('../../DataView');
var Component = require('./Component');
var Group = require('./Group');
var ItemBox = require('./item/ItemBox');
var ItemPoint = require('./item/ItemPoint');
var ItemRange = require('./item/ItemRange');
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
/** /**
@ -1220,7 +1231,7 @@ ItemSet.prototype._onAddItem = function (event) {
} }
else { else {
// add item // add item
var xAbs = vis.util.getAbsoluteLeft(this.dom.frame);
var xAbs = util.getAbsoluteLeft(this.dom.frame);
var x = event.gesture.center.pageX - xAbs; var x = event.gesture.center.pageX - xAbs;
var start = this.body.util.toTime(x); var start = this.body.util.toTime(x);
var newItem = { var newItem = {
@ -1338,3 +1349,4 @@ ItemSet.itemSetFromTarget = function(event) {
return null; return null;
}; };
module.exports = ItemSet;

src/timeline/component/Legend.js → lib/timeline/component/Legend.js View File

@ -1,5 +1,9 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var Component = require('./Component');
/** /**
* Created by Alex on 6/17/14.
* Legend for Graph2d
*/ */
function Legend(body, options, side) { function Legend(body, options, side) {
this.body = body; this.body = body;
@ -27,7 +31,7 @@ function Legend(body, options, side) {
this._create(); this._create();
this.setOptions(options); this.setOptions(options);
};
}
Legend.prototype = new Component(); Legend.prototype = new Component();
@ -69,7 +73,7 @@ Legend.prototype._create = function() {
this.dom.frame.appendChild(this.svg); this.dom.frame.appendChild(this.svg);
this.dom.frame.appendChild(this.dom.textArea); this.dom.frame.appendChild(this.dom.textArea);
}
};
/** /**
* Hide the component from the DOM * Hide the component from the DOM
@ -95,7 +99,7 @@ Legend.prototype.show = function() {
Legend.prototype.setOptions = function(options) { Legend.prototype.setOptions = function(options) {
var fields = ['enabled','orientation','icons','left','right']; var fields = ['enabled','orientation','icons','left','right'];
util.selectiveDeepExtend(fields, this.options, options); util.selectiveDeepExtend(fields, this.options, options);
}
};
Legend.prototype.redraw = function() { Legend.prototype.redraw = function() {
if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) { if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) {
@ -142,7 +146,7 @@ Legend.prototype.redraw = function() {
this.drawLegendIcons(); this.drawLegendIcons();
} }
var content = "";
var content = '';
for (var groupId in this.groups) { for (var groupId in this.groups) {
if (this.groups.hasOwnProperty(groupId)) { if (this.groups.hasOwnProperty(groupId)) {
content += this.groups[groupId].content + '<br />'; content += this.groups[groupId].content + '<br />';
@ -151,13 +155,13 @@ Legend.prototype.redraw = function() {
this.dom.textArea.innerHTML = content; this.dom.textArea.innerHTML = content;
this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px'; this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
} }
}
};
Legend.prototype.drawLegendIcons = function() { Legend.prototype.drawLegendIcons = function() {
if (this.dom.frame.parentNode) { if (this.dom.frame.parentNode) {
DOMutil.prepareElements(this.svgElements); DOMutil.prepareElements(this.svgElements);
var padding = window.getComputedStyle(this.dom.frame).paddingTop; var padding = window.getComputedStyle(this.dom.frame).paddingTop;
var iconOffset = Number(padding.replace("px",''));
var iconOffset = Number(padding.replace('px',''));
var x = iconOffset; var x = iconOffset;
var iconWidth = this.options.iconSize; var iconWidth = this.options.iconSize;
var iconHeight = 0.75 * this.options.iconSize; var iconHeight = 0.75 * this.options.iconSize;
@ -174,4 +178,6 @@ Legend.prototype.drawLegendIcons = function() {
DOMutil.cleanupElements(this.svgElements); DOMutil.cleanupElements(this.svgElements);
} }
}
};
module.exports = Legend;

src/timeline/component/LineGraph.js → lib/timeline/component/LineGraph.js View File

@ -1,3 +1,12 @@
var util = require('../../util');
var DOMutil = require('../../DOMutil');
var DataSet = require('../../DataSet');
var DataView = require('../../DataView');
var Component = require('./Component');
var DataAxis = require('./DataAxis');
var GraphGroup = require('./GraphGroup');
var Legend = require('./Legend');
var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
/** /**
@ -1059,7 +1068,4 @@ LineGraph.prototype._linear = function(data) {
return d; return d;
}; };
module.exports = LineGraph;

src/timeline/component/TimeAxis.js → lib/timeline/component/TimeAxis.js View File

@ -1,3 +1,7 @@
var util = require('../../util');
var Component = require('./Component');
var TimeStep = require('../TimeStep');
/** /**
* A horizontal time axis * A horizontal time axis
* @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
@ -387,3 +391,5 @@ TimeAxis.prototype._calculateCharSize = function () {
TimeAxis.prototype.snap = function(date) { TimeAxis.prototype.snap = function(date) {
return this.step.snap(date); return this.step.snap(date);
}; };
module.exports = TimeAxis;

src/timeline/component/css/animation.css → lib/timeline/component/css/animation.css View File


src/timeline/component/css/currenttime.css → lib/timeline/component/css/currenttime.css View File


src/timeline/component/css/customtime.css → lib/timeline/component/css/customtime.css View File


src/timeline/component/css/dataaxis.css → lib/timeline/component/css/dataaxis.css View File


src/timeline/component/css/item.css → lib/timeline/component/css/item.css View File


src/timeline/component/css/itemset.css → lib/timeline/component/css/itemset.css View File


src/timeline/component/css/labelset.css → lib/timeline/component/css/labelset.css View File


src/timeline/component/css/panel.css → lib/timeline/component/css/panel.css View File


src/timeline/component/css/pathStyles.css → lib/timeline/component/css/pathStyles.css View File


src/timeline/component/css/timeaxis.css → lib/timeline/component/css/timeaxis.css View File


src/timeline/component/css/timeline.css → lib/timeline/component/css/timeline.css View File


src/timeline/component/item/Item.js → lib/timeline/component/item/Item.js View File

@ -1,3 +1,5 @@
var Hammer = require('hammerjs');
/** /**
* @constructor Item * @constructor Item
* @param {Object} data Object containing (optional) parameters type, * @param {Object} data Object containing (optional) parameters type,
@ -137,3 +139,5 @@ Item.prototype._repaintDeleteButton = function (anchor) {
this.dom.deleteButton = null; this.dom.deleteButton = null;
} }
}; };
module.exports = Item;

src/timeline/component/item/ItemBox.js → lib/timeline/component/item/ItemBox.js View File

@ -1,3 +1,5 @@
var Item = require('./Item');
/** /**
* @constructor ItemBox * @constructor ItemBox
* @extends Item * @extends Item
@ -234,3 +236,5 @@ ItemBox.prototype.repositionY = function() {
dot.style.top = (-this.props.dot.height / 2) + 'px'; dot.style.top = (-this.props.dot.height / 2) + 'px';
}; };
module.exports = ItemBox;

src/timeline/component/item/ItemPoint.js → lib/timeline/component/item/ItemPoint.js View File

@ -1,3 +1,5 @@
var Item = require('./Item');
/** /**
* @constructor ItemPoint * @constructor ItemPoint
* @extends Item * @extends Item
@ -194,3 +196,5 @@ ItemPoint.prototype.repositionY = function() {
point.style.top = (this.parent.height - this.top - this.height) + 'px'; point.style.top = (this.parent.height - this.top - this.height) + 'px';
} }
}; };
module.exports = ItemPoint;

src/timeline/component/item/ItemRange.js → lib/timeline/component/item/ItemRange.js View File

@ -1,3 +1,6 @@
var Hammer = require('hammerjs');
var Item = require('./Item');
/** /**
* @constructor ItemRange * @constructor ItemRange
* @extends Item * @extends Item
@ -284,3 +287,5 @@ ItemRange.prototype._repaintDragRight = function () {
this.dom.dragRight = null; this.dom.dragRight = null;
} }
}; };
module.exports = ItemRange;

src/timeline/img/delete.png → lib/timeline/img/delete.png View File


src/util.js → lib/util.js View File

@ -1,14 +1,16 @@
/**
* utility functions
*/
var util = {};
// utility functions
// first check if moment.js is already loaded in the browser window, if so,
// use this instance. Else, load via commonjs.
var Hammer = require('./module/hammer');
var moment = require('./module/moment');
/** /**
* Test whether given object is a number * Test whether given object is a number
* @param {*} object * @param {*} object
* @return {Boolean} isNumber * @return {Boolean} isNumber
*/ */
util.isNumber = function(object) {
exports.isNumber = function(object) {
return (object instanceof Number || typeof object == 'number'); return (object instanceof Number || typeof object == 'number');
}; };
@ -17,7 +19,7 @@ util.isNumber = function(object) {
* @param {*} object * @param {*} object
* @return {Boolean} isString * @return {Boolean} isString
*/ */
util.isString = function(object) {
exports.isString = function(object) {
return (object instanceof String || typeof object == 'string'); return (object instanceof String || typeof object == 'string');
}; };
@ -26,11 +28,11 @@ util.isString = function(object) {
* @param {Date | String} object * @param {Date | String} object
* @return {Boolean} isDate * @return {Boolean} isDate
*/ */
util.isDate = function(object) {
exports.isDate = function(object) {
if (object instanceof Date) { if (object instanceof Date) {
return true; return true;
} }
else if (util.isString(object)) {
else if (exports.isString(object)) {
// test whether this string contains a date // test whether this string contains a date
var match = ASPDateRegex.exec(object); var match = ASPDateRegex.exec(object);
if (match) { if (match) {
@ -49,7 +51,7 @@ util.isDate = function(object) {
* @param {*} object * @param {*} object
* @return {Boolean} isDataTable * @return {Boolean} isDataTable
*/ */
util.isDataTable = function(object) {
exports.isDataTable = function(object) {
return (typeof (google) !== 'undefined') && return (typeof (google) !== 'undefined') &&
(google.visualization) && (google.visualization) &&
(google.visualization.DataTable) && (google.visualization.DataTable) &&
@ -61,7 +63,7 @@ util.isDataTable = function(object) {
* source: http://stackoverflow.com/a/105074/1262753 * source: http://stackoverflow.com/a/105074/1262753
* @return {String} uuid * @return {String} uuid
*/ */
util.randomUUID = function() {
exports.randomUUID = function() {
var S4 = function () { var S4 = function () {
return Math.floor( return Math.floor(
Math.random() * 0x10000 /* 65536 */ Math.random() * 0x10000 /* 65536 */
@ -84,7 +86,7 @@ util.randomUUID = function() {
* @param {... Object} b * @param {... Object} b
* @return {Object} a * @return {Object} a
*/ */
util.extend = function (a, b) {
exports.extend = function (a, b) {
for (var i = 1, len = arguments.length; i < len; i++) { for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i]; var other = arguments[i];
for (var prop in other) { for (var prop in other) {
@ -105,7 +107,7 @@ util.extend = function (a, b) {
* @param {... Object} b * @param {... Object} b
* @return {Object} a * @return {Object} a
*/ */
util.selectiveExtend = function (props, a, b) {
exports.selectiveExtend = function (props, a, b) {
if (!Array.isArray(props)) { if (!Array.isArray(props)) {
throw new Error('Array with property names expected as first argument'); throw new Error('Array with property names expected as first argument');
} }
@ -131,7 +133,7 @@ util.selectiveExtend = function (props, a, b) {
* @param {... Object} b * @param {... Object} b
* @return {Object} a * @return {Object} a
*/ */
util.selectiveDeepExtend = function (props, a, b) {
exports.selectiveDeepExtend = function (props, a, b) {
// TODO: add support for Arrays to deepExtend // TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) { if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend'); throw new TypeError('Arrays are not supported by deepExtend');
@ -146,7 +148,7 @@ util.selectiveDeepExtend = function (props, a, b) {
a[prop] = {}; a[prop] = {};
} }
if (a[prop].constructor === Object) { if (a[prop].constructor === Object) {
util.deepExtend(a[prop], b[prop]);
exports.deepExtend(a[prop], b[prop]);
} }
else { else {
a[prop] = b[prop]; a[prop] = b[prop];
@ -169,7 +171,7 @@ util.selectiveDeepExtend = function (props, a, b) {
* @param {Object} b * @param {Object} b
* @returns {Object} * @returns {Object}
*/ */
util.deepExtend = function(a, b) {
exports.deepExtend = function(a, b) {
// TODO: add support for Arrays to deepExtend // TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) { if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend'); throw new TypeError('Arrays are not supported by deepExtend');
@ -182,7 +184,7 @@ util.deepExtend = function(a, b) {
a[prop] = {}; a[prop] = {};
} }
if (a[prop].constructor === Object) { if (a[prop].constructor === Object) {
util.deepExtend(a[prop], b[prop]);
exports.deepExtend(a[prop], b[prop]);
} }
else { else {
a[prop] = b[prop]; a[prop] = b[prop];
@ -204,7 +206,7 @@ util.deepExtend = function(a, b) {
* @return {boolean} Returns true if both arrays have the same length and same * @return {boolean} Returns true if both arrays have the same length and same
* elements. * elements.
*/ */
util.equalArray = function (a, b) {
exports.equalArray = function (a, b) {
if (a.length != b.length) return false; if (a.length != b.length) return false;
for (var i = 0, len = a.length; i < len; i++) { for (var i = 0, len = a.length; i < len; i++) {
@ -223,7 +225,7 @@ util.equalArray = function (a, b) {
* @return {*} object * @return {*} object
* @throws Error * @throws Error
*/ */
util.convert = function(object, type) {
exports.convert = function(object, type) {
var match; var match;
if (object === undefined) { if (object === undefined) {
@ -255,7 +257,7 @@ util.convert = function(object, type) {
return String(object); return String(object);
case 'Date': case 'Date':
if (util.isNumber(object)) {
if (exports.isNumber(object)) {
return new Date(object); return new Date(object);
} }
if (object instanceof Date) { if (object instanceof Date) {
@ -264,7 +266,7 @@ util.convert = function(object, type) {
else if (moment.isMoment(object)) { else if (moment.isMoment(object)) {
return new Date(object.valueOf()); return new Date(object.valueOf());
} }
if (util.isString(object)) {
if (exports.isString(object)) {
match = ASPDateRegex.exec(object); match = ASPDateRegex.exec(object);
if (match) { if (match) {
// object is an ASP date // object is an ASP date
@ -276,12 +278,12 @@ util.convert = function(object, type) {
} }
else { else {
throw new Error( throw new Error(
'Cannot convert object of type ' + util.getType(object) +
'Cannot convert object of type ' + exports.getType(object) +
' to type Date'); ' to type Date');
} }
case 'Moment': case 'Moment':
if (util.isNumber(object)) {
if (exports.isNumber(object)) {
return moment(object); return moment(object);
} }
if (object instanceof Date) { if (object instanceof Date) {
@ -290,7 +292,7 @@ util.convert = function(object, type) {
else if (moment.isMoment(object)) { else if (moment.isMoment(object)) {
return moment(object); return moment(object);
} }
if (util.isString(object)) {
if (exports.isString(object)) {
match = ASPDateRegex.exec(object); match = ASPDateRegex.exec(object);
if (match) { if (match) {
// object is an ASP date // object is an ASP date
@ -302,12 +304,12 @@ util.convert = function(object, type) {
} }
else { else {
throw new Error( throw new Error(
'Cannot convert object of type ' + util.getType(object) +
'Cannot convert object of type ' + exports.getType(object) +
' to type Date'); ' to type Date');
} }
case 'ISODate': case 'ISODate':
if (util.isNumber(object)) {
if (exports.isNumber(object)) {
return new Date(object); return new Date(object);
} }
else if (object instanceof Date) { else if (object instanceof Date) {
@ -316,7 +318,7 @@ util.convert = function(object, type) {
else if (moment.isMoment(object)) { else if (moment.isMoment(object)) {
return object.toDate().toISOString(); return object.toDate().toISOString();
} }
else if (util.isString(object)) {
else if (exports.isString(object)) {
match = ASPDateRegex.exec(object); match = ASPDateRegex.exec(object);
if (match) { if (match) {
// object is an ASP date // object is an ASP date
@ -328,18 +330,18 @@ util.convert = function(object, type) {
} }
else { else {
throw new Error( throw new Error(
'Cannot convert object of type ' + util.getType(object) +
'Cannot convert object of type ' + exports.getType(object) +
' to type ISODate'); ' to type ISODate');
} }
case 'ASPDate': case 'ASPDate':
if (util.isNumber(object)) {
if (exports.isNumber(object)) {
return '/Date(' + object + ')/'; return '/Date(' + object + ')/';
} }
else if (object instanceof Date) { else if (object instanceof Date) {
return '/Date(' + object.valueOf() + ')/'; return '/Date(' + object.valueOf() + ')/';
} }
else if (util.isString(object)) {
else if (exports.isString(object)) {
match = ASPDateRegex.exec(object); match = ASPDateRegex.exec(object);
var value; var value;
if (match) { if (match) {
@ -353,7 +355,7 @@ util.convert = function(object, type) {
} }
else { else {
throw new Error( throw new Error(
'Cannot convert object of type ' + util.getType(object) +
'Cannot convert object of type ' + exports.getType(object) +
' to type ASPDate'); ' to type ASPDate');
} }
@ -368,11 +370,11 @@ util.convert = function(object, type) {
var ASPDateRegex = /^\/?Date\((\-?\d+)/i; var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
/** /**
* Get the type of an object, for example util.getType([]) returns 'Array'
* Get the type of an object, for example exports.getType([]) returns 'Array'
* @param {*} object * @param {*} object
* @return {String} type * @return {String} type
*/ */
util.getType = function(object) {
exports.getType = function(object) {
var type = typeof object; var type = typeof object;
if (type == 'object') { if (type == 'object') {
@ -415,7 +417,7 @@ util.getType = function(object) {
* @return {number} left The absolute left position of this element * @return {number} left The absolute left position of this element
* in the browser page. * in the browser page.
*/ */
util.getAbsoluteLeft = function(elem) {
exports.getAbsoluteLeft = function(elem) {
var doc = document.documentElement; var doc = document.documentElement;
var body = document.body; var body = document.body;
@ -435,7 +437,7 @@ util.getAbsoluteLeft = function(elem) {
* @return {number} top The absolute top position of this element * @return {number} top The absolute top position of this element
* in the browser page. * in the browser page.
*/ */
util.getAbsoluteTop = function(elem) {
exports.getAbsoluteTop = function(elem) {
var doc = document.documentElement; var doc = document.documentElement;
var body = document.body; var body = document.body;
@ -454,7 +456,7 @@ util.getAbsoluteTop = function(elem) {
* @param {Event} event * @param {Event} event
* @return {Number} pageY * @return {Number} pageY
*/ */
util.getPageY = function(event) {
exports.getPageY = function(event) {
if ('pageY' in event) { if ('pageY' in event) {
return event.pageY; return event.pageY;
} }
@ -480,7 +482,7 @@ util.getPageY = function(event) {
* @param {Event} event * @param {Event} event
* @return {Number} pageX * @return {Number} pageX
*/ */
util.getPageX = function(event) {
exports.getPageX = function(event) {
if ('pageY' in event) { if ('pageY' in event) {
return event.pageX; return event.pageX;
} }
@ -506,7 +508,7 @@ util.getPageX = function(event) {
* @param {Element} elem * @param {Element} elem
* @param {String} className * @param {String} className
*/ */
util.addClassName = function(elem, className) {
exports.addClassName = function(elem, className) {
var classes = elem.className.split(' '); var classes = elem.className.split(' ');
if (classes.indexOf(className) == -1) { if (classes.indexOf(className) == -1) {
classes.push(className); // add the class to the array classes.push(className); // add the class to the array
@ -519,7 +521,7 @@ util.addClassName = function(elem, className) {
* @param {Element} elem * @param {Element} elem
* @param {String} className * @param {String} className
*/ */
util.removeClassName = function(elem, className) {
exports.removeClassName = function(elem, className) {
var classes = elem.className.split(' '); var classes = elem.className.split(' ');
var index = classes.indexOf(className); var index = classes.indexOf(className);
if (index != -1) { if (index != -1) {
@ -537,7 +539,7 @@ util.removeClassName = function(elem, className) {
* the object or array with three parameters: * the object or array with three parameters:
* callback(value, index, object) * callback(value, index, object)
*/ */
util.forEach = function(object, callback) {
exports.forEach = function(object, callback) {
var i, var i,
len; len;
if (object instanceof Array) { if (object instanceof Array) {
@ -562,7 +564,7 @@ util.forEach = function(object, callback) {
* @param {Object} object * @param {Object} object
* @param {Array} array * @param {Array} array
*/ */
util.toArray = function(object) {
exports.toArray = function(object) {
var array = []; var array = [];
for (var prop in object) { for (var prop in object) {
@ -579,7 +581,7 @@ util.toArray = function(object) {
* @param {*} value * @param {*} value
* @return {Boolean} changed * @return {Boolean} changed
*/ */
util.updateProperty = function(object, key, value) {
exports.updateProperty = function(object, key, value) {
if (object[key] !== value) { if (object[key] !== value) {
object[key] = value; object[key] = value;
return true; return true;
@ -597,7 +599,7 @@ util.updateProperty = function(object, key, value) {
* @param {function} listener The callback function to be executed * @param {function} listener The callback function to be executed
* @param {boolean} [useCapture] * @param {boolean} [useCapture]
*/ */
util.addEventListener = function(element, action, listener, useCapture) {
exports.addEventListener = function(element, action, listener, useCapture) {
if (element.addEventListener) { if (element.addEventListener) {
if (useCapture === undefined) if (useCapture === undefined)
useCapture = false; useCapture = false;
@ -619,7 +621,7 @@ util.addEventListener = function(element, action, listener, useCapture) {
* @param {function} listener The listener function * @param {function} listener The listener function
* @param {boolean} [useCapture] * @param {boolean} [useCapture]
*/ */
util.removeEventListener = function(element, action, listener, useCapture) {
exports.removeEventListener = function(element, action, listener, useCapture) {
if (element.removeEventListener) { if (element.removeEventListener) {
// non-IE browsers // non-IE browsers
if (useCapture === undefined) if (useCapture === undefined)
@ -642,7 +644,7 @@ util.removeEventListener = function(element, action, listener, useCapture) {
* @param {Event} event * @param {Event} event
* @return {Element} target element * @return {Element} target element
*/ */
util.getTarget = function(event) {
exports.getTarget = function(event) {
// code from http://www.quirksmode.org/js/events_properties.html // code from http://www.quirksmode.org/js/events_properties.html
if (!event) { if (!event) {
event = window.event; event = window.event;
@ -670,7 +672,7 @@ util.getTarget = function(event) {
* @param {Element} element * @param {Element} element
* @param {Event} event * @param {Event} event
*/ */
util.fakeGesture = function(element, event) {
exports.fakeGesture = function(element, event) {
var eventType = null; var eventType = null;
// for hammer.js 1.0.5 // for hammer.js 1.0.5
@ -692,7 +694,7 @@ util.fakeGesture = function(element, event) {
return gesture; return gesture;
}; };
util.option = {};
exports.option = {};
/** /**
* Convert a value into a boolean * Convert a value into a boolean
@ -700,7 +702,7 @@ util.option = {};
* @param {Boolean} [defaultValue] * @param {Boolean} [defaultValue]
* @returns {Boolean} bool * @returns {Boolean} bool
*/ */
util.option.asBoolean = function (value, defaultValue) {
exports.option.asBoolean = function (value, defaultValue) {
if (typeof value == 'function') { if (typeof value == 'function') {
value = value(); value = value();
} }
@ -718,7 +720,7 @@ util.option.asBoolean = function (value, defaultValue) {
* @param {Number} [defaultValue] * @param {Number} [defaultValue]
* @returns {Number} number * @returns {Number} number
*/ */
util.option.asNumber = function (value, defaultValue) {
exports.option.asNumber = function (value, defaultValue) {
if (typeof value == 'function') { if (typeof value == 'function') {
value = value(); value = value();
} }
@ -736,7 +738,7 @@ util.option.asNumber = function (value, defaultValue) {
* @param {String} [defaultValue] * @param {String} [defaultValue]
* @returns {String} str * @returns {String} str
*/ */
util.option.asString = function (value, defaultValue) {
exports.option.asString = function (value, defaultValue) {
if (typeof value == 'function') { if (typeof value == 'function') {
value = value(); value = value();
} }
@ -754,15 +756,15 @@ util.option.asString = function (value, defaultValue) {
* @param {String} [defaultValue] * @param {String} [defaultValue]
* @returns {String} size * @returns {String} size
*/ */
util.option.asSize = function (value, defaultValue) {
exports.option.asSize = function (value, defaultValue) {
if (typeof value == 'function') { if (typeof value == 'function') {
value = value(); value = value();
} }
if (util.isString(value)) {
if (exports.isString(value)) {
return value; return value;
} }
else if (util.isNumber(value)) {
else if (exports.isNumber(value)) {
return value + 'px'; return value + 'px';
} }
else { else {
@ -776,7 +778,7 @@ util.option.asSize = function (value, defaultValue) {
* @param {HTMLElement} [defaultValue] * @param {HTMLElement} [defaultValue]
* @returns {HTMLElement | null} dom * @returns {HTMLElement | null} dom
*/ */
util.option.asElement = function (value, defaultValue) {
exports.option.asElement = function (value, defaultValue) {
if (typeof value == 'function') { if (typeof value == 'function') {
value = value(); value = value();
} }
@ -786,7 +788,7 @@ util.option.asElement = function (value, defaultValue) {
util.GiveDec = function(Hex) {
exports.GiveDec = function(Hex) {
var Value; var Value;
if (Hex == "A") if (Hex == "A")
@ -807,7 +809,7 @@ util.GiveDec = function(Hex) {
return Value; return Value;
}; };
util.GiveHex = function(Dec) {
exports.GiveHex = function(Dec) {
var Value; var Value;
if(Dec == 10) if(Dec == 10)
@ -834,15 +836,15 @@ util.GiveHex = function(Dec) {
* @param {Object | String} color * @param {Object | String} color
* @return {Object} colorObject * @return {Object} colorObject
*/ */
util.parseColor = function(color) {
exports.parseColor = function(color) {
var c; var c;
if (util.isString(color)) {
if (util.isValidHex(color)) {
var hsv = util.hexToHSV(color);
if (exports.isString(color)) {
if (exports.isValidHex(color)) {
var hsv = exports.hexToHSV(color);
var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)}; var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6}; var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
var darkerColorHex = exports.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
var lighterColorHex = exports.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
c = { c = {
background: color, background: color,
@ -877,7 +879,7 @@ util.parseColor = function(color) {
c.background = color.background || 'white'; c.background = color.background || 'white';
c.border = color.border || c.background; c.border = color.border || c.background;
if (util.isString(color.highlight)) {
if (exports.isString(color.highlight)) {
c.highlight = { c.highlight = {
border: color.highlight, border: color.highlight,
background: color.highlight background: color.highlight
@ -889,7 +891,7 @@ util.parseColor = function(color) {
c.highlight.border = color.highlight && color.highlight.border || c.border; c.highlight.border = color.highlight && color.highlight.border || c.border;
} }
if (util.isString(color.hover)) {
if (exports.isString(color.hover)) {
c.hover = { c.hover = {
border: color.hover, border: color.hover,
background: color.hover background: color.hover
@ -911,15 +913,15 @@ util.parseColor = function(color) {
* @param {String} hex * @param {String} hex
* @returns {{r: *, g: *, b: *}} * @returns {{r: *, g: *, b: *}}
*/ */
util.hexToRGB = function(hex) {
exports.hexToRGB = function(hex) {
hex = hex.replace("#","").toUpperCase(); hex = hex.replace("#","").toUpperCase();
var a = util.GiveDec(hex.substring(0, 1));
var b = util.GiveDec(hex.substring(1, 2));
var c = util.GiveDec(hex.substring(2, 3));
var d = util.GiveDec(hex.substring(3, 4));
var e = util.GiveDec(hex.substring(4, 5));
var f = util.GiveDec(hex.substring(5, 6));
var a = exports.GiveDec(hex.substring(0, 1));
var b = exports.GiveDec(hex.substring(1, 2));
var c = exports.GiveDec(hex.substring(2, 3));
var d = exports.GiveDec(hex.substring(3, 4));
var e = exports.GiveDec(hex.substring(4, 5));
var f = exports.GiveDec(hex.substring(5, 6));
var r = (a * 16) + b; var r = (a * 16) + b;
var g = (c * 16) + d; var g = (c * 16) + d;
@ -928,13 +930,13 @@ util.hexToRGB = function(hex) {
return {r:r,g:g,b:b}; return {r:r,g:g,b:b};
}; };
util.RGBToHex = function(red,green,blue) {
var a = util.GiveHex(Math.floor(red / 16));
var b = util.GiveHex(red % 16);
var c = util.GiveHex(Math.floor(green / 16));
var d = util.GiveHex(green % 16);
var e = util.GiveHex(Math.floor(blue / 16));
var f = util.GiveHex(blue % 16);
exports.RGBToHex = function(red,green,blue) {
var a = exports.GiveHex(Math.floor(red / 16));
var b = exports.GiveHex(red % 16);
var c = exports.GiveHex(Math.floor(green / 16));
var d = exports.GiveHex(green % 16);
var e = exports.GiveHex(Math.floor(blue / 16));
var f = exports.GiveHex(blue % 16);
var hex = a + b + c + d + e + f; var hex = a + b + c + d + e + f;
return "#" + hex; return "#" + hex;
@ -950,7 +952,7 @@ util.RGBToHex = function(red,green,blue) {
* @returns {*} * @returns {*}
* @constructor * @constructor
*/ */
util.RGBToHSV = function(red,green,blue) {
exports.RGBToHSV = function(red,green,blue) {
red=red/255; green=green/255; blue=blue/255; red=red/255; green=green/255; blue=blue/255;
var minRGB = Math.min(red,Math.min(green,blue)); var minRGB = Math.min(red,Math.min(green,blue));
var maxRGB = Math.max(red,Math.max(green,blue)); var maxRGB = Math.max(red,Math.max(green,blue));
@ -978,7 +980,7 @@ util.RGBToHSV = function(red,green,blue) {
* @returns {{r: number, g: number, b: number}} * @returns {{r: number, g: number, b: number}}
* @constructor * @constructor
*/ */
util.HSVToRGB = function(h, s, v) {
exports.HSVToRGB = function(h, s, v) {
var r, g, b; var r, g, b;
var i = Math.floor(h * 6); var i = Math.floor(h * 6);
@ -999,17 +1001,17 @@ util.HSVToRGB = function(h, s, v) {
return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) }; return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
}; };
util.HSVToHex = function(h, s, v) {
var rgb = util.HSVToRGB(h, s, v);
return util.RGBToHex(rgb.r, rgb.g, rgb.b);
exports.HSVToHex = function(h, s, v) {
var rgb = exports.HSVToRGB(h, s, v);
return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
}; };
util.hexToHSV = function(hex) {
var rgb = util.hexToRGB(hex);
return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
exports.hexToHSV = function(hex) {
var rgb = exports.hexToRGB(hex);
return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
}; };
util.isValidHex = function(hex) {
exports.isValidHex = function(hex) {
var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex); var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
return isOk; return isOk;
}; };
@ -1022,13 +1024,13 @@ util.isValidHex = function(hex) {
* @param referenceObject * @param referenceObject
* @returns {*} * @returns {*}
*/ */
util.selectiveBridgeObject = function(fields, referenceObject) {
exports.selectiveBridgeObject = function(fields, referenceObject) {
if (typeof referenceObject == "object") { if (typeof referenceObject == "object") {
var objectTo = Object.create(referenceObject); var objectTo = Object.create(referenceObject);
for (var i = 0; i < fields.length; i++) { for (var i = 0; i < fields.length; i++) {
if (referenceObject.hasOwnProperty(fields[i])) { if (referenceObject.hasOwnProperty(fields[i])) {
if (typeof referenceObject[fields[i]] == "object") { if (typeof referenceObject[fields[i]] == "object") {
objectTo[fields[i]] = util.bridgeObject(referenceObject[fields[i]]);
objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
} }
} }
} }
@ -1046,13 +1048,13 @@ util.selectiveBridgeObject = function(fields, referenceObject) {
* @param referenceObject * @param referenceObject
* @returns {*} * @returns {*}
*/ */
util.bridgeObject = function(referenceObject) {
exports.bridgeObject = function(referenceObject) {
if (typeof referenceObject == "object") { if (typeof referenceObject == "object") {
var objectTo = Object.create(referenceObject); var objectTo = Object.create(referenceObject);
for (var i in referenceObject) { for (var i in referenceObject) {
if (referenceObject.hasOwnProperty(i)) { if (referenceObject.hasOwnProperty(i)) {
if (typeof referenceObject[i] == "object") { if (typeof referenceObject[i] == "object") {
objectTo[i] = util.bridgeObject(referenceObject[i]);
objectTo[i] = exports.bridgeObject(referenceObject[i]);
} }
} }
} }
@ -1073,7 +1075,7 @@ util.bridgeObject = function(referenceObject) {
* @param [String] option | this is the option key in the options argument * @param [String] option | this is the option key in the options argument
* @private * @private
*/ */
util.mergeOptions = function (mergeTarget, options, option) {
exports.mergeOptions = function (mergeTarget, options, option) {
if (options[option] !== undefined) { if (options[option] !== undefined) {
if (typeof options[option] == 'boolean') { if (typeof options[option] == 'boolean') {
mergeTarget[option].enabled = options[option]; mergeTarget[option].enabled = options[option];
@ -1099,7 +1101,7 @@ util.mergeOptions = function (mergeTarget, options, option) {
* @param [String] option | this is the option key in the options argument * @param [String] option | this is the option key in the options argument
* @private * @private
*/ */
util.mergeOptions = function (mergeTarget, options, option) {
exports.mergeOptions = function (mergeTarget, options, option) {
if (options[option] !== undefined) { if (options[option] !== undefined) {
if (typeof options[option] == 'boolean') { if (typeof options[option] == 'boolean') {
mergeTarget[option].enabled = options[option]; mergeTarget[option].enabled = options[option];
@ -1134,7 +1136,7 @@ util.mergeOptions = function (mergeTarget, options, option) {
* @returns {number} * @returns {number}
* @private * @private
*/ */
util.binarySearch = function(orderedItems, range, field, field2) {
exports.binarySearch = function(orderedItems, range, field, field2) {
var array = orderedItems; var array = orderedItems;
var interval = range.end - range.start; var interval = range.end - range.start;
@ -1200,7 +1202,7 @@ util.binarySearch = function(orderedItems, range, field, field2) {
* @returns {number} * @returns {number}
* @private * @private
*/ */
util.binarySearchGeneric = function(orderedItems, target, field, sidePreference) {
exports.binarySearchGeneric = function(orderedItems, target, field, sidePreference) {
var array = orderedItems; var array = orderedItems;
var found = false; var found = false;
var low = 0; var low = 0;

+ 22
- 12
package.json View File

@ -20,21 +20,31 @@
"network", "network",
"browser" "browser"
], ],
"main": "./index",
"scripts": { "scripts": {
"test": "jake test --trace",
"build": "jake --trace"
"test": "mocha",
"build": "gulp",
"watch": "gulp watch",
"watch-dev": "gulp watch-dev"
},
"dependencies": {
"emitter-component": "^1.1.1",
"hammerjs": "1.0.5",
"moment": "^2.7.0",
"mousetrap": "0.0.1"
}, },
"dependencies": {},
"devDependencies": { "devDependencies": {
"jake": "latest",
"jake-utils": "latest",
"clean-css": "latest", "clean-css": "latest",
"browserify": "3.22",
"wrench": "latest",
"moment": "latest",
"hammerjs": "1.0.5",
"mousetrap": "latest",
"emitter-component": "latest",
"node-watch": "latest"
"gulp": "^3.8.5",
"gulp-concat": "^2.2.0",
"gulp-minify-css": "^0.3.6",
"gulp-rename": "^1.2.0",
"gulp-util": "^2.2.19",
"merge-stream": "^0.1.5",
"mocha": "^1.20.1",
"rimraf": "^2.2.8",
"uglify-js": "^2.4.14",
"webpack": "^1.3.1-beta7",
"wrench": "latest"
} }
} }

+ 0
- 84
src/module/exports.js View File

@ -1,84 +0,0 @@
/**
* vis.js module exports
*/
var vis = {
moment: moment,
util: util,
DOMutil: DOMutil,
DataSet: DataSet,
DataView: DataView,
Timeline: Timeline,
Graph2d: Graph2d,
timeline: {
DataStep: DataStep,
Range: Range,
stack: stack,
TimeStep: TimeStep,
components: {
items: {
Item: Item,
ItemBox: ItemBox,
ItemPoint: ItemPoint,
ItemRange: ItemRange
},
Component: Component,
CurrentTime: CurrentTime,
CustomTime: CustomTime,
DataAxis: DataAxis,
GraphGroup: GraphGroup,
Group: Group,
ItemSet: ItemSet,
Legend: Legend,
LineGraph: LineGraph,
TimeAxis: TimeAxis
}
},
Network: Network,
network: {
Edge: Edge,
Groups: Groups,
Images: Images,
Node: Node,
Popup: Popup
},
// Deprecated since v3.0.0
Graph: function () {
throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
},
Graph3d: Graph3d
};
/**
* CommonJS module exports
*/
if (typeof exports !== 'undefined') {
exports = vis;
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = vis;
}
/**
* AMD module exports
*/
if (typeof(define) === 'function') {
define(function () {
return vis;
});
}
/**
* Window exports
*/
if (typeof window !== 'undefined') {
// attach the module to the window, load as a regular javascript file
window['vis'] = vis;
}

+ 0
- 31
src/module/imports.js View File

@ -1,31 +0,0 @@
/**
* vis.js module imports
*/
// Try to load dependencies from the global window object.
// If not available there, load via require.
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
var Emitter = require('emitter-component');
var Hammer;
if (typeof window !== 'undefined') {
// load hammer.js only when running in a browser (where window is available)
Hammer = window['Hammer'] || require('hammerjs');
}
else {
Hammer = function () {
throw Error('hammer.js is only available in a browser, not in node.js.');
}
}
var mousetrap;
if (typeof window !== 'undefined') {
// load mousetrap.js only when running in a browser (where window is available)
mousetrap = window['mousetrap'] || require('mousetrap');
}
else {
mousetrap = function () {
throw Error('mouseTrap is only available in a browser, not in node.js.');
}
}

+ 0
- 829
src/network/dotparser.js View File

@ -1,829 +0,0 @@
(function(exports) {
/**
* Parse a text source containing data in DOT language into a JSON object.
* The object contains two lists: one with nodes and one with edges.
*
* DOT language reference: http://www.graphviz.org/doc/info/lang.html
*
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graph An object containing two parameters:
* {Object[]} nodes
* {Object[]} edges
*/
function parseDOT (data) {
dot = data;
return parseGraph();
}
// token types enumeration
var TOKENTYPE = {
NULL : 0,
DELIMITER : 1,
IDENTIFIER: 2,
UNKNOWN : 3
};
// map with all delimiters
var DELIMITERS = {
'{': true,
'}': true,
'[': true,
']': true,
';': true,
'=': true,
',': true,
'->': true,
'--': true
};
var dot = ''; // current dot file
var index = 0; // current index in dot file
var c = ''; // current token character in expr
var token = ''; // current token
var tokenType = TOKENTYPE.NULL; // type of the token
/**
* Get the first character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function first() {
index = 0;
c = dot.charAt(0);
}
/**
* Get the next character from the dot file.
* The character is stored into the char c. If the end of the dot file is
* reached, the function puts an empty string in c.
*/
function next() {
index++;
c = dot.charAt(index);
}
/**
* Preview the next character from the dot file.
* @return {String} cNext
*/
function nextPreview() {
return dot.charAt(index + 1);
}
/**
* Test whether given character is alphabetic or numeric
* @param {String} c
* @return {Boolean} isAlphaNumeric
*/
var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
function isAlphaNumeric(c) {
return regexAlphaNumeric.test(c);
}
/**
* Merge all properties of object b into object b
* @param {Object} a
* @param {Object} b
* @return {Object} a
*/
function merge (a, b) {
if (!a) {
a = {};
}
if (b) {
for (var name in b) {
if (b.hasOwnProperty(name)) {
a[name] = b[name];
}
}
}
return a;
}
/**
* Set a value in an object, where the provided parameter name can be a
* path with nested parameters. For example:
*
* var obj = {a: 2};
* setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
*
* @param {Object} obj
* @param {String} path A parameter name or dot-separated parameter path,
* like "color.highlight.border".
* @param {*} value
*/
function setValue(obj, path, value) {
var keys = path.split('.');
var o = obj;
while (keys.length) {
var key = keys.shift();
if (keys.length) {
// this isn't the end point
if (!o[key]) {
o[key] = {};
}
o = o[key];
}
else {
// this is the end point
o[key] = value;
}
}
}
/**
* Add a node to a graph object. If there is already a node with
* the same id, their attributes will be merged.
* @param {Object} graph
* @param {Object} node
*/
function addNode(graph, node) {
var i, len;
var current = null;
// find root graph (in case of subgraph)
var graphs = [graph]; // list with all graphs from current graph to root graph
var root = graph;
while (root.parent) {
graphs.push(root.parent);
root = root.parent;
}
// find existing node (at root level) by its id
if (root.nodes) {
for (i = 0, len = root.nodes.length; i < len; i++) {
if (node.id === root.nodes[i].id) {
current = root.nodes[i];
break;
}
}
}
if (!current) {
// this is a new node
current = {
id: node.id
};
if (graph.node) {
// clone default attributes
current.attr = merge(current.attr, graph.node);
}
}
// add node to this (sub)graph and all its parent graphs
for (i = graphs.length - 1; i >= 0; i--) {
var g = graphs[i];
if (!g.nodes) {
g.nodes = [];
}
if (g.nodes.indexOf(current) == -1) {
g.nodes.push(current);
}
}
// merge attributes
if (node.attr) {
current.attr = merge(current.attr, node.attr);
}
}
/**
* Add an edge to a graph object
* @param {Object} graph
* @param {Object} edge
*/
function addEdge(graph, edge) {
if (!graph.edges) {
graph.edges = [];
}
graph.edges.push(edge);
if (graph.edge) {
var attr = merge({}, graph.edge); // clone default attributes
edge.attr = merge(attr, edge.attr); // merge attributes
}
}
/**
* Create an edge to a graph object
* @param {Object} graph
* @param {String | Number | Object} from
* @param {String | Number | Object} to
* @param {String} type
* @param {Object | null} attr
* @return {Object} edge
*/
function createEdge(graph, from, to, type, attr) {
var edge = {
from: from,
to: to,
type: type
};
if (graph.edge) {
edge.attr = merge({}, graph.edge); // clone default attributes
}
edge.attr = merge(edge.attr || {}, attr); // merge attributes
return edge;
}
/**
* Get next token in the current dot file.
* The token and token type are available as token and tokenType
*/
function getToken() {
tokenType = TOKENTYPE.NULL;
token = '';
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
next();
}
do {
var isComment = false;
// skip comment
if (c == '#') {
// find the previous non-space character
var i = index - 1;
while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
i--;
}
if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
// the # is at the start of a line, this is indeed a line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
}
if (c == '/' && nextPreview() == '/') {
// skip line comment
while (c != '' && c != '\n') {
next();
}
isComment = true;
}
if (c == '/' && nextPreview() == '*') {
// skip block comment
while (c != '') {
if (c == '*' && nextPreview() == '/') {
// end of block comment found. skip these last two characters
next();
next();
break;
}
else {
next();
}
}
isComment = true;
}
// skip over whitespaces
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
next();
}
}
while (isComment);
// check for end of dot file
if (c == '') {
// token is still empty
tokenType = TOKENTYPE.DELIMITER;
return;
}
// check for delimiters consisting of 2 characters
var c2 = c + nextPreview();
if (DELIMITERS[c2]) {
tokenType = TOKENTYPE.DELIMITER;
token = c2;
next();
next();
return;
}
// check for delimiters consisting of 1 character
if (DELIMITERS[c]) {
tokenType = TOKENTYPE.DELIMITER;
token = c;
next();
return;
}
// check for an identifier (number or string)
// TODO: more precise parsing of numbers/strings (and the port separator ':')
if (isAlphaNumeric(c) || c == '-') {
token += c;
next();
while (isAlphaNumeric(c)) {
token += c;
next();
}
if (token == 'false') {
token = false; // convert to boolean
}
else if (token == 'true') {
token = true; // convert to boolean
}
else if (!isNaN(Number(token))) {
token = Number(token); // convert to number
}
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// check for a string enclosed by double quotes
if (c == '"') {
next();
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
token += c;
if (c == '"') { // skip the escape character
next();
}
next();
}
if (c != '"') {
throw newSyntaxError('End of string " expected');
}
next();
tokenType = TOKENTYPE.IDENTIFIER;
return;
}
// something unknown is found, wrong characters, a syntax error
tokenType = TOKENTYPE.UNKNOWN;
while (c != '') {
token += c;
next();
}
throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
}
/**
* Parse a graph.
* @returns {Object} graph
*/
function parseGraph() {
var graph = {};
first();
getToken();
// optional strict keyword
if (token == 'strict') {
graph.strict = true;
getToken();
}
// graph or digraph keyword
if (token == 'graph' || token == 'digraph') {
graph.type = token;
getToken();
}
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
graph.id = token;
getToken();
}
// open angle bracket
if (token != '{') {
throw newSyntaxError('Angle bracket { expected');
}
getToken();
// statements
parseStatements(graph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// end of file
if (token !== '') {
throw newSyntaxError('End of file expected');
}
getToken();
// remove temporary default properties
delete graph.node;
delete graph.edge;
delete graph.graph;
return graph;
}
/**
* Parse a list with statements.
* @param {Object} graph
*/
function parseStatements (graph) {
while (token !== '' && token != '}') {
parseStatement(graph);
if (token == ';') {
getToken();
}
}
}
/**
* Parse a single statement. Can be a an attribute statement, node
* statement, a series of node statements and edge statements, or a
* parameter.
* @param {Object} graph
*/
function parseStatement(graph) {
// parse subgraph
var subgraph = parseSubgraph(graph);
if (subgraph) {
// edge statements
parseEdge(graph, subgraph);
return;
}
// parse an attribute statement
var attr = parseAttributeStatement(graph);
if (attr) {
return;
}
// parse node
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
var id = token; // id can be a string or a number
getToken();
if (token == '=') {
// id statement
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier expected');
}
graph[id] = token;
getToken();
// TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
}
else {
parseNodeStatement(graph, id);
}
}
/**
* Parse a subgraph
* @param {Object} graph parent graph object
* @return {Object | null} subgraph
*/
function parseSubgraph (graph) {
var subgraph = null;
// optional subgraph keyword
if (token == 'subgraph') {
subgraph = {};
subgraph.type = 'subgraph';
getToken();
// optional graph id
if (tokenType == TOKENTYPE.IDENTIFIER) {
subgraph.id = token;
getToken();
}
}
// open angle bracket
if (token == '{') {
getToken();
if (!subgraph) {
subgraph = {};
}
subgraph.parent = graph;
subgraph.node = graph.node;
subgraph.edge = graph.edge;
subgraph.graph = graph.graph;
// statements
parseStatements(subgraph);
// close angle bracket
if (token != '}') {
throw newSyntaxError('Angle bracket } expected');
}
getToken();
// remove temporary default properties
delete subgraph.node;
delete subgraph.edge;
delete subgraph.graph;
delete subgraph.parent;
// register at the parent graph
if (!graph.subgraphs) {
graph.subgraphs = [];
}
graph.subgraphs.push(subgraph);
}
return subgraph;
}
/**
* parse an attribute statement like "node [shape=circle fontSize=16]".
* Available keywords are 'node', 'edge', 'graph'.
* The previous list with default attributes will be replaced
* @param {Object} graph
* @returns {String | null} keyword Returns the name of the parsed attribute
* (node, edge, graph), or null if nothing
* is parsed.
*/
function parseAttributeStatement (graph) {
// attribute statements
if (token == 'node') {
getToken();
// node attributes
graph.node = parseAttributeList();
return 'node';
}
else if (token == 'edge') {
getToken();
// edge attributes
graph.edge = parseAttributeList();
return 'edge';
}
else if (token == 'graph') {
getToken();
// graph attributes
graph.graph = parseAttributeList();
return 'graph';
}
return null;
}
/**
* parse a node statement
* @param {Object} graph
* @param {String | Number} id
*/
function parseNodeStatement(graph, id) {
// node statement
var node = {
id: id
};
var attr = parseAttributeList();
if (attr) {
node.attr = attr;
}
addNode(graph, node);
// edge statements
parseEdge(graph, id);
}
/**
* Parse an edge or a series of edges
* @param {Object} graph
* @param {String | Number} from Id of the from node
*/
function parseEdge(graph, from) {
while (token == '->' || token == '--') {
var to;
var type = token;
getToken();
var subgraph = parseSubgraph(graph);
if (subgraph) {
to = subgraph;
}
else {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Identifier or subgraph expected');
}
to = token;
addNode(graph, {
id: to
});
getToken();
}
// parse edge attributes
var attr = parseAttributeList();
// create edge
var edge = createEdge(graph, from, to, type, attr);
addEdge(graph, edge);
from = to;
}
}
/**
* Parse a set with attributes,
* for example [label="1.000", shape=solid]
* @return {Object | null} attr
*/
function parseAttributeList() {
var attr = null;
while (token == '[') {
getToken();
attr = {};
while (token !== '' && token != ']') {
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Attribute name expected');
}
var name = token;
getToken();
if (token != '=') {
throw newSyntaxError('Equal sign = expected');
}
getToken();
if (tokenType != TOKENTYPE.IDENTIFIER) {
throw newSyntaxError('Attribute value expected');
}
var value = token;
setValue(attr, name, value); // name can be a path
getToken();
if (token ==',') {
getToken();
}
}
if (token != ']') {
throw newSyntaxError('Bracket ] expected');
}
getToken();
}
return attr;
}
/**
* Create a syntax error with extra information on current token and index.
* @param {String} message
* @returns {SyntaxError} err
*/
function newSyntaxError(message) {
return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
}
/**
* Chop off text after a maximum length
* @param {String} text
* @param {Number} maxLength
* @returns {String}
*/
function chop (text, maxLength) {
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
}
/**
* Execute a function fn for each pair of elements in two arrays
* @param {Array | *} array1
* @param {Array | *} array2
* @param {function} fn
*/
function forEach2(array1, array2, fn) {
if (array1 instanceof Array) {
array1.forEach(function (elem1) {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(elem1, elem2);
});
}
else {
fn(elem1, array2);
}
});
}
else {
if (array2 instanceof Array) {
array2.forEach(function (elem2) {
fn(array1, elem2);
});
}
else {
fn(array1, array2);
}
}
}
/**
* Convert a string containing a graph in DOT language into a map containing
* with nodes and edges in the format of graph.
* @param {String} data Text containing a graph in DOT-notation
* @return {Object} graphData
*/
function DOTToGraph (data) {
// parse the DOT file
var dotData = parseDOT(data);
var graphData = {
nodes: [],
edges: [],
options: {}
};
// copy the nodes
if (dotData.nodes) {
dotData.nodes.forEach(function (dotNode) {
var graphNode = {
id: dotNode.id,
label: String(dotNode.label || dotNode.id)
};
merge(graphNode, dotNode.attr);
if (graphNode.image) {
graphNode.shape = 'image';
}
graphData.nodes.push(graphNode);
});
}
// copy the edges
if (dotData.edges) {
/**
* Convert an edge in DOT format to an edge with VisGraph format
* @param {Object} dotEdge
* @returns {Object} graphEdge
*/
function convertEdge(dotEdge) {
var graphEdge = {
from: dotEdge.from,
to: dotEdge.to
};
merge(graphEdge, dotEdge.attr);
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
return graphEdge;
}
dotData.edges.forEach(function (dotEdge) {
var from, to;
if (dotEdge.from instanceof Object) {
from = dotEdge.from.nodes;
}
else {
from = {
id: dotEdge.from
}
}
if (dotEdge.to instanceof Object) {
to = dotEdge.to.nodes;
}
else {
to = {
id: dotEdge.to
}
}
if (dotEdge.from instanceof Object && dotEdge.from.edges) {
dotEdge.from.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
forEach2(from, to, function (from, to) {
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
if (dotEdge.to instanceof Object && dotEdge.to.edges) {
dotEdge.to.edges.forEach(function (subEdge) {
var graphEdge = convertEdge(subEdge);
graphData.edges.push(graphEdge);
});
}
});
}
// copy the options
if (dotData.attr) {
graphData.options = dotData.attr;
}
return graphData;
}
// exports
exports.parseDOT = parseDOT;
exports.DOTToGraph = DOTToGraph;
})(typeof util !== 'undefined' ? util : exports);

+ 0
- 1143
src/network/networkMixins/ClusterMixin.js
File diff suppressed because it is too large
View File


+ 0
- 311
src/network/networkMixins/HierarchicalLayoutMixin.js View File

@ -1,311 +0,0 @@
var HierarchicalLayoutMixin = {
_resetLevels : function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
}
}
}
},
/**
* This is the main function to layout the nodes in a hierarchical way.
* It checks if the node details are supplied correctly
*
* @private
*/
_setupHierarchicalLayout : function() {
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
this.constants.hierarchicalLayout.levelSeparation *= -1;
}
else {
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
}
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
}
}
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) {
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent(true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
this._determineLevels(hubsize);
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system.
this._placeNodesByHierarchy(distribution);
// start the simulation.
this.start();
}
}
},
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
* @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
_placeNodesByHierarchy : function(distribution) {
var nodeId, node;
// start placing all the level 0 nodes first. Then recursively position their branches.
for (nodeId in distribution[0].nodes) {
if (distribution[0].nodes.hasOwnProperty(nodeId)) {
node = distribution[0].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[0].minPos;
node.xFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
else {
if (node.yFixed) {
node.y = distribution[0].minPos;
node.yFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
},
/**
* This function get the distribution of levels based on hubsize
*
* @returns {Object}
* @private
*/
_getDistribution : function() {
var distribution = {};
var nodeId, node, level;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
if (!distribution.hasOwnProperty(node.level)) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[node.id] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
},
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
_determineLevels : function(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
}
}
// branch from hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
},
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
*
* @private
*/
_changeConstants : function() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
this.constants.smoothCurves = false;
this._configureSmoothCurves();
},
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
*
* @param edges
* @param parentId
* @param distribution
* @param parentLevel
* @private
*/
_placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
},
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevel : function(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
},
/**
* Unfix nodes
*
* @private
*/
_restoreNodes : function() {
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId].xFixed = false;
this.nodes[nodeId].yFixed = false;
}
}
}
};

+ 0
- 576
src/network/networkMixins/ManipulationMixin.js View File

@ -1,576 +0,0 @@
/**
* Created by Alex on 2/4/14.
*/
var manipulationMixin = {
/**
* clears the toolbar div element of children
*
* @private
*/
_clearManipulatorBar : function() {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
},
/**
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
* these functions to their original functionality, we saved them in this.cachedFunctions.
* This function restores these functions to their original function.
*
* @private
*/
_restoreOverloadedFunctions : function() {
for (var functionName in this.cachedFunctions) {
if (this.cachedFunctions.hasOwnProperty(functionName)) {
this[functionName] = this.cachedFunctions[functionName];
}
}
},
/**
* Enable or disable edit-mode.
*
* @private
*/
_toggleEditMode : function() {
this.editMode = !this.editMode;
var toolbar = document.getElementById("network-manipulationDiv");
var closeDiv = document.getElementById("network-manipulation-closeDiv");
var editModeDiv = document.getElementById("network-manipulation-editMode");
if (this.editMode == true) {
toolbar.style.display="block";
closeDiv.style.display="block";
editModeDiv.style.display="none";
closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
toolbar.style.display="none";
closeDiv.style.display="none";
editModeDiv.style.display="block";
closeDiv.onclick = null;
}
this._createManipulatorBar()
},
/**
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
* @private
*/
_createManipulatorBar : function() {
// remove bound functions
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
if (this.edgeBeingEdited !== undefined) {
this.edgeBeingEdited._disableControlNodes();
this.edgeBeingEdited = undefined;
this.selectedControlNode = null;
}
// restore overloaded functions
this._restoreOverloadedFunctions();
// resume calculation
this.freezeSimulation = false;
// reset global variables
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false;
if (this.editMode == true) {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI add' id='network-manipulate-addNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI delete' id='network-manipulate-delete'>" +
"<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
}
// bind the icons
var addNodeButton = document.getElementById("network-manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
var addEdgeButton = document.getElementById("network-manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
var editButton = document.getElementById("network-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
var editButton = document.getElementById("network-manipulate-editEdge");
editButton.onclick = this._createEditEdgeToolbar.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("network-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
var closeDiv = document.getElementById("network-manipulation-closeDiv");
closeDiv.onclick = this._toggleEditMode.bind(this);
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
}
else {
this.editModeDiv.innerHTML = "" +
"<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
var editModeButton = document.getElementById("network-manipulate-editModeButton");
editModeButton.onclick = this._toggleEditMode.bind(this);
}
},
/**
* Create the toolbar for adding Nodes
*
* @private
*/
_createAddNodeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._addNode.bind(this);
this.on('select', this.boundFunction);
},
/**
* create the toolbar to connect nodes
*
* @private
*/
_createAddEdgeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
this._unselectAll(true);
this.freezeSimulation = true;
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this._unselectAll();
this.forceAppendSelection = false;
this.blockConnectingEdgeSelection = true;
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._handleConnect.bind(this);
this.on('select', this.boundFunction);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this._handleTouch = this._handleConnect;
this._handleOnRelease = this._finishConnect;
// redraw to show the unselect
this._redraw();
},
/**
* create the toolbar to edit edges
*
* @private
*/
_createEditEdgeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this.edgeBeingEdited = this._getSelectedEdge();
this.edgeBeingEdited._enableControlNodes();
this.manipulationDiv.innerHTML = "" +
"<span class='network-manipulationUI back' id='network-manipulate-back'>" +
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='network-seperatorLine'></div>" +
"<span class='network-manipulationUI none' id='network-manipulate-back'>" +
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("network-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this.cachedFunctions["_handleTap"] = this._handleTap;
this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleTouch = this._selectControlNode;
this._handleTap = function () {};
this._handleOnDrag = this._controlNodeDrag;
this._handleDragStart = function () {}
this._handleOnRelease = this._releaseControlNode;
// redraw to show the unselect
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_selectControlNode : function(pointer) {
this.edgeBeingEdited.controlNodes.from.unselect();
this.edgeBeingEdited.controlNodes.to.unselect();
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
if (this.selectedControlNode !== null) {
this.selectedControlNode.select();
this.freezeSimulation = true;
}
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_controlNodeDrag : function(event) {
var pointer = this._getPointer(event.gesture.center);
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
}
this._redraw();
},
_releaseControlNode : function(pointer) {
var newNode = this._getNodeAt(pointer);
if (newNode != null) {
if (this.edgeBeingEdited.controlNodes.from.selected == true) {
this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
this.edgeBeingEdited.controlNodes.from.unselect();
}
if (this.edgeBeingEdited.controlNodes.to.selected == true) {
this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
this.edgeBeingEdited.controlNodes.to.unselect();
}
}
else {
this.edgeBeingEdited._restoreControlNodes();
}
this.freezeSimulation = false;
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_handleConnect : function(pointer) {
if (this._getSelectedNodeCount() == 0) {
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._selectObject(node,false);
// create a node the temporary line can look at
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetNode'].x = node.x;
this.sectors['support']['nodes']['targetNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetViaNode'].x = node.x;
this.sectors['support']['nodes']['targetViaNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
// create a temporary edge
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
this.edges['connectionEdge'].from = node;
this.edges['connectionEdge'].connected = true;
this.edges['connectionEdge'].smooth = true;
this.edges['connectionEdge'].selected = true;
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.gesture.center);
this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
};
this.moving = true;
this.start();
}
}
}
},
_finishConnect : function(pointer) {
if (this._getSelectedNodeCount() == 1) {
// restore the drag function
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
delete this.cachedFunctions["_handleOnDrag"];
// remember the edge id
var connectFromId = this.edges['connectionEdge'].fromId;
// remove the temporary nodes and edge
delete this.edges['connectionEdge'];
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._createEdge(connectFromId,node.id);
this._createManipulatorBar();
}
}
this._unselectAll();
}
},
/**
* Adds a node on the specified location
*/
_addNode : function() {
if (this._selectionIsEmpty() && this.editMode == true) {
var positionObject = this._pointerToPositionObject(this.pointerPosition);
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
if (this.triggerFunctions.add) {
if (this.triggerFunctions.add.length == 2) {
var me = this;
this.triggerFunctions.add(defaultData, function(finalizedData) {
me.nodesData.add(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels['addError']);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
else {
this.nodesData.add(defaultData);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
},
/**
* connect two nodes with a new edge.
*
* @private
*/
_createEdge : function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.connect) {
if (this.triggerFunctions.connect.length == 2) {
var me = this;
this.triggerFunctions.connect(defaultData, function(finalizedData) {
me.edgesData.add(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.add(defaultData);
this.moving = true;
this.start();
}
}
},
/**
* connect two nodes with a new edge.
*
* @private
*/
_editEdge : function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.editEdge) {
if (this.triggerFunctions.editEdge.length == 2) {
var me = this;
this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
me.edgesData.update(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.update(defaultData);
this.moving = true;
this.start();
}
}
},
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
*
* @private
*/
_editNode : function() {
if (this.triggerFunctions.edit && this.editMode == true) {
var node = this._getSelectedNode();
var data = {id:node.id,
label: node.label,
group: node.group,
shape: node.shape,
color: {
background:node.color.background,
border:node.color.border,
highlight: {
background:node.color.highlight.background,
border:node.color.highlight.border
}
}};
if (this.triggerFunctions.edit.length == 2) {
var me = this;
this.triggerFunctions.edit(data, function (finalizedData) {
me.nodesData.update(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["editError"]);
}
}
else {
alert(this.constants.labels["editBoundError"]);
}
},
/**
* delete everything in the selection
*
* @private
*/
_deleteSelected : function() {
if (!this._selectionIsEmpty() && this.editMode == true) {
if (!this._clusterInSelection()) {
var selectedNodes = this.getSelectedNodes();
var selectedEdges = this.getSelectedEdges();
if (this.triggerFunctions.del) {
var me = this;
var data = {nodes: selectedNodes, edges: selectedEdges};
if (this.triggerFunctions.del.length = 2) {
this.triggerFunctions.del(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
me._unselectAll();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["deleteError"])
}
}
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this._unselectAll();
this.moving = true;
this.start();
}
}
else {
alert(this.constants.labels["deleteClusterError"]);
}
}
}
};

+ 0
- 199
src/network/networkMixins/MixinLoader.js View File

@ -1,199 +0,0 @@
/**
* Created by Alex on 2/10/14.
*/
var networkMixinLoaders = {
/**
* Load a mixin into the network object
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_loadMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Network.prototype[mixinFunction] = sourceVariable[mixinFunction];
}
}
},
/**
* removes a mixin from the network object.
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_clearMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Network.prototype[mixinFunction] = undefined;
}
}
},
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
_loadPhysicsSystem: function () {
this._loadMixin(physicsMixin);
this._loadSelectedForceSolver();
if (this.constants.configurePhysics == true) {
this._loadPhysicsConfiguration();
}
},
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
_loadClusterSystem: function () {
this.clusterSession = 0;
this.hubThreshold = 5;
this._loadMixin(ClusterMixin);
},
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
_loadSectorSystem: function () {
this.sectors = {};
this.activeSector = ["default"];
this.sectors["active"] = {};
this.sectors["active"]["default"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = {};
this.sectors["support"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
this._loadMixin(SectorMixin);
},
/**
* Mixin the selection system and initialize the parameters required
*
* @private
*/
_loadSelectionSystem: function () {
this.selectionObj = {nodes: {}, edges: {}};
this._loadMixin(SelectionMixin);
},
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
_loadManipulationSystem: function () {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false;
if (this.constants.dataManipulation.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'network-manipulationDiv';
this.manipulationDiv.id = 'network-manipulationDiv';
if (this.editMode == true) {
this.manipulationDiv.style.display = "block";
}
else {
this.manipulationDiv.style.display = "none";
}
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
if (this.editModeDiv === undefined) {
this.editModeDiv = document.createElement('div');
this.editModeDiv.className = 'network-manipulation-editMode';
this.editModeDiv.id = 'network-manipulation-editMode';
if (this.editMode == true) {
this.editModeDiv.style.display = "none";
}
else {
this.editModeDiv.style.display = "block";
}
this.containerElement.insertBefore(this.editModeDiv, this.frame);
}
if (this.closeDiv === undefined) {
this.closeDiv = document.createElement('div');
this.closeDiv.className = 'network-manipulation-closeDiv';
this.closeDiv.id = 'network-manipulation-closeDiv';
this.closeDiv.style.display = this.manipulationDiv.style.display;
this.containerElement.insertBefore(this.closeDiv, this.frame);
}
// load the manipulation functions
this._loadMixin(manipulationMixin);
// create the manipulator toolbar
this._createManipulatorBar();
}
else {
if (this.manipulationDiv !== undefined) {
// removes all the bindings and overloads
this._createManipulatorBar();
// remove the manipulation divs
this.containerElement.removeChild(this.manipulationDiv);
this.containerElement.removeChild(this.editModeDiv);
this.containerElement.removeChild(this.closeDiv);
this.manipulationDiv = undefined;
this.editModeDiv = undefined;
this.closeDiv = undefined;
// remove the mixin functions
this._clearMixin(manipulationMixin);
}
}
},
/**
* Mixin the navigation (User Interface) system and initialize the parameters required
*
* @private
*/
_loadNavigationControls: function () {
this._loadMixin(NavigationMixin);
// the clean function removes the button divs, this is done to remove the bindings.
this._cleanNavigation();
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
},
/**
* Mixin the hierarchical layout system.
*
* @private
*/
_loadHierarchySystem: function () {
this._loadMixin(HierarchicalLayoutMixin);
}
};

+ 0
- 205
src/network/networkMixins/NavigationMixin.js View File

@ -1,205 +0,0 @@
/**
* Created by Alex on 1/22/14.
*/
var NavigationMixin = {
_cleanNavigation : function() {
// clean up previosu navigation items
var wrapper = document.getElementById('network-navigation_wrapper');
if (wrapper != null) {
this.containerElement.removeChild(wrapper);
}
document.onmouseup = null;
},
/**
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
*
* @private
*/
_loadNavigationElements : function() {
this._cleanNavigation();
this.navigationDivs = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
this.navigationDivs['wrapper'] = document.createElement('div');
this.navigationDivs['wrapper'].id = "network-navigation_wrapper";
this.navigationDivs['wrapper'].style.position = "absolute";
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDivs[navigationDivs[i]] = document.createElement('div');
this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
document.onmouseup = this._stopMovement.bind(this);
},
/**
* this stops all movement induced by the navigation buttons
*
* @private
*/
_stopMovement : function() {
this._xStopMoving();
this._yStopMoving();
this._stopZoom();
},
/**
* stops the actions performed by page up and down etc.
*
* @param event
* @private
*/
_preventDefault : function(event) {
if (event !== undefined) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
},
/**
* move the screen up
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
* To avoid this behaviour, we do the translation in the start loop.
*
* @private
*/
_moveUp : function(event) {
this.yIncrement = this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['up'].className += " active";
}
},
/**
* move the screen down
* @private
*/
_moveDown : function(event) {
this.yIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['down'].className += " active";
}
},
/**
* move the screen left
* @private
*/
_moveLeft : function(event) {
this.xIncrement = this.constants.keyboard.speed.x;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['left'].className += " active";
}
},
/**
* move the screen right
* @private
*/
_moveRight : function(event) {
this.xIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['right'].className += " active";
}
},
/**
* Zoom in, using the same method as the movement.
* @private
*/
_zoomIn : function(event) {
this.zoomIncrement = this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className += " active";
}
},
/**
* Zoom out
* @private
*/
_zoomOut : function() {
this.zoomIncrement = -this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomOut'].className += " active";
}
},
/**
* Stop zooming and unhighlight the zoom controls
* @private
*/
_stopZoom : function() {
this.zoomIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
}
},
/**
* Stop moving in the Y direction and unHighlight the up and down
* @private
*/
_yStopMoving : function() {
this.yIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
}
},
/**
* Stop moving in the X direction and unHighlight left and right.
* @private
*/
_xStopMoving : function() {
this.xIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
}
}
};

+ 0
- 552
src/network/networkMixins/SectorsMixin.js View File

@ -1,552 +0,0 @@
/**
* Creation of the SectorMixin var.
*
* This contains all the functions the Network object can use to employ the sector system.
* The sector system is always used by Network, though the benefits only apply to the use of clustering.
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
*
* Alex de Mulder
* 21-01-2013
*/
var SectorMixin = {
/**
* This function is only called by the setData function of the Network object.
* This loads the global references into the active sector. This initializes the sector.
*
* @private
*/
_putDataInSector : function() {
this.sectors["active"][this._sector()].nodes = this.nodes;
this.sectors["active"][this._sector()].edges = this.edges;
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
},
/**
* /**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied (active) sector. If a type is defined, do the specific type
*
* @param {String} sectorId
* @param {String} [sectorType] | "active" or "frozen"
* @private
*/
_switchToSector : function(sectorId, sectorType) {
if (sectorType === undefined || sectorType == "active") {
this._switchToActiveSector(sectorId);
}
else {
this._switchToFrozenSector(sectorId);
}
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @param sectorId
* @private
*/
_switchToActiveSector : function(sectorId) {
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
this.nodes = this.sectors["active"][sectorId]["nodes"];
this.edges = this.sectors["active"][sectorId]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @param sectorId
* @private
*/
_switchToSupportSector : function() {
this.nodeIndices = this.sectors["support"]["nodeIndices"];
this.nodes = this.sectors["support"]["nodes"];
this.edges = this.sectors["support"]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
*
* @param sectorId
* @private
*/
_switchToFrozenSector : function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the currently active sector.
*
* @private
*/
_loadLatestSector : function() {
this._switchToSector(this._sector());
},
/**
* This function returns the currently active sector Id
*
* @returns {String}
* @private
*/
_sector : function() {
return this.activeSector[this.activeSector.length-1];
},
/**
* This function returns the previously active sector Id
*
* @returns {String}
* @private
*/
_previousSector : function() {
if (this.activeSector.length > 1) {
return this.activeSector[this.activeSector.length-2];
}
else {
throw new TypeError('there are not enough sectors in the this.activeSector array.');
}
},
/**
* We add the active sector at the end of the this.activeSector array
* This ensures it is the currently active sector returned by _sector() and it reaches the top
* of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
*
* @param newId
* @private
*/
_setActiveSector : function(newId) {
this.activeSector.push(newId);
},
/**
* We remove the currently active sector id from the active sector stack. This happens when
* we reactivate the previously active sector
*
* @private
*/
_forgetLastSector : function() {
this.activeSector.pop();
},
/**
* This function creates a new active sector with the supplied newId. This newId
* is the expanding node id.
*
* @param {String} newId | Id of the new active sector
* @private
*/
_createNewSector : function(newId) {
// create the new sector
this.sectors["active"][newId] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": this.scale,
"drawingNode": undefined};
// create the new sector render node. This gives visual feedback that you are in a new sector.
this.sectors["active"][newId]['drawingNode'] = new Node(
{id:newId,
color: {
background: "#eaefef",
border: "495c5e"
}
},{},{},this.constants);
this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
},
/**
* This function removes the currently active sector. This is called when we create a new
* active sector.
*
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
_deleteActiveSector : function(sectorId) {
delete this.sectors["active"][sectorId];
},
/**
* This function removes the currently active sector. This is called when we reactivate
* the previously active sector.
*
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
_deleteFrozenSector : function(sectorId) {
delete this.sectors["frozen"][sectorId];
},
/**
* Freezing an active sector means moving it from the "active" object to the "frozen" object.
* We copy the references, then delete the active entree.
*
* @param sectorId
* @private
*/
_freezeSector : function(sectorId) {
// we move the set references from the active to the frozen stack.
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
// we have moved the sector data into the frozen set, we now remove it from the active set
this._deleteActiveSector(sectorId);
},
/**
* This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
* object to the "active" object.
*
* @param sectorId
* @private
*/
_activateSector : function(sectorId) {
// we move the set references from the frozen to the active stack.
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
// we have moved the sector data into the active set, we now remove it from the frozen stack
this._deleteFrozenSector(sectorId);
},
/**
* This function merges the data from the currently active sector with a frozen sector. This is used
* in the process of reverting back to the previously active sector.
* The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
* upon the creation of a new active sector.
*
* @param sectorId
* @private
*/
_mergeThisWithFrozen : function(sectorId) {
// copy all nodes
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
}
}
// copy all edges (if not fully clustered, else there are no edges)
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
}
}
// merge the nodeIndices
for (var i = 0; i < this.nodeIndices.length; i++) {
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
}
},
/**
* This clusters the sector to one cluster. It was a single cluster before this process started so
* we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
*
* @private
*/
_collapseThisToSingleCluster : function() {
this.clusterToFit(1,false);
},
/**
* We create a new active sector from the node that we want to open.
*
* @param node
* @private
*/
_addSector : function(node) {
// this is the currently active sector
var sector = this._sector();
// // this should allow me to select nodes from a frozen set.
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
// console.log("the node is part of the active sector");
// }
// else {
// console.log("I dont know what the fuck happened!!");
// }
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
delete this.nodes[node.id];
var unqiueIdentifier = util.randomUUID();
// we fully freeze the currently active sector
this._freezeSector(sector);
// we create a new active sector. This sector has the Id of the node to ensure uniqueness
this._createNewSector(unqiueIdentifier);
// we add the active sector to the sectors array to be able to revert these steps later on
this._setActiveSector(unqiueIdentifier);
// we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
this._switchToSector(this._sector());
// finally we add the node we removed from our previous active sector to the new active sector
this.nodes[node.id] = node;
},
/**
* We close the sector that is currently open and revert back to the one before.
* If the active sector is the "default" sector, nothing happens.
*
* @private
*/
_collapseSector : function() {
// the currently active sector
var sector = this._sector();
// we cannot collapse the default sector
if (sector != "default") {
if ((this.nodeIndices.length == 1) ||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
var previousSector = this._previousSector();
// we collapse the sector back to a single cluster
this._collapseThisToSingleCluster();
// we move the remaining nodes, edges and nodeIndices to the previous sector.
// This previous sector is the one we will reactivate
this._mergeThisWithFrozen(previousSector);
// the previously active (frozen) sector now has all the data from the currently active sector.
// we can now delete the active sector.
this._deleteActiveSector(sector);
// we activate the previously active (and currently frozen) sector.
this._activateSector(previousSector);
// we load the references from the newly active sector into the global references
this._switchToSector(previousSector);
// we forget the previously active sector because we reverted to the one before
this._forgetLastSector();
// finally, we update the node index list.
this._updateNodeIndexList();
// we refresh the list with calulation nodes and calculation node indices.
this._updateCalculationNodes();
}
}
},
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllActiveSectors : function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
},
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInSupportSector : function(runFunction,argument) {
if (argument === undefined) {
this._switchToSupportSector();
this[runFunction]();
}
else {
this._switchToSupportSector();
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
},
/**
* This runs a function in all frozen sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllFrozenSectors : function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
this._loadLatestSector();
},
/**
* This runs a function in all sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllSectors : function(runFunction,argument) {
var args = Array.prototype.splice.call(arguments, 1);
if (argument === undefined) {
this._doInAllActiveSectors(runFunction);
this._doInAllFrozenSectors(runFunction);
}
else {
if (args.length > 1) {
this._doInAllActiveSectors(runFunction,args[0],args[1]);
this._doInAllFrozenSectors(runFunction,args[0],args[1]);
}
else {
this._doInAllActiveSectors(runFunction,argument);
this._doInAllFrozenSectors(runFunction,argument);
}
}
},
/**
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
*
* @private
*/
_clearNodeIndexList : function() {
var sector = this._sector();
this.sectors["active"][sector]["nodeIndices"] = [];
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
},
/**
* Draw the encompassing sector node
*
* @param ctx
* @param sectorType
* @private
*/
_drawSectorNodes : function(ctx,sectorType) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var sector in this.sectors[sectorType]) {
if (this.sectors[sectorType].hasOwnProperty(sector)) {
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
this._switchToSector(sector,sectorType);
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.resize(ctx);
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
}
}
node = this.sectors[sectorType][sector]["drawingNode"];
node.x = 0.5 * (maxX + minX);
node.y = 0.5 * (maxY + minY);
node.width = 2 * (node.x - minX);
node.height = 2 * (node.y - minY);
node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
node.setScale(this.scale);
node._drawCircle(ctx);
}
}
}
},
_drawAllSectorNodes : function(ctx) {
this._drawSectorNodes(ctx,"frozen");
this._drawSectorNodes(ctx,"active");
this._loadLatestSector();
}
};

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save