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.

415 lines
11 KiB

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