Skip to content

Commit a423148

Browse files
authored
helpers.curve cleanup (#8608)
* helpers.curve cleanup * Use distanceBetweenPoints
1 parent d48a62a commit a423148

File tree

8 files changed

+172
-144
lines changed

8 files changed

+172
-144
lines changed

docs/docs/getting-started/v3-migration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ The following properties were renamed during v3 development:
416416
* `helpers.drawRoundedRectangle` was renamed to `helpers.roundedRect`
417417
* `helpers.getValueOrDefault` was renamed to `helpers.valueOrDefault`
418418
* `LayoutItem.fullWidth` was renamed to `LayoutItem.fullSize`
419+
* `Point.controlPointPreviousX` was renamed to `Point.cp1x`
420+
* `Point.controlPointPreviousY` was renamed to `Point.cp1y`
421+
* `Point.controlPointNextX` was renamed to `Point.cp2x`
422+
* `Point.controlPointNextY` was renamed to `Point.cp2y`
419423
* `Scale.calculateTickRotation` was renamed to `Scale.calculateLabelRotation`
420424
* `Tooltip.options.legendColorBackgroupd` was renamed to `Tooltip.options.multiKeyBackground`
421425

src/helpers/helpers.canvas.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,10 @@ export function _bezierCurveTo(ctx, previous, target, flip) {
292292
return ctx.lineTo(target.x, target.y);
293293
}
294294
ctx.bezierCurveTo(
295-
flip ? previous.controlPointPreviousX : previous.controlPointNextX,
296-
flip ? previous.controlPointPreviousY : previous.controlPointNextY,
297-
flip ? target.controlPointNextX : target.controlPointPreviousX,
298-
flip ? target.controlPointNextY : target.controlPointPreviousY,
295+
flip ? previous.cp1x : previous.cp2x,
296+
flip ? previous.cp1y : previous.cp2y,
297+
flip ? target.cp2x : target.cp1x,
298+
flip ? target.cp2y : target.cp1y,
299299
target.x,
300300
target.y);
301301
}

src/helpers/helpers.curve.js

Lines changed: 108 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {almostEquals, sign} from './helpers.math';
1+
import {almostEquals, distanceBetweenPoints, sign} from './helpers.math';
22
import {_isPointInArea} from './helpers.canvas';
33

44
const EPSILON = Number.EPSILON || 1e-14;
5+
const getPoint = (points, i) => i < points.length && !points[i].skip && points[i];
56

67
export function splineCurve(firstPoint, middlePoint, afterPoint, t) {
78
// 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) {
1213
const previous = firstPoint.skip ? middlePoint : firstPoint;
1314
const current = middlePoint;
1415
const next = afterPoint.skip ? middlePoint : afterPoint;
15-
16-
const d01 = Math.sqrt(Math.pow(current.x - previous.x, 2) + Math.pow(current.y - previous.y, 2));
17-
const d12 = Math.sqrt(Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2));
16+
const d01 = distanceBetweenPoints(current, previous);
17+
const d12 = distanceBetweenPoints(next, current);
1818

1919
let s01 = d01 / (d01 + d12);
2020
let s12 = d12 / (d01 + d12);
@@ -38,114 +38,138 @@ export function splineCurve(firstPoint, middlePoint, afterPoint, t) {
3838
};
3939
}
4040

41-
export function splineCurveMonotone(points) {
42-
// This function calculates Bézier control points in a similar way than |splineCurve|,
43-
// but preserves monotonicity of the provided data and ensures no local extremums are added
44-
// between the dataset discrete points due to the interpolation.
45-
// See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
46-
47-
const pointsWithTangents = (points || []).map((point) => ({
48-
model: point,
49-
deltaK: 0,
50-
mK: 0
51-
}));
52-
53-
// Calculate slopes (deltaK) and initialize tangents (mK)
54-
const pointsLen = pointsWithTangents.length;
55-
let i, pointBefore, pointCurrent, pointAfter;
56-
for (i = 0; i < pointsLen; ++i) {
57-
pointCurrent = pointsWithTangents[i];
58-
if (pointCurrent.model.skip) {
41+
/**
42+
* Adjust tangents to ensure monotonic properties
43+
*/
44+
function monotoneAdjust(points, deltaK, mK) {
45+
const pointsLen = points.length;
46+
47+
let alphaK, betaK, tauK, squaredMagnitude, pointCurrent;
48+
let pointAfter = getPoint(points, 0);
49+
for (let i = 0; i < pointsLen - 1; ++i) {
50+
pointCurrent = pointAfter;
51+
pointAfter = getPoint(points, i + 1);
52+
if (!pointCurrent || !pointAfter) {
5953
continue;
6054
}
6155

62-
pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
63-
pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
64-
if (pointAfter && !pointAfter.model.skip) {
65-
const slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x);
66-
67-
// In the case of two points that appear at the same x pixel, slopeDeltaX is 0
68-
pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0;
56+
if (almostEquals(deltaK[i], 0, EPSILON)) {
57+
mK[i] = mK[i + 1] = 0;
58+
continue;
6959
}
7060

71-
if (!pointBefore || pointBefore.model.skip) {
72-
pointCurrent.mK = pointCurrent.deltaK;
73-
} else if (!pointAfter || pointAfter.model.skip) {
74-
pointCurrent.mK = pointBefore.deltaK;
75-
} else if (sign(pointBefore.deltaK) !== sign(pointCurrent.deltaK)) {
76-
pointCurrent.mK = 0;
77-
} else {
78-
pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2;
61+
alphaK = mK[i] / deltaK[i];
62+
betaK = mK[i + 1] / deltaK[i];
63+
squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
64+
if (squaredMagnitude <= 9) {
65+
continue;
7966
}
67+
68+
tauK = 3 / Math.sqrt(squaredMagnitude);
69+
mK[i] = alphaK * tauK * deltaK[i];
70+
mK[i + 1] = betaK * tauK * deltaK[i];
8071
}
72+
}
8173

82-
// Adjust tangents to ensure monotonic properties
83-
let alphaK, betaK, tauK, squaredMagnitude;
84-
for (i = 0; i < pointsLen - 1; ++i) {
85-
pointCurrent = pointsWithTangents[i];
86-
pointAfter = pointsWithTangents[i + 1];
87-
if (pointCurrent.model.skip || pointAfter.model.skip) {
88-
continue;
89-
}
74+
function monotoneCompute(points, mK) {
75+
const pointsLen = points.length;
76+
let deltaX, pointBefore, pointCurrent;
77+
let pointAfter = getPoint(points, 0);
9078

91-
if (almostEquals(pointCurrent.deltaK, 0, EPSILON)) {
92-
pointCurrent.mK = pointAfter.mK = 0;
79+
for (let i = 0; i < pointsLen; ++i) {
80+
pointBefore = pointCurrent;
81+
pointCurrent = pointAfter;
82+
pointAfter = getPoint(points, i + 1);
83+
if (!pointCurrent) {
9384
continue;
9485
}
9586

96-
alphaK = pointCurrent.mK / pointCurrent.deltaK;
97-
betaK = pointAfter.mK / pointCurrent.deltaK;
98-
squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
99-
if (squaredMagnitude <= 9) {
100-
continue;
87+
const {x, y} = pointCurrent;
88+
if (pointBefore) {
89+
deltaX = (x - pointBefore.x) / 3;
90+
pointCurrent.cp1x = x - deltaX;
91+
pointCurrent.cp1y = y - deltaX * mK[i];
92+
}
93+
if (pointAfter) {
94+
deltaX = (pointAfter.x - x) / 3;
95+
pointCurrent.cp2x = x + deltaX;
96+
pointCurrent.cp2y = y + deltaX * mK[i];
10197
}
102-
103-
tauK = 3 / Math.sqrt(squaredMagnitude);
104-
pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK;
105-
pointAfter.mK = betaK * tauK * pointCurrent.deltaK;
10698
}
99+
}
100+
101+
/**
102+
* This function calculates Bézier control points in a similar way than |splineCurve|,
103+
* but preserves monotonicity of the provided data and ensures no local extremums are added
104+
* between the dataset discrete points due to the interpolation.
105+
* See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
106+
*
107+
* @param {{
108+
* x: number,
109+
* y: number,
110+
* skip?: boolean,
111+
* cp1x?: number,
112+
* cp1y?: number,
113+
* cp2x?: number,
114+
* cp2y?: number,
115+
* }[]} points
116+
*/
117+
export function splineCurveMonotone(points) {
118+
const pointsLen = points.length;
119+
const deltaK = Array(pointsLen).fill(0);
120+
const mK = Array(pointsLen);
121+
122+
// Calculate slopes (deltaK) and initialize tangents (mK)
123+
let i, pointBefore, pointCurrent;
124+
let pointAfter = getPoint(points, 0);
107125

108-
// Compute control points
109-
let deltaX;
110126
for (i = 0; i < pointsLen; ++i) {
111-
pointCurrent = pointsWithTangents[i];
112-
if (pointCurrent.model.skip) {
127+
pointBefore = pointCurrent;
128+
pointCurrent = pointAfter;
129+
pointAfter = getPoint(points, i + 1);
130+
if (!pointCurrent) {
113131
continue;
114132
}
115133

116-
pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
117-
pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
118-
if (pointBefore && !pointBefore.model.skip) {
119-
deltaX = (pointCurrent.model.x - pointBefore.model.x) / 3;
120-
pointCurrent.model.controlPointPreviousX = pointCurrent.model.x - deltaX;
121-
pointCurrent.model.controlPointPreviousY = pointCurrent.model.y - deltaX * pointCurrent.mK;
122-
}
123-
if (pointAfter && !pointAfter.model.skip) {
124-
deltaX = (pointAfter.model.x - pointCurrent.model.x) / 3;
125-
pointCurrent.model.controlPointNextX = pointCurrent.model.x + deltaX;
126-
pointCurrent.model.controlPointNextY = pointCurrent.model.y + deltaX * pointCurrent.mK;
134+
if (pointAfter) {
135+
const slopeDeltaX = (pointAfter.x - pointCurrent.x);
136+
137+
// In the case of two points that appear at the same x pixel, slopeDeltaX is 0
138+
deltaK[i] = slopeDeltaX !== 0 ? (pointAfter.y - pointCurrent.y) / slopeDeltaX : 0;
127139
}
140+
mK[i] = !pointBefore ? deltaK[i]
141+
: !pointAfter ? deltaK[i - 1]
142+
: (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0
143+
: (deltaK[i - 1] + deltaK[i]) / 2;
128144
}
145+
146+
monotoneAdjust(points, deltaK, mK);
147+
148+
monotoneCompute(points, mK);
129149
}
130150

131151
function capControlPoint(pt, min, max) {
132152
return Math.max(Math.min(pt, max), min);
133153
}
134154

135155
function capBezierPoints(points, area) {
136-
let i, ilen, point;
156+
let i, ilen, point, inArea, inAreaPrev;
157+
let inAreaNext = _isPointInArea(points[0], area);
137158
for (i = 0, ilen = points.length; i < ilen; ++i) {
138-
point = points[i];
139-
if (!_isPointInArea(point, area)) {
159+
inAreaPrev = inArea;
160+
inArea = inAreaNext;
161+
inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area);
162+
if (!inArea) {
140163
continue;
141164
}
142-
if (i > 0 && _isPointInArea(points[i - 1], area)) {
143-
point.controlPointPreviousX = capControlPoint(point.controlPointPreviousX, area.left, area.right);
144-
point.controlPointPreviousY = capControlPoint(point.controlPointPreviousY, area.top, area.bottom);
165+
point = points[i];
166+
if (inAreaPrev) {
167+
point.cp1x = capControlPoint(point.cp1x, area.left, area.right);
168+
point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom);
145169
}
146-
if (i < points.length - 1 && _isPointInArea(points[i + 1], area)) {
147-
point.controlPointNextX = capControlPoint(point.controlPointNextX, area.left, area.right);
148-
point.controlPointNextY = capControlPoint(point.controlPointNextY, area.top, area.bottom);
170+
if (inAreaNext) {
171+
point.cp2x = capControlPoint(point.cp2x, area.left, area.right);
172+
point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom);
149173
}
150174
}
151175
}
@@ -173,10 +197,10 @@ export function _updateBezierControlPoints(points, options, area, loop) {
173197
points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen],
174198
options.tension
175199
);
176-
point.controlPointPreviousX = controlPoints.previous.x;
177-
point.controlPointPreviousY = controlPoints.previous.y;
178-
point.controlPointNextX = controlPoints.next.x;
179-
point.controlPointNextY = controlPoints.next.y;
200+
point.cp1x = controlPoints.previous.x;
201+
point.cp1y = controlPoints.previous.y;
202+
point.cp2x = controlPoints.next.x;
203+
point.cp2y = controlPoints.next.y;
180204
prev = point;
181205
}
182206
}

src/helpers/helpers.interpolation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function _steppedInterpolation(p1, p2, t, mode) {
2424
* @private
2525
*/
2626
export function _bezierInterpolation(p1, p2, t, mode) { // eslint-disable-line no-unused-vars
27-
const cp1 = {x: p1.controlPointNextX, y: p1.controlPointNextY};
28-
const cp2 = {x: p2.controlPointPreviousX, y: p2.controlPointPreviousY};
27+
const cp1 = {x: p1.cp2x, y: p1.cp2y};
28+
const cp2 = {x: p2.cp1x, y: p2.cp1y};
2929
const a = _pointInLine(p1, cp1, t);
3030
const b = _pointInLine(cp1, cp2, t);
3131
const c = _pointInLine(cp2, p2, t);

test/specs/controller.radar.tests.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ describe('Chart.controllers.radar', function() {
161161
].forEach(function(expected, i) {
162162
expect(meta.data[i].x).toBeCloseToPixel(expected.x);
163163
expect(meta.data[i].y).toBeCloseToPixel(expected.y);
164-
expect(meta.data[i].controlPointPreviousX).toBeCloseToPixel(expected.cppx);
165-
expect(meta.data[i].controlPointPreviousY).toBeCloseToPixel(expected.cppy);
166-
expect(meta.data[i].controlPointNextX).toBeCloseToPixel(expected.cpnx);
167-
expect(meta.data[i].controlPointNextY).toBeCloseToPixel(expected.cpny);
164+
expect(meta.data[i].cp1x).toBeCloseToPixel(expected.cppx);
165+
expect(meta.data[i].cp1y).toBeCloseToPixel(expected.cppy);
166+
expect(meta.data[i].cp2x).toBeCloseToPixel(expected.cpnx);
167+
expect(meta.data[i].cp2y).toBeCloseToPixel(expected.cpny);
168168
expect(meta.data[i].options).toEqual(jasmine.objectContaining({
169169
backgroundColor: Chart.defaults.backgroundColor,
170170
borderWidth: 1,

0 commit comments

Comments
 (0)