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.

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