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.

514 lines
14 KiB

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
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
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. let util = require("../../util");
  2. let DataSet = require('../../DataSet');
  3. let DataView = require('../../DataView');
  4. var Node = require("./components/Node").default;
  5. /**
  6. * Handler for Nodes
  7. */
  8. class NodesHandler {
  9. /**
  10. * @param {Object} body
  11. * @param {Images} images
  12. * @param {Array.<Group>} groups
  13. * @param {LayoutEngine} layoutEngine
  14. */
  15. constructor(body, images, groups, layoutEngine) {
  16. this.body = body;
  17. this.images = images;
  18. this.groups = groups;
  19. this.layoutEngine = layoutEngine;
  20. // create the node API in the body container
  21. this.body.functions.createNode = this.create.bind(this);
  22. this.nodesListeners = {
  23. add: (event, params) => { this.add(params.items); },
  24. update: (event, params) => { this.update(params.items, params.data, params.oldData); },
  25. remove: (event, params) => { this.remove(params.items); }
  26. };
  27. this.defaultOptions = {
  28. borderWidth: 1,
  29. borderWidthSelected: 2,
  30. brokenImage: undefined,
  31. color: {
  32. border: '#2B7CE9',
  33. background: '#97C2FC',
  34. highlight: {
  35. border: '#2B7CE9',
  36. background: '#D2E5FF'
  37. },
  38. hover: {
  39. border: '#2B7CE9',
  40. background: '#D2E5FF'
  41. }
  42. },
  43. fixed: {
  44. x: false,
  45. y: false
  46. },
  47. font: {
  48. color: '#343434',
  49. size: 14, // px
  50. face: 'arial',
  51. background: 'none',
  52. strokeWidth: 0, // px
  53. strokeColor: '#ffffff',
  54. align: 'center',
  55. vadjust: 0,
  56. multi: false,
  57. bold: {
  58. mod: 'bold'
  59. },
  60. boldital: {
  61. mod: 'bold italic'
  62. },
  63. ital: {
  64. mod: 'italic'
  65. },
  66. mono: {
  67. mod: '',
  68. size: 15, // px
  69. face: 'monospace',
  70. vadjust: 2
  71. }
  72. },
  73. group: undefined,
  74. hidden: false,
  75. icon: {
  76. face: 'FontAwesome', //'FontAwesome',
  77. code: undefined, //'\uf007',
  78. size: 50, //50,
  79. color: '#2B7CE9' //'#aa00ff'
  80. },
  81. image: undefined, // --> URL
  82. label: undefined,
  83. labelHighlightBold: true,
  84. level: undefined,
  85. margin: {
  86. top: 5,
  87. right: 5,
  88. bottom: 5,
  89. left: 5
  90. },
  91. mass: 1,
  92. physics: true,
  93. scaling: {
  94. min: 10,
  95. max: 30,
  96. label: {
  97. enabled: false,
  98. min: 14,
  99. max: 30,
  100. maxVisible: 30,
  101. drawThreshold: 5
  102. },
  103. customScalingFunction: function (min, max, total, value) {
  104. if (max === min) {
  105. return 0.5;
  106. }
  107. else {
  108. let scale = 1 / (max - min);
  109. return Math.max(0, (value - min) * scale);
  110. }
  111. }
  112. },
  113. shadow: {
  114. enabled: false,
  115. color: 'rgba(0,0,0,0.5)',
  116. size: 10,
  117. x: 5,
  118. y: 5
  119. },
  120. shape: 'ellipse',
  121. shapeProperties: {
  122. borderDashes: false, // only for borders
  123. borderRadius: 6, // only for box shape
  124. interpolation: true, // only for image and circularImage shapes
  125. useImageSize: false, // only for image and circularImage shapes
  126. useBorderWithImage: false // only for image shape
  127. },
  128. size: 25,
  129. title: undefined,
  130. value: undefined,
  131. x: undefined,
  132. y: undefined
  133. };
  134. // Protect from idiocy
  135. if (this.defaultOptions.mass <= 0) {
  136. throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative';
  137. }
  138. this.options = util.bridgeObject(this.defaultOptions);
  139. this.bindEventListeners();
  140. }
  141. /**
  142. * Binds event listeners
  143. */
  144. bindEventListeners() {
  145. // refresh the nodes. Used when reverting from hierarchical layout
  146. this.body.emitter.on('refreshNodes', this.refresh.bind(this));
  147. this.body.emitter.on('refresh', this.refresh.bind(this));
  148. this.body.emitter.on('destroy', () => {
  149. util.forEach(this.nodesListeners, (callback, event) => {
  150. if (this.body.data.nodes)
  151. this.body.data.nodes.off(event, callback);
  152. });
  153. delete this.body.functions.createNode;
  154. delete this.nodesListeners.add;
  155. delete this.nodesListeners.update;
  156. delete this.nodesListeners.remove;
  157. delete this.nodesListeners;
  158. });
  159. }
  160. /**
  161. *
  162. * @param {Object} options
  163. */
  164. setOptions(options) {
  165. if (options !== undefined) {
  166. Node.parseOptions(this.options, options);
  167. // update the shape in all nodes
  168. if (options.shape !== undefined) {
  169. for (let nodeId in this.body.nodes) {
  170. if (this.body.nodes.hasOwnProperty(nodeId)) {
  171. this.body.nodes[nodeId].updateShape();
  172. }
  173. }
  174. }
  175. // update the font in all nodes
  176. if (options.font !== undefined) {
  177. for (let nodeId in this.body.nodes) {
  178. if (this.body.nodes.hasOwnProperty(nodeId)) {
  179. this.body.nodes[nodeId].updateLabelModule();
  180. this.body.nodes[nodeId].needsRefresh();
  181. }
  182. }
  183. }
  184. // update the shape size in all nodes
  185. if (options.size !== undefined) {
  186. for (let nodeId in this.body.nodes) {
  187. if (this.body.nodes.hasOwnProperty(nodeId)) {
  188. this.body.nodes[nodeId].needsRefresh();
  189. }
  190. }
  191. }
  192. // update the state of the variables if needed
  193. if (options.hidden !== undefined || options.physics !== undefined) {
  194. this.body.emitter.emit('_dataChanged');
  195. }
  196. }
  197. }
  198. /**
  199. * Set a data set with nodes for the network
  200. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  201. * @param {boolean} [doNotEmit=false]
  202. * @private
  203. */
  204. setData(nodes, doNotEmit = false) {
  205. let oldNodesData = this.body.data.nodes;
  206. if (nodes instanceof DataSet || nodes instanceof DataView) {
  207. this.body.data.nodes = nodes;
  208. }
  209. else if (Array.isArray(nodes)) {
  210. this.body.data.nodes = new DataSet();
  211. this.body.data.nodes.add(nodes);
  212. }
  213. else if (!nodes) {
  214. this.body.data.nodes = new DataSet();
  215. }
  216. else {
  217. throw new TypeError('Array or DataSet expected');
  218. }
  219. if (oldNodesData) {
  220. // unsubscribe from old dataset
  221. util.forEach(this.nodesListeners, function (callback, event) {
  222. oldNodesData.off(event, callback);
  223. });
  224. }
  225. // remove drawn nodes
  226. this.body.nodes = {};
  227. if (this.body.data.nodes) {
  228. // subscribe to new dataset
  229. let me = this;
  230. util.forEach(this.nodesListeners, function (callback, event) {
  231. me.body.data.nodes.on(event, callback);
  232. });
  233. // draw all new nodes
  234. let ids = this.body.data.nodes.getIds();
  235. this.add(ids, true);
  236. }
  237. if (doNotEmit === false) {
  238. this.body.emitter.emit("_dataChanged");
  239. }
  240. }
  241. /**
  242. * Add nodes
  243. * @param {number[] | string[]} ids
  244. * @param {boolean} [doNotEmit=false]
  245. * @private
  246. */
  247. add(ids, doNotEmit = false) {
  248. let id;
  249. let newNodes = [];
  250. for (let i = 0; i < ids.length; i++) {
  251. id = ids[i];
  252. let properties = this.body.data.nodes.get(id);
  253. let node = this.create(properties);
  254. newNodes.push(node);
  255. this.body.nodes[id] = node; // note: this may replace an existing node
  256. }
  257. this.layoutEngine.positionInitially(newNodes);
  258. if (doNotEmit === false) {
  259. this.body.emitter.emit("_dataChanged");
  260. }
  261. }
  262. /**
  263. * Update existing nodes, or create them when not yet existing
  264. * @param {number[] | string[]} ids id's of changed nodes
  265. * @param {Array} changedData array with changed data
  266. * @param {Array|undefined} oldData optional; array with previous data
  267. * @private
  268. */
  269. update(ids, changedData, oldData) {
  270. let nodes = this.body.nodes;
  271. let dataChanged = false;
  272. for (let i = 0; i < ids.length; i++) {
  273. let id = ids[i];
  274. let node = nodes[id];
  275. let data = changedData[i];
  276. if (node !== undefined) {
  277. // update node
  278. if (node.setOptions(data)) {
  279. dataChanged = true;
  280. }
  281. }
  282. else {
  283. dataChanged = true;
  284. // create node
  285. node = this.create(data);
  286. nodes[id] = node;
  287. }
  288. }
  289. if (!dataChanged && oldData !== undefined) {
  290. // Check for any changes which should trigger a layout recalculation
  291. // For now, this is just 'level' for hierarchical layout
  292. // Assumption: old and new data arranged in same order; at time of writing, this holds.
  293. dataChanged = changedData.some(function(newValue, index) {
  294. let oldValue = oldData[index];
  295. return (oldValue && oldValue.level !== newValue.level);
  296. });
  297. }
  298. if (dataChanged === true) {
  299. this.body.emitter.emit("_dataChanged");
  300. }
  301. else {
  302. this.body.emitter.emit("_dataUpdated");
  303. }
  304. }
  305. /**
  306. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  307. * @param {number[] | string[]} ids
  308. * @private
  309. */
  310. remove(ids) {
  311. let nodes = this.body.nodes;
  312. for (let i = 0; i < ids.length; i++) {
  313. let id = ids[i];
  314. delete nodes[id];
  315. }
  316. this.body.emitter.emit("_dataChanged");
  317. }
  318. /**
  319. * create a node
  320. * @param {Object} properties
  321. * @param {class} [constructorClass=Node.default]
  322. * @returns {*}
  323. */
  324. create(properties, constructorClass = Node) {
  325. return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions)
  326. }
  327. /**
  328. *
  329. * @param {boolean} [clearPositions=false]
  330. */
  331. refresh(clearPositions = false) {
  332. util.forEach(this.body.nodes, (node, nodeId) => {
  333. let data = this.body.data.nodes.get(nodeId);
  334. if (data !== undefined) {
  335. if (clearPositions === true) {
  336. node.setOptions({x:null, y:null});
  337. }
  338. node.setOptions({ fixed: false });
  339. node.setOptions(data);
  340. }
  341. });
  342. }
  343. /**
  344. * Returns the positions of the nodes.
  345. * @param {Array.<Node.id>|String} [ids] --> optional, can be array of nodeIds, can be string
  346. * @returns {{}}
  347. */
  348. getPositions(ids) {
  349. let dataArray = {};
  350. if (ids !== undefined) {
  351. if (Array.isArray(ids) === true) {
  352. for (let i = 0; i < ids.length; i++) {
  353. if (this.body.nodes[ids[i]] !== undefined) {
  354. let node = this.body.nodes[ids[i]];
  355. dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  356. }
  357. }
  358. }
  359. else {
  360. if (this.body.nodes[ids] !== undefined) {
  361. let node = this.body.nodes[ids];
  362. dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
  363. }
  364. }
  365. }
  366. else {
  367. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  368. let node = this.body.nodes[this.body.nodeIndices[i]];
  369. dataArray[this.body.nodeIndices[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  370. }
  371. }
  372. return dataArray;
  373. }
  374. /**
  375. * Load the XY positions of the nodes into the dataset.
  376. */
  377. storePositions() {
  378. // todo: add support for clusters and hierarchical.
  379. let dataArray = [];
  380. var dataset = this.body.data.nodes.getDataSet();
  381. for (let nodeId in dataset._data) {
  382. if (dataset._data.hasOwnProperty(nodeId)) {
  383. let node = this.body.nodes[nodeId];
  384. if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
  385. dataArray.push({ id: node.id, x: Math.round(node.x), y: Math.round(node.y) });
  386. }
  387. }
  388. }
  389. dataset.update(dataArray);
  390. }
  391. /**
  392. * get the bounding box of a node.
  393. * @param {Node.id} nodeId
  394. * @returns {j|*}
  395. */
  396. getBoundingBox(nodeId) {
  397. if (this.body.nodes[nodeId] !== undefined) {
  398. return this.body.nodes[nodeId].shape.boundingBox;
  399. }
  400. }
  401. /**
  402. * Get the Ids of nodes connected to this node.
  403. * @param {Node.id} nodeId
  404. * @param {'to'|'from'|undefined} direction values 'from' and 'to' select respectively parent and child nodes only.
  405. * Any other value returns both parent and child nodes.
  406. * @returns {Array}
  407. */
  408. getConnectedNodes(nodeId, direction) {
  409. let nodeList = [];
  410. if (this.body.nodes[nodeId] !== undefined) {
  411. let node = this.body.nodes[nodeId];
  412. let nodeObj = {}; // used to quickly check if node already exists
  413. for (let i = 0; i < node.edges.length; i++) {
  414. let edge = node.edges[i];
  415. if (direction !== 'to' && edge.toId == node.id) { // these are double equals since ids can be numeric or string
  416. if (nodeObj[edge.fromId] === undefined) {
  417. nodeList.push(edge.fromId);
  418. nodeObj[edge.fromId] = true;
  419. }
  420. }
  421. else if (direction !== 'from' && edge.fromId == node.id) { // these are double equals since ids can be numeric or string
  422. if (nodeObj[edge.toId] === undefined) {
  423. nodeList.push(edge.toId);
  424. nodeObj[edge.toId] = true;
  425. }
  426. }
  427. }
  428. }
  429. return nodeList;
  430. }
  431. /**
  432. * Get the ids of the edges connected to this node.
  433. * @param {Node.id} nodeId
  434. * @returns {*}
  435. */
  436. getConnectedEdges(nodeId) {
  437. let edgeList = [];
  438. if (this.body.nodes[nodeId] !== undefined) {
  439. let node = this.body.nodes[nodeId];
  440. for (let i = 0; i < node.edges.length; i++) {
  441. edgeList.push(node.edges[i].id)
  442. }
  443. }
  444. else {
  445. console.log("NodeId provided for getConnectedEdges does not exist. Provided: ", nodeId);
  446. }
  447. return edgeList;
  448. }
  449. /**
  450. * Move a node.
  451. *
  452. * @param {Node.id} nodeId
  453. * @param {number} x
  454. * @param {number} y
  455. */
  456. moveNode(nodeId, x, y) {
  457. if (this.body.nodes[nodeId] !== undefined) {
  458. this.body.nodes[nodeId].x = Number(x);
  459. this.body.nodes[nodeId].y = Number(y);
  460. setTimeout(() => {this.body.emitter.emit("startSimulation")},0);
  461. }
  462. else {
  463. console.log("Node id supplied to moveNode does not exist. Provided: ", nodeId);
  464. }
  465. }
  466. }
  467. export default NodesHandler;