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.

1019 lines
35 KiB

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