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

Add hitTolerance option to all annotations #902

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions docs/guide/types/_commonInnerLabel.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the annotation draw time if unset
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font
| `height` | `number`\|`string` | `undefined` | Overrides the height of the image or canvas element. Could be set in pixel by a number, or in percentage of current height of image or canvas element by a string. If undefined, uses the height of the image or canvas element. It is used only when the content is an image or canvas element.
| `hitTolerance` | `number` | `undefined` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `opacity` | `number` | `undefined` | Overrides the opacity of the image or canvas element. Could be set a number in the range 0.0 to 1.0, inclusive. If undefined, uses the opacity of the image or canvas element. It is used only when the content is an image or canvas element.
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
| [`position`](#position) | `string`\|`{x: string, y: string}` | `'center'` | Anchor position of label in the annotation.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/_commonOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The following options are available for all annotations.
| [`borderShadowColor`](#styling) | [`Color`](../options.md#color) | Yes | `'transparent'`
| [`display`](#general) | `boolean` | Yes | `true`
| [`drawTime`](#general) | `string` | Yes | `'afterDatasetsDraw'`
| [`hitTolerance`](#general) | `number` | Yes | `0`
| [`init`](../configuration.html#common) | `boolean` | [See initial animation](../configuration.html#initial-animation) | `undefined`
| [`id`](#general) | `string` | No | `undefined`
| [`shadowBlur`](#styling) | `number` | Yes | `0`
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/box.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ If one of the axes does not match an axis in the chart, the box will take the en
| `adjustScaleRange` | Should the scale range be adjusted if this annotation is out of range.
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `rotation` | Rotation of the box in degrees.
| `xMax` | Right edge of the box in units along the x axis.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/ellipse.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ If one of the axes does not match an axis in the chart, the ellipse will take th
| `adjustScaleRange` | Should the scale range be adjusted if this annotation is out of range.
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `rotation` | Rotation of the ellipse in degrees, default is 0.
| `xMax` | Right edge of the ellipse in units along the x axis.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/label.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `height` | Overrides the height of the image or canvas element. Could be set in pixel by a number, or in percentage of current height of image or canvas element by a string. If undefined, uses the height of the image or canvas element. It is used only when the content is an image or canvas element.
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `padding` | The padding to add around the text label.
| `rotation` | Rotation of the label in degrees.
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/types/line.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ If `scaleID` is unset, then `xScaleID` and `yScaleID` are used to draw a line fr
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `endValue` | End two of the line when a single scale is specified.
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `scaleID` | ID of the scale in single scale mode. If unset, `xScaleID` and `yScaleID` are used.
| `value` | End one of the line when a single scale is specified.
| `xMax` | X coordinate of end two of the line in units along the x axis.
Expand Down Expand Up @@ -137,6 +139,7 @@ All of these options can be [Scriptable](../options.md#scriptable-options)
| `drawTime` | `string` | `options.drawTime` | See [drawTime](../options#draw-time). Defaults to the line annotation draw time if unset.
| [`font`](#fonts-and-colors) | [`Font`\|`Font[]`](../options#font) | `{ weight: 'bold' }` | Label font.
| `height` | `number`\|`string` | `undefined` | Overrides the height of the image or canvas element. Could be set in pixel by a number, or in percentage of current height of image or canvas element by a string. If undefined, uses the height of the image or canvas element. It is used only when the content is an image or canvas element.
| `hitTolerance` | `number` | `undefined` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `opacity` | `number` | `undefined` | Overrides the opacity of the image or canvas element. Could be set a number in the range 0.0 to 1.0, inclusive. If undefined, uses the opacity of the image or canvas element. It is used only when the content is an image or canvas element.
| `padding` | [`Padding`](../options.md#padding) | `6` | The padding to add around the text label.
| `position` | `string` | `'center'` | Anchor position of label on line. Possible options are: `'start'`, `'center'`, `'end'`. It can be set by a string in percentage format `'number%'` which are representing the percentage on the width of the line where the label will be located.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/point.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
| `adjustScaleRange` | Should the scale range be adjusted if this annotation is out of range.
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `radius` | Size of the point in pixels.
| `rotation` | Rotation of point, in degrees.
Expand Down
1 change: 1 addition & 0 deletions docs/guide/types/polygon.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ The 4 coordinates, xMin, xMax, yMin, yMax are optional. If not specified, the bo
| `adjustScaleRange` | Should the scale range be adjusted if this annotation is out of range.
| `display` | Whether or not this annotation is visible.
| `drawTime` | See [drawTime](../options.md#draw-time).
| `hitTolerance` | Amount of pixels to interact with annotations within some distance of the mouse point.
| `id` | Identifies a unique id for the annotation and it will be stored in the element context. When the annotations are defined by an object, the id is automatically set using the key used to store the annotations in the object. When the annotations are configured by an array, the id, passed by this option in the annotation, will be used.
| `radius` | Size of the polygon in pixels.
| `rotation` | Rotation of polygon, in degrees.
Expand Down
24 changes: 15 additions & 9 deletions src/helpers/helpers.core.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const isOlderPart = (act, req) => req > act || (act.length > req.length && act.s
export const EPSILON = 0.001;
export const clamp = (x, from, to) => Math.min(to, Math.max(from, x));

/**
* @param {{value: number, start: number, end: number}} limit
* @param {number} hitSize
* @returns {boolean}
*/
export const inLimit = (limit, hitSize) => limit.value >= limit.start - hitSize && limit.value <= limit.end + hitSize;

/**
* @param {Object} obj
* @param {number} from
Expand All @@ -26,28 +33,27 @@ export function clampAll(obj, from, to) {
* @param {Point} point
* @param {Point} center
* @param {number} radius
* @param {number} borderWidth
* @param {number} hitSize
* @returns {boolean}
*/
export function inPointRange(point, center, radius, borderWidth) {
export function inPointRange(point, center, radius, hitSize) {
if (!point || !center || radius <= 0) {
return false;
}
const hBorderWidth = borderWidth / 2;
return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hBorderWidth, 2);
return (Math.pow(point.x - center.x, 2) + Math.pow(point.y - center.y, 2)) <= Math.pow(radius + hitSize, 2);
}

/**
* @param {Point} point
* @param {{x: number, y: number, x2: number, y2: number}} rect
* @param {InteractionAxis} axis
* @param {number} borderWidth
* @param {{borderWidth: number, hitTolerance: number}} hitsize
* @returns {boolean}
*/
export function inBoxRange(point, {x, y, x2, y2}, axis, borderWidth) {
const hBorderWidth = borderWidth / 2;
const inRangeX = point.x >= x - hBorderWidth - EPSILON && point.x <= x2 + hBorderWidth + EPSILON;
const inRangeY = point.y >= y - hBorderWidth - EPSILON && point.y <= y2 + hBorderWidth + EPSILON;
export function inBoxRange(point, {x, y, x2, y2}, axis, {borderWidth, hitTolerance}) {
const hitSize = borderWidth / 2 + hitTolerance / 2;
const inRangeX = point.x >= x - hitSize - EPSILON && point.x <= x2 + hitSize + EPSILON;
const inRangeY = point.y >= y - hitSize - EPSILON && point.y <= y2 + hitSize + EPSILON;
if (axis === 'x') {
return inRangeX;
} else if (axis === 'y') {
Expand Down
4 changes: 3 additions & 1 deletion src/types/box.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default class BoxAnnotation extends Element {

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

getCenterPoint(useFinalPosition) {
Expand Down Expand Up @@ -43,6 +43,7 @@ BoxAnnotation.defaults = {
borderWidth: 1,
display: true,
init: undefined,
hitTolerance: 0,
label: {
backgroundColor: 'transparent',
borderWidth: 0,
Expand All @@ -61,6 +62,7 @@ BoxAnnotation.defaults = {
weight: 'bold'
},
height: undefined,
hitTolerance: undefined,
opacity: undefined,
padding: 6,
position: 'center',
Expand Down
13 changes: 6 additions & 7 deletions src/types/ellipse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ export default class EllipseAnnotation extends Element {

inRange(mouseX, mouseY, axis, useFinalPosition) {
const rotation = this.options.rotation;
const borderWidth = this.options.borderWidth;
const hitSize = (this.options.borderWidth + this.options.hitTolerance) / 2;
if (axis !== 'x' && axis !== 'y') {
return pointInEllipse({x: mouseX, y: mouseY}, this.getProps(['width', 'height', 'centerX', 'centerY'], useFinalPosition), rotation, borderWidth);
return pointInEllipse({x: mouseX, y: mouseY}, this.getProps(['width', 'height', 'centerX', 'centerY'], useFinalPosition), rotation, hitSize);
}
const {x, y, x2, y2} = this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition);
const hBorderWidth = borderWidth / 2;
const limit = axis === 'y' ? {start: y, end: y2} : {start: x, end: x2};
const rotatedPoint = rotated({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), toRadians(-rotation));
return rotatedPoint[axis] >= limit.start - hBorderWidth - EPSILON && rotatedPoint[axis] <= limit.end + hBorderWidth + EPSILON;
return rotatedPoint[axis] >= limit.start - hitSize - EPSILON && rotatedPoint[axis] <= limit.end + hitSize + EPSILON;
}

getCenterPoint(useFinalPosition) {
Expand Down Expand Up @@ -59,6 +58,7 @@ EllipseAnnotation.defaults = {
borderShadowColor: 'transparent',
borderWidth: 1,
display: true,
hitTolerance: 0,
init: undefined,
label: Object.assign({}, BoxAnnotation.defaults.label),
rotation: 0,
Expand All @@ -85,7 +85,7 @@ EllipseAnnotation.descriptors = {
}
};

function pointInEllipse(p, ellipse, rotation, borderWidth) {
function pointInEllipse(p, ellipse, rotation, hitSize) {
const {width, height, centerX, centerY} = ellipse;
const xRadius = width / 2;
const yRadius = height / 2;
Expand All @@ -95,10 +95,9 @@ function pointInEllipse(p, ellipse, rotation, borderWidth) {
}
// https://stackoverflow.com/questions/7946187/point-and-ellipse-rotated-position-test-algorithm
const angle = toRadians(rotation || 0);
const hBorderWidth = borderWidth / 2 || 0;
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const a = Math.pow(cosAngle * (p.x - centerX) + sinAngle * (p.y - centerY), 2);
const b = Math.pow(sinAngle * (p.x - centerX) - cosAngle * (p.y - centerY), 2);
return (a / Math.pow(xRadius + hBorderWidth, 2)) + (b / Math.pow(yRadius + hBorderWidth, 2)) <= 1.0001;
return (a / Math.pow(xRadius + hitSize, 2)) + (b / Math.pow(yRadius + hitSize, 2)) <= 1.0001;
}
3 changes: 2 additions & 1 deletion src/types/label.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default class LabelAnnotation extends Element {

inRange(mouseX, mouseY, axis, useFinalPosition) {
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);
return inBoxRange({x, y}, this.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), axis, this.options);
}

getCenterPoint(useFinalPosition) {
Expand Down Expand Up @@ -87,6 +87,7 @@ LabelAnnotation.defaults = {
weight: undefined
},
height: undefined,
hitTolerance: 0,
init: undefined,
opacity: undefined,
padding: 6,
Expand Down
16 changes: 10 additions & 6 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, distanceBetweenPoints} from 'chart.js/helpers';
import {EPSILON, clamp, rotated, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle, getElementCenterPoint, toPosition, getSize, resolveLineProperties, initAnimationProperties} from '../helpers';
import {EPSILON, clamp, rotated, measureLabelSize, getRelativePosition, setBorderStyle, setShadowStyle, getElementCenterPoint, toPosition, getSize, resolveLineProperties, initAnimationProperties, inLimit} from '../helpers';
import LabelAnnotation from './label';

const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
Expand All @@ -17,23 +17,24 @@ const angleInCurve = (start, cp, end, t) => -Math.atan2(coordAngleInCurve(start.
export default class LineAnnotation extends Element {

inRange(mouseX, mouseY, axis, useFinalPosition) {
const hBorderWidth = this.options.borderWidth / 2;
const hitSize = (this.options.borderWidth + this.options.hitTolerance) / 2;
if (axis !== 'x' && axis !== 'y') {
const point = {mouseX, mouseY};
const {path, ctx} = this;
if (path) {
setBorderStyle(ctx, this.options);
ctx.lineWidth += this.options.hitTolerance;
const {chart} = this.$context;
const mx = mouseX * chart.currentDevicePixelRatio;
const my = mouseY * chart.currentDevicePixelRatio;
const result = ctx.isPointInStroke(path, mx, my) || isOnLabel(this, point, useFinalPosition);
ctx.restore();
return result;
}
const epsilon = sqr(hBorderWidth);
const epsilon = sqr(hitSize);
return intersects(this, point, epsilon, useFinalPosition) || isOnLabel(this, point, useFinalPosition);
}
return inAxisRange(this, {mouseX, mouseY}, axis, {hBorderWidth, useFinalPosition});
return inAxisRange(this, {mouseX, mouseY}, axis, {hitSize, useFinalPosition});
}

getCenterPoint(useFinalPosition) {
Expand Down Expand Up @@ -142,6 +143,7 @@ LineAnnotation.defaults = {
display: true,
endValue: undefined,
init: undefined,
hitTolerance: 0,
label: {
backgroundColor: 'rgba(0,0,0,0.8)',
backgroundShadowColor: 'transparent',
Expand All @@ -166,6 +168,7 @@ LineAnnotation.defaults = {
weight: 'bold'
},
height: undefined,
hitTolerance: undefined,
opacity: undefined,
padding: 6,
position: 'center',
Expand Down Expand Up @@ -211,9 +214,9 @@ LineAnnotation.defaultRoutes = {
borderColor: 'color'
};

function inAxisRange(element, {mouseX, mouseY}, axis, {hBorderWidth, useFinalPosition}) {
function inAxisRange(element, {mouseX, mouseY}, axis, {hitSize, useFinalPosition}) {
const limit = rangeLimit(mouseX, mouseY, element.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition), axis);
return (limit.value >= limit.start - hBorderWidth && limit.value <= limit.end + hBorderWidth) || isOnLabel(element, {mouseX, mouseY}, useFinalPosition, axis);
return inLimit(limit, hitSize) || isOnLabel(element, {mouseX, mouseY}, useFinalPosition, axis);
}

function isLineInArea({x, y, x2, y2}, {top, right, bottom, left}) {
Expand Down Expand Up @@ -258,6 +261,7 @@ function intersects(element, {mouseX, mouseY}, epsilon = EPSILON, useFinalPositi
const dy = y2 - y1;
const lenSq = sqr(dx) + sqr(dy);
const t = lenSq === 0 ? -1 : ((mouseX - x1) * dx + (mouseY - y1) * dy) / lenSq;

let xx, yy;
if (t < 0) {
xx = x1;
Expand Down
10 changes: 5 additions & 5 deletions src/types/point.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import {Element} from 'chart.js';
import {inPointRange, getElementCenterPoint, resolvePointProperties, setBorderStyle, setShadowStyle, isImageOrCanvas, initAnimationProperties, drawPoint} from '../helpers';
import {inPointRange, getElementCenterPoint, resolvePointProperties, setBorderStyle, setShadowStyle, isImageOrCanvas, initAnimationProperties, drawPoint, inLimit} from '../helpers';

export default class PointAnnotation extends Element {

inRange(mouseX, mouseY, axis, useFinalPosition) {
const {x, y, x2, y2, width} = this.getProps(['x', 'y', 'x2', 'y2', 'width'], useFinalPosition);
const borderWidth = this.options.borderWidth;
const hitSize = (this.options.borderWidth + this.options.hitTolerance) / 2;
if (axis !== 'x' && axis !== 'y') {
return inPointRange({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), width / 2, borderWidth);
return inPointRange({x: mouseX, y: mouseY}, this.getCenterPoint(useFinalPosition), width / 2, hitSize);
}
const hBorderWidth = borderWidth / 2;
const limit = axis === 'y' ? {start: y, end: y2, value: mouseY} : {start: x, end: x2, value: mouseX};
return limit.value >= limit.start - hBorderWidth && limit.value <= limit.end + hBorderWidth;
return inLimit(limit, hitSize);
}

getCenterPoint(useFinalPosition) {
Expand Down Expand Up @@ -54,6 +53,7 @@ PointAnnotation.defaults = {
borderShadowColor: 'transparent',
borderWidth: 1,
display: true,
hitTolerance: 0,
init: undefined,
pointStyle: 'circle',
radius: 10,
Expand Down