[Chartjs]-Chartjs stacked line with spanGaps not spanning correctly

1👍

First of all, I find that the first chart works as
expected, the values where the second line is, are
exactly those they should be: [35, 29, 33,----,40,40,39,37].
Maybe it’s easier to see if you add beginAtZero: true
to your y axis (see example below).

As for the not-working version, this seems to be the default
behavior, I’ve seen it in other libraries, (e.g., echarts,
see this SO post).
If I think more about it, I come to the conclusion
that there is a reason for it, and I wouldn’t call it
a bug anymore:

  • with spanGaps all we do is ignore a non-numeric value, drawing a segment over a gap,
    as it were missing.

  • what we are expecting is that non-numeric value to
    be replaced by a new value that may be computed through
    interpolation. That’s more than spanGaps is supposed to
    do.

Still, the interpolation behavior is possible (and
not extremely complicated) to implement in the chartjs code.

It can also be implemented to a certain extent in userland.
Here’s an extension to the LineController
that I called LineControllerWithGapInterpolation, with
the id line_with_gap_interpolation (to be used as the
type of the chart or dataset).

It has some limitations: it only works with lines (no
spline interpolation, tension option is ignored), and it
needs that all the x values are defined (so for that I
added the 11th day to your example).

//import {Chart, registerables, Legend, LineController} from "./chart.js";
//Chart.register(...registerables);

// for umd script: (remove if import above is used)
const LineController = Chart.LineController;

class LineControllerWithGapInterpolation extends LineController{
    static id = 'line_with_gap_interpolation';

    initialize(){
        super.initialize();
        this.options.tension = 0;
    }

    parse(start, count){
        const { _cachedMeta: meta } = this;
        const { _stacked  } = meta;
        super.parse(start, count);
        if(_stacked) {
            const parsed = meta._parsed;
            let lastX = null, lastY = null, allOK = true, changed = false;
            const parsedWithInterp = parsed.map(
                function(o, i){
                    if(!allOK){
                        return o;
                    }
                    const o2 = {x:o.x, y: o.y};
                    if(Number.isFinite(o.y)){
                        lastX = o.x;
                        lastY = o.y;
                        if(!Number.isFinite(lastX)){
                            allOK = false;
                        }
                    }
                    else{
                        let nextX = null, nextY = null;
                        for(let j = i + 1; j < parsed.length; j++){
                            if(Number.isFinite(parsed[j].y)){
                                nextX = parsed[j].x;
                                nextY = parsed[j].y;
                                if(!Number.isFinite(nextX)){
                                    allOK = false;
                                }
                            }
                        }
                        //interpolation
                        o2.y = (nextY - lastY) / (nextX - lastX) * (o.x - lastX) + lastY;
                        changed = true;
                    }
                    return o2;
                }
            );
            if(allOK){
                if(changed){
                    const save_parsed = parsed.map(o => o);
                    this._data = parsedWithInterp.map(({y}) => y);
                    super.parse(start, count);
                    this._cachedMeta._parsed.forEach((o, i) => {o.y = save_parsed[i].y});
                }
            }
            else{
                console.warn('x data missing, could not interpolate'+
                    (((meta.index || meta.index === 0) && ' dataset '+meta.index) ?? ''))
            }
        }
    }
}

Chart.register(LineControllerWithGapInterpolation);

var config_working = {
    type: 'line',

    data: {
        labels: ['01 May 2023', '02 May 2023', '03 May 2023', '04 May 2023', '05 May 2023', '06 May 2023', '07 May 2023', '08 May 2023', '09 May 2023', '10 May 2023', '11 May 2023'],

        datasets: [{
            label: 'Full History',
            borderColor: '#3182BD',
            backgroundColor: '#3182BD',
            data: [20, 20.5, 21, 22, 25, 26, 24, 28, 30, 25, 22],
            // Count is 11
            fill: true
        // },{
        //     label: 'Another Full History',
        //     borderColor: '#BD8231',
        //     backgroundColor: '#BD8231',
        //     data: [20, 12, 21, 22, 25, 25.5, 24, 28, 30, 26, 22],
        //     // Count is 11
        //     fill: true
        },{
            label: 'Gappy data',
            borderColor: '#2c6145',
            backgroundColor: '#2c6145',
            data: [15, 14, 12, undefined, undefined, undefined, undefined, 12, 10, 14, 15],
            //should be: [35, 29, 33,----,40,40,39,37]
            // Count is 11
            fill: true,
        }]
    },

    options: {
        type: 'line',
        spanGaps: true,
        responsive: true,
        maintainAspectRatio: false,
        pointRadius: 0,
        scales: {
            y: {
                stacked: true,
                beginAtZero: true
            }
        }
    }
};

var config_problem = {
    type: 'line_with_gap_interpolation',

    data: {
        labels: ['01 May 2023', '02 May 2023', '03 May 2023', '04 May 2023', '05 May 2023', '06 May 2023', '07 May 2023', '08 May 2023', '09 May 2023', '10 May 2023', '11 May 2023'],

        datasets: [{
            label: 'With one gap',
            borderColor: '#3182BD',
            backgroundColor: '#3182BD',
            data: [20, undefined, 21, 22, 25, 26, 24, 28, 30, 25, 22],
            fill: true
        // },{
        //     label: 'With two gaps',
        //     borderColor: '#BD8231',
        //     backgroundColor: '#BD8231',
        //     data: [20, 12, 21, 22, 25, undefined, 24, 28, 30, undefined, 22],
        //     fill: true
        },{
            label: 'Gappy data',
            borderColor: '#2c6145',
            backgroundColor: '#2c6145',
            data: [15, 14, 12, undefined, undefined, undefined, undefined, 12, 10, 14, 15],
            fill: true
        }]
    },

    options: {
        type: 'line',
        spanGaps: true,
        responsive: true,
        maintainAspectRatio: false,
        pointRadius: 0,
        scales: {
            y: {
                stacked: true,
                beginAtZero: true
            }
        }
    }
};

window.onload = function() {
     var ctx = document.getElementById('cvs-working').getContext('2d');
     window.myLine = new Chart(ctx, config_working);

    var ctx = document.getElementById('cvs-problem').getContext('2d');
    window.myLine = new Chart(ctx, config_problem);
};
<div style="width:46vw; height: 300px; display: inline-block">
    <canvas id="cvs-working"></canvas>
</div>
<div style="width:46vw; height: 300px; display: inline-block">
    <canvas id="cvs-problem"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.0/chart.umd.js" integrity="sha512-CMF3tQtjOoOJoOKlsS7/2loJlkyctwzSoDK/S40iAB+MqWSaf50uObGQSk5Ny/gfRhRCjNLvoxuCvdnERU4WGg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Leave a comment