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.

271 lines
8.5 KiB

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