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.

489 lines
13 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. background:{
  103. enabled: false,
  104. color: 'rgba(111,111,111,1)',
  105. size:10,
  106. dashes: false
  107. },
  108. smooth: {
  109. enabled: true,
  110. type: "dynamic",
  111. forceDirection:'none',
  112. roundness: 0.5
  113. },
  114. title:undefined,
  115. width: 1,
  116. value: undefined
  117. };
  118. util.deepExtend(this.options, this.defaultOptions);
  119. this.bindEventListeners();
  120. }
  121. /**
  122. * Binds event listeners
  123. */
  124. bindEventListeners() {
  125. // this allows external modules to force all dynamic curves to turn static.
  126. this.body.emitter.on("_forceDisableDynamicCurves", (type, emit = true) => {
  127. if (type === 'dynamic') {
  128. type = 'continuous';
  129. }
  130. let dataChanged = false;
  131. for (let edgeId in this.body.edges) {
  132. if (this.body.edges.hasOwnProperty(edgeId)) {
  133. let edge = this.body.edges[edgeId];
  134. let edgeData = this.body.data.edges._data[edgeId];
  135. // only forcibly remove the smooth curve if the data has been set of the edge has the smooth curves defined.
  136. // this is because a change in the global would not affect these curves.
  137. if (edgeData !== undefined) {
  138. let smoothOptions = edgeData.smooth;
  139. if (smoothOptions !== undefined) {
  140. if (smoothOptions.enabled === true && smoothOptions.type === 'dynamic') {
  141. if (type === undefined) {
  142. edge.setOptions({smooth: false});
  143. }
  144. else {
  145. edge.setOptions({smooth: {type: type}});
  146. }
  147. dataChanged = true;
  148. }
  149. }
  150. }
  151. }
  152. }
  153. if (emit === true && dataChanged === true) {
  154. this.body.emitter.emit("_dataChanged");
  155. }
  156. });
  157. // this is called when options of EXISTING nodes or edges have changed.
  158. //
  159. // NOTE: Not true, called when options have NOT changed, for both existing as well as new nodes.
  160. // See update() for logic.
  161. // TODO: Verify and examine the consequences of this. It might still trigger when
  162. // non-option fields have changed, but then reconnecting edges is still useless.
  163. // Alternatively, it might also be called when edges are removed.
  164. //
  165. this.body.emitter.on("_dataUpdated", () => {
  166. this.reconnectEdges();
  167. });
  168. // refresh the edges. Used when reverting from hierarchical layout
  169. this.body.emitter.on("refreshEdges", this.refresh.bind(this));
  170. this.body.emitter.on("refresh", this.refresh.bind(this));
  171. this.body.emitter.on("destroy", () => {
  172. util.forEach(this.edgesListeners, (callback, event) => {
  173. if (this.body.data.edges)
  174. this.body.data.edges.off(event, callback);
  175. });
  176. delete this.body.functions.createEdge;
  177. delete this.edgesListeners.add;
  178. delete this.edgesListeners.update;
  179. delete this.edgesListeners.remove;
  180. delete this.edgesListeners;
  181. });
  182. }
  183. /**
  184. *
  185. * @param {Object} options
  186. */
  187. setOptions(options) {
  188. if (options !== undefined) {
  189. // use the parser from the Edge class to fill in all shorthand notations
  190. Edge.parseOptions(this.options, options, true, this.defaultOptions, true);
  191. // update smooth settings in all edges
  192. let dataChanged = false;
  193. if (options.smooth !== undefined) {
  194. for (let edgeId in this.body.edges) {
  195. if (this.body.edges.hasOwnProperty(edgeId)) {
  196. dataChanged = this.body.edges[edgeId].updateEdgeType() || dataChanged;
  197. }
  198. }
  199. }
  200. // update fonts in all edges
  201. if (options.font !== undefined) {
  202. for (let edgeId in this.body.edges) {
  203. if (this.body.edges.hasOwnProperty(edgeId)) {
  204. this.body.edges[edgeId].updateLabelModule();
  205. }
  206. }
  207. }
  208. // update the state of the variables if needed
  209. if (options.hidden !== undefined || options.physics !== undefined || dataChanged === true) {
  210. this.body.emitter.emit('_dataChanged');
  211. }
  212. }
  213. }
  214. /**
  215. * Load edges by reading the data table
  216. * @param {Array | DataSet | DataView} edges The data containing the edges.
  217. * @param {boolean} [doNotEmit=false]
  218. * @private
  219. */
  220. setData(edges, doNotEmit = false) {
  221. var oldEdgesData = this.body.data.edges;
  222. if (edges instanceof DataSet || edges instanceof DataView) {
  223. this.body.data.edges = edges;
  224. }
  225. else if (Array.isArray(edges)) {
  226. this.body.data.edges = new DataSet();
  227. this.body.data.edges.add(edges);
  228. }
  229. else if (!edges) {
  230. this.body.data.edges = new DataSet();
  231. }
  232. else {
  233. throw new TypeError('Array or DataSet expected');
  234. }
  235. // TODO: is this null or undefined or false?
  236. if (oldEdgesData) {
  237. // unsubscribe from old dataset
  238. util.forEach(this.edgesListeners, (callback, event) => {oldEdgesData.off(event, callback);});
  239. }
  240. // remove drawn edges
  241. this.body.edges = {};
  242. // TODO: is this null or undefined or false?
  243. if (this.body.data.edges) {
  244. // subscribe to new dataset
  245. util.forEach(this.edgesListeners, (callback, event) => {this.body.data.edges.on(event, callback);});
  246. // draw all new nodes
  247. var ids = this.body.data.edges.getIds();
  248. this.add(ids, true);
  249. }
  250. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  251. if (doNotEmit === false) {
  252. this.body.emitter.emit("_dataChanged");
  253. }
  254. }
  255. /**
  256. * Add edges
  257. * @param {number[] | string[]} ids
  258. * @param {boolean} [doNotEmit=false]
  259. * @private
  260. */
  261. add(ids, doNotEmit = false) {
  262. var edges = this.body.edges;
  263. var edgesData = this.body.data.edges;
  264. for (let i = 0; i < ids.length; i++) {
  265. var id = ids[i];
  266. var oldEdge = edges[id];
  267. if (oldEdge) {
  268. oldEdge.disconnect();
  269. }
  270. var data = edgesData.get(id, {"showInternalIds" : true});
  271. edges[id] = this.create(data);
  272. }
  273. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  274. if (doNotEmit === false) {
  275. this.body.emitter.emit("_dataChanged");
  276. }
  277. }
  278. /**
  279. * Update existing edges, or create them when not yet existing
  280. * @param {number[] | string[]} ids
  281. * @private
  282. */
  283. update(ids) {
  284. var edges = this.body.edges;
  285. var edgesData = this.body.data.edges;
  286. var dataChanged = false;
  287. for (var i = 0; i < ids.length; i++) {
  288. var id = ids[i];
  289. var data = edgesData.get(id);
  290. var edge = edges[id];
  291. if (edge !== undefined) {
  292. // update edge
  293. edge.disconnect();
  294. dataChanged = edge.setOptions(data) || dataChanged; // if a support node is added, data can be changed.
  295. edge.connect();
  296. }
  297. else {
  298. // create edge
  299. this.body.edges[id] = this.create(data);
  300. dataChanged = true;
  301. }
  302. }
  303. if (dataChanged === true) {
  304. this.body.emitter.emit('_adjustEdgesForHierarchicalLayout');
  305. this.body.emitter.emit("_dataChanged");
  306. }
  307. else {
  308. this.body.emitter.emit("_dataUpdated");
  309. }
  310. }
  311. /**
  312. * Remove existing edges. Non existing ids will be ignored
  313. * @param {number[] | string[]} ids
  314. * @param {boolean} [emit=true]
  315. * @private
  316. */
  317. remove(ids, emit = true) {
  318. if (ids.length === 0) return; // early out
  319. var edges = this.body.edges;
  320. util.forEach(ids, (id) => {
  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. util.forEach(this.body.edges, (edge, edgeId) => {
  335. let data = this.body.data.edges._data[edgeId];
  336. if (data !== undefined) {
  337. edge.setOptions(data);
  338. }
  339. });
  340. }
  341. /**
  342. *
  343. * @param {Object} properties
  344. * @returns {Edge}
  345. */
  346. create(properties) {
  347. return new Edge(properties, this.body, this.options, this.defaultOptions)
  348. }
  349. /**
  350. * Reconnect all edges
  351. * @private
  352. */
  353. reconnectEdges() {
  354. var id;
  355. var nodes = this.body.nodes;
  356. var edges = this.body.edges;
  357. for (id in nodes) {
  358. if (nodes.hasOwnProperty(id)) {
  359. nodes[id].edges = [];
  360. }
  361. }
  362. for (id in edges) {
  363. if (edges.hasOwnProperty(id)) {
  364. var edge = edges[id];
  365. edge.from = null;
  366. edge.to = null;
  367. edge.connect();
  368. }
  369. }
  370. }
  371. /**
  372. *
  373. * @param {Edge.id} edgeId
  374. * @returns {Array}
  375. */
  376. getConnectedNodes(edgeId) {
  377. let nodeList = [];
  378. if (this.body.edges[edgeId] !== undefined) {
  379. let edge = this.body.edges[edgeId];
  380. if (edge.fromId !== undefined) {nodeList.push(edge.fromId);}
  381. if (edge.toId !== undefined) {nodeList.push(edge.toId);}
  382. }
  383. return nodeList;
  384. }
  385. /**
  386. * There is no direct relation between the nodes and the edges DataSet,
  387. * so the right place to do call this is in the handler for event `_dataUpdated`.
  388. */
  389. _updateState() {
  390. this._addMissingEdges();
  391. this._removeInvalidEdges();
  392. }
  393. /**
  394. * Scan for missing nodes and remove corresponding edges, if any.
  395. * @private
  396. */
  397. _removeInvalidEdges() {
  398. let edgesToDelete = [];
  399. util.forEach(this.body.edges, (edge, id) => {
  400. let toNode = this.body.nodes[edge.toId];
  401. let fromNode = this.body.nodes[edge.fromId];
  402. // Skip clustering edges here, let the Clustering module handle those
  403. if ((toNode !== undefined && toNode.isCluster === true)
  404. || (fromNode !== undefined && fromNode.isCluster === true)) {
  405. return;
  406. }
  407. if (toNode === undefined || fromNode === undefined) {
  408. edgesToDelete.push(id);
  409. }
  410. });
  411. this.remove(edgesToDelete, false);
  412. }
  413. /**
  414. * add all edges from dataset that are not in the cached state
  415. * @private
  416. */
  417. _addMissingEdges() {
  418. let edgesData = this.body.data.edges;
  419. if (edgesData === undefined || edgesData === null) {
  420. return; // No edges DataSet yet; can happen on startup
  421. }
  422. let edges = this.body.edges;
  423. let addIds = [];
  424. edgesData.forEach((edgeData, edgeId) => {
  425. let edge = edges[edgeId];
  426. if(edge===undefined) {
  427. addIds.push(edgeId);
  428. }
  429. });
  430. this.add(addIds,true);
  431. }
  432. }
  433. export default EdgesHandler;