diff --git a/docs/03-Line-Chart.md b/docs/03-Line-Chart.md
index 8a77ae0833b..f62a20879df 100644
--- a/docs/03-Line-Chart.md
+++ b/docs/03-Line-Chart.md
@@ -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 left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used.
+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.
diff --git a/samples/line-cubicInterpolationMode.html b/samples/line-cubicInterpolationMode.html
new file mode 100644
index 00000000000..97dac46ca43
--- /dev/null
+++ b/samples/line-cubicInterpolationMode.html
@@ -0,0 +1,113 @@
+
+
+
+
+ Line Chart - Cubic interpolation mode
+
+
+
+
+
+
+
+
+
+
+
+ Randomize Data
+
+
+
+
\ No newline at end of file
diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js
index 54bbb2308fc..567083e32e8 100644
--- a/src/controllers/controller.line.js
+++ b/src/controllers/controller.line.js
@@ -90,6 +90,7 @@ module.exports = function(Chart) {
borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle),
fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill),
steppedLine: custom.steppedLine ? custom.steppedLine : helpers.getValueOrDefault(dataset.steppedLine, lineElementOptions.stepped),
+ cubicInterpolationMode: custom.cubicInterpolationMode ? custom.cubicInterpolationMode : helpers.getValueOrDefault(dataset.cubicInterpolationMode, lineElementOptions.cubicInterpolationMode),
// Scale
scaleTop: scale.top,
scaleBottom: scale.bottom,
@@ -248,30 +249,45 @@ module.exports = function(Chart) {
var meta = me.getMeta();
var area = me.chart.chartArea;
- // only consider points that are drawn in case the spanGaps option is ued
- var points = (meta.data || []).filter(function(pt) { return !pt._model.skip; });
+ // Only consider points that are drawn in case the spanGaps option is used
+ var points = (meta.data || []);
+ if (meta.dataset._model.spanGaps) points = points.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 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 (this.sign(pointBefore.deltaK) != this.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.model.skip || pointAfter.model.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];
diff --git a/test/core.helpers.tests.js b/test/core.helpers.tests.js
index ccc59c14ace..b8278171e8f 100644
--- a/test/core.helpers.tests.js
+++ b/test/core.helpers.tests.js
@@ -425,6 +425,156 @@ describe('Core helper tests', function() {
});
});
+ it('should spline curves with monotone cubic interpolation', function() {
+ var dataPoints = [
+ { _model: { x: 0, y: 0, skip: false } },
+ { _model: { x: 3, y: 6, skip: false } },
+ { _model: { x: 9, y: 6, skip: false } },
+ { _model: { x: 12, y: 60, skip: false } },
+ { _model: { x: 15, y: 60, skip: false } },
+ { _model: { x: 18, y: 120, skip: false } },
+ { _model: { x: null, y: null, skip: true } },
+ { _model: { x: 21, y: 180, skip: false } },
+ { _model: { x: 24, y: 120, skip: false } },
+ { _model: { x: 27, y: 125, skip: false } },
+ { _model: { x: 30, y: 105, skip: false } },
+ { _model: { x: 33, y: 110, skip: false } },
+ { _model: { x: 36, y: 170, skip: false } }
+ ];
+ helpers.splineCurveMonotone(dataPoints);
+ expect(dataPoints).toEqual([{
+ _model: {
+ x: 0,
+ y: 0,
+ skip: false,
+ controlPointNextX: 1,
+ controlPointNextY: 2
+ }
+ },
+ {
+ _model: {
+ x: 3,
+ y: 6,
+ skip: false,
+ controlPointPreviousX: 2,
+ controlPointPreviousY: 6,
+ controlPointNextX: 5,
+ controlPointNextY: 6
+ }
+ },
+ {
+ _model: {
+ x: 9,
+ y: 6,
+ skip: false,
+ controlPointPreviousX: 7,
+ controlPointPreviousY: 6,
+ controlPointNextX: 10,
+ controlPointNextY: 6
+ }
+ },
+ {
+ _model: {
+ x: 12,
+ y: 60,
+ skip: false,
+ controlPointPreviousX: 11,
+ controlPointPreviousY: 60,
+ controlPointNextX: 13,
+ controlPointNextY: 60
+ }
+ },
+ {
+ _model: {
+ x: 15,
+ y: 60,
+ skip: false,
+ controlPointPreviousX: 14,
+ controlPointPreviousY: 60,
+ controlPointNextX: 16,
+ controlPointNextY: 60
+ }
+ },
+ {
+ _model: {
+ x: 18,
+ y: 120,
+ skip: false,
+ controlPointPreviousX: 17,
+ controlPointPreviousY: 100
+ }
+ },
+ {
+ _model: {
+ x: null,
+ y: null,
+ skip: true
+ }
+ },
+ {
+ _model: {
+ x: 21,
+ y: 180,
+ skip: false,
+ controlPointNextX: 22,
+ controlPointNextY: 160
+ }
+ },
+ {
+ _model: {
+ x: 24,
+ y: 120,
+ skip: false,
+ controlPointPreviousX: 23,
+ controlPointPreviousY: 120,
+ controlPointNextX: 25,
+ controlPointNextY: 120
+ }
+ },
+ {
+ _model: {
+ x: 27,
+ y: 125,
+ skip: false,
+ controlPointPreviousX: 26,
+ controlPointPreviousY: 125,
+ controlPointNextX: 28,
+ controlPointNextY: 125
+ }
+ },
+ {
+ _model: {
+ x: 30,
+ y: 105,
+ skip: false,
+ controlPointPreviousX: 29,
+ controlPointPreviousY: 105,
+ controlPointNextX: 31,
+ controlPointNextY: 105
+ }
+ },
+ {
+ _model: {
+ x: 33,
+ y: 110,
+ skip: false,
+ controlPointPreviousX: 32,
+ controlPointPreviousY: 105,
+ controlPointNextX: 34,
+ controlPointNextY: 115
+ }
+ },
+ {
+ _model: {
+ x: 36,
+ y: 170,
+ skip: false,
+ controlPointPreviousX: 35,
+ controlPointPreviousY: 150
+ }
+ }]);
+ });
+
it('should get the next or previous item in an array', function() {
var testData = [0, 1, 2];