Search code examples
echartsconfidence-interval

How to create a dynamic confidence band in ECharts with varying transparency based on proximity to lower and upper bounds?


I'm working on a data visualization project using ECharts, and I need to create a confidence band that adjusts its transparency based on the proximity to the lower and upper bounds. Specifically, I want the transparency to be higher (less opaque) when the data points are near the lower or upper bounds, and lower (more opaque) when the data points are farther away from the bounds.

The below is a perfect example solution, but it lacks the dynamic transparency:

https://echarts.apache.org/examples/en/editor.html?c=confidence-band

ideally it should be as in below: enter image description here

I think the line gradient can be used to do this as in below code:

option = {
  xAxis: {
    type: 'category'
  },
  yAxis: {
    type: 'value'
  },
  series: [
    {
      data: [820, 932, 901, 934, 1290, 1330, 1320],
      type: 'line'
    },
    {
      color: 'rgba(255, 70, 131, 0)',
      data: [300, 500],
      type: 'line',
      stack: 'area-1'
    },
    {
      color: 'rgba(255, 70, 131, 0)',
      stack: 'area-1',
      data: [900, 1200],
      type: 'line',
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          {
            offset: 0.8667,
            color: 'rgba(255, 70, 131, 0.8667)'
          },
          {
            offset: 0.1333,
            color: 'rgba(255, 70, 131, 0.1333)'
          }
        ])
      }
    }
  ]
};

but the problem is that how to properly put the right offset, as you can see in the below image it's not right as in the bottom it's dark, which is wrong, it has to be dark when near the blue line enter image description here


Solution

  • This is the best i could do. Its not exactly what you wanted because the gradient is taken between the minimal and maximal y value over all x values and not per point.

    myChart.showLoading();
    $.get(ROOT_PATH + '/data/asset/data/confidence-band.json', function (data) {
      myChart.hideLoading();
      var base = -data.reduce(function (min, val) {
        return Math.floor(Math.min(min, val.l));
      }, Infinity);
      myChart.setOption(
        (option = {
          title: {
            text: 'Confidence Band',
            subtext: 'Example in MetricsGraphics.js',
            left: 'center'
          },
          tooltip: {
            trigger: 'axis',
            axisPointer: {
              type: 'cross',
              animation: false,
              label: {
                backgroundColor: '#ccc',
                borderColor: '#aaa',
                borderWidth: 1,
                shadowBlur: 0,
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                color: '#222'
              }
            },
            formatter: function (params) {
              return (
                params[2].name +
                '<br />' +
                ((params[2].value - base) * 100).toFixed(1) +
                '%'
              );
            }
          },
          grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            containLabel: true
          },
          xAxis: {
            type: 'category',
            data: data.map(function (item) {
              return item.date;
            }),
            axisLabel: {
              formatter: function (value, idx) {
                var date = new Date(value);
                return idx === 0
                  ? value
                  : [date.getMonth() + 1, date.getDate()].join('-');
              }
            },
            boundaryGap: false
          },
          yAxis: {
            axisLabel: {
              formatter: function (val) {
                return (val - base) * 100 + '%';
              }
            },
            axisPointer: {
              label: {
                formatter: function (params) {
                  return ((params.value - base) * 100).toFixed(1) + '%';
                }
              }
            },
            splitNumber: 3
          },
          series: [
            
            {
              name: 'L',
              type: 'line',
              data: data.map(function (item) {
                return item.l + base;
              }),
              lineStyle: {opacity: 0},
              symbol: 'none',
              stack: 'bandLowerPart',
            },
            
            {
              type: 'line',
              data: data.map(function (item) {
                return item.value - item.l;
              }),
              lineStyle: {opacity: 0},
              showSymbol: false,
              stack: 'bandLowerPart',
              areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0.35, 0, 0, [
                  {
                    offset: 0.5,
                    color: 'rgba(112,128,144, 0.3)'
                  },
                  {
                    offset: 1,
                    color: 'rgba(112,128,144, 1)'
                  }
                ])
              },
            },
            
            {
              type: 'line',
              data: data.map(function (item) {
                return item.value + base;
              }),
              itemStyle: {color: '#333'},
              showSymbol: false,
              stack: 'bandUpperPart',
            },
            
            {
              name: 'U',
              type: 'line',
              data: data.map(function (item) {
                return item.u - item.value;
              }),
              lineStyle: {opacity: 0},
              symbol: 'none',
              stack: 'bandUpperPart',
              areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
                  {
                    offset: 0.5,
                    color: 'rgba(112,128,144, 0.3)'
                  },
                  {
                    offset: 1,
                    color: 'rgba(112,128,144, 1)'
                  }
                ])
              },
            },
            
          ]
        })
      );
    });
    

    I just built up on your example and put two gradients (one from upper line and one from lower line) instead of one from upper to lower.


    I managed to build a really hacky solution by slicing the data into small pieces and plotting a gradient for each piece. Code is also pretty messy. Do with it what you want:

    Example

    myChart.showLoading();
    $.get(ROOT_PATH + '/data/asset/data/confidence-band.json', function (data) {
      myChart.hideLoading();
      var base = -data.reduce(function (min, val) {
        return Math.floor(Math.min(min, val.l));
      }, Infinity);
      
      data.map((item) => {item.lb = item.l + base});
      data.map((item) => {item.mlb = item.value - item.l});
      data.map((item) => {item.mub = item.value + base});
      data.map((item) => {item.ub = item.u - item.value});
    
      let seriesList = [];
      const filteredDatasets = [];
    
      function createSeries(nrSlices) {
        const sliceSize = Math.ceil(data.length / nrSlices);
        for (let index = 0; index < nrSlices; index++) {
        
          let lowIndex = index*sliceSize-1;
          let highIndex = lowIndex + sliceSize+1;
          if (highIndex >= data.length) {
            highIndex = data.length -  1;
          }
          
          const datasetId = 'dataset_' + index;
          const datasetSliced = data.slice(lowIndex, highIndex);
          filteredDatasets.push({
            id: datasetId,
            source: datasetSliced,
          });
    
          const slice = [
            {
              type: 'line',
              datasetId: datasetId,
              encode: {x: 'date', y: 'lb'},
              lineStyle: { opacity: 0 },
              symbol: 'none',
              stack: 'bandLowerPart' + index
            },
    
            {
              type: 'line',
              datasetId: datasetId,
              encode: {x: 'date', y: 'mlb'},
              lineStyle: { opacity: 0 },
              showSymbol: false,
              stack: 'bandLowerPart' + index,
              areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
                  {
                    offset: 1,
                    color: 'rgba(112,128,144, 1)'
                  },
                  {
                    offset: 0.3,
                    color: 'rgba(112,128,144, 0.3)'
                  }
                ])
              }
            },
    
            {
              type: 'line',
              datasetId: datasetId,
              encode: {x: 'date', y: 'mub'},
              itemStyle: { color: '#333' },
              showSymbol: false,
              stack: 'bandUpperPart' + index
            },
    
            {
              type: 'line',
              datasetId: datasetId,
              encode: {x: 'date', y: 'ub'},
              lineStyle: { opacity: 0 },
              symbol: 'none',
              stack: 'bandUpperPart' + index,
              areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 0.6, [
                  {
                    offset: 0.3,
                    color: 'rgba(112,128,144, 0.3)'
                  },
                  {
                    offset: 1,
                    color: 'rgba(112,128,144, 1)'
                  }
                ])
              }
            }
          ];
          seriesList = seriesList.concat(slice);
        }
      }
    
      createSeries(100);
    
      myChart.setOption(
        (option = {
          title: {
            text: 'Confidence Band',
            subtext: 'Example in MetricsGraphics.js',
            left: 'center'
          },
          dataset: filteredDatasets,
          tooltip: {
            trigger: 'axis',
            axisPointer: {
              type: 'cross',
              animation: false,
              label: {
                backgroundColor: '#ccc',
                borderColor: '#aaa',
                borderWidth: 1,
                shadowBlur: 0,
                shadowOffsetX: 0,
                shadowOffsetY: 0,
                color: '#222'
              }
            },
            formatter: function (params) {
              return (
                params[2].name +
                '<br />' +
                ((params[2].value - base) * 100).toFixed(1) +
                '%'
              );
            }
          },
          grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            containLabel: true
          },
          xAxis: {
            type: 'category',
            data: data.map(function (item) {
              return item.date;
            }),
            axisLabel: {
              formatter: function (value, idx) {
                var date = new Date(value);
                return idx === 0
                  ? value
                  : [date.getMonth() + 1, date.getDate()].join('-');
              }
            },
            boundaryGap: false
          },
          yAxis: {
            axisLabel: {
              formatter: function (val) {
                return (val - base) * 100 + '%';
              }
            },
            axisPointer: {
              label: {
                formatter: function (params) {
                  return ((params.value - base) * 100).toFixed(1) + '%';
                }
              }
            },
            splitNumber: 3
          },
          series: seriesList,
        })
      );
    });