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.

1138 lines
39 KiB

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