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.

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