vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1929 lines
54 KiB

11 years ago
11 years ago
  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. this._initializeMixinLoaders();
  14. // create variables and set default values
  15. this.containerElement = container;
  16. this.width = '100%';
  17. this.height = '100%';
  18. // render and calculation settings
  19. this.renderRefreshRate = 60; // hz (fps)
  20. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  21. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  22. this.maxRenderSteps = 3; // max amount of physics ticks per render step.
  23. this.stabilize = true; // stabilize before displaying the graph
  24. this.selectable = true;
  25. // these functions are triggered when the dataset is edited
  26. this.triggerFunctions = {add:null,edit:null,connect:null,delete:null};
  27. // set constant values
  28. this.constants = {
  29. nodes: {
  30. radiusMin: 5,
  31. radiusMax: 20,
  32. radius: 5,
  33. shape: 'ellipse',
  34. image: undefined,
  35. widthMin: 16, // px
  36. widthMax: 64, // px
  37. fixed: false,
  38. fontColor: 'black',
  39. fontSize: 14, // px
  40. fontFace: 'verdana',
  41. level: -1,
  42. color: {
  43. border: '#2B7CE9',
  44. background: '#97C2FC',
  45. highlight: {
  46. border: '#2B7CE9',
  47. background: '#D2E5FF'
  48. }
  49. },
  50. borderColor: '#2B7CE9',
  51. backgroundColor: '#97C2FC',
  52. highlightColor: '#D2E5FF',
  53. group: undefined
  54. },
  55. edges: {
  56. widthMin: 1,
  57. widthMax: 15,
  58. width: 1,
  59. style: 'line',
  60. color: '#848484',
  61. fontColor: '#343434',
  62. fontSize: 14, // px
  63. fontFace: 'arial',
  64. dash: {
  65. length: 10,
  66. gap: 5,
  67. altLength: undefined
  68. }
  69. },
  70. configurePhysics:false,
  71. physics: {
  72. barnesHut: {
  73. enabled: true,
  74. theta: 1 / 0.6, // inverted to save time during calculation
  75. gravitationalConstant: -2000,
  76. centralGravity: 0.3,
  77. springLength: 100,
  78. springConstant: 0.05,
  79. damping: 0.09
  80. },
  81. repulsion: {
  82. centralGravity: 0.1,
  83. springLength: 200,
  84. springConstant: 0.05,
  85. nodeDistance: 100,
  86. damping: 0.09
  87. },
  88. hierarchicalRepulsion: {
  89. enabled: false,
  90. centralGravity: 0.0,
  91. springLength: 100,
  92. springConstant: 0.01,
  93. nodeDistance: 60,
  94. damping: 0.09
  95. },
  96. damping: null,
  97. centralGravity: null,
  98. springLength: null,
  99. springConstant: null
  100. },
  101. clustering: { // Per Node in Cluster = PNiC
  102. enabled: false, // (Boolean) | global on/off switch for clustering.
  103. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  104. 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
  105. 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
  106. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  107. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  108. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  109. 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.
  110. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  111. maxFontSize: 1000,
  112. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  113. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  114. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  115. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  116. height: 1, // (px PNiC) | growth of the height per node in cluster.
  117. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  118. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  119. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  120. clusterLevelDifference: 2
  121. },
  122. navigation: {
  123. enabled: false
  124. },
  125. keyboard: {
  126. enabled: false,
  127. speed: {x: 10, y: 10, zoom: 0.02}
  128. },
  129. dataManipulation: {
  130. enabled: false,
  131. initiallyVisible: false
  132. },
  133. hierarchicalLayout: {
  134. enabled:false,
  135. levelSeparation: 150,
  136. nodeSpacing: 100,
  137. direction: "UD" // UD, DU, LR, RL
  138. },
  139. smoothCurves: true,
  140. maxVelocity: 10,
  141. minVelocity: 0.1, // px/s
  142. maxIterations: 1000 // maximum number of iteration to stabilize
  143. };
  144. this.editMode = this.constants.dataManipulation.initiallyVisible;
  145. // Node variables
  146. var graph = this;
  147. this.groups = new Groups(); // object with groups
  148. this.images = new Images(); // object with images
  149. this.images.setOnloadCallback(function () {
  150. graph._redraw();
  151. });
  152. // keyboard navigation variables
  153. this.xIncrement = 0;
  154. this.yIncrement = 0;
  155. this.zoomIncrement = 0;
  156. // loading all the mixins:
  157. // load the force calculation functions, grouped under the physics system.
  158. this._loadPhysicsSystem();
  159. // create a frame and canvas
  160. this._create();
  161. // load the sector system. (mandatory, fully integrated with Graph)
  162. this._loadSectorSystem();
  163. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  164. this._loadClusterSystem();
  165. // load the selection system. (mandatory, required by Graph)
  166. this._loadSelectionSystem();
  167. // load the selection system. (mandatory, required by Graph)
  168. this._loadHierarchySystem();
  169. // apply options
  170. this.setOptions(options);
  171. // other vars
  172. this.freezeSimulation = false;// freeze the simulation
  173. this.cachedFunctions = {};
  174. // containers for nodes and edges
  175. this.calculationNodes = {};
  176. this.calculationNodeIndices = [];
  177. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  178. this.nodes = {}; // object with Node objects
  179. this.edges = {}; // object with Edge objects
  180. // position and scale variables and objects
  181. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  182. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  183. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  184. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  185. this.scale = 1; // defining the global scale variable in the constructor
  186. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  187. // datasets or dataviews
  188. this.nodesData = null; // A DataSet or DataView
  189. this.edgesData = null; // A DataSet or DataView
  190. // create event listeners used to subscribe on the DataSets of the nodes and edges
  191. this.nodesListeners = {
  192. 'add': function (event, params) {
  193. graph._addNodes(params.items);
  194. graph.start();
  195. },
  196. 'update': function (event, params) {
  197. graph._updateNodes(params.items);
  198. graph.start();
  199. },
  200. 'remove': function (event, params) {
  201. graph._removeNodes(params.items);
  202. graph.start();
  203. }
  204. };
  205. this.edgesListeners = {
  206. 'add': function (event, params) {
  207. graph._addEdges(params.items);
  208. graph.start();
  209. },
  210. 'update': function (event, params) {
  211. graph._updateEdges(params.items);
  212. graph.start();
  213. },
  214. 'remove': function (event, params) {
  215. graph._removeEdges(params.items);
  216. graph.start();
  217. }
  218. };
  219. // properties for the animation
  220. this.moving = true;
  221. this.timer = undefined; // Scheduling function. Is definded in this.start();
  222. // load data (the disable start variable will be the same as the enabled clustering)
  223. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  224. // hierarchical layout
  225. if (this.constants.hierarchicalLayout.enabled == true) {
  226. this._setupHierarchicalLayout();
  227. }
  228. else {
  229. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  230. if (this.stabilize == false) {
  231. this.zoomExtent(true,this.constants.clustering.enabled);
  232. }
  233. }
  234. // if clustering is disabled, the simulation will have started in the setData function
  235. if (this.constants.clustering.enabled) {
  236. this.startWithClustering();
  237. }
  238. }
  239. // Extend Graph with an Emitter mixin
  240. Emitter(Graph.prototype);
  241. /**
  242. * Get the script path where the vis.js library is located
  243. *
  244. * @returns {string | null} path Path or null when not found. Path does not
  245. * end with a slash.
  246. * @private
  247. */
  248. Graph.prototype._getScriptPath = function() {
  249. var scripts = document.getElementsByTagName( 'script' );
  250. // find script named vis.js or vis.min.js
  251. for (var i = 0; i < scripts.length; i++) {
  252. var src = scripts[i].src;
  253. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  254. if (match) {
  255. // return path without the script name
  256. return src.substring(0, src.length - match[0].length);
  257. }
  258. }
  259. return null;
  260. };
  261. /**
  262. * Find the center position of the graph
  263. * @private
  264. */
  265. Graph.prototype._getRange = function() {
  266. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  267. for (var nodeId in this.nodes) {
  268. if (this.nodes.hasOwnProperty(nodeId)) {
  269. node = this.nodes[nodeId];
  270. if (minX > (node.x)) {minX = node.x;}
  271. if (maxX < (node.x)) {maxX = node.x;}
  272. if (minY > (node.y)) {minY = node.y;}
  273. if (maxY < (node.y)) {maxY = node.y;}
  274. }
  275. }
  276. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  277. };
  278. /**
  279. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  280. * @returns {{x: number, y: number}}
  281. * @private
  282. */
  283. Graph.prototype._findCenter = function(range) {
  284. return {x: (0.5 * (range.maxX + range.minX)),
  285. y: (0.5 * (range.maxY + range.minY))};
  286. };
  287. /**
  288. * center the graph
  289. *
  290. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  291. */
  292. Graph.prototype._centerGraph = function(range) {
  293. var center = this._findCenter(range);
  294. center.x *= this.scale;
  295. center.y *= this.scale;
  296. center.x -= 0.5 * this.frame.canvas.clientWidth;
  297. center.y -= 0.5 * this.frame.canvas.clientHeight;
  298. this._setTranslation(-center.x,-center.y); // set at 0,0
  299. };
  300. /**
  301. * This function zooms out to fit all data on screen based on amount of nodes
  302. *
  303. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  304. */
  305. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  306. if (initialZoom === undefined) {
  307. initialZoom = false;
  308. }
  309. if (disableStart === undefined) {
  310. disableStart = false;
  311. }
  312. var range = this._getRange();
  313. var zoomLevel;
  314. if (initialZoom == true) {
  315. var numberOfNodes = this.nodeIndices.length;
  316. if (this.constants.smoothCurves == true) {
  317. if (this.constants.clustering.enabled == true &&
  318. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  319. zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  320. }
  321. else {
  322. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  323. }
  324. }
  325. else {
  326. if (this.constants.clustering.enabled == true &&
  327. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  328. zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  329. }
  330. else {
  331. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  332. }
  333. }
  334. // correct for larger canvasses.
  335. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  336. zoomLevel *= factor;
  337. }
  338. else {
  339. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  340. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  341. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  342. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  343. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  344. }
  345. if (zoomLevel > 1.0) {
  346. zoomLevel = 1.0;
  347. }
  348. this._setScale(zoomLevel);
  349. this._centerGraph(range);
  350. if (disableStart == false) {
  351. this.moving = true;
  352. this.start();
  353. }
  354. };
  355. /**
  356. * Update the this.nodeIndices with the most recent node index list
  357. * @private
  358. */
  359. Graph.prototype._updateNodeIndexList = function() {
  360. this._clearNodeIndexList();
  361. for (var idx in this.nodes) {
  362. if (this.nodes.hasOwnProperty(idx)) {
  363. this.nodeIndices.push(idx);
  364. }
  365. }
  366. };
  367. /**
  368. * Set nodes and edges, and optionally options as well.
  369. *
  370. * @param {Object} data Object containing parameters:
  371. * {Array | DataSet | DataView} [nodes] Array with nodes
  372. * {Array | DataSet | DataView} [edges] Array with edges
  373. * {String} [dot] String containing data in DOT format
  374. * {Options} [options] Object with options
  375. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  376. */
  377. Graph.prototype.setData = function(data, disableStart) {
  378. if (disableStart === undefined) {
  379. disableStart = false;
  380. }
  381. if (data && data.dot && (data.nodes || data.edges)) {
  382. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  383. ' parameter pair "nodes" and "edges", but not both.');
  384. }
  385. // set options
  386. this.setOptions(data && data.options);
  387. // set all data
  388. if (data && data.dot) {
  389. // parse DOT file
  390. if(data && data.dot) {
  391. var dotData = vis.util.DOTToGraph(data.dot);
  392. this.setData(dotData);
  393. return;
  394. }
  395. }
  396. else {
  397. this._setNodes(data && data.nodes);
  398. this._setEdges(data && data.edges);
  399. }
  400. this._putDataInSector();
  401. if (!disableStart) {
  402. // find a stable position or start animating to a stable position
  403. if (this.stabilize) {
  404. this._doStabilize();
  405. }
  406. this.start();
  407. }
  408. };
  409. /**
  410. * Set options
  411. * @param {Object} options
  412. */
  413. Graph.prototype.setOptions = function (options) {
  414. if (options) {
  415. var prop;
  416. // retrieve parameter values
  417. if (options.width !== undefined) {this.width = options.width;}
  418. if (options.height !== undefined) {this.height = options.height;}
  419. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  420. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  421. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  422. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  423. if (options.onAdd) {
  424. this.triggerFunctions.add = options.onAdd;
  425. }
  426. if (options.onEdit) {
  427. this.triggerFunctions.edit = options.onEdit;
  428. }
  429. if (options.onConnect) {
  430. this.triggerFunctions.connect = options.onConnect;
  431. }
  432. if (options.onDelete) {
  433. this.triggerFunctions.delete = options.onDelete;
  434. }
  435. if (options.physics) {
  436. if (options.physics.barnesHut) {
  437. this.constants.physics.barnesHut.enabled = true;
  438. for (prop in options.physics.barnesHut) {
  439. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  440. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  441. }
  442. }
  443. }
  444. if (options.physics.repulsion) {
  445. this.constants.physics.barnesHut.enabled = false;
  446. for (prop in options.physics.repulsion) {
  447. if (options.physics.repulsion.hasOwnProperty(prop)) {
  448. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  449. }
  450. }
  451. }
  452. }
  453. if (options.hierarchicalLayout) {
  454. this.constants.hierarchicalLayout.enabled = true;
  455. for (prop in options.hierarchicalLayout) {
  456. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  457. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  458. }
  459. }
  460. }
  461. else if (options.hierarchicalLayout !== undefined) {
  462. this.constants.hierarchicalLayout.enabled = false;
  463. }
  464. if (options.clustering) {
  465. this.constants.clustering.enabled = true;
  466. for (prop in options.clustering) {
  467. if (options.clustering.hasOwnProperty(prop)) {
  468. this.constants.clustering[prop] = options.clustering[prop];
  469. }
  470. }
  471. }
  472. else if (options.clustering !== undefined) {
  473. this.constants.clustering.enabled = false;
  474. }
  475. if (options.navigation) {
  476. this.constants.navigation.enabled = true;
  477. for (prop in options.navigation) {
  478. if (options.navigation.hasOwnProperty(prop)) {
  479. this.constants.navigation[prop] = options.navigation[prop];
  480. }
  481. }
  482. }
  483. else if (options.navigation !== undefined) {
  484. this.constants.navigation.enabled = false;
  485. }
  486. if (options.keyboard) {
  487. this.constants.keyboard.enabled = true;
  488. for (prop in options.keyboard) {
  489. if (options.keyboard.hasOwnProperty(prop)) {
  490. this.constants.keyboard[prop] = options.keyboard[prop];
  491. }
  492. }
  493. }
  494. else if (options.keyboard !== undefined) {
  495. this.constants.keyboard.enabled = false;
  496. }
  497. if (options.dataManipulation) {
  498. this.constants.dataManipulation.enabled = true;
  499. for (prop in options.dataManipulation) {
  500. if (options.dataManipulation.hasOwnProperty(prop)) {
  501. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  502. }
  503. }
  504. }
  505. else if (options.dataManipulation !== undefined) {
  506. this.constants.dataManipulation.enabled = false;
  507. }
  508. // TODO: work out these options and document them
  509. if (options.edges) {
  510. for (prop in options.edges) {
  511. if (options.edges.hasOwnProperty(prop)) {
  512. this.constants.edges[prop] = options.edges[prop];
  513. }
  514. }
  515. if (!options.edges.fontColor) {
  516. this.constants.edges.fontColor = options.edges.color;
  517. }
  518. // Added to support dashed lines
  519. // David Jordan
  520. // 2012-08-08
  521. if (options.edges.dash) {
  522. if (options.edges.dash.length !== undefined) {
  523. this.constants.edges.dash.length = options.edges.dash.length;
  524. }
  525. if (options.edges.dash.gap !== undefined) {
  526. this.constants.edges.dash.gap = options.edges.dash.gap;
  527. }
  528. if (options.edges.dash.altLength !== undefined) {
  529. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  530. }
  531. }
  532. }
  533. if (options.nodes) {
  534. for (prop in options.nodes) {
  535. if (options.nodes.hasOwnProperty(prop)) {
  536. this.constants.nodes[prop] = options.nodes[prop];
  537. }
  538. }
  539. if (options.nodes.color) {
  540. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  541. }
  542. /*
  543. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  544. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  545. */
  546. }
  547. if (options.groups) {
  548. for (var groupname in options.groups) {
  549. if (options.groups.hasOwnProperty(groupname)) {
  550. var group = options.groups[groupname];
  551. this.groups.add(groupname, group);
  552. }
  553. }
  554. }
  555. }
  556. // (Re)loading the mixins that can be enabled or disabled in the options.
  557. // load the force calculation functions, grouped under the physics system.
  558. this._loadPhysicsSystem();
  559. // load the navigation system.
  560. this._loadNavigationControls();
  561. // load the data manipulation system
  562. this._loadManipulationSystem();
  563. // configure the smooth curves
  564. this._configureSmoothCurves();
  565. // bind keys. If disabled, this will not do anything;
  566. this._createKeyBinds();
  567. this.setSize(this.width, this.height);
  568. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  569. this._setScale(1);
  570. this._redraw();
  571. };
  572. /**
  573. * Create the main frame for the Graph.
  574. * This function is executed once when a Graph object is created. The frame
  575. * contains a canvas, and this canvas contains all objects like the axis and
  576. * nodes.
  577. * @private
  578. */
  579. Graph.prototype._create = function () {
  580. // remove all elements from the container element.
  581. while (this.containerElement.hasChildNodes()) {
  582. this.containerElement.removeChild(this.containerElement.firstChild);
  583. }
  584. this.frame = document.createElement('div');
  585. this.frame.className = 'graph-frame';
  586. this.frame.style.position = 'relative';
  587. this.frame.style.overflow = 'hidden';
  588. this.frame.style.zIndex = "1";
  589. // create the graph canvas (HTML canvas element)
  590. this.frame.canvas = document.createElement( 'canvas' );
  591. this.frame.canvas.style.position = 'relative';
  592. this.frame.appendChild(this.frame.canvas);
  593. if (!this.frame.canvas.getContext) {
  594. var noCanvas = document.createElement( 'DIV' );
  595. noCanvas.style.color = 'red';
  596. noCanvas.style.fontWeight = 'bold' ;
  597. noCanvas.style.padding = '10px';
  598. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  599. this.frame.canvas.appendChild(noCanvas);
  600. }
  601. var me = this;
  602. this.drag = {};
  603. this.pinch = {};
  604. this.hammer = Hammer(this.frame.canvas, {
  605. prevent_default: true
  606. });
  607. this.hammer.on('tap', me._onTap.bind(me) );
  608. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  609. this.hammer.on('hold', me._onHold.bind(me) );
  610. this.hammer.on('pinch', me._onPinch.bind(me) );
  611. this.hammer.on('touch', me._onTouch.bind(me) );
  612. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  613. this.hammer.on('drag', me._onDrag.bind(me) );
  614. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  615. this.hammer.on('release', me._onRelease.bind(me) );
  616. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  617. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  618. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  619. // add the frame to the container element
  620. this.containerElement.appendChild(this.frame);
  621. };
  622. /**
  623. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  624. * @private
  625. */
  626. Graph.prototype._createKeyBinds = function() {
  627. var me = this;
  628. this.mousetrap = mousetrap;
  629. this.mousetrap.reset();
  630. if (this.constants.keyboard.enabled == true) {
  631. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  632. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  633. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  634. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  635. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  636. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  637. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  638. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  639. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  640. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  641. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  642. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  643. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  644. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  645. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  646. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  647. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  648. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  649. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  650. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  651. }
  652. if (this.constants.dataManipulation.enabled == true) {
  653. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  654. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  655. }
  656. };
  657. /**
  658. * Get the pointer location from a touch location
  659. * @param {{pageX: Number, pageY: Number}} touch
  660. * @return {{x: Number, y: Number}} pointer
  661. * @private
  662. */
  663. Graph.prototype._getPointer = function (touch) {
  664. return {
  665. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  666. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  667. };
  668. };
  669. /**
  670. * On start of a touch gesture, store the pointer
  671. * @param event
  672. * @private
  673. */
  674. Graph.prototype._onTouch = function (event) {
  675. this.drag.pointer = this._getPointer(event.gesture.center);
  676. this.drag.pinched = false;
  677. this.pinch.scale = this._getScale();
  678. this._handleTouch(this.drag.pointer);
  679. };
  680. /**
  681. * handle drag start event
  682. * @private
  683. */
  684. Graph.prototype._onDragStart = function () {
  685. this._handleDragStart();
  686. };
  687. /**
  688. * This function is called by _onDragStart.
  689. * It is separated out because we can then overload it for the datamanipulation system.
  690. *
  691. * @private
  692. */
  693. Graph.prototype._handleDragStart = function() {
  694. var drag = this.drag;
  695. var node = this._getNodeAt(drag.pointer);
  696. // note: drag.pointer is set in _onTouch to get the initial touch location
  697. drag.dragging = true;
  698. drag.selection = [];
  699. drag.translation = this._getTranslation();
  700. drag.nodeId = null;
  701. if (node != null) {
  702. drag.nodeId = node.id;
  703. // select the clicked node if not yet selected
  704. if (!node.isSelected()) {
  705. this._selectObject(node,false);
  706. }
  707. // create an array with the selected nodes and their original location and status
  708. for (var objectId in this.selectionObj) {
  709. if (this.selectionObj.hasOwnProperty(objectId)) {
  710. var object = this.selectionObj[objectId];
  711. if (object instanceof Node) {
  712. var s = {
  713. id: object.id,
  714. node: object,
  715. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  716. x: object.x,
  717. y: object.y,
  718. xFixed: object.xFixed,
  719. yFixed: object.yFixed
  720. };
  721. object.xFixed = true;
  722. object.yFixed = true;
  723. drag.selection.push(s);
  724. }
  725. }
  726. }
  727. }
  728. };
  729. /**
  730. * handle drag event
  731. * @private
  732. */
  733. Graph.prototype._onDrag = function (event) {
  734. this._handleOnDrag(event)
  735. };
  736. /**
  737. * This function is called by _onDrag.
  738. * It is separated out because we can then overload it for the datamanipulation system.
  739. *
  740. * @private
  741. */
  742. Graph.prototype._handleOnDrag = function(event) {
  743. if (this.drag.pinched) {
  744. return;
  745. }
  746. var pointer = this._getPointer(event.gesture.center);
  747. var me = this,
  748. drag = this.drag,
  749. selection = drag.selection;
  750. if (selection && selection.length) {
  751. // calculate delta's and new location
  752. var deltaX = pointer.x - drag.pointer.x,
  753. deltaY = pointer.y - drag.pointer.y;
  754. // update position of all selected nodes
  755. selection.forEach(function (s) {
  756. var node = s.node;
  757. if (!s.xFixed) {
  758. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  759. }
  760. if (!s.yFixed) {
  761. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  762. }
  763. });
  764. // start _animationStep if not yet running
  765. if (!this.moving) {
  766. this.moving = true;
  767. this.start();
  768. }
  769. }
  770. else {
  771. // move the graph
  772. var diffX = pointer.x - this.drag.pointer.x;
  773. var diffY = pointer.y - this.drag.pointer.y;
  774. this._setTranslation(
  775. this.drag.translation.x + diffX,
  776. this.drag.translation.y + diffY);
  777. this._redraw();
  778. this.moved = true;
  779. }
  780. };
  781. /**
  782. * handle drag start event
  783. * @private
  784. */
  785. Graph.prototype._onDragEnd = function () {
  786. this.drag.dragging = false;
  787. var selection = this.drag.selection;
  788. if (selection) {
  789. selection.forEach(function (s) {
  790. // restore original xFixed and yFixed
  791. s.node.xFixed = s.xFixed;
  792. s.node.yFixed = s.yFixed;
  793. });
  794. }
  795. };
  796. /**
  797. * handle tap/click event: select/unselect a node
  798. * @private
  799. */
  800. Graph.prototype._onTap = function (event) {
  801. var pointer = this._getPointer(event.gesture.center);
  802. this.pointerPosition = pointer;
  803. this._handleTap(pointer);
  804. };
  805. /**
  806. * handle doubletap event
  807. * @private
  808. */
  809. Graph.prototype._onDoubleTap = function (event) {
  810. var pointer = this._getPointer(event.gesture.center);
  811. this._handleDoubleTap(pointer);
  812. };
  813. /**
  814. * handle long tap event: multi select nodes
  815. * @private
  816. */
  817. Graph.prototype._onHold = function (event) {
  818. var pointer = this._getPointer(event.gesture.center);
  819. this.pointerPosition = pointer;
  820. this._handleOnHold(pointer);
  821. };
  822. /**
  823. * handle the release of the screen
  824. *
  825. * @private
  826. */
  827. Graph.prototype._onRelease = function (event) {
  828. var pointer = this._getPointer(event.gesture.center);
  829. this._handleOnRelease(pointer);
  830. };
  831. /**
  832. * Handle pinch event
  833. * @param event
  834. * @private
  835. */
  836. Graph.prototype._onPinch = function (event) {
  837. var pointer = this._getPointer(event.gesture.center);
  838. this.drag.pinched = true;
  839. if (!('scale' in this.pinch)) {
  840. this.pinch.scale = 1;
  841. }
  842. // TODO: enabled moving while pinching?
  843. var scale = this.pinch.scale * event.gesture.scale;
  844. this._zoom(scale, pointer)
  845. };
  846. /**
  847. * Zoom the graph in or out
  848. * @param {Number} scale a number around 1, and between 0.01 and 10
  849. * @param {{x: Number, y: Number}} pointer Position on screen
  850. * @return {Number} appliedScale scale is limited within the boundaries
  851. * @private
  852. */
  853. Graph.prototype._zoom = function(scale, pointer) {
  854. var scaleOld = this._getScale();
  855. if (scale < 0.00001) {
  856. scale = 0.00001;
  857. }
  858. if (scale > 10) {
  859. scale = 10;
  860. }
  861. // + this.frame.canvas.clientHeight / 2
  862. var translation = this._getTranslation();
  863. var scaleFrac = scale / scaleOld;
  864. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  865. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  866. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  867. "y" : this._canvasToY(pointer.y)};
  868. this._setScale(scale);
  869. this._setTranslation(tx, ty);
  870. this.updateClustersDefault();
  871. this._redraw();
  872. return scale;
  873. };
  874. /**
  875. * Event handler for mouse wheel event, used to zoom the timeline
  876. * See http://adomas.org/javascript-mouse-wheel/
  877. * https://github.com/EightMedia/hammer.js/issues/256
  878. * @param {MouseEvent} event
  879. * @private
  880. */
  881. Graph.prototype._onMouseWheel = function(event) {
  882. // retrieve delta
  883. var delta = 0;
  884. if (event.wheelDelta) { /* IE/Opera. */
  885. delta = event.wheelDelta/120;
  886. } else if (event.detail) { /* Mozilla case. */
  887. // In Mozilla, sign of delta is different than in IE.
  888. // Also, delta is multiple of 3.
  889. delta = -event.detail/3;
  890. }
  891. // If delta is nonzero, handle it.
  892. // Basically, delta is now positive if wheel was scrolled up,
  893. // and negative, if wheel was scrolled down.
  894. if (delta) {
  895. // calculate the new scale
  896. var scale = this._getScale();
  897. var zoom = delta / 10;
  898. if (delta < 0) {
  899. zoom = zoom / (1 - zoom);
  900. }
  901. scale *= (1 + zoom);
  902. // calculate the pointer location
  903. var gesture = util.fakeGesture(this, event);
  904. var pointer = this._getPointer(gesture.center);
  905. // apply the new scale
  906. this._zoom(scale, pointer);
  907. }
  908. // Prevent default actions caused by mouse wheel.
  909. event.preventDefault();
  910. };
  911. /**
  912. * Mouse move handler for checking whether the title moves over a node with a title.
  913. * @param {Event} event
  914. * @private
  915. */
  916. Graph.prototype._onMouseMoveTitle = function (event) {
  917. var gesture = util.fakeGesture(this, event);
  918. var pointer = this._getPointer(gesture.center);
  919. // check if the previously selected node is still selected
  920. if (this.popupNode) {
  921. this._checkHidePopup(pointer);
  922. }
  923. // start a timeout that will check if the mouse is positioned above
  924. // an element
  925. var me = this;
  926. var checkShow = function() {
  927. me._checkShowPopup(pointer);
  928. };
  929. if (this.popupTimer) {
  930. clearInterval(this.popupTimer); // stop any running calculationTimer
  931. }
  932. if (!this.drag.dragging) {
  933. this.popupTimer = setTimeout(checkShow, 300);
  934. }
  935. };
  936. /**
  937. * Check if there is an element on the given position in the graph
  938. * (a node or edge). If so, and if this element has a title,
  939. * show a popup window with its title.
  940. *
  941. * @param {{x:Number, y:Number}} pointer
  942. * @private
  943. */
  944. Graph.prototype._checkShowPopup = function (pointer) {
  945. var obj = {
  946. left: this._canvasToX(pointer.x),
  947. top: this._canvasToY(pointer.y),
  948. right: this._canvasToX(pointer.x),
  949. bottom: this._canvasToY(pointer.y)
  950. };
  951. var id;
  952. var lastPopupNode = this.popupNode;
  953. if (this.popupNode == undefined) {
  954. // search the nodes for overlap, select the top one in case of multiple nodes
  955. var nodes = this.nodes;
  956. for (id in nodes) {
  957. if (nodes.hasOwnProperty(id)) {
  958. var node = nodes[id];
  959. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  960. this.popupNode = node;
  961. break;
  962. }
  963. }
  964. }
  965. }
  966. if (this.popupNode === undefined) {
  967. // search the edges for overlap
  968. var edges = this.edges;
  969. for (id in edges) {
  970. if (edges.hasOwnProperty(id)) {
  971. var edge = edges[id];
  972. if (edge.connected && (edge.getTitle() !== undefined) &&
  973. edge.isOverlappingWith(obj)) {
  974. this.popupNode = edge;
  975. break;
  976. }
  977. }
  978. }
  979. }
  980. if (this.popupNode) {
  981. // show popup message window
  982. if (this.popupNode != lastPopupNode) {
  983. var me = this;
  984. if (!me.popup) {
  985. me.popup = new Popup(me.frame);
  986. }
  987. // adjust a small offset such that the mouse cursor is located in the
  988. // bottom left location of the popup, and you can easily move over the
  989. // popup area
  990. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  991. me.popup.setText(me.popupNode.getTitle());
  992. me.popup.show();
  993. }
  994. }
  995. else {
  996. if (this.popup) {
  997. this.popup.hide();
  998. }
  999. }
  1000. };
  1001. /**
  1002. * Check if the popup must be hided, which is the case when the mouse is no
  1003. * longer hovering on the object
  1004. * @param {{x:Number, y:Number}} pointer
  1005. * @private
  1006. */
  1007. Graph.prototype._checkHidePopup = function (pointer) {
  1008. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  1009. this.popupNode = undefined;
  1010. if (this.popup) {
  1011. this.popup.hide();
  1012. }
  1013. }
  1014. };
  1015. /**
  1016. * Set a new size for the graph
  1017. * @param {string} width Width in pixels or percentage (for example '800px'
  1018. * or '50%')
  1019. * @param {string} height Height in pixels or percentage (for example '400px'
  1020. * or '30%')
  1021. */
  1022. Graph.prototype.setSize = function(width, height) {
  1023. this.frame.style.width = width;
  1024. this.frame.style.height = height;
  1025. this.frame.canvas.style.width = '100%';
  1026. this.frame.canvas.style.height = '100%';
  1027. this.frame.canvas.width = this.frame.canvas.clientWidth;
  1028. this.frame.canvas.height = this.frame.canvas.clientHeight;
  1029. if (this.manipulationDiv !== undefined) {
  1030. this.manipulationDiv.style.width = this.frame.canvas.clientWidth;
  1031. }
  1032. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  1033. };
  1034. /**
  1035. * Set a data set with nodes for the graph
  1036. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  1037. * @private
  1038. */
  1039. Graph.prototype._setNodes = function(nodes) {
  1040. var oldNodesData = this.nodesData;
  1041. if (nodes instanceof DataSet || nodes instanceof DataView) {
  1042. this.nodesData = nodes;
  1043. }
  1044. else if (nodes instanceof Array) {
  1045. this.nodesData = new DataSet();
  1046. this.nodesData.add(nodes);
  1047. }
  1048. else if (!nodes) {
  1049. this.nodesData = new DataSet();
  1050. }
  1051. else {
  1052. throw new TypeError('Array or DataSet expected');
  1053. }
  1054. if (oldNodesData) {
  1055. // unsubscribe from old dataset
  1056. util.forEach(this.nodesListeners, function (callback, event) {
  1057. oldNodesData.off(event, callback);
  1058. });
  1059. }
  1060. // remove drawn nodes
  1061. this.nodes = {};
  1062. if (this.nodesData) {
  1063. // subscribe to new dataset
  1064. var me = this;
  1065. util.forEach(this.nodesListeners, function (callback, event) {
  1066. me.nodesData.on(event, callback);
  1067. });
  1068. // draw all new nodes
  1069. var ids = this.nodesData.getIds();
  1070. this._addNodes(ids);
  1071. }
  1072. this._updateSelection();
  1073. };
  1074. /**
  1075. * Add nodes
  1076. * @param {Number[] | String[]} ids
  1077. * @private
  1078. */
  1079. Graph.prototype._addNodes = function(ids) {
  1080. var id;
  1081. for (var i = 0, len = ids.length; i < len; i++) {
  1082. id = ids[i];
  1083. var data = this.nodesData.get(id);
  1084. var node = new Node(data, this.images, this.groups, this.constants);
  1085. this.nodes[id] = node; // note: this may replace an existing node
  1086. if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
  1087. var radius = 10 * 0.1*ids.length;
  1088. var angle = 2 * Math.PI * Math.random();
  1089. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  1090. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  1091. // note: no not use node.isMoving() here, as that gives the current
  1092. // velocity of the node, which is zero after creation of the node.
  1093. this.moving = true;
  1094. }
  1095. }
  1096. this._updateNodeIndexList();
  1097. this._updateCalculationNodes();
  1098. this._reconnectEdges();
  1099. this._updateValueRange(this.nodes);
  1100. this.updateLabels();
  1101. };
  1102. /**
  1103. * Update existing nodes, or create them when not yet existing
  1104. * @param {Number[] | String[]} ids
  1105. * @private
  1106. */
  1107. Graph.prototype._updateNodes = function(ids) {
  1108. var nodes = this.nodes,
  1109. nodesData = this.nodesData;
  1110. for (var i = 0, len = ids.length; i < len; i++) {
  1111. var id = ids[i];
  1112. var node = nodes[id];
  1113. var data = nodesData.get(id);
  1114. if (node) {
  1115. // update node
  1116. node.setProperties(data, this.constants);
  1117. }
  1118. else {
  1119. // create node
  1120. node = new Node(properties, this.images, this.groups, this.constants);
  1121. nodes[id] = node;
  1122. if (!node.isFixed()) {
  1123. this.moving = true;
  1124. }
  1125. }
  1126. }
  1127. this._updateNodeIndexList();
  1128. this._reconnectEdges();
  1129. this._updateValueRange(nodes);
  1130. };
  1131. /**
  1132. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  1133. * @param {Number[] | String[]} ids
  1134. * @private
  1135. */
  1136. Graph.prototype._removeNodes = function(ids) {
  1137. var nodes = this.nodes;
  1138. for (var i = 0, len = ids.length; i < len; i++) {
  1139. var id = ids[i];
  1140. delete nodes[id];
  1141. }
  1142. this._updateNodeIndexList();
  1143. this._reconnectEdges();
  1144. this._updateSelection();
  1145. this._updateValueRange(nodes);
  1146. };
  1147. /**
  1148. * Load edges by reading the data table
  1149. * @param {Array | DataSet | DataView} edges The data containing the edges.
  1150. * @private
  1151. * @private
  1152. */
  1153. Graph.prototype._setEdges = function(edges) {
  1154. var oldEdgesData = this.edgesData;
  1155. if (edges instanceof DataSet || edges instanceof DataView) {
  1156. this.edgesData = edges;
  1157. }
  1158. else if (edges instanceof Array) {
  1159. this.edgesData = new DataSet();
  1160. this.edgesData.add(edges);
  1161. }
  1162. else if (!edges) {
  1163. this.edgesData = new DataSet();
  1164. }
  1165. else {
  1166. throw new TypeError('Array or DataSet expected');
  1167. }
  1168. if (oldEdgesData) {
  1169. // unsubscribe from old dataset
  1170. util.forEach(this.edgesListeners, function (callback, event) {
  1171. oldEdgesData.off(event, callback);
  1172. });
  1173. }
  1174. // remove drawn edges
  1175. this.edges = {};
  1176. if (this.edgesData) {
  1177. // subscribe to new dataset
  1178. var me = this;
  1179. util.forEach(this.edgesListeners, function (callback, event) {
  1180. me.edgesData.on(event, callback);
  1181. });
  1182. // draw all new nodes
  1183. var ids = this.edgesData.getIds();
  1184. this._addEdges(ids);
  1185. }
  1186. this._reconnectEdges();
  1187. };
  1188. /**
  1189. * Add edges
  1190. * @param {Number[] | String[]} ids
  1191. * @private
  1192. */
  1193. Graph.prototype._addEdges = function (ids) {
  1194. var edges = this.edges,
  1195. edgesData = this.edgesData;
  1196. for (var i = 0, len = ids.length; i < len; i++) {
  1197. var id = ids[i];
  1198. var oldEdge = edges[id];
  1199. if (oldEdge) {
  1200. oldEdge.disconnect();
  1201. }
  1202. var data = edgesData.get(id, {"showInternalIds" : true});
  1203. edges[id] = new Edge(data, this, this.constants);
  1204. }
  1205. this.moving = true;
  1206. this._updateValueRange(edges);
  1207. this._createBezierNodes();
  1208. this._updateCalculationNodes();
  1209. };
  1210. /**
  1211. * Update existing edges, or create them when not yet existing
  1212. * @param {Number[] | String[]} ids
  1213. * @private
  1214. */
  1215. Graph.prototype._updateEdges = function (ids) {
  1216. var edges = this.edges,
  1217. edgesData = this.edgesData;
  1218. for (var i = 0, len = ids.length; i < len; i++) {
  1219. var id = ids[i];
  1220. var data = edgesData.get(id);
  1221. var edge = edges[id];
  1222. if (edge) {
  1223. // update edge
  1224. edge.disconnect();
  1225. edge.setProperties(data, this.constants);
  1226. edge.connect();
  1227. }
  1228. else {
  1229. // create edge
  1230. edge = new Edge(data, this, this.constants);
  1231. this.edges[id] = edge;
  1232. }
  1233. }
  1234. this._createBezierNodes();
  1235. this.moving = true;
  1236. this._updateValueRange(edges);
  1237. };
  1238. /**
  1239. * Remove existing edges. Non existing ids will be ignored
  1240. * @param {Number[] | String[]} ids
  1241. * @private
  1242. */
  1243. Graph.prototype._removeEdges = function (ids) {
  1244. var edges = this.edges;
  1245. for (var i = 0, len = ids.length; i < len; i++) {
  1246. var id = ids[i];
  1247. var edge = edges[id];
  1248. if (edge) {
  1249. if (edge.via != null) {
  1250. delete this.sectors['support']['nodes'][edge.via.id];
  1251. }
  1252. edge.disconnect();
  1253. delete edges[id];
  1254. }
  1255. }
  1256. this.moving = true;
  1257. this._updateValueRange(edges);
  1258. this._updateCalculationNodes();
  1259. };
  1260. /**
  1261. * Reconnect all edges
  1262. * @private
  1263. */
  1264. Graph.prototype._reconnectEdges = function() {
  1265. var id,
  1266. nodes = this.nodes,
  1267. edges = this.edges;
  1268. for (id in nodes) {
  1269. if (nodes.hasOwnProperty(id)) {
  1270. nodes[id].edges = [];
  1271. }
  1272. }
  1273. for (id in edges) {
  1274. if (edges.hasOwnProperty(id)) {
  1275. var edge = edges[id];
  1276. edge.from = null;
  1277. edge.to = null;
  1278. edge.connect();
  1279. }
  1280. }
  1281. };
  1282. /**
  1283. * Update the values of all object in the given array according to the current
  1284. * value range of the objects in the array.
  1285. * @param {Object} obj An object containing a set of Edges or Nodes
  1286. * The objects must have a method getValue() and
  1287. * setValueRange(min, max).
  1288. * @private
  1289. */
  1290. Graph.prototype._updateValueRange = function(obj) {
  1291. var id;
  1292. // determine the range of the objects
  1293. var valueMin = undefined;
  1294. var valueMax = undefined;
  1295. for (id in obj) {
  1296. if (obj.hasOwnProperty(id)) {
  1297. var value = obj[id].getValue();
  1298. if (value !== undefined) {
  1299. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  1300. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  1301. }
  1302. }
  1303. }
  1304. // adjust the range of all objects
  1305. if (valueMin !== undefined && valueMax !== undefined) {
  1306. for (id in obj) {
  1307. if (obj.hasOwnProperty(id)) {
  1308. obj[id].setValueRange(valueMin, valueMax);
  1309. }
  1310. }
  1311. }
  1312. };
  1313. /**
  1314. * Redraw the graph with the current data
  1315. * chart will be resized too.
  1316. */
  1317. Graph.prototype.redraw = function() {
  1318. this.setSize(this.width, this.height);
  1319. this._redraw();
  1320. };
  1321. /**
  1322. * Redraw the graph with the current data
  1323. * @private
  1324. */
  1325. Graph.prototype._redraw = function() {
  1326. var ctx = this.frame.canvas.getContext('2d');
  1327. // clear the canvas
  1328. var w = this.frame.canvas.width;
  1329. var h = this.frame.canvas.height;
  1330. ctx.clearRect(0, 0, w, h);
  1331. // set scaling and translation
  1332. ctx.save();
  1333. ctx.translate(this.translation.x, this.translation.y);
  1334. ctx.scale(this.scale, this.scale);
  1335. this.canvasTopLeft = {
  1336. "x": this._canvasToX(0),
  1337. "y": this._canvasToY(0)
  1338. };
  1339. this.canvasBottomRight = {
  1340. "x": this._canvasToX(this.frame.canvas.clientWidth),
  1341. "y": this._canvasToY(this.frame.canvas.clientHeight)
  1342. };
  1343. this._doInAllSectors("_drawAllSectorNodes",ctx);
  1344. this._doInAllSectors("_drawEdges",ctx);
  1345. this._doInAllSectors("_drawNodes",ctx,false);
  1346. // this._doInSupportSector("_drawNodes",ctx,true);
  1347. // this._drawTree(ctx,"#F00F0F");
  1348. // restore original scaling and translation
  1349. ctx.restore();
  1350. };
  1351. /**
  1352. * Set the translation of the graph
  1353. * @param {Number} offsetX Horizontal offset
  1354. * @param {Number} offsetY Vertical offset
  1355. * @private
  1356. */
  1357. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  1358. if (this.translation === undefined) {
  1359. this.translation = {
  1360. x: 0,
  1361. y: 0
  1362. };
  1363. }
  1364. if (offsetX !== undefined) {
  1365. this.translation.x = offsetX;
  1366. }
  1367. if (offsetY !== undefined) {
  1368. this.translation.y = offsetY;
  1369. }
  1370. };
  1371. /**
  1372. * Get the translation of the graph
  1373. * @return {Object} translation An object with parameters x and y, both a number
  1374. * @private
  1375. */
  1376. Graph.prototype._getTranslation = function() {
  1377. return {
  1378. x: this.translation.x,
  1379. y: this.translation.y
  1380. };
  1381. };
  1382. /**
  1383. * Scale the graph
  1384. * @param {Number} scale Scaling factor 1.0 is unscaled
  1385. * @private
  1386. */
  1387. Graph.prototype._setScale = function(scale) {
  1388. this.scale = scale;
  1389. };
  1390. /**
  1391. * Get the current scale of the graph
  1392. * @return {Number} scale Scaling factor 1.0 is unscaled
  1393. * @private
  1394. */
  1395. Graph.prototype._getScale = function() {
  1396. return this.scale;
  1397. };
  1398. /**
  1399. * Convert a horizontal point on the HTML canvas to the x-value of the model
  1400. * @param {number} x
  1401. * @returns {number}
  1402. * @private
  1403. */
  1404. Graph.prototype._canvasToX = function(x) {
  1405. return (x - this.translation.x) / this.scale;
  1406. };
  1407. /**
  1408. * Convert an x-value in the model to a horizontal point on the HTML canvas
  1409. * @param {number} x
  1410. * @returns {number}
  1411. * @private
  1412. */
  1413. Graph.prototype._xToCanvas = function(x) {
  1414. return x * this.scale + this.translation.x;
  1415. };
  1416. /**
  1417. * Convert a vertical point on the HTML canvas to the y-value of the model
  1418. * @param {number} y
  1419. * @returns {number}
  1420. * @private
  1421. */
  1422. Graph.prototype._canvasToY = function(y) {
  1423. return (y - this.translation.y) / this.scale;
  1424. };
  1425. /**
  1426. * Convert an y-value in the model to a vertical point on the HTML canvas
  1427. * @param {number} y
  1428. * @returns {number}
  1429. * @private
  1430. */
  1431. Graph.prototype._yToCanvas = function(y) {
  1432. return y * this.scale + this.translation.y ;
  1433. };
  1434. /**
  1435. * Redraw all nodes
  1436. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  1437. * @param {CanvasRenderingContext2D} ctx
  1438. * @param {Boolean} [alwaysShow]
  1439. * @private
  1440. */
  1441. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  1442. if (alwaysShow === undefined) {
  1443. alwaysShow = false;
  1444. }
  1445. // first draw the unselected nodes
  1446. var nodes = this.nodes;
  1447. var selected = [];
  1448. for (var id in nodes) {
  1449. if (nodes.hasOwnProperty(id)) {
  1450. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  1451. if (nodes[id].isSelected()) {
  1452. selected.push(id);
  1453. }
  1454. else {
  1455. if (nodes[id].inArea() || alwaysShow) {
  1456. nodes[id].draw(ctx);
  1457. }
  1458. }
  1459. }
  1460. }
  1461. // draw the selected nodes on top
  1462. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  1463. if (nodes[selected[s]].inArea() || alwaysShow) {
  1464. nodes[selected[s]].draw(ctx);
  1465. }
  1466. }
  1467. };
  1468. /**
  1469. * Redraw all edges
  1470. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  1471. * @param {CanvasRenderingContext2D} ctx
  1472. * @private
  1473. */
  1474. Graph.prototype._drawEdges = function(ctx) {
  1475. var edges = this.edges;
  1476. for (var id in edges) {
  1477. if (edges.hasOwnProperty(id)) {
  1478. var edge = edges[id];
  1479. edge.setScale(this.scale);
  1480. if (edge.connected) {
  1481. edges[id].draw(ctx);
  1482. }
  1483. }
  1484. }
  1485. };
  1486. /**
  1487. * Find a stable position for all nodes
  1488. * @private
  1489. */
  1490. Graph.prototype._doStabilize = function() {
  1491. // find stable position
  1492. var count = 0;
  1493. while (this.moving && count < this.constants.maxIterations) {
  1494. this._physicsTick();
  1495. count++;
  1496. }
  1497. this.zoomExtent(false,true);
  1498. };
  1499. /**
  1500. * Check if any of the nodes is still moving
  1501. * @param {number} vmin the minimum velocity considered as 'moving'
  1502. * @return {boolean} true if moving, false if non of the nodes is moving
  1503. * @private
  1504. */
  1505. Graph.prototype._isMoving = function(vmin) {
  1506. var nodes = this.nodes;
  1507. for (var id in nodes) {
  1508. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  1509. return true;
  1510. }
  1511. }
  1512. return false;
  1513. };
  1514. /**
  1515. * /**
  1516. * Perform one discrete step for all nodes
  1517. *
  1518. * @private
  1519. */
  1520. Graph.prototype._discreteStepNodes = function() {
  1521. var interval = 0.65;
  1522. var nodes = this.nodes;
  1523. var nodeId;
  1524. if (this.constants.maxVelocity > 0) {
  1525. for (nodeId in nodes) {
  1526. if (nodes.hasOwnProperty(nodeId)) {
  1527. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  1528. }
  1529. }
  1530. }
  1531. else {
  1532. for (nodeId in nodes) {
  1533. if (nodes.hasOwnProperty(nodeId)) {
  1534. nodes[nodeId].discreteStep(interval);
  1535. }
  1536. }
  1537. }
  1538. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  1539. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  1540. this.moving = true;
  1541. }
  1542. else {
  1543. this.moving = this._isMoving(vminCorrected);
  1544. }
  1545. };
  1546. Graph.prototype._physicsTick = function() {
  1547. if (!this.freezeSimulation) {
  1548. if (this.moving) {
  1549. this._doInAllActiveSectors("_initializeForceCalculation");
  1550. if (this.constants.smoothCurves) {
  1551. this._doInSupportSector("_discreteStepNodes");
  1552. }
  1553. this._doInAllActiveSectors("_discreteStepNodes");
  1554. this._findCenter(this._getRange())
  1555. }
  1556. }
  1557. };
  1558. /**
  1559. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  1560. * It reschedules itself at the beginning of the function
  1561. *
  1562. * @private
  1563. */
  1564. Graph.prototype._animationStep = function() {
  1565. // reset the timer so a new scheduled animation step can be set
  1566. this.timer = undefined;
  1567. // handle the keyboad movement
  1568. this._handleNavigation();
  1569. // this schedules a new animation step
  1570. this.start();
  1571. // start the physics simulation
  1572. var calculationTime = Date.now();
  1573. var maxSteps = 1;
  1574. this._physicsTick();
  1575. var timeRequired = Date.now() - calculationTime;
  1576. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxRenderSteps) {
  1577. this._physicsTick();
  1578. timeRequired = Date.now() - calculationTime;
  1579. maxSteps++;
  1580. }
  1581. // start the rendering process
  1582. var renderTime = Date.now();
  1583. this._redraw();
  1584. this.renderTime = Date.now() - renderTime;
  1585. };
  1586. /**
  1587. * Schedule a animation step with the refreshrate interval.
  1588. *
  1589. * @poram {Boolean} runCalculationStep
  1590. */
  1591. Graph.prototype.start = function() {
  1592. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  1593. if (!this.timer) {
  1594. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  1595. }
  1596. }
  1597. else {
  1598. this._redraw();
  1599. }
  1600. };
  1601. /**
  1602. * Move the graph according to the keyboard presses.
  1603. *
  1604. * @private
  1605. */
  1606. Graph.prototype._handleNavigation = function() {
  1607. if (this.xIncrement != 0 || this.yIncrement != 0) {
  1608. var translation = this._getTranslation();
  1609. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  1610. }
  1611. if (this.zoomIncrement != 0) {
  1612. var center = {
  1613. x: this.frame.canvas.clientWidth / 2,
  1614. y: this.frame.canvas.clientHeight / 2
  1615. };
  1616. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  1617. }
  1618. };
  1619. /**
  1620. * Freeze the _animationStep
  1621. */
  1622. Graph.prototype.toggleFreeze = function() {
  1623. if (this.freezeSimulation == false) {
  1624. this.freezeSimulation = true;
  1625. }
  1626. else {
  1627. this.freezeSimulation = false;
  1628. this.start();
  1629. }
  1630. };
  1631. Graph.prototype._configureSmoothCurves = function(disableStart) {
  1632. if (disableStart === undefined) {
  1633. disableStart = true;
  1634. }
  1635. if (this.constants.smoothCurves == true) {
  1636. this._createBezierNodes();
  1637. }
  1638. else {
  1639. // delete the support nodes
  1640. this.sectors['support']['nodes'] = {};
  1641. for (var edgeId in this.edges) {
  1642. if (this.edges.hasOwnProperty(edgeId)) {
  1643. this.edges[edgeId].smooth = false;
  1644. this.edges[edgeId].via = null;
  1645. }
  1646. }
  1647. }
  1648. this._updateCalculationNodes();
  1649. if (!disableStart) {
  1650. this.moving = true;
  1651. this.start();
  1652. }
  1653. };
  1654. Graph.prototype._createBezierNodes = function() {
  1655. if (this.constants.smoothCurves == true) {
  1656. for (var edgeId in this.edges) {
  1657. if (this.edges.hasOwnProperty(edgeId)) {
  1658. var edge = this.edges[edgeId];
  1659. if (edge.via == null) {
  1660. edge.smooth = true;
  1661. var nodeId = "edgeId:".concat(edge.id);
  1662. this.sectors['support']['nodes'][nodeId] = new Node(
  1663. {id:nodeId,
  1664. mass:1,
  1665. shape:'circle',
  1666. internalMultiplier:1
  1667. },{},{},this.constants);
  1668. edge.via = this.sectors['support']['nodes'][nodeId];
  1669. edge.via.parentEdgeId = edge.id;
  1670. edge.positionBezierNode();
  1671. }
  1672. }
  1673. }
  1674. }
  1675. };
  1676. Graph.prototype._initializeMixinLoaders = function () {
  1677. for (var mixinFunction in graphMixinLoaders) {
  1678. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  1679. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  1680. }
  1681. }
  1682. };