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.

989 lines
33 KiB

10 years ago
9 years ago
9 years ago
9 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. * Attach a edge to the node
  72. * @param {Edge} edge
  73. */
  74. Node.prototype.attachEdge = function(edge) {
  75. if (this.edges.indexOf(edge) == -1) {
  76. this.edges.push(edge);
  77. }
  78. };
  79. /**
  80. * Detach a edge from the node
  81. * @param {Edge} edge
  82. */
  83. Node.prototype.detachEdge = function(edge) {
  84. var index = this.edges.indexOf(edge);
  85. if (index != -1) {
  86. this.edges.splice(index, 1);
  87. }
  88. };
  89. /**
  90. * Set or overwrite properties for the node
  91. * @param {Object} properties an object with properties
  92. * @param {Object} constants and object with default, global properties
  93. */
  94. Node.prototype.setProperties = function(properties, constants) {
  95. if (!properties) {
  96. return;
  97. }
  98. this.properties = properties;
  99. var fields = ['borderWidth', 'borderWidthSelected', 'shape', 'image', 'brokenImage', 'radius', 'fontColor',
  100. 'fontSize', 'fontFace', 'fontFill', 'fontStrokeWidth', 'fontStrokeColor', 'group', 'mass', 'fontDrawThreshold',
  101. 'scaleFontWithValue', 'fontSizeMaxVisible', 'customScalingFunction', 'iconFontFace', 'icon', 'iconColor', 'iconSize',
  102. 'value'
  103. ];
  104. util.selectiveDeepExtend(fields, this.options, properties);
  105. // basic properties
  106. if (properties.id !== undefined) {this.id = properties.id;}
  107. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  108. if (properties.title !== undefined) {this.title = properties.title;}
  109. if (properties.x !== undefined) {this.x = properties.x; this.predefinedPosition = true;}
  110. if (properties.y !== undefined) {this.y = properties.y; this.predefinedPosition = true;}
  111. if (properties.value !== undefined) {this.value = properties.value;}
  112. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  113. // navigation controls properties
  114. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  115. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  116. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  117. if (this.id === undefined) {
  118. throw "Node must have an id";
  119. }
  120. // copy group properties
  121. if (typeof properties.group === 'number' || (typeof properties.group === 'string' && properties.group != '')) {
  122. var groupObj = this.grouplist.get(properties.group);
  123. util.deepExtend(this.options, groupObj);
  124. // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case.
  125. this.options.color = util.parseColor(this.options.color);
  126. }
  127. // individual shape properties
  128. if (properties.radius !== undefined) {this.baseRadiusValue = this.options.radius;}
  129. if (properties.color !== undefined) {this.options.color = util.parseColor(properties.color);}
  130. if (this.options.image !== undefined && this.options.image!= "") {
  131. if (this.imagelist) {
  132. this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage);
  133. }
  134. else {
  135. throw "No imagelist provided";
  136. }
  137. }
  138. if (properties.allowedToMoveX !== undefined) {
  139. this.xFixed = !properties.allowedToMoveX;
  140. this.allowedToMoveX = properties.allowedToMoveX;
  141. }
  142. else if (properties.x !== undefined && this.allowedToMoveX == false) {
  143. this.xFixed = true;
  144. }
  145. if (properties.allowedToMoveY !== undefined) {
  146. this.yFixed = !properties.allowedToMoveY;
  147. this.allowedToMoveY = properties.allowedToMoveY;
  148. }
  149. else if (properties.y !== undefined && this.allowedToMoveY == false) {
  150. this.yFixed = true;
  151. }
  152. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  153. if (this.options.shape === 'image' || this.options.shape === 'circularImage') {
  154. this.options.radiusMin = constants.nodes.widthMin;
  155. this.options.radiusMax = constants.nodes.widthMax;
  156. }
  157. // choose draw method depending on the shape
  158. switch (this.options.shape) {
  159. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  160. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  161. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  162. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  163. // TODO: add diamond shape
  164. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  165. case 'circularImage': this.draw = this._drawCircularImage; this.resize = this._resizeCircularImage; break;
  166. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  167. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  168. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  169. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  170. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  171. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  172. case 'icon': this.draw = this._drawIcon; this.resize = this._resizeIcon; break;
  173. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  174. }
  175. // reset the size of the node, this can be changed
  176. this._reset();
  177. };
  178. /**
  179. * select this node
  180. */
  181. Node.prototype.select = function() {
  182. this.selected = true;
  183. this._reset();
  184. };
  185. /**
  186. * unselect this node
  187. */
  188. Node.prototype.unselect = function() {
  189. this.selected = false;
  190. this._reset();
  191. };
  192. /**
  193. * Reset the calculated size of the node, forces it to recalculate its size
  194. */
  195. Node.prototype.clearSizeCache = function() {
  196. this._reset();
  197. };
  198. /**
  199. * Reset the calculated size of the node, forces it to recalculate its size
  200. * @private
  201. */
  202. Node.prototype._reset = function() {
  203. this.width = undefined;
  204. this.height = undefined;
  205. };
  206. /**
  207. * get the title of this node.
  208. * @return {string} title The title of the node, or undefined when no title
  209. * has been set.
  210. */
  211. Node.prototype.getTitle = function() {
  212. return typeof this.title === "function" ? this.title() : this.title;
  213. };
  214. /**
  215. * Calculate the distance to the border of the Node
  216. * @param {CanvasRenderingContext2D} ctx
  217. * @param {Number} angle Angle in radians
  218. * @returns {number} distance Distance to the border in pixels
  219. */
  220. Node.prototype.distanceToBorder = function (ctx, angle) {
  221. var borderWidth = 1;
  222. if (!this.width) {
  223. this.resize(ctx);
  224. }
  225. switch (this.options.shape) {
  226. case 'circle':
  227. case 'dot':
  228. return this.options.radius+ borderWidth;
  229. case 'ellipse':
  230. var a = this.width / 2;
  231. var b = this.height / 2;
  232. var w = (Math.sin(angle) * a);
  233. var h = (Math.cos(angle) * b);
  234. return a * b / Math.sqrt(w * w + h * h);
  235. // TODO: implement distanceToBorder for database
  236. // TODO: implement distanceToBorder for triangle
  237. // TODO: implement distanceToBorder for triangleDown
  238. case 'box':
  239. case 'image':
  240. case 'text':
  241. default:
  242. if (this.width) {
  243. return Math.min(
  244. Math.abs(this.width / 2 / Math.cos(angle)),
  245. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  246. // TODO: reckon with border radius too in case of box
  247. }
  248. else {
  249. return 0;
  250. }
  251. }
  252. // TODO: implement calculation of distance to border for all shapes
  253. };
  254. /**
  255. * Check if this node has a fixed x and y position
  256. * @return {boolean} true if fixed, false if not
  257. */
  258. Node.prototype.isFixed = function() {
  259. return (this.xFixed && this.yFixed);
  260. };
  261. /**
  262. * check if this node is selecte
  263. * @return {boolean} selected True if node is selected, else false
  264. */
  265. Node.prototype.isSelected = function() {
  266. return this.selected;
  267. };
  268. /**
  269. * Retrieve the value of the node. Can be undefined
  270. * @return {Number} value
  271. */
  272. Node.prototype.getValue = function() {
  273. return this.value;
  274. };
  275. /**
  276. * Calculate the distance from the nodes location to the given location (x,y)
  277. * @param {Number} x
  278. * @param {Number} y
  279. * @return {Number} value
  280. */
  281. Node.prototype.getDistance = function(x, y) {
  282. var dx = this.x - x,
  283. dy = this.y - y;
  284. return Math.sqrt(dx * dx + dy * dy);
  285. };
  286. /**
  287. * Adjust the value range of the node. The node will adjust it's radius
  288. * based on its value.
  289. * @param {Number} min
  290. * @param {Number} max
  291. */
  292. Node.prototype.setValueRange = function(min, max, total) {
  293. if (!this.radiusFixed && this.value !== undefined) {
  294. var scale = this.options.customScalingFunction(min, max, total, this.value);
  295. var radiusDiff = this.options.radiusMax - this.options.radiusMin;
  296. if (this.options.scaleFontWithValue == true) {
  297. var fontDiff = this.options.fontSizeMax - this.options.fontSizeMin;
  298. this.options.fontSize = this.options.fontSizeMin + scale * fontDiff;
  299. }
  300. this.options.radius = this.options.radiusMin + scale * radiusDiff;
  301. }
  302. this.baseRadiusValue = this.options.radius;
  303. };
  304. /**
  305. * Draw this node in the given canvas
  306. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  307. * @param {CanvasRenderingContext2D} ctx
  308. */
  309. Node.prototype.draw = function(ctx) {
  310. throw "Draw method not initialized for node";
  311. };
  312. /**
  313. * Recalculate the size of this node in the given canvas
  314. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  315. * @param {CanvasRenderingContext2D} ctx
  316. */
  317. Node.prototype.resize = function(ctx) {
  318. throw "Resize method not initialized for node";
  319. };
  320. /**
  321. * Check if this object is overlapping with the provided object
  322. * @param {Object} obj an object with parameters left, top, right, bottom
  323. * @return {boolean} True if location is located on node
  324. */
  325. Node.prototype.isOverlappingWith = function(obj) {
  326. return (this.left < obj.right &&
  327. this.left + this.width > obj.left &&
  328. this.top < obj.bottom &&
  329. this.top + this.height > obj.top);
  330. };
  331. Node.prototype._resizeImage = function (ctx) {
  332. // TODO: pre calculate the image size
  333. if (!this.width || !this.height) { // undefined or 0
  334. var width, height;
  335. if (this.value) {
  336. this.options.radius= this.baseRadiusValue;
  337. var scale = this.imageObj.height / this.imageObj.width;
  338. if (scale !== undefined) {
  339. width = this.options.radius|| this.imageObj.width;
  340. height = this.options.radius* scale || this.imageObj.height;
  341. }
  342. else {
  343. width = 0;
  344. height = 0;
  345. }
  346. }
  347. else {
  348. width = this.imageObj.width;
  349. height = this.imageObj.height;
  350. }
  351. this.width = width;
  352. this.height = height;
  353. }
  354. };
  355. Node.prototype._drawImageAtPosition = function (ctx) {
  356. if (this.imageObj.width != 0 ) {
  357. // draw the image
  358. ctx.globalAlpha = 1.0;
  359. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  360. }
  361. };
  362. Node.prototype._drawImageLabel = function (ctx) {
  363. var yLabel;
  364. var offset = 0;
  365. if (this.height){
  366. offset = this.height / 2;
  367. var labelDimensions = this.getTextSize(ctx);
  368. if (labelDimensions.lineCount >= 1){
  369. offset += labelDimensions.height / 2;
  370. offset += 3;
  371. }
  372. }
  373. yLabel = this.y + offset;
  374. this._label(ctx, this.label, this.x, yLabel, undefined);
  375. };
  376. Node.prototype._drawImage = function (ctx) {
  377. this._resizeImage(ctx);
  378. this.left = this.x - this.width / 2;
  379. this.top = this.y - this.height / 2;
  380. this._drawImageAtPosition(ctx);
  381. this.boundingBox.top = this.top;
  382. this.boundingBox.left = this.left;
  383. this.boundingBox.right = this.left + this.width;
  384. this.boundingBox.bottom = this.top + this.height;
  385. this._drawImageLabel(ctx);
  386. this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  387. this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  388. this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  389. };
  390. Node.prototype._resizeCircularImage = function (ctx) {
  391. if(!this.imageObj.src || !this.imageObj.width || !this.imageObj.height){
  392. if (!this.width) {
  393. var diameter = this.options.radius * 2;
  394. this.width = diameter;
  395. this.height = diameter;
  396. this._swapToImageResizeWhenImageLoaded = true;
  397. }
  398. }
  399. else {
  400. if (this._swapToImageResizeWhenImageLoaded) {
  401. this.width = 0;
  402. this.height = 0;
  403. delete this._swapToImageResizeWhenImageLoaded;
  404. }
  405. this._resizeImage(ctx);
  406. }
  407. };
  408. Node.prototype._drawCircularImage = function (ctx) {
  409. this._resizeCircularImage(ctx);
  410. this.left = this.x - this.width / 2;
  411. this.top = this.y - this.height / 2;
  412. var centerX = this.left + (this.width / 2);
  413. var centerY = this.top + (this.height / 2);
  414. var radius = Math.abs(this.height / 2);
  415. this._drawRawCircle(ctx, centerX, centerY, radius);
  416. ctx.save();
  417. ctx.circle(this.x, this.y, radius);
  418. ctx.stroke();
  419. ctx.clip();
  420. this._drawImageAtPosition(ctx);
  421. ctx.restore();
  422. this.boundingBox.top = this.y - this.options.radius;
  423. this.boundingBox.left = this.x - this.options.radius;
  424. this.boundingBox.right = this.x + this.options.radius;
  425. this.boundingBox.bottom = this.y + this.options.radius;
  426. this._drawImageLabel(ctx);
  427. this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  428. this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  429. this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  430. };
  431. Node.prototype._resizeBox = function (ctx) {
  432. if (!this.width) {
  433. var margin = 5;
  434. var textSize = this.getTextSize(ctx);
  435. this.width = textSize.width + 2 * margin;
  436. this.height = textSize.height + 2 * margin;
  437. }
  438. };
  439. Node.prototype._drawBox = function (ctx) {
  440. this._resizeBox(ctx);
  441. this.left = this.x - this.width / 2;
  442. this.top = this.y - this.height / 2;
  443. var borderWidth = this.options.borderWidth;
  444. var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  445. ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  446. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
  447. ctx.lineWidth *= this.networkScaleInv;
  448. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  449. ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  450. ctx.roundRect(this.left, this.top, this.width, this.height, this.options.radius);
  451. ctx.fill();
  452. ctx.stroke();
  453. this.boundingBox.top = this.top;
  454. this.boundingBox.left = this.left;
  455. this.boundingBox.right = this.left + this.width;
  456. this.boundingBox.bottom = this.top + this.height;
  457. this._label(ctx, this.label, this.x, this.y);
  458. };
  459. Node.prototype._resizeDatabase = function (ctx) {
  460. if (!this.width) {
  461. var margin = 5;
  462. var textSize = this.getTextSize(ctx);
  463. var size = textSize.width + 2 * margin;
  464. this.width = size;
  465. this.height = size;
  466. }
  467. };
  468. Node.prototype._drawDatabase = function (ctx) {
  469. this._resizeDatabase(ctx);
  470. this.left = this.x - this.width / 2;
  471. this.top = this.y - this.height / 2;
  472. var borderWidth = this.options.borderWidth;
  473. var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  474. ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  475. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
  476. ctx.lineWidth *= this.networkScaleInv;
  477. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  478. ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  479. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  480. ctx.fill();
  481. ctx.stroke();
  482. this.boundingBox.top = this.top;
  483. this.boundingBox.left = this.left;
  484. this.boundingBox.right = this.left + this.width;
  485. this.boundingBox.bottom = this.top + this.height;
  486. this._label(ctx, this.label, this.x, this.y);
  487. };
  488. Node.prototype._resizeCircle = function (ctx) {
  489. if (!this.width) {
  490. var margin = 5;
  491. var textSize = this.getTextSize(ctx);
  492. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  493. this.options.radius = diameter / 2;
  494. this.width = diameter;
  495. this.height = diameter;
  496. }
  497. };
  498. Node.prototype._drawRawCircle = function (ctx, x, y, radius) {
  499. var borderWidth = this.options.borderWidth;
  500. var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  501. ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  502. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
  503. ctx.lineWidth *= this.networkScaleInv;
  504. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  505. ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  506. ctx.circle(this.x, this.y, radius);
  507. ctx.fill();
  508. ctx.stroke();
  509. };
  510. Node.prototype._drawCircle = function (ctx) {
  511. this._resizeCircle(ctx);
  512. this.left = this.x - this.width / 2;
  513. this.top = this.y - this.height / 2;
  514. this._drawRawCircle(ctx, this.x, this.y, this.options.radius);
  515. this.boundingBox.top = this.y - this.options.radius;
  516. this.boundingBox.left = this.x - this.options.radius;
  517. this.boundingBox.right = this.x + this.options.radius;
  518. this.boundingBox.bottom = this.y + this.options.radius;
  519. this._label(ctx, this.label, this.x, this.y);
  520. };
  521. Node.prototype._resizeEllipse = function (ctx) {
  522. if (!this.width) {
  523. var textSize = this.getTextSize(ctx);
  524. this.width = textSize.width * 1.5;
  525. this.height = textSize.height * 2;
  526. if (this.width < this.height) {
  527. this.width = this.height;
  528. }
  529. var defaultSize = this.width;
  530. }
  531. };
  532. Node.prototype._drawEllipse = function (ctx) {
  533. this._resizeEllipse(ctx);
  534. this.left = this.x - this.width / 2;
  535. this.top = this.y - this.height / 2;
  536. var borderWidth = this.options.borderWidth;
  537. var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  538. ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  539. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
  540. ctx.lineWidth *= this.networkScaleInv;
  541. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  542. ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  543. ctx.ellipse(this.left, this.top, this.width, this.height);
  544. ctx.fill();
  545. ctx.stroke();
  546. this.boundingBox.top = this.top;
  547. this.boundingBox.left = this.left;
  548. this.boundingBox.right = this.left + this.width;
  549. this.boundingBox.bottom = this.top + this.height;
  550. this._label(ctx, this.label, this.x, this.y);
  551. };
  552. Node.prototype._drawDot = function (ctx) {
  553. this._drawShape(ctx, 'circle');
  554. };
  555. Node.prototype._drawTriangle = function (ctx) {
  556. this._drawShape(ctx, 'triangle');
  557. };
  558. Node.prototype._drawTriangleDown = function (ctx) {
  559. this._drawShape(ctx, 'triangleDown');
  560. };
  561. Node.prototype._drawSquare = function (ctx) {
  562. this._drawShape(ctx, 'square');
  563. };
  564. Node.prototype._drawStar = function (ctx) {
  565. this._drawShape(ctx, 'star');
  566. };
  567. Node.prototype._resizeShape = function (ctx) {
  568. if (!this.width) {
  569. this.options.radius= this.baseRadiusValue;
  570. var size = 2 * this.options.radius;
  571. this.width = size;
  572. this.height = size;
  573. }
  574. };
  575. Node.prototype._drawShape = function (ctx, shape) {
  576. this._resizeShape(ctx);
  577. this.left = this.x - this.width / 2;
  578. this.top = this.y - this.height / 2;
  579. var borderWidth = this.options.borderWidth;
  580. var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
  581. var radiusMultiplier = 2;
  582. // choose draw method depending on the shape
  583. switch (shape) {
  584. case 'dot': radiusMultiplier = 2; break;
  585. case 'square': radiusMultiplier = 2; break;
  586. case 'triangle': radiusMultiplier = 3; break;
  587. case 'triangleDown': radiusMultiplier = 3; break;
  588. case 'star': radiusMultiplier = 4; break;
  589. }
  590. ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
  591. ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
  592. ctx.lineWidth *= this.networkScaleInv;
  593. ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
  594. ctx.fillStyle = this.selected ? this.options.color.highlight.background : this.hover ? this.options.color.hover.background : this.options.color.background;
  595. ctx[shape](this.x, this.y, this.options.radius);
  596. ctx.fill();
  597. ctx.stroke();
  598. this.boundingBox.top = this.y - this.options.radius;
  599. this.boundingBox.left = this.x - this.options.radius;
  600. this.boundingBox.right = this.x + this.options.radius;
  601. this.boundingBox.bottom = this.y + this.options.radius;
  602. if (this.label) {
  603. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'hanging',true);
  604. this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  605. this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  606. this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  607. }
  608. };
  609. Node.prototype._resizeText = function (ctx) {
  610. if (!this.width) {
  611. var margin = 5;
  612. var textSize = this.getTextSize(ctx);
  613. this.width = textSize.width + 2 * margin;
  614. this.height = textSize.height + 2 * margin;
  615. }
  616. };
  617. Node.prototype._drawText = function (ctx) {
  618. this._resizeText(ctx);
  619. this.left = this.x - this.width / 2;
  620. this.top = this.y - this.height / 2;
  621. this._label(ctx, this.label, this.x, this.y);
  622. this.boundingBox.top = this.top;
  623. this.boundingBox.left = this.left;
  624. this.boundingBox.right = this.left + this.width;
  625. this.boundingBox.bottom = this.top + this.height;
  626. };
  627. Node.prototype._resizeIcon = function (ctx) {
  628. if (!this.width) {
  629. var margin = 5;
  630. var iconSize =
  631. {
  632. width: Number(this.options.iconSize),
  633. height: Number(this.options.iconSize)
  634. };
  635. this.width = iconSize.width + 2 * margin;
  636. this.height = iconSize.height + 2 * margin;
  637. }
  638. };
  639. Node.prototype._drawIcon = function (ctx) {
  640. this._resizeIcon(ctx);
  641. this.options.iconSize = this.options.iconSize || 50;
  642. this.left = this.x - this.width / 2;
  643. this.top = this.y - this.height / 2;
  644. this._icon(ctx);
  645. this.boundingBox.top = this.y - this.options.iconSize/2;
  646. this.boundingBox.left = this.x - this.options.iconSize/2;
  647. this.boundingBox.right = this.x + this.options.iconSize/2;
  648. this.boundingBox.bottom = this.y + this.options.iconSize/2;
  649. if (this.label) {
  650. var iconTextSpacing = 5;
  651. this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, 'top', true);
  652. this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
  653. this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
  654. this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
  655. }
  656. };
  657. Node.prototype._icon = function (ctx) {
  658. var relativeIconSize = Number(this.options.iconSize) * this.networkScale;
  659. if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) {
  660. var iconSize = Number(this.options.iconSize);
  661. ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace;
  662. // draw icon
  663. ctx.fillStyle = this.options.iconColor || "black";
  664. ctx.textAlign = "center";
  665. ctx.textBaseline = "middle";
  666. ctx.fillText(this.options.icon, this.x, this.y);
  667. }
  668. };
  669. Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
  670. var relativeFontSize = Number(this.options.fontSize) * this.networkScale;
  671. if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) {
  672. var fontSize = Number(this.options.fontSize);
  673. // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
  674. if (relativeFontSize >= this.options.fontSizeMaxVisible) {
  675. fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
  676. }
  677. // fade in when relative scale is between threshold and threshold - 1
  678. var fontColor = this.options.fontColor || "#000000";
  679. var strokecolor = this.options.fontStrokeColor;
  680. if (relativeFontSize <= this.options.fontDrawThreshold) {
  681. var opacity = Math.max(0,Math.min(1,1 - (this.options.fontDrawThreshold - relativeFontSize)));
  682. fontColor = util.overrideOpacity(fontColor, opacity);
  683. strokecolor = util.overrideOpacity(strokecolor, opacity);
  684. }
  685. ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;
  686. var lines = text.split('\n');
  687. var lineCount = lines.length;
  688. var yLine = y + (1 - lineCount) / 2 * fontSize;
  689. if (labelUnderNode == true) {
  690. yLine = y + (1 - lineCount) / (2 * fontSize);
  691. }
  692. // font fill from edges now for nodes!
  693. var width = ctx.measureText(lines[0]).width;
  694. for (var i = 1; i < lineCount; i++) {
  695. var lineWidth = ctx.measureText(lines[i]).width;
  696. width = lineWidth > width ? lineWidth : width;
  697. }
  698. var height = fontSize * lineCount;
  699. var left = x - width / 2;
  700. var top = y - height / 2;
  701. if (baseline == "hanging") {
  702. top += 0.5 * fontSize;
  703. top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
  704. yLine += 4; // distance from node
  705. }
  706. this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
  707. // create the fontfill background
  708. if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
  709. ctx.fillStyle = this.options.fontFill;
  710. ctx.fillRect(left, top, width, height);
  711. }
  712. // draw text
  713. ctx.fillStyle = fontColor;
  714. ctx.textAlign = align || "center";
  715. ctx.textBaseline = baseline || "middle";
  716. if (this.options.fontStrokeWidth > 0){
  717. ctx.lineWidth = this.options.fontStrokeWidth;
  718. ctx.strokeStyle = strokecolor;
  719. ctx.lineJoin = 'round';
  720. }
  721. for (var i = 0; i < lineCount; i++) {
  722. if(this.options.fontStrokeWidth){
  723. ctx.strokeText(lines[i], x, yLine);
  724. }
  725. ctx.fillText(lines[i], x, yLine);
  726. yLine += fontSize;
  727. }
  728. }
  729. };
  730. Node.prototype.getTextSize = function(ctx) {
  731. if (this.label !== undefined) {
  732. var fontSize = Number(this.options.fontSize);
  733. if (fontSize * this.networkScale > this.options.fontSizeMaxVisible) {
  734. fontSize = Number(this.options.fontSizeMaxVisible) * this.networkScaleInv;
  735. }
  736. ctx.font = (this.selected ? "bold " : "") + fontSize + "px " + this.options.fontFace;
  737. var lines = this.label.split('\n'),
  738. height = (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, lineCount: lines.length};
  744. }
  745. else {
  746. return {"width": 0, "height": 0, lineCount: 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.options.mass) : Math.sqrt(energyBefore/this.options.mass);
  814. this.vx = Math.sqrt(energyBefore/this.options.mass);
  815. energyBefore = this.vy * this.vy * massBeforeClustering;
  816. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.options.mass) : Math.sqrt(energyBefore/this.options.mass);
  817. this.vy = Math.sqrt(energyBefore/this.options.mass);
  818. };
  819. module.exports = Node;