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.

474 lines
13 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. var util = require("../../util");
  2. var DataSet = require('../../DataSet');
  3. var DataView = require('../../DataView');
  4. var Edge = require("./components/Edge").default;
  5. var Label = require("./components/shared/Label").default;
  6. /**
  7. * Handler for Edges
  8. */
  9. class EdgesHandler {
  10. /**
  11. * @param {Object} body
  12. * @param {Array.<Image>} images
  13. * @param {Array.<Group>} groups
  14. */
  15. constructor(body, images, groups) {
  16. this.body = body;
  17. this.images = images;
  18. this.groups = groups;
  19. // create the edge API in the body container
  20. this.body.functions.createEdge = this.create.bind(this);
  21. this.edgesListeners = {
  22. add: (event, params) => {this.add(params.items);},
  23. update: (event, params) => {this.update(params.items);},
  24. remove: (event, params) => {this.remove(params.items);}
  25. };
  26. this.options = {};
  27. this.defaultOptions = {
  28. arrows: {
  29. to: {enabled: false, scaleFactor:1, type: 'arrow'}, // boolean / {arrowScaleFactor:1} / {enabled: false, arrowScaleFactor:1}
  30. middle: {enabled: false, scaleFactor:1, type: 'arrow'},
  31. from: {enabled: false, scaleFactor:1, type: 'arrow'}
  32. },
  33. arrowStrikethrough: true,
  34. color: {
  35. color:'#848484',
  36. highlight:'#848484',
  37. hover: '#848484',
  38. inherit: 'from',
  39. opacity:1.0
  40. },
  41. dashes: false,
  42. font: {
  43. color: '#343434',
  44. size: 14, // px
  45. face: 'arial',
  46. background: 'none',
  47. strokeWidth: 2, // px
  48. strokeColor: '#ffffff',
  49. align:'horizontal',
  50. multi: false,
  51. vadjust: 0,
  52. bold: {
  53. mod: 'bold'
  54. },
  55. boldital: {
  56. mod: 'bold italic'
  57. },
  58. ital: {
  59. mod: 'italic'
  60. },
  61. mono: {
  62. mod: '',
  63. size: 15, // px
  64. face: 'courier new',
  65. vadjust: 2
  66. }
  67. },
  68. hidden: false,
  69. hoverWidth: 1.5,
  70. label: undefined,
  71. labelHighlightBold: true,
  72. length: undefined,
  73. physics: true,
  74. scaling:{
  75. min: 1,
  76. max: 15,
  77. label: {
  78. enabled: true,
  79. min: 14,
  80. max: 30,
  81. maxVisible: 30,
  82. drawThreshold: 5
  83. },
  84. customScalingFunction: function (min,max,total,value) {
  85. if (max === min) {
  86. return 0.5;
  87. }
  88. else {
  89. var scale = 1 / (max - min);
  90. return Math.max(0,(value - min)*scale);
  91. }
  92. }
  93. },
  94. selectionWidth: 1.5,
  95. selfReferenceSize:20,
  96. shadow:{
  97. enabled: false,
  98. color: 'rgba(0,0,0,0.5)',
  99. size:10,
  100. x:5,
  101. y:5
  102. },
  103. smooth: {
  104. enabled: true,
  105. type: "dynamic",
  106. forceDirection:'none',
  107. roundness: 0.5
  108. },
  109. title:undefined,
  110. width: 1,
  111. value: undefined
  112. };
  113. util.deepExtend(this.options, this.defaultOptions);
  114. this.bindEventListeners();
  115. }
  116. /**
  117. * Binds event listeners
  118. */
  119. bindEventListeners() {
  120. // this allows external modules to force all dynamic curves to turn static.
  121. this.body.emitter.on("_forceDisableDynamicCurves", (type, emit = true) => {
  122. if (type === 'dynamic') {
  123. type = 'continuous';
  124. }
  125. let dataChanged = false;
  126. for (let edgeId in this.body.edges) {
  127. if (this.body.edges.hasOwnProperty(edgeId)) {
  128. let edge = this.body.edges[edgeId];
  129. let edgeData = this.body.data.edges._data[edgeId];
  130. // only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.
  131. // this is because a change in the global would not affect these curves.
  132. if (edgeData !== undefined) {
  133. let smoothOptions = edgeData.smooth;
  134. if (smoothOptions !== undefined) {
  135. if (smoothOptions.enabled === true && smoothOptions.type === 'dynamic') {
  136. if (type === undefined) {
  137. edge.setOptions({smooth: false});
  138. }
  139. else {
  140. edge.setOptions({smooth: {type: type}});
  141. }
  142. dataChanged = true;
  143. }
  144. }
  145. }
  146. }
  147. }
  148. if (emit === true && dataChanged === true) {
  149. this.body.emitter.emit("_dataChanged");
  150. }
  151. });
  152. // this is called when options of EXISTING nodes or edges have changed.
  153. //
  154. // NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.
  155. // See update() for logic.
  156. // TODO: Verify and examine the consequences of this. It might still trigger when
  157. // non-option fields have changed, but then reconnecting edges is still useless.
  158. // Alternatively, it might also be called when edges are removed.
  159. //
  160. this.body.emitter.on("_dataUpdated", () => {
  161. this.reconnectEdges();
  162. });
  163. // refresh the edges. Used when reverting from hierarchical layout
  164. this.body.emitter.on("refreshEdges", this.refresh.bind(this));
  165. this.body.emitter.on("refresh", this.refresh.bind(this));
  166. this.body.emitter.on("destroy", () => {
  167. util.forEach(this.edgesListeners, (callback, event) => {
  168. if (this.body.data.edges)
  169. this.body.data.edges.off(event, callback);
  170. });
  171. delete this.body.functions.createEdge;
  172. delete this.edgesListeners.add;
  173. delete this.edgesListeners.update;
  174. delete this.edgesListeners.remove;
  175. delete this.edgesListeners;
  176. });
  177. }
  178. /**
  179. *
  180. * @param {Object} options
  181. */
  182. setOptions(options) {
  183. this.edgeOptions = options;
  184. if (options !== undefined) {
  185. // use the parser from the Edge class to fill in all shorthand notations
  186. Edge.parseOptions(this.options, options, true, this.defaultOptions, true);
  187. // update smooth settings in all edges
  188. let dataChanged = false;
  189. if (options.smooth !== undefined) {
  190. for (let edgeId in this.body.edges) {
  191. if (this.body.edges.hasOwnProperty(edgeId)) {
  192. dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged;
  193. }
  194. }
  195. }
  196. // update fonts in all edges
  197. if (options.font !== undefined) {
  198. // use the parser from the Label class to fill in all shorthand notations
  199. Label.parseOptions(this.options.font, options);
  200. for (let edgeId in this.body.edges) {
  201. if (this.body.edges.hasOwnProperty(edgeId)) {
  202. this.body.edges[edgeId].updateLabelModule();
  203. }
  204. }
  205. }
  206. // update the state of the variables if needed
  207. if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) {
  208. this.body.emitter.emit('_dataChanged');
  209. }
  210. }
  211. }
  212. /**
  213. * Load edges by reading the data table
  214. * @param {Array | DataSet | DataView} edges The data containing the edges.
  215. * @param {boolean} [doNotEmit=false]
  216. * @private
  217. */
  218. setData(edges, doNotEmit = false) {
  219. var oldEdgesData = this.body.data.edges;
  220. if (edges instanceof DataSet || edges instanceof DataView) {
  221. this.body.data.edges = edges;
  222. }
  223. else if (Array.isArray(edges)) {
  224. this.body.data.edges = new DataSet();
  225. this.body.data.edges.add(edges);
  226. }
  227. else if (!edges) {
  228. this.body.data.edges = new DataSet();
  229. }
  230. else {
  231. throw new TypeError('Array or DataSet expected');
  232. }
  233. // TODO: is this null or undefined or false?
  234. if (oldEdgesData) {
  235. // unsubscribe from old dataset
  236. util.forEach(this.edgesListeners, (callback, event) => {oldEdgesData.off(event, callback);});
  237. }
  238. // remove drawn edges
  239. this.body.edges = {};
  240. // TODO: is this null or undefined or false?
  241. if (this.body.data.edges) {
  242. // subscribe to new dataset
  243. util.forEach(this.edgesListeners, (callback, event) => {this.body.data.edges.on(event, callback);});
  244. // draw all new nodes
  245. var ids = this.body.data.edges.getIds();
  246. this.add(ids, true);
  247. }
  248. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  249. if (doNotEmit === false) {
  250. this.body.emitter.emit("_dataChanged");
  251. }
  252. }
  253. /**
  254. * Add edges
  255. * @param {number[] | string[]} ids
  256. * @param {boolean} [doNotEmit=false]
  257. * @private
  258. */
  259. add(ids, doNotEmit = false) {
  260. var edges = this.body.edges;
  261. var edgesData = this.body.data.edges;
  262. for (let i = 0; i < ids.length; i++) {
  263. var id = ids[i];
  264. var oldEdge = edges[id];
  265. if (oldEdge) {
  266. oldEdge.disconnect();
  267. }
  268. var data = edgesData.get(id, {"showInternalIds" : true});
  269. edges[id] = this.create(data);
  270. }
  271. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  272. if (doNotEmit === false) {
  273. this.body.emitter.emit("_dataChanged");
  274. }
  275. }
  276. /**
  277. * Update existing edges, or create them when not yet existing
  278. * @param {number[] | string[]} ids
  279. * @private
  280. */
  281. update(ids) {
  282. var edges = this.body.edges;
  283. var edgesData = this.body.data.edges;
  284. var dataChanged = false;
  285. for (var i = 0; i < ids.length; i++) {
  286. var id = ids[i];
  287. var data = edgesData.get(id);
  288. var edge = edges[id];
  289. if (edge !== undefined) {
  290. // update edge
  291. edge.disconnect();
  292. dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed.
  293. edge.connect();
  294. }
  295. else {
  296. // create edge
  297. this.body.edges[id] = this.create(data);
  298. dataChanged = true;
  299. }
  300. }
  301. if (dataChanged === true) {
  302. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  303. this.body.emitter.emit("_dataChanged");
  304. }
  305. else {
  306. this.body.emitter.emit("_dataUpdated");
  307. }
  308. }
  309. /**
  310. * Remove existing edges. Non existing ids will be ignored
  311. * @param {number[] | string[]} ids
  312. * @param {boolean} [emit=true]
  313. * @private
  314. */
  315. remove(ids, emit = true) {
  316. if (ids.length === 0) return; // early out
  317. var edges = this.body.edges;
  318. for (var i = 0; i < ids.length; i++) {
  319. var id = ids[i];
  320. var edge = edges[id];
  321. if (edge !== undefined) {
  322. edge.remove();
  323. }
  324. }
  325. if (emit) {
  326. this.body.emitter.emit("_dataChanged");
  327. }
  328. }
  329. /**
  330. * Refreshes Edge Handler
  331. */
  332. refresh() {
  333. let edges = this.body.edges;
  334. for (let edgeId in edges) {
  335. let edge = undefined;
  336. if (edges.hasOwnProperty(edgeId)) {
  337. edge = edges[edgeId];
  338. }
  339. let data = this.body.data.edges._data[edgeId];
  340. if (edge !== undefined && data !== undefined) {
  341. edge.setOptions(data);
  342. }
  343. }
  344. }
  345. /**
  346. *
  347. * @param {Object} properties
  348. * @returns {Edge}
  349. */
  350. create(properties) {
  351. // It is not at all clear why all these separate options should be passed:
  352. //
  353. // - this.edgeOptions is set in setOptions()
  354. // the value of which is also added to this.options with parseOptions()
  355. // - this.defaultOptions has been added to this.options with util.extend() in ctor
  356. //
  357. // So, in theory, this.options should be enough.
  358. // The only reason I can think of for this, is that precedence is important.
  359. // TODO: make unit tests for this, to check if edgeOptions and defaultOptions are redundant
  360. //
  361. return new Edge(properties, this.body, this.options, this.defaultOptions, this.edgeOptions)
  362. }
  363. /**
  364. * Reconnect all edges
  365. * @private
  366. */
  367. reconnectEdges() {
  368. var id;
  369. var nodes = this.body.nodes;
  370. var edges = this.body.edges;
  371. for (id in nodes) {
  372. if (nodes.hasOwnProperty(id)) {
  373. nodes[id].edges = [];
  374. }
  375. }
  376. for (id in edges) {
  377. if (edges.hasOwnProperty(id)) {
  378. var edge = edges[id];
  379. edge.from = null;
  380. edge.to = null;
  381. edge.connect();
  382. }
  383. }
  384. }
  385. /**
  386. *
  387. * @param {Edge.id} edgeId
  388. * @returns {Array}
  389. */
  390. getConnectedNodes(edgeId) {
  391. let nodeList = [];
  392. if (this.body.edges[edgeId] !== undefined) {
  393. let edge = this.body.edges[edgeId];
  394. if (edge.fromId !== undefined) {nodeList.push(edge.fromId);}
  395. if (edge.toId !== undefined) {nodeList.push(edge.toId);}
  396. }
  397. return nodeList;
  398. }
  399. /**
  400. * Scan for missing nodes and remove corresponding edges, if any.
  401. *
  402. * There is no direct relation between the nodes and the edges DataSet,
  403. * so the right place to do call this is in the handler for event `_dataUpdated`.
  404. */
  405. _updateState() {
  406. let edgesToDelete = [];
  407. for(let id in this.body.edges) {
  408. let edge = this.body.edges[id];
  409. let toNode = this.body.nodes[edge.toId];
  410. let fromNode = this.body.nodes[edge.fromId];
  411. // Skip clustering edges here, let the Clustering module handle those
  412. if ((toNode !== undefined && toNode.isCluster === true)
  413. || (fromNode !== undefined && fromNode.isCluster === true)) {
  414. continue;
  415. }
  416. if (toNode === undefined || fromNode === undefined) {
  417. edgesToDelete.push(id);
  418. }
  419. }
  420. this.remove(edgesToDelete, false);
  421. }
  422. }
  423. export default EdgesHandler;