-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Monotone cubic interpolation #3112
Changes from 5 commits
2409908
d06fbc7
4aabc0c
ef66bf5
566ede1
3869dd1
7635ab4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
<!doctype html> | ||
<html> | ||
|
||
<head> | ||
<title>Line Chart - Cubic interpolation mode</title> | ||
<script src="../dist/Chart.bundle.js"></script> | ||
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | ||
<style> | ||
canvas{ | ||
-moz-user-select: none; | ||
-webkit-user-select: none; | ||
-ms-user-select: none; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<div style="width:75%;"> | ||
<canvas id="canvas"></canvas> | ||
</div> | ||
<br> | ||
<br> | ||
<button id="randomizeData">Randomize Data</button> | ||
<script> | ||
|
||
var randomScalingFactor = function() { | ||
return Math.round(Math.random() * 100); | ||
//return 0; | ||
}; | ||
var randomColorFactor = function() { | ||
return Math.round(Math.random() * 255); | ||
}; | ||
var randomColor = function(opacity) { | ||
return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')'; | ||
}; | ||
|
||
var datapoints = [0, 20, 20, 60, 60, 120, NaN, 180, 120, 125, 105, 110, 170]; | ||
var config = { | ||
type: 'line', | ||
data: { | ||
labels: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], | ||
datasets: [{ | ||
label: "Cubic interpolation (monotone)", | ||
data: datapoints, | ||
borderColor: 'rgba(255, 0, 0, 0.7)', | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
fill: false, | ||
cubicInterpolationMode: 'monotone' | ||
}, { | ||
label: "Cubic interpolation (default)", | ||
data: datapoints, | ||
borderColor: 'rgba(0, 0, 255, 0.3)', | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
fill: false, | ||
}, { | ||
label: "Linear interpolation", | ||
data: datapoints, | ||
borderColor: 'rgba(0, 0, 0, 0.10)', | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
fill: false, | ||
lineTension: 0 | ||
}] | ||
}, | ||
options: { | ||
responsive: true, | ||
title:{ | ||
display:true, | ||
text:'Chart.js Line Chart - Cubic interpolation mode' | ||
}, | ||
tooltips: { | ||
mode: 'label' | ||
}, | ||
hover: { | ||
mode: 'dataset' | ||
}, | ||
scales: { | ||
xAxes: [{ | ||
display: true, | ||
scaleLabel: { | ||
display: true | ||
} | ||
}], | ||
yAxes: [{ | ||
display: true, | ||
scaleLabel: { | ||
display: true, | ||
labelString: 'Value' | ||
}, | ||
ticks: { | ||
suggestedMin: -10, | ||
suggestedMax: 200, | ||
} | ||
}] | ||
} | ||
} | ||
}; | ||
|
||
window.onload = function() { | ||
var ctx = document.getElementById("canvas").getContext("2d"); | ||
window.myLine = new Chart(ctx, config); | ||
}; | ||
|
||
$('#randomizeData').click(function() { | ||
for (var i = 0, len = datapoints.length; i < len; ++i) { | ||
datapoints[i] = Math.random() < 0.05 ? NaN : randomScalingFactor(); | ||
} | ||
window.myLine.update(); | ||
}); | ||
|
||
</script> | ||
</body> | ||
|
||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I somehow missed the compatibility table for Math.sign despite having checked Math.EPSILON ... I'll fix that. |
||
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]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it right to remove this setting? It's helpful when the graph is zoomed and we want the control points to be allowed to go outside of the drawing area.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not removed, if I've done that right. It is moved after the calls to
splineCurve
orsplineCurveMonotone
.I also think it should be faster this way, because very few JavaScript compilers are able to remove or hoist this
needToCap
check (thus ASM jump) in thecapIfNecessary
function (called in the loop). It is also a lot more difficult to avoid/not execute/not compile the loop and call.capControlPoint
should be quite easily inlined by most recent compilers, and the loop can be completely ignored (and even not compiled at all for very recent compilers that do branch pruning). Not that it matters much for loops on < 100 items ^^,There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I missed that the
capControlPoint
was behind the setting.You are right that it will probably be a bit faster :)