mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Implement monotone cubic interpolation (see issue #3086).
This commit is contained in:
parent
ab340bd2cd
commit
2409908027
@ -39,7 +39,8 @@ label | `String` | The label for the dataset which appears in the legend and too
|
||||
xAxisID | `String` | The ID of the x axis to plot this dataset on
|
||||
yAxisID | `String` | The ID of the y axis to plot this dataset on
|
||||
fill | `Boolean` | If true, fill the area under the line
|
||||
lineTension | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. *Note* This was renamed from 'tension' but the old name still works.
|
||||
cubicInterpolationMode | `String` | Algorithm used to interpolate a smooth curve from the discrete data points. Options are 'default' and 'monotone'. The 'default' algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. The 'monotone' algorithm is more suited to `y = f(x)` datasets : it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. If unknown or `undefined`, this options is treated as 'default'.
|
||||
lineTension | `Number` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. *Note* This was renamed from 'tension' but the old name still works.
|
||||
backgroundColor | `Color` | The fill color under the line. See [Colors](#chart-configuration-colors)
|
||||
borderWidth | `Number` | The width of the line in pixels
|
||||
borderColor | `Color` | The color of the line.
|
||||
|
||||
@ -252,26 +252,40 @@ module.exports = function(Chart) {
|
||||
var points = (meta.data || []).filter(function(pt) { return !pt._model.skip; });
|
||||
var i, ilen, point, model, controlPoints;
|
||||
|
||||
var needToCap = me.chart.options.elements.line.capBezierPoints;
|
||||
function capIfNecessary(pt, min, max) {
|
||||
return needToCap ? Math.max(Math.min(pt, max), min) : pt;
|
||||
function capControlPoint(pt, min, max) {
|
||||
return Math.max(Math.min(pt, max), min);
|
||||
}
|
||||
|
||||
for (i=0, ilen=points.length; i<ilen; ++i) {
|
||||
point = points[i];
|
||||
model = point._model;
|
||||
controlPoints = helpers.splineCurve(
|
||||
helpers.previousItem(points, i)._model,
|
||||
model,
|
||||
helpers.nextItem(points, i)._model,
|
||||
meta.dataset._model.tension
|
||||
);
|
||||
|
||||
model.controlPointPreviousX = capIfNecessary(controlPoints.previous.x, area.left, area.right);
|
||||
model.controlPointPreviousY = capIfNecessary(controlPoints.previous.y, area.top, area.bottom);
|
||||
model.controlPointNextX = capIfNecessary(controlPoints.next.x, area.left, area.right);
|
||||
model.controlPointNextY = capIfNecessary(controlPoints.next.y, area.top, area.bottom);
|
||||
if (me.chart.options.elements.line.cubicInterpolationMode == 'monotone') {
|
||||
helpers.splineCurveMonotone(points);
|
||||
}
|
||||
else {
|
||||
for (i = 0, ilen = points.length; i < ilen; ++i) {
|
||||
point = points[i];
|
||||
model = point._model;
|
||||
controlPoints = helpers.splineCurve(
|
||||
helpers.previousItem(points, i)._model,
|
||||
model,
|
||||
helpers.nextItem(points, i)._model,
|
||||
meta.dataset._model.tension
|
||||
);
|
||||
model.controlPointPreviousX = controlPoints.previous.x;
|
||||
model.controlPointPreviousY = controlPoints.previous.y;
|
||||
model.controlPointNextX = controlPoints.next.x;
|
||||
model.controlPointNextY = controlPoints.next.y;
|
||||
}
|
||||
}
|
||||
|
||||
if (me.chart.options.elements.line.capBezierPoints) {
|
||||
for (i = 0, ilen = points.length; i < ilen; ++i) {
|
||||
model = points[i]._model;
|
||||
model.controlPointPreviousX = capControlPoint(model.controlPointPreviousX, area.left, area.right);
|
||||
model.controlPointPreviousY = capControlPoint(model.controlPointPreviousY, area.top, area.bottom);
|
||||
model.controlPointNextX = capControlPoint(model.controlPointNextX, area.left, area.right);
|
||||
model.controlPointNextY = capControlPoint(model.controlPointNextY, area.top, area.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
draw: function(ease) {
|
||||
|
||||
@ -338,6 +338,77 @@ module.exports = function(Chart) {
|
||||
}
|
||||
};
|
||||
};
|
||||
helpers.EPSILON = Number.EPSILON || 1e-14;
|
||||
helpers.splineCurveMonotone = function(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
|
||||
|
||||
var pointsWithTangents = (points || []).map(function(point) {
|
||||
return {
|
||||
model: point._model,
|
||||
deltaK: 0,
|
||||
mK: 0
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate slopes (deltaK) and initialize tangents (mK)
|
||||
var pointsLen = pointsWithTangents.length;
|
||||
var i, pointBefore, pointCurrent, pointAfter;
|
||||
for (i = 0; i < pointsLen; ++i) {
|
||||
pointCurrent = pointsWithTangents[i];
|
||||
if (pointCurrent.model.skip) continue;
|
||||
pointBefore = i > 0 ? pointsWithTangents[i - 1] : null;
|
||||
pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null;
|
||||
if (pointAfter && !pointAfter.model.skip) {
|
||||
pointCurrent.deltaK = (pointAfter.model.y - pointCurrent.model.y) / (pointAfter.model.x - pointCurrent.model.x);
|
||||
}
|
||||
if (!pointBefore || pointBefore.model.skip) pointCurrent.mK = pointCurrent.deltaK;
|
||||
else if (!pointAfter || pointAfter.model.skip) pointCurrent.mK = pointBefore.deltaK;
|
||||
else if (Math.sign(pointBefore.deltaK) != Math.sign(pointCurrent.deltaK)) pointCurrent.mK = 0;
|
||||
else pointCurrent.mK = (pointBefore.deltaK + pointCurrent.deltaK) / 2;
|
||||
}
|
||||
|
||||
// Adjust tangents to ensure monotonic properties
|
||||
var alphaK, betaK, tauK, squaredMagnitude;
|
||||
for (i = 0; i < pointsLen - 1; ++i) {
|
||||
pointCurrent = pointsWithTangents[i];
|
||||
pointAfter = pointsWithTangents[i + 1];
|
||||
if (pointCurrent.skip || pointAfter.skip) continue;
|
||||
if (helpers.almostEquals(pointCurrent.deltaK, 0, this.EPSILON))
|
||||
{
|
||||
pointCurrent.mK = pointAfter.mK = 0;
|
||||
continue;
|
||||
}
|
||||
alphaK = pointCurrent.mK / pointCurrent.deltaK;
|
||||
betaK = pointAfter.mK / pointCurrent.deltaK;
|
||||
squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
|
||||
if (squaredMagnitude <= 9) continue;
|
||||
tauK = 3 / Math.sqrt(squaredMagnitude);
|
||||
pointCurrent.mK = alphaK * tauK * pointCurrent.deltaK;
|
||||
pointAfter.mK = betaK * tauK * pointCurrent.deltaK;
|
||||
}
|
||||
|
||||
// Compute control points
|
||||
var deltaX;
|
||||
for (i = 0; i < pointsLen; ++i) {
|
||||
pointCurrent = pointsWithTangents[i];
|
||||
if (pointCurrent.model.skip) 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
helpers.nextItem = function(collection, index, loop) {
|
||||
if (loop) {
|
||||
return index >= collection.length - 1 ? collection[0] : collection[index + 1];
|
||||
|
||||
@ -425,6 +425,40 @@ describe('Core helper tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should spline curves with monotone cubic interpolation', function() {
|
||||
var dataPoints = [
|
||||
{ x: 0, y: 0, skip: false },
|
||||
{ x: 3, y: 6, skip: false },
|
||||
{ x: 9, y: 6, skip: false },
|
||||
{ x: 12, y: 60, skip: false },
|
||||
{ x: 15, y: 60, skip: false },
|
||||
{ x: 18, y: 120, skip: false },
|
||||
{ x: NaN, y: NaN, skip: true },
|
||||
{ x: 21, y: 180, skip: false },
|
||||
{ x: 24, y: 120, skip: false },
|
||||
{ x: 27, y: 125, skip: false },
|
||||
{ x: 30, y: 105, skip: false },
|
||||
{ x: 33, y: 110, skip: false },
|
||||
{ x: 36, y: 170, skip: false }
|
||||
];
|
||||
helpers.splineCurveMonotone(dataPoints);
|
||||
expect(dataPoints).toEqual([
|
||||
{ x: 0, y: 0, skip: false, controlPointPreviousX: undefined, controlPointPreviousY: undefined, controlPointNextX: 1 , controlPointNextY: 2 },
|
||||
{ x: 3, y: 6, skip: false, controlPointPreviousX: 2 , controlPointPreviousY: 6 , controlPointNextX: 5 , controlPointNextY: 6 },
|
||||
{ x: 9, y: 6, skip: false, controlPointPreviousX: 7 , controlPointPreviousY: 6 , controlPointNextX: 10 , controlPointNextY: 6 },
|
||||
{ x: 12, y: 60, skip: false, controlPointPreviousX: 11 , controlPointPreviousY: 60 , controlPointNextX: 13 , controlPointNextY: 60 },
|
||||
{ x: 15, y: 60, skip: false, controlPointPreviousX: 14 , controlPointPreviousY: 60 , controlPointNextX: 16 , controlPointNextY: 60 },
|
||||
{ x: 18, y: 120, skip: false, controlPointPreviousX: 17 , controlPointPreviousY: 100 , controlPointNextX: undefined, controlPointNextY: undefined },
|
||||
{ x: NaN, y: NaN, skip: true , controlPointPreviousX: undefined, controlPointPreviousY: undefined, controlPointNextX: undefined, controlPointNextY: undefined },
|
||||
{ x: 21, y: 180, skip: false, controlPointPreviousX: undefined, controlPointPreviousY: undefined, controlPointNextX: 22 , controlPointNextY: 160 },
|
||||
{ x: 24, y: 120, skip: false, controlPointPreviousX: 23 , controlPointPreviousY: 120 , controlPointNextX: 25 , controlPointNextY: 120 },
|
||||
{ x: 27, y: 125, skip: false, controlPointPreviousX: 26 , controlPointPreviousY: 125 , controlPointNextX: 28 , controlPointNextY: 125 },
|
||||
{ x: 30, y: 105, skip: false, controlPointPreviousX: 29 , controlPointPreviousY: 105 , controlPointNextX: 31 , controlPointNextY: 105 },
|
||||
{ x: 33, y: 110, skip: false, controlPointPreviousX: 32 , controlPointPreviousY: 105 , controlPointNextX: 34 , controlPointNextY: 115 },
|
||||
{ x: 36, y: 170, skip: false, controlPointPreviousX: 35 , controlPointPreviousY: 150 , controlPointNextX: undefined, controlPointNextY: undefined }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get the next or previous item in an array', function() {
|
||||
var testData = [0, 1, 2];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user