How to add OnClick Event on labels in Chart.js v2.0?

25,683

Solution 1

getElementsAtEvent checks only the main elements of the chart (bars, points, sectors...). If you want to consider labels too, you'll have to reimplement the functionality for labels.

Most of the code you need is already available in different methods in the Chart.js library code. Just copy-paste / cleanup as done below.


Script

Your click hander should be

$('#myChart').click(function (e) {
    var helpers = Chart.helpers;

    var eventPosition = helpers.getRelativePosition(e, myRadarChart.chart);
    var mouseX = eventPosition.x;
    var mouseY = eventPosition.y;

    var activePoints = [];
    // loop through all the labels
    helpers.each(myRadarChart.scale.ticks, function (label, index) {
        for (var i = this.getValueCount() - 1; i >= 0; i--) {
            // here we effectively get the bounding box for each label
            var pointLabelPosition = this.getPointPosition(i, this.getDistanceFromCenterForValue(this.options.reverse ? this.min : this.max) + 5);

            var pointLabelFontSize = helpers.getValueOrDefault(this.options.pointLabels.fontSize, Chart.defaults.global.defaultFontSize);
            var pointLabeFontStyle = helpers.getValueOrDefault(this.options.pointLabels.fontStyle, Chart.defaults.global.defaultFontStyle);
            var pointLabeFontFamily = helpers.getValueOrDefault(this.options.pointLabels.fontFamily, Chart.defaults.global.defaultFontFamily);
            var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily);
            ctx.font = pointLabeFont;

            var labelsCount = this.pointLabels.length,
                halfLabelsCount = this.pointLabels.length / 2,
                quarterLabelsCount = halfLabelsCount / 2,
                upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
                exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
            var width = ctx.measureText(this.pointLabels[i]).width;
            var height = pointLabelFontSize;

            var x, y;

            if (i === 0 || i === halfLabelsCount)
                x = pointLabelPosition.x - width / 2;
            else if (i < halfLabelsCount)
                x = pointLabelPosition.x;
            else
                x = pointLabelPosition.x - width;

            if (exactQuarter)
                y = pointLabelPosition.y - height / 2;
            else if (upperHalf)
                y = pointLabelPosition.y - height;
            else
                y = pointLabelPosition.y

            // check if the click was within the bounding box
            if ((mouseY >= y && mouseY <= y + height) && (mouseX >= x && mouseX <= x + width))
                activePoints.push({ index: i, label: this.pointLabels[i] });
        }
    }, myRadarChart.scale);

    var firstPoint = activePoints[0];
    if (firstPoint !== undefined) {
        alert(firstPoint.index + ': ' + firstPoint.label);
    }
});

Fiddle - http://jsfiddle.net/1Lngmtz7/

Solution 2

in chart.js 2.5 (maybe even earlier) you can put an onClick in the options:

'legend' : {
    'onClick' : function (evt, item) {
                    console.log ('legend onClick', evt, item);
                },
    'display' : true,
    'labels' : ...

Solution 3

I came up with a solution for this for version 2.8.0 by copying the label position calculations from the RadialLinear scale into an event handler.

document.getElementById("myChart").onclick = function (e) {
    var helpers = Chart.helpers;
    var scale = myRadarChart.scale;
    var opts = scale.options;
    var tickOpts = opts.ticks;

    // Position of click relative to canvas.
    var mouseX = e.offsetX;
    var mouseY = e.offsetY;

    var labelPadding = 5; // number pixels to expand label bounding box by

    // get the label render position
    // calcs taken from drawPointLabels() in scale.radialLinear.js
    var tickBackdropHeight = (tickOpts.display && opts.display) ?
        helpers.valueOrDefault(tickOpts.fontSize, Chart.defaults.global.defaultFontSize)
        + 5: 0;
    var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max);
    for (var i = 0; i < scale.pointLabels.length; i++) {
        // Extra spacing for top value due to axis labels
        var extra = (i === 0 ? tickBackdropHeight / 2 : 0);
        var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5);

        // get label size info.
        // TODO fix width=0 calc in Brave?
        // https://github.com/brave/brave-browser/issues/1738
        var plSize = scale._pointLabelSizes[i];

        // get label textAlign info
        var angleRadians = scale.getIndexAngle(i);
        var angle = helpers.toDegrees(angleRadians);
        var textAlign = 'right';
        if (angle == 0 || angle == 180) {
            textAlign = 'center';
        } else if (angle < 180) {
            textAlign = 'left';
        }

        // get label vertical offset info
        // also from drawPointLabels() calcs
        var verticalTextOffset = 0;
        if (angle === 90 || angle === 270) {
            verticalTextOffset = plSize.h / 2;
        } else if (angle > 270 || angle < 90) {
            verticalTextOffset = plSize.h;
        }

        // Calculate bounding box based on textAlign
        var labelTop = pointLabelPosition.y - verticalTextOffset - labelPadding;
        var labelHeight = 2*labelPadding + plSize.h;
        var labelBottom = labelTop + labelHeight;

        var labelWidth = plSize.w + 2*labelPadding;
        var labelLeft;
        switch (textAlign) {
        case 'center':
          var labelLeft = pointLabelPosition.x - labelWidth/2;
          break;
        case 'left':
          var labelLeft = pointLabelPosition.x - labelPadding;

          break;
        case 'right':
          var labelLeft = pointLabelPosition.x - labelWidth + labelPadding;
          break;
        default:
          console.log('ERROR: unknown textAlign '+textAlign);
        }
        var labelRight = labelLeft + labelWidth;

        // Render a rectangle for testing purposes
        ctx.save();
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 1;
        ctx.strokeRect(labelLeft, labelTop, labelWidth, labelHeight);
        ctx.restore();

        // compare to the current click
        if (mouseX >= labelLeft && mouseX <= labelRight && mouseY <= labelBottom && mouseY >= labelTop) {
            alert(scale.pointLabels[i]+' clicked');
            // Break loop to prevent multiple clicks, if they overlap we take the first one.
            break;
        }
    }
};

JSFiddle here:

https://jsfiddle.net/simoncoggins/7r08uLk9/

The downside of this approach is it that it will break if the core labelling implementation changes in the future. It would be better if the library separated the calculation of label position from its rendering and started exposing the position info via the API. Then this solution could be greatly simplified and would be more robust to library changes.

I've opened a ticket offering to make that change here:

https://github.com/chartjs/Chart.js/issues/6549

Please comment on that issue if it would be useful to you.

Solution 4

Cannot read property 'getDatasetMeta' of undefined Chartjs

If you got this error from doc example of chartjs, the client is the problem. Replace it with the client of your current instance.

In my case im using myHorizontalBar

var defaultLegendClickHandler = function(e, legendItem) {
    var index = legendItem.datasetIndex;
    var ci = window.myHorizontalBar; //this.chart;

    var meta = ci.getDatasetMeta(index);

    // See controller.isDatasetVisible comment
    meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null;

    // We hid a dataset ... rerender the chart
    ci.update();
}  

myHorizontalBar

        window.onload = function() {
        var ctx = document.getElementById('canvas').getContext('2d');
        window.myHorizontalBar = new Chart(ctx, {
            type: 'horizontalBar',
            data: horizontalBarChartData,
            options: {
Share:
25,683
infiniteloop
Author by

infiniteloop

Updated on July 14, 2022

Comments

  • infiniteloop
    infiniteloop almost 2 years

    Looking For a method to add onClick handle on the "label" element in chartjs 2.0 As using below method will return "undifined" in console.log whenever clicking on any one of the label attributes in Char.js V2.0 RadarChart.

    var data = {
        // below line is the labels 
        labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"],
        datasets: [
            {
                label: "My First dataset", //this only shows as legend, not label.
    
                backgroundColor: "rgba(179,181,198,0.2)",
                borderColor: "rgba(179,181,198,1)",
                pointBackgroundColor: "rgba(179,181,198,1)",
                pointBorderColor: "#fff",
                pointHoverBackgroundColor: "#fff",
                pointHoverBorderColor: "rgba(179,181,198,1)",
                data: [65, 59, 90, 81, 56, 55, 40]
            },
           ....
    

    //Below is how to OnClick on chart points in chart.js V2, 
    //However, it didn't apply to labels, will return "undifined" .   
    $('#ChartV2').click(function(e) {
                    var activePoints = myRadarChart.getElementsAtEvent(e);                  
                    var firstPoint = activePoints[0];
                    console.log(firstPoint);
                    if (firstPoint !== undefined){
                       alert(firstPoint._index);                        
                    }
     });

  • infiniteloop
    infiniteloop about 8 years
    Great lesson with example in the fiddle. very helpful for people who need to extend features from original version of chart.js , considering other chart libraries, I put a reference here from the chart lib comparison made by fusioncharts.com/javascript-charting-comparison , it says "The charts of chart.js are drawn using Canvas and hence cannot offer any interactivity.", your example extended its functionality and proved it wrong.
  • IncredibleHat
    IncredibleHat about 6 years
    This blows out the default onClick behavior of the legend items. Which is a touchy thing to get working (the chart.js docs are extremely vague and their example code throws errors).
  • João Pimentel Ferreira
    João Pimentel Ferreira about 4 years
    @IncredibleHat, if you want to keep the default behaviour at the end of the function onClick add Chart.defaults.global.legend.onClick.call(this, evt, item)