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.

1012 lines
34 KiB

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