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.

975 lines
32 KiB

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