diff --git a/docs/guide/migrationV2.md b/docs/guide/migrationV2.md index 7ebdc5b5f..b6c60fcc4 100644 --- a/docs/guide/migrationV2.md +++ b/docs/guide/migrationV2.md @@ -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; @@ -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. diff --git a/docs/samples/line/labelVisibility.md b/docs/samples/line/labelVisibility.md index fcd43e654..addab6459 100644 --- a/docs/samples/line/labelVisibility.md +++ b/docs/samples/line/labelVisibility.md @@ -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; } }; diff --git a/src/annotation.js b/src/annotation.js index 6a82cb074..db8e1eb60 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -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: { @@ -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); } } } diff --git a/src/types/box.js b/src/types/box.js index 5739bc5ce..6f3cbf104 100644 --- a/src/types/box.js +++ b/src/types/box.js @@ -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 }; @@ -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 }; } diff --git a/src/types/label.js b/src/types/label.js index 240ad30b4..4fa715e00 100644 --- a/src/types/label.js +++ b/src/types/label.js @@ -1,13 +1,13 @@ 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); } @@ -15,20 +15,19 @@ export default class LabelAnnotation extends Element { 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); @@ -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; @@ -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(); diff --git a/src/types/line.js b/src/types/line.js index 03648e5f6..78b871dca 100644 --- a/src/types/line.js +++ b/src/types/line.js @@ -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; @@ -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) { @@ -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'}); @@ -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; } } @@ -166,6 +147,9 @@ LineAnnotation.defaults = { borderRadius: 6, borderShadowColor: 'transparent', borderWidth: 0, + callout: { + display: false + }, color: '#fff', content: null, display: false, @@ -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) { @@ -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) { @@ -364,7 +311,7 @@ function calculateLabelPosition(properties, label, sizes, chartArea) { centerY, width, height, - rotation + rotation: toDegrees(rotation) }; } diff --git a/test/fixtures/line/label-dynamic-hide.js b/test/fixtures/line/label-dynamic-hide.js index 34d5fcbfc..c0ddeb883 100644 --- a/test/fixtures/line/label-dynamic-hide.js +++ b/test/fixtures/line/label-dynamic-hide.js @@ -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; } }, diff --git a/test/fixtures/line/label-dynamic-show.js b/test/fixtures/line/label-dynamic-show.js index 9acce182c..29a6caa97 100644 --- a/test/fixtures/line/label-dynamic-show.js +++ b/test/fixtures/line/label-dynamic-show.js @@ -36,7 +36,7 @@ module.exports = { display: false }, enter({element}) { - element.options.label.display = true; + element.label.options.display = true; return true; } }, diff --git a/test/fixtures/line/labelShadowColors.js b/test/fixtures/line/labelShadowColors.js index ed06cf1fb..5e88dc90a 100644 --- a/test/fixtures/line/labelShadowColors.js +++ b/test/fixtures/line/labelShadowColors.js @@ -1,4 +1,5 @@ module.exports = { + tolerance: 0.0015, config: { type: 'scatter', options: { @@ -42,6 +43,7 @@ module.exports = { borderColor: 'black', borderWidth: 5, label: { + drawTime: 'afterDraw', position: 'center', backgroundColor: 'red', borderColor: 'rgb(101, 33, 171)', diff --git a/test/specs/line.spec.js b/test/specs/line.spec.js index 6195e1ffa..4e5ea0ee7 100644 --- a/test/specs/line.spec.js +++ b/test/specs/line.spec.js @@ -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); } } @@ -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); } } @@ -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];