skip to Main Content

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?

2

Answers


  1. 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>
    Login or Signup to reply.
  2. Depending on your Chart (and options) one could calculate the points yourself. You just would have to check the chatjs code on github, too match the functions, how the lines are generated and create/find a function that gives you the points.

    Here a crude demo:
    (this demo just works with max one gap and the beizer curve, but with better math skills more is possible)

        console.clear();
    
        function calcBiezierCurvePoint(idx) {
          let metaData = chart.getDatasetMeta(0).data;
    
          if (!metaData[idx].skip) {
            console.info(metaData[idx])
            return metaData[idx]
          }
    
          let prev = null
          let next = null
          let prevIdx = idx - 1;
          let nextIdx = idx + 1;
          while (!prev || !next) {
            if (!metaData[prevIdx].skip) {
              prev = metaData[prevIdx]
            }
    
            if (!metaData[nextIdx].skip) {
              next = metaData[nextIdx]
            }
          }
    
          console.info({ prev, next });
    
          let minY = Math.min(prev.y, next.y)
          let maxY = Math.max(prev.y, next.y)
    
          let minX = Math.min(prev.x, next.x)
          let maxX = Math.max(prev.x, next.x)
    
          let deltaX = maxX - minX;
          let deltaY = maxY - minY;
    
          let _c = calcBiezier({
            x: (prev.cp2x - minX) / deltaX,
            y: (prev.cp2y - minY) / deltaY
          }, {
            x: (next.cp1x - minX) / deltaX,
            y: (next.cp1y - minY) / deltaY
          }, deltaY / deltaX);
    
          console.info({ _c }, deltaY / deltaX)
    
          console.info((_c.x * deltaX) + minX, (_c.y / deltaX * deltaY) + minY)
          return { x: (_c.x * deltaX) / 2 + minX, y: (_c.y * deltaY) / 2 + minY }
    
        }
    
        // P = (1-t)P1 + tP2
        function calcBiezier(p1, p2, t) {
          let x = (1 - t) * p1.x + p2.x
          let y = (1 - t) * p1.y + p2.y
          return { x, y }
        }
    
        var options = {
          type: 'line',
          data: {
            labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange", "Blue", "Yellow", "Green", "Purple", "Green"],
            datasets: [
              {
                label: '# of Points',
                data: [7, null, 11, null, 5, null, 8, null, 3, null, 7],
                fill: true,
                tension: 0.4,
                borderWidth: 1,
                pointHitRadius: 25,
                spanGaps: true
              }
            ]
          },
          options: {
            scales: {
              y: {
                min: 0,
                max: 20
              }
            },
          }
        }
    
        let ctx = document.getElementById('chartJSContainer').getContext('2d');
        let chart = new Chart(ctx, options);
    
    
        setTimeout(() => {
          let points = [
            calcBiezierCurvePoint(0),
            calcBiezierCurvePoint(1),
            calcBiezierCurvePoint(2),
            calcBiezierCurvePoint(3),
            calcBiezierCurvePoint(4),
            calcBiezierCurvePoint(5),
          ];
    
          for (let p of points) {
            console.info({ p })
            ctx.fillRect(p.x, p.y, 10, 10)
            ctx.fillRect(p.cp1x, p.cp1y, 5, 5)
            ctx.fillRect(p.cp2x, p.cp2y, 5, 5)
          }
    
          ctx.beginPath();
          ctx.moveTo(points[0].x, points[0].y)
          ctx.bezierCurveTo(points[0].cp2x, points[0].cp2y, points[2].cp1x, points[2].cp1y,
            points[2].x, points[2].y);
          ctx.stroke();
    
        }, 2000);
    html, body { margin: 0; padding: 0;}
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.6.0/chart.min.js"></script>
    <canvas id="chartJSContainer" style="height: 180px; width: 500px;"></canvas>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search