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

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 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;