Skip to content

Commit b9cbccf

Browse files
authored
Enable different fonts and colors configuration on multiline labels (#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
1 parent 3abab64 commit b9cbccf

File tree

13 files changed

+352
-35
lines changed

13 files changed

+352
-35
lines changed

docs/.vuepress/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ module.exports = {
166166
'label/image',
167167
'label/innerChart',
168168
'label/lowerUpper',
169+
'label/fontsColors',
169170
'label/autoscaling'
170171
]
171172
},

docs/guide/types/_commonInnerLabel.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
66

77
| Name | Type | Default | Notes
88
| ---- | ---- | :----: | ----
9-
| `color` | [`Color`](../options.md#color) | `'black'` | Text color.
9+
| [`color`](#fonts-and-colors) | [`Color`\|`Color[]`](../options#color) | `'black'` | Text color.
1010
| `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.
1111
| `display` | `boolean` | `false` | Whether or not the label is shown.
12-
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options.md#draw-time). Defaults to the annotation draw time if unset
13-
| `font` | [`Font`](../options.md#font) | `{ weight: 'bold' }` | Label font
12+
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the annotation draw time if unset
13+
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font
1414
| `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.
1515
| `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.
1616
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
@@ -34,3 +34,7 @@ A position can be set in 2 different values types:
3434
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.
3535

3636
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'`.
37+
38+
### Fonts and colors
39+
40+
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.

docs/guide/types/label.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ The following options are available for label annotations.
6060
| [`borderRadius`](#borderradius) | `number` \| `object` | Yes | `0`
6161
| [`borderWidth`](#styling) | `number`| Yes | `0`
6262
| [`callout`](#callout) | `object` | Yes |
63-
| [`color`](#styling) | [`Color`](../options.md#color) | Yes | `'black'`
63+
| [`color`](#styling) | [`Color`\|`Color[]`](../options#color) | Yes | `'black'`
6464
| [`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`
65-
| [`font`](#styling) | [`Font`](../options.md#font) | Yes | `{}`
65+
| [`font`](#styling) | [`Font`\|`Font[]`](../options#font) | Yes | `{}`
6666
| [`height`](#general) | `number`\|`string` | Yes | `undefined`
6767
| [`opacity`](#styling) | `number` | Yes | `undefined`
6868
| [`padding`](#general) | [`Padding`](../options.md#padding) | Yes | `6`
@@ -122,8 +122,8 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
122122
| `borderJoinStyle` | Border line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin).
123123
| `borderShadowColor` | The color of the border shadow. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowColor).
124124
| `borderWidth` | Stroke width (in pixels).
125-
| `color` | Text color.
126-
| `font` | Text font.
125+
| `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.
126+
| `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.
127127
| `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.
128128
| `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).
129129
| `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).

docs/guide/types/line.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
131131
| `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).
132132
| `borderWidth` | `number` | `0` | The border line width (in pixels).
133133
| [`callout`](#callout) | `object` | | Can connect the label to the line. See [callout](#callout).
134-
| `color` | [`Color`](../options.md#color) | `'#fff'` | Text color.
134+
| [`color`](#fonts-and-colors) | [`Color`\|`Color[]`](../options#color) | `'#fff'` | Text color.
135135
| `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.
136136
| `display` | `boolean` | `false` | Whether or not the label is shown.
137-
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options.md#draw-time). Defaults to the line annotation draw time if unset.
138-
| `font` | [`Font`](../options.md#font) | `{ weight: 'bold' }` | Label font.
137+
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the line annotation draw time if unset.
138+
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font.
139139
| `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.
140140
| `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.
141141
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
@@ -156,6 +156,10 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
156156

157157
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.
158158

159+
### Fonts and colors
160+
161+
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.
162+
159163
### Callout
160164

161165
A callout can connect the label to the line when the label is arbitrarily (by `xAdjust` and `yAdjust` options) moved from its original position.

docs/samples/label/fontsColors.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Fonts and colors
2+
3+
```js chart-editor
4+
// <block:setup:5>
5+
const DATA_COUNT = 12;
6+
const MIN = 0;
7+
const MAX = 100;
8+
9+
const numberCfg = {count: DATA_COUNT, min: MIN, max: MAX};
10+
11+
const data = {
12+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
13+
datasets: [{
14+
data: Utils.numbers(numberCfg)
15+
}]
16+
};
17+
// </block:setup>
18+
19+
// <block:annotation1:1>
20+
const annotation1 = {
21+
type: 'label',
22+
backgroundColor: 'rgba(0,0,0,0.2)',
23+
borderRadius: 6,
24+
borderWidth: 0,
25+
callout: {
26+
display: true
27+
},
28+
color: ['black,', 'black', 'green'],
29+
content: ['March', 'is', 'annotated'],
30+
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
31+
position: {
32+
x: 'center',
33+
y: 'end'
34+
},
35+
xValue: 'March',
36+
yAdjust: (ctx) => yOffset(ctx, 'March'),
37+
yValue: (ctx) => yValue(ctx, 'March')
38+
};
39+
// </block:annotation1>
40+
41+
// <block:annotation2:2>
42+
const annotation2 = {
43+
type: 'label',
44+
backgroundColor: 'rgba(0,0,0,0.2)',
45+
borderRadius: 6,
46+
borderWidth: 0,
47+
callout: {
48+
display: true
49+
},
50+
color: ['black,', 'black', 'green'],
51+
content: ['June', 'is', 'annotated'],
52+
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
53+
position: {
54+
x: 'center',
55+
y: 'end'
56+
},
57+
xValue: 'June',
58+
yAdjust: (ctx) => yOffset(ctx, 'June'),
59+
yValue: (ctx) => yValue(ctx, 'June')
60+
};
61+
// </block:annotation2>
62+
63+
// <block:annotation3:3>
64+
const annotation3 = {
65+
type: 'label',
66+
backgroundColor: 'rgba(0,0,0,0.2)',
67+
borderRadius: 6,
68+
borderWidth: 0,
69+
callout: {
70+
display: true
71+
},
72+
color: ['black,', 'black', 'green'],
73+
content: ['October', 'is', 'annotated'],
74+
font: [{size: 16, weight: 'bold'}, {family: 'courier'}],
75+
position: {
76+
x: 'center',
77+
y: 'end'
78+
},
79+
xValue: 'October',
80+
yAdjust: (ctx) => yOffset(ctx, 'October'),
81+
yValue: (ctx) => yValue(ctx, 'October')
82+
};
83+
// </block:annotation3>
84+
85+
// <block:utils:4>
86+
function yValue(ctx, label) {
87+
const chart = ctx.chart;
88+
const dataset = chart.data.datasets[0];
89+
return dataset.data[chart.data.labels.indexOf(label)];
90+
}
91+
92+
function yOffset(ctx, label) {
93+
const value = yValue(ctx, label);
94+
const chart = ctx.chart;
95+
const scale = chart.scales.y;
96+
const y = scale.getPixelForValue(value);
97+
const lblPos = scale.getPixelForValue(100);
98+
return lblPos - y - 5;
99+
}
100+
101+
// </block:utils>
102+
103+
/* <block:config:0> */
104+
const config = {
105+
type: 'bar',
106+
data,
107+
options: {
108+
scales: {
109+
y: {
110+
beginAtZero: true,
111+
max: 130,
112+
min: 0,
113+
grid: {
114+
color: (ctx)=> ctx.tick.value <= 100 ?
115+
ctx.chart.scales.x.options.grid.color :
116+
undefined
117+
},
118+
ticks: {
119+
callback: (value) => value > 100 ? '' : value
120+
}
121+
}
122+
},
123+
plugins: {
124+
annotation: {
125+
annotations: {
126+
annotation1,
127+
annotation2,
128+
annotation3
129+
}
130+
}
131+
}
132+
}
133+
};
134+
/* </block:config> */
135+
136+
const actions = [
137+
{
138+
name: 'Randomize',
139+
handler: function(chart) {
140+
chart.data.datasets.forEach(function(dataset, i) {
141+
dataset.data = dataset.data.map(() => Utils.rand(MIN, MAX));
142+
});
143+
chart.update();
144+
}
145+
}
146+
];
147+
148+
module.exports = {
149+
actions: actions,
150+
config: config,
151+
};
152+
```

src/annotation.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers';
33
import {handleEvent, eventHooks, updateListeners} from './events';
44
import {invokeHook, elementHooks, updateHooks} from './hooks';
55
import {adjustScaleRange, verifyScaleOptions} from './scale';
6-
import {updateElements, resolveType} from './elements';
6+
import {updateElements, resolveType, isIndexable} from './elements';
77
import {annotationTypes} from './types';
88
import {requireVersion} from './helpers';
99
import {version} from '../package.json';
@@ -137,8 +137,10 @@ export default {
137137
},
138138
common: {
139139
label: {
140+
_indexable: isIndexable,
140141
_fallback: true
141-
}
142+
},
143+
_indexable: isIndexable
142144
}
143145
},
144146

src/elements.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const hooks = eventHooks.concat(elementHooks);
1616
* @typedef { import('../../types/options').AnnotationPluginOptions } AnnotationPluginOptions
1717
*/
1818

19+
/**
20+
* @param {string} prop
21+
* @returns {boolean}
22+
*/
23+
export const isIndexable = (prop) => prop === 'color' || prop === 'font';
24+
1925
/**
2026
* Resolve the annotation type, checking if is supported.
2127
* @param {string} [type=line] - annotation type
@@ -126,7 +132,7 @@ function resolveObj(resolver, defs) {
126132
for (const prop of Object.keys(defs)) {
127133
const optDefs = defs[prop];
128134
const value = resolver[prop];
129-
result[prop] = isObject(optDefs) ? resolveObj(value, optDefs) : value;
135+
result[prop] = isObject(optDefs) && !isIndexable(prop) ? resolveObj(value, optDefs) : value;
130136
}
131137
return result;
132138
}

src/helpers/helpers.canvas.js

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import {clampAll, clamp} from './helpers.core';
33
import {calculateTextAlignment, getSize} from './helpers.options';
44

55
const widthCache = new Map();
6+
const fontsKey = (fonts) => fonts.reduce(function(prev, item) {
7+
prev += item.string;
8+
return prev;
9+
}, '');
610

711
/**
812
* @typedef { import('chart.js').Point } Point
@@ -77,22 +81,13 @@ export function measureLabelSize(ctx, options) {
7781
height: getSize(content.height, options.height)
7882
};
7983
}
80-
const font = toFont(options.font);
84+
const optFont = options.font;
85+
const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
8186
const strokeWidth = options.textStrokeWidth;
8287
const lines = isArray(content) ? content : [content];
83-
const mapKey = lines.join() + font.string + strokeWidth + (ctx._measureText ? '-spriting' : '');
88+
const mapKey = lines.join() + fontsKey(fonts) + strokeWidth + (ctx._measureText ? '-spriting' : '');
8489
if (!widthCache.has(mapKey)) {
85-
ctx.save();
86-
ctx.font = font.string;
87-
const count = lines.length;
88-
let width = 0;
89-
for (let i = 0; i < count; i++) {
90-
const text = lines[i];
91-
width = Math.max(width, ctx.measureText(text).width + strokeWidth);
92-
}
93-
ctx.restore();
94-
const height = count * font.lineHeight + strokeWidth;
95-
widthCache.set(mapKey, {width, height});
90+
widthCache.set(mapKey, calculateLabelSize(ctx, lines, fonts, strokeWidth));
9691
}
9792
return widthCache.get(mapKey);
9893
}
@@ -137,19 +132,19 @@ export function drawLabel(ctx, rect, options) {
137132
return;
138133
}
139134
const labels = isArray(content) ? content : [content];
140-
const font = toFont(options.font);
141-
const lh = font.lineHeight;
135+
const optFont = options.font;
136+
const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
137+
const optColor = options.color;
138+
const colors = isArray(optColor) ? optColor : [optColor];
142139
const x = calculateTextAlignment(rect, options);
143-
const y = rect.y + (lh / 2) + options.textStrokeWidth / 2;
140+
const y = rect.y + options.textStrokeWidth / 2;
144141
ctx.save();
145-
ctx.font = font.string;
146142
ctx.textBaseline = 'middle';
147143
ctx.textAlign = options.textAlign;
148144
if (setTextStrokeStyle(ctx, options)) {
149-
labels.forEach((l, i) => ctx.strokeText(l, x, y + (i * lh)));
145+
applyLabelDecoration(ctx, {x, y}, labels, fonts);
150146
}
151-
ctx.fillStyle = options.color;
152-
labels.forEach((l, i) => ctx.fillText(l, x, y + (i * lh)));
147+
applyLabelContent(ctx, {x, y}, labels, {fonts, colors});
153148
ctx.restore();
154149
}
155150

@@ -164,6 +159,50 @@ function setTextStrokeStyle(ctx, options) {
164159
}
165160
}
166161

162+
function calculateLabelSize(ctx, lines, fonts, strokeWidth) {
163+
ctx.save();
164+
const count = lines.length;
165+
let width = 0;
166+
let height = strokeWidth;
167+
for (let i = 0; i < count; i++) {
168+
const font = fonts[Math.min(i, fonts.length - 1)];
169+
ctx.font = font.string;
170+
const text = lines[i];
171+
width = Math.max(width, ctx.measureText(text).width + strokeWidth);
172+
height += font.lineHeight;
173+
}
174+
ctx.restore();
175+
return {width, height};
176+
}
177+
178+
function applyLabelDecoration(ctx, {x, y}, labels, fonts) {
179+
ctx.beginPath();
180+
let lhs = 0;
181+
labels.forEach(function(l, i) {
182+
const f = fonts[Math.min(i, fonts.length - 1)];
183+
const lh = f.lineHeight;
184+
ctx.font = f.string;
185+
ctx.strokeText(l, x, y + lh / 2 + lhs);
186+
lhs += lh;
187+
});
188+
ctx.stroke();
189+
}
190+
191+
function applyLabelContent(ctx, {x, y}, labels, {fonts, colors}) {
192+
let lhs = 0;
193+
labels.forEach(function(l, i) {
194+
const c = colors[Math.min(i, colors.length - 1)];
195+
const f = fonts[Math.min(i, fonts.length - 1)];
196+
const lh = f.lineHeight;
197+
ctx.beginPath();
198+
ctx.font = f.string;
199+
ctx.fillStyle = c;
200+
ctx.fillText(l, x, y + lh / 2 + lhs);
201+
lhs += lh;
202+
ctx.fill();
203+
});
204+
}
205+
167206
function getOpacity(value, elementValue) {
168207
const opacity = isNumber(value) ? value : elementValue;
169208
return isNumber(opacity) ? clamp(opacity, 0, 1) : 1;

0 commit comments

Comments
 (0)