Skip to content

Commit

Permalink
Enable sub elements (#555)
Browse files Browse the repository at this point in the history
* Enable sub elements

* Box chore

* helpers chore

* CC

* Docs, initProperties (for animation staring point)

* subElements drawn within clip

* Fix clipping + modify fixture to catch

* Another fixture

* and another fixture

* ffs
  • Loading branch information
kurkle committed Jan 17, 2022
1 parent 44f961e commit 52adba7
Show file tree
Hide file tree
Showing 23 changed files with 356 additions and 109 deletions.
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 @@ -158,7 +158,7 @@ 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.

Expand Down
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

0 comments on commit 52adba7

Please sign in to comment.