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.

969 lines
32 KiB

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