From: Jukka Kurkela Date: Wed, 10 Mar 2021 14:32:54 +0000 (+0200) Subject: helpers.curve cleanup (#8608) X-Git-Tag: v3.0.0-beta.14~31 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a4231486ebade0d1f4bbea9b7a425a85cb9d5ff8;p=thirdparty%2FChart.js.git helpers.curve cleanup (#8608) * helpers.curve cleanup * Use distanceBetweenPoints --- diff --git a/docs/docs/getting-started/v3-migration.md b/docs/docs/getting-started/v3-migration.md index 9068d9fe6..0839631ea 100644 --- a/docs/docs/getting-started/v3-migration.md +++ b/docs/docs/getting-started/v3-migration.md @@ -416,6 +416,10 @@ The following properties were renamed during v3 development: * `helpers.drawRoundedRectangle` was renamed to `helpers.roundedRect` * `helpers.getValueOrDefault` was renamed to `helpers.valueOrDefault` * `LayoutItem.fullWidth` was renamed to `LayoutItem.fullSize` +* `Point.controlPointPreviousX` was renamed to `Point.cp1x` +* `Point.controlPointPreviousY` was renamed to `Point.cp1y` +* `Point.controlPointNextX` was renamed to `Point.cp2x` +* `Point.controlPointNextY` was renamed to `Point.cp2y` * `Scale.calculateTickRotation` was renamed to `Scale.calculateLabelRotation` * `Tooltip.options.legendColorBackgroupd` was renamed to `Tooltip.options.multiKeyBackground` diff --git a/src/helpers/helpers.canvas.js b/src/helpers/helpers.canvas.js index 02adec42c..158f8af2d 100644 --- a/src/helpers/helpers.canvas.js +++ b/src/helpers/helpers.canvas.js @@ -292,10 +292,10 @@ export function _bezierCurveTo(ctx, previous, target, flip) { return ctx.lineTo(target.x, target.y); } ctx.bezierCurveTo( - flip ? previous.controlPointPreviousX : previous.controlPointNextX, - flip ? previous.controlPointPreviousY : previous.controlPointNextY, - flip ? target.controlPointNextX : target.controlPointPreviousX, - flip ? target.controlPointNextY : target.controlPointPreviousY, + flip ? previous.cp1x : previous.cp2x, + flip ? previous.cp1y : previous.cp2y, + flip ? target.cp2x : target.cp1x, + flip ? target.cp2y : target.cp1y, target.x, target.y); } diff --git a/src/helpers/helpers.curve.js b/src/helpers/helpers.curve.js index 4a2445cf6..238a33c13 100644 --- a/src/helpers/helpers.curve.js +++ b/src/helpers/helpers.curve.js @@ -1,7 +1,8 @@ -import {almostEquals, sign} from './helpers.math'; +import {almostEquals, distanceBetweenPoints, sign} from './helpers.math'; import {_isPointInArea} from './helpers.canvas'; const EPSILON = Number.EPSILON || 1e-14; +const getPoint = (points, i) => i < points.length && !points[i].skip && points[i]; export function splineCurve(firstPoint, middlePoint, afterPoint, t) { // Props to Rob Spencer at scaled innovation for his post on splining between points @@ -12,9 +13,8 @@ export function splineCurve(firstPoint, middlePoint, afterPoint, t) { const previous = firstPoint.skip ? middlePoint : firstPoint; const current = middlePoint; const next = afterPoint.skip ? middlePoint : afterPoint; - - const d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2)); - const d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2)); + const d01 = distanceBetweenPoints(current, previous); + const d12 = distanceBetweenPoints(next, current); let s01 = d01 / (d01 + d12); let s12 = d12 / (d01 + d12); @@ -38,94 +38,114 @@ export function splineCurve(firstPoint, middlePoint, afterPoint, t) { }; } -export function splineCurveMonotone(points) { - // This function calculates Bézier control points in a similar way than |splineCurve|, - // but preserves monotonicity of the provided data and ensures no local extremums are added - // between the dataset discrete points due to the interpolation. - // See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation - - const pointsWithTangents = (points || []).map((point) => ({ - model: point, - deltaK: 0, - mK: 0 - })); - - // Calculate slopes (deltaK) and initialize tangents (mK) - const pointsLen = pointsWithTangents.length; - let i, pointBefore, pointCurrent, pointAfter; - for (i = 0; i < pointsLen; ++i) { - pointCurrent = pointsWithTangents[i]; - if (pointCurrent.model.skip) { +/** + * Adjust tangents to ensure monotonic properties + */ +function monotoneAdjust(points, deltaK, mK) { + const pointsLen = points.length; + + let alphaK, betaK, tauK, squaredMagnitude, pointCurrent; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent || !pointAfter) { continue; } - pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; - pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; - if (pointAfter && !pointAfter.model.skip) { - const slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); - - // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 - pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; + if (almostEquals(deltaK[i], 0, EPSILON)) { + mK[i] = mK[i + 1] = 0; + continue; } - if (!pointBefore || pointBefore.model.skip) { - pointCurrent.mK = pointCurrent.deltaK; - } else if (!pointAfter || pointAfter.model.skip) { - pointCurrent.mK = pointBefore.deltaK; - } else if (sign(pointBefore.deltaK) !== sign(pointCurrent.deltaK)) { - pointCurrent.mK = 0; - } else { - pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2; + alphaK = mK[i] / deltaK[i]; + betaK = mK[i + 1] / deltaK[i]; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; } + + tauK = 3 / Math.sqrt(squaredMagnitude); + mK[i] = alphaK * tauK * deltaK[i]; + mK[i + 1] = betaK * tauK * deltaK[i]; } +} - // Adjust tangents to ensure monotonic properties - let alphaK, betaK, tauK, squaredMagnitude; - for (i = 0; i < pointsLen - 1; ++i) { - pointCurrent = pointsWithTangents[i]; - pointAfter = pointsWithTangents[i + 1]; - if (pointCurrent.model.skip || pointAfter.model.skip) { - continue; - } +function monotoneCompute(points, mK) { + const pointsLen = points.length; + let deltaX, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); - if (almostEquals(pointCurrent.deltaK, 0, EPSILON)) { - pointCurrent.mK = pointAfter.mK = 0; + for (let i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { continue; } - alphaK = pointCurrent.mK / pointCurrent.deltaK; - betaK = pointAfter.mK / pointCurrent.deltaK; - squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); - if (squaredMagnitude <= 9) { - continue; + const {x, y} = pointCurrent; + if (pointBefore) { + deltaX = (x - pointBefore.x) / 3; + pointCurrent.cp1x = x - deltaX; + pointCurrent.cp1y = y - deltaX * mK[i]; + } + if (pointAfter) { + deltaX = (pointAfter.x - x) / 3; + pointCurrent.cp2x = x + deltaX; + pointCurrent.cp2y = y + deltaX * mK[i]; } - - tauK = 3 / Math.sqrt(squaredMagnitude); - pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK; - pointAfter.mK = betaK * tauK * pointCurrent.deltaK; } +} + +/** + * This function calculates Bézier control points in a similar way than |splineCurve|, + * but preserves monotonicity of the provided data and ensures no local extremums are added + * between the dataset discrete points due to the interpolation. + * See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + * + * @param {{ + * x: number, + * y: number, + * skip?: boolean, + * cp1x?: number, + * cp1y?: number, + * cp2x?: number, + * cp2y?: number, + * }[]} points + */ +export function splineCurveMonotone(points) { + const pointsLen = points.length; + const deltaK = Array(pointsLen).fill(0); + const mK = Array(pointsLen); + + // Calculate slopes (deltaK) and initialize tangents (mK) + let i, pointBefore, pointCurrent; + let pointAfter = getPoint(points, 0); - // Compute control points - let deltaX; for (i = 0; i < pointsLen; ++i) { - pointCurrent = pointsWithTangents[i]; - if (pointCurrent.model.skip) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { continue; } - pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; - pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; - if (pointBefore && !pointBefore.model.skip) { - deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3; - pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX; - pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK; - } - if (pointAfter && !pointAfter.model.skip) { - deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3; - pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX; - pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK; + if (pointAfter) { + const slopeDeltaX = (pointAfter.x - pointCurrent.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + deltaK[i] = slopeDeltaX !== 0 ? (pointAfter.y - pointCurrent.y) / slopeDeltaX : 0; } + mK[i] = !pointBefore ? deltaK[i] + : !pointAfter ? deltaK[i - 1] + : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0 + : (deltaK[i - 1] + deltaK[i]) / 2; } + + monotoneAdjust(points, deltaK, mK); + + monotoneCompute(points, mK); } function capControlPoint(pt, min, max) { @@ -133,19 +153,23 @@ function capControlPoint(pt, min, max) { } function capBezierPoints(points, area) { - let i, ilen, point; + let i, ilen, point, inArea, inAreaPrev; + let inAreaNext = _isPointInArea(points[0], area); for (i = 0, ilen = points.length; i < ilen; ++i) { - point = points[i]; - if (!_isPointInArea(point, area)) { + inAreaPrev = inArea; + inArea = inAreaNext; + inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area); + if (!inArea) { continue; } - if (i > 0 && _isPointInArea(points[i - 1], area)) { - point.controlPointPreviousX = capControlPoint(point.controlPointPreviousX, area.left, area.right); - point.controlPointPreviousY = capControlPoint(point.controlPointPreviousY, area.top, area.bottom); + point = points[i]; + if (inAreaPrev) { + point.cp1x = capControlPoint(point.cp1x, area.left, area.right); + point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom); } - if (i < points.length - 1 && _isPointInArea(points[i + 1], area)) { - point.controlPointNextX = capControlPoint(point.controlPointNextX, area.left, area.right); - point.controlPointNextY = capControlPoint(point.controlPointNextY, area.top, area.bottom); + if (inAreaNext) { + point.cp2x = capControlPoint(point.cp2x, area.left, area.right); + point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom); } } } @@ -173,10 +197,10 @@ export function _updateBezierControlPoints(points, options, area, loop) { points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], options.tension ); - point.controlPointPreviousX = controlPoints.previous.x; - point.controlPointPreviousY = controlPoints.previous.y; - point.controlPointNextX = controlPoints.next.x; - point.controlPointNextY = controlPoints.next.y; + point.cp1x = controlPoints.previous.x; + point.cp1y = controlPoints.previous.y; + point.cp2x = controlPoints.next.x; + point.cp2y = controlPoints.next.y; prev = point; } } diff --git a/src/helpers/helpers.interpolation.js b/src/helpers/helpers.interpolation.js index 6441efaad..563b0d3cd 100644 --- a/src/helpers/helpers.interpolation.js +++ b/src/helpers/helpers.interpolation.js @@ -24,8 +24,8 @@ export function _steppedInterpolation(p1, p2, t, mode) { * @private */ export function _bezierInterpolation(p1, p2, t, mode) { // eslint-disable-line no-unused-vars - const cp1 = {x: p1.controlPointNextX, y: p1.controlPointNextY}; - const cp2 = {x: p2.controlPointPreviousX, y: p2.controlPointPreviousY}; + const cp1 = {x: p1.cp2x, y: p1.cp2y}; + const cp2 = {x: p2.cp1x, y: p2.cp1y}; const a = _pointInLine(p1, cp1, t); const b = _pointInLine(cp1, cp2, t); const c = _pointInLine(cp2, p2, t); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js index 86e556322..b87031585 100644 --- a/test/specs/controller.radar.tests.js +++ b/test/specs/controller.radar.tests.js @@ -161,10 +161,10 @@ describe('Chart.controllers.radar', function() { ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(expected.x); expect(meta.data[i].y).toBeCloseToPixel(expected.y); - expect(meta.data[i].controlPointPreviousX).toBeCloseToPixel(expected.cppx); - expect(meta.data[i].controlPointPreviousY).toBeCloseToPixel(expected.cppy); - expect(meta.data[i].controlPointNextX).toBeCloseToPixel(expected.cpnx); - expect(meta.data[i].controlPointNextY).toBeCloseToPixel(expected.cpny); + expect(meta.data[i].cp1x).toBeCloseToPixel(expected.cppx); + expect(meta.data[i].cp1y).toBeCloseToPixel(expected.cppy); + expect(meta.data[i].cp2x).toBeCloseToPixel(expected.cpnx); + expect(meta.data[i].cp2y).toBeCloseToPixel(expected.cpny); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.backgroundColor, borderWidth: 1, diff --git a/test/specs/helpers.curve.tests.js b/test/specs/helpers.curve.tests.js index 25a447bef..96e7956a9 100644 --- a/test/specs/helpers.curve.tests.js +++ b/test/specs/helpers.curve.tests.js @@ -69,51 +69,51 @@ describe('Curve helper tests', function() { x: 0, y: 0, skip: false, - controlPointNextX: 1, - controlPointNextY: 2 + cp2x: 1, + cp2y: 2 }, { x: 3, y: 6, skip: false, - controlPointPreviousX: 2, - controlPointPreviousY: 6, - controlPointNextX: 5, - controlPointNextY: 6 + cp1x: 2, + cp1y: 6, + cp2x: 5, + cp2y: 6 }, { x: 9, y: 6, skip: false, - controlPointPreviousX: 7, - controlPointPreviousY: 6, - controlPointNextX: 10, - controlPointNextY: 6 + cp1x: 7, + cp1y: 6, + cp2x: 10, + cp2y: 6 }, { x: 12, y: 60, skip: false, - controlPointPreviousX: 11, - controlPointPreviousY: 60, - controlPointNextX: 13, - controlPointNextY: 60 + cp1x: 11, + cp1y: 60, + cp2x: 13, + cp2y: 60 }, { x: 15, y: 60, skip: false, - controlPointPreviousX: 14, - controlPointPreviousY: 60, - controlPointNextX: 16, - controlPointNextY: 60 + cp1x: 14, + cp1y: 60, + cp2x: 16, + cp2y: 60 }, { x: 18, y: 120, skip: false, - controlPointPreviousX: 17, - controlPointPreviousY: 100 + cp1x: 17, + cp1y: 100 }, { x: null, @@ -124,60 +124,60 @@ describe('Curve helper tests', function() { x: 21, y: 180, skip: false, - controlPointNextX: 22, - controlPointNextY: 160 + cp2x: 22, + cp2y: 160 }, { x: 24, y: 120, skip: false, - controlPointPreviousX: 23, - controlPointPreviousY: 120, - controlPointNextX: 25, - controlPointNextY: 120 + cp1x: 23, + cp1y: 120, + cp2x: 25, + cp2y: 120 }, { x: 27, y: 125, skip: false, - controlPointPreviousX: 26, - controlPointPreviousY: 125, - controlPointNextX: 28, - controlPointNextY: 125 + cp1x: 26, + cp1y: 125, + cp2x: 28, + cp2y: 125 }, { x: 30, y: 105, skip: false, - controlPointPreviousX: 29, - controlPointPreviousY: 105, - controlPointNextX: 31, - controlPointNextY: 105 + cp1x: 29, + cp1y: 105, + cp2x: 31, + cp2y: 105 }, { x: 33, y: 110, skip: false, - controlPointPreviousX: 32, - controlPointPreviousY: 110, - controlPointNextX: 33, - controlPointNextY: 110 + cp1x: 32, + cp1y: 110, + cp2x: 33, + cp2y: 110 }, { x: 33, y: 110, skip: false, - controlPointPreviousX: 33, - controlPointPreviousY: 110, - controlPointNextX: 34, - controlPointNextY: 110 + cp1x: 33, + cp1y: 110, + cp2x: 34, + cp2y: 110 }, { x: 36, y: 170, skip: false, - controlPointPreviousX: 35, - controlPointPreviousY: 150 + cp1x: 35, + cp1y: 150 }]); }); }); diff --git a/test/specs/helpers.interpolation.tests.js b/test/specs/helpers.interpolation.tests.js index 644d93dbc..5d436deae 100644 --- a/test/specs/helpers.interpolation.tests.js +++ b/test/specs/helpers.interpolation.tests.js @@ -25,8 +25,8 @@ describe('helpers.interpolation', function() { }); it('Should interpolate a point in curve', function() { - const pt1 = {x: 10, y: 10, controlPointNextX: 12, controlPointNextY: 12}; - const pt2 = {x: 20, y: 30, controlPointPreviousX: 18, controlPointPreviousY: 28}; + const pt1 = {x: 10, y: 10, cp2x: 12, cp2y: 12}; + const pt2 = {x: 20, y: 30, cp1x: 18, cp1y: 28}; expect(_bezierInterpolation(pt1, pt2, 0)).toEqual({x: 10, y: 10}); expect(_bezierInterpolation(pt1, pt2, 0.2)).toBeCloseToPoint({x: 11.616, y: 12.656}); diff --git a/types/helpers/helpers.curve.d.ts b/types/helpers/helpers.curve.d.ts index 55a5efa21..8182c5ab5 100644 --- a/types/helpers/helpers.curve.d.ts +++ b/types/helpers/helpers.curve.d.ts @@ -19,10 +19,10 @@ export function splineCurve( export interface MonotoneSplinePoint extends SplinePoint { skip: boolean; - controlPointPreviousX?: number; - controlPointPreviousY?: number; - controlPointNextX?: number; - controlPointNextY?: number; + cp1x?: number; + cp1y?: number; + cp2x?: number; + cp2y?: number; } /**