]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
helpers.curve cleanup (#8608)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Wed, 10 Mar 2021 14:32:54 +0000 (16:32 +0200)
committerGitHub <noreply@github.com>
Wed, 10 Mar 2021 14:32:54 +0000 (09:32 -0500)
* helpers.curve cleanup
* Use distanceBetweenPoints

docs/docs/getting-started/v3-migration.md
src/helpers/helpers.canvas.js
src/helpers/helpers.curve.js
src/helpers/helpers.interpolation.js
test/specs/controller.radar.tests.js
test/specs/helpers.curve.tests.js
test/specs/helpers.interpolation.tests.js
types/helpers/helpers.curve.d.ts

index 9068d9fe6e4da2d5da2d07f1a46037b98cd80f08..0839631eaf2216c14cf8e1a8f2dd4c5c28bc3d28 100644 (file)
@@ -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`
 
index 02adec42c25cc5d5661b16460ec3766e7e5a424e..158f8af2d3a3a69ea388e8dfe487918f1f97e741 100644 (file)
@@ -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);
 }
index 4a2445cf60e599da5ad1b562f27a4f0c1782b60a..238a33c13c952f0800a57bc2277106c42bb73206 100644 (file)
@@ -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;
     }
   }
index 6441efaad3fbf6ef9098c211b8517e78d5e6756a..563b0d3cd1db7de05da83398a70cea003e8b792f 100644 (file)
@@ -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);
index 86e55632271b5a8c0b4c24af832fb76210e48aeb..b870315854f7f14fd30ee2e69f28c6e852463b4f 100644 (file)
@@ -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,
index 25a447befd70e8382bc9b926795a2c91d90c4aa6..96e7956a935a69846182a3208f34dda11949c627 100644 (file)
@@ -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
     }]);
   });
 });
index 644d93dbcfbf49f2189e9de2aa21452f0c472eac..5d436deae32d5bf391bd2bd58e28957ca4c7ba58 100644 (file)
@@ -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});
index 55a5efa215cc2bd7981ea3a60395dd14348aa632..8182c5ab5ff3e61e3958e6726962b5c8bf038ea3 100644 (file)
@@ -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;
 }
 
 /**