VolkovLabs / business-charts

The Business Charts panel allows you to integrate charts and graphs created by the Apache ECharts library into your Grafana dashboard.
https://docs.volkovlabs.io
Apache License 2.0
139 stars 17 forks source link

Ability to use Radar Chart alongside other charts with synchronized data. #331

Closed TJMakesTech closed 1 month ago

TJMakesTech commented 1 month ago

Hello! I have been using your library and I find it amazing. I am visualising time series sensor data, while extracting key maximum values and displaying that to the user. You can see an example here: Business-Charts Panel Grafana

I would really prefer to have radar plots instead of pie charts above the line graph, as my 4 fields are consistent and don't add up to 100%. I have been able to get them working separately to the line graph but the data synchronization as the user hovers over the line graph is a requirement for me.

When I try and add chart type 'radar' it does not work at all, whereas 'pie' works perfectly. I also think that it works in direct apache echarts but not in the business charts grafana plugin. But I am just an amateur :)

Can you confirm my suspicion and provide some guidance?

Thanks, Tom

p.s. my chart code below:

const lineStyle = {
  width: 4,
  opacity: 0.5
};

let dataTN = [];
let dataTW = [];
let dataTS = [];
let dataTE = [];
let dataAve = [];
let timer = [];

let TN = [];
let TW = [];
let TS = [];
let TE = [];

let BN = [];
let BW = [];
let BS = [];
let BE = [];

let N = [];
let W = [];
let S = [];
let E = [];

let Top = [];
let Bot = [];

let centerIndex = 0;

context.panel.data.series.forEach((s) => {
  TN = s.fields.find((f) => f.name === "TN")?.values || [];
  TW = s.fields.find((f) => f.name === "TW")?.values || [];
  TS = s.fields.find((f) => f.name === "TS")?.values || [];
  TE = s.fields.find((f) => f.name === "TE")?.values || [];

  BN = s.fields.find((f) => f.name === "BN")?.values || [];
  BW = s.fields.find((f) => f.name === "BW")?.values || [];
  BS = s.fields.find((f) => f.name === "BS")?.values || [];
  BE = s.fields.find((f) => f.name === "BE")?.values || [];

  const sTime = s.fields.find((f) => f.type === 'time').values.buffer || s.fields.find((f) => f.type === 'time').values;

  dataTW = TW.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataTN = TN.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataTS = TS.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataTE = TE.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataBW = BW.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataBN = BN.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataBS = BS.map((d, i) => [sTime[i], d.toFixed(2)]);
  dataBE = BE.map((d, i) => [sTime[i], d.toFixed(2)]);
  timer = sTime;

  // Compute the average for each data point
  const Taverage = TN.map((_, i) => {
    // Get the values for the current index, defaulting to 0 if undefined
    const tn = TN[i] ?? 0;
    const tw = TW[i] ?? 0;
    const ts = TS[i] ?? 0;
    const te = TE[i] ?? 0;

    // Calculate the average
    return (tn + tw + ts + te) / 4;
  });

  Top = Taverage;

  dataTop = Top.map((d, i) => [sTime[i], d.toFixed(2)]);

  centerIndex = (dataTop.length / 2)

  // Compute the average for each data point
  const Baverage = BN.map((_, i) => {
    // Get the values for the current index, defaulting to 0 if undefined
    const bn = BN[i] ?? 0;
    const bw = BW[i] ?? 0;
    const bs = BS[i] ?? 0;
    const be = BE[i] ?? 0;

    // Calculate the average
    return (bn + bw + bs + be) / 4;
  });

  Bot = Baverage;
  dataBot = Bot.map((d, i) => [sTime[i], d.toFixed(2)]);

});

const averageTN = (TN.reduce((sum, value) => sum + value, 0) / TN.length).toFixed(0);;
const averageTE = (TE.reduce((sum, value) => sum + value, 0) / TE.length).toFixed(0);;
const averageTS = (TS.reduce((sum, value) => sum + value, 0) / TS.length).toFixed(0);;
const averageTW = (TW.reduce((sum, value) => sum + value, 0) / TW.length).toFixed(0);;

const averageBN = (BN.reduce((sum, value) => sum + value, 0) / BN.length).toFixed(0);;
const averageBE = (BE.reduce((sum, value) => sum + value, 0) / BE.length).toFixed(0);;
const averageBS = (BS.reduce((sum, value) => sum + value, 0) / BS.length).toFixed(0);;
const averageBW = (BW.reduce((sum, value) => sum + value, 0) / BW.length).toFixed(0);;

const averageTop = (Top.reduce((sum, value) => sum + value, 0) / Top.length).toFixed(0);;
const averageBot = (Bot.reduce((sum, value) => sum + value, 0) / Bot.length).toFixed(0);;

let pieDataUpper =
  [
    { value: Math.max(...TN), name: 'North' },
    { value: Math.max(...TE), name: 'East' },
    { value: Math.max(...TS), name: 'South' },
    { value: Math.max(...TW), name: 'West' }
  ]

let pieDataLower =
  [
    { value: Math.max(...BN), name: 'North' },
    { value: Math.max(...BE), name: 'East' },
    { value: Math.max(...BS), name: 'South' },
    { value: Math.max(...BW), name: 'West' }
  ]

context.panel.chart.on('updateAxisPointer', function (event) {
  const xAxisInfo = event.axesInfo[0];
  const yAxisInfo = event.axesInfo[1];
  if (xAxisInfo) {
    const dimension = xAxisInfo.value + 1;

    const index = timer.findIndex(t => t === xAxisInfo.value);

    const tnValue = TN[index];
    const twValue = TW[index];
    const tsValue = TS[index];
    const teValue = TE[index];

    const bnValue = BN[index];
    const bwValue = BW[index];
    const bsValue = BS[index];
    const beValue = BE[index];

    pieDataUpper =
      [
        { value: tnValue, name: 'North' },
        { value: teValue, name: 'East' },
        { value: tsValue, name: 'South' },
        { value: twValue, name: 'West' }
      ]

    context.panel.chart.setOption({
      series: {
        id: 'pie',
        data: pieDataUpper,
        label: {
          formatter: '{b}: {@[' + dimension + ']}%'
        },
        /*encode: {
          value: dimension,
          tooltip: dimension
        }*/
      }
    });

    pieDataLower =
      [
        { value: bnValue, name: 'North' },
        { value: beValue, name: 'East' },
        { value: bsValue, name: 'South' },
        { value: bwValue, name: 'West' }
      ]

    context.panel.chart.setOption({
      series: {
        id: 'pie2',
        data: pieDataLower,
        label: {
          formatter: '{b}: {@[' + dimension + ']}%'
        },
        /*encode: {
          value: dimension,
          tooltip: dimension
        }*/
      }
    });

  }
});

/**
 * Enable Data Zoom by default
 */
setTimeout(() => context.panel.chart.dispatchAction({
  type: 'takeGlobalCursor',
  key: 'dataZoomSelect',
  dataZoomSelectActive: true,
}), 500);

context.panel.chart.dispatchAction({
  type: 'showTip',
  seriesIndex: 0,   // Refers to the first (or only) series in this case
  dataIndex: centerIndex  // Show tooltip at the center index
});

/**
 * Update Time Range on Zoom
 */
context.panel.chart.on('datazoom', function (params) {
  const startValue = params.batch[0]?.startValue;
  const endValue = params.batch[0]?.endValue;
  locationService.partial({ from: startValue, to: endValue });
});

option = {
  backgroundColor: 'transparent',
  tooltip: {
    trigger: 'axis',
    "position": [150, 50],  // Example of setting fixed coordinates [x, y]
    axisPointer: {
      type: "line"
    }
  },
  legend: {
    orient: 'vertical',
    left: 'left',
    itemGap: 20,
    top: 'middle',
    data: ['Upper', 'Lower', 'TN', 'TE', 'TW', 'TS', 'BN', 'BE', 'BW', 'BS'], // Names of the series
    selected: {
      'Upper': true,
      'Lower': true,
      'TN': false, // Initially hide Series1
      'TE': false,  // Initially show Series2
      'TW': false,  // Initially show Series3
      'TS': false,  // Initially hide Series4
      'BN': false, // Initially hide Series1
      'BE': false,  // Initially show Series2
      'BW': false,  // Initially show Series3
      'BS': false  // Initially hide Series4
    },
    selectedMode: 'multiple' // Allow multiple series selection
  },
  toolbox: {
    feature: {
      dataZoom: {
        yAxisIndex: 'none',
        icon: {
          zoom: 'path://',
          back: 'path://',
        },
      },
      saveAsImage: {},
    }
  },
  xAxis: {
    type: 'time',
  },
  yAxis: {
    type: 'value',
    min: '0',
    max: '100'
  },
  visualMap: [
    {
      bottom: '2%',
      right: '1%',
      type: 'continuous',
      //calculable: true,
      text: ['100%', '0%'], // Labels for the max and min values
      itemHeight: 250, // Adjust the height of the visual map indicator
      itemWidth: 20,  // Adjust the width if needed
      orient: 'vertical', // Ensure it is vertical
      left: 'right',
      top: 'bottom',
      min: 0,
      max: 100,
      //dimension: 3, // the fourth dimension of series.data (i.e. value[3]) is mapped
      //seriesIndex: 4, // The fourth series is mapped.
      inRange: {
        // The visual configuration in the selected range
        color: ['red', '#c8d64b', 'green'], // A list of colors that defines the graph color mapping
        // the minimum value of the data is mapped to 'blue', and
        // the maximum value is mapped to 'red', // the maximum value is mapped to 'red', // the maximum value is mapped to 'red'.
        // The rest is automatically calculated linearly.
        symbolSize: [30, 100] // Defines the mapping range for the graphic size.
        // The minimum value of the data is mapped to 30, // and the maximum value is mapped to 100.
        // The maximum value is mapped to 100.
        // The rest is calculated linearly automatically.
      },
      outOfRange: {
        // Check the out of range visual configuration
        symbolSize: [30, 100]
      }
    }
    // ...
  ],
  dataZoom: [
    {
      type: 'inside',
      start: 20,
      end: 80
    },
    {
      start: 20,
      end: 80
    }
  ],
  grid: {
    left: '10%',
    right: '6%',
    top: '50%',
    bottom: 50,
    containLabel: true,
  },
  title: [
    {
      text: 'Upper',
      left: '34.5%',
      top: '0%',
      textAlign: 'center'
    },
    {
      text: 'Lower',
      left: '74.5%',
      top: '0%',
      textAlign: 'center'
    }
  ],
  series: [
    {
      name: 'Upper',
      type: 'line',
      lineStyle: lineStyle,
      data: dataTop,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'Lower',
      type: 'line',
      lineStyle: lineStyle,
      data: dataBot,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'TN',
      type: 'line',
      lineStyle: lineStyle,
      data: dataTN,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'TE',
      type: 'line',
      lineStyle: lineStyle,
      data: dataTE,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F96143C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'TS',
      type: 'line',
      lineStyle: lineStyle,
      data: dataTS,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'TW',
      type: 'line',
      lineStyle: lineStyle,
      data: dataTW,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#B3E4A1'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'BN',
      type: 'line',
      lineStyle: lineStyle,
      data: dataBN,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'BE',
      type: 'line',
      lineStyle: lineStyle,
      data: dataBE,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F96143C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'BS',
      type: 'line',
      lineStyle: lineStyle,
      data: dataBS,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#F9713C'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'BW',
      type: 'line',
      lineStyle: lineStyle,
      data: dataBW,
      symbol: 'none',
      seriesLayoutBy: 'row',
      itemStyle: {
        //color: '#B3E4A1'
      },
      areaStyle: {
        opacity: 0.005
      },
      emphasis: { focus: 'series' }
    },
    {
      name: 'pie',
      type: 'pie',
      id: 'pie',
      data: pieDataUpper,
      radius: '30%',
      startAngle: 135, // Rotates the pie chart by 45 degrees,
      center: ['35%', '27.5%'],
      emphasis: {
        focus: 'self'
      },
      label: {
        formatter: '{b}: {@2012}%'
      }
    },
    {
      name: 'pie2',
      type: 'pie',
      id: 'pie2',
      data: pieDataLower,
      radius: '30%',
      startAngle: 135, // Rotates the pie chart by 45 degrees,
      center: ['75%', '27.5%'],
      emphasis: {
        focus: 'self'
      },
      label: {
        formatter: '{b}: {@2012}%'
      }
    }
  ]
};

return option;
vitPinchuk commented 1 month ago

@TJMakesTech Thank you for your question.

What plugin version do you use?

Since plugin version 6.2.0 6.2.0 we add support for radar type in visual editor.

We have blog post on it blog-post

And we have examples with the type of radar in our radar dashboard radar dashboard

Please check these links. I think it will be helpful and can explain how to use the radar-type diagram

Let me know of any updates. Thank you.