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.

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