ankane / chartkick

Create beautiful JavaScript charts with one line of Ruby
https://chartkick.com
MIT License
6.35k stars 570 forks source link

[pro-tip][chart.js] Snippet for how to register an onClick handler via stimulus controller #562

Closed drnic closed 3 years ago

drnic commented 3 years ago

This is a pro-tip for #64 which I'm creating as a fresh issue (and then closing) because comments are locked on #64.

I wanted to register an onClick handler on Chart.js without giving up on chartkick/chartkick.js. Here's what I did to get something working:

  <div data-controller="chart" data-chart-id-value="some-uuid">
    <%= column_chart new_prior_learning_credit_request_workflows_reports_path, {
        id: "some-uuid",
        defer: false
    } %>
  </div>

Whilst defer: false is the default, I saw in #489 that it might become defer: true by default in future; so making it explicit here for future proofing. The chart needs to exist asap so that the stimulus controller can find it.

// app/javascript/controllers/chart_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static values = {
    id: String
  }

  // Mount an onClick handler on the Chart.js chart
  // Unfortunately, Chart.js v2.9 only provides which x-axis group was clicked,
  // and we don't know which specific column was clicked within a group.
  // The "event" provides X,Y coords where the chart was clicked, but
  // I cannot imagine how we'd convert that back into "which stack was clicked within a group"
  connect() {
    var chart = Chartkick.charts[this.idValue]
    setTimeout(() => {
      if (chart.getAdapter() == "chartjs") {
        let chartConfig = chart.getChartObject().config
        chartConfig.options.onClick = function (event, native, active) {
          if (native.length > 0) {
            let xAxisIndex = native[0]._index
            console.log("clicked on x-axis column", xAxisIndex)
          }
        }
        chart.getChartObject().update(chartConfig)
      }
    }, 1000)
  }
}

The reason for the setTimeout(.., 1000) is that the chart.js's udpate() method is called multiple times internally, which can override my chartConfig. So I just wait a second and then invoke update() and hope that's good enough. I'd look for a better way to inject my config.options.onClick handler in future.

I am using grouped/stacked column charts, and alas the event handler is not given enough information to discern which feature/column/stack/series was clicked, only that "something in x-axis 3 was clicked"

image
ankane commented 3 years ago

Thanks for sharing @drnic!

wnm commented 2 years ago

hey @drnic, thanks so much for this!!

I'm using stimulus in my project as well, and was searching for how to set it up with chartjs, and your post is really helpful!

The reason for the setTimeout(.., 1000) is that the chart.js's udpate() method is called multiple times internally, which can override my chartConfig. So I just wait a second and then invoke update() and hope that's good enough. I'd look for a better way to inject my config.options.onClick handler in future.

Any updates on this? Did you find a better solution since posting? Did you run into any problems with just using setTimout?

philsmy commented 1 year ago

(I know this is closed, but this is exactly my issue!)

It seems to me that the timeout is needed to wait for the data to be loaded from the remote URL. If you don't have remote data, but instead have inline, you can get rid of the setTimeout. It would be nice if there were a way to just do this on a callback or event after the data has been loaded.