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.

479 lines
12 KiB

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