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.

981 lines
33 KiB

10 years ago
10 years ago
10 years ago
10 years ago
  1. var util = require('../util');
  2. /**
  3. * @class Node
  4. * A node. A node can be connected to other nodes via one or multiple edges.
  5. * @param {object} properties An object containing properties for the node. All
  6. * properties are optional, except for the id.
  7. * {number} id Id of the node. Required
  8. * {string} label Text label for the node
  9. * {number} x Horizontal position of the node
  10. * {number} y Vertical position of the node
  11. * {string} shape Node shape, available:
  12. * "database", "circle", "ellipse",
  13. * "box", "image", "text", "dot",
  14. * "star", "triangle", "triangleDown",
  15. * "square"
  16. * {string} image An image url
  17. * {string} title An title text, can be HTML
  18. * {anytype} group A group name or number
  19. * @param {Network.Images} imagelist A list with images. Only needed
  20. * when the node has an image
  21. * @param {Network.Groups} grouplist A list with groups. Needed for
  22. * retrieving group properties
  23. * @param {Object} constants An object with default values for
  24. * example for the color
  25. *
  26. */
  27. function Node(properties, imagelist, grouplist, constants) {
  28. this.selected = false;
  29. this.hover = false;
  30. this.edges = []; // all edges connected to this node
  31. this.dynamicEdges = [];
  32. this.reroutedEdges = {};
  33. this.group = constants.nodes.group;
  34. this.fontSize = Number(constants.nodes.fontSize);
  35. this.fontFace = constants.nodes.fontFace;
  36. this.fontColor = constants.nodes.fontColor;
  37. this.fontDrawThreshold = 3;
  38. this.color = constants.nodes.color;
  39. // set defaults for the properties
  40. this.id = undefined;
  41. this.shape = constants.nodes.shape;
  42. this.image = constants.nodes.image;
  43. this.x = null;
  44. this.y = null;
  45. this.xFixed = false;
  46. this.yFixed = false;
  47. this.horizontalAlignLeft = true; // these are for the navigation controls
  48. this.verticalAlignTop = true; // these are for the navigation controls
  49. this.radius = constants.nodes.radius;
  50. this.baseRadiusValue = constants.nodes.radius;
  51. this.radiusFixed = false;
  52. this.radiusMin = constants.nodes.radiusMin;
  53. this.radiusMax = constants.nodes.radiusMax;
  54. this.level = -1;
  55. this.preassignedLevel = false;
  56. this.borderWidth = constants.nodes.borderWidth;
  57. this.borderWidthSelected = constants.nodes.borderWidthSelected;
  58. this.imagelist = imagelist;
  59. this.grouplist = grouplist;
  60. // physics properties
  61. this.fx = 0.0; // external force x
  62. this.fy = 0.0; // external force y
  63. this.vx = 0.0; // velocity x
  64. this.vy = 0.0; // velocity y
  65. this.minForce = constants.minForce;
  66. this.damping = constants.physics.damping;
  67. this.mass = 1; // kg
  68. this.fixedData = {x:null,y:null};
  69. this.setProperties(properties, constants);
  70. // creating the variables for clustering
  71. this.resetCluster();
  72. this.dynamicEdgesLength = 0;
  73. this.clusterSession = 0;
  74. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  75. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  76. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  77. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  78. this.growthIndicator = 0;
  79. // variables to tell the node about the network.
  80. this.networkScaleInv = 1;
  81. this.networkScale = 1;
  82. this.canvasTopLeft = {"x": -300, "y": -300};
  83. this.canvasBottomRight = {"x": 300, "y": 300};
  84. this.parentEdgeId = null;
  85. }
  86. /**
  87. * (re)setting the clustering variables and objects
  88. */
  89. Node.prototype.resetCluster = function() {
  90. // clustering variables
  91. this.formationScale = undefined; // this is used to determine when to open the cluster
  92. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  93. this.containedNodes = {};
  94. this.containedEdges = {};
  95. this.clusterSessions = [];
  96. };
  97. /**
  98. * Attach a edge to the node
  99. * @param {Edge} edge
  100. */
  101. Node.prototype.attachEdge = function(edge) {
  102. if (this.edges.indexOf(edge) == -1) {
  103. this.edges.push(edge);
  104. }
  105. if (this.dynamicEdges.indexOf(edge) == -1) {
  106. this.dynamicEdges.push(edge);
  107. }
  108. this.dynamicEdgesLength = this.dynamicEdges.length;
  109. };
  110. /**
  111. * Detach a edge from the node
  112. * @param {Edge} edge
  113. */
  114. Node.prototype.detachEdge = function(edge) {
  115. var index = this.edges.indexOf(edge);
  116. if (index != -1) {
  117. this.edges.splice(index, 1);
  118. this.dynamicEdges.splice(index, 1);
  119. }
  120. this.dynamicEdgesLength = this.dynamicEdges.length;
  121. };
  122. /**
  123. * Set or overwrite properties for the node
  124. * @param {Object} properties an object with properties
  125. * @param {Object} constants and object with default, global properties
  126. */
  127. Node.prototype.setProperties = function(properties, constants) {
  128. if (!properties) {
  129. return;
  130. }
  131. this.originalLabel = undefined;
  132. // basic properties
  133. if (properties.id !== undefined) {this.id = properties.id;}
  134. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  135. if (properties.title !== undefined) {this.title = properties.title;}
  136. if (properties.group !== undefined) {this.group = properties.group;}
  137. if (properties.x !== undefined) {this.x = properties.x;}
  138. if (properties.y !== undefined) {this.y = properties.y;}
  139. if (properties.value !== undefined) {this.value = properties.value;}
  140. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  141. if (properties.borderWidth !== undefined) {this.borderWidth = properties.borderWidth;}
  142. if (properties.borderWidthSelected !== undefined) {this.borderWidthSelected = properties.borderWidthSelected;}
  143. // physics
  144. if (properties.mass !== undefined) {this.mass = properties.mass;}
  145. // navigation controls properties
  146. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  147. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  148. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  149. if (this.id === undefined) {
  150. throw "Node must have an id";
  151. }
  152. // copy group properties
  153. if (this.group !== undefined && this.group != "") {
  154. var groupObj = this.grouplist.get(this.group);
  155. for (var prop in groupObj) {
  156. if (groupObj.hasOwnProperty(prop)) {
  157. this[prop] = groupObj[prop];
  158. }
  159. }
  160. }
  161. // individual shape properties
  162. if (properties.shape !== undefined) {this.shape = properties.shape;}
  163. if (properties.image !== undefined) {this.image = properties.image;}
  164. if (properties.radius !== undefined) {this.radius = properties.radius; this.baseRadiusValue = this.radius;}
  165. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  166. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  167. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  168. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  169. if (this.image !== undefined && this.image != "") {
  170. if (this.imagelist) {
  171. this.imageObj = this.imagelist.load(this.image);
  172. }
  173. else {
  174. throw "No imagelist provided";
  175. }
  176. }
  177. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  178. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  179. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  180. if (this.shape == 'image') {
  181. this.radiusMin = constants.nodes.widthMin;
  182. this.radiusMax = constants.nodes.widthMax;
  183. }
  184. // choose draw method depending on the shape
  185. switch (this.shape) {
  186. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  187. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  188. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  189. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  190. // TODO: add diamond shape
  191. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  192. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  193. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  194. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  195. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  196. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  197. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  198. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  199. }
  200. // reset the size of the node, this can be changed
  201. this._reset();
  202. };
  203. /**
  204. * select this node
  205. */
  206. Node.prototype.select = function() {
  207. this.selected = true;
  208. this._reset();
  209. };
  210. /**
  211. * unselect this node
  212. */
  213. Node.prototype.unselect = function() {
  214. this.selected = false;
  215. this._reset();
  216. };
  217. /**
  218. * Reset the calculated size of the node, forces it to recalculate its size
  219. */
  220. Node.prototype.clearSizeCache = function() {
  221. this._reset();
  222. };
  223. /**
  224. * Reset the calculated size of the node, forces it to recalculate its size
  225. * @private
  226. */
  227. Node.prototype._reset = function() {
  228. this.width = undefined;
  229. this.height = undefined;
  230. };
  231. /**
  232. * get the title of this node.
  233. * @return {string} title The title of the node, or undefined when no title
  234. * has been set.
  235. */
  236. Node.prototype.getTitle = function() {
  237. return typeof this.title === "function" ? this.title() : this.title;
  238. };
  239. /**
  240. * Calculate the distance to the border of the Node
  241. * @param {CanvasRenderingContext2D} ctx
  242. * @param {Number} angle Angle in radians
  243. * @returns {number} distance Distance to the border in pixels
  244. */
  245. Node.prototype.distanceToBorder = function (ctx, angle) {
  246. var borderWidth = 1;
  247. if (!this.width) {
  248. this.resize(ctx);
  249. }
  250. switch (this.shape) {
  251. case 'circle':
  252. case 'dot':
  253. return this.radius + borderWidth;
  254. case 'ellipse':
  255. var a = this.width / 2;
  256. var b = this.height / 2;
  257. var w = (Math.sin(angle) * a);
  258. var h = (Math.cos(angle) * b);
  259. return a * b / Math.sqrt(w * w + h * h);
  260. // TODO: implement distanceToBorder for database
  261. // TODO: implement distanceToBorder for triangle
  262. // TODO: implement distanceToBorder for triangleDown
  263. case 'box':
  264. case 'image':
  265. case 'text':
  266. default:
  267. if (this.width) {
  268. return Math.min(
  269. Math.abs(this.width / 2 / Math.cos(angle)),
  270. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  271. // TODO: reckon with border radius too in case of box
  272. }
  273. else {
  274. return 0;
  275. }
  276. }
  277. // TODO: implement calculation of distance to border for all shapes
  278. };
  279. /**
  280. * Set forces acting on the node
  281. * @param {number} fx Force in horizontal direction
  282. * @param {number} fy Force in vertical direction
  283. */
  284. Node.prototype._setForce = function(fx, fy) {
  285. this.fx = fx;
  286. this.fy = fy;
  287. };
  288. /**
  289. * Add forces acting on the node
  290. * @param {number} fx Force in horizontal direction
  291. * @param {number} fy Force in vertical direction
  292. * @private
  293. */
  294. Node.prototype._addForce = function(fx, fy) {
  295. this.fx += fx;
  296. this.fy += fy;
  297. };
  298. /**
  299. * Perform one discrete step for the node
  300. * @param {number} interval Time interval in seconds
  301. */
  302. Node.prototype.discreteStep = function(interval) {
  303. if (!this.xFixed) {
  304. var dx = this.damping * this.vx; // damping force
  305. var ax = (this.fx - dx) / this.mass; // acceleration
  306. this.vx += ax * interval; // velocity
  307. this.x += this.vx * interval; // position
  308. }
  309. if (!this.yFixed) {
  310. var dy = this.damping * this.vy; // damping force
  311. var ay = (this.fy - dy) / this.mass; // acceleration
  312. this.vy += ay * interval; // velocity
  313. this.y += this.vy * interval; // position
  314. }
  315. };
  316. /**
  317. * Perform one discrete step for the node
  318. * @param {number} interval Time interval in seconds
  319. * @param {number} maxVelocity The speed limit imposed on the velocity
  320. */
  321. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  322. if (!this.xFixed) {
  323. var dx = this.damping * this.vx; // damping force
  324. var ax = (this.fx - dx) / this.mass; // acceleration
  325. this.vx += ax * interval; // velocity
  326. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  327. this.x += this.vx * interval; // position
  328. }
  329. else {
  330. this.fx = 0;
  331. }
  332. if (!this.yFixed) {
  333. var dy = this.damping * this.vy; // damping force
  334. var ay = (this.fy - dy) / this.mass; // acceleration
  335. this.vy += ay * interval; // velocity
  336. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  337. this.y += this.vy * interval; // position
  338. }
  339. else {
  340. this.fy = 0;
  341. }
  342. };
  343. /**
  344. * Check if this node has a fixed x and y position
  345. * @return {boolean} true if fixed, false if not
  346. */
  347. Node.prototype.isFixed = function() {
  348. return (this.xFixed && this.yFixed);
  349. };
  350. /**
  351. * Check if this node is moving
  352. * @param {number} vmin the minimum velocity considered as "moving"
  353. * @return {boolean} true if moving, false if it has no velocity
  354. */
  355. // TODO: replace this method with calculating the kinetic energy
  356. Node.prototype.isMoving = function(vmin) {
  357. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  358. };
  359. /**
  360. * check if this node is selecte
  361. * @return {boolean} selected True if node is selected, else false
  362. */
  363. Node.prototype.isSelected = function() {
  364. return this.selected;
  365. };
  366. /**
  367. * Retrieve the value of the node. Can be undefined
  368. * @return {Number} value
  369. */
  370. Node.prototype.getValue = function() {
  371. return this.value;
  372. };
  373. /**
  374. * Calculate the distance from the nodes location to the given location (x,y)
  375. * @param {Number} x
  376. * @param {Number} y
  377. * @return {Number} value
  378. */
  379. Node.prototype.getDistance = function(x, y) {
  380. var dx = this.x - x,
  381. dy = this.y - y;
  382. return Math.sqrt(dx * dx + dy * dy);
  383. };
  384. /**
  385. * Adjust the value range of the node. The node will adjust it's radius
  386. * based on its value.
  387. * @param {Number} min
  388. * @param {Number} max
  389. */
  390. Node.prototype.setValueRange = function(min, max) {
  391. if (!this.radiusFixed && this.value !== undefined) {
  392. if (max == min) {
  393. this.radius = (this.radiusMin + this.radiusMax) / 2;
  394. }
  395. else {
  396. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  397. this.radius = (this.value - min) * scale + this.radiusMin;
  398. }
  399. }
  400. this.baseRadiusValue = this.radius;
  401. };
  402. /**
  403. * Draw this node in the given canvas
  404. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  405. * @param {CanvasRenderingContext2D} ctx
  406. */
  407. Node.prototype.draw = function(ctx) {
  408. throw "Draw method not initialized for node";
  409. };
  410. /**
  411. * Recalculate the size of this node in the given canvas
  412. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  413. * @param {CanvasRenderingContext2D} ctx
  414. */
  415. Node.prototype.resize = function(ctx) {
  416. throw "Resize method not initialized for node";
  417. };
  418. /**
  419. * Check if this object is overlapping with the provided object
  420. * @param {Object} obj an object with parameters left, top, right, bottom
  421. * @return {boolean} True if location is located on node
  422. */
  423. Node.prototype.isOverlappingWith = function(obj) {
  424. return (this.left < obj.right &&
  425. this.left + this.width > obj.left &&
  426. this.top < obj.bottom &&
  427. this.top + this.height > obj.top);
  428. };
  429. Node.prototype._resizeImage = function (ctx) {
  430. // TODO: pre calculate the image size
  431. if (!this.width || !this.height) { // undefined or 0
  432. var width, height;
  433. if (this.value) {
  434. this.radius = this.baseRadiusValue;
  435. var scale = this.imageObj.height / this.imageObj.width;
  436. if (scale !== undefined) {
  437. width = this.radius || this.imageObj.width;
  438. height = this.radius * scale || this.imageObj.height;
  439. }
  440. else {
  441. width = 0;
  442. height = 0;
  443. }
  444. }
  445. else {
  446. width = this.imageObj.width;
  447. height = this.imageObj.height;
  448. }
  449. this.width = width;
  450. this.height = height;
  451. this.growthIndicator = 0;
  452. if (this.width > 0 && this.height > 0) {
  453. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  454. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  455. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  456. this.growthIndicator = this.width - width;
  457. }
  458. }
  459. };
  460. Node.prototype._drawImage = function (ctx) {
  461. this._resizeImage(ctx);
  462. this.left = this.x - this.width / 2;
  463. this.top = this.y - this.height / 2;
  464. var yLabel;
  465. if (this.imageObj.width != 0 ) {
  466. // draw the shade
  467. if (this.clusterSize > 1) {
  468. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  469. lineWidth *= this.networkScaleInv;
  470. lineWidth = Math.min(0.2 * this.width,lineWidth);
  471. ctx.globalAlpha = 0.5;
  472. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  473. }
  474. // draw the image
  475. ctx.globalAlpha = 1.0;
  476. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  477. yLabel = this.y + this.height / 2;
  478. }
  479. else {
  480. // image still loading... just draw the label for now
  481. yLabel = this.y;
  482. }
  483. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  484. };
  485. Node.prototype._resizeBox = function (ctx) {
  486. if (!this.width) {
  487. var margin = 5;
  488. var textSize = this.getTextSize(ctx);
  489. this.width = textSize.width + 2 * margin;
  490. this.height = textSize.height + 2 * margin;
  491. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  492. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  493. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  494. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  495. }
  496. };
  497. Node.prototype._drawBox = function (ctx) {
  498. this._resizeBox(ctx);
  499. this.left = this.x - this.width / 2;
  500. this.top = this.y - this.height / 2;
  501. var clusterLineWidth = 2.5;
  502. var borderWidth = this.borderWidth;
  503. var selectionLineWidth = this.borderWidthSelected || 2 * this.borderWidth;
  504. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  505. // draw the outer border
  506. if (this.clusterSize > 1) {
  507. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  508. ctx.lineWidth *= this.networkScaleInv;
  509. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  510. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  511. ctx.stroke();
  512. }
  513. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  514. ctx.lineWidth *= this.networkScaleInv;
  515. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  516. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  517. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  518. ctx.fill();
  519. ctx.stroke();
  520. this._label(ctx, this.label, this.x, this.y);
  521. };
  522. Node.prototype._resizeDatabase = function (ctx) {
  523. if (!this.width) {
  524. var margin = 5;
  525. var textSize = this.getTextSize(ctx);
  526. var size = textSize.width + 2 * margin;
  527. this.width = size;
  528. this.height = size;
  529. // scaling used for clustering
  530. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  531. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  532. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  533. this.growthIndicator = this.width - size;
  534. }
  535. };
  536. Node.prototype._drawDatabase = function (ctx) {
  537. this._resizeDatabase(ctx);
  538. this.left = this.x - this.width / 2;
  539. this.top = this.y - this.height / 2;
  540. var clusterLineWidth = 2.5;
  541. var borderWidth = this.borderWidth;
  542. var selectionLineWidth = this.borderWidthSelected || 2 * this.borderWidth;
  543. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  544. // draw the outer border
  545. if (this.clusterSize > 1) {
  546. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  547. ctx.lineWidth *= this.networkScaleInv;
  548. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  549. ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
  550. ctx.stroke();
  551. }
  552. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  553. ctx.lineWidth *= this.networkScaleInv;
  554. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  555. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  556. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  557. ctx.fill();
  558. ctx.stroke();
  559. this._label(ctx, this.label, this.x, this.y);
  560. };
  561. Node.prototype._resizeCircle = function (ctx) {
  562. if (!this.width) {
  563. var margin = 5;
  564. var textSize = this.getTextSize(ctx);
  565. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  566. this.radius = diameter / 2;
  567. this.width = diameter;
  568. this.height = diameter;
  569. // scaling used for clustering
  570. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  571. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  572. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  573. this.growthIndicator = this.radius - 0.5*diameter;
  574. }
  575. };
  576. Node.prototype._drawCircle = function (ctx) {
  577. this._resizeCircle(ctx);
  578. this.left = this.x - this.width / 2;
  579. this.top = this.y - this.height / 2;
  580. var clusterLineWidth = 2.5;
  581. var borderWidth = this.borderWidth;
  582. var selectionLineWidth = this.borderWidthSelected || 2 * this.borderWidth;
  583. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  584. // draw the outer border
  585. if (this.clusterSize > 1) {
  586. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  587. ctx.lineWidth *= this.networkScaleInv;
  588. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  589. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  590. ctx.stroke();
  591. }
  592. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  593. ctx.lineWidth *= this.networkScaleInv;
  594. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  595. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  596. ctx.circle(this.x, this.y, this.radius);
  597. ctx.fill();
  598. ctx.stroke();
  599. this._label(ctx, this.label, this.x, this.y);
  600. };
  601. Node.prototype._resizeEllipse = function (ctx) {
  602. if (!this.width) {
  603. var textSize = this.getTextSize(ctx);
  604. this.width = textSize.width * 1.5;
  605. this.height = textSize.height * 2;
  606. if (this.width < this.height) {
  607. this.width = this.height;
  608. }
  609. var defaultSize = this.width;
  610. // scaling used for clustering
  611. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  612. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  613. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  614. this.growthIndicator = this.width - defaultSize;
  615. }
  616. };
  617. Node.prototype._drawEllipse = function (ctx) {
  618. this._resizeEllipse(ctx);
  619. this.left = this.x - this.width / 2;
  620. this.top = this.y - this.height / 2;
  621. var clusterLineWidth = 2.5;
  622. var borderWidth = this.borderWidth;
  623. var selectionLineWidth = this.borderWidthSelected || 2 * this.borderWidth;
  624. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  625. // draw the outer border
  626. if (this.clusterSize > 1) {
  627. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  628. ctx.lineWidth *= this.networkScaleInv;
  629. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  630. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  631. ctx.stroke();
  632. }
  633. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  634. ctx.lineWidth *= this.networkScaleInv;
  635. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  636. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  637. ctx.ellipse(this.left, this.top, this.width, this.height);
  638. ctx.fill();
  639. ctx.stroke();
  640. this._label(ctx, this.label, this.x, this.y);
  641. };
  642. Node.prototype._drawDot = function (ctx) {
  643. this._drawShape(ctx, 'circle');
  644. };
  645. Node.prototype._drawTriangle = function (ctx) {
  646. this._drawShape(ctx, 'triangle');
  647. };
  648. Node.prototype._drawTriangleDown = function (ctx) {
  649. this._drawShape(ctx, 'triangleDown');
  650. };
  651. Node.prototype._drawSquare = function (ctx) {
  652. this._drawShape(ctx, 'square');
  653. };
  654. Node.prototype._drawStar = function (ctx) {
  655. this._drawShape(ctx, 'star');
  656. };
  657. Node.prototype._resizeShape = function (ctx) {
  658. if (!this.width) {
  659. this.radius = this.baseRadiusValue;
  660. var size = 2 * this.radius;
  661. this.width = size;
  662. this.height = size;
  663. // scaling used for clustering
  664. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  665. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  666. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  667. this.growthIndicator = this.width - size;
  668. }
  669. };
  670. Node.prototype._drawShape = function (ctx, shape) {
  671. this._resizeShape(ctx);
  672. this.left = this.x - this.width / 2;
  673. this.top = this.y - this.height / 2;
  674. var clusterLineWidth = 2.5;
  675. var borderWidth = this.borderWidth;
  676. var selectionLineWidth = this.borderWidthSelected || 2 * this.borderWidth;
  677. var radiusMultiplier = 2;
  678. // choose draw method depending on the shape
  679. switch (shape) {
  680. case 'dot': radiusMultiplier = 2; break;
  681. case 'square': radiusMultiplier = 2; break;
  682. case 'triangle': radiusMultiplier = 3; break;
  683. case 'triangleDown': radiusMultiplier = 3; break;
  684. case 'star': radiusMultiplier = 4; break;
  685. }
  686. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  687. // draw the outer border
  688. if (this.clusterSize > 1) {
  689. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  690. ctx.lineWidth *= this.networkScaleInv;
  691. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  692. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  693. ctx.stroke();
  694. }
  695. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  696. ctx.lineWidth *= this.networkScaleInv;
  697. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  698. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  699. ctx[shape](this.x, this.y, this.radius);
  700. ctx.fill();
  701. ctx.stroke();
  702. if (this.label) {
  703. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
  704. }
  705. };
  706. Node.prototype._resizeText = function (ctx) {
  707. if (!this.width) {
  708. var margin = 5;
  709. var textSize = this.getTextSize(ctx);
  710. this.width = textSize.width + 2 * margin;
  711. this.height = textSize.height + 2 * margin;
  712. // scaling used for clustering
  713. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  714. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  715. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  716. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  717. }
  718. };
  719. Node.prototype._drawText = function (ctx) {
  720. this._resizeText(ctx);
  721. this.left = this.x - this.width / 2;
  722. this.top = this.y - this.height / 2;
  723. this._label(ctx, this.label, this.x, this.y);
  724. };
  725. Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
  726. if (text && this.fontSize * this.networkScale > this.fontDrawThreshold) {
  727. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  728. ctx.fillStyle = this.fontColor || "black";
  729. ctx.textAlign = align || "center";
  730. ctx.textBaseline = baseline || "middle";
  731. var lines = text.split('\n');
  732. var lineCount = lines.length;
  733. var fontSize = (this.fontSize + 4);
  734. var yLine = y + (1 - lineCount) / 2 * fontSize;
  735. if (labelUnderNode == true) {
  736. yLine = y + (1 - lineCount) / (2 * fontSize);
  737. }
  738. for (var i = 0; i < lineCount; i++) {
  739. ctx.fillText(lines[i], x, yLine);
  740. yLine += fontSize;
  741. }
  742. }
  743. };
  744. Node.prototype.getTextSize = function(ctx) {
  745. if (this.label !== undefined) {
  746. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  747. var lines = this.label.split('\n'),
  748. height = (this.fontSize + 4) * lines.length,
  749. width = 0;
  750. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  751. width = Math.max(width, ctx.measureText(lines[i]).width);
  752. }
  753. return {"width": width, "height": height};
  754. }
  755. else {
  756. return {"width": 0, "height": 0};
  757. }
  758. };
  759. /**
  760. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  761. * there is a safety margin of 0.3 * width;
  762. *
  763. * @returns {boolean}
  764. */
  765. Node.prototype.inArea = function() {
  766. if (this.width !== undefined) {
  767. return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
  768. this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
  769. this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
  770. this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
  771. }
  772. else {
  773. return true;
  774. }
  775. };
  776. /**
  777. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  778. * @returns {boolean}
  779. */
  780. Node.prototype.inView = function() {
  781. return (this.x >= this.canvasTopLeft.x &&
  782. this.x < this.canvasBottomRight.x &&
  783. this.y >= this.canvasTopLeft.y &&
  784. this.y < this.canvasBottomRight.y);
  785. };
  786. /**
  787. * This allows the zoom level of the network to influence the rendering
  788. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  789. *
  790. * @param scale
  791. * @param canvasTopLeft
  792. * @param canvasBottomRight
  793. */
  794. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  795. this.networkScaleInv = 1.0/scale;
  796. this.networkScale = scale;
  797. this.canvasTopLeft = canvasTopLeft;
  798. this.canvasBottomRight = canvasBottomRight;
  799. };
  800. /**
  801. * This allows the zoom level of the network to influence the rendering
  802. *
  803. * @param scale
  804. */
  805. Node.prototype.setScale = function(scale) {
  806. this.networkScaleInv = 1.0/scale;
  807. this.networkScale = scale;
  808. };
  809. /**
  810. * set the velocity at 0. Is called when this node is contained in another during clustering
  811. */
  812. Node.prototype.clearVelocity = function() {
  813. this.vx = 0;
  814. this.vy = 0;
  815. };
  816. /**
  817. * Basic preservation of (kinectic) energy
  818. *
  819. * @param massBeforeClustering
  820. */
  821. Node.prototype.updateVelocity = function(massBeforeClustering) {
  822. var energyBefore = this.vx * this.vx * massBeforeClustering;
  823. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  824. this.vx = Math.sqrt(energyBefore/this.mass);
  825. energyBefore = this.vy * this.vy * massBeforeClustering;
  826. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  827. this.vy = Math.sqrt(energyBefore/this.mass);
  828. };
  829. module.exports = Node;