Chart.js 标签颜色定位在 tooltip/legend
Chart.js Label color positioning in tooltip/legend
设计师决定制作一个非常标准的甜甜圈图表,不过有一些 non-standard tooltip/legend。
See here
中间的文字不是问题,使用 chart.js 上下文来填充该文字。但是,当涉及到图例和工具提示时,事情变得一团糟。
对于工具提示,我尝试使用标题回调来放置粗体文本,并使用标签回调我能够创建文本,但这里的问题是标签颜色。其实它的形状是正方形的,在标题下面,我没找到什么配置把它放在一边and/or make it bigger.
至于图例,我找到的唯一配置是制作颜色 "point style" 或将它们定位在其他地方。
有什么好的方法可以得到想要的结果吗?
我实际上也在使用 ng2-charts,我知道它有一些 "monkey-patch" 文件可以做一些事情,但如果不真正了解 chart.js 的内部结构,我无法完全理解它的作用 and/or 如何在不更改依赖源代码的情况下编辑它
--- 免责声明:下面的大部分代码都是从 chart.js 粘贴而来的,只有很少的改动。 Chart.js 代码在 MIT License 下,如果你打算使用它,请参考该 License ---
找到了方法,虽然它可能不是最好的and/or最可重用的方法。
对于图例,图例本身是chart.js源代码中的一个插件。出于这个原因,我只是简单地覆盖了插件逻辑:
const defaults = Chart.defaults;
const Element = Chart.Element;
const helpers = Chart.helpers;
const layouts = Chart.layouts;
const columnLegendPlugin = {
id: 'column-legend',
beforeInit: function (chart) {
this.stash_draw = chart.legend.draw;
chart.legend.draw = function () {
const me = chart.legend;
const opts = me.options;
const labelOpts = opts.labels;
const globalDefaults = defaults.global;
const defaultColor = globalDefaults.defaultColor;
const lineDefault = globalDefaults.elements.line;
const legendHeight = me.height;
const columnHeights = me.columnHeights;
const legendWidth = me.width;
const lineWidths = me.lineWidths;
if(opts.display) {
const ctx = me.ctx;
const fontColor = helpers.valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor);
const labelFont = helpers.options._parseFont(labelOpts);
const fontSize = labelFont.size;
let cursor;
// Canvas setup
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.lineWidth = 0.5;
ctx.strokeStyle = fontColor; // for strikethrough effect
ctx.fillStyle = fontColor; // render in correct colour
ctx.font = labelFont.string;
const boxWidth = getBoxWidth(labelOpts, fontSize);
const hitboxes = me.legendHitBoxes;
// current position
const drawLegendBox = function (x, y, legendItem) {
if(isNaN(boxWidth) || boxWidth <= 0) {
return;
}
// Set the ctx for the box
ctx.save();
const lineWidth = helpers.valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth);
ctx.fillStyle = helpers.valueOrDefault(legendItem.fillStyle, defaultColor);
ctx.lineCap = helpers.valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle);
ctx.lineDashOffset = helpers.valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset);
ctx.lineJoin = helpers.valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = helpers.valueOrDefault(legendItem.strokeStyle, defaultColor);
if(ctx.setLineDash) {
// IE 9 and 10 do not support line dash
ctx.setLineDash(helpers.valueOrDefault(legendItem.lineDash, lineDefault.borderDash));
}
if(labelOpts && labelOpts.usePointStyle) {
// Recalculate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
const radius = boxWidth * Math.SQRT2 / 2;
const centerX = x + boxWidth / 2;
const centerY = y + fontSize / 2;
// Draw pointStyle as legend symbol
helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation);
} else {
// Draw box as legend symbol
ctx.fillRect(x, y, boxWidth, Math.min(fontSize, labelOpts.boxHeight));
if(lineWidth !== 0) {
ctx.strokeRect(x, y, boxWidth, Math.min(fontSize, labelOpts.boxHeight));
}
}
ctx.restore();
};
const fillText = function (x, y, legendItem, textWidth) {
const halfFontSize = fontSize / 2;
const xLeft = /* boxWidth + halfFontSize + */ x;
//const yMiddle = y + halfFontSize;
const yMiddle = y + labelOpts.yShift;
if(legendItem.text && legendItem.text.length > labelOpts.maxLabelLength) {
legendItem.text = (legendItem.text as string).slice(0, labelOpts.maxLabelLength) + '.';
}
ctx.fillText(legendItem.text, xLeft, yMiddle);
if(legendItem.hidden) {
// Strikethrough the text if hidden
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(xLeft, yMiddle);
ctx.lineTo(xLeft + textWidth, yMiddle);
ctx.stroke();
}
};
const alignmentOffset = function (dimension, blockSize) {
switch(opts.align) {
case 'start':
return labelOpts.padding;
case 'end':
return dimension - blockSize;
default: // center
return (dimension - blockSize + labelOpts.padding) / 2;
}
};
// Horizontal
const isHorizontal = me.isHorizontal();
if(isHorizontal) {
cursor = {
x: me.left + alignmentOffset(legendWidth, lineWidths[0]),
y: me.top + labelOpts.padding,
line: 0
};
} else {
cursor = {
x: me.left + labelOpts.padding,
y: me.top + alignmentOffset(legendHeight, columnHeights[0]),
line: 0
};
}
const itemHeight = fontSize + labelOpts.padding;
helpers.each(me.legendItems, function (legendItem, i) {
const textWidth = Math.min(ctx.measureText(legendItem.text).width, 100);
const width = boxWidth + (fontSize / 2) + textWidth;
let x = cursor.x;
let y = cursor.y;
// Use (me.left + me.minSize.width) and (me.top + me.minSize.height)
// instead of me.right and me.bottom because me.width and me.height
// may have been changed since me.minSize was calculated
if(isHorizontal) {
if(i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) {
y = cursor.y += itemHeight;
cursor.line++;
x = cursor.x = me.left + alignmentOffset(legendWidth, lineWidths[cursor.line]);
}
} else if(i > 0 && y + itemHeight > me.top + me.minSize.height) {
x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding;
cursor.line++;
y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
}
drawLegendBox(x, y, legendItem);
hitboxes[i].left = x;
hitboxes[i].top = y;
hitboxes[i].height = labelOpts.yShift + labelOpts.boxHeight + labelOpts.fontSize;
hitboxes[i].width = Math.max(Math.min(ctx.measureText(legendItem.text).width, 100), boxWidth);
// Fill the actual label
fillText(x, y, legendItem, textWidth);
if(isHorizontal) {
cursor.x += width + labelOpts.padding;
} else {
cursor.y += itemHeight;
}
});
}
};
}
};
这段代码工作得很好,如果你需要使用旧图例,只需实现一个逻辑来重用隐藏函数。
虽然工具提示很难处理,因为它们是核心功能,几乎没有导出 API。但是您可以覆盖原型,重新使用 chart.js:
中的代码
const defaults = Chart.defaults;
const Element = Chart.Element;
const helpers = Chart.helpers;
const layouts = Chart.layouts;
function getAlignedX(vm, align) {
return align === 'center'
? vm.x + vm.width / 2
: align === 'right'
? vm.x + vm.width - vm.xPadding
: vm.x + vm.xPadding;
}
export const niceTooltipPlugin = {
id: 'nice-tooltip-plugin',
beforeInit: function (chart) {
Chart.Tooltip.prototype.draw = function () {
const ctx = this._chart.ctx;
const vm = this._view;
if(vm.opacity === 0) {
return;
}
const tooltipSize = {
width: Math.max(vm.width, ctx.measureText(vm.body[0].lines[0].tooltipLabel).width + 50, ctx.measureText(vm.body[0].lines[0].tooltipData).width + 50),
height: 1.5 * vm.height
};
const pt = {
x: vm.x,
y: vm.y
};
const opacity = vm.opacity;
// Truthy/falsey value for empty tooltip
const hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length;
if(this._options.enabled && hasTooltipContent) {
ctx.save();
ctx.globalAlpha = opacity;
// Draw Background
this.drawBackground(pt, vm, ctx, tooltipSize);
// Draw Title, Body, and Footer
pt.y += vm.yPadding;
// Titles
this.drawTitle(pt, vm, ctx);
// Body
this.drawBody(pt, vm, ctx);
// Footer
this.drawFooter(pt, vm, ctx);
ctx.restore();
}
};
Chart.Tooltip.prototype.drawBody = function (pt, vm, ctx) {
const bodyFontSize = vm.bodyFontSize;
const bodySpacing = vm.bodySpacing;
const bodyAlign = vm._bodyAlign;
const body = vm.body;
const drawColorBoxes = vm.displayColors;
const labelColors = vm.labelColors;
let xLinePadding = 0;
const colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0;
let textColor;
ctx.textAlign = bodyAlign;
ctx.textBaseline = 'top';
ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
pt.x = getAlignedX(vm, bodyAlign);
// Before Body
const fillLineOfText = function (line) {
ctx.fillText(line, pt.x + xLinePadding, pt.y);
pt.y += bodyFontSize + bodySpacing;
};
// Before body lines
ctx.fillStyle = vm.bodyFontColor;
helpers.each(vm.beforeBody, fillLineOfText);
xLinePadding = drawColorBoxes && bodyAlign !== 'right'
? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
: 0;
// Draw body lines now
helpers.each(body, function (bodyItem, i) {
textColor = vm.labelTextColors[i];
ctx.fillStyle = textColor;
helpers.each(bodyItem.before, fillLineOfText);
helpers.each(bodyItem.lines, function (line) {
// Draw Legend-like boxes if needed
if(drawColorBoxes) {
/* // Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = vm.legendColorBackground;
ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize);
// Border
ctx.lineWidth = 1;
ctx.strokeStyle = labelColors[i].borderColor;
ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize);
// Inner square
ctx.fillStyle = labelColors[i].backgroundColor;
ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
ctx.fillStyle = textColor; */
ctx.fillStyle = labelColors[i].backgroundColor;
helpers.canvas.drawPoint(ctx, undefined, 5, pt.x, pt.y + 12, 360);
ctx.fillStyle = textColor;
}
ctx.font = helpers.fontString(bodyFontSize, "bold", vm._bodyFontFamily);
fillLineOfText(line.tooltipLabel);
ctx.font = helpers.fontString(bodyFontSize, "normal", vm._bodyFontFamily);
ctx.fillStyle = "#b0b0b0";
fillLineOfText(line.tooltipData);
ctx.fillStyle = textColor;
});
helpers.each(bodyItem.after, fillLineOfText);
});
};
}
};
设计师决定制作一个非常标准的甜甜圈图表,不过有一些 non-standard tooltip/legend。 See here
中间的文字不是问题,使用 chart.js 上下文来填充该文字。但是,当涉及到图例和工具提示时,事情变得一团糟。
对于工具提示,我尝试使用标题回调来放置粗体文本,并使用标签回调我能够创建文本,但这里的问题是标签颜色。其实它的形状是正方形的,在标题下面,我没找到什么配置把它放在一边and/or make it bigger.
至于图例,我找到的唯一配置是制作颜色 "point style" 或将它们定位在其他地方。
有什么好的方法可以得到想要的结果吗?
我实际上也在使用 ng2-charts,我知道它有一些 "monkey-patch" 文件可以做一些事情,但如果不真正了解 chart.js 的内部结构,我无法完全理解它的作用 and/or 如何在不更改依赖源代码的情况下编辑它
--- 免责声明:下面的大部分代码都是从 chart.js 粘贴而来的,只有很少的改动。 Chart.js 代码在 MIT License 下,如果你打算使用它,请参考该 License ---
找到了方法,虽然它可能不是最好的and/or最可重用的方法。
对于图例,图例本身是chart.js源代码中的一个插件。出于这个原因,我只是简单地覆盖了插件逻辑:
const defaults = Chart.defaults;
const Element = Chart.Element;
const helpers = Chart.helpers;
const layouts = Chart.layouts;
const columnLegendPlugin = {
id: 'column-legend',
beforeInit: function (chart) {
this.stash_draw = chart.legend.draw;
chart.legend.draw = function () {
const me = chart.legend;
const opts = me.options;
const labelOpts = opts.labels;
const globalDefaults = defaults.global;
const defaultColor = globalDefaults.defaultColor;
const lineDefault = globalDefaults.elements.line;
const legendHeight = me.height;
const columnHeights = me.columnHeights;
const legendWidth = me.width;
const lineWidths = me.lineWidths;
if(opts.display) {
const ctx = me.ctx;
const fontColor = helpers.valueOrDefault(labelOpts.fontColor, globalDefaults.defaultFontColor);
const labelFont = helpers.options._parseFont(labelOpts);
const fontSize = labelFont.size;
let cursor;
// Canvas setup
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.lineWidth = 0.5;
ctx.strokeStyle = fontColor; // for strikethrough effect
ctx.fillStyle = fontColor; // render in correct colour
ctx.font = labelFont.string;
const boxWidth = getBoxWidth(labelOpts, fontSize);
const hitboxes = me.legendHitBoxes;
// current position
const drawLegendBox = function (x, y, legendItem) {
if(isNaN(boxWidth) || boxWidth <= 0) {
return;
}
// Set the ctx for the box
ctx.save();
const lineWidth = helpers.valueOrDefault(legendItem.lineWidth, lineDefault.borderWidth);
ctx.fillStyle = helpers.valueOrDefault(legendItem.fillStyle, defaultColor);
ctx.lineCap = helpers.valueOrDefault(legendItem.lineCap, lineDefault.borderCapStyle);
ctx.lineDashOffset = helpers.valueOrDefault(legendItem.lineDashOffset, lineDefault.borderDashOffset);
ctx.lineJoin = helpers.valueOrDefault(legendItem.lineJoin, lineDefault.borderJoinStyle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = helpers.valueOrDefault(legendItem.strokeStyle, defaultColor);
if(ctx.setLineDash) {
// IE 9 and 10 do not support line dash
ctx.setLineDash(helpers.valueOrDefault(legendItem.lineDash, lineDefault.borderDash));
}
if(labelOpts && labelOpts.usePointStyle) {
// Recalculate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
const radius = boxWidth * Math.SQRT2 / 2;
const centerX = x + boxWidth / 2;
const centerY = y + fontSize / 2;
// Draw pointStyle as legend symbol
helpers.canvas.drawPoint(ctx, legendItem.pointStyle, radius, centerX, centerY, legendItem.rotation);
} else {
// Draw box as legend symbol
ctx.fillRect(x, y, boxWidth, Math.min(fontSize, labelOpts.boxHeight));
if(lineWidth !== 0) {
ctx.strokeRect(x, y, boxWidth, Math.min(fontSize, labelOpts.boxHeight));
}
}
ctx.restore();
};
const fillText = function (x, y, legendItem, textWidth) {
const halfFontSize = fontSize / 2;
const xLeft = /* boxWidth + halfFontSize + */ x;
//const yMiddle = y + halfFontSize;
const yMiddle = y + labelOpts.yShift;
if(legendItem.text && legendItem.text.length > labelOpts.maxLabelLength) {
legendItem.text = (legendItem.text as string).slice(0, labelOpts.maxLabelLength) + '.';
}
ctx.fillText(legendItem.text, xLeft, yMiddle);
if(legendItem.hidden) {
// Strikethrough the text if hidden
ctx.beginPath();
ctx.lineWidth = 2;
ctx.moveTo(xLeft, yMiddle);
ctx.lineTo(xLeft + textWidth, yMiddle);
ctx.stroke();
}
};
const alignmentOffset = function (dimension, blockSize) {
switch(opts.align) {
case 'start':
return labelOpts.padding;
case 'end':
return dimension - blockSize;
default: // center
return (dimension - blockSize + labelOpts.padding) / 2;
}
};
// Horizontal
const isHorizontal = me.isHorizontal();
if(isHorizontal) {
cursor = {
x: me.left + alignmentOffset(legendWidth, lineWidths[0]),
y: me.top + labelOpts.padding,
line: 0
};
} else {
cursor = {
x: me.left + labelOpts.padding,
y: me.top + alignmentOffset(legendHeight, columnHeights[0]),
line: 0
};
}
const itemHeight = fontSize + labelOpts.padding;
helpers.each(me.legendItems, function (legendItem, i) {
const textWidth = Math.min(ctx.measureText(legendItem.text).width, 100);
const width = boxWidth + (fontSize / 2) + textWidth;
let x = cursor.x;
let y = cursor.y;
// Use (me.left + me.minSize.width) and (me.top + me.minSize.height)
// instead of me.right and me.bottom because me.width and me.height
// may have been changed since me.minSize was calculated
if(isHorizontal) {
if(i > 0 && x + width + labelOpts.padding > me.left + me.minSize.width) {
y = cursor.y += itemHeight;
cursor.line++;
x = cursor.x = me.left + alignmentOffset(legendWidth, lineWidths[cursor.line]);
}
} else if(i > 0 && y + itemHeight > me.top + me.minSize.height) {
x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding;
cursor.line++;
y = cursor.y = me.top + alignmentOffset(legendHeight, columnHeights[cursor.line]);
}
drawLegendBox(x, y, legendItem);
hitboxes[i].left = x;
hitboxes[i].top = y;
hitboxes[i].height = labelOpts.yShift + labelOpts.boxHeight + labelOpts.fontSize;
hitboxes[i].width = Math.max(Math.min(ctx.measureText(legendItem.text).width, 100), boxWidth);
// Fill the actual label
fillText(x, y, legendItem, textWidth);
if(isHorizontal) {
cursor.x += width + labelOpts.padding;
} else {
cursor.y += itemHeight;
}
});
}
};
}
};
这段代码工作得很好,如果你需要使用旧图例,只需实现一个逻辑来重用隐藏函数。
虽然工具提示很难处理,因为它们是核心功能,几乎没有导出 API。但是您可以覆盖原型,重新使用 chart.js:
中的代码const defaults = Chart.defaults;
const Element = Chart.Element;
const helpers = Chart.helpers;
const layouts = Chart.layouts;
function getAlignedX(vm, align) {
return align === 'center'
? vm.x + vm.width / 2
: align === 'right'
? vm.x + vm.width - vm.xPadding
: vm.x + vm.xPadding;
}
export const niceTooltipPlugin = {
id: 'nice-tooltip-plugin',
beforeInit: function (chart) {
Chart.Tooltip.prototype.draw = function () {
const ctx = this._chart.ctx;
const vm = this._view;
if(vm.opacity === 0) {
return;
}
const tooltipSize = {
width: Math.max(vm.width, ctx.measureText(vm.body[0].lines[0].tooltipLabel).width + 50, ctx.measureText(vm.body[0].lines[0].tooltipData).width + 50),
height: 1.5 * vm.height
};
const pt = {
x: vm.x,
y: vm.y
};
const opacity = vm.opacity;
// Truthy/falsey value for empty tooltip
const hasTooltipContent = vm.title.length || vm.beforeBody.length || vm.body.length || vm.afterBody.length || vm.footer.length;
if(this._options.enabled && hasTooltipContent) {
ctx.save();
ctx.globalAlpha = opacity;
// Draw Background
this.drawBackground(pt, vm, ctx, tooltipSize);
// Draw Title, Body, and Footer
pt.y += vm.yPadding;
// Titles
this.drawTitle(pt, vm, ctx);
// Body
this.drawBody(pt, vm, ctx);
// Footer
this.drawFooter(pt, vm, ctx);
ctx.restore();
}
};
Chart.Tooltip.prototype.drawBody = function (pt, vm, ctx) {
const bodyFontSize = vm.bodyFontSize;
const bodySpacing = vm.bodySpacing;
const bodyAlign = vm._bodyAlign;
const body = vm.body;
const drawColorBoxes = vm.displayColors;
const labelColors = vm.labelColors;
let xLinePadding = 0;
const colorX = drawColorBoxes ? getAlignedX(vm, 'left') : 0;
let textColor;
ctx.textAlign = bodyAlign;
ctx.textBaseline = 'top';
ctx.font = helpers.fontString(bodyFontSize, vm._bodyFontStyle, vm._bodyFontFamily);
pt.x = getAlignedX(vm, bodyAlign);
// Before Body
const fillLineOfText = function (line) {
ctx.fillText(line, pt.x + xLinePadding, pt.y);
pt.y += bodyFontSize + bodySpacing;
};
// Before body lines
ctx.fillStyle = vm.bodyFontColor;
helpers.each(vm.beforeBody, fillLineOfText);
xLinePadding = drawColorBoxes && bodyAlign !== 'right'
? bodyAlign === 'center' ? (bodyFontSize / 2 + 1) : (bodyFontSize + 2)
: 0;
// Draw body lines now
helpers.each(body, function (bodyItem, i) {
textColor = vm.labelTextColors[i];
ctx.fillStyle = textColor;
helpers.each(bodyItem.before, fillLineOfText);
helpers.each(bodyItem.lines, function (line) {
// Draw Legend-like boxes if needed
if(drawColorBoxes) {
/* // Fill a white rect so that colours merge nicely if the opacity is < 1
ctx.fillStyle = vm.legendColorBackground;
ctx.fillRect(colorX, pt.y, bodyFontSize, bodyFontSize);
// Border
ctx.lineWidth = 1;
ctx.strokeStyle = labelColors[i].borderColor;
ctx.strokeRect(colorX, pt.y, bodyFontSize, bodyFontSize);
// Inner square
ctx.fillStyle = labelColors[i].backgroundColor;
ctx.fillRect(colorX + 1, pt.y + 1, bodyFontSize - 2, bodyFontSize - 2);
ctx.fillStyle = textColor; */
ctx.fillStyle = labelColors[i].backgroundColor;
helpers.canvas.drawPoint(ctx, undefined, 5, pt.x, pt.y + 12, 360);
ctx.fillStyle = textColor;
}
ctx.font = helpers.fontString(bodyFontSize, "bold", vm._bodyFontFamily);
fillLineOfText(line.tooltipLabel);
ctx.font = helpers.fontString(bodyFontSize, "normal", vm._bodyFontFamily);
ctx.fillStyle = "#b0b0b0";
fillLineOfText(line.tooltipData);
ctx.fillStyle = textColor;
});
helpers.each(bodyItem.after, fillLineOfText);
});
};
}
};