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.

1105 lines
36 KiB

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