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.

276 lines
8.7 KiB

  1. let util = require('../../../../util');
  2. class Label {
  3. constructor(body,options,edgelabel = false) {
  4. this.body = body;
  5. this.pointToSelf = false;
  6. this.baseSize = undefined;
  7. this.fontOptions = {};
  8. this.setOptions(options);
  9. this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached
  10. this.isEdgeLabel = edgelabel;
  11. }
  12. setOptions(options, allowDeletion = false) {
  13. this.nodeOptions = options;
  14. // We want to keep the font options seperated from the node options.
  15. // The node options have to mirror the globals when they are not overruled.
  16. this.fontOptions = util.deepExtend({},options.font, true);
  17. if (options.label !== undefined) {
  18. this.labelDirty = true;
  19. }
  20. if (options.font !== undefined) {
  21. Label.parseOptions(this.fontOptions, options, allowDeletion);
  22. if (typeof options.font === 'string') {
  23. this.baseSize = this.fontOptions.size;
  24. }
  25. else if (typeof options.font === 'object') {
  26. if (options.font.size !== undefined) {
  27. this.baseSize = options.font.size;
  28. }
  29. }
  30. }
  31. }
  32. static parseOptions(parentOptions, newOptions, allowDeletion = false) {
  33. if (typeof newOptions.font === 'string') {
  34. let newOptionsArray = newOptions.font.split(" ");
  35. parentOptions.size = newOptionsArray[0].replace("px",'');
  36. parentOptions.face = newOptionsArray[1];
  37. parentOptions.color = newOptionsArray[2];
  38. }
  39. else if (typeof newOptions.font === 'object') {
  40. util.fillIfDefined(parentOptions, newOptions.font, allowDeletion);
  41. }
  42. parentOptions.size = Number(parentOptions.size);
  43. }
  44. /**
  45. * Main function. This is called from anything that wants to draw a label.
  46. * @param ctx
  47. * @param x
  48. * @param y
  49. * @param selected
  50. * @param baseline
  51. */
  52. draw(ctx, x, y, selected, baseline = 'middle') {
  53. // if no label, return
  54. if (this.nodeOptions.label === undefined)
  55. return;
  56. // check if we have to render the label
  57. let viewFontSize = this.fontOptions.size * this.body.view.scale;
  58. if (this.nodeOptions.label && viewFontSize < this.nodeOptions.scaling.label.drawThreshold - 1)
  59. return;
  60. // update the size cache if required
  61. this.calculateLabelSize(ctx, selected, x, y, baseline);
  62. // create the fontfill background
  63. this._drawBackground(ctx);
  64. // draw text
  65. this._drawText(ctx, selected, x, y, baseline);
  66. }
  67. /**
  68. * Draws the label background
  69. * @param {CanvasRenderingContext2D} ctx
  70. * @private
  71. */
  72. _drawBackground(ctx) {
  73. if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") {
  74. ctx.fillStyle = this.fontOptions.background;
  75. let lineMargin = 2;
  76. if (this.isEdgeLabel) {
  77. switch (this.fontOptions.align) {
  78. case 'middle':
  79. ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height);
  80. break;
  81. case 'top':
  82. ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height);
  83. break;
  84. case 'bottom':
  85. ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height);
  86. break;
  87. default:
  88. ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
  89. break;
  90. }
  91. } else {
  92. ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
  93. }
  94. }
  95. }
  96. /**
  97. *
  98. * @param ctx
  99. * @param x
  100. * @param baseline
  101. * @private
  102. */
  103. _drawText(ctx, selected, x, y, baseline = 'middle') {
  104. let fontSize = this.fontOptions.size;
  105. let viewFontSize = fontSize * this.body.view.scale;
  106. // this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
  107. if (viewFontSize >= this.nodeOptions.scaling.label.maxVisible) {
  108. fontSize = Number(this.nodeOptions.scaling.label.maxVisible) / this.body.view.scale;
  109. }
  110. let yLine = this.size.yLine;
  111. let [fontColor, strokeColor] = this._getColor(viewFontSize);
  112. [x, yLine] = this._setAlignment(ctx, x, yLine, baseline);
  113. // configure context for drawing the text
  114. ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face;
  115. ctx.fillStyle = fontColor;
  116. // When the textAlign property is 'left', make label left-justified
  117. if ((!this.isEdgeLabel) && this.fontOptions.align === 'left') {
  118. ctx.textAlign = this.fontOptions.align;
  119. x = x - 0.5 * this.size.width; // Shift label 1/2-distance to the left
  120. } else {
  121. ctx.textAlign = 'center';
  122. }
  123. // set the strokeWidth
  124. if (this.fontOptions.strokeWidth > 0) {
  125. ctx.lineWidth = this.fontOptions.strokeWidth;
  126. ctx.strokeStyle = strokeColor;
  127. ctx.lineJoin = 'round';
  128. }
  129. // draw the text
  130. for (let i = 0; i < this.lineCount; i++) {
  131. if (this.fontOptions.strokeWidth > 0) {
  132. ctx.strokeText(this.lines[i], x, yLine);
  133. }
  134. ctx.fillText(this.lines[i], x, yLine);
  135. yLine += fontSize;
  136. }
  137. }
  138. _setAlignment(ctx, x, yLine, baseline) {
  139. // check for label alignment (for edges)
  140. // TODO: make alignment for nodes
  141. if (this.isEdgeLabel && this.fontOptions.align !== 'horizontal' && this.pointToSelf === false) {
  142. x = 0;
  143. yLine = 0;
  144. let lineMargin = 2;
  145. if (this.fontOptions.align === 'top') {
  146. ctx.textBaseline = 'alphabetic';
  147. yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
  148. }
  149. else if (this.fontOptions.align === 'bottom') {
  150. ctx.textBaseline = 'hanging';
  151. yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
  152. }
  153. else {
  154. ctx.textBaseline = 'middle';
  155. }
  156. }
  157. else {
  158. ctx.textBaseline = baseline;
  159. }
  160. return [x,yLine];
  161. }
  162. /**
  163. * fade in when relative scale is between threshold and threshold - 1.
  164. * If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here.
  165. *
  166. * @param viewFontSize
  167. * @returns {*[]}
  168. * @private
  169. */
  170. _getColor(viewFontSize) {
  171. let fontColor = this.fontOptions.color || '#000000';
  172. let strokeColor = this.fontOptions.strokeColor || '#ffffff';
  173. if (viewFontSize <= this.nodeOptions.scaling.label.drawThreshold) {
  174. let opacity = Math.max(0, Math.min(1, 1 - (this.nodeOptions.scaling.label.drawThreshold - viewFontSize)));
  175. fontColor = util.overrideOpacity(fontColor, opacity);
  176. strokeColor = util.overrideOpacity(strokeColor, opacity);
  177. }
  178. return [fontColor, strokeColor];
  179. }
  180. /**
  181. *
  182. * @param ctx
  183. * @param selected
  184. * @returns {{width: number, height: number}}
  185. */
  186. getTextSize(ctx, selected = false) {
  187. let size = {
  188. width: this._processLabel(ctx,selected),
  189. height: this.fontOptions.size * this.lineCount,
  190. lineCount: this.lineCount
  191. };
  192. return size;
  193. }
  194. /**
  195. *
  196. * @param ctx
  197. * @param selected
  198. * @param x
  199. * @param y
  200. * @param baseline
  201. */
  202. calculateLabelSize(ctx, selected, x = 0, y = 0, baseline = 'middle') {
  203. if (this.labelDirty === true) {
  204. this.size.width = this._processLabel(ctx,selected);
  205. }
  206. this.size.height = this.fontOptions.size * this.lineCount;
  207. this.size.left = x - this.size.width * 0.5;
  208. this.size.top = y - this.size.height * 0.5;
  209. this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
  210. if (baseline === "hanging") {
  211. this.size.top += 0.5 * this.fontOptions.size;
  212. this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
  213. this.size.yLine += 4; // distance from node
  214. }
  215. this.labelDirty = false;
  216. }
  217. /**
  218. * This calculates the width as well as explodes the label string and calculates the amount of lines.
  219. * @param ctx
  220. * @param selected
  221. * @returns {number}
  222. * @private
  223. */
  224. _processLabel(ctx,selected) {
  225. let width = 0;
  226. let lines = [''];
  227. let lineCount = 0;
  228. if (this.nodeOptions.label !== undefined) {
  229. lines = String(this.nodeOptions.label).split('\n');
  230. lineCount = lines.length;
  231. ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face;
  232. width = ctx.measureText(lines[0]).width;
  233. for (let i = 1; i < lineCount; i++) {
  234. let lineWidth = ctx.measureText(lines[i]).width;
  235. width = lineWidth > width ? lineWidth : width;
  236. }
  237. }
  238. this.lines = lines;
  239. this.lineCount = lineCount;
  240. return width;
  241. }
  242. }
  243. export default Label;