Echarts:绘制信号的方差

Echarts: Plot the variance of signals

我想在图表中绘制多个信号的方差(或者基本上填满上下信号之间的 space)。 是否可以创建此类图表?

我看到了置信带示例 (https://echarts.apache.org/examples/en/editor.html?c=confidence-band),但这似乎只对图表中的一个信号有效。

另一个解决方案是在信号周围使用 markArea 绘制数千个小矩形,但这会降低图表的性能(例如,滚动 x 轴时)并且看起来不是很平滑。

据我所知,Echarts社区的常见做法是用seriesread docs)绘制常用图表类型(柱状图,折线图,...),并用custom series编写可视化逻辑为独一无二。 Echarts 还有一些 API 方法(未记录),如 registerVisualregisterLayout 可用于重新定义布局、计算等。

对于所描述的任务,您需要使用 custom 系列来计算波段坐标。这不是很简单,因为(在我看来)具有置信区间的强制性要求很少见。

关于性能。 Echarts 默认使用 Canvas 渲染可视化部分。通常 Canvas 在 HTML 中没有太多用于显示图表的部分,它只是浏览器渲染的图像数据,几乎不需要显示多少数据点。换句话说,我们看到的是 PNG,而不是很多 divsvgg 和其他像 SVG 中那样具有几何基元的层,但是繁重的计算复杂的业务逻辑可能会影响响应能力UI 与其他图表一样。

下面的示例我将如何实现此功能。我不确定这是正确的方法,但它可以工作并且可以调整。

    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 的改进版本-此解决方案考虑了最小值、最大值或。带的动态边界厚度

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);