Chart.js OnClick event with a mixed chart, which chart did I click?

11,333

HTML

<div id="test" style="height:600px; width:600px;">
    <canvas id="myCanvas" style="border: 1px solid black; margin: 25px 25px, display: none;" height="300" >Canvas</canvas>
</div>

JS

var ctx = document.getElementById("myCanvas");
var newArr;

var config = new Chart(ctx,{
   type: 'bar',
   data: {
      labels: ["Test","Test","Test"],
      datasets: [{
         label: 'Dataset1',
         yAxisID: 'Dataset1',
         type: "line",
         borderColor: "red",
         backgroundColor: "red",
         data: [70,60,50],
         fill: false
      },
      {
         label: 'Dataset0',
         type: "bar",
         backgroundColor: "blue",
         data: [100,90,80]
      }]
   },
   options: {
      scales: {
         xAxes: [{ barPercentage: 1.0 }],
         yAxes: [{ id: 'Dataset1', position: 'left', type: 'linear',
                   ticks: { display: false, min: 0, beginAtZero: true, max: 120 },
                   scaleLabel: { display: true, labelString: "TestScale" } }]
      },
      responsive: true,
      maintainAspectRatio: false,
      legend : { display: true, position: 'bottom' },
      onClick: chartClickEvent
   }
}); // end of var config

function chartClickEvent(event, array){
   if(typeof newArr === 'undefined'){
        newArr = array;
   }

   if (window.config === 'undefined' || window.config == null)
   {
      return;
   }
   if (event === 'undefined' || event == null)
   {
      return;
   }
   if (newArr === 'undefined' || newArr == null)
   {
      return;
   }
   if (newArr.length <= 0)
   {
      return;
   }
   var active = window.config.getElementAtEvent(event);
   if (active === 'undefined' || active == null || active.length === 0)
   {
      return;
   }

   var elementIndex = active[0]._datasetIndex;
   console.log("elementIndex: " + elementIndex + "; array length: " + newArr.length);

   if (newArr[elementIndex] === 'undefined' || newArr[elementIndex] == null){
      return;
   }

   var chartData = newArr[elementIndex]['_chart'].config.data;
   var idx = newArr[elementIndex]['_index'];

   var label = chartData.labels[idx];
   var value = chartData.datasets[elementIndex].data[idx];
   var series = chartData.datasets[elementIndex].label;

   alert(series + ':' + label + ':' + value);
}
Share:
11,333
JustLooking
Author by

JustLooking

Updated on June 19, 2022

Comments

  • JustLooking
    JustLooking almost 2 years

    EDIT: Modified to add options, and a suggested (from the answer) chartClickEvent, here is a jsfiddle: http://jsfiddle.net/jmpxgufu/174/

    Imagine if you will a Chart.js mixed chart with the following config:

    var config = {
       type: 'bar',
       data: {
          labels: ["Test","Test","Test"],
          datasets: [{
             label: 'Dataset1',
             yAxisID: 'Dataset1',
             type: "line",
             borderColor: "red",
             backgroundColor: "red",
             data: [70,60,50],
             fill: false
          },
          {
             label: 'Dataset0',
             type: "bar",
             backgroundColor: "blue",
             data: [100,90,80]
          }]
       },
       options: {
          scales: {
             xAxes: [{ barPercentage: 1.0 }],
             yAxes: [{ id: 'Dataset1', position: 'left', type: 'linear',
                       ticks: { display: false, min: 0, beginAtZero: true, max: 120 },
                       scaleLabel: { display: true, labelString: "TestScale" } }]
          },
          responsive: true,
          maintainAspectRatio: false,
          legend : { display: true, position: 'bottom' },
          onClick: chartClickEvent
       }
    }; // end of var config
    
    function chartClickEvent(event, array)
    {
       if (window.myChart === undefined || window.myChart == null)
       {
          return;
       }
       if (event === undefined || event == null)
       {
          return;
       }
       if (array === undefined || array == null)
       {
          return;
       }
       if (array.length <= 0)
       {
          return;
       }
       var active = window.myChart.getElementAtEvent(event);
       if (active === undefined || active == null)
       {
          return;
       }
       var elementIndex = active[0]._datasetIndex;
       console.log("elementIndex: " + elementIndex + "; array length: " + array.length);
       if (array[elementIndex] === undefined || array[elementIndex] == null)
       {
          return;
       }
    
       var chartData = array[elementIndex]['_chart'].config.data;
       var idx = array[elementIndex]['_index'];
    
       var label = chartData.labels[idx];
       var value = chartData.datasets[elementIndex].data[idx];
       var series = chartData.datasets[elementIndex].label;
    
       alert(series + ':' + label + ':' + value);
    }
    

    As my chartClickEvent says, my array is length 2, because I have two charts. That's great and all, but I have no idea how to figure out whether to use array[0] or array[1]. If they click specifically the line data point, I want to do something with that data (array[0]), if they click the big blue bar, I want to do something with that data (array[1]). How do I tell whether they clicked on the line or the bar?

    Thank you.

  • JustLooking
    JustLooking over 6 years
    Bring it on in for a virtual hug! Thanks!
  • JustLooking
    JustLooking over 6 years
    Uh-oh. Discovered a bug when using datasetIndex. Let's say you have a line and a bar (like my example). The line will have a datasetIndex of 0, and the bar will have a datasetIndex of 1. The array will be of length 2. So far, so good. Here's the issue: if by using the chart.js legend, I hide the lines, this is what I get (since there's only a bar now): datasetIndex of 1, and array size of 1!! So, now I have exceeded the length of the array. How does one find the true "index", based on what is visible?
  • JustLooking
    JustLooking over 6 years
    Obviously, in this instance, I have just a bar and a line. So, of course, if the array length was 1, I could just grab the 0th index out of that array. But that's hard-coding this event to that data. I'm looking for a generic solution. I mean, for example, imagine if there were 6 bars and lines, and you hid three items from the legend, and the dataIndex you got was 5, and the array length was 3. Now what do you do? That's why I was looking for something generic.
  • Matt
    Matt over 6 years
    What are you doing to get this error? I put another line or bar dataset and hide the middle one I will get a Unable to get property '_datasetIndex' of undefined or null reference error but I can still click on the other 2 and get the index of them. If you can let me know what you are trying to do with it now then I can recreate the issue and better assist
  • JustLooking
    JustLooking over 6 years
    I modified my original question to contain the options I am using, and the chartClickEvent I have put together (using the config.getElementAtEvent). I think what you are describing is the error. I shouldn't get undefined if something is hidden. For example, with just a simple bar and line (I didn't change that), on first load, if I click the big blue bar (any of them, I chose the first), I get an alert that says: Dataset0:Test:100 (all of the pieces of info I want from that data element). The console.log tells me that the bar was elementIndex 1, for an array length of 2.
  • JustLooking
    JustLooking over 6 years
    Now, go and check Dataset1 (the red one) in the legend, so that it hides it. Now, when I click the big blue bar, I get no alert. It doesn't get that far because of an if guard I put in place. That's because console.log is reporting an elementIndex of 1, but this time with an array of 1. See? Before it was an array of 2, so I could do array[1] and retrieve the data. Now, if I do array[1] I exceed the bounds of the array, because the array changes size upon hiding things in the ledger. Yet, the datasetIndex always remains the same.
  • Matt
    Matt over 6 years
    If you go and replace your myChart entries with config do you still get the error?
  • JustLooking
    JustLooking over 6 years
    Hrmm, not sure what you mean. But I'm thinking it boils down to the fact that the array changes length, but the dataSetIndex doesn't adjust.
  • JustLooking
    JustLooking over 6 years
    I mean, I can solve all of this by doing this (instead of using the array):
  • JustLooking
    JustLooking over 6 years
    var elementIndex = active[0]._datasetIndex; var idx = active[0]['_index']; var chartData = active[0]['_chart'].config.data;
  • JustLooking
    JustLooking over 6 years
    So then I'm like, what's the point of passing that array to the chartClickEvent???
  • JustLooking
    JustLooking over 6 years
    When I override the tooltip callback for labels, it's defined as: function(tooltipItem, data) { ... }, so I can do something like: data.datasets[tooltipItem.datasetIndex].label; .... that's what I would expect from the chartClickEvent! That the event would get me event.dataSetIndex, and then I can peek into the array using that index.
  • Matt
    Matt over 6 years
    I believe the error is just because of clicking on the label and nothing more. But what I did to help this is created a global var newArr; and right inside the function chartClickEvent did if(typeof newArr === 'undefined'){ newArr = array; } Changed your checks to point to newArr instead of array and in the if statement after your var active = .. did an or check to see if active.length === 0. Always get alerts, no console errors.
  • JustLooking
    JustLooking over 6 years
    It's definitely not because of clicking on the labels. That's what the array.length <= 0 guard finds and returns. And that behavior changes based on whether you define a scale (options) or not - which is odd. Have you seen the jsfiddle? You will see that clicking the labels/legends does not error.
  • JustLooking
    JustLooking over 6 years
    You can make those changes in the jsfiddle, and then save/update it. And post the link here. I'm really not seeing how that will help. The issue is that the array length changes and the dataIndex remains the same. Also, it seems odd that I would have to do all these things (global variables). What is the point in having an event and an array object passed to the click event, then? It seems to me I should be able to use both of those. Yet your solution uses a global variable (unconfirmed) and my solution doesn't use the passed in array variable.
  • Matt
    Matt over 6 years
    I can't access it from where I currently am.
  • Matt
    Matt over 6 years
    I updated my answer to show how I am doing everything. Maybe it's something that simple.
  • JustLooking
    JustLooking over 6 years
    Yeah, in my haste to get you something, I did have a check for active.length that got omitted. But that wasn't the issue. What you are effectively doing is getting a capture of the array, for your first click, that you never modify/replace. That's why it "works" for you. So you are saving the state of the array so that the length of that array never changes. That seems odd that one would have to jump through hoops and save the state. What happens if I turn off a legend item first, before clicking? Am I now saving a different state of that array? This is odd.
  • JustLooking
    JustLooking over 6 years
    Take your same code, click the legend first (turn off dataset1, the red one), and then click the blue bar. Your solution would fail.
  • JustLooking
    JustLooking over 6 years
    That's because you are now saving the array in the state of length 1 instead of length 2.
  • JustLooking
    JustLooking over 6 years
    Your getElementAtEvent function helped me greatly. But again, what is odd is that the array length changes (the array passed to the click event), but working backwards from the active element, the dataSetIndex does not. So, you can run into situations where the dataSetIndex is 5, and the length of the array is 1. So, array[5] on a length of 1 array would be undefined. I really think I'm running into a bug. Or something just isn't right. We should be able to extract a better index from the passed in event parameter, one that works with the passed in array parameter.
  • Matt
    Matt over 6 years
    Yeah. I'm sorry there isn't someone smarter to help out. I'm also new to the whole chart stuff too so I am going as is and testing. I apologize for it.
  • JustLooking
    JustLooking over 6 years
    LOL. No, you were awesome! Like I said, you got me the getElementAtEvent function which saved me a ton of time. Very much appreciated. I'm just picking nits in regards to what I'm seeing from the chart.js people. Something just doesn't seem right. But dude, thanks again for your help. Just bouncing something off someone is great.
  • Wojciech Jakubas
    Wojciech Jakubas over 4 years
    I do not have config defined which means this code will not work for me. But you could use this instead. This is what I would suggest: var active = this.getElementAtEvent(event);