tanner
tanner

Reputation: 179

Specify varying thickness of each bar in Chart.js bar chart

Working on a custom chart that can determine the thickness of a bar in a bar or horizontal bar chart based on a value provided by the data, not a pre-set width as possible with barThickness or barPercentage / categoryPercentage.

Goal: I can create a horizontal bar chart with data that contains an x value and a thickness, where the thickness becomes the relative thickness of that bar in relation to the other bars.

For example, this code:

const data = {
  labels: ['Northeast', 'Midwest', 'South', 'West'],
  datasets: [
    {
      data: [
        { x: 10, thickness: 18 },
        { x: 20, thickness: 8 },
        { x: 13, thickness: 5 },
        { x: 4, thickness: 12 }
      ],
    },
  ],
}

const context = document.querySelector('#myChart').getContext('2d')

const barChart = new Chart(context, {
    type: 'horizontalBar',
    data: data,
})

... would render a chart looking something like this:

desired result

... instead of this:

current view

(didn't include irrelevant style options here).

Key features going for:

How can this be done?

I know I can hack the drawing of bar chart controllers to specify explicit widths to the rectangles drawn, but this fails to produce bars that are evenly spaced. Instead they have variable width within their fixed-width category, which is not the look I'm going for:

bad widths

My hunch is that I need to extend a new axis but this seems complex.

Hoping to get any suggestions on best approaches! Thanks!

Similar to this unanswered question. Encountered these answers in my search which seemed promising but ultimately weren't the right answers:

Upvotes: 5

Views: 2621

Answers (1)

tanner
tanner

Reputation: 179

There are three things that need to be done to support this feature:

  1. Use the category scale to calculate bar thickness in proportion to axis size (width for vertical, height for horizontal)
  2. Override the #getPixelForValue() method in the category scale so that the category widths reflect the variant thickness, instead of being uniform
  3. Change the dimensions of the rectangles drawn by the controller using the values calculated by the scale

Implemented these as a custom chart in this package: https://github.com/swayable/chartjs-chart-superbar using Chart.js's new chart and new axes customization features.

Solved (1) by adding behavior like this to the category scale:

getBarThickness(thicknessPercentage) {
  let thickness = thicknessPercentage * this._axisSize(),
    spacing = this.options.spacing * 2

  return thickness - spacing
}

_axisSize() {
  return this.isHorizontal() ? this.width : this.height
}

(2) is a bit more complicated because the pixel for each value in the category scale needs to take into account all of the various thicknesses for each previous item in the dataset when calculating the distance from the edge (left for horizontal, or top for vertical). Otherwise the thicknesses of the categories in the axis will not match the thicknesses of the bars, and the category axis labels will not be centered on the bars.

Relevant file for (1) and (2): https://github.com/swayable/chartjs-chart-superbar/blob/master/src/scale.thickCategory.js

(3) is solved by changing the relevant dimension in the element's _view and _model properties using the Scale#getBarThickness() method above.

Relevant file for (3): https://github.com/swayable/chartjs-chart-superbar/blob/master/src/mixin.barThickness.js


Figured most of this out by reading the source code for Chart.js, especially scale.category.js and core.datasetController.js. Also took some inspiration from the source code for Chart.smith.js.

Upvotes: 5

Related Questions