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
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", "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;