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.

1170 lines
40 KiB

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