Search code examples
javascriptgraphchartsgoogle-visualizationecharts

ECharts Confidence Band (Line) chart defaulting to 0 with null values


I am converting Google Chart confidence band (line) to Apache Echarts and I have a major issue when using areaStyle on confidence-band stacking in the line charts with null values.

The issue arises only when having null values in the min/max lines. The connectNulls seems to not be affecting the areaStyle, where the actual line is drawn. Also, the option for skipping xAxis values where the yAxis values are nulls is not an option since in some cases, multiple confidence bands sets will be show (as seen in the second screenshot).

Here is a screenshot of the issue

And here is a screenshot of how it should look like

Here is a simplified data and options:

var dataPoints = [
    {
        "date": "1988",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "1993",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "1994",
        "value": -25.06951746031746,
        "max": -4.489982345190423,
        "min": -45.6490525754445
    },
    {
        "date": "1995",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "1996",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "1997",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "1998",
        "value": -22.481579457241587,
        "max": -13.261933619688394,
        "min": -31.70122529479478
    },
    {
        "date": "1999",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2001",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2002",
        "value": -15.762091749175111,
        "max": -7.987435161930629,
        "min": -23.536748336419592
    },
    {
        "date": "2004",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2005",
        "value": -15.991292690156723,
        "max": -10.553503467629257,
        "min": -21.42908191268419
    },
    {
        "date": "2006",
        "value": -15.571244626678256,
        "max": -10.249573325928019,
        "min": -20.892915927428493
    },
    {
        "date": "2008",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2009",
        "value": -15.220561608383338,
        "max": -10.272665580901526,
        "min": -20.168457635865153
    },
    {
        "date": "2010",
        "value": -14.663968598988458,
        "max": -9.842327372229358,
        "min": -19.48560982574756
    },
    {
        "date": "2011",
        "value": -12.819286135182983,
        "max": -7.97250885239146,
        "min": -17.666063417974506
    },
    {
        "date": "2012",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2014",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2015",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2016",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2017",
        "value": -13.945108519331527,
        "max": -9.216373427532616,
        "min": -18.673843611130437
    },
    {
        "date": "2018",
        "value": null,
        "max": null,
        "min": null
    },
    {
        "date": "2019",
        "value": -13.384577319672625,
        "max": -9.017162823138271,
        "min": -17.751991816206978
    }
];

option =
    {
        grid: {
            containLabel: true,
        },
        tooltip: {
            trigger: 'axis',
            backgroundColor: '#38393c',
            textStyle: {
                color: "#FFF",
            },
        },
        legend: {
            show: true,
            top: 'top',
        },
        xAxis: {
            type: 'category',
            boundaryGap: false,
            data: dataPoints.map(function (item) {
                return item.date;
            }),
        },
        yAxis: {
        },
        series: [
        {   //Max Line
            name: 'Max',
            type: 'line',
            data: dataPoints.map(function (item) {
                return item.max;
            }),
            lineStyle: {
                opacity: 1,
            },
            areaStyle: {
                color: 'transparent',
                opacity: 1,
            },
            stack: 'confidence-band',
            stackStrategy: 'all',
            symbol: 'none',
            connectNulls: true,
        },
        {   //Min Line
            name: 'Min',
            type: 'line',
            data: dataPoints.map(function (item) {
                return item.min - item.max;
            }),
            lineStyle: {
                opacity: 1,
            },
            areaStyle: {
                color: 'red',
                opacity: 0.5,
            },
            stack: 'confidence-band',
            stackStrategy: 'all',
            symbol: 'none',
            connectNulls: true,
        },
        {   //Value Line
            name: 'Mean Values',
            type: 'line',
            data: dataPoints.map(function (item) {
                return item.value;
            }),
            itemStyle: {
                color: 'teal',
            },
            showSymbol: false,
            connectNulls: true,
        },
    ],
    };

Here is a link to the echarts with reproducible example

Thanks!


Solution

  • I think this possibly qualifies as a bug in echarts - it fails to work with nulls in stacked data. It boils down to this if statement that results in a replacement of null to zero in polygons, shapes that get filled if areaStyle is enabled. Still, given the complexity of the code, I don't know whether that replacement was not necessary for some other type of plot.

    In the code below I made a patch that filters out NaN values from polygons produced by LineView.prototype._newPolygon; I prepended it to your code. It appears to do the job, but this is a very hacky solution. I would recommend considering taking out the null values, or even interpolating for the missing data.

    const LineView = echarts.ChartView.getClass('line');
    const _newPolygon = LineView.prototype._newPolygon;
    LineView.prototype._newPolygon = function(points, stackedOnPoints){
        const polygon = _newPolygon.call(this, points, stackedOnPoints);
        if(points && stackedOnPoints){
            const setShape = polygon.setShape;
            polygon.setShape = function(keyOrObj, value){
                const _poly = setShape.call(polygon, keyOrObj, value);
                const points = polygon.shape.points,
                    stackedOnPoints = polygon.shape.stackedOnPoints,
                    points2 = [], stackedOnPoints2 = [];
                let foundNaN = false;
                for(let i = 0; i < points.length/2; i++){
                    if(!isNaN(points[2*i+1]) && !isNaN(stackedOnPoints[2*i+1])){
                        points2.push(points[2*i], points[2*i+1]);
                        stackedOnPoints2.push(stackedOnPoints[2*i], stackedOnPoints[2*i+1]);
                    }
                    else{
                        foundNaN = true;
                    }
                }
                if(foundNaN){
                    polygon.shape.points = new Float32Array(points2);
                    polygon.shape.stackedOnPoints = new Float32Array(stackedOnPoints2);
                }
    
                return _poly;
            }
        }
        return polygon;
    };
    
    var dom = document.getElementById('chart-container');
    var myChart = echarts.init(dom, null, {
        renderer: 'canvas',
        useDirtyRect: false
    });
    var app = {};
    
    var option;
    
    var dataPoints = [
        {
            "date": "1988",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "1993",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "1994",
            "value": -25.06951746031746,
            "max": -4.489982345190423,
            "min": -45.6490525754445
        },
        {
            "date": "1995",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "1996",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "1997",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "1998",
            "value": -22.481579457241587,
            "max": -13.261933619688394,
            "min": -31.70122529479478
        },
        {
            "date": "1999",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2001",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2002",
            "value": -15.762091749175111,
            "max": -7.987435161930629,
            "min": -23.536748336419592
        },
        {
            "date": "2004",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2005",
            "value": -15.991292690156723,
            "max": -10.553503467629257,
            "min": -21.42908191268419
        },
        {
            "date": "2006",
            "value": -15.571244626678256,
            "max": -10.249573325928019,
            "min": -20.892915927428493
        },
        {
            "date": "2008",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2009",
            "value": -15.220561608383338,
            "max": -10.272665580901526,
            "min": -20.168457635865153
        },
        {
            "date": "2010",
            "value": -14.663968598988458,
            "max": -9.842327372229358,
            "min": -19.48560982574756
        },
        {
            "date": "2011",
            "value": -12.819286135182983,
            "max": -7.97250885239146,
            "min": -17.666063417974506
        },
        {
            "date": "2012",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2014",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2015",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2016",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2017",
            "value": -13.945108519331527,
            "max": -9.216373427532616,
            "min": -18.673843611130437
        },
        {
            "date": "2018",
            "value": null,
            "max": null,
            "min": null
        },
        {
            "date": "2019",
            "value": -13.384577319672625,
            "max": -9.017162823138271,
            "min": -17.751991816206978
        }
    ];
    
    option =
        {
            grid: {
                containLabel: true,
            },
            tooltip: {
                trigger: 'axis',
                backgroundColor: '#38393c',
                textStyle: {
                    color: "#FFF",
                },
            },
            legend: {
                show: true,
                top: 'top',
            },
            xAxis: {
                type: 'category',
                boundaryGap: false,
                data: dataPoints.map(function (item) {
                    return item.date;
                }),
            },
            yAxis: {
            },
            series: [
                {   //Max Line
                    name: 'Max',
                    type: 'line',
                    data: dataPoints.map(function (item) {
                        return item.max;
                    }),
                    lineStyle: {
                        opacity: 1,
                    },
                    areaStyle: {
                        color: 'transparent',
                        opacity: 1,
                    },
                    stack: 'confidence-band',
                    stackStrategy: 'all',
                    symbol: 'none',
                    connectNulls: true,
                },
                {   //Min Line
                    name: 'Min',
                    type: 'line',
                    data: dataPoints.map(function (item) {
                        return item.min - item.max;
                    }),
                    lineStyle: {
                        opacity: 1,
                    },
                    areaStyle: {
                        color: 'red',
                        opacity: 0.5,
                    },
                    stack: 'confidence-band',
                    stackStrategy: 'all',
                    symbol: 'none',
                    connectNulls: true,
                },
                {   //Value Line
                    name: 'Mean Values',
                    type: 'line',
                    data: dataPoints.map(function (item) {
                        return item.value;
                    }),
                    itemStyle: {
                        color: 'teal',
                    },
                    showSymbol: false,
                    connectNulls: true,
                },
            ],
        };
    
    if (option && typeof option === 'object') {
        myChart.setOption(option);
    }
    
    window.addEventListener('resize', myChart.resize);
    #chart-container {
        position: relative;
        height: 100vh;
        overflow: hidden;
    }
    <script src="https://fastly.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
    
    <div id="chart-container"></div>