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.

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