Skip to content
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

Enable sub elements #555

Merged
merged 10 commits into from Jan 17, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/types/box.md
Expand Up @@ -134,7 +134,7 @@ All of these options can be [Scriptable](../options#scriptable-options)
| `xAdjust` | `number` | `0` | Adjustment along x-axis (left-right) of label relative to computed position. Negative values move the label left, positive right.
| `yAdjust` | `number` | `0` | Adjustment along y-axis (top-bottom) of label relative to computed position. Negative values move the label up, positive down.

#### Position
### Position

A position can be set in 2 different values types:

Expand Down
3 changes: 1 addition & 2 deletions docs/guide/types/label.md
Expand Up @@ -95,7 +95,6 @@ If one of the axes does not match an axis in the chart, the content will be rend

The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the box is expanded out to the edges in the respective direction and the box size is used to calculated the center of the point. To enable to use the box positioning, the `radius` must be set to `0` or `NaN`.


| Name | Description
| ---- | ----
| `adjustScaleRange` | Should the scale range be adjusted if this annotation is out of range.
Expand Down Expand Up @@ -155,7 +154,7 @@ If this value is a number, it is applied to all corners of the rectangle (topLef

A callout connects the label by a line to the selected point.

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

```js chart-editor
/* <block:options:0> */
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/types/line.md
Expand Up @@ -157,6 +157,6 @@ All of these options can be [Scriptable](../options#scriptable-options)
| `yAdjust` | `number` | `0` | Adjustment along y-axis (top-bottom) of label relative to computed position. Negative values move the label up, positive down.
| `yPadding` | `number` | `6` | Padding of label to add top/bottom. This is **deprecated**. Use `padding`.

#### borderRadius
### borderRadius

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.
3 changes: 1 addition & 2 deletions docs/guide/types/point.md
Expand Up @@ -80,8 +80,7 @@ The following options are available for point annotations.

If one of the axes does not match an axis in the chart, the point annotation will take the center of the chart as point. The 2 coordinates, xValue, yValue are optional. If not specified, the point annotation will take the center of the chart as point.

The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the box is expanded out to the edges in the respective direction and the box size is used to calculated the center of the point. To enable to use the box positioning, the `radius` must be set to `0` or `NaN`.

The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the box is expanded out to the edges in the respective direction and the box size is used to calculated the center of the point. To enable to use the box positioning, the `radius` must be set to `0` or `NaN`.

| Name | Description
| ---- | ----
Expand Down
58 changes: 57 additions & 1 deletion docs/guide/types/polygon.md
Expand Up @@ -63,6 +63,7 @@ The following options are available for polygon annotations.
| [`borderWidth`](#styling) | `number`| Yes | `1`
| [`display`](#general) | `boolean` | Yes | `true`
| [`drawTime`](#general) | `string` | Yes | `'afterDatasetsDraw'`
| [`point`](#point) | `object` | Yes | `{radius: 0}`
| [`radius`](#general) | `number` | Yes | `10`
| [`rotation`](#general) | `number` | Yes | `0`
| [`shadowBlur`](#styling) | `number` | Yes | `0`
Expand All @@ -84,7 +85,7 @@ The following options are available for polygon annotations.

If one of the axes does not match an axis in the chart, the polygon annotation will take the center of the chart as point. The 2 coordinates, xValue, yValue are optional. If not specified, the polygon annotation will take the center of the chart.

The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the box is expanded out to the edges in the respective direction and the box size is used to calculated the center of the point. To enable to use the box positioning, the `radius` must be set to `0` or `NaN`.
The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the box is expanded out to the edges in the respective direction and the box size is used to calculated the center of the point. To enable to use the box positioning, the `radius` must be set to `0` or `NaN`.

| Name | Description
| ---- | ----
Expand Down Expand Up @@ -121,3 +122,58 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
| `shadowBlur` | The amount of blur applied to shadow. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur).
| `shadowOffsetX` | The distance that shadow will be offset horizontally. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetX).
| `shadowOffsetY` | The distance that shadow will be offset vertically. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowOffsetY).

### Point

Polygon consists of points. These points are actually [Point Annotations](point) and all of the [styling options](point#styling) can be configured. General options affecting the location of the point are ignored.

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

```js chart-editor
/* <block:options:0> */
const options = {
plugins: {
autocolors: false,
annotation: {
annotations: {
pentagon: {
type: 'polygon',
xValue: 1,
yValue: 60,
sides: 4,
radius: 60,
backgroundColor: 'rgba(255, 99, 132, 0.25)',
point: {
radius: 10,
borderWidth: 2,
borderColor: '#666',
backgroundColor: 'rgba(99, 132, 255, 0.25)',
}
}
}
}
}
};
/* </block:options> */

/* <block:config:1> */
const config = {
type: 'line',
data: {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'My First Dataset',
data: [65, 59, 80, 81, 56, 55, 40],
fill: false,
borderColor: 'rgb(75, 192, 192)',
tension: 0.1
}]
},
options
};
/* </block:config> */

module.exports = {
config
};
```
14 changes: 14 additions & 0 deletions docs/samples/polygon/basic.md
Expand Up @@ -123,6 +123,20 @@ const actions = [
});
chart.update();
}
},
{
name: 'Add a side to annotation 1',
handler: function(chart) {
chart.options.plugins.annotation.annotations.annotation1.sides++;
chart.update();
}
},
{
name: 'Remove a side from annotation 1',
handler: function(chart) {
chart.options.plugins.annotation.annotations.annotation1.sides--;
chart.update();
}
}
];

Expand Down
100 changes: 71 additions & 29 deletions src/annotation.js
Expand Up @@ -3,28 +3,22 @@ import {clipArea, unclipArea, isObject, isArray} from 'chart.js/helpers';
import {handleEvent, hooks, updateListeners} from './events';
import {adjustScaleRange, verifyScaleOptions} from './scale';
import {annotationTypes} from './types';
import {requireVersion} from './helpers';
import {version} from '../package.json';

const chartStates = new Map();
const versionParts = Chart.version.split('.');

export default {
id: 'annotation',

version,

beforeRegister() {
requireVersion('chart.js', '3.7', Chart.version);
},

afterRegister() {
Chart.register(annotationTypes);

// TODO: Remove this workaround when strictly requiring Chart.js v3.7 or newer
if (versionParts[0] === '3' && parseInt(versionParts[1], 10) <= 6) {
// Workaround for https://github.com/chartjs/chartjs-plugin-annotation/issues/572
Chart.defaults.set('elements.lineAnnotation', {
callout: {},
font: {},
padding: 6
});
}
},

afterUnregister() {
Expand Down Expand Up @@ -155,18 +149,49 @@ function updateElements(chart, state, options, mode) {
const elements = resyncElements(state.elements, annotations);

for (let i = 0; i < annotations.length; i++) {
const annotation = annotations[i];
let el = elements[i];
const elementClass = annotationTypes[resolveType(annotation.type)];
if (!el || !(el instanceof elementClass)) {
el = elements[i] = new elementClass();
}
const opts = resolveAnnotationOptions(annotation.setContext(getContext(chart, el, annotation)));
const properties = el.resolveElementProperties(chart, opts);
const annotationOptions = annotations[i];
const element = getOrCreateElement(elements, i, annotationOptions.type);
const resolver = annotationOptions.setContext(getContext(chart, element, annotationOptions));
const resolvedOptions = resolveAnnotationOptions(resolver);
const properties = element.resolveElementProperties(chart, resolvedOptions);

properties.skip = isNaN(properties.x) || isNaN(properties.y);
properties.options = opts;
animations.update(el, properties);
properties.options = resolvedOptions;

if ('elements' in properties) {
updateSubElements(element, properties, resolver, animations);
// Remove the sub-element definitions from properties, so the actual elements
// are not overwritten by their definitions
delete properties.elements;
}

animations.update(element, properties);
}
}

function updateSubElements(mainElement, {elements, initProperties}, resolver, animations) {
const subElements = mainElement.elements || (mainElement.elements = []);
subElements.length = elements.length;
for (let i = 0; i < elements.length; i++) {
const definition = elements[i];
const properties = definition.properties;
const subElement = getOrCreateElement(subElements, i, definition.type, initProperties);
const subResolver = resolver[definition.optionScope].override(definition);
properties.options = resolveAnnotationOptions(subResolver);
animations.update(subElement, properties);
}
}

function getOrCreateElement(elements, index, type, initProperties) {
const elementClass = annotationTypes[resolveType(type)];
let element = elements[index];
if (!element || !(element instanceof elementClass)) {
element = elements[index] = new elementClass();
if (isObject(initProperties)) {
Object.assign(element, initProperties);
}
}
return element;
}

function resolveAnnotationOptions(resolver) {
Expand All @@ -175,7 +200,9 @@ function resolveAnnotationOptions(resolver) {
result.id = resolver.id;
result.type = resolver.type;
result.drawTime = resolver.drawTime;
Object.assign(result, resolveObj(resolver, elementClass.defaults), resolveObj(resolver, elementClass.defaultRoutes));
Object.assign(result,
resolveObj(resolver, elementClass.defaults),
resolveObj(resolver, elementClass.defaultRoutes));
for (const hook of hooks) {
result[hook] = resolver[hook];
}
Expand Down Expand Up @@ -215,21 +242,20 @@ function resyncElements(elements, annotations) {

function draw(chart, caller, clip) {
const {ctx, chartArea} = chart;
const state = chartStates.get(chart);
const {visibleElements} = chartStates.get(chart);

if (clip) {
clipArea(ctx, chartArea);
}
state.visibleElements.forEach(el => {
if (el.options.drawTime === caller) {
el.draw(ctx);
}
});

drawElements(ctx, visibleElements, caller);
drawSubElements(ctx, visibleElements, caller);

if (clip) {
unclipArea(ctx);
}

state.visibleElements.forEach(el => {
visibleElements.forEach(el => {
if (!('drawLabel' in el)) {
return;
}
Expand All @@ -239,3 +265,19 @@ function draw(chart, caller, clip) {
}
});
}

function drawElements(ctx, elements, caller) {
for (const el of elements) {
if (el.options.drawTime === caller) {
el.draw(ctx);
}
}
}

function drawSubElements(ctx, elements, caller) {
for (const el of elements) {
if (isArray(el.elements)) {
drawElements(ctx, el.elements, caller);
}
}
}
40 changes: 39 additions & 1 deletion src/helpers/helpers.chart.js
Expand Up @@ -2,11 +2,30 @@ import {isFinite} from 'chart.js/helpers';
import {getRectCenterPoint} from './helpers.geometric';
import {isBoundToPoint} from './helpers.options';

/**
* @typedef { import("chart.js").Chart } Chart
* @typedef { import("chart.js").Scale } Scale
* @typedef { import("chart.js").Point } Point
* @typedef { import('../../types/options').CoreAnnotationOptions } CoreAnnotationOptions
* @typedef { import('../../types/options').PointAnnotationOptions } PointAnnotationOptions
*/

/**
* @param {Scale} scale
* @param {number|string} value
* @param {number} fallback
* @returns {number}
*/
export function scaleValue(scale, value, fallback) {
value = typeof value === 'number' ? value : scale.parse(value);
return isFinite(value) ? scale.getPixelForValue(value) : fallback;
}

/**
* @param {Scale} scale
* @param {{start: number, end: number}} options
* @returns {{start: number, end: number}}
*/
function getChartDimensionByScale(scale, options) {
if (scale) {
const min = scaleValue(scale, options.min, options.start);
Expand All @@ -22,6 +41,11 @@ function getChartDimensionByScale(scale, options) {
};
}

/**
* @param {Chart} chart
* @param {CoreAnnotationOptions} options
* @returns {Point}
*/
export function getChartPoint(chart, options) {
const {chartArea, scales} = chart;
const xScale = scales[options.xScaleID];
Expand All @@ -39,13 +63,18 @@ export function getChartPoint(chart, options) {
return {x, y};
}

/**
* @param {Chart} chart
* @param {CoreAnnotationOptions} options
* @returns {{x?:number, y?: number, x2?: number, y2?: number, width?: number, height?: number}}
*/
export function getChartRect(chart, options) {
const xScale = chart.scales[options.xScaleID];
const yScale = chart.scales[options.yScaleID];
let {top: y, left: x, bottom: y2, right: x2} = chart.chartArea;

if (!xScale && !yScale) {
return {options: {}};
return {};
}

const xDim = getChartDimensionByScale(xScale, {min: options.xMin, max: options.xMax, start: x, end: x2});
Expand All @@ -65,6 +94,10 @@ export function getChartRect(chart, options) {
};
}

/**
* @param {Chart} chart
* @param {PointAnnotationOptions} options
*/
export function getChartCircle(chart, options) {
const point = getChartPoint(chart, options);
return {
Expand All @@ -75,6 +108,11 @@ export function getChartCircle(chart, options) {
};
}

/**
* @param {Chart} chart
* @param {PointAnnotationOptions} options
* @returns
*/
export function resolvePointPosition(chart, options) {
if (!isBoundToPoint(options)) {
const box = getChartRect(chart, options);
Expand Down
16 changes: 16 additions & 0 deletions src/helpers/helpers.core.js
Expand Up @@ -27,3 +27,19 @@ export function getElementCenterPoint(element, useFinalPosition) {
const {x, y} = element.getProps(['x', 'y'], useFinalPosition);
return {x, y};
}

const isOlderPart = (act, req) => req > act || (act.length > req.length && act.substr(0, req.length) === req);
etimberg marked this conversation as resolved.
Show resolved Hide resolved

export function requireVersion(pkg, min, ver) {
const parts = ver.split('.');
let i = 0;
for (const req of min.split('.')) {
const act = parts[i++];
if (parseInt(req, 10) < parseInt(act, 10)) {
break;
}
if (isOlderPart(act, req)) {
throw new Error(`${pkg} v${ver} is not supported. v${min} or newer is required.`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kurkle maybe I'm wrong but this looks like a breaking change.
We are forcing all users to use CHART.JS 3.7.0. Maybe a warning could be emitted and in the version 2.0.0 of the plugin could be an error.
Furthermore, afa I understood, this strong dependency is related to the use of sub elements, currently used only in polygons and then, if a user is not using polygons, is anyway forced to change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kurkle if we want to maintain it, we should also add a note in the doc and change the README.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are probably right, we could do with a warning. The requirement is related to fallback from array options. So applies to use cases that proved annotations as array too. Those "work", but defaults come from line annotation and not from the actual type.

The workaround this PR removed from afterRegister needs to be restored and verify it contains sub-objects of every annotation types defaults.

}
}
}