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.

423 lines
11 KiB

9 years ago
9 years ago
9 years ago
  1. let util = require("../../util");
  2. let DataSet = require('../../DataSet');
  3. let DataView = require('../../DataView');
  4. import Node from "./components/Node";
  5. import Label from "./components/shared/Label";
  6. class NodesHandler {
  7. constructor(body, images, groups, layoutEngine) {
  8. this.body = body;
  9. this.images = images;
  10. this.groups = groups;
  11. this.layoutEngine = layoutEngine;
  12. // create the node API in the body container
  13. this.body.functions.createNode = this.create.bind(this);
  14. this.nodesListeners = {
  15. add: (event, params) => { this.add(params.items); },
  16. update: (event, params) => { this.update(params.items, params.data); },
  17. remove: (event, params) => { this.remove(params.items); }
  18. };
  19. this.options = {};
  20. this.defaultOptions = {
  21. borderWidth: 1,
  22. borderWidthSelected: 2,
  23. brokenImage: undefined,
  24. color: {
  25. border: '#2B7CE9',
  26. background: '#97C2FC',
  27. highlight: {
  28. border: '#2B7CE9',
  29. background: '#D2E5FF'
  30. },
  31. hover: {
  32. border: '#2B7CE9',
  33. background: '#D2E5FF'
  34. }
  35. },
  36. fixed: {
  37. x: false,
  38. y: false
  39. },
  40. font: {
  41. color: '#343434',
  42. size: 14, // px
  43. face: 'arial',
  44. background: 'none',
  45. strokeWidth: 0, // px
  46. strokeColor: '#ffffff',
  47. align: 'horizontal'
  48. },
  49. group: undefined,
  50. hidden: false,
  51. icon: {
  52. face: 'FontAwesome', //'FontAwesome',
  53. code: undefined, //'\uf007',
  54. size: 50, //50,
  55. color: '#2B7CE9' //'#aa00ff'
  56. },
  57. image: undefined, // --> URL
  58. label: undefined,
  59. labelHighlightBold: true,
  60. level: undefined,
  61. mass: 1,
  62. physics: true,
  63. scaling: {
  64. min: 10,
  65. max: 30,
  66. label: {
  67. enabled: false,
  68. min: 14,
  69. max: 30,
  70. maxVisible: 30,
  71. drawThreshold: 5
  72. },
  73. customScalingFunction: function (min, max, total, value) {
  74. if (max === min) {
  75. return 0.5;
  76. }
  77. else {
  78. let scale = 1 / (max - min);
  79. return Math.max(0, (value - min) * scale);
  80. }
  81. }
  82. },
  83. shadow: {
  84. enabled: false,
  85. size: 10,
  86. x: 5,
  87. y: 5
  88. },
  89. shape: 'ellipse',
  90. shapeProperties: {
  91. borderDashes: false
  92. },
  93. size: 25,
  94. title: undefined,
  95. value: undefined,
  96. x: undefined,
  97. y: undefined
  98. };
  99. util.extend(this.options, this.defaultOptions);
  100. this.bindEventListeners();
  101. }
  102. bindEventListeners() {
  103. // refresh the nodes. Used when reverting from hierarchical layout
  104. this.body.emitter.on('refreshNodes', this.refresh.bind(this));
  105. this.body.emitter.on('refresh', this.refresh.bind(this));
  106. this.body.emitter.on('destroy', () => {
  107. delete this.body.functions.createNode;
  108. delete this.nodesListeners.add;
  109. delete this.nodesListeners.update;
  110. delete this.nodesListeners.remove;
  111. delete this.nodesListeners;
  112. });
  113. }
  114. setOptions(options) {
  115. if (options !== undefined) {
  116. Node.parseOptions(this.options, options);
  117. // update the shape in all nodes
  118. if (options.shape !== undefined) {
  119. for (let nodeId in this.body.nodes) {
  120. if (this.body.nodes.hasOwnProperty(nodeId)) {
  121. this.body.nodes[nodeId].updateShape();
  122. }
  123. }
  124. }
  125. // update the shape size in all nodes
  126. if (options.font !== undefined) {
  127. Label.parseOptions(this.options.font, options);
  128. for (let nodeId in this.body.nodes) {
  129. if (this.body.nodes.hasOwnProperty(nodeId)) {
  130. this.body.nodes[nodeId].updateLabelModule();
  131. this.body.nodes[nodeId]._reset();
  132. }
  133. }
  134. }
  135. // update the shape size in all nodes
  136. if (options.size !== undefined) {
  137. for (let nodeId in this.body.nodes) {
  138. if (this.body.nodes.hasOwnProperty(nodeId)) {
  139. this.body.nodes[nodeId]._reset();
  140. }
  141. }
  142. }
  143. // update the state of the letiables if needed
  144. if (options.hidden !== undefined || options.physics !== undefined) {
  145. this.body.emitter.emit('_dataChanged');
  146. }
  147. }
  148. }
  149. /**
  150. * Set a data set with nodes for the network
  151. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  152. * @private
  153. */
  154. setData(nodes, doNotEmit = false) {
  155. let oldNodesData = this.body.data.nodes;
  156. if (nodes instanceof DataSet || nodes instanceof DataView) {
  157. this.body.data.nodes = nodes;
  158. }
  159. else if (Array.isArray(nodes)) {
  160. this.body.data.nodes = new DataSet();
  161. this.body.data.nodes.add(nodes);
  162. }
  163. else if (!nodes) {
  164. this.body.data.nodes = new DataSet();
  165. }
  166. else {
  167. throw new TypeError('Array or DataSet expected');
  168. }
  169. if (oldNodesData) {
  170. // unsubscribe from old dataset
  171. util.forEach(this.nodesListeners, function (callback, event) {
  172. oldNodesData.off(event, callback);
  173. });
  174. }
  175. // remove drawn nodes
  176. this.body.nodes = {};
  177. if (this.body.data.nodes) {
  178. // subscribe to new dataset
  179. let me = this;
  180. util.forEach(this.nodesListeners, function (callback, event) {
  181. me.body.data.nodes.on(event, callback);
  182. });
  183. // draw all new nodes
  184. let ids = this.body.data.nodes.getIds();
  185. this.add(ids, true);
  186. }
  187. if (doNotEmit === false) {
  188. this.body.emitter.emit("_dataChanged");
  189. }
  190. }
  191. /**
  192. * Add nodes
  193. * @param {Number[] | String[]} ids
  194. * @private
  195. */
  196. add(ids, doNotEmit = false) {
  197. let id;
  198. let newNodes = [];
  199. for (let i = 0; i < ids.length; i++) {
  200. id = ids[i];
  201. let properties = this.body.data.nodes.get(id);
  202. let node = this.create(properties);
  203. newNodes.push(node);
  204. this.body.nodes[id] = node; // note: this may replace an existing node
  205. }
  206. this.layoutEngine.positionInitially(newNodes);
  207. if (doNotEmit === false) {
  208. this.body.emitter.emit("_dataChanged");
  209. }
  210. }
  211. /**
  212. * Update existing nodes, or create them when not yet existing
  213. * @param {Number[] | String[]} ids
  214. * @private
  215. */
  216. update(ids, changedData) {
  217. let nodes = this.body.nodes;
  218. let dataChanged = false;
  219. for (let i = 0; i < ids.length; i++) {
  220. let id = ids[i];
  221. let node = nodes[id];
  222. let data = changedData[i];
  223. if (node !== undefined) {
  224. // update node
  225. dataChanged = node.setOptions(data);
  226. }
  227. else {
  228. dataChanged = true;
  229. // create node
  230. node = this.create(data);
  231. nodes[id] = node;
  232. }
  233. }
  234. if (dataChanged === true) {
  235. this.body.emitter.emit("_dataChanged");
  236. }
  237. else {
  238. this.body.emitter.emit("_dataUpdated");
  239. }
  240. }
  241. /**
  242. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  243. * @param {Number[] | String[]} ids
  244. * @private
  245. */
  246. remove(ids) {
  247. let nodes = this.body.nodes;
  248. for (let i = 0; i < ids.length; i++) {
  249. let id = ids[i];
  250. delete nodes[id];
  251. }
  252. this.body.emitter.emit("_dataChanged");
  253. }
  254. /**
  255. * create a node
  256. * @param properties
  257. * @param constructorClass
  258. */
  259. create(properties, constructorClass = Node) {
  260. return new constructorClass(properties, this.body, this.images, this.groups, this.options)
  261. }
  262. refresh() {
  263. let nodes = this.body.nodes;
  264. for (let nodeId in nodes) {
  265. let node = undefined;
  266. if (nodes.hasOwnProperty(nodeId)) {
  267. node = nodes[nodeId];
  268. }
  269. let data = this.body.data.nodes._data[nodeId];
  270. if (node !== undefined && data !== undefined) {
  271. node.setOptions({ fixed: false });
  272. node.setOptions(data);
  273. }
  274. }
  275. }
  276. /**
  277. * Returns the positions of the nodes.
  278. * @param ids --> optional, can be array of nodeIds, can be string
  279. * @returns {{}}
  280. */
  281. getPositions(ids) {
  282. let dataArray = {};
  283. if (ids !== undefined) {
  284. if (Array.isArray(ids) === true) {
  285. for (let i = 0; i < ids.length; i++) {
  286. if (this.body.nodes[ids[i]] !== undefined) {
  287. let node = this.body.nodes[ids[i]];
  288. dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  289. }
  290. }
  291. }
  292. else {
  293. if (this.body.nodes[ids] !== undefined) {
  294. let node = this.body.nodes[ids];
  295. dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
  296. }
  297. }
  298. }
  299. else {
  300. for (let nodeId in this.body.nodes) {
  301. if (this.body.nodes.hasOwnProperty(nodeId)) {
  302. let node = this.body.nodes[nodeId];
  303. dataArray[nodeId] = { x: Math.round(node.x), y: Math.round(node.y) };
  304. }
  305. }
  306. }
  307. return dataArray;
  308. }
  309. /**
  310. * Load the XY positions of the nodes into the dataset.
  311. */
  312. storePositions() {
  313. // todo: add support for clusters and hierarchical.
  314. let dataArray = [];
  315. var dataset = this.body.data.nodes.getDataSet();
  316. for (let nodeId in dataset._data) {
  317. if (dataset._data.hasOwnProperty(nodeId)) {
  318. let node = this.body.nodes[nodeId];
  319. if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
  320. dataArray.push({ id: nodeId, x: Math.round(node.x), y: Math.round(node.y) });
  321. }
  322. }
  323. }
  324. dataset.update(dataArray);
  325. }
  326. /**
  327. * get the bounding box of a node.
  328. * @param nodeId
  329. * @returns {j|*}
  330. */
  331. getBoundingBox(nodeId) {
  332. if (this.body.nodes[nodeId] !== undefined) {
  333. return this.body.nodes[nodeId].shape.boundingBox;
  334. }
  335. }
  336. /**
  337. * Get the Ids of nodes connected to this node.
  338. * @param nodeId
  339. * @returns {Array}
  340. */
  341. getConnectedNodes(nodeId) {
  342. let nodeList = [];
  343. if (this.body.nodes[nodeId] !== undefined) {
  344. let node = this.body.nodes[nodeId];
  345. let nodeObj = {}; // used to quickly check if node already exists
  346. for (let i = 0; i < node.edges.length; i++) {
  347. let edge = node.edges[i];
  348. if (edge.toId == nodeId) { // these are double equals since ids can be numeric or string
  349. if (nodeObj[edge.fromId] === undefined) {
  350. nodeList.push(edge.fromId);
  351. nodeObj[edge.fromId] = true;
  352. }
  353. }
  354. else if (edge.fromId == nodeId) { // these are double equals since ids can be numeric or string
  355. if (nodeObj[edge.toId] === undefined) {
  356. nodeList.push(edge.toId);
  357. nodeObj[edge.toId] = true;
  358. }
  359. }
  360. }
  361. }
  362. return nodeList;
  363. }
  364. /**
  365. * Get the ids of the edges connected to this node.
  366. * @param nodeId
  367. * @returns {*}
  368. */
  369. getConnectedEdges(nodeId) {
  370. let edgeList = [];
  371. if (this.body.nodes[nodeId] !== undefined) {
  372. let node = this.body.nodes[nodeId];
  373. for (let i = 0; i < node.edges.length; i++) {
  374. edgeList.push(node.edges[i].id)
  375. }
  376. }
  377. else {
  378. console.log("NodeId provided for getConnectedEdges does not exist. Provided: ", nodeId)
  379. }
  380. return edgeList;
  381. }
  382. }
  383. export default NodesHandler;