Skip to content

Commit

Permalink
Enable different fonts and colors configuration on multiline labels (c…
Browse files Browse the repository at this point in the history
…hartjs#801)

* Add element diagrams to the annotation types guide

* Enable different fonts and colors configuration on multiline labels

* updates type definitions

* fixes CC

* documentation

* fixes check for font as array

* adds sample

* fixes CC

* changes font and color as indexable

* add tolerance

* add tolerance 2

* add tolerance 3

* add tolerance 4

* add tolerance 4

* fallback tolerance changes

* Test with new registered fixtures

* Another test if adjust is passed

* additional test with a label fixture

* add new fixture for polygon to test if works

* add new fixtures for ellipse from FF

* fallback tests on fixtures
  • Loading branch information
stockiNail authored Feb 25, 2023
1 parent 3abab64 commit b9cbccf
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 35 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ module.exports = {
'label/image',
'label/innerChart',
'label/lowerUpper',
'label/fontsColors',
'label/autoscaling'
]
},
Expand Down
10 changes: 7 additions & 3 deletions docs/guide/types/_commonInnerLabel.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ All of these options can be [Scriptable](../options.md#scriptable-options)

| Name | Type | Default | Notes
| ---- | ---- | :----: | ----
| `color` | [`Color`](../options.md#color) | `'black'` | Text color.
| [`color`](#fonts-and-colors) | [`Color`\|`Color[]`](../options#color) | `'black'` | Text color.
| `content` | `string`\|`string[]`\|[`Image`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image)\|[`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) | `null` | The content to show in the label.
| `display` | `boolean` | `false` | Whether or not the label is shown.
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options.md#draw-time). Defaults to the annotation draw time if unset
| `font` | [`Font`](../options.md#font) | `{ weight: 'bold' }` | Label font
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the annotation draw time if unset
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font
| `height` | `number`\|`string` | `undefined` | Overrides the height of the image or canvas element. Could be set in pixel by a number, or in percentage of current height of image or canvas element by a string. If undefined, uses the height of the image or canvas element. It is used only when the content is an image or canvas element.
| `opacity` | `number` | `undefined` | Overrides the opacity of the image or canvas element. Could be set a number in the range 0.0 to 1.0, inclusive. If undefined, uses the opacity of the image or canvas element. It is used only when the content is an image or canvas element.
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
Expand All @@ -34,3 +34,7 @@ A position can be set in 2 different values types:
If this value is a string (possible options are `'start'`, `'center'`, `'end'` or a string in percentage format), it is applied to vertical and horizontal position in the annotation.

If this value is an object, the `x` property defines the horizontal alignment in the annotation. Similarly, the `y` property defines the vertical alignment in the annotation. Possible options for both properties are `'start'`, `'center'`, `'end'`, a string in percentage format. Omitted property have value of the default, `'center'`.

### Fonts and colors

When the label to draw has multiple lines, you can use different font and color for each line of the label. This is enabled configuring an array of fonts or colors for those options. When the lines are more than the configured fonts of colors, the last configuration of those options is used for all remaining lines.
8 changes: 4 additions & 4 deletions docs/guide/types/label.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ The following options are available for label annotations.
| [`borderRadius`](#borderradius) | `number` \| `object` | Yes | `0`
| [`borderWidth`](#styling) | `number`| Yes | `0`
| [`callout`](#callout) | `object` | Yes |
| [`color`](#styling) | [`Color`](../options.md#color) | Yes | `'black'`
| [`color`](#styling) | [`Color`\|`Color[]`](../options#color) | Yes | `'black'`
| [`content`](#general) | `string`\|`string[]`\|[`Image`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image)\|[`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) | Yes | `null`
| [`font`](#styling) | [`Font`](../options.md#font) | Yes | `{}`
| [`font`](#styling) | [`Font`\|`Font[]`](../options#font) | Yes | `{}`
| [`height`](#general) | `number`\|`string` | Yes | `undefined`
| [`opacity`](#styling) | `number` | Yes | `undefined`
| [`padding`](#general) | [`Padding`](../options.md#padding) | Yes | `6`
Expand Down Expand Up @@ -122,8 +122,8 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
| `borderJoinStyle` | Border line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin).
| `borderShadowColor` | The color of the border shadow. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowColor).
| `borderWidth` | Stroke width (in pixels).
| `color` | Text color.
| `font` | Text font.
| `color` | Text color. When the label to draw has multiple lines, you can use different color for each line of the label. This is enabled configuring an array of colors. When the lines are more than the configured colors, the last configuration of this option is used for all remaining lines.
| `font` | Text font. When the label to draw has multiple lines, you can use different font for each line of the label. This is enabled configuring an array of fonts. When the lines are more than the configured fonts, the last configuration of this option is used for all remaining lines.
| `opacity` | Overrides the opacity of the image or canvas element. Could be set a number in the range 0.0 to 1.0, inclusive. If undefined, uses the opacity of the image or canvas element. It is used only when the content is an image or canvas element.
| `shadowBlur` | The amount of blur applied to shadow of the box where the label is located. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur).
| `shadowOffsetX` | The distance that shadow, of the box where the label is located, will be offset horizontally. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetX).
Expand Down
10 changes: 7 additions & 3 deletions docs/guide/types/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
| `borderShadowColor` | [`Color`](../options.md#color) | `'transparent'` | The color of border shadow of the box where the label is located. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowColor).
| `borderWidth` | `number` | `0` | The border line width (in pixels).
| [`callout`](#callout) | `object` | | Can connect the label to the line. See [callout](#callout).
| `color` | [`Color`](../options.md#color) | `'#fff'` | Text color.
| [`color`](#fonts-and-colors) | [`Color`\|`Color[]`](../options#color) | `'#fff'` | Text color.
| `content` | `string`\|`string[]`\|[`Image`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image)\|[`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) | `null` | The content to show in the label.
| `display` | `boolean` | `false` | Whether or not the label is shown.
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options.md#draw-time). Defaults to the line annotation draw time if unset.
| `font` | [`Font`](../options.md#font) | `{ weight: 'bold' }` | Label font.
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the line annotation draw time if unset.
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font.
| `height` | `number`\|`string` | `undefined` | Overrides the height of the image or canvas element. Could be set in pixel by a number, or in percentage of current height of image or canvas element by a string. If undefined, uses the height of the image or canvas element. It is used only when the content is an image or canvas element.
| `opacity` | `number` | `undefined` | Overrides the opacity of the image or canvas element. Could be set a number in the range 0.0 to 1.0, inclusive. If undefined, uses the opacity of the image or canvas element. It is used only when the content is an image or canvas element.
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
Expand All @@ -156,6 +156,10 @@ All of these options can be [Scriptable](../options.md#scriptable-options)

If this value is a number, it is applied to all corners of the rectangle (topLeft, topRight, bottomLeft, bottomRight). If this value is an object, the `topLeft` property defines the top-left corners border radius. Similarly, the `topRight`, `bottomLeft`, and `bottomRight` properties can also be specified. Omitted corners have radius of 0.

### Fonts and colors

When the label to draw has multiple lines, you can use different font and color for each line of the label. This is enabled configuring an array of fonts or colors for those options. When the lines are more than the configured fonts of colors, the last configuration of those options is used for all remaining lines.

### Callout

A callout can connect the label to the line when the label is arbitrarily (by `xAdjust` and `yAdjust` options) moved from its original position.
Expand Down
152 changes: 152 additions & 0 deletions docs/samples/label/fontsColors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Fonts and colors

```js chart-editor
// <block:setup:5>
const DATA_COUNT = 12;
const MIN = 0;
const MAX = 100;

const numberCfg = {count: DATA_COUNT, min: MIN, max: MAX};

const data = {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
datasets: [{
data: Utils.numbers(numberCfg)
}]
};
// </block:setup>

// <block:annotation1:1>
const annotation1 = {
type: 'label',
backgroundColor: 'rgba(0,0,0,0.2)',
borderRadius: 6,
borderWidth: 0,
callout: {
display: true
},
color: ['black,', 'black', 'green'],
content: ['March', 'is', 'annotated'],
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
position: {
x: 'center',
y: 'end'
},
xValue: 'March',
yAdjust: (ctx) => yOffset(ctx, 'March'),
yValue: (ctx) => yValue(ctx, 'March')
};
// </block:annotation1>

// <block:annotation2:2>
const annotation2 = {
type: 'label',
backgroundColor: 'rgba(0,0,0,0.2)',
borderRadius: 6,
borderWidth: 0,
callout: {
display: true
},
color: ['black,', 'black', 'green'],
content: ['June', 'is', 'annotated'],
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
position: {
x: 'center',
y: 'end'
},
xValue: 'June',
yAdjust: (ctx) => yOffset(ctx, 'June'),
yValue: (ctx) => yValue(ctx, 'June')
};
// </block:annotation2>

// <block:annotation3:3>
const annotation3 = {
type: 'label',
backgroundColor: 'rgba(0,0,0,0.2)',
borderRadius: 6,
borderWidth: 0,
callout: {
display: true
},
color: ['black,', 'black', 'green'],
content: ['October', 'is', 'annotated'],
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
position: {
x: 'center',
y: 'end'
},
xValue: 'October',
yAdjust: (ctx) => yOffset(ctx, 'October'),
yValue: (ctx) => yValue(ctx, 'October')
};
// </block:annotation3>

// <block:utils:4>
function yValue(ctx, label) {
const chart = ctx.chart;
const dataset = chart.data.datasets[0];
return dataset.data[chart.data.labels.indexOf(label)];
}

function yOffset(ctx, label) {
const value = yValue(ctx, label);
const chart = ctx.chart;
const scale = chart.scales.y;
const y = scale.getPixelForValue(value);
const lblPos = scale.getPixelForValue(100);
return lblPos - y - 5;
}

// </block:utils>

/* <block:config:0> */
const config = {
type: 'bar',
data,
options: {
scales: {
y: {
beginAtZero: true,
max: 130,
min: 0,
grid: {
color: (ctx)=> ctx.tick.value <= 100 ?
ctx.chart.scales.x.options.grid.color :
undefined
},
ticks: {
callback: (value) => value > 100 ? '' : value
}
}
},
plugins: {
annotation: {
annotations: {
annotation1,
annotation2,
annotation3
}
}
}
}
};
/* </block:config> */

const actions = [
{
name: 'Randomize',
handler: function(chart) {
chart.data.datasets.forEach(function(dataset, i) {
dataset.data = dataset.data.map(() => Utils.rand(MIN, MAX));
});
chart.update();
}
}
];

module.exports = {
actions: actions,
config: config,
};
```
6 changes: 4 additions & 2 deletions src/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers';
import {handleEvent, eventHooks, updateListeners} from './events';
import {invokeHook, elementHooks, updateHooks} from './hooks';
import {adjustScaleRange, verifyScaleOptions} from './scale';
import {updateElements, resolveType} from './elements';
import {updateElements, resolveType, isIndexable} from './elements';
import {annotationTypes} from './types';
import {requireVersion} from './helpers';
import {version} from '../package.json';
Expand Down Expand Up @@ -137,8 +137,10 @@ export default {
},
common: {
label: {
_indexable: isIndexable,
_fallback: true
}
},
_indexable: isIndexable
}
},

Expand Down
8 changes: 7 additions & 1 deletion src/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ const hooks = eventHooks.concat(elementHooks);
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
*/

/**
* @param {string} prop
* @returns {boolean}
*/
export const isIndexable = (prop) => prop === 'color' || prop === 'font';

/**
* Resolve the annotation type, checking if is supported.
* @param {string} [type=line] - annotation type
Expand Down Expand Up @@ -126,7 +132,7 @@ function resolveObj(resolver, defs) {
for (const prop of Object.keys(defs)) {
const optDefs = defs[prop];
const value = resolver[prop];
result[prop] = isObject(optDefs) ? resolveObj(value, optDefs) : value;
result[prop] = isObject(optDefs) && !isIndexable(prop) ? resolveObj(value, optDefs) : value;
}
return result;
}
Expand Down
79 changes: 59 additions & 20 deletions src/helpers/helpers.canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {clampAll, clamp} from './helpers.core';
import {calculateTextAlignment, getSize} from './helpers.options';

const widthCache = new Map();
const fontsKey = (fonts) => fonts.reduce(function(prev, item) {
prev += item.string;
return prev;
}, '');

/**
* @typedef { import('chart.js').Point } Point
Expand Down Expand Up @@ -77,22 +81,13 @@ export function measureLabelSize(ctx, options) {
height: getSize(content.height, options.height)
};
}
const font = toFont(options.font);
const optFont = options.font;
const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
const strokeWidth = options.textStrokeWidth;
const lines = isArray(content) ? content : [content];
const mapKey = lines.join() + font.string + strokeWidth + (ctx._measureText ? '-spriting' : '');
const mapKey = lines.join() + fontsKey(fonts) + strokeWidth + (ctx._measureText ? '-spriting' : '');
if (!widthCache.has(mapKey)) {
ctx.save();
ctx.font = font.string;
const count = lines.length;
let width = 0;
for (let i = 0; i < count; i++) {
const text = lines[i];
width = Math.max(width, ctx.measureText(text).width + strokeWidth);
}
ctx.restore();
const height = count * font.lineHeight + strokeWidth;
widthCache.set(mapKey, {width, height});
widthCache.set(mapKey, calculateLabelSize(ctx, lines, fonts, strokeWidth));
}
return widthCache.get(mapKey);
}
Expand Down Expand Up @@ -137,19 +132,19 @@ export function drawLabel(ctx, rect, options) {
return;
}
const labels = isArray(content) ? content : [content];
const font = toFont(options.font);
const lh = font.lineHeight;
const optFont = options.font;
const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
const optColor = options.color;
const colors = isArray(optColor) ? optColor : [optColor];
const x = calculateTextAlignment(rect, options);
const y = rect.y + (lh / 2) + options.textStrokeWidth / 2;
const y = rect.y + options.textStrokeWidth / 2;
ctx.save();
ctx.font = font.string;
ctx.textBaseline = 'middle';
ctx.textAlign = options.textAlign;
if (setTextStrokeStyle(ctx, options)) {
labels.forEach((l, i) => ctx.strokeText(l, x, y + (i * lh)));
applyLabelDecoration(ctx, {x, y}, labels, fonts);
}
ctx.fillStyle = options.color;
labels.forEach((l, i) => ctx.fillText(l, x, y + (i * lh)));
applyLabelContent(ctx, {x, y}, labels, {fonts, colors});
ctx.restore();
}

Expand All @@ -164,6 +159,50 @@ function setTextStrokeStyle(ctx, options) {
}
}

function calculateLabelSize(ctx, lines, fonts, strokeWidth) {
ctx.save();
const count = lines.length;
let width = 0;
let height = strokeWidth;
for (let i = 0; i < count; i++) {
const font = fonts[Math.min(i, fonts.length - 1)];
ctx.font = font.string;
const text = lines[i];
width = Math.max(width, ctx.measureText(text).width + strokeWidth);
height += font.lineHeight;
}
ctx.restore();
return {width, height};
}

function applyLabelDecoration(ctx, {x, y}, labels, fonts) {
ctx.beginPath();
let lhs = 0;
labels.forEach(function(l, i) {
const f = fonts[Math.min(i, fonts.length - 1)];
const lh = f.lineHeight;
ctx.font = f.string;
ctx.strokeText(l, x, y + lh / 2 + lhs);
lhs += lh;
});
ctx.stroke();
}

function applyLabelContent(ctx, {x, y}, labels, {fonts, colors}) {
let lhs = 0;
labels.forEach(function(l, i) {
const c = colors[Math.min(i, colors.length - 1)];
const f = fonts[Math.min(i, fonts.length - 1)];
const lh = f.lineHeight;
ctx.beginPath();
ctx.font = f.string;
ctx.fillStyle = c;
ctx.fillText(l, x, y + lh / 2 + lhs);
lhs += lh;
ctx.fill();
});
}

function getOpacity(value, elementValue) {
const opacity = isNumber(value) ? value : elementValue;
return isNumber(opacity) ? clamp(opacity, 0, 1) : 1;
Expand Down
Loading

0 comments on commit b9cbccf

Please sign in to comment.