4

I have a Radar chart with labels, I want to have a click event on the Label of the Radar chart but the element always returns null. I have looked at other Stack over flow questions notedly 1 and this 2. one talks about doing it in vanilla JS approach and other one just is not working for me , Can some one point me to what am i doing wrong ?

End goal -> I want to get the label which is clicked and add a strike through toggle to that label so that i can toggle the data point on and off in the radar chart.

My Implementation

class Chart extends Component {
  constructor(props) {
    super(props);
    this.state = {
      chartData: props.chartData
    };
  }
  render() {
    return (
      <div className="chart">
        <Radar
          data={this.state.chartData}
          options={{
            title: {
              display: true,
              text: "Testing",
              fontSize: 25
            },
            legend: {
              display: true,
              position: "right"
            },
            onClick: function(evt, element) {
              // onClickNot working element null
              console.log(evt, element);
              if (element.length > 0) {
                console.log(element, element[0]._datasetInde);
                // you can also get dataset of your selected element
                console.log(data.datasets[element[0]._datasetIndex]);
              }
            }
          }}
        />
      </div>
    );
  }
}

**Link to the sample implementation **

isherwood
  • 52,576
  • 15
  • 105
  • 143
INFOSYS
  • 1,347
  • 8
  • 21
  • 45

2 Answers2

1

Note: This answer implementation doesn't implement strikethrough. Strikethrough could be implemented by putting unicode character \u0366 between each character of the label string. Here's an example how do this with Javascript. The reason I'm not showcasing this here, is because it didn't really look that great when I tested it on codesandbox.

In a newer version of chart.js radial scale point label positions were exposed. In the example below I'm using chart.js version 3.2.0 and react-chartjs-2 version 3.0.3.

We can use the bounding box of each label and the clicked position to determine if we've clicked on a label.

I've used a ref on the chart to get access to the label data.

I've chosen to set the data value corresponding to a label to 0. I do this, because if you were to remove an element corresponding to a label, the label would disappear along with it. My choice probably makes more sense if you see it in action in the demo below.

const swapPreviousCurrent = (data) => {
  const temp = data.currentValue;
  data.currentValue = data.previousValue;
  data.previousValue = temp;
};

class Chart extends Component {
  constructor(props) {
    super(props);
    this.state = {
      chartData: props.chartData
    };
    this.radarRef = {};
    this.labelsStrikeThrough = props.chartData.datasets.map((dataset) => {
      return dataset.data.map((d, dataIndex) => {
        return {
          data: {
            previousValue: 0,
            currentValue: d
          },
          label: {
            previousValue: `${props.chartData.labels[dataIndex]} (x)`,
            currentValue: props.chartData.labels[dataIndex]
          }
        };
      });
    });
  }

  render() {
    return (
      <div className="chart">
        <Radar
          ref={(radarRef) => (this.radarRef = radarRef)}
          data={this.state.chartData}
          options={{
            title: {
              display: true,
              text: "Testing",
              fontSize: 25
            },
            legend: {
              display: true,
              position: "right"
            }
          }}
          getElementAtEvent={(element, event) => {
            const clickX = event.nativeEvent.offsetX;
            const clickY = event.nativeEvent.offsetY;
            const scale = this.radarRef.scales.r;
            const pointLabelItems = scale._pointLabelItems;
            pointLabelItems.forEach((pointLabelItem, index) => {
              if (
                clickX >= pointLabelItem.left &&
                clickX <= pointLabelItem.right &&
                clickY >= pointLabelItem.top &&
                clickY <= pointLabelItem.bottom
              ) {
                // We've clicked inside the bounding box, swap labels and label data for each dataset
                this.radarRef.data.datasets.forEach((dataset, datasetIndex) => {
                  swapPreviousCurrent(
                    this.labelsStrikeThrough[datasetIndex][index].data
                  );
                  swapPreviousCurrent(
                    this.labelsStrikeThrough[datasetIndex][index].label
                  );

                  this.radarRef.data.datasets[datasetIndex].data[
                    index
                  ] = this.labelsStrikeThrough[datasetIndex][
                    index
                  ].data.previousValue;

                  this.radarRef.data.labels[index] = this.labelsStrikeThrough[
                    datasetIndex
                  ][index].label.previousValue;
                });
                // labels and data have been changed, update the graph
                this.radarRef.update();
              }
            });
          }}
        />
      </div>
    );
  }
}

So I use the ref on the chart to get acces to the label positions and I use the event of getElementAtEvent to get the clicked x and y positions using event.nativeEvent.offsetX and event.nativeEvent.offsetY.

When we've clicked on the label I've chosen to update the value of the ref and swap the label data value between 0 and its actual value. I swap the label itself between itself and itself concatenated with '(x)'.

sandbox example

The reason I'm not using state here is because I don't want to rerender the chart when the label data updates.

Bas van der Linden
  • 11,895
  • 4
  • 22
  • 47
  • This is really really awesome something for me to learn and explore too , the tweak to prevent re render is also really nice.. One thing i saw is the bounding rec is very small and it requires us to click on a very specific area , very close to the edge of the start of label for it to register in one click else it takes multiple clicks (2) to get it working.. Let me see if i can increase the box for the bounding rec for labels – INFOSYS Apr 30 '21 at 18:10
  • @INFOSYS The easiest way to account for the small bounding box would be to make the `if` statement more lenient. Instead of having `clickX >= pointLabelItem.left` you could do `clickX >= pointLabelItem.left - 30` for example. The latter would effectively give you some click padding on the left. You could do something similar for the other directions. You could subtract a value from the top and left directions and add a value to the right and bottom directions. – Bas van der Linden Apr 30 '21 at 18:26
0

You could run a function that modifies your dataset:

You would create the function where you have your data set

chartClick(index) {
    console.log(index);
    //this.setState({}) Modify your datasets properties
  }

Pass the function as props

<Chart chartClick={this.chartClick} chartData={this.state.chartData} />

Execute the function when clicked

onClick: (e, element) => {
              if (element.length) {
                this.props.chartClick(element[0]._datasetIndex);
              }
            }
danielm2402
  • 542
  • 2
  • 11
  • This works if clicking on the points, but not when clicking on the labels. This also doesn't mention strikethrough. That could be done using [this](https://stackoverflow.com/a/53836006/9098350) though. Also I think for OP's use case it should be `_index` instead of `_datasetIndex`. – Bas van der Linden Apr 25 '21 at 17:29
  • @BasvanderLinden Did you try using `onElementsClick = {elems => {}}` ? – danielm2402 Apr 25 '21 at 17:32
  • I've tried `getElementAtEvent` and `onElementsClick` and both return an empty array if you click on a label. Works for points, not the labels. – Bas van der Linden Apr 25 '21 at 17:36
  • @BasvanderLinden i was able to get it on the points even using getElementsAtEvent previously , i want something for the label and not for the point is that possible ? – INFOSYS Apr 25 '21 at 17:58
  • @danielm2402 yes indeed i need it for the label instead of the data points. – INFOSYS Apr 25 '21 at 17:58