Issue
I'm wondering if ChartJS exposes an API for plugins that allows us to obtain the coordinate of a null
point that has been "Spanned" by ChartJS?
For example as illustrated in this question ChartJS enables smooth curves through null
points when settings spanGaps
to true
.
So we could have data points like this.
data: [7, null, 11, null, 5 , null, 8, null, 3, null, 7],
Corresponding to these labels.
["Red", "Blue", "Yellow", "Green", "Purple", "Orange", "Blue", "Yellow", "Green", "Purple", "Green"],
Is there a way ( Perhaps via the Plugin API ) to get the values of the nulls?
So for a given quadratic curve that chart JS draws for:
data: [7, null, 11, null, 5 , null, 8, null, 3, null, 7],
We could call for example:
const arr = ChartJSPluginAPI.deriveNulls(data);
And get something like:
data: [7, 8, 11, 6, 5 , 6, 8, 4, 3, 5, 7],
Thoughts?
Solution
Finding the interpolated values as they are calculated by chart.js is not trivial, because chart.js performs the interpolation in graphical mode only, so we have to get the graphical values of the existing points for the relevant datasets meta, perform the interpolation, and then get back to the real space, using the y-axis scaling. This is however safer than trying to emulate the interpolation mathematically in real space.
The first thing in finding the intermediate values is to identify the
functions used by chart.js to interpolate all the values of the curves;
there are three functions: _steppedInterpolation
for
stepped
line charts, _bezierInterpolation
if there is a tension
option set,
and the default linear interpolation as _pointInLine
.
const _bezierInterpolation = Chart.helpers._bezierInterpolation,
_steppedInterpolation = Chart.helpers._steppedInterpolation,
_pointInLine = Chart.helpers._pointInLine;
Note that if modules are used, the helpers
module needs to be imported
separately.
An important point is to perform the computation after the animation is
completed, since for instance the bezier coefficients (if bezier interpolation
was used) are constantly recomputed during the animation, and their final
values can only be obtained after the animation is
finalized. So, we implement the computation in the animation's
onComplete
handler:
onComplete: function({chart}){
const datasetMetas = chart.getSortedVisibleDatasetMetas();
for(const datasetMeta of datasetMetas){
if(datasetMeta.type === 'line'){
const controller = datasetMeta.controller,
spanGaps = controller.options.spanGaps,
dataRaw = controller._data;
if(spanGaps && dataRaw.includes(null)){
const gData = datasetMeta.data,
yScale = datasetMeta.yScale,
yValues = []; // the final result
const tension = controller.options.tension || controller.options.elements.line.tension;
interpolation = controller.options.stepped ? _steppedInterpolation :
tension ? _bezierInterpolation : _pointInLine;
for(let i = 0; i < gData.length; i++){
if(dataRaw[i] !== null){
yValues.push(dataRaw[i]);
}
else if(i === 0 || i ===gData.length-1){
yValues.push(null); // no interpolation for extreme points
}
else{
const pLeft = gData[i-1],
pThis = gData[i],
pRight = gData[i+1];
const xgLeft = pLeft.x, xg = pThis.x, xgRight = pRight.x,
frac = (xg - xgLeft) / (xgRight - xgLeft);
let {y: yg} = interpolation(pLeft, pRight, frac);
yValues.push(yScale.getValueForPixel(yg));
}
}
console.log(`For dataset ${controller.index}:`, yValues);
}
}
}
}
This solution assumes the index axis is x
and the value axis is y
;
it should work regardless of the type
of the x
axis (category
,
linear
, time
, etc.).
Here's a full snippet with this solution applied to the OP example.
const _bezierInterpolation = Chart.helpers._bezierInterpolation,
_steppedInterpolation = Chart.helpers._steppedInterpolation,
_pointInLine = Chart.helpers._pointInLine;
// note: different access with modules
const config = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange", "Blue", "Yellow", "Green", "Purple", "Green"],
datasets: [{
label: '# of Votes',
data: [7, null, 11, null, 5 , null, 8, null, 3, null, 7],
spanGaps: true,
fill: true,
borderWidth: 1,
pointHitRadius: 25,
tension: 0.4
}]
},
options: {
animation:{
onComplete: function({chart}){
const datesetMetas = chart.getSortedVisibleDatasetMetas();
for(const datasetMeta of datesetMetas){
if(datasetMeta.type === 'line'){
const controller = datasetMeta.controller,
spanGaps = controller.options.spanGaps,
dataRaw = controller._data;
if(spanGaps && dataRaw.includes(null)){
const gData = datasetMeta.data,
yScale = datasetMeta.yScale,
yValues = []; // the final result
const tension = controller.options.tension || controller.options.elements.line.tension;
interpolation = controller.options.stepped ? _steppedInterpolation :
tension ? _bezierInterpolation : _pointInLine;
for(let i = 0; i < gData.length; i++){
if(dataRaw[i] !== null){
yValues.push(dataRaw[i]);
}
else if(i === 0 || i ===gData.length-1){
yValues.push(null); // no interpolation for extreme points
}
else{
const pLeft = gData[i-1],
pThis = gData[i],
pRight = gData[i+1];
const xgLeft = pLeft.x, xg = pThis.x, xgRight = pRight.x,
frac = (xg - xgLeft) / (xgRight - xgLeft);
let {y: yg} = interpolation(pLeft, pRight, frac);
yValues.push(yScale.getValueForPixel(yg));
}
}
console.log(`For dataset ${controller.index}:`, yValues);
}
}
}
}
},
scales: {
y: {
min: 0,
max: 20
}
}
}
}
const chart = new Chart('chartJSContainer', config);
<div style="min-height: 60vh">
<canvas id="chartJSContainer" style="background-color: #eee;">
</canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Answered By - kikon
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.