Skip to content

Enable line annotation label as label sub-element #727

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 12 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions docs/guide/migrationV2.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ A number of changes were made to the configuration options passed to the plugin

## Elements

In `chartjs-plugin-annotation` plugin version 2 the label of box annotation is a sub-element. This has changed how to access to the label options. Now the label options are at `element.label.options`. The following example shows how to show and hide the label when the mouse is hovering the box:
In `chartjs-plugin-annotation` plugin version 2 the label of box and line annotations is a sub-element. This has changed how to access to the label options. Now the label options are at `element.label.options`. The following example shows how to show and hide the label when the mouse is hovering the box:

```javascript
type: 'box',
type: 'box', // or 'line'
enter: function({element}) {
element.label.options.display = true;
return true;
Expand All @@ -37,7 +37,6 @@ leave: function({element}) {
`chartjs-plugin-annotation` plugin version 2 hides the following methods in the `line` annotation element because they should be used only internally:

* `intersects`
* `labelIsVisible`
* `isOnLabel`

`chartjs-plugin-annotation` plugin version 2 normalizes the properties of the annotation elements in order to be based on common box model.
Expand Down
4 changes: 2 additions & 2 deletions docs/samples/line/labelVisibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ const annotation1 = {
// For simple property changes, you can directly modify the annotation
// element's properties then return true to force chart re-drawing. This is faster.
enter({element}, event) {
element.options.label.display = true;
element.label.options.display = true;
return true;
},
leave({element}, event) {
element.options.label.display = false;
element.label.options.display = false;
return true;
}
};
Expand Down
27 changes: 8 additions & 19 deletions src/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,11 @@ export default {
defaults: {
animations: {
numbers: {
properties: ['x', 'y', 'x2', 'y2', 'width', 'height', 'centerX', 'centerY', 'pointX', 'pointY', 'labelX', 'labelY', 'labelWidth', 'labelHeight', 'radius'],
properties: ['x', 'y', 'x2', 'y2', 'width', 'height', 'centerX', 'centerY', 'pointX', 'pointY', 'radius'],
type: 'number'
},
},
clip: true,
dblClickSpeed: 350, // ms
common: {
drawTime: 'afterDatasetsDraw',
interaction: {
Expand Down Expand Up @@ -144,40 +143,30 @@ export default {
function draw(chart, caller, clip) {
const {ctx, canvas, chartArea} = chart;
const {visibleElements} = chartStates.get(chart);
let box = {x: 0, y: 0, width: canvas.width, height: canvas.height};
let area = {left: 0, top: 0, width: canvas.width, height: canvas.height};

if (clip) {
clipArea(ctx, chartArea);
box = {x: chartArea.left, y: chartArea.top, width: chartArea.width, height: chartArea.height};
area = chartArea;
}

drawElements(ctx, visibleElements, caller, box);
drawElements(chart, visibleElements, caller, area);

if (clip) {
unclipArea(ctx);
}

visibleElements.forEach(el => {
if (!('drawLabel' in el)) {
return;
}
const label = el.options.label;
if (label && label.display && label.content && label.drawTime === caller) {
el.drawLabel(ctx, chartArea);
}
});
}

function drawElements(ctx, elements, caller, area) {
function drawElements(chart, elements, caller, area) {
for (const el of elements) {
if (el.options.drawTime === caller) {
el.draw(ctx);
el.draw(chart.ctx, area);
}
if (el.elements && el.elements.length) {
const box = 'getBoundingBox' in el ? el.getBoundingBox() : area;
for (const sub of el.elements) {
if (sub.options.drawTime === caller) {
sub.draw(ctx, box);
if (sub.options.display && sub.options.drawTime === caller) {
sub.draw(chart.ctx, box);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/types/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ export default class BoxAnnotation extends Element {
const borderWidth = this.options.borderWidth;
const halfBorder = borderWidth / 2;
return {
x: x + halfBorder + padding.left,
y: y + halfBorder + padding.top,
left: x + halfBorder + padding.left,
top: y + halfBorder + padding.top,
width: width - borderWidth - padding.width,
height: height - borderWidth - padding.height
};
Expand Down Expand Up @@ -161,6 +161,7 @@ function resolveLabelElementProperties(chart, properties, options) {
width,
height,
centerX: x + width / 2,
centerY: y + height / 2
centerY: y + height / 2,
rotation: label.rotation
};
}
18 changes: 9 additions & 9 deletions src/types/label.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import {Element} from 'chart.js';
import {drawBox, drawLabel, measureLabelSize, getChartPoint, toPosition, setBorderStyle, getSize, inBoxRange, isBoundToPoint, resolveBoxProperties, getRelativePosition, translate, rotated, getElementCenterPoint} from '../helpers';
import {toPadding, toRadians, distanceBetweenPoints, isObject} from 'chart.js/helpers';
import {toPadding, toRadians, distanceBetweenPoints} from 'chart.js/helpers';

const positions = ['left', 'bottom', 'top', 'right'];

export default class LabelAnnotation extends Element {

inRange(mouseX, mouseY, axis, useFinalPosition) {
const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.options.rotation));
const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-this.rotation));
return inBoxRange({x, y}, this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), axis, this.options.borderWidth);
}

getCenterPoint(useFinalPosition) {
return getElementCenterPoint(this, useFinalPosition);
}

draw(ctx, box) {
draw(ctx, area) {
const options = this.options;
if (!options.display || !options.content) {
return;
}
ctx.save();
translate(ctx, this.getCenterPoint(), options.rotation);
translate(ctx, this.getCenterPoint(), this.rotation);
drawCallout(ctx, this);
drawBox(ctx, this, options);
if (isObject(box)) {
const {x, y, width, height} = box;
if (area) {
// clip
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.rect(area.left, area.top, area.width, area.height);
ctx.clip();
}
drawLabel(ctx, getLabelSize(this), options);
Expand All @@ -49,7 +48,8 @@ export default class LabelAnnotation extends Element {
const properties = {
pointX: point.x,
pointY: point.y,
...boxSize
...boxSize,
rotation: options.rotation
};
properties.calloutPosition = options.callout.display && resolveCalloutPosition(properties, options.callout, options.rotation);
return properties;
Expand Down Expand Up @@ -163,7 +163,7 @@ function drawCallout(ctx, element) {
}
ctx.moveTo(sideStart.x, sideStart.y);
ctx.lineTo(sideEnd.x, sideEnd.y);
const rotatedPoint = rotated({x: pointX, y: pointY}, element.getCenterPoint(), toRadians(-options.rotation));
const rotatedPoint = rotated({x: pointX, y: pointY}, element.getCenterPoint(), toRadians(-element.rotation));
ctx.lineTo(rotatedPoint.x, rotatedPoint.y);
ctx.stroke();
ctx.restore();
Expand Down
105 changes: 26 additions & 79 deletions src/types/line.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Element} from 'chart.js';
import {PI, toRadians, toDegrees, toPadding, valueOrDefault} from 'chart.js/helpers';
import {EPSILON, clamp, scaleValue, rotated, drawBox, drawLabel, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle, translate, getElementCenterPoint, inBoxRange, retrieveScaleID, getDimensionByScale} from '../helpers';
import {PI, toRadians, toDegrees, toPadding} from 'chart.js/helpers';
import {EPSILON, clamp, scaleValue, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle, getElementCenterPoint, retrieveScaleID, getDimensionByScale} from '../helpers';

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 @@ -50,36 +50,14 @@ export default class LineAnnotation extends Element {
ctx.restore();
}

drawLabel(ctx, chartArea) {
if (!labelIsVisible(this, false, chartArea)) {
return;
}
const {labelX, labelY, labelCenterX, labelCenterY, labelWidth, labelHeight, labelRotation, labelPadding, labelTextSize, options: {label}} = this;

ctx.save();
translate(ctx, {x: labelCenterX, y: labelCenterY}, labelRotation);

const boxRect = {
x: labelX,
y: labelY,
width: labelWidth,
height: labelHeight
};
drawBox(ctx, boxRect, label);

const labelTextRect = {
x: labelX + labelPadding.left + label.borderWidth / 2,
y: labelY + labelPadding.top + label.borderWidth / 2,
width: labelTextSize.width,
height: labelTextSize.height
};
drawLabel(ctx, labelTextRect, label);
ctx.restore();
get label() {
return this.elements && this.elements[0];
}

resolveElementProperties(chart, options) {
const scale = chart.scales[options.scaleID];
const area = translateArea(chart.chartArea, {y: 'top', x: 'left', y2: 'bottom', x2: 'right'});
const {scales, chartArea} = chart;
const scale = scales[options.scaleID];
const area = {x: chartArea.left, y: chartArea.top, x2: chartArea.right, y2: chartArea.bottom};
let min, max;

if (scale) {
Expand All @@ -93,8 +71,8 @@ export default class LineAnnotation extends Element {
area.y2 = max;
}
} else {
const xScale = chart.scales[retrieveScaleID(chart.scales, options, 'xScaleID')];
const yScale = chart.scales[retrieveScaleID(chart.scales, options, 'yScaleID')];
const xScale = scales[retrieveScaleID(scales, options, 'xScaleID')];
const yScale = scales[retrieveScaleID(scales, options, 'yScaleID')];

if (xScale) {
applyScaleValueToDimension(area, xScale, {min: options.xMin, max: options.xMax, start: xScale.left, end: xScale.right, startProp: 'x', endProp: 'x2'});
Expand All @@ -111,11 +89,14 @@ export default class LineAnnotation extends Element {
: {x, y, x2, y2, width: Math.abs(x2 - x), height: Math.abs(y2 - y)};
properties.centerX = (x2 + x) / 2;
properties.centerY = (y2 + y) / 2;

const label = options.label;
if (label && label.content) {
return loadLabelRect(properties, chart, label);
if (!inside) {
options.label.display = false;
}
properties.elements = [{
type: 'label',
optionScope: 'label',
properties: resolveLabelElementProperties(chart, properties, options.label)
}];
return properties;
}
}
Expand Down Expand Up @@ -166,6 +147,9 @@ LineAnnotation.defaults = {
borderRadius: 6,
borderShadowColor: 'transparent',
borderWidth: 0,
callout: {
display: false
},
color: '#fff',
content: null,
display: false,
Expand Down Expand Up @@ -276,36 +260,9 @@ function intersects(element, {mouseX, mouseY}, epsilon = EPSILON, useFinalPositi
return (sqr(mouseX - xx) + sqr(mouseY - yy)) <= epsilon;
}

/**
* @param {boolean} useFinalPosition - use the element's animation target instead of current position
* @param {top, right, bottom, left} [chartArea] - optional, area of the chart
* @returns {boolean} true if the label is visible
*/
function labelIsVisible(element, useFinalPosition, chartArea) {
const labelOpts = element.options.label;
if (!labelOpts || !labelOpts.display) {
return false;
}
return !chartArea || isLineInArea(element.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), chartArea);
}

function isOnLabel(element, {mouseX, mouseY}, useFinalPosition, axis) {
if (!labelIsVisible(element, useFinalPosition)) {
return false;
}
const {labelX, labelY, labelX2, labelY2, labelCenterX, labelCenterY, labelRotation} = element.getProps(['labelX', 'labelY', 'labelX2', 'labelY2', 'labelCenterX', 'labelCenterY', 'labelRotation'], useFinalPosition);
const {x, y} = rotated({x: mouseX, y: mouseY}, {x: labelCenterX, y: labelCenterY}, -toRadians(labelRotation));
return inBoxRange({x, y}, {x: labelX, y: labelY, x2: labelX2, y2: labelY2}, axis, element.options.label.borderWidth);
}

function translateArea(source, mapping) {
const ret = {};
const keys = Object.keys(mapping);
const read = prop => valueOrDefault(source[prop], source[mapping[prop]]);
for (const prop of keys) {
ret[prop] = read(prop);
}
return ret;
const label = element.label;
return label.options.display && label.inRange(mouseX, mouseY, axis, useFinalPosition);
}

function applyScaleValueToDimension(area, scale, options) {
Expand All @@ -314,25 +271,15 @@ function applyScaleValueToDimension(area, scale, options) {
area[options.endProp] = dim.end;
}

function loadLabelRect(properties, chart, 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);
const width = textSize.width + padding.width + borderWidth;
const height = textSize.height + padding.height + borderWidth;
const labelRect = calculateLabelPosition(properties, options, {width, height, padding}, chart.chartArea);
properties.labelX = labelRect.x;
properties.labelY = labelRect.y;
properties.labelX2 = labelRect.x2;
properties.labelY2 = labelRect.y2;
properties.labelCenterX = labelRect.centerX;
properties.labelCenterY = labelRect.centerY;
properties.labelWidth = labelRect.width;
properties.labelHeight = labelRect.height;
properties.labelRotation = toDegrees(labelRect.rotation);
properties.labelPadding = padding;
properties.labelTextSize = textSize;
return properties;
return calculateLabelPosition(properties, options, {width, height, padding}, chart.chartArea);
}

function calculateAutoRotation(properties) {
Expand Down Expand Up @@ -364,7 +311,7 @@ function calculateLabelPosition(properties, label, sizes, chartArea) {
centerY,
width,
height,
rotation
rotation: toDegrees(rotation)
};
}

Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/line/label-dynamic-hide.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ module.exports = {
display: false
},
enter({element}) {
element.options.label.display = true;
element.label.options.display = true;
return true;
},
leave({element}) {
element.options.label.display = false;
element.label.options.display = false;
return true;
}
},
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/line/label-dynamic-show.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ module.exports = {
display: false
},
enter({element}) {
element.options.label.display = true;
element.label.options.display = true;
return true;
}
},
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/line/labelShadowColors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
tolerance: 0.0015,
config: {
type: 'scatter',
options: {
Expand Down Expand Up @@ -42,6 +43,7 @@ module.exports = {
borderColor: 'black',
borderWidth: 5,
label: {
drawTime: 'afterDraw',
position: 'center',
backgroundColor: 'red',
borderColor: 'rgb(101, 33, 171)',
Expand Down
Loading