2

I want to plot the variance of multiple signals in a chart (or basically fillup the space between an upper and a lower signal). Is it possible to create such kind of charts?

Example of a chart with signal and its variance

I saw the confidence-band example (https://echarts.apache.org/examples/en/editor.html?c=confidence-band) , however this seems to work only for one signal in a chart.

Another solution would be to draw thousands of small rectangles using markArea around the signals but this slows down the performance of the chart (e.g. when scrolling the x-axisis) and doesnt look very smooth.

mkersche17
  • 105
  • 13

2 Answers2

2

As I know the common practice in Echarts community draw usual chart type (bar, line, ...) with series (read docs) and write visual logic by custom series for unique. Also Echarts has some API methods (undocumented) like registerVisual, registerLayout that can be used for redefine layouts, computation and so on.

For described task you need to use custom series for calculate bands coordinates. It's not very simple because (it seems to me) mandatory requirements with confidence band is rare.

About performance. Echarts by default use Canvas for render visual parts. Usually Canvas has no many parts in HTML for display chart, it's just imageData rendered by browser and it almost doesn't matter how many data point need to display. In other words, we see PNG, and not a lot of div, svg, g and others layers with geometric primitives as in SVG but heavy computation complex business logic may affect the responsiveness of UI as in other charts.

Below example how would I implement this feature. I'm not sure that's the right way but it work and can be tuned.

    var dates = ['2020-01-03','2020-01-31','2020-02-17','2020-02-18','2020-03-13','2020-04-10','2020-05-01','2020-05-19','2020-05-22','2020-05-25'];
    var sensor1 = [0.6482086334797242, 0.9121368038482911, 0.3205730196548609, 0.8712238348969002, 0.4487714576177558, 0.9895025457815625, 0.0415490306934774, 0.1592908349676395, 0.5356690594518069, 0.9949108727912939];
    var sensor2 = [0.8278430459565170, 0.5700757488718124, 0.9803575576802187, 0.0770264671179814,0.2843735619252158,0.8140209568127250,0.6055633547296827,0.9554255125528607,0.1703504100638565,0.5653245914197297];

    // Calculate fake bands coordinates
    function calcContourCoords(seriesData, ctx){
      var addNoise = idx => Math.round(Math.random() * 8 * idx);
      var pixelCoords = seriesData.map((dataPoint, idx) => {
        return [
          ctx.convertToPixel({ xAxisIndex: 0 }, idx) + addNoise(idx),
          ctx.convertToPixel({ yAxisIndex: 0 }, dataPoint) + addNoise(idx)
        ]
      });

      var polyfilltype = ClipperLib.PolyFillType.pftEvenOdd;
      var linePath = new ClipperLib.Path();
      var delta = 15;
      var scale = 1;

      for (var i = 0; i < pixelCoords.length; i++){
        var point = new ClipperLib.IntPoint(...pixelCoords[i]);
        linePath.push(point);
      }

      var co = new ClipperLib.ClipperOffset(1.0, 0.25);
          co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etOpenSquare);
          co.Execute(linePath, delta * scale);

      return co.m_destPoly.map(c => [c.X, c.Y])
    }

    // Render visual by calculated coords
    function renderItem(params, api){

      // Prevent multiple call
      if (params.context.rendered) return;
      params.context.rendered = true;

      // Get stored in series data for band
      var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
      var seriesData = series.get('data');

      // Calculate band coordinates for series
      var bandCoords = calcContourCoords(seriesData, myChart);

      // Draw band
      return {
        type: 'polygon',
        shape: {
          points: echarts.graphic.clipPointsByRect(bandCoords, {
            x: params.coordSys.x,
            y: params.coordSys.y,
            width: params.coordSys.width,
            height: params.coordSys.height
          })
        },
        style: api.style({
          fill: series.option.itemStyle.color
        })
      };
    }

    // =============

  var option = {
      tooltip: {},
      legend: {
        data:['Label']
      },
      xAxis: [
        { name: 'x0', data: dates, boundaryGap: true },
        { name: 'x1', data: dates, boundaryGap: true, show: false },
      ],
      yAxis: [
        { name: 'y0' },
        { name: 'y1', show: false },
      ],
      series: [

        // First line
        {
          name: 'Sensor1',
          type: 'line',
          data: sensor1,
          itemStyle: { color: 'rgba(69, 170, 242, 1)' },
          yAxisIndex: 0,
          xAxisIndex: 0,
        },
        {
          name: 'BandSensor1',
          type: 'custom',
          data: sensor1,
          itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
          renderItem: renderItem,
          yAxisIndex: 0,
          xAxisIndex: 0,
        },

        // Second line
        {
          name: 'Sensor2',
          type: 'line',
          data: sensor2,
          itemStyle: { color: 'rgba(253, 151, 68, 1)' },
          yAxisIndex: 1,
          xAxisIndex: 1,
        },
        {
          name: 'BandSensor2',
          type: 'custom',
          data: sensor2,
          itemStyle: { color: 'rgba(253, 151, 68, 0.2)' },
          renderItem: renderItem,
          yAxisIndex: 1,
          xAxisIndex: 1,
        },
      ]
  };

  var myChart = echarts.init(document.getElementById('main'));
      myChart.setOption(option);
<script src="https://cdn.jsdelivr.net/npm/clipper-lib@6.4.2/clipper.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.7.0/echarts.min.js"></script>
<div id="main" style="width: 800px;height:600px;"></div>
Sergey Fedorov
  • 3,696
  • 2
  • 17
  • 21
1

Improved version of @Sergey Fedorov- this solution takes into account min,max values or. dynamic border thicness of the band

enter image description here

var dates = ['2020-01-03', '2020-01-31', '2020-02-17', '2020-02-18', '2020-03-13', '2020-04-10', '2020-05-01', '2020-05-19', '2020-05-22', '2020-05-25', '2020-05-27'];

    const data_raw1 = [
        { min: -5, mean: 0, max: 0 },
        { min: 1, mean: 2, max: 5 },
        { min: 2, mean: 4, max: 6 },
        { min: 4, mean: 5, max: 8 },
        { min: 7, mean: 11, max: 14 },
        { min: 11, mean: 15, max: 17 },
        { min: 6, mean: 8, max: 8.5 },
        { min: -1, mean: 5, max: 6 },
        { min: 4, mean: 9, max: 12 },
        { min: 14, mean: 18, max: 22 },
        { min: 18, mean: 20, max: 21 },
    ];


    const data_raw2 = [
        { min: 10, mean: 15, max: 20 },
        { min: 12, mean: 25, max: 30 },
        { min: 22, mean: 26, max: 32 },
        { min: 30, mean: 31, max: 45 },
        { min: 47, mean: 49, max: 50 },
        { min: 30, mean: 32, max: 41 },
        { min: 34, mean: 36, max: 38 },
        { min: 40, mean: 42, max: 45 },
        { min: 47, mean: 49, max: 56 },
        { min: 60, mean: 68, max: 70 },
        { min: 75, mean: 80, max: 85 },
    ];

    const data_raw3 = data_raw2.map(d => ({ min: d.min * 1.2 + 10, mean: d.mean * 1.4 + 11, max: d.max * 1.5 + 12 }))

    function calcContourCoords(seriesData, ctx) {
        console.log("seriesData=", seriesData);

        const pixelCoords = []

        for (let i = 0; i < seriesData.length; i++) {
            console.log(i, seriesData[i]);

            pixelCoords.push([
                ctx.convertToPixel({ xAxisIndex: 0 }, i),
                ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
            ]);
        }

        console.log("\n")

        for (let i = seriesData.length - 1; i >= 0; i--) {
            console.log(i, seriesData[i]);

            pixelCoords.push([
                ctx.convertToPixel({ xAxisIndex: 0 }, i),
                ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].min)
            ]);

            if (i == 0) {
                pixelCoords.push([
                    ctx.convertToPixel({ xAxisIndex: 0 }, i),
                    ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
                ]);
            }

        }

        var linePath = new ClipperLib.Path();

        var delta = 10;
        var scale = 1;

        for (var i = 0; i < pixelCoords.length; i++) {
            var point = new ClipperLib.IntPoint(...pixelCoords[i]);
            linePath.push(point);
        }

        var co = new ClipperLib.ClipperOffset(1.0, 0.25);
        co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon);
        co.Execute(linePath, delta * scale);

        return co.m_destPoly.map(c => [c.X, c.Y])
    }

    // Render visual by calculated coords
    function renderItem(params, api) {

        // Prevent multiple call
        if (params.context.rendered) return;
        params.context.rendered = true;

        // Get stored in series data for band
        var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
        var seriesData = series.get('data');

        // Calculate band coordinates for series
        var bandCoords = calcContourCoords(seriesData, myChart);

        // Draw band
        return {
            type: 'polygon',
            shape: {
                points: echarts.graphic.clipPointsByRect(bandCoords, {
                    x: params.coordSys.x,
                    y: params.coordSys.y,
                    width: params.coordSys.width,
                    height: params.coordSys.height
                })
            },
            style: api.style({
                fill: series.option.itemStyle.color
            })
        };
    }

    // =============

    var option = {
        tooltip: {},
        legend: {
            data: ['Label']
        },
        xAxis: [
            { name: 'x0', data: dates, boundaryGap: true },
            { name: 'x1', data: dates, boundaryGap: true, show: false },
        ],
        yAxis: [
            { name: 'y0' },
            { name: 'y1', show: false },
        ],
        series: [

            // First line
            {
                name: 'Sensor1',
                type: 'line',
                data: data_raw1.map(d => d.mean),
                itemStyle: { color: 'rgba(69, 170, 242, 1)' },
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'red' },
            },
            {
                name: 'BandSensor1',
                type: 'custom',
                data: data_raw1,
                itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
                renderItem: renderItem,
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'red', opacity: 0.1 },
            },
            {
                name: 'Sensor2',
                type: 'line',
                data: data_raw2.map(d => d.mean),
                itemStyle: { color: 'rgba(69, 170, 242, 1)' },
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'blue' },
            },
            {
                name: 'BandSensor2',
                type: 'custom',
                data: data_raw2,
                itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
                renderItem: renderItem,
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'blue', opacity: 0.1 },
            },
            {
                name: 'Sensor3',
                type: 'line',
                data: data_raw3.map(d => d.mean),
                itemStyle: { color: 'rgba(69, 170, 242, 1)' },
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'green' },
            },
            {
                name: 'BandSensor3',
                type: 'custom',
                data: data_raw3,
                itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
                renderItem: renderItem,
                yAxisIndex: 0,
                xAxisIndex: 0,
                itemStyle: { color: 'green', opacity: 0.1 },
            },
        ]
    };

    var myChart = echarts.init(document.getElementById('chart'));
    myChart.setOption(option);
Night bird
  • 62
  • 7
  • WARNING: This solution does not return proper confidence bands if the bands are too narrow. There will always be a minimal area around the main line. – Sim Jun 04 '22 at 21:56