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.

966 lines
32 KiB

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