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.

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