vis-rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+