5

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
  • 2,405
  • 4
  • 28
  • 38
  • What do you mean by using `array[0]` or `array[1]`? You can use whatever you want. Just keep in mind that the array will be likely in the same order that you inputted as data. – lilezek Oct 10 '17 at 17:54
  • Yes, I agree. What I mean is that I want to figure out how out if they clicked the bar, or the line. As I mentioned, array[0] will contain information about the line, array[1] will contain information about the bar. In other words, there's a point in my code where I would execute: var chartData = array[0]['_chart'].config.data; if it was the line I wanted the data from, or var chartData = array[1]['_chart'].config.data; if it was the bar I wanted the data from. But how do I know if they clicked the line vs. bar? Yest I have an array of length 2, but which of the two items do I use? – JustLooking Oct 10 '17 at 17:58
  • Try debugging the `event` object. It may contain an `event.target`. – lilezek Oct 10 '17 at 17:59
  • Yes, I was fiddling around with that, hoping I could get it from there. event is the PointerEvent, of course, but event.target is an HTMLCanvasElement object, and event.target.id is chart-area (which is just the id I gave to my canvas tag (that's wrapped in a div tag). So I was having a hard time examining this CanvasElement for the info I need. – JustLooking Oct 10 '17 at 18:04

1 Answers1

6

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);
}
Matt
  • 1,062
  • 1
  • 8
  • 11
  • Bring it on in for a virtual hug! Thanks! – JustLooking Oct 10 '17 at 18:30
  • 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 Oct 25 '17 at 00:15
  • 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. – JustLooking Oct 25 '17 at 00:18
  • 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 – Matt Oct 25 '17 at 12:19
  • 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 Oct 25 '17 at 16:13
  • 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. – JustLooking Oct 25 '17 at 16:16
  • If you go and replace your `myChart` entries with `config` do you still get the error? – Matt Oct 25 '17 at 16:52
  • 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 Oct 25 '17 at 16:57
  • I mean, I can solve all of this by doing this (instead of using the array): – JustLooking Oct 25 '17 at 17:04
  • var elementIndex = active[0]._datasetIndex; var idx = active[0]['_index']; var chartData = active[0]['_chart'].config.data; – JustLooking Oct 25 '17 at 17:04
  • So then I'm like, what's the point of passing that array to the chartClickEvent??? – JustLooking Oct 25 '17 at 17:05
  • 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. – JustLooking Oct 25 '17 at 17:07
  • 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. – Matt Oct 25 '17 at 17:30
  • 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 Oct 25 '17 at 17:38
  • 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. – JustLooking Oct 25 '17 at 17:40
  • I can't access it from where I currently am. – Matt Oct 25 '17 at 17:42
  • I updated my answer to show how I am doing everything. Maybe it's something that simple. – Matt Oct 25 '17 at 17:43
  • 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 Oct 25 '17 at 17:53
  • 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 Oct 25 '17 at 17:54
  • That's because you are now saving the array in the state of length 1 instead of length 2. – JustLooking Oct 25 '17 at 17:55
  • 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. – JustLooking Oct 25 '17 at 17:57
  • 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. – Matt Oct 25 '17 at 18:03
  • 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. – JustLooking Oct 25 '17 at 18:10
  • 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);` – Wojciech Jakubas Jan 04 '20 at 18:51