diff --git a/src/types/line.js b/src/types/line.js index 56d5675a5..ca44c1ffb 100644 --- a/src/types/line.js +++ b/src/types/line.js @@ -1,39 +1,83 @@ -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 LabelAnnotation from './label'; - -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; -const interpolateY = (x, p1, p2) => pointInLine(p1, p2, Math.abs((x - p1.x) / (p2.x - p1.x))).y; -const sqr = v => v * v; -const rangeLimit = (mouseX, mouseY, {x, y, x2, y2}, axis) => axis === 'y' ? {start: Math.min(y, y2), end: Math.max(y, y2), value: mouseY} : {start: Math.min(x, x2), end: Math.max(x, x2), value: mouseX}; +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 LabelAnnotation from "./label"; + +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; +const interpolateY = (x, p1, p2) => + pointInLine(p1, p2, Math.abs((x - p1.x) / (p2.x - p1.x))).y; +const sqr = (v) => v * v; +const rangeLimit = (mouseX, mouseY, { x, y, x2, y2 }, axis) => + axis === "y" + ? { start: Math.min(y, y2), end: Math.max(y, y2), value: mouseY } + : { start: Math.min(x, x2), end: Math.max(x, x2), value: mouseX }; // http://www.independent-software.com/determining-coordinates-on-a-html-canvas-bezier-curve.html -const coordInCurve = (start, cp, end, t) => (1 - t) * (1 - t) * start + 2 * (1 - t) * t * cp + t * t * end; -const pointInCurve = (start, cp, end, t) => ({x: coordInCurve(start.x, cp.x, end.x, t), y: coordInCurve(start.y, cp.y, end.y, t)}); -const coordAngleInCurve = (start, cp, end, t) => 2 * (1 - t) * (cp - start) + 2 * t * (end - cp); -const angleInCurve = (start, cp, end, t) => -Math.atan2(coordAngleInCurve(start.x, cp.x, end.x, t), coordAngleInCurve(start.y, cp.y, end.y, t)) + 0.5 * PI; +const coordInCurve = (start, cp, end, t) => + (1 - t) * (1 - t) * start + 2 * (1 - t) * t * cp + t * t * end; +const pointInCurve = (start, cp, end, t) => ({ + x: coordInCurve(start.x, cp.x, end.x, t), + y: coordInCurve(start.y, cp.y, end.y, t), +}); +const coordAngleInCurve = (start, cp, end, t) => + 2 * (1 - t) * (cp - start) + 2 * t * (end - cp); +const angleInCurve = (start, cp, end, t) => + -Math.atan2( + coordAngleInCurve(start.x, cp.x, end.x, t), + coordAngleInCurve(start.y, cp.y, end.y, t) + ) + + 0.5 * PI; export default class LineAnnotation extends Element { - inRange(mouseX, mouseY, axis, useFinalPosition) { const hBorderWidth = this.options.borderWidth / 2; - if (axis !== 'x' && axis !== 'y') { - const point = {mouseX, mouseY}; - const {path, ctx} = this; + if (axis !== "x" && axis !== "y") { + const point = { mouseX, mouseY }; + const { path, ctx } = this; if (path) { setBorderStyle(ctx, this.options); - const {chart} = this.$context; + 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); + const result = + ctx.isPointInStroke(path, mx, my) || + isOnLabel(this, point, useFinalPosition); ctx.restore(); return result; } const epsilon = sqr(hBorderWidth); - return intersects(this, point, epsilon, useFinalPosition) || isOnLabel(this, point, useFinalPosition); + return ( + intersects(this, point, epsilon, useFinalPosition) || + isOnLabel(this, point, useFinalPosition) + ); } - return inAxisRange(this, {mouseX, mouseY}, axis, {hBorderWidth, useFinalPosition}); + return inAxisRange(this, { mouseX, mouseY }, axis, { + hBorderWidth, + useFinalPosition, + }); } getCenterPoint(useFinalPosition) { @@ -41,7 +85,7 @@ export default class LineAnnotation extends Element { } draw(ctx) { - const {x, y, x2, y2, cp, options} = this; + const { x, y, x2, y2, cp, options } = this; ctx.save(); if (!setBorderStyle(ctx, options)) { @@ -55,7 +99,7 @@ export default class LineAnnotation extends Element { drawCurve(ctx, this, cp, length); return ctx.restore(); } - const {startOpts, endOpts, startAdjust, endAdjust} = getArrowHeads(this); + const { startOpts, endOpts, startAdjust, endAdjust } = getArrowHeads(this); const angle = Math.atan2(y2 - y, x2 - x); ctx.translate(x, y); ctx.rotate(angle); @@ -75,43 +119,57 @@ export default class LineAnnotation extends Element { resolveElementProperties(chart, options) { const area = resolveLineProperties(chart, options); - const {x, y, x2, y2} = area; + const { x, y, x2, y2 } = area; const inside = isLineInArea(area, chart.chartArea); const properties = inside - ? limitLineToArea({x, y}, {x: x2, y: y2}, chart.chartArea) - : {x, y, x2, y2, width: Math.abs(x2 - x), height: Math.abs(y2 - y)}; + ? limitLineToArea({ x, y }, { x: x2, y: y2 }, chart.chartArea) + : { x, y, x2, y2, width: Math.abs(x2 - x), height: Math.abs(y2 - y) }; properties.centerX = (x2 + x) / 2; properties.centerY = (y2 + y) / 2; - properties.initProperties = initAnimationProperties(chart, properties, options); + properties.initProperties = initAnimationProperties( + chart, + properties, + options + ); if (options.curve) { - const p1 = {x: properties.x, y: properties.y}; - const p2 = {x: properties.x2, y: properties.y2}; - properties.cp = getControlPoint(properties, options, distanceBetweenPoints(p1, p2)); + const p1 = { x: properties.x, y: properties.y }; + const p2 = { x: properties.x2, y: properties.y2 }; + properties.cp = getControlPoint( + properties, + options, + distanceBetweenPoints(p1, p2) + ); } - const labelProperties = resolveLabelElementProperties(chart, properties, options.label); + const labelProperties = resolveLabelElementProperties( + chart, + properties, + options.label + ); // additonal prop to manage zoom/pan labelProperties._visible = inside; - properties.elements = [{ - type: 'label', - optionScope: 'label', - properties: labelProperties, - initProperties: properties.initProperties - }]; + properties.elements = [ + { + type: "label", + optionScope: "label", + properties: labelProperties, + initProperties: properties.initProperties, + }, + ]; return properties; } } -LineAnnotation.id = 'lineAnnotation'; +LineAnnotation.id = "lineAnnotation"; const arrowHeadsDefaults = { backgroundColor: undefined, backgroundShadowColor: undefined, - borderCapStyle: 'butt', + borderCapStyle: undefined, borderColor: undefined, borderDash: undefined, borderDashOffset: undefined, - borderJoinStyle: 'miter', + borderJoinStyle: undefined, borderShadowColor: undefined, borderWidth: undefined, display: undefined, @@ -120,7 +178,7 @@ const arrowHeadsDefaults = { shadowBlur: undefined, shadowOffsetX: undefined, shadowOffsetY: undefined, - width: undefined + width: undefined, }; LineAnnotation.defaults = { @@ -131,34 +189,34 @@ LineAnnotation.defaults = { fill: false, length: 12, start: Object.assign({}, arrowHeadsDefaults), - width: 6 + width: 6, }, - borderCapStyle: 'butt', + borderCapStyle: "butt", borderDash: [], borderDashOffset: 0, - borderJoinStyle: 'miter', - borderShadowColor: 'transparent', + borderJoinStyle: "miter", + borderShadowColor: "transparent", borderWidth: 2, curve: false, controlPoint: { - y: '-50%' + y: "-50%", }, display: true, endValue: undefined, init: undefined, label: { - backgroundColor: 'rgba(0,0,0,0.8)', - backgroundShadowColor: 'transparent', - borderCapStyle: 'butt', - borderColor: 'black', + backgroundColor: "rgba(0,0,0,0.8)", + backgroundShadowColor: "transparent", + borderCapStyle: "butt", + borderColor: "black", borderDash: [], borderDashOffset: 0, - borderJoinStyle: 'miter', + borderJoinStyle: "miter", borderRadius: 6, - borderShadowColor: 'transparent', + borderShadowColor: "transparent", borderWidth: 0, callout: Object.assign({}, LabelAnnotation.defaults.callout), - color: '#fff', + color: "#fff", content: null, display: false, drawTime: undefined, @@ -167,23 +225,23 @@ LineAnnotation.defaults = { lineHeight: undefined, size: undefined, style: undefined, - weight: 'bold' + weight: "bold", }, height: undefined, opacity: undefined, padding: 6, - position: 'center', + position: "center", rotation: 0, shadowBlur: 0, shadowOffsetX: 0, shadowOffsetY: 0, - textAlign: 'center', + textAlign: "center", textStrokeColor: undefined, textStrokeWidth: 0, width: undefined, xAdjust: 0, yAdjust: 0, - z: undefined + z: undefined, }, scaleID: undefined, shadowBlur: 0, @@ -196,31 +254,45 @@ LineAnnotation.defaults = { yMax: undefined, yMin: undefined, yScaleID: undefined, - z: 0 + z: 0, }; LineAnnotation.descriptors = { arrowHeads: { start: { - _fallback: true + _fallback: true, }, end: { - _fallback: true + _fallback: true, }, - _fallback: true - } + _fallback: true, + }, }; LineAnnotation.defaultRoutes = { - borderColor: 'color' + borderColor: "color", }; -function inAxisRange(element, {mouseX, mouseY}, axis, {hBorderWidth, 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); +function inAxisRange( + element, + { mouseX, mouseY }, + axis, + { hBorderWidth, 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) + ); } -function isLineInArea({x, y, x2, y2}, {top, right, bottom, left}) { +function isLineInArea({ x, y, x2, y2 }, { top, right, bottom, left }) { return !( (x < left && x2 < left) || (x > right && x2 > right) || @@ -229,39 +301,50 @@ function isLineInArea({x, y, x2, y2}, {top, right, bottom, left}) { ); } -function limitPointToArea({x, y}, p2, {top, right, bottom, left}) { +function limitPointToArea({ x, y }, p2, { top, right, bottom, left }) { if (x < left) { - y = interpolateY(left, {x, y}, p2); + y = interpolateY(left, { x, y }, p2); x = left; } if (x > right) { - y = interpolateY(right, {x, y}, p2); + y = interpolateY(right, { x, y }, p2); x = right; } if (y < top) { - x = interpolateX(top, {x, y}, p2); + x = interpolateX(top, { x, y }, p2); y = top; } if (y > bottom) { - x = interpolateX(bottom, {x, y}, p2); + x = interpolateX(bottom, { x, y }, p2); y = bottom; } - return {x, y}; + return { x, y }; } function limitLineToArea(p1, p2, area) { - const {x, y} = limitPointToArea(p1, p2, area); - const {x: x2, y: y2} = limitPointToArea(p2, p1, area); - return {x, y, x2, y2, width: Math.abs(x2 - x), height: Math.abs(y2 - y)}; + const { x, y } = limitPointToArea(p1, p2, area); + const { x: x2, y: y2 } = limitPointToArea(p2, p1, area); + return { x, y, x2, y2, width: Math.abs(x2 - x), height: Math.abs(y2 - y) }; } -function intersects(element, {mouseX, mouseY}, epsilon = EPSILON, useFinalPosition) { +function intersects( + element, + { mouseX, mouseY }, + epsilon = EPSILON, + useFinalPosition +) { // Adapted from https://stackoverflow.com/a/6853926/25507 - const {x: x1, y: y1, x2, y2} = element.getProps(['x', 'y', 'x2', 'y2'], useFinalPosition); + const { + x: x1, + y: y1, + x2, + y2, + } = element.getProps(["x", "y", "x2", "y2"], useFinalPosition); const dx = x2 - x1; const dy = y2 - y1; const lenSq = sqr(dx) + sqr(dy); - const t = lenSq === 0 ? -1 : ((mouseX - x1) * dx + (mouseY - y1) * dy) / lenSq; + const t = + lenSq === 0 ? -1 : ((mouseX - x1) * dx + (mouseY - y1) * dy) / lenSq; let xx, yy; if (t < 0) { xx = x1; @@ -273,12 +356,15 @@ function intersects(element, {mouseX, mouseY}, epsilon = EPSILON, useFinalPositi xx = x1 + t * dx; yy = y1 + t * dy; } - return (sqr(mouseX - xx) + sqr(mouseY - yy)) <= epsilon; + return sqr(mouseX - xx) + sqr(mouseY - yy) <= epsilon; } -function isOnLabel(element, {mouseX, mouseY}, useFinalPosition, axis) { +function isOnLabel(element, { mouseX, mouseY }, useFinalPosition, axis) { const label = element.label; - return label.options.display && label.inRange(mouseX, mouseY, axis, useFinalPosition); + return ( + label.options.display && + label.inRange(mouseX, mouseY, axis, useFinalPosition) + ); } function resolveLabelElementProperties(chart, properties, options) { @@ -287,41 +373,70 @@ function resolveLabelElementProperties(chart, properties, options) { const textSize = measureLabelSize(chart.ctx, options); const width = textSize.width + padding.width + borderWidth; const height = textSize.height + padding.height + borderWidth; - return calculateLabelPosition(properties, options, {width, height, padding}, chart.chartArea); + return calculateLabelPosition( + properties, + options, + { width, height, padding }, + chart.chartArea + ); } function calculateAutoRotation(properties) { - const {x, y, x2, y2} = properties; + const { x, y, x2, y2 } = properties; const rotation = Math.atan2(y2 - y, x2 - x); // Flip the rotation if it goes > PI/2 or < -PI/2, so label stays upright - return rotation > PI / 2 ? rotation - PI : rotation < PI / -2 ? rotation + PI : rotation; + return rotation > PI / 2 + ? rotation - PI + : rotation < PI / -2 + ? rotation + PI + : rotation; } function calculateLabelPosition(properties, label, sizes, chartArea) { - const {width, height, padding} = sizes; - const {xAdjust, yAdjust} = label; - const p1 = {x: properties.x, y: properties.y}; - const p2 = {x: properties.x2, y: properties.y2}; - const rotation = label.rotation === 'auto' ? calculateAutoRotation(properties) : toRadians(label.rotation); + const { width, height, padding } = sizes; + const { xAdjust, yAdjust } = label; + const p1 = { x: properties.x, y: properties.y }; + const p2 = { x: properties.x2, y: properties.y2 }; + const rotation = + label.rotation === "auto" + ? calculateAutoRotation(properties) + : toRadians(label.rotation); const size = rotatedSize(width, height, rotation); - const t = calculateT(properties, label, {labelSize: size, padding}, chartArea); - const pt = properties.cp ? pointInCurve(p1, properties.cp, p2, t) : pointInLine(p1, p2, t); - const xCoordinateSizes = {size: size.w, min: chartArea.left, max: chartArea.right, padding: padding.left}; - const yCoordinateSizes = {size: size.h, min: chartArea.top, max: chartArea.bottom, padding: padding.top}; + const t = calculateT( + properties, + label, + { labelSize: size, padding }, + chartArea + ); + const pt = properties.cp + ? pointInCurve(p1, properties.cp, p2, t) + : pointInLine(p1, p2, t); + const xCoordinateSizes = { + size: size.w, + min: chartArea.left, + max: chartArea.right, + padding: padding.left, + }; + const yCoordinateSizes = { + size: size.h, + min: chartArea.top, + max: chartArea.bottom, + padding: padding.top, + }; const centerX = adjustLabelCoordinate(pt.x, xCoordinateSizes) + xAdjust; const centerY = adjustLabelCoordinate(pt.y, yCoordinateSizes) + yAdjust; return { - x: centerX - (width / 2), - y: centerY - (height / 2), - x2: centerX + (width / 2), - y2: centerY + (height / 2), + x: centerX - width / 2, + y: centerY - height / 2, + x2: centerX + width / 2, + y2: centerY + height / 2, centerX, centerY, pointX: pt.x, pointY: pt.y, width, height, - rotation: toDegrees(rotation) + rotation: toDegrees(rotation), }; } @@ -330,17 +445,29 @@ function rotatedSize(width, height, rotation) { const sin = Math.sin(rotation); return { w: Math.abs(width * cos) + Math.abs(height * sin), - h: Math.abs(width * sin) + Math.abs(height * cos) + h: Math.abs(width * sin) + Math.abs(height * cos), }; } function calculateT(properties, label, sizes, chartArea) { let t; const space = spaceAround(properties, chartArea); - if (label.position === 'start') { - t = calculateTAdjust({w: properties.x2 - properties.x, h: properties.y2 - properties.y}, sizes, label, space); - } else if (label.position === 'end') { - t = 1 - calculateTAdjust({w: properties.x - properties.x2, h: properties.y - properties.y2}, sizes, label, space); + if (label.position === "start") { + t = calculateTAdjust( + { w: properties.x2 - properties.x, h: properties.y2 - properties.y }, + sizes, + label, + space + ); + } else if (label.position === "end") { + t = + 1 - + calculateTAdjust( + { w: properties.x - properties.x2, h: properties.y - properties.y2 }, + sizes, + label, + space + ); } else { t = getRelativePosition(1, label.position); } @@ -348,16 +475,16 @@ function calculateT(properties, label, sizes, chartArea) { } function calculateTAdjust(lineSize, sizes, label, space) { - const {labelSize, padding} = sizes; + const { labelSize, padding } = sizes; const lineW = lineSize.w * space.dx; const lineH = lineSize.h * space.dy; - const x = (lineW > 0) && ((labelSize.w / 2 + padding.left - space.x) / lineW); - const y = (lineH > 0) && ((labelSize.h / 2 + padding.top - space.y) / lineH); + const x = lineW > 0 && (labelSize.w / 2 + padding.left - space.x) / lineW; + const y = lineH > 0 && (labelSize.h / 2 + padding.top - space.y) / lineH; return clamp(Math.max(x, y), 0, 0.25); } function spaceAround(properties, chartArea) { - const {x, x2, y, y2} = properties; + const { x, x2, y, y2 } = properties; const t = Math.min(y, y2) - chartArea.top; const l = Math.min(x, x2) - chartArea.left; const b = chartArea.bottom - Math.max(y, y2); @@ -366,21 +493,21 @@ function spaceAround(properties, chartArea) { x: Math.min(l, r), y: Math.min(t, b), dx: l <= r ? 1 : -1, - dy: t <= b ? 1 : -1 + dy: t <= b ? 1 : -1, }; } function adjustLabelCoordinate(coordinate, labelSizes) { - const {size, min, max, padding} = labelSizes; + const { size, min, max, padding } = labelSizes; const halfSize = size / 2; if (size > max - min) { // if it does not fit, display as much as possible return (max + min) / 2; } - if (min >= (coordinate - padding - halfSize)) { + if (min >= coordinate - padding - halfSize) { coordinate = min + padding + halfSize; } - if (max <= (coordinate + padding + halfSize)) { + if (max <= coordinate + padding + halfSize) { coordinate = max - padding - halfSize; } return coordinate; @@ -394,7 +521,7 @@ function getArrowHeads(line) { startOpts: arrowStartOpts, endOpts: arrowEndOpts, startAdjust: getLineAdjust(line, arrowStartOpts), - endAdjust: getLineAdjust(line, arrowEndOpts) + endAdjust: getLineAdjust(line, arrowEndOpts), }; } @@ -402,10 +529,10 @@ function getLineAdjust(line, arrowOpts) { if (!arrowOpts || !arrowOpts.display) { return 0; } - const {length, width} = arrowOpts; + const { length, width } = arrowOpts; const adjust = line.options.borderWidth / 2; - const p1 = {x: length, y: width + adjust}; - const p2 = {x: 0, y: adjust}; + const p1 = { x: length, y: width + adjust }; + const p2 = { x: 0, y: adjust }; return Math.abs(interpolateX(0, p1, p2)); } @@ -413,7 +540,7 @@ function drawArrowHead(ctx, offset, adjust, arrowOpts) { if (!arrowOpts || !arrowOpts.display) { return; } - const {length, width, fill, backgroundColor, borderColor} = arrowOpts; + const { length, width, fill, backgroundColor, borderColor } = arrowOpts; const arrowOffsetX = Math.abs(offset - length) + adjust; ctx.beginPath(); setShadowStyle(ctx, arrowOpts); @@ -425,7 +552,7 @@ function drawArrowHead(ctx, offset, adjust, arrowOpts) { ctx.fillStyle = backgroundColor || borderColor; ctx.closePath(); ctx.fill(); - ctx.shadowColor = 'transparent'; + ctx.shadowColor = "transparent"; } else { ctx.shadowColor = arrowOpts.borderShadowColor; } @@ -433,17 +560,17 @@ function drawArrowHead(ctx, offset, adjust, arrowOpts) { } function getControlPoint(properties, options, distance) { - const {x, y, x2, y2, centerX, centerY} = properties; + const { x, y, x2, y2, centerX, centerY } = properties; const angle = Math.atan2(y2 - y, x2 - x); const cp = toPosition(options.controlPoint, 0); const point = { x: centerX + getSize(distance, cp.x, false), - y: centerY + getSize(distance, cp.y, false) + y: centerY + getSize(distance, cp.y, false), }; - return rotated(point, {x: centerX, y: centerY}, angle); + return rotated(point, { x: centerX, y: centerY }, angle); } -function drawArrowHeadOnCurve(ctx, {x, y}, {angle, adjust}, arrowOpts) { +function drawArrowHeadOnCurve(ctx, { x, y }, { angle, adjust }, arrowOpts) { if (!arrowOpts || !arrowOpts.display) { return; } @@ -455,10 +582,10 @@ function drawArrowHeadOnCurve(ctx, {x, y}, {angle, adjust}, arrowOpts) { } function drawCurve(ctx, element, cp, length) { - const {x, y, x2, y2, options} = element; - const {startOpts, endOpts, startAdjust, endAdjust} = getArrowHeads(element); - const p1 = {x, y}; - const p2 = {x: x2, y: y2}; + const { x, y, x2, y2, options } = element; + const { startOpts, endOpts, startAdjust, endAdjust } = getArrowHeads(element); + const p1 = { x, y }; + const p2 = { x: x2, y: y2 }; const startAngle = angleInCurve(p1, cp, p2, 0); const endAngle = angleInCurve(p1, cp, p2, 1) - PI; const ps = pointInCurve(p1, cp, p2, startAdjust / length); @@ -472,6 +599,16 @@ function drawCurve(ctx, element, cp, length) { ctx.stroke(path); element.path = path; element.ctx = ctx; - drawArrowHeadOnCurve(ctx, ps, {angle: startAngle, adjust: startAdjust}, startOpts); - drawArrowHeadOnCurve(ctx, pe, {angle: endAngle, adjust: endAdjust}, endOpts); + drawArrowHeadOnCurve( + ctx, + ps, + { angle: startAngle, adjust: startAdjust }, + startOpts + ); + drawArrowHeadOnCurve( + ctx, + pe, + { angle: endAngle, adjust: endAdjust }, + endOpts + ); }