Search code examples
javascriptlinescatter-plotecharts

Echarts: scatter connected to the axis with a line (similar to the PACF plot)


I am using echarts (js). Is there any way to connect the dot of the scatter plot with the 0 value of the y axis? It should be like a pacf plot (https://www.statsmodels.org/stable/_images/graphics_tsa_plot_pacf.png)

It should be something like this:

enter image description here

In order to allow the reproducibility, this is the chart option:

let data = {
    "schema": {
      "fields": [
        {
          "name": "tamaño",
          "type": "string"
        },
        {
          "name": "expo",
          "type": "number"
        },
        {
          "name": "impo",
          "type": "number"
        },
        {
          "name": "saldo",
          "type": "number"
        }
      ],
      "pandas_version": "1.4.0"
    },
    "data": [
      {
        "tamaño": "Micro",
        "expo": 1988766908.4407976,
        "impo": 4256928697.57,
        "saldo": -2268161789.129203
      },
      {
        "tamaño": "Pequeña",
        "expo": 3681585633.166469,
        "impo": 7548231175.14,
        "saldo": -3866645541.9735312
      },
      {
        "tamaño": "Mediana",
        "expo": 7626112142.6330385,
        "impo": 12756460382.520002,
        "saldo": -5130348239.886964
      },
      {
        "tamaño": "Grande",
        "expo": 64637850302.33969,
        "impo": 38622133149.46996,
        "saldo": 26015717152.869728
      }
    ]
  }

let categories = data.data.map(item => item.tamaño);
let expoData = data.data.map(item => item.expo/ 1_000_000);
let impoData = data.data.map(item => -item.impo/ 1_000_000);
let saldoData = data.data.map(item => item.saldo/ 1_000_000);
let lineData = saldoData.map((d, i) => [[0, d], [0, i]]);
console.log(lineData)
export var optionActividadIntercambio = {
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow',
      },
    //   }
    },
    legend: {
        bottom: 10,
    },
    xAxis: [
      {
        type: 'value',
      },
    ],
    yAxis: [
        {
          type: 'category',
          data: categories,
          axisTick: {
            show: false
          }
        }
      ],
      series: [
        {
          name: 'Exportaciones',
          type: 'bar',
          data: expoData,
          stack: 'Total',
          emphasis: {
            focus: 'series'
          },
          label: {
            show: false,
            position: 'inside'
          },
          itemStyle:{color:"rgba(238,178,9,1)"}
        },
        {
          name: 'Importaciones',
          type: 'bar',
          data: impoData,
          stack: 'Total',
          emphasis: {
            focus: 'series'
          },
          label: {
            show: false
          },
          itemStyle:{color:"rgba(163,133,165,1)"}
        },
        {
          name: 'Saldo',
          type: 'scatter',
          data: saldoData,
        //   stack: 'Total',
          emphasis: {
            focus: 'series'
          },
          label: {
            show: false,
            position: 'left'
          }
        },
        {
            name: 'Saldo Line',
            type: 'line',
            data: lineData,
            showSymbol: true, // don't show symbols for line series
            lineStyle: {
              width: 10 // thinner line
            }
          }
      ]      
  };

Help is much appreciated, since I've passed hours reading the documentation😢.

Best regards


Solution

  • You already defined the line in your chart, but the coordinates are wrong; in a first attempt, the coordinates could be thought as:

    let lineData = saldoData.flatMap((d, i) => [[d, i], [0, i]]);
    

    This creates a multi-segment that goes continuously from the first data to the last. In order to get rid of the unwanted connections between levels, one could simply introduce a null point in between:

    let lineData = saldoData.flatMap((d, i) => [[d, i], [0, i], []]);
    

    which interrupts the line as intended in conjunction with keeping the default conectNulls: false.

    And with tweaking the visual properties of the line, one can have this snippet:

    const dom = document.getElementById('chart-container');
    const myChart = echarts.init(dom, null, {
        renderer: 'canvas',
        useDirtyRect: false
    });
    
    const data = {
        "schema": {
            "fields": [{
                "name": "tamaño",
                "type": "string"
            },
                {
                    "name": "expo",
                    "type": "number"
                },
                {
                    "name": "impo",
                    "type": "number"
                },
                {
                    "name": "saldo",
                    "type": "number"
                }
            ],
            "pandas_version": "1.4.0"
        },
        "data": [{
            "tamaño": "Micro",
            "expo": 1988766908.4407976,
            "impo": 4256928697.57,
            "saldo": -2268161789.129203
        },
            {
                "tamaño": "Pequeña",
                "expo": 3681585633.166469,
                "impo": 7548231175.14,
                "saldo": -3866645541.9735312
            },
            {
                "tamaño": "Mediana",
                "expo": 7626112142.6330385,
                "impo": 12756460382.520002,
                "saldo": -5130348239.886964
            },
            {
                "tamaño": "Grande",
                "expo": 64637850302.33969,
                "impo": 38622133149.46996,
                "saldo": 26015717152.869728
            }
        ]
    }
    
    let categories = data.data.map(item => item.tamaño);
    let expoData = data.data.map(item => item.expo / 1_000_000);
    let impoData = data.data.map(item => -item.impo / 1_000_000);
    let saldoData = data.data.map(item => item.saldo / 1_000_000);
    let lineData = saldoData.flatMap((d, i) => [[d, i], [0, i], []]);
    const optionActividadIntercambio = {
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'shadow',
            },
        },
        legend: {
            bottom: 10,
        },
        xAxis: [{
            type: 'value',
        },],
        yAxis: [{
            type: 'category',
            data: categories,
            axisTick: {
                show: false
            }
        }],
        series: [{
            name: 'Exportaciones',
            type: 'bar',
            data: expoData,
            stack: 'Total',
            emphasis: {
                focus: 'series'
            },
            label: {
                show: false,
                position: 'inside'
            },
            itemStyle: {
                color: "rgba(238,178,9,1)"
            }
        },
            {
                name: 'Importaciones',
                type: 'bar',
                data: impoData,
                stack: 'Total',
                emphasis: {
                    focus: 'series'
                },
                label: {
                    show: false
                },
                itemStyle: {
                    color: "rgba(163,133,165,1)"
                }
            },
            {
                name: 'Saldo',
                type: 'scatter',
                data: saldoData,
                emphasis: {
                    focus: 'series'
                },
                label: {
                    show: false,
                    position: 'left'
                }
            },
            {
                name: 'Saldo Line', // no name to exclude from legend
                type: 'line',
                data: lineData,
                showSymbol: false,
                itemStyle: {
                    opacity: 0 // to remove line ends from legend
                },
                lineStyle: {
                    color: '#000',
                    width: 2
                },
                tooltip: {
                    show: false // remove redundant info from tooltip
                },
                label: {
                    show: false
                }
            }
        ]
    };
    
    myChart.setOption(optionActividadIntercambio);
    window.addEventListener('resize', myChart.resize);
    #chart-container {
      position: relative;
      height: 100vh;
      overflow: hidden;
    }
    <div id="chart-container"></div>
    <script src="https://fastly.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>

    The same in jsFiddle.


    Now, since we have the line series, we can use that to create both the line and the point (so remove the scatter series). The only difficulty would be to show only one of the points at the end of the line; that can be done using the fact that symbol (as well as symbolSize) can be given functions as values, so they can be changed from point to point. For example,

    symbol: value => value[0] ? 'circle' : 'none'
    

    will make a circle only for those points that have a non-zero value. Full code in jsFiddle

    If zero date values are possible, a safer solution would be to add the symbol in the data point, as the third coordinate:

    let lineData = saldoData.flatMap((d, i) => [[d, i, 'circle'], [0, i], []]);
    

    with

    symbol: value => value[2] || 'none',
    

    Full code in jsFiddle.


    Another possibility, with some advantages, would be to make the line as a markLine of the scatter series. The data for the markLine could be constructed as:

    let markLineData = saldoData.map((d, i) => [{coord: [d, i]}, {coord: [0, i]}]);
    

    and with that the markLine option inside the scatter series would be:

     markLine:{
        symbol: 'none',
        lineStyle:{
            color: '#000',
            type: 'solid',
            width: 2
        },
        data: markLineData
    }
    

    Full code in jsFiddle.