Skip to content

Enable callout in the label of line annotation #740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ module.exports = {
'line/limited',
'line/average',
'line/standardDeviation',
'line/callout',
'line/visibility',
'line/labelVisibility',
'line/canvas',
Expand Down
23 changes: 23 additions & 0 deletions docs/guide/types/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ All of these options can be [Scriptable](../options#scriptable-options)
| [`borderRadius`](#borderradius) | `number` \| `object` | `6` | Radius of label box corners in pixels.
| `borderShadowColor` | [`Color`](../options#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#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.
Expand All @@ -149,6 +150,28 @@ All of these options can be [Scriptable](../options#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.

### 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.

Namespace: `options.annotations[annotationID].label.callout`, it defines options for the callout on the label of the line annotation.

All of these options can be [Scriptable](../options#scriptable-options).

| Name | Type | Default | Notes
| ---- | ---- | :----: | ----
| `borderCapStyle` | `string` | `'butt'` | Cap style of the border line of callout. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap).
| `borderColor` | [`Color`](../options#color) | `undefined` | Stroke color of the pointer of the callout.
| `borderDash` | `number[]` | `[]` | Length and spacing of dashes of callout. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash).
| `borderDashOffset` | `number` | `0` | Offset for line dashes of callout. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset).
| `borderJoinStyle` | `string` | `'miter'` | Border line join style of the callout. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin).
| `borderWidth` | `number` | `1` | Stroke width of the pointer of the callout.
| `display` | `boolean` | `false` | If true, the callout is drawn.
| `margin` | `number` | `5` | Amount of pixels between the label and the callout separator.
| `position` | `string` | `'auto'` | The position of callout, with respect to the label. Could be `left`, `top`, `right`, `bottom` or `auto`.
| `side` | `number` | `5` | Width of the starter line of callout pointer.
| `start` | `number`\|`string` | `'50%'` | The percentage of the separator dimension to use as starting point for callout pointer. Could be set in pixel by a number, or in percentage of the separator dimension by a string.

## Arrow heads

Namespace: `options.annotations[annotationID].arrowHeads`, it defines options for the line annotation arrow heads.
Expand Down
179 changes: 179 additions & 0 deletions docs/samples/line/callout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Callout

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

Utils.srand(8);

const labels = [];
for (let i = 0; i < DATA_COUNT; ++i) {
labels.push('' + i);
}

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

const data = {
labels: labels,
datasets: [{
data: Utils.numbers(numberCfg)
}]
};
// </block:setup>

// <block:annotation1:1>
const annotation1 = {
type: 'line',
borderColor: 'rgb(100, 149, 237)',
borderDash: [6, 6],
borderDashOffset: 0,
borderWidth: 3,
label: {
display: true,
backgroundColor: 'rgb(100, 149, 237)',
content: (ctx) => 'Average: ' + average(ctx).toFixed(2)
},
scaleID: 'y',
value: (ctx) => average(ctx)
};
// </block:annotation1>

// <block:annotation2:2>
const annotation2 = {
type: 'line',
borderColor: 'rgba(102, 102, 102, 0.5)',
borderDash: [6, 6],
borderDashOffset: 0,
borderWidth: 3,
label: {
display: true,
backgroundColor: 'rgba(102, 102, 102, 0.5)',
borderWidth: 1,
borderColor: 'rgba(102, 102, 102, 0.5)',
callout: {
display: true,
borderColor: 'rgba(102, 102, 102, 0.5)',
borderDash: [6, 6],
borderWidth: 2,
margin: 0
},
color: 'black',
content: (ctx) => (average(ctx) + standardDeviation(ctx)).toFixed(2),
position: 'start',
xAdjust: 100,
yAdjust: -50
},
scaleID: 'y',
value: (ctx) => average(ctx) + standardDeviation(ctx)
};
// </block:annotation2>

// <block:annotation3:3>
const annotation3 = {
type: 'line',
borderColor: 'rgba(102, 102, 102, 0.5)',
borderDash: [6, 6],
borderDashOffset: 0,
borderWidth: 3,
label: {
display: true,
backgroundColor: 'rgba(102, 102, 102, 0.5)',
borderWidth: 1,
borderColor: 'rgba(102, 102, 102, 0.5)',
callout: {
display: true,
borderColor: 'rgba(102, 102, 102, 0.5)',
borderDash: [6, 6],
borderWidth: 2,
margin: 0
},
color: 'black',
content: (ctx) => (average(ctx) - standardDeviation(ctx)).toFixed(2),
position: 'end',
xAdjust: -100,
yAdjust: 50
},
scaleID: 'y',
value: (ctx) => average(ctx) - standardDeviation(ctx)
};
// </block:annotation3>

/* <block:config:0> */
const config = {
type: 'line',
data,
options: {
scale: {
y: {
beginAtZero: true,
max: 120,
min: 0
}
},
plugins: {
annotation: {
annotations: {
annotation1,
annotation2,
annotation3
}
}
}
}
};
/* </block:config> */

// <block:utils:4>
function average(ctx) {
const values = ctx.chart.data.datasets[0].data;
return values.reduce((a, b) => a + b, 0) / values.length;
}

function standardDeviation(ctx) {
const values = ctx.chart.data.datasets[0].data;
const n = values.length;
const mean = average(ctx);
return Math.sqrt(values.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
}

// </block:utils>

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();
}
},
{
name: 'Add data',
handler: function(chart) {
chart.data.labels.push(chart.data.labels.length);
chart.data.datasets.forEach(function(dataset, i) {
dataset.data.push(Utils.rand(MIN, MAX));
});
chart.update();
}
},
{
name: 'Remove data',
handler: function(chart) {
chart.data.labels.shift();
chart.data.datasets.forEach(function(dataset, i) {
dataset.data.shift();
});
chart.update();
}
}
];

module.exports = {
actions: actions,
config: config,
};
```
9 changes: 4 additions & 5 deletions src/types/line.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Element} from 'chart.js';
import {PI, toRadians, toDegrees, toPadding} from 'chart.js/helpers';
import {EPSILON, clamp, scaleValue, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle, getElementCenterPoint, retrieveScaleID, getDimensionByScale} from '../helpers';
import LabelAnnotation from './label';

const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
const interpolateX = (y, p1, p2) => pointInLine(p1, p2, Math.abs((y - p1.y) / (p2.y - p1.y))).x;
Expand Down Expand Up @@ -147,9 +148,7 @@ LineAnnotation.defaults = {
borderRadius: 6,
borderShadowColor: 'transparent',
borderWidth: 0,
callout: {
display: false
},
callout: Object.assign({}, LabelAnnotation.defaults.callout),
color: '#fff',
content: null,
display: false,
Expand Down Expand Up @@ -274,8 +273,6 @@ function applyScaleValueToDimension(area, scale, options) {
}

function resolveLabelElementProperties(chart, properties, options) {
// TODO to remove by another PR to enable callout for line label
options.callout.display = false;
const borderWidth = options.borderWidth;
const padding = toPadding(options.padding);
const textSize = measureLabelSize(chart.ctx, options);
Expand Down Expand Up @@ -311,6 +308,8 @@ function calculateLabelPosition(properties, label, sizes, chartArea) {
y2: centerY + (height / 2),
centerX,
centerY,
pointX: pt.x,
pointY: pt.y,
width,
height,
rotation: toDegrees(rotation)
Expand Down
106 changes: 106 additions & 0 deletions test/fixtures/line/labelCallout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
module.exports = {
config: {
type: 'scatter',
options: {
scales: {
x: {
display: false,
min: 0,
max: 10
},
y: {
display: false,
min: 0,
max: 10
}
},
plugins: {
annotation: {
annotations: {
line1: {
type: 'line',
scaleID: 'y',
value: 8,
borderColor: 'black',
borderWidth: 5,
label: {
display: true,
backgroundColor: '#f5f5f5',
borderColor: 'black',
borderRadius: 0,
borderWidth: 1,
content: 'yAdjust: -40, callout position: auto',
yAdjust: -40,
callout: {
display: true
}
}
},
line2: {
type: 'line',
scaleID: 'y',
value: 6,
borderColor: 'black',
borderWidth: 5,
label: {
display: true,
backgroundColor: '#f5f5f5',
borderColor: 'black',
borderRadius: 0,
borderWidth: 1,
content: ['yAdjust: -40, xAdjust: -150', 'callout position: auto'],
yAdjust: -40,
xAdjust: -150,
callout: {
display: true
}
}
},
line3: {
type: 'line',
scaleID: 'y',
value: 4,
borderColor: 'black',
borderWidth: 5,
label: {
display: true,
backgroundColor: '#f5f5f5',
borderColor: 'black',
borderRadius: 0,
borderWidth: 1,
content: ['yAdjust: 40, xAdjust: 150', 'callout position: auto'],
yAdjust: 40,
xAdjust: 150,
callout: {
display: true
}
}
},
line4: {
type: 'line',
scaleID: 'y',
value: 2,
borderColor: 'black',
borderWidth: 5,
label: {
display: true,
backgroundColor: '#f5f5f5',
borderColor: 'black',
borderRadius: 0,
borderWidth: 1,
content: 'yAdjust: 40, callout position: auto',
yAdjust: 40,
callout: {
display: true
}
}
}
}
}
}
}
},
options: {
spriteText: true
}
};
Binary file added test/fixtures/line/labelCallout.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading