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.
 
 
 

277 lines
8.7 KiB

let util = require('../../../../util');
class Label {
constructor(body,options,edgelabel = false) {
this.body = body;
this.pointToSelf = false;
this.baseSize = undefined;
this.fontOptions = {};
this.setOptions(options);
this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached
this.isEdgeLabel = edgelabel;
}
setOptions(options, allowDeletion = false) {
this.nodeOptions = options;
// We want to keep the font options seperated from the node options.
// The node options have to mirror the globals when they are not overruled.
this.fontOptions = util.deepExtend({},options.font, true);
if (options.label !== undefined) {
this.labelDirty = true;
}
if (options.font !== undefined) {
Label.parseOptions(this.fontOptions, options, allowDeletion);
if (typeof options.font === 'string') {
this.baseSize = this.fontOptions.size;
}
else if (typeof options.font === 'object') {
if (options.font.size !== undefined) {
this.baseSize = options.font.size;
}
}
}
}
static parseOptions(parentOptions, newOptions, allowDeletion = false) {
if (typeof newOptions.font === 'string') {
let newOptionsArray = newOptions.font.split(" ");
parentOptions.size = newOptionsArray[0].replace("px",'');
parentOptions.face = newOptionsArray[1];
parentOptions.color = newOptionsArray[2];
}
else if (typeof newOptions.font === 'object') {
util.fillIfDefined(parentOptions, newOptions.font, allowDeletion);
}
parentOptions.size = Number(parentOptions.size);
}
/**
* Main function. This is called from anything that wants to draw a label.
* @param ctx
* @param x
* @param y
* @param selected
* @param baseline
*/
draw(ctx, x, y, selected, baseline = 'middle') {
// if no label, return
if (this.nodeOptions.label === undefined)
return;
// check if we have to render the label
let viewFontSize = this.fontOptions.size * this.body.view.scale;
if (this.nodeOptions.label && viewFontSize < this.nodeOptions.scaling.label.drawThreshold - 1)
return;
// update the size cache if required
this.calculateLabelSize(ctx, selected, x, y, baseline);
// create the fontfill background
this._drawBackground(ctx);
// draw text
this._drawText(ctx, selected, x, y, baseline);
}
/**
* Draws the label background
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawBackground(ctx) {
if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") {
ctx.fillStyle = this.fontOptions.background;
let lineMargin = 2;
if (this.isEdgeLabel) {
switch (this.fontOptions.align) {
case 'middle':
ctx.fillRect(-this.size.width * 0.5, -this.size.height * 0.5, this.size.width, this.size.height);
break;
case 'top':
ctx.fillRect(-this.size.width * 0.5, -(this.size.height + lineMargin), this.size.width, this.size.height);
break;
case 'bottom':
ctx.fillRect(-this.size.width * 0.5, lineMargin, this.size.width, this.size.height);
break;
default:
ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
break;
}
} else {
ctx.fillRect(this.size.left, this.size.top - 0.5*lineMargin, this.size.width, this.size.height);
}
}
}
/**
*
* @param ctx
* @param x
* @param baseline
* @private
*/
_drawText(ctx, selected, x, y, baseline = 'middle') {
let fontSize = this.fontOptions.size;
let viewFontSize = fontSize * this.body.view.scale;
// this ensures that there will not be HUGE letters on screen by setting an upper limit on the visible text size (regardless of zoomLevel)
if (viewFontSize >= this.nodeOptions.scaling.label.maxVisible) {
fontSize = Number(this.nodeOptions.scaling.label.maxVisible) / this.body.view.scale;
}
let yLine = this.size.yLine;
let [fontColor, strokeColor] = this._getColor(viewFontSize);
[x, yLine] = this._setAlignment(ctx, x, yLine, baseline);
// configure context for drawing the text
ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + fontSize + "px " + this.fontOptions.face;
ctx.fillStyle = fontColor;
// When the textAlign property is 'left', make label left-justified
if ((!this.isEdgeLabel) && this.fontOptions.align === 'left') {
ctx.textAlign = this.fontOptions.align;
x = x - 0.5 * this.size.width; // Shift label 1/2-distance to the left
} else {
ctx.textAlign = 'center';
}
// set the strokeWidth
if (this.fontOptions.strokeWidth > 0) {
ctx.lineWidth = this.fontOptions.strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.lineJoin = 'round';
}
// draw the text
for (let i = 0; i < this.lineCount; i++) {
if (this.fontOptions.strokeWidth > 0) {
ctx.strokeText(this.lines[i], x, yLine);
}
ctx.fillText(this.lines[i], x, yLine);
yLine += fontSize;
}
}
_setAlignment(ctx, x, yLine, baseline) {
// check for label alignment (for edges)
// TODO: make alignment for nodes
if (this.isEdgeLabel && this.fontOptions.align !== 'horizontal' && this.pointToSelf === false) {
x = 0;
yLine = 0;
let lineMargin = 2;
if (this.fontOptions.align === 'top') {
ctx.textBaseline = 'alphabetic';
yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
}
else if (this.fontOptions.align === 'bottom') {
ctx.textBaseline = 'hanging';
yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
}
else {
ctx.textBaseline = 'middle';
}
}
else {
ctx.textBaseline = baseline;
}
return [x,yLine];
}
/**
* fade in when relative scale is between threshold and threshold - 1.
* If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here.
*
* @param viewFontSize
* @returns {*[]}
* @private
*/
_getColor(viewFontSize) {
let fontColor = this.fontOptions.color || '#000000';
let strokeColor = this.fontOptions.strokeColor || '#ffffff';
if (viewFontSize <= this.nodeOptions.scaling.label.drawThreshold) {
let opacity = Math.max(0, Math.min(1, 1 - (this.nodeOptions.scaling.label.drawThreshold - viewFontSize)));
fontColor = util.overrideOpacity(fontColor, opacity);
strokeColor = util.overrideOpacity(strokeColor, opacity);
}
return [fontColor, strokeColor];
}
/**
*
* @param ctx
* @param selected
* @returns {{width: number, height: number}}
*/
getTextSize(ctx, selected = false) {
let size = {
width: this._processLabel(ctx,selected),
height: this.fontOptions.size * this.lineCount,
lineCount: this.lineCount
};
return size;
}
/**
*
* @param ctx
* @param selected
* @param x
* @param y
* @param baseline
*/
calculateLabelSize(ctx, selected, x = 0, y = 0, baseline = 'middle') {
if (this.labelDirty === true) {
this.size.width = this._processLabel(ctx,selected);
}
this.size.height = this.fontOptions.size * this.lineCount;
this.size.left = x - this.size.width * 0.5;
this.size.top = y - this.size.height * 0.5;
this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
if (baseline === "hanging") {
this.size.top += 0.5 * this.fontOptions.size;
this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
this.size.yLine += 4; // distance from node
}
this.labelDirty = false;
}
/**
* This calculates the width as well as explodes the label string and calculates the amount of lines.
* @param ctx
* @param selected
* @returns {number}
* @private
*/
_processLabel(ctx,selected) {
let width = 0;
let lines = [''];
let lineCount = 0;
if (this.nodeOptions.label !== undefined) {
lines = String(this.nodeOptions.label).split('\n');
lineCount = lines.length;
ctx.font = (selected && this.nodeOptions.labelHighlightBold ? 'bold ' : '') + this.fontOptions.size + "px " + this.fontOptions.face;
width = ctx.measureText(lines[0]).width;
for (let i = 1; i < lineCount; i++) {
let lineWidth = ctx.measureText(lines[i]).width;
width = lineWidth > width ? lineWidth : width;
}
}
this.lines = lines;
this.lineCount = lineCount;
return width;
}
}
export default Label;