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 2 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
19 changes: 5 additions & 14 deletions src/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,33 +144,24 @@ function draw(chart, caller, clip) {
box = {x: chartArea.left, y: chartArea.top, width: chartArea.width, height: chartArea.height};
}

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

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 || el.options.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);
}
if (el.elements && el.elements.length) {
const box = 'getBoundingBox' in el ? el.getBoundingBox() : area;
const labelIsVisible = 'labelIsVisible' in el ? el.labelIsVisible(false, chart.chartArea) : true;
for (const sub of el.elements) {
if (sub.options.drawTime === caller) {
sub.draw(ctx, box);
sub.draw(chart.ctx, box, labelIsVisible);
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function updateSubElements(mainElement, {elements, initProperties}, resolver, an
const properties = definition.properties;
const subElement = getOrCreateElement(subElements, i, definition.type, initProperties);
const subResolver = resolver[definition.optionScope].override(definition);
properties.options = resolveAnnotationOptions(subResolver);
properties.options = resolveAnnotationOptions(subResolver, resolver);
animations.update(subElement, properties);
}
}
Expand All @@ -103,12 +103,12 @@ function getOrCreateElement(elements, index, type, initProperties) {
return element;
}

function resolveAnnotationOptions(resolver) {
function resolveAnnotationOptions(resolver, mainResolver) {
const elementClass = annotationTypes[resolveType(resolver.type)];
const result = {};
result.id = resolver.id;
result.type = resolver.type;
result.drawTime = resolver.drawTime;
result.drawTime = resolver.drawTime || (mainResolver && mainResolver.drawTime);
Object.assign(result,
resolveObj(resolver, elementClass.defaults),
resolveObj(resolver, elementClass.defaultRoutes));
Expand Down
9 changes: 5 additions & 4 deletions src/types/label.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ 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 rotation = this.rotation || this.options.rotation;
const {x, y} = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-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, box, labelIsVisible = true) {
const options = this.options;
if (!options.display || !options.content) {
if (!labelIsVisible || !options.display || !options.content) {
return;
}
ctx.save();
translate(ctx, this.getCenterPoint(), options.rotation);
translate(ctx, this.getCenterPoint(), this.rotation || options.rotation);
drawCallout(ctx, this);
drawBox(ctx, this, options);
if (isObject(box)) {
Expand Down
92 changes: 32 additions & 60 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 {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,31 +50,21 @@ 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;
get label() {
return this.elements && this.elements[0];
}

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();
/**
* @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
*/
labelIsVisible(useFinalPosition, chartArea) {
const labelOpts = this.label.options;
if (!labelOpts || !labelOpts.display) {
return false;
}
return !chartArea || isLineInArea(this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), chartArea);
}

resolveElementProperties(chart, options) {
Expand Down Expand Up @@ -111,11 +101,11 @@ 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);
}
properties.elements = [{
type: 'label',
optionScope: 'label',
properties: resolveLabelElementProperties(chart, properties, options.label)
}];
return properties;
}
}
Expand Down Expand Up @@ -166,6 +156,9 @@ LineAnnotation.defaults = {
borderRadius: 6,
borderShadowColor: 'transparent',
borderWidth: 0,
callout: {
display: false
},
color: '#fff',
content: null,
display: false,
Expand Down Expand Up @@ -276,26 +269,11 @@ 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)) {
if (!element.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);
return element.label.inRange(mouseX, mouseY, axis, useFinalPosition);
}

function translateArea(source, mapping) {
Expand All @@ -314,25 +292,19 @@ 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 {
...labelRect,
rotation: toDegrees(labelRect.rotation)
};
}

function calculateAutoRotation(properties) {
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
28 changes: 14 additions & 14 deletions test/specs/line.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ describe('Line annotation', function() {
it('should return true inside label of element', function() {
for (const borderWidth of [0, 10]) {
const halfBorder = borderWidth / 2;
element.options.label.borderWidth = borderWidth;
element.label.options.borderWidth = borderWidth;
const rad = rotation / 180 * Math.PI;
for (const ax of [element.labelX - halfBorder, element.labelX2 + halfBorder]) {
for (const ay of [element.labelY - halfBorder, element.labelY2 + halfBorder]) {
for (const ax of [element.label.x - halfBorder, element.label.x2 + halfBorder]) {
for (const ay of [element.label.y - halfBorder, element.label.y2 + halfBorder]) {
const {x, y} = rotated({x: ax, y: ay},
{x: element.labelCenterX, y: element.labelCenterY}, rad);
{x: element.label.centerX, y: element.label.centerY}, rad);
expect(element.inRange(x, y)).withContext(`rotation: ${rotation}, borderWidth: ${borderWidth}, {x: ${x.toFixed(1)}, y: ${y.toFixed(1)}}`).toEqual(true);
}
}
Expand All @@ -86,12 +86,12 @@ describe('Line annotation', function() {
it('should return false outside label of element', function() {
for (const borderWidth of [0, 10]) {
const halfBorder = borderWidth / 2;
element.options.label.borderWidth = borderWidth;
element.label.options.borderWidth = borderWidth;
const rad = rotation / 180 * Math.PI;
for (const ax of [element.labelX - halfBorder - 1, element.labelX2 + halfBorder + 1]) {
for (const ay of [element.labelY - halfBorder - 1, element.labelY2 + halfBorder + 1]) {
for (const ax of [element.label.x - halfBorder - 1, element.label.x2 + halfBorder + 1]) {
for (const ay of [element.label.y - halfBorder - 1, element.label.y2 + halfBorder + 1]) {
const {x, y} = rotated({x: ax, y: ay},
{x: element.labelCenterX, y: element.labelCenterY}, rad);
{x: element.label.centerX, y: element.label.centerY}, rad);
expect(element.inRange(x, y)).withContext(`rotation: ${rotation}, borderWidth: ${borderWidth}, {x: ${x.toFixed(1)}, y: ${y.toFixed(1)}}`).toEqual(false);
}
}
Expand Down Expand Up @@ -201,12 +201,12 @@ describe('Line annotation', function() {
[true, false].forEach(function(intersect) {
interactionOpts.intersect = intersect;
const elementsCounts = interaction.axes[axis].intersect[intersect];
const points = [{x: outerEl.labelX, y: outerEl.labelY},
{x: innerEl.labelX, y: innerEl.labelY},
{x: innerEl.labelCenterX, y: innerEl.labelCenterY},
{x: innerEl.labelX2 + 1, y: innerEl.labelY},
{x: outerEl.labelX2 + 1, y: outerEl.labelY},
{x: outerEl.labelX + 1, y: outCenter.y - outerEl.height / 2 - 1}];
const points = [{x: outerEl.label.x, y: outerEl.label.y},
{x: innerEl.label.x, y: innerEl.label.y},
{x: innerEl.label.centerX, y: innerEl.label.centerY},
{x: innerEl.label.x2 + 1, y: innerEl.label.y},
{x: outerEl.label.x2 + 1, y: outerEl.label.y},
{x: outerEl.label.x + 1, y: outCenter.y - outerEl.height / 2 - 1}];

for (let i = 0; i < points.length; i++) {
const point = points[i];
Expand Down