Search code examples
javascriptgrafanaecharts

Non-standard X and Y axes scale in Grafana Apache echarts


I'm trying to replicate this (scatter) chart with non-standard x and y axes using Apache Echarts inside Grafana: Chart to replicate

However, looking through the documentation and examples I can't find a way to have custom scale on axis. Is there any easy way to do it? Or the only option is have to have is as SVG map and translate coordinates depending on the region? Are there are any other Grafana tools that would make it much easier?

I've tried using the logarithmic scale too but I couldn't make it I couldn't make it look like the source material.


Solution

  • The simplest solution in echarts to get what you want is to transform numeric axes to categories, which means basically to transform all data to strings.

    const xStepValues = [0, 1, 2, 10, 100],
        yStepValues = [0, 1, 10, 25, 50];
    const dataSets = [
         [
              [0, 25],
              [1, 10],
              [1, 0]
         ],
         [
              [0, 50],
              [1, 50],
              [2, 25],
              [2, 0]
         ],
         [
              [0, 50],
              [2, 50],
              [10, 25],
              [10, 0]
         ],
         [
              [0, 50],
              [100, 50]
         ]
    ].map(dataSet=>dataSet.map(([x, y])=>[''+x, y+'%']));
    
    const option = {
         legend: {
              show: false,
         },
         tooltip:{
              show: false
         },
         xAxis: {
              //type: 'category',
              data: xStepValues.map(x=>''+x), 
              boundaryGap: false,
              z: 50,
              splitLine:{
                   show: true, z:100,
                   lineStyle:{color:'#888', width:2}
              }
         },
         yAxis: {
              //type: 'category',
              data:yStepValues.map(y=>y+'%'), 
              boundaryGap: false,
              z: 50,
              splitLine:{
                   show: true,
                   lineStyle:{color:'#888', width:2}
              }
         },
         series: [
              {
                   type: 'line',
                   data: dataSets[0],
                   lineStyle: {
                        opacity: 0,
                   },
                   symbolSize: 0,
                   areaStyle: {
                        color: '#284',
                        opacity: 1,
                   },
                   z: 40
              },
              {
                   type: 'line',
                   data: dataSets[1],
                   lineStyle: {
                        opacity: 0,
                   },
                   symbolSize: 0,
                   areaStyle: {
                        color: '#4d8',
                        opacity: 1,
                   },
                   z: 30
              },
              {
                   type: 'line',
                   data: dataSets[2],
                   lineStyle: {
                        opacity: 0,
                   },
                   symbolSize: 0,
                   areaStyle: {
                        color: '#ed4',
                        opacity: 1,
                   },
                   z: 20
              },
              {
                   type: 'line',
                   data: dataSets[3],
                   lineStyle: {
                        opacity: 0,
                   },
                   symbolSize: 0,
                   areaStyle: {
                        color: '#d23',
                        opacity: 1,
                   },
                   z: 10
              },
         ],
    };
    const myChart = echarts.init(document.getElementById('chart-container'));
    myChart.setOption(option);
    window.addEventListener('resize', myChart.resize);
    <div id="chart-container" style="height:100vh"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.1/echarts.min.js"></script>


    That was the simplest solution; it only works for basic functionality - it can only work with x and y values that are on the axes labels, it can't do intermediate points. If intermediate values are needed, one has to consider a less simple solution - implementing some form of custom axes projection.

    Since echarts doesn't seem to allow tweaking its internals to that level in userland, we have to do the next best thing - fake it. That means we transform the true data - what is written on the axes labels, to "fake" data - where those labels are actually placed, where they appear on the axes. We'll use a piecewise linear mapping between the real and fake spaces, which seems to be the assumption the OP makes in this case. Other mappings can be implemented - piecewise logarithmic, higher order splines, etc.

    So, assuming the first two labels ('0' and '1') are the same in the true and fake spaces (we are at liberty to choose other values in the fake space, but this this seems the most natural one), this means the the third label, '10' is to be placed at fake position 2, the fourth label '25' is to be placed at position 4 and so on.

    On the x axis the real data 0, 1, 2, 10, 100 is transformed to fake data 0, 1, 2, 3, 4 and on the y axis the real data 0, 1, 10, 25, 50 is transformed to fake data 0, 1, 2, 3, 4 - this means the fake data will be the index in the array of true label data. And in between them we apply linear interpolation using the left and right labels - for instance a real x value of 6 is halfway between 2 and 10 so in fake space it will appear halfway between fake(2) = 2 and fake(10) = 3, that is at 2.5.

    Here's a snippet for this version:

    const xStepValues = [0, 1, 2, 10, 100],
        yStepValues = [0, 1, 10, 25, 50],
        // transform step values to their indices
        true2fake = trueValues => function(x){
            for(let i = 0; i < xStepValues.length-1; i++){
                if(x >= trueValues[i] && x <= trueValues[i+1]){
                    // linear interpolation between two consecutive trueValues
                    return i + (x-trueValues[i])/(trueValues[i+1]-trueValues[i]);
                }
            }
            throw new Error(`Invalid x value ${x}`);
        };
    
    const dataSets = [
        {
            data: [
                [0, 25], [1, 10], [1, 0]
            ],
            color:'#284',
            z: 40 // top-most
        },
        {
            data:[
                [0, 50], [1, 50], [2, 25], [2, 0]
            ],
            color:'#4d8',
            z: 30
        },
        {
            data:[
                [0, 50], [2, 50], [10, 25], [10, 0]
            ],
            color:'#ed4',
            z: 20
        },
        {
            data:[
                [0, 50], [100, 50]
            ],
            color:'#d23',
            z: 10 // bottom-most
        }
    ];
    
    const point = {data: [4, 45.25], color: '#44f', z: 50};
    
    const fakeX = x => true2fake(xStepValues)(x),
        fakeY = y => true2fake(yStepValues)(y),
        fakeXY = ([x, y])=>[fakeX(x), fakeY(y)];
    
    const option = {
        legend: {
            show: false,
        },
        tooltip:{
            show: false
        },
        xAxis: {
            minInterval: 1,
            maxInterval: 1,
            axisLabel: {
                formatter: function(index, value){
                    return xStepValues[value];
                }
            },
            boundaryGap: false,
            z: 50,
            splitLine:{
                show: true, z:100,
                lineStyle:{color:'#888', width:2}
            }
        },
        yAxis: {
            minInterval: 1,
            maxInterval: 1,
            axisLabel: {
                formatter: function(index, value){
                    return yStepValues[value]+'%';
                }
            },
            boundaryGap: false,
            z: 50,
            splitLine:{
                show: true,
                lineStyle:{color:'#888', width:2}
            }
        },
        series: Array.from({length: dataSets.length}, // colored areas
            (_, i) => ({
                type: 'line',
                data: dataSets[i].data.map(fakeXY),
                lineStyle: {
                    opacity: 0,
                },
                symbolSize: 0,
                areaStyle: {
                   color: dataSets[i].color,
                   opacity: 1,
                },
                z: dataSets[i].z
            })).concat([{ // the point
                 type: 'scatter',
                 symbolSize: 10,
                 itemStyle:{
                    color: point.color,
                    opacity: 1,
                 },
                 data: [fakeXY(point.data)],
                 z: point.z
             }])
    };
    const myChart = echarts.init(document.getElementById('chart-container'));
    myChart.setOption(option);
    window.addEventListener('resize', myChart.resize);
    <div id="chart-container" style="height:100vh"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.1/echarts.min.js"></script>