Search code examples
reactjschartshighcharts

Highcharts - columnrange - How to prevent dataLabels formatter from triggering several times per data (series)?


I have a datetime chart which supposed to show some time intervals. I needed to add some labels to each time interval which shows some data from the array I pass to chart. But the problem is: the more elements I have in the array, the weirder behaviour I get. Datalabels overlapping each other and rendering at the columns they should not be rendered at. I found out that the formatter triggers multiple times, and I want to find out is there any way to prevent this behaviour and render datalabels only once per data.

Here's what I'm trying to do:

chart: {
        type: 'columnrange',
        margin: 0,
        inverted: true,
        backgroundColor: 'transparent',
        events: {
          load: function (ctx) {
            for (var i = 0; i < data.length; i++) {
              this.addSeries({
                type: 'columnrange',
                grouping: false,
                data: [
                  {
                    low: i < data.filter(x => x.rowNumber === 4).length
                      ? data.filter(x => x.rowNumber === 4)[i].intervalStart
                      : Date.UTC(1, 1, 1, 0, 0, 0),
                    high: i < data.filter(x => x.rowNumber === 4).length
                      ? data.filter(x => x.rowNumber === 4)[i].intervalEnd
                      : Date.UTC(1, 1, 1, 0, 0, 0),
                    color: i < data.filter(x => x.rowNumber === 4).length
                      ? (data.filter(x => x.rowNumber === 4)[i].status === 1 ? "#6ebb86" : "#434348")
                      : "transparent",
                    dataLabels: {
                      enabled: true,
                      verticalAlign: 'middle',
                      align: 'center',
                      inside: true,
                      crop: true,
                      style: {
                        fontSize: '14px',
                      },

                      formatter: (e) => {
                        return i < data.filter(x => x.rowNumber === 4).length
                          ? data.filter(x => x.rowNumber === 4)[i]?.dataLabel
                          : ""
                      }
                    }
                  }
                ],
                borderColor: 'transparent',
              });
            }
          },
        },
      },

The example of dataLabels overlapping and mismatch


Solution

  • I have found the solution and I want to share it with others.

    Instead of writing dataPoints in the load function, I decided to move it to plotOptions -> series so that from there I could access the series itself. This is what it looks like:

    plotOptions: {
            columnrange: {
              grouping: false,
              pointPadding: -0.2,
              states: {
                hover: {
                  borderColor: 'transparent',
                },
                inactive: {
                  enabled: false,
                },
              },
            },
            series: {
              cursor: 'pointer',
              point: {
                events: {
                  click: function () {
                    triggerEditInterval(
                      this.series.chart.hoverPoint?.options.low!,
                      this.series.chart.hoverPoint?.options.high!)
                  },
                }
              },
              dataLabels: {
                enabled: true,
                verticalAlign: 'middle',
                align: 'center',
                inside: true,
                crop: true,
                style: {
                  fontSize: '14px',
                },
    
                formatter: function () {
                  var actualPoints = this.series.points.filter(p => typeof p.y === 'number'); //getting the list of actual points
    
                  if (this.y === actualPoints.find(x => x.y === this.y)?.y) { // if the y of current point exists in our actual points list
                    if (actualPoints.findIndex(x => x.y === this.y)! === 0) { // check if actual point is first category
                      return data.find(x => x.intervalEnd === this.y && x.rowNumber === 4)?.dataLabel // return the label of element from our data element with appropriate coordinates and rowNumber for first category
                    }
                    else {
                      return data.find(x => x.intervalEnd === this.y && x.rowNumber === 5)?.dataLabel // else return the element with rowNumber for second category
                    }
                  }
                }
              }
            },
          },
    

    It works just as intended