vis-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.gitmodules +3 -0
  4. data/.project +11 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +202 -0
  7. data/README.md +29 -0
  8. data/Rakefile +1 -0
  9. data/lib/vis/rails/engine.rb +6 -0
  10. data/lib/vis/rails/version.rb +5 -0
  11. data/lib/vis/rails.rb +7 -0
  12. data/vendor/assets/javascripts/vis.js +1 -0
  13. data/vendor/assets/stylesheets/vis.css +3 -0
  14. data/vendor/assets/vis/DataSet.js +936 -0
  15. data/vendor/assets/vis/DataView.js +281 -0
  16. data/vendor/assets/vis/EventBus.js +89 -0
  17. data/vendor/assets/vis/events.js +116 -0
  18. data/vendor/assets/vis/graph/ClusterMixin.js +1019 -0
  19. data/vendor/assets/vis/graph/Edge.js +620 -0
  20. data/vendor/assets/vis/graph/Graph.js +2111 -0
  21. data/vendor/assets/vis/graph/Groups.js +80 -0
  22. data/vendor/assets/vis/graph/Images.js +41 -0
  23. data/vendor/assets/vis/graph/NavigationMixin.js +245 -0
  24. data/vendor/assets/vis/graph/Node.js +978 -0
  25. data/vendor/assets/vis/graph/Popup.js +105 -0
  26. data/vendor/assets/vis/graph/SectorsMixin.js +547 -0
  27. data/vendor/assets/vis/graph/SelectionMixin.js +515 -0
  28. data/vendor/assets/vis/graph/dotparser.js +829 -0
  29. data/vendor/assets/vis/graph/img/downarrow.png +0 -0
  30. data/vendor/assets/vis/graph/img/leftarrow.png +0 -0
  31. data/vendor/assets/vis/graph/img/minus.png +0 -0
  32. data/vendor/assets/vis/graph/img/plus.png +0 -0
  33. data/vendor/assets/vis/graph/img/rightarrow.png +0 -0
  34. data/vendor/assets/vis/graph/img/uparrow.png +0 -0
  35. data/vendor/assets/vis/graph/img/zoomExtends.png +0 -0
  36. data/vendor/assets/vis/graph/shapes.js +225 -0
  37. data/vendor/assets/vis/module/exports.js +68 -0
  38. data/vendor/assets/vis/module/header.js +24 -0
  39. data/vendor/assets/vis/module/imports.js +32 -0
  40. data/vendor/assets/vis/shim.js +252 -0
  41. data/vendor/assets/vis/timeline/Controller.js +172 -0
  42. data/vendor/assets/vis/timeline/Range.js +553 -0
  43. data/vendor/assets/vis/timeline/Stack.js +192 -0
  44. data/vendor/assets/vis/timeline/TimeStep.js +449 -0
  45. data/vendor/assets/vis/timeline/Timeline.js +476 -0
  46. data/vendor/assets/vis/timeline/component/Component.js +148 -0
  47. data/vendor/assets/vis/timeline/component/ContentPanel.js +113 -0
  48. data/vendor/assets/vis/timeline/component/CurrentTime.js +101 -0
  49. data/vendor/assets/vis/timeline/component/CustomTime.js +255 -0
  50. data/vendor/assets/vis/timeline/component/Group.js +129 -0
  51. data/vendor/assets/vis/timeline/component/GroupSet.js +546 -0
  52. data/vendor/assets/vis/timeline/component/ItemSet.js +612 -0
  53. data/vendor/assets/vis/timeline/component/Panel.js +112 -0
  54. data/vendor/assets/vis/timeline/component/RootPanel.js +215 -0
  55. data/vendor/assets/vis/timeline/component/TimeAxis.js +522 -0
  56. data/vendor/assets/vis/timeline/component/css/currenttime.css +5 -0
  57. data/vendor/assets/vis/timeline/component/css/customtime.css +6 -0
  58. data/vendor/assets/vis/timeline/component/css/groupset.css +59 -0
  59. data/vendor/assets/vis/timeline/component/css/item.css +93 -0
  60. data/vendor/assets/vis/timeline/component/css/itemset.css +17 -0
  61. data/vendor/assets/vis/timeline/component/css/panel.css +14 -0
  62. data/vendor/assets/vis/timeline/component/css/timeaxis.css +41 -0
  63. data/vendor/assets/vis/timeline/component/css/timeline.css +2 -0
  64. data/vendor/assets/vis/timeline/component/item/Item.js +81 -0
  65. data/vendor/assets/vis/timeline/component/item/ItemBox.js +302 -0
  66. data/vendor/assets/vis/timeline/component/item/ItemPoint.js +237 -0
  67. data/vendor/assets/vis/timeline/component/item/ItemRange.js +251 -0
  68. data/vendor/assets/vis/timeline/component/item/ItemRangeOverflow.js +91 -0
  69. data/vendor/assets/vis/util.js +673 -0
  70. data/vis-rails.gemspec +47 -0
  71. metadata +142 -0
@@ -0,0 +1,2111 @@
1
+ /**
2
+ * @constructor Graph
3
+ * Create a graph visualization, displaying nodes and edges.
4
+ *
5
+ * @param {Element} container The DOM element in which the Graph will
6
+ * be created. Normally a div element.
7
+ * @param {Object} data An object containing parameters
8
+ * {Array} nodes
9
+ * {Array} edges
10
+ * @param {Object} options Options
11
+ */
12
+ function Graph (container, data, options) {
13
+ // create variables and set default values
14
+ this.containerElement = container;
15
+ this.width = '100%';
16
+ this.height = '100%';
17
+ // to give everything a nice fluidity, we seperate the rendering and calculating of the forces
18
+ this.renderRefreshRate = 60; // hz (fps)
19
+ this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
20
+ this.stabilize = true; // stabilize before displaying the graph
21
+ this.selectable = true;
22
+
23
+ this.forceFactor = 50000;
24
+
25
+ // set constant values
26
+ this.constants = {
27
+ nodes: {
28
+ radiusMin: 5,
29
+ radiusMax: 20,
30
+ radius: 5,
31
+ distance: 100, // px
32
+ shape: 'ellipse',
33
+ image: undefined,
34
+ widthMin: 16, // px
35
+ widthMax: 64, // px
36
+ fontColor: 'black',
37
+ fontSize: 14, // px
38
+ //fontFace: verdana,
39
+ fontFace: 'arial',
40
+ color: {
41
+ border: '#2B7CE9',
42
+ background: '#97C2FC',
43
+ highlight: {
44
+ border: '#2B7CE9',
45
+ background: '#D2E5FF'
46
+ }
47
+ },
48
+ borderColor: '#2B7CE9',
49
+ backgroundColor: '#97C2FC',
50
+ highlightColor: '#D2E5FF',
51
+ group: undefined
52
+ },
53
+ edges: {
54
+ widthMin: 1,
55
+ widthMax: 15,
56
+ width: 1,
57
+ style: 'line',
58
+ color: '#343434',
59
+ fontColor: '#343434',
60
+ fontSize: 14, // px
61
+ fontFace: 'arial',
62
+ //distance: 100, //px
63
+ length: 100, // px
64
+ dash: {
65
+ length: 10,
66
+ gap: 5,
67
+ altLength: undefined
68
+ }
69
+ },
70
+ clustering: { // Per Node in Cluster = PNiC
71
+ enabled: false, // (Boolean) | global on/off switch for clustering.
72
+ initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
73
+ clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
74
+ reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
75
+ chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
76
+ clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
77
+ sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
78
+ screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
79
+ fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
80
+ forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
81
+ distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
82
+ edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
83
+ nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster.
84
+ height: 10, // (px PNiC) | growth of the height per node in cluster.
85
+ radius: 10}, // (px PNiC) | growth of the radius per node in cluster.
86
+ activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
87
+ massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
88
+ },
89
+ navigation: {
90
+ enabled: false,
91
+ iconPath: this._getScriptPath() + '/img'
92
+ },
93
+ keyboard: {
94
+ enabled: false,
95
+ speed: {x: 10, y: 10, zoom: 0.02}
96
+ },
97
+ minVelocity: 2, // px/s
98
+ maxIterations: 1000 // maximum number of iteration to stabilize
99
+ };
100
+
101
+ // Node variables
102
+ this.groups = new Groups(); // object with groups
103
+ this.images = new Images(); // object with images
104
+ this.images.setOnloadCallback(function () {
105
+ graph._redraw();
106
+ });
107
+
108
+ // navigation variables
109
+ this.xIncrement = 0;
110
+ this.yIncrement = 0;
111
+ this.zoomIncrement = 0;
112
+
113
+ // create a frame and canvas
114
+ this._create();
115
+
116
+ // load the sector system. (mandatory, fully integrated with Graph)
117
+ this._loadSectorSystem();
118
+
119
+ // apply options
120
+ this.setOptions(options);
121
+
122
+ // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
123
+ this._loadClusterSystem();
124
+
125
+ // load the selection system. (mandatory, required by Graph)
126
+ this._loadSelectionSystem();
127
+
128
+ // other vars
129
+ var graph = this;
130
+ this.freezeSimulation = false;// freeze the simulation
131
+
132
+ this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
133
+ this.nodes = {}; // object with Node objects
134
+ this.edges = {}; // object with Edge objects
135
+
136
+ this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
137
+ this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
138
+
139
+ this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
140
+ this.scale = 1; // defining the global scale variable in the constructor
141
+ this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
142
+ // TODO: create a counter to keep track on the number of nodes having values
143
+ // TODO: create a counter to keep track on the number of nodes currently moving
144
+ // TODO: create a counter to keep track on the number of edges having values
145
+
146
+ this.nodesData = null; // A DataSet or DataView
147
+ this.edgesData = null; // A DataSet or DataView
148
+
149
+ // create event listeners used to subscribe on the DataSets of the nodes and edges
150
+ var me = this;
151
+ this.nodesListeners = {
152
+ 'add': function (event, params) {
153
+ me._addNodes(params.items);
154
+ me.start();
155
+ },
156
+ 'update': function (event, params) {
157
+ me._updateNodes(params.items);
158
+ me.start();
159
+ },
160
+ 'remove': function (event, params) {
161
+ me._removeNodes(params.items);
162
+ me.start();
163
+ }
164
+ };
165
+ this.edgesListeners = {
166
+ 'add': function (event, params) {
167
+ me._addEdges(params.items);
168
+ me.start();
169
+ },
170
+ 'update': function (event, params) {
171
+ me._updateEdges(params.items);
172
+ me.start();
173
+ },
174
+ 'remove': function (event, params) {
175
+ me._removeEdges(params.items);
176
+ me.start();
177
+ }
178
+ };
179
+
180
+ // properties of the data
181
+ this.moving = false; // True if any of the nodes have an undefined position
182
+ this.timer = undefined;
183
+
184
+ // load data (the disable start variable will be the same as the enabled clustering)
185
+ this.setData(data,this.constants.clustering.enabled);
186
+
187
+ // zoom so all data will fit on the screen
188
+ this.zoomToFit(true);
189
+
190
+ // if clustering is disabled, the simulation will have started in the setData function
191
+ if (this.constants.clustering.enabled) {
192
+ this.startWithClustering();
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Get the script path where the vis.js library is located
198
+ *
199
+ * @returns {string | null} path Path or null when not found. Path does not
200
+ * end with a slash.
201
+ * @private
202
+ */
203
+ Graph.prototype._getScriptPath = function() {
204
+ var scripts = document.getElementsByTagName( 'script' );
205
+
206
+ // find script named vis.js or vis.min.js
207
+ for (var i = 0; i < scripts.length; i++) {
208
+ var src = scripts[i].src;
209
+ var match = src && /\/?vis(.min)?\.js$/.exec(src);
210
+ if (match) {
211
+ // return path without the script name
212
+ return src.substring(0, src.length - match[0].length);
213
+ }
214
+ }
215
+
216
+ return null;
217
+ };
218
+
219
+
220
+ /**
221
+ * Find the center position of the graph
222
+ * @private
223
+ */
224
+ Graph.prototype._getRange = function() {
225
+ var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
226
+ for (var i = 0; i < this.nodeIndices.length; i++) {
227
+ node = this.nodes[this.nodeIndices[i]];
228
+ if (minX > (node.x - node.width)) {minX = node.x - node.width;}
229
+ if (maxX < (node.x + node.width)) {maxX = node.x + node.width;}
230
+ if (minY > (node.y - node.height)) {minY = node.y - node.height;}
231
+ if (maxY < (node.y + node.height)) {maxY = node.y + node.height;}
232
+ }
233
+ return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
234
+ };
235
+
236
+
237
+ /**
238
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
239
+ * @returns {{x: number, y: number}}
240
+ * @private
241
+ */
242
+ Graph.prototype._findCenter = function(range) {
243
+ var center = {x: (0.5 * (range.maxX + range.minX)),
244
+ y: (0.5 * (range.maxY + range.minY))};
245
+ return center;
246
+ };
247
+
248
+
249
+ /**
250
+ * center the graph
251
+ *
252
+ * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
253
+ */
254
+ Graph.prototype._centerGraph = function(range) {
255
+ var center = this._findCenter(range);
256
+
257
+ center.x *= this.scale;
258
+ center.y *= this.scale;
259
+ center.x -= 0.5 * this.frame.canvas.clientWidth;
260
+ center.y -= 0.5 * this.frame.canvas.clientHeight;
261
+
262
+ this._setTranslation(-center.x,-center.y); // set at 0,0
263
+ };
264
+
265
+
266
+ /**
267
+ * This function zooms out to fit all data on screen based on amount of nodes
268
+ *
269
+ * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
270
+ */
271
+ Graph.prototype.zoomToFit = function(initialZoom) {
272
+ if (initialZoom === undefined) {
273
+ initialZoom = false;
274
+ }
275
+
276
+ var numberOfNodes = this.nodeIndices.length;
277
+ var range = this._getRange();
278
+
279
+ if (initialZoom == true) {
280
+ if (this.constants.clustering.enabled == true &&
281
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
282
+ var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
283
+ }
284
+ else {
285
+ var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
286
+ }
287
+ }
288
+ else {
289
+ var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
290
+ var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
291
+
292
+ var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
293
+ var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
294
+
295
+ zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
296
+ }
297
+
298
+ if (zoomLevel > 1.0) {
299
+ zoomLevel = 1.0;
300
+ }
301
+
302
+ this.pinch.mousewheelScale = zoomLevel;
303
+ this._setScale(zoomLevel);
304
+ this._centerGraph(range);
305
+ this.start();
306
+ };
307
+
308
+
309
+ /**
310
+ * Update the this.nodeIndices with the most recent node index list
311
+ * @private
312
+ */
313
+ Graph.prototype._updateNodeIndexList = function() {
314
+ this._clearNodeIndexList();
315
+ for (var idx in this.nodes) {
316
+ if (this.nodes.hasOwnProperty(idx)) {
317
+ this.nodeIndices.push(idx);
318
+ }
319
+ }
320
+ };
321
+
322
+
323
+ /**
324
+ * Set nodes and edges, and optionally options as well.
325
+ *
326
+ * @param {Object} data Object containing parameters:
327
+ * {Array | DataSet | DataView} [nodes] Array with nodes
328
+ * {Array | DataSet | DataView} [edges] Array with edges
329
+ * {String} [dot] String containing data in DOT format
330
+ * {Options} [options] Object with options
331
+ * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
332
+ */
333
+ Graph.prototype.setData = function(data, disableStart) {
334
+ if (disableStart === undefined) {
335
+ disableStart = false;
336
+ }
337
+
338
+ if (data && data.dot && (data.nodes || data.edges)) {
339
+ throw new SyntaxError('Data must contain either parameter "dot" or ' +
340
+ ' parameter pair "nodes" and "edges", but not both.');
341
+ }
342
+
343
+ // set options
344
+ this.setOptions(data && data.options);
345
+
346
+ // set all data
347
+ if (data && data.dot) {
348
+ // parse DOT file
349
+ if(data && data.dot) {
350
+ var dotData = vis.util.DOTToGraph(data.dot);
351
+ this.setData(dotData);
352
+ return;
353
+ }
354
+ }
355
+ else {
356
+ this._setNodes(data && data.nodes);
357
+ this._setEdges(data && data.edges);
358
+ }
359
+
360
+ this._putDataInSector();
361
+
362
+ if (!disableStart) {
363
+ // find a stable position or start animating to a stable position
364
+ if (this.stabilize) {
365
+ this._doStabilize();
366
+ }
367
+ this.moving = true;
368
+ this.start();
369
+ }
370
+ };
371
+
372
+ /**
373
+ * Set options
374
+ * @param {Object} options
375
+ */
376
+ Graph.prototype.setOptions = function (options) {
377
+ if (options) {
378
+ // retrieve parameter values
379
+ if (options.width !== undefined) {this.width = options.width;}
380
+ if (options.height !== undefined) {this.height = options.height;}
381
+ if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
382
+ if (options.selectable !== undefined) {this.selectable = options.selectable;}
383
+
384
+ if (options.clustering) {
385
+ this.constants.clustering.enabled = true;
386
+ for (var prop in options.clustering) {
387
+ if (options.clustering.hasOwnProperty(prop)) {
388
+ this.constants.clustering[prop] = options.clustering[prop];
389
+ }
390
+ }
391
+ }
392
+ else if (options.clustering !== undefined) {
393
+ this.constants.clustering.enabled = false;
394
+ }
395
+
396
+ if (options.navigation) {
397
+ this.constants.navigation.enabled = true;
398
+ for (var prop in options.navigation) {
399
+ if (options.navigation.hasOwnProperty(prop)) {
400
+ this.constants.navigation[prop] = options.navigation[prop];
401
+ }
402
+ }
403
+ }
404
+ else if (options.navigation !== undefined) {
405
+ this.constants.navigation.enabled = false;
406
+ }
407
+
408
+ if (options.keyboard) {
409
+ this.constants.keyboard.enabled = true;
410
+ for (var prop in options.keyboard) {
411
+ if (options.keyboard.hasOwnProperty(prop)) {
412
+ this.constants.keyboard[prop] = options.keyboard[prop];
413
+ }
414
+ }
415
+ }
416
+ else if (options.keyboard !== undefined) {
417
+ this.constants.keyboard.enabled = false;
418
+ }
419
+
420
+
421
+ // TODO: work out these options and document them
422
+ if (options.edges) {
423
+ for (prop in options.edges) {
424
+ if (options.edges.hasOwnProperty(prop)) {
425
+ this.constants.edges[prop] = options.edges[prop];
426
+ }
427
+ }
428
+
429
+ if (options.edges.length !== undefined &&
430
+ options.nodes && options.nodes.distance === undefined) {
431
+ this.constants.edges.length = options.edges.length;
432
+ this.constants.nodes.distance = options.edges.length * 1.25;
433
+ }
434
+
435
+ if (!options.edges.fontColor) {
436
+ this.constants.edges.fontColor = options.edges.color;
437
+ }
438
+
439
+ // Added to support dashed lines
440
+ // David Jordan
441
+ // 2012-08-08
442
+ if (options.edges.dash) {
443
+ if (options.edges.dash.length !== undefined) {
444
+ this.constants.edges.dash.length = options.edges.dash.length;
445
+ }
446
+ if (options.edges.dash.gap !== undefined) {
447
+ this.constants.edges.dash.gap = options.edges.dash.gap;
448
+ }
449
+ if (options.edges.dash.altLength !== undefined) {
450
+ this.constants.edges.dash.altLength = options.edges.dash.altLength;
451
+ }
452
+ }
453
+ }
454
+
455
+ if (options.nodes) {
456
+ for (prop in options.nodes) {
457
+ if (options.nodes.hasOwnProperty(prop)) {
458
+ this.constants.nodes[prop] = options.nodes[prop];
459
+ }
460
+ }
461
+
462
+ if (options.nodes.color) {
463
+ this.constants.nodes.color = Node.parseColor(options.nodes.color);
464
+ }
465
+
466
+ /*
467
+ if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
468
+ if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
469
+ */
470
+ }
471
+ if (options.groups) {
472
+ for (var groupname in options.groups) {
473
+ if (options.groups.hasOwnProperty(groupname)) {
474
+ var group = options.groups[groupname];
475
+ this.groups.add(groupname, group);
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ this.setSize(this.width, this.height);
482
+ this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
483
+ this._setScale(1);
484
+
485
+ // load the navigation system.
486
+ this._loadNavigationControls();
487
+
488
+ // bind keys. If disabled, this will not do anything;
489
+ this._createKeyBinds();
490
+
491
+ this._redraw();
492
+ };
493
+
494
+ /**
495
+ * Add event listener
496
+ * @param {String} event Event name. Available events:
497
+ * 'select'
498
+ * @param {function} callback Callback function, invoked as callback(properties)
499
+ * where properties is an optional object containing
500
+ * event specific properties.
501
+ */
502
+ Graph.prototype.on = function on (event, callback) {
503
+ var available = ['select'];
504
+
505
+ if (available.indexOf(event) == -1) {
506
+ throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
507
+ }
508
+
509
+ events.addListener(this, event, callback);
510
+ };
511
+
512
+ /**
513
+ * Remove an event listener
514
+ * @param {String} event Event name
515
+ * @param {function} callback Callback function
516
+ */
517
+ Graph.prototype.off = function off (event, callback) {
518
+ events.removeListener(this, event, callback);
519
+ };
520
+
521
+ /**
522
+ * fire an event
523
+ * @param {String} event The name of an event, for example 'select'
524
+ * @param {Object} params Optional object with event parameters
525
+ * @private
526
+ */
527
+ Graph.prototype._trigger = function (event, params) {
528
+ events.trigger(this, event, params);
529
+ };
530
+
531
+
532
+ /**
533
+ * Create the main frame for the Graph.
534
+ * This function is executed once when a Graph object is created. The frame
535
+ * contains a canvas, and this canvas contains all objects like the axis and
536
+ * nodes.
537
+ * @private
538
+ */
539
+ Graph.prototype._create = function () {
540
+ // remove all elements from the container element.
541
+ while (this.containerElement.hasChildNodes()) {
542
+ this.containerElement.removeChild(this.containerElement.firstChild);
543
+ }
544
+
545
+ this.frame = document.createElement('div');
546
+ this.frame.className = 'graph-frame';
547
+ this.frame.style.position = 'relative';
548
+ this.frame.style.overflow = 'hidden';
549
+
550
+ // create the graph canvas (HTML canvas element)
551
+ this.frame.canvas = document.createElement( 'canvas' );
552
+ this.frame.canvas.style.position = 'relative';
553
+ this.frame.appendChild(this.frame.canvas);
554
+ if (!this.frame.canvas.getContext) {
555
+ var noCanvas = document.createElement( 'DIV' );
556
+ noCanvas.style.color = 'red';
557
+ noCanvas.style.fontWeight = 'bold' ;
558
+ noCanvas.style.padding = '10px';
559
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
560
+ this.frame.canvas.appendChild(noCanvas);
561
+ }
562
+
563
+ var me = this;
564
+ this.drag = {};
565
+ this.pinch = {};
566
+ this.hammer = Hammer(this.frame.canvas, {
567
+ prevent_default: true
568
+ });
569
+ this.hammer.on('tap', me._onTap.bind(me) );
570
+ this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
571
+ this.hammer.on('hold', me._onHold.bind(me) );
572
+ this.hammer.on('pinch', me._onPinch.bind(me) );
573
+ this.hammer.on('touch', me._onTouch.bind(me) );
574
+ this.hammer.on('dragstart', me._onDragStart.bind(me) );
575
+ this.hammer.on('drag', me._onDrag.bind(me) );
576
+ this.hammer.on('dragend', me._onDragEnd.bind(me) );
577
+ this.hammer.on('release', me._onRelease.bind(me) );
578
+ this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
579
+ this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
580
+ this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
581
+
582
+ // add the frame to the container element
583
+ this.containerElement.appendChild(this.frame);
584
+ };
585
+
586
+
587
+ /**
588
+ * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
589
+ * @private
590
+ */
591
+ Graph.prototype._createKeyBinds = function() {
592
+ var me = this;
593
+ this.mousetrap = mousetrap;
594
+
595
+ this.mousetrap.reset();
596
+
597
+ if (this.constants.keyboard.enabled == true) {
598
+ this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
599
+ this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
600
+ this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
601
+ this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
602
+ this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
603
+ this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
604
+ this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
605
+ this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
606
+ this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
607
+ this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
608
+ this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
609
+ this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
610
+ this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
611
+ this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
612
+ this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
613
+ this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
614
+ this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
615
+ this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
616
+ this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
617
+ this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
618
+ }
619
+ /*
620
+ this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me));
621
+ this.mousetrap.bind("-",this.increaseClusterLevel.bind(me));
622
+ this.mousetrap.bind("s",this.singleStep.bind(me));
623
+ this.mousetrap.bind("h",this.updateClustersDefault.bind(me));
624
+ this.mousetrap.bind("c",this._collapseSector.bind(me));
625
+ this.mousetrap.bind("f",this.toggleFreeze.bind(me));
626
+ */
627
+ }
628
+
629
+ /**
630
+ * Get the pointer location from a touch location
631
+ * @param {{pageX: Number, pageY: Number}} touch
632
+ * @return {{x: Number, y: Number}} pointer
633
+ * @private
634
+ */
635
+ Graph.prototype._getPointer = function (touch) {
636
+ return {
637
+ x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
638
+ y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
639
+ };
640
+ };
641
+
642
+ /**
643
+ * On start of a touch gesture, store the pointer
644
+ * @param event
645
+ * @private
646
+ */
647
+ Graph.prototype._onTouch = function (event) {
648
+ this.drag.pointer = this._getPointer(event.gesture.touches[0]);
649
+ this.drag.pinched = false;
650
+ this.pinch.scale = this._getScale();
651
+
652
+ this._handleTouch(this.drag.pointer);
653
+ };
654
+
655
+ /**
656
+ * handle drag start event
657
+ * @private
658
+ */
659
+ Graph.prototype._onDragStart = function () {
660
+ var drag = this.drag;
661
+ var node = this._getNodeAt(drag.pointer);
662
+ // note: drag.pointer is set in _onTouch to get the initial touch location
663
+
664
+ drag.dragging = true;
665
+ drag.selection = [];
666
+ drag.translation = this._getTranslation();
667
+ drag.nodeId = null;
668
+
669
+ if (node != null) {
670
+ drag.nodeId = node.id;
671
+ // select the clicked node if not yet selected
672
+ if (!node.isSelected()) {
673
+ this._selectNode(node,false);
674
+ }
675
+
676
+ // create an array with the selected nodes and their original location and status
677
+ var me = this;
678
+ this.selection.forEach(function (id) {
679
+ var node = me.nodes[id];
680
+ if (node) {
681
+ var s = {
682
+ id: id,
683
+ node: node,
684
+
685
+ // store original x, y, xFixed and yFixed, make the node temporarily Fixed
686
+ x: node.x,
687
+ y: node.y,
688
+ xFixed: node.xFixed,
689
+ yFixed: node.yFixed
690
+ };
691
+
692
+ node.xFixed = true;
693
+ node.yFixed = true;
694
+
695
+ drag.selection.push(s);
696
+ }
697
+ });
698
+ }
699
+ };
700
+
701
+ /**
702
+ * handle drag event
703
+ * @private
704
+ */
705
+ Graph.prototype._onDrag = function (event) {
706
+ if (this.drag.pinched) {
707
+ return;
708
+ }
709
+
710
+ var pointer = this._getPointer(event.gesture.touches[0]);
711
+
712
+ var me = this,
713
+ drag = this.drag,
714
+ selection = drag.selection;
715
+ if (selection && selection.length) {
716
+ // calculate delta's and new location
717
+ var deltaX = pointer.x - drag.pointer.x,
718
+ deltaY = pointer.y - drag.pointer.y;
719
+
720
+ // update position of all selected nodes
721
+ selection.forEach(function (s) {
722
+ var node = s.node;
723
+
724
+ if (!s.xFixed) {
725
+ node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
726
+ }
727
+
728
+ if (!s.yFixed) {
729
+ node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
730
+ }
731
+ });
732
+
733
+ // start animation if not yet running
734
+ if (!this.moving) {
735
+ this.moving = true;
736
+ this.start();
737
+ }
738
+ }
739
+ else {
740
+ // move the graph
741
+ var diffX = pointer.x - this.drag.pointer.x;
742
+ var diffY = pointer.y - this.drag.pointer.y;
743
+
744
+ this._setTranslation(
745
+ this.drag.translation.x + diffX,
746
+ this.drag.translation.y + diffY);
747
+ this._redraw();
748
+ this.moved = true;
749
+ }
750
+ };
751
+
752
+ /**
753
+ * handle drag start event
754
+ * @private
755
+ */
756
+ Graph.prototype._onDragEnd = function () {
757
+ this.drag.dragging = false;
758
+ var selection = this.drag.selection;
759
+ if (selection) {
760
+ selection.forEach(function (s) {
761
+ // restore original xFixed and yFixed
762
+ s.node.xFixed = s.xFixed;
763
+ s.node.yFixed = s.yFixed;
764
+ });
765
+ }
766
+ };
767
+
768
+ /**
769
+ * handle tap/click event: select/unselect a node
770
+ * @private
771
+ */
772
+ Graph.prototype._onTap = function (event) {
773
+ var pointer = this._getPointer(event.gesture.touches[0]);
774
+ this._handleTap(pointer);
775
+ };
776
+
777
+
778
+ /**
779
+ * handle doubletap event
780
+ * @private
781
+ */
782
+ Graph.prototype._onDoubleTap = function (event) {
783
+ var pointer = this._getPointer(event.gesture.touches[0]);
784
+ this._handleDoubleTap(pointer);
785
+
786
+ };
787
+
788
+
789
+ /**
790
+ * handle long tap event: multi select nodes
791
+ * @private
792
+ */
793
+ Graph.prototype._onHold = function (event) {
794
+ var pointer = this._getPointer(event.gesture.touches[0]);
795
+ this._handleOnHold(pointer);
796
+ };
797
+
798
+ /**
799
+ * handle the release of the screen
800
+ *
801
+ * @param event
802
+ * @private
803
+ */
804
+ Graph.prototype._onRelease = function (event) {
805
+ this._handleOnRelease();
806
+ };
807
+
808
+ /**
809
+ * Handle pinch event
810
+ * @param event
811
+ * @private
812
+ */
813
+ Graph.prototype._onPinch = function (event) {
814
+ var pointer = this._getPointer(event.gesture.center);
815
+
816
+ this.drag.pinched = true;
817
+ if (!('scale' in this.pinch)) {
818
+ this.pinch.scale = 1;
819
+ }
820
+
821
+ // TODO: enabled moving while pinching?
822
+ var scale = this.pinch.scale * event.gesture.scale;
823
+ this._zoom(scale, pointer)
824
+ };
825
+
826
+ /**
827
+ * Zoom the graph in or out
828
+ * @param {Number} scale a number around 1, and between 0.01 and 10
829
+ * @param {{x: Number, y: Number}} pointer Position on screen
830
+ * @return {Number} appliedScale scale is limited within the boundaries
831
+ * @private
832
+ */
833
+ Graph.prototype._zoom = function(scale, pointer) {
834
+ var scaleOld = this._getScale();
835
+ if (scale < 0.00001) {
836
+ scale = 0.00001;
837
+ }
838
+ if (scale > 10) {
839
+ scale = 10;
840
+ }
841
+ // + this.frame.canvas.clientHeight / 2
842
+ var translation = this._getTranslation();
843
+
844
+ var scaleFrac = scale / scaleOld;
845
+ var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
846
+ var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
847
+
848
+ this.areaCenter = {"x" : this._canvasToX(pointer.x),
849
+ "y" : this._canvasToY(pointer.y)};
850
+
851
+ // this.areaCenter = {"x" : pointer.x,"y" : pointer.y };
852
+ // console.log(translation.x,translation.y,pointer.x,pointer.y,scale);
853
+ this.pinch.mousewheelScale = scale;
854
+ this._setScale(scale);
855
+ this._setTranslation(tx, ty);
856
+ this.updateClustersDefault();
857
+ this._redraw();
858
+
859
+ return scale;
860
+ };
861
+
862
+ /**
863
+ * Event handler for mouse wheel event, used to zoom the timeline
864
+ * See http://adomas.org/javascript-mouse-wheel/
865
+ * https://github.com/EightMedia/hammer.js/issues/256
866
+ * @param {MouseEvent} event
867
+ * @private
868
+ */
869
+ Graph.prototype._onMouseWheel = function(event) {
870
+ // retrieve delta
871
+ var delta = 0;
872
+ if (event.wheelDelta) { /* IE/Opera. */
873
+ delta = event.wheelDelta/120;
874
+ } else if (event.detail) { /* Mozilla case. */
875
+ // In Mozilla, sign of delta is different than in IE.
876
+ // Also, delta is multiple of 3.
877
+ delta = -event.detail/3;
878
+ }
879
+
880
+ // If delta is nonzero, handle it.
881
+ // Basically, delta is now positive if wheel was scrolled up,
882
+ // and negative, if wheel was scrolled down.
883
+ if (delta) {
884
+ if (!('mousewheelScale' in this.pinch)) {
885
+ this.pinch.mousewheelScale = 1;
886
+ }
887
+
888
+ // calculate the new scale
889
+ var scale = this.pinch.mousewheelScale;
890
+ var zoom = delta / 10;
891
+ if (delta < 0) {
892
+ zoom = zoom / (1 - zoom);
893
+ }
894
+ scale *= (1 + zoom);
895
+
896
+ // calculate the pointer location
897
+ var gesture = util.fakeGesture(this, event);
898
+ var pointer = this._getPointer(gesture.center);
899
+
900
+ // apply the new scale
901
+ scale = this._zoom(scale, pointer);
902
+
903
+ // store the new, applied scale -- this is now done in _zoom
904
+ // this.pinch.mousewheelScale = scale;
905
+ }
906
+
907
+ // Prevent default actions caused by mouse wheel.
908
+ event.preventDefault();
909
+ };
910
+
911
+
912
+ /**
913
+ * Mouse move handler for checking whether the title moves over a node with a title.
914
+ * @param {Event} event
915
+ * @private
916
+ */
917
+ Graph.prototype._onMouseMoveTitle = function (event) {
918
+ var gesture = util.fakeGesture(this, event);
919
+ var pointer = this._getPointer(gesture.center);
920
+
921
+ // check if the previously selected node is still selected
922
+ if (this.popupNode) {
923
+ this._checkHidePopup(pointer);
924
+ }
925
+
926
+ // start a timeout that will check if the mouse is positioned above
927
+ // an element
928
+ var me = this;
929
+ var checkShow = function() {
930
+ me._checkShowPopup(pointer);
931
+ };
932
+ if (this.popupTimer) {
933
+ clearInterval(this.popupTimer); // stop any running calculationTimer
934
+ }
935
+ if (!this.drag.dragging) {
936
+ this.popupTimer = setTimeout(checkShow, 300);
937
+ }
938
+ };
939
+
940
+ /**
941
+ * Check if there is an element on the given position in the graph
942
+ * (a node or edge). If so, and if this element has a title,
943
+ * show a popup window with its title.
944
+ *
945
+ * @param {{x:Number, y:Number}} pointer
946
+ * @private
947
+ */
948
+ Graph.prototype._checkShowPopup = function (pointer) {
949
+ var obj = {
950
+ left: this._canvasToX(pointer.x),
951
+ top: this._canvasToY(pointer.y),
952
+ right: this._canvasToX(pointer.x),
953
+ bottom: this._canvasToY(pointer.y)
954
+ };
955
+
956
+ var id;
957
+ var lastPopupNode = this.popupNode;
958
+
959
+ if (this.popupNode == undefined) {
960
+ // search the nodes for overlap, select the top one in case of multiple nodes
961
+ var nodes = this.nodes;
962
+ for (id in nodes) {
963
+ if (nodes.hasOwnProperty(id)) {
964
+ var node = nodes[id];
965
+ if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
966
+ this.popupNode = node;
967
+ break;
968
+ }
969
+ }
970
+ }
971
+ }
972
+
973
+ if (this.popupNode === undefined) {
974
+ // search the edges for overlap
975
+ var edges = this.edges;
976
+ for (id in edges) {
977
+ if (edges.hasOwnProperty(id)) {
978
+ var edge = edges[id];
979
+ if (edge.connected && (edge.getTitle() !== undefined) &&
980
+ edge.isOverlappingWith(obj)) {
981
+ this.popupNode = edge;
982
+ break;
983
+ }
984
+ }
985
+ }
986
+ }
987
+
988
+ if (this.popupNode) {
989
+ // show popup message window
990
+ if (this.popupNode != lastPopupNode) {
991
+ var me = this;
992
+ if (!me.popup) {
993
+ me.popup = new Popup(me.frame);
994
+ }
995
+
996
+ // adjust a small offset such that the mouse cursor is located in the
997
+ // bottom left location of the popup, and you can easily move over the
998
+ // popup area
999
+ me.popup.setPosition(pointer.x - 3, pointer.y - 3);
1000
+ me.popup.setText(me.popupNode.getTitle());
1001
+ me.popup.show();
1002
+ }
1003
+ }
1004
+ else {
1005
+ if (this.popup) {
1006
+ this.popup.hide();
1007
+ }
1008
+ }
1009
+ };
1010
+
1011
+ /**
1012
+ * Check if the popup must be hided, which is the case when the mouse is no
1013
+ * longer hovering on the object
1014
+ * @param {{x:Number, y:Number}} pointer
1015
+ * @private
1016
+ */
1017
+ Graph.prototype._checkHidePopup = function (pointer) {
1018
+ if (!this.popupNode || !this._getNodeAt(pointer) ) {
1019
+ this.popupNode = undefined;
1020
+ if (this.popup) {
1021
+ this.popup.hide();
1022
+ }
1023
+ }
1024
+ };
1025
+
1026
+
1027
+ /**
1028
+ * Temporary method to test calculating a hub value for the nodes
1029
+ * @param {number} level Maximum number edges between two nodes in order
1030
+ * to call them connected. Optional, 1 by default
1031
+ * @return {Number[]} connectioncount array with the connection count
1032
+ * for each node
1033
+ * @private
1034
+ */
1035
+ Graph.prototype._getConnectionCount = function(level) {
1036
+ if (level == undefined) {
1037
+ level = 1;
1038
+ }
1039
+
1040
+ // get the nodes connected to given nodes
1041
+ function getConnectedNodes(nodes) {
1042
+ var connectedNodes = [];
1043
+
1044
+ for (var j = 0, jMax = nodes.length; j < jMax; j++) {
1045
+ var node = nodes[j];
1046
+
1047
+ // find all nodes connected to this node
1048
+ var edges = node.edges;
1049
+ for (var i = 0, iMax = edges.length; i < iMax; i++) {
1050
+ var edge = edges[i];
1051
+ var other = null;
1052
+
1053
+ // check if connected
1054
+ if (edge.from == node)
1055
+ other = edge.to;
1056
+ else if (edge.to == node)
1057
+ other = edge.from;
1058
+
1059
+ // check if the other node is not already in the list with nodes
1060
+ var k, kMax;
1061
+ if (other) {
1062
+ for (k = 0, kMax = nodes.length; k < kMax; k++) {
1063
+ if (nodes[k] == other) {
1064
+ other = null;
1065
+ break;
1066
+ }
1067
+ }
1068
+ }
1069
+ if (other) {
1070
+ for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
1071
+ if (connectedNodes[k] == other) {
1072
+ other = null;
1073
+ break;
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ if (other)
1079
+ connectedNodes.push(other);
1080
+ }
1081
+ }
1082
+
1083
+ return connectedNodes;
1084
+ }
1085
+
1086
+ var connections = [];
1087
+ var nodes = this.nodes;
1088
+ for (var id in nodes) {
1089
+ if (nodes.hasOwnProperty(id)) {
1090
+ var c = [nodes[id]];
1091
+ for (var l = 0; l < level; l++) {
1092
+ c = c.concat(getConnectedNodes(c));
1093
+ }
1094
+ connections.push(c);
1095
+ }
1096
+ }
1097
+
1098
+ var hubs = [];
1099
+ for (var i = 0, len = connections.length; i < len; i++) {
1100
+ hubs.push(connections[i].length);
1101
+ }
1102
+
1103
+ return hubs;
1104
+ };
1105
+
1106
+ /**
1107
+ * Set a new size for the graph
1108
+ * @param {string} width Width in pixels or percentage (for example '800px'
1109
+ * or '50%')
1110
+ * @param {string} height Height in pixels or percentage (for example '400px'
1111
+ * or '30%')
1112
+ */
1113
+ Graph.prototype.setSize = function(width, height) {
1114
+ this.frame.style.width = width;
1115
+ this.frame.style.height = height;
1116
+
1117
+ this.frame.canvas.style.width = '100%';
1118
+ this.frame.canvas.style.height = '100%';
1119
+
1120
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
1121
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
1122
+
1123
+ if (this.constants.navigation.enabled == true) {
1124
+ this._relocateNavigation();
1125
+ }
1126
+ };
1127
+
1128
+ /**
1129
+ * Set a data set with nodes for the graph
1130
+ * @param {Array | DataSet | DataView} nodes The data containing the nodes.
1131
+ * @private
1132
+ */
1133
+ Graph.prototype._setNodes = function(nodes) {
1134
+ var oldNodesData = this.nodesData;
1135
+
1136
+ if (nodes instanceof DataSet || nodes instanceof DataView) {
1137
+ this.nodesData = nodes;
1138
+ }
1139
+ else if (nodes instanceof Array) {
1140
+ this.nodesData = new DataSet();
1141
+ this.nodesData.add(nodes);
1142
+ }
1143
+ else if (!nodes) {
1144
+ this.nodesData = new DataSet();
1145
+ }
1146
+ else {
1147
+ throw new TypeError('Array or DataSet expected');
1148
+ }
1149
+
1150
+ if (oldNodesData) {
1151
+ // unsubscribe from old dataset
1152
+ util.forEach(this.nodesListeners, function (callback, event) {
1153
+ oldNodesData.unsubscribe(event, callback);
1154
+ });
1155
+ }
1156
+
1157
+ // remove drawn nodes
1158
+ this.nodes = {};
1159
+
1160
+ if (this.nodesData) {
1161
+ // subscribe to new dataset
1162
+ var me = this;
1163
+ util.forEach(this.nodesListeners, function (callback, event) {
1164
+ me.nodesData.subscribe(event, callback);
1165
+ });
1166
+
1167
+ // draw all new nodes
1168
+ var ids = this.nodesData.getIds();
1169
+ this._addNodes(ids);
1170
+ }
1171
+ this._updateSelection();
1172
+ };
1173
+
1174
+ /**
1175
+ * Add nodes
1176
+ * @param {Number[] | String[]} ids
1177
+ * @private
1178
+ */
1179
+ Graph.prototype._addNodes = function(ids) {
1180
+ var id;
1181
+ for (var i = 0, len = ids.length; i < len; i++) {
1182
+ id = ids[i];
1183
+ var data = this.nodesData.get(id);
1184
+ var node = new Node(data, this.images, this.groups, this.constants);
1185
+ this.nodes[id] = node; // note: this may replace an existing node
1186
+
1187
+ if (!node.isFixed()) {
1188
+ // TODO: position new nodes in a smarter way!
1189
+ var radius = this.constants.edges.length * 2;
1190
+ var count = ids.length;
1191
+ var angle = 2 * Math.PI * (i / count);
1192
+ node.x = radius * Math.cos(angle);
1193
+ node.y = radius * Math.sin(angle);
1194
+
1195
+ // note: no not use node.isMoving() here, as that gives the current
1196
+ // velocity of the node, which is zero after creation of the node.
1197
+ this.moving = true;
1198
+ }
1199
+ }
1200
+ this._updateNodeIndexList();
1201
+ this._reconnectEdges();
1202
+ this._updateValueRange(this.nodes);
1203
+ };
1204
+
1205
+ /**
1206
+ * Update existing nodes, or create them when not yet existing
1207
+ * @param {Number[] | String[]} ids
1208
+ * @private
1209
+ */
1210
+ Graph.prototype._updateNodes = function(ids) {
1211
+ var nodes = this.nodes,
1212
+ nodesData = this.nodesData;
1213
+ for (var i = 0, len = ids.length; i < len; i++) {
1214
+ var id = ids[i];
1215
+ var node = nodes[id];
1216
+ var data = nodesData.get(id);
1217
+ if (node) {
1218
+ // update node
1219
+ node.setProperties(data, this.constants);
1220
+ }
1221
+ else {
1222
+ // create node
1223
+ node = new Node(properties, this.images, this.groups, this.constants);
1224
+ nodes[id] = node;
1225
+
1226
+ if (!node.isFixed()) {
1227
+ this.moving = true;
1228
+ }
1229
+ }
1230
+ }
1231
+ this._updateNodeIndexList();
1232
+ this._reconnectEdges();
1233
+ this._updateValueRange(nodes);
1234
+ };
1235
+
1236
+ /**
1237
+ * Remove existing nodes. If nodes do not exist, the method will just ignore it.
1238
+ * @param {Number[] | String[]} ids
1239
+ * @private
1240
+ */
1241
+ Graph.prototype._removeNodes = function(ids) {
1242
+ var nodes = this.nodes;
1243
+ for (var i = 0, len = ids.length; i < len; i++) {
1244
+ var id = ids[i];
1245
+ delete nodes[id];
1246
+ }
1247
+ this._updateNodeIndexList();
1248
+ this._reconnectEdges();
1249
+ this._updateSelection();
1250
+ this._updateValueRange(nodes);
1251
+ };
1252
+
1253
+ /**
1254
+ * Load edges by reading the data table
1255
+ * @param {Array | DataSet | DataView} edges The data containing the edges.
1256
+ * @private
1257
+ * @private
1258
+ */
1259
+ Graph.prototype._setEdges = function(edges) {
1260
+ var oldEdgesData = this.edgesData;
1261
+
1262
+ if (edges instanceof DataSet || edges instanceof DataView) {
1263
+ this.edgesData = edges;
1264
+ }
1265
+ else if (edges instanceof Array) {
1266
+ this.edgesData = new DataSet();
1267
+ this.edgesData.add(edges);
1268
+ }
1269
+ else if (!edges) {
1270
+ this.edgesData = new DataSet();
1271
+ }
1272
+ else {
1273
+ throw new TypeError('Array or DataSet expected');
1274
+ }
1275
+
1276
+ if (oldEdgesData) {
1277
+ // unsubscribe from old dataset
1278
+ util.forEach(this.edgesListeners, function (callback, event) {
1279
+ oldEdgesData.unsubscribe(event, callback);
1280
+ });
1281
+ }
1282
+
1283
+ // remove drawn edges
1284
+ this.edges = {};
1285
+
1286
+ if (this.edgesData) {
1287
+ // subscribe to new dataset
1288
+ var me = this;
1289
+ util.forEach(this.edgesListeners, function (callback, event) {
1290
+ me.edgesData.subscribe(event, callback);
1291
+ });
1292
+
1293
+ // draw all new nodes
1294
+ var ids = this.edgesData.getIds();
1295
+ this._addEdges(ids);
1296
+ }
1297
+
1298
+ this._reconnectEdges();
1299
+ };
1300
+
1301
+ /**
1302
+ * Add edges
1303
+ * @param {Number[] | String[]} ids
1304
+ * @private
1305
+ */
1306
+ Graph.prototype._addEdges = function (ids) {
1307
+ var edges = this.edges,
1308
+ edgesData = this.edgesData;
1309
+
1310
+ for (var i = 0, len = ids.length; i < len; i++) {
1311
+ var id = ids[i];
1312
+
1313
+ var oldEdge = edges[id];
1314
+ if (oldEdge) {
1315
+ oldEdge.disconnect();
1316
+ }
1317
+
1318
+ var data = edgesData.get(id, {"showInternalIds" : true});
1319
+ edges[id] = new Edge(data, this, this.constants);
1320
+ }
1321
+
1322
+ this.moving = true;
1323
+ this._updateValueRange(edges);
1324
+ };
1325
+
1326
+ /**
1327
+ * Update existing edges, or create them when not yet existing
1328
+ * @param {Number[] | String[]} ids
1329
+ * @private
1330
+ */
1331
+ Graph.prototype._updateEdges = function (ids) {
1332
+ var edges = this.edges,
1333
+ edgesData = this.edgesData;
1334
+ for (var i = 0, len = ids.length; i < len; i++) {
1335
+ var id = ids[i];
1336
+
1337
+ var data = edgesData.get(id);
1338
+ var edge = edges[id];
1339
+ if (edge) {
1340
+ // update edge
1341
+ edge.disconnect();
1342
+ edge.setProperties(data, this.constants);
1343
+ edge.connect();
1344
+ }
1345
+ else {
1346
+ // create edge
1347
+ edge = new Edge(data, this, this.constants);
1348
+ this.edges[id] = edge;
1349
+ }
1350
+ }
1351
+
1352
+ this.moving = true;
1353
+ this._updateValueRange(edges);
1354
+ };
1355
+
1356
+ /**
1357
+ * Remove existing edges. Non existing ids will be ignored
1358
+ * @param {Number[] | String[]} ids
1359
+ * @private
1360
+ */
1361
+ Graph.prototype._removeEdges = function (ids) {
1362
+ var edges = this.edges;
1363
+ for (var i = 0, len = ids.length; i < len; i++) {
1364
+ var id = ids[i];
1365
+ var edge = edges[id];
1366
+ if (edge) {
1367
+ edge.disconnect();
1368
+ delete edges[id];
1369
+ }
1370
+ }
1371
+
1372
+ this.moving = true;
1373
+ this._updateValueRange(edges);
1374
+ };
1375
+
1376
+ /**
1377
+ * Reconnect all edges
1378
+ * @private
1379
+ */
1380
+ Graph.prototype._reconnectEdges = function() {
1381
+ var id,
1382
+ nodes = this.nodes,
1383
+ edges = this.edges;
1384
+ for (id in nodes) {
1385
+ if (nodes.hasOwnProperty(id)) {
1386
+ nodes[id].edges = [];
1387
+ }
1388
+ }
1389
+
1390
+ for (id in edges) {
1391
+ if (edges.hasOwnProperty(id)) {
1392
+ var edge = edges[id];
1393
+ edge.from = null;
1394
+ edge.to = null;
1395
+ edge.connect();
1396
+ }
1397
+ }
1398
+ };
1399
+
1400
+ /**
1401
+ * Update the values of all object in the given array according to the current
1402
+ * value range of the objects in the array.
1403
+ * @param {Object} obj An object containing a set of Edges or Nodes
1404
+ * The objects must have a method getValue() and
1405
+ * setValueRange(min, max).
1406
+ * @private
1407
+ */
1408
+ Graph.prototype._updateValueRange = function(obj) {
1409
+ var id;
1410
+
1411
+ // determine the range of the objects
1412
+ var valueMin = undefined;
1413
+ var valueMax = undefined;
1414
+ for (id in obj) {
1415
+ if (obj.hasOwnProperty(id)) {
1416
+ var value = obj[id].getValue();
1417
+ if (value !== undefined) {
1418
+ valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
1419
+ valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
1420
+ }
1421
+ }
1422
+ }
1423
+
1424
+ // adjust the range of all objects
1425
+ if (valueMin !== undefined && valueMax !== undefined) {
1426
+ for (id in obj) {
1427
+ if (obj.hasOwnProperty(id)) {
1428
+ obj[id].setValueRange(valueMin, valueMax);
1429
+ }
1430
+ }
1431
+ }
1432
+ };
1433
+
1434
+ /**
1435
+ * Redraw the graph with the current data
1436
+ * chart will be resized too.
1437
+ */
1438
+ Graph.prototype.redraw = function() {
1439
+ this.setSize(this.width, this.height);
1440
+
1441
+ this._redraw();
1442
+ };
1443
+
1444
+ /**
1445
+ * Redraw the graph with the current data
1446
+ * @private
1447
+ */
1448
+ Graph.prototype._redraw = function() {
1449
+ var ctx = this.frame.canvas.getContext('2d');
1450
+ // clear the canvas
1451
+ var w = this.frame.canvas.width;
1452
+ var h = this.frame.canvas.height;
1453
+ ctx.clearRect(0, 0, w, h);
1454
+
1455
+ // set scaling and translation
1456
+ ctx.save();
1457
+ ctx.translate(this.translation.x, this.translation.y);
1458
+ ctx.scale(this.scale, this.scale);
1459
+
1460
+ this.canvasTopLeft = {
1461
+ "x": this._canvasToX(0),
1462
+ "y": this._canvasToY(0)
1463
+ };
1464
+ this.canvasBottomRight = {
1465
+ "x": this._canvasToX(this.frame.canvas.clientWidth),
1466
+ "y": this._canvasToY(this.frame.canvas.clientHeight)
1467
+ };
1468
+
1469
+ this._doInAllSectors("_drawAllSectorNodes",ctx);
1470
+ this._doInAllSectors("_drawEdges",ctx);
1471
+ this._doInAllSectors("_drawNodes",ctx);
1472
+
1473
+ // restore original scaling and translation
1474
+ ctx.restore();
1475
+
1476
+ if (this.constants.navigation.enabled == true) {
1477
+ this._doInNavigationSector("_drawNodes",ctx,true);
1478
+ }
1479
+ };
1480
+
1481
+ /**
1482
+ * Set the translation of the graph
1483
+ * @param {Number} offsetX Horizontal offset
1484
+ * @param {Number} offsetY Vertical offset
1485
+ * @private
1486
+ */
1487
+ Graph.prototype._setTranslation = function(offsetX, offsetY) {
1488
+ if (this.translation === undefined) {
1489
+ this.translation = {
1490
+ x: 0,
1491
+ y: 0
1492
+ };
1493
+ }
1494
+
1495
+ if (offsetX !== undefined) {
1496
+ this.translation.x = offsetX;
1497
+ }
1498
+ if (offsetY !== undefined) {
1499
+ this.translation.y = offsetY;
1500
+ }
1501
+ };
1502
+
1503
+ /**
1504
+ * Get the translation of the graph
1505
+ * @return {Object} translation An object with parameters x and y, both a number
1506
+ * @private
1507
+ */
1508
+ Graph.prototype._getTranslation = function() {
1509
+ return {
1510
+ x: this.translation.x,
1511
+ y: this.translation.y
1512
+ };
1513
+ };
1514
+
1515
+ /**
1516
+ * Scale the graph
1517
+ * @param {Number} scale Scaling factor 1.0 is unscaled
1518
+ * @private
1519
+ */
1520
+ Graph.prototype._setScale = function(scale) {
1521
+ this.scale = scale;
1522
+ };
1523
+
1524
+ /**
1525
+ * Get the current scale of the graph
1526
+ * @return {Number} scale Scaling factor 1.0 is unscaled
1527
+ * @private
1528
+ */
1529
+ Graph.prototype._getScale = function() {
1530
+ return this.scale;
1531
+ };
1532
+
1533
+ /**
1534
+ * Convert a horizontal point on the HTML canvas to the x-value of the model
1535
+ * @param {number} x
1536
+ * @returns {number}
1537
+ * @private
1538
+ */
1539
+ Graph.prototype._canvasToX = function(x) {
1540
+ return (x - this.translation.x) / this.scale;
1541
+ };
1542
+
1543
+ /**
1544
+ * Convert an x-value in the model to a horizontal point on the HTML canvas
1545
+ * @param {number} x
1546
+ * @returns {number}
1547
+ * @private
1548
+ */
1549
+ Graph.prototype._xToCanvas = function(x) {
1550
+ return x * this.scale + this.translation.x;
1551
+ };
1552
+
1553
+ /**
1554
+ * Convert a vertical point on the HTML canvas to the y-value of the model
1555
+ * @param {number} y
1556
+ * @returns {number}
1557
+ * @private
1558
+ */
1559
+ Graph.prototype._canvasToY = function(y) {
1560
+ return (y - this.translation.y) / this.scale;
1561
+ };
1562
+
1563
+ /**
1564
+ * Convert an y-value in the model to a vertical point on the HTML canvas
1565
+ * @param {number} y
1566
+ * @returns {number}
1567
+ * @private
1568
+ */
1569
+ Graph.prototype._yToCanvas = function(y) {
1570
+ return y * this.scale + this.translation.y ;
1571
+ };
1572
+
1573
+ /**
1574
+ * Redraw all nodes
1575
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
1576
+ * @param {CanvasRenderingContext2D} ctx
1577
+ * @param {Boolean} [alwaysShow]
1578
+ * @private
1579
+ */
1580
+ Graph.prototype._drawNodes = function(ctx,alwaysShow) {
1581
+ if (alwaysShow === undefined) {
1582
+ alwaysShow = false;
1583
+ }
1584
+
1585
+ // first draw the unselected nodes
1586
+ var nodes = this.nodes;
1587
+ var selected = [];
1588
+
1589
+ for (var id in nodes) {
1590
+ if (nodes.hasOwnProperty(id)) {
1591
+ nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
1592
+ if (nodes[id].isSelected()) {
1593
+ selected.push(id);
1594
+ }
1595
+ else {
1596
+ if (nodes[id].inArea() || alwaysShow) {
1597
+ nodes[id].draw(ctx);
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ // draw the selected nodes on top
1604
+ for (var s = 0, sMax = selected.length; s < sMax; s++) {
1605
+ if (nodes[selected[s]].inArea() || alwaysShow) {
1606
+ nodes[selected[s]].draw(ctx);
1607
+ }
1608
+ }
1609
+ };
1610
+
1611
+ /**
1612
+ * Redraw all edges
1613
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
1614
+ * @param {CanvasRenderingContext2D} ctx
1615
+ * @private
1616
+ */
1617
+ Graph.prototype._drawEdges = function(ctx) {
1618
+ var edges = this.edges;
1619
+ for (var id in edges) {
1620
+ if (edges.hasOwnProperty(id)) {
1621
+ var edge = edges[id];
1622
+ edge.setScale(this.scale);
1623
+ if (edge.connected) {
1624
+ edges[id].draw(ctx);
1625
+ }
1626
+ }
1627
+ }
1628
+ };
1629
+
1630
+ /**
1631
+ * Find a stable position for all nodes
1632
+ * @private
1633
+ */
1634
+ Graph.prototype._doStabilize = function() {
1635
+ //var start = new Date();
1636
+
1637
+ // find stable position
1638
+ var count = 0;
1639
+ var vmin = this.constants.minVelocity;
1640
+ var stable = false;
1641
+ while (!stable && count < this.constants.maxIterations) {
1642
+ this._initializeForceCalculation();
1643
+ this._discreteStepNodes();
1644
+ stable = !this._isMoving(vmin);
1645
+ count++;
1646
+ }
1647
+ this.zoomToFit();
1648
+
1649
+ // var end = new Date();
1650
+
1651
+ // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
1652
+ };
1653
+
1654
+
1655
+ /**
1656
+ * Before calculating the forces, we check if we need to cluster to keep up performance and we check
1657
+ * if there is more than one node. If it is just one node, we dont calculate anything.
1658
+ *
1659
+ * @private
1660
+ */
1661
+ Graph.prototype._initializeForceCalculation = function() {
1662
+ // stop calculation if there is only one node
1663
+ if (this.nodeIndices.length == 1) {
1664
+ this.nodes[this.nodeIndices[0]]._setForce(0,0);
1665
+ }
1666
+ else {
1667
+ // if there are too many nodes on screen, we cluster without repositioning
1668
+ if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
1669
+ this.clusterToFit(this.constants.clustering.reduceToNodes, false);
1670
+ }
1671
+
1672
+ // we now start the force calculation
1673
+ this._calculateForces();
1674
+ }
1675
+ };
1676
+
1677
+
1678
+ /**
1679
+ * Calculate the external forces acting on the nodes
1680
+ * Forces are caused by: edges, repulsing forces between nodes, gravity
1681
+ * @private
1682
+ */
1683
+ Graph.prototype._calculateForces = function() {
1684
+ // var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)),
1685
+ // "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))}
1686
+ // create a local edge to the nodes and edges, that is faster
1687
+ var dx, dy, angle, distance, fx, fy,
1688
+ repulsingForce, springForce, length, edgeLength,
1689
+ node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter;
1690
+ var clusterSize;
1691
+ var nodes = this.nodes;
1692
+ var edges = this.edges;
1693
+
1694
+ // Gravity is required to keep separated groups from floating off
1695
+ // the forces are reset to zero in this loop by using _setForce instead
1696
+ // of _addForce
1697
+ var gravity = 0.08 * this.forceFactor;
1698
+ for (i = 0; i < this.nodeIndices.length; i++) {
1699
+ node = nodes[this.nodeIndices[i]];
1700
+ // gravity does not apply when we are in a pocket sector
1701
+ if (this._sector() == "default") {
1702
+ dx = -node.x;// + screenCenterPos.x;
1703
+ dy = -node.y;// + screenCenterPos.y;
1704
+
1705
+ angle = Math.atan2(dy, dx);
1706
+ fx = Math.cos(angle) * gravity;
1707
+ fy = Math.sin(angle) * gravity;
1708
+ }
1709
+ else {
1710
+ fx = 0;
1711
+ fy = 0;
1712
+ }
1713
+ node._setForce(fx, fy);
1714
+
1715
+ node.updateDamping(this.nodeIndices.length);
1716
+ }
1717
+
1718
+ // repulsing forces between nodes
1719
+ var minimumDistance = this.constants.nodes.distance,
1720
+ steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
1721
+
1722
+
1723
+ // we loop from i over all but the last entree in the array
1724
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
1725
+ for (i = 0; i < this.nodeIndices.length-1; i++) {
1726
+ node1 = nodes[this.nodeIndices[i]];
1727
+ for (j = i+1; j < this.nodeIndices.length; j++) {
1728
+ node2 = nodes[this.nodeIndices[j]];
1729
+ clusterSize = (node1.clusterSize + node2.clusterSize - 2);
1730
+ dx = node2.x - node1.x;
1731
+ dy = node2.y - node1.y;
1732
+ distance = Math.sqrt(dx * dx + dy * dy);
1733
+
1734
+
1735
+ // clusters have a larger region of influence
1736
+ minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
1737
+ if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
1738
+ angle = Math.atan2(dy, dx);
1739
+
1740
+ if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
1741
+ repulsingForce = 1.0;
1742
+ }
1743
+ else {
1744
+ // TODO: correct factor for repulsing force
1745
+ //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
1746
+ //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
1747
+ repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
1748
+ }
1749
+ // amplify the repulsion for clusters.
1750
+ repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
1751
+ repulsingForce *= this.forceFactor;
1752
+
1753
+
1754
+ fx = Math.cos(angle) * repulsingForce;
1755
+ fy = Math.sin(angle) * repulsingForce ;
1756
+
1757
+ node1._addForce(-fx, -fy);
1758
+ node2._addForce(fx, fy);
1759
+ }
1760
+ }
1761
+ }
1762
+
1763
+ /*
1764
+ // repulsion of the edges on the nodes and
1765
+ for (var nodeId in nodes) {
1766
+ if (nodes.hasOwnProperty(nodeId)) {
1767
+ node = nodes[nodeId];
1768
+ for(var edgeId in edges) {
1769
+ if (edges.hasOwnProperty(edgeId)) {
1770
+ edge = edges[edgeId];
1771
+
1772
+ // get the center of the edge
1773
+ xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
1774
+ yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
1775
+
1776
+ // calculate normally distributed force
1777
+ dx = node.x - xCenter;
1778
+ dy = node.y - yCenter;
1779
+ distance = Math.sqrt(dx * dx + dy * dy);
1780
+ if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
1781
+ angle = Math.atan2(dy, dx);
1782
+
1783
+ if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
1784
+ repulsingForce = 1.0;
1785
+ }
1786
+ else {
1787
+ // TODO: correct factor for repulsing force
1788
+ //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
1789
+ //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
1790
+ repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
1791
+ }
1792
+ fx = Math.cos(angle) * repulsingForce;
1793
+ fy = Math.sin(angle) * repulsingForce;
1794
+ node._addForce(fx, fy);
1795
+ edge.from._addForce(-fx/2,-fy/2);
1796
+ edge.to._addForce(-fx/2,-fy/2);
1797
+ }
1798
+ }
1799
+ }
1800
+ }
1801
+ }
1802
+ */
1803
+
1804
+ // forces caused by the edges, modelled as springs
1805
+ for (edgeId in edges) {
1806
+ if (edges.hasOwnProperty(edgeId)) {
1807
+ edge = edges[edgeId];
1808
+ if (edge.connected) {
1809
+ // only calculate forces if nodes are in the same sector
1810
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
1811
+ clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
1812
+ dx = (edge.to.x - edge.from.x);
1813
+ dy = (edge.to.y - edge.from.y);
1814
+ //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
1815
+ //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
1816
+ //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
1817
+ edgeLength = edge.length;
1818
+ // this implies that the edges between big clusters are longer
1819
+ edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
1820
+ length = Math.sqrt(dx * dx + dy * dy);
1821
+ angle = Math.atan2(dy, dx);
1822
+
1823
+ springForce = edge.stiffness * (edgeLength - length) * this.forceFactor;
1824
+
1825
+ fx = Math.cos(angle) * springForce;
1826
+ fy = Math.sin(angle) * springForce;
1827
+
1828
+ edge.from._addForce(-fx, -fy);
1829
+ edge.to._addForce(fx, fy);
1830
+ }
1831
+ }
1832
+ }
1833
+ }
1834
+ /*
1835
+ // TODO: re-implement repulsion of edges
1836
+
1837
+ // repulsing forces between edges
1838
+ var minimumDistance = this.constants.edges.distance,
1839
+ steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
1840
+ for (var l = 0; l < edges.length; l++) {
1841
+ //Keep distance from other edge centers
1842
+ for (var l2 = l + 1; l2 < this.edges.length; l2++) {
1843
+ //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
1844
+ //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
1845
+ //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
1846
+ var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
1847
+ ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
1848
+ l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
1849
+ l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
1850
+
1851
+ // calculate normally distributed force
1852
+ dx = l2x - lx,
1853
+ dy = l2y - ly,
1854
+ distance = Math.sqrt(dx * dx + dy * dy),
1855
+ angle = Math.atan2(dy, dx),
1856
+
1857
+
1858
+ // TODO: correct factor for repulsing force
1859
+ //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
1860
+ //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
1861
+ repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
1862
+ fx = Math.cos(angle) * repulsingforce,
1863
+ fy = Math.sin(angle) * repulsingforce;
1864
+
1865
+ edges[l].from._addForce(-fx, -fy);
1866
+ edges[l].to._addForce(-fx, -fy);
1867
+ edges[l2].from._addForce(fx, fy);
1868
+ edges[l2].to._addForce(fx, fy);
1869
+ }
1870
+ }
1871
+ */
1872
+ };
1873
+
1874
+
1875
+ /**
1876
+ * Check if any of the nodes is still moving
1877
+ * @param {number} vmin the minimum velocity considered as 'moving'
1878
+ * @return {boolean} true if moving, false if non of the nodes is moving
1879
+ * @private
1880
+ */
1881
+ Graph.prototype._isMoving = function(vmin) {
1882
+ var vminCorrected = vmin / this.scale;
1883
+ var nodes = this.nodes;
1884
+ for (var id in nodes) {
1885
+ if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
1886
+ return true;
1887
+ }
1888
+ }
1889
+ return false;
1890
+ };
1891
+
1892
+
1893
+ /**
1894
+ * /**
1895
+ * Perform one discrete step for all nodes
1896
+ *
1897
+ * @param interval
1898
+ * @private
1899
+ */
1900
+ Graph.prototype._discreteStepNodes = function() {
1901
+ var interval = 0.01;
1902
+ var nodes = this.nodes;
1903
+ for (var id in nodes) {
1904
+ if (nodes.hasOwnProperty(id)) {
1905
+ nodes[id].discreteStep(interval);
1906
+ }
1907
+ }
1908
+
1909
+ var vmin = this.constants.minVelocity;
1910
+ this.moving = this._isMoving(vmin);
1911
+ };
1912
+
1913
+
1914
+
1915
+ /**
1916
+ * Start animating nodes and edges
1917
+ *
1918
+ * @poram {Boolean} runCalculationStep
1919
+ */
1920
+ Graph.prototype.start = function() {
1921
+ if (!this.freezeSimulation) {
1922
+
1923
+ if (this.moving) {
1924
+ this._doInAllActiveSectors("_initializeForceCalculation");
1925
+ this._doInAllActiveSectors("_discreteStepNodes");
1926
+ this._findCenter(this._getRange())
1927
+ }
1928
+
1929
+ if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
1930
+ // start animation. only start calculationTimer if it is not already running
1931
+ if (!this.timer) {
1932
+ var graph = this;
1933
+ this.timer = window.setTimeout(function () {
1934
+ graph.timer = undefined;
1935
+
1936
+ // keyboad movement
1937
+ if (graph.xIncrement != 0 || graph.yIncrement != 0) {
1938
+ var translation = graph._getTranslation();
1939
+ graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement);
1940
+ }
1941
+ if (graph.zoomIncrement != 0) {
1942
+ var center = {
1943
+ x: graph.frame.canvas.clientWidth / 2,
1944
+ y: graph.frame.canvas.clientHeight / 2
1945
+ };
1946
+ graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
1947
+ }
1948
+
1949
+ graph.start();
1950
+ graph._redraw();
1951
+
1952
+ //this.end = window.performance.now();
1953
+ //this.time = this.end - this.startTime;
1954
+ //console.log('refresh time: ' + this.time);
1955
+ //this.startTime = window.performance.now();
1956
+
1957
+ }, this.renderTimestep);
1958
+ }
1959
+ }
1960
+ else {
1961
+ this._redraw();
1962
+ }
1963
+ }
1964
+ };
1965
+
1966
+
1967
+
1968
+
1969
+ Graph.prototype.singleStep = function() {
1970
+ if (this.moving) {
1971
+ this._initializeForceCalculation();
1972
+ this._discreteStepNodes();
1973
+
1974
+ var vmin = this.constants.minVelocity;
1975
+ this.moving = this._isMoving(vmin);
1976
+ this._redraw();
1977
+ }
1978
+ };
1979
+
1980
+
1981
+
1982
+ /**
1983
+ * Freeze the animation
1984
+ */
1985
+ Graph.prototype.toggleFreeze = function() {
1986
+ if (this.freezeSimulation == false) {
1987
+ this.freezeSimulation = true;
1988
+ }
1989
+ else {
1990
+ this.freezeSimulation = false;
1991
+ this.start();
1992
+ }
1993
+ };
1994
+
1995
+ /**
1996
+ * Mixin the cluster system and initialize the parameters required.
1997
+ *
1998
+ * @private
1999
+ */
2000
+ Graph.prototype._loadClusterSystem = function() {
2001
+ this.clusterSession = 0;
2002
+ this.hubThreshold = 5;
2003
+
2004
+ for (var mixinFunction in ClusterMixin) {
2005
+ if (ClusterMixin.hasOwnProperty(mixinFunction)) {
2006
+ Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction];
2007
+ }
2008
+ }
2009
+ }
2010
+
2011
+ /**
2012
+ * Mixin the sector system and initialize the parameters required
2013
+ *
2014
+ * @private
2015
+ */
2016
+ Graph.prototype._loadSectorSystem = function() {
2017
+ this.sectors = {};
2018
+ this.activeSector = ["default"];
2019
+ this.sectors["active"] = {};
2020
+ this.sectors["active"]["default"] = {"nodes":{},
2021
+ "edges":{},
2022
+ "nodeIndices":[],
2023
+ "formationScale": 1.0,
2024
+ "drawingNode": undefined};
2025
+ this.sectors["frozen"] = {};
2026
+ this.sectors["navigation"] = {"nodes":{},
2027
+ "edges":{},
2028
+ "nodeIndices":[],
2029
+ "formationScale": 1.0,
2030
+ "drawingNode": undefined};
2031
+
2032
+ this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
2033
+ for (var mixinFunction in SectorMixin) {
2034
+ if (SectorMixin.hasOwnProperty(mixinFunction)) {
2035
+ Graph.prototype[mixinFunction] = SectorMixin[mixinFunction];
2036
+ }
2037
+ }
2038
+ };
2039
+
2040
+
2041
+ /**
2042
+ * Mixin the selection system and initialize the parameters required
2043
+ *
2044
+ * @private
2045
+ */
2046
+ Graph.prototype._loadSelectionSystem = function() {
2047
+ this.selection = [];
2048
+ this.selectionObj = {};
2049
+
2050
+ for (var mixinFunction in SelectionMixin) {
2051
+ if (SelectionMixin.hasOwnProperty(mixinFunction)) {
2052
+ Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction];
2053
+ }
2054
+ }
2055
+ }
2056
+
2057
+
2058
+ /**
2059
+ * Mixin the navigation (User Interface) system and initialize the parameters required
2060
+ *
2061
+ * @private
2062
+ */
2063
+ Graph.prototype._loadNavigationControls = function() {
2064
+ for (var mixinFunction in NavigationMixin) {
2065
+ if (NavigationMixin.hasOwnProperty(mixinFunction)) {
2066
+ Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction];
2067
+ }
2068
+ }
2069
+
2070
+ if (this.constants.navigation.enabled == true) {
2071
+ this._loadNavigationElements();
2072
+ }
2073
+ }
2074
+
2075
+ /**
2076
+ * this function exists to avoid errors when not loading the navigation system
2077
+ */
2078
+ Graph.prototype._relocateNavigation = function() {
2079
+ // empty, is overloaded by navigation system
2080
+ }
2081
+
2082
+ /**
2083
+ * * this function exists to avoid errors when not loading the navigation system
2084
+ */
2085
+ Graph.prototype._unHighlightAll = function() {
2086
+ // empty, is overloaded by the navigation system
2087
+ }
2088
+
2089
+
2090
+
2091
+
2092
+
2093
+
2094
+
2095
+
2096
+
2097
+
2098
+
2099
+
2100
+
2101
+
2102
+
2103
+
2104
+
2105
+
2106
+
2107
+
2108
+
2109
+
2110
+
2111
+