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.

964 lines
33 KiB

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