1

I am testing out HighCharts and ChartJS to see which to use. For Highcharts, I was able to find a hack to create a bar chart that had double grouping on the x-axis.

This is what I want to look like:

enter image description here

I am new to both charting JS options and wonder if there is a way to do this in ChartJS.

I have the datasets something like this:

    xAxis: {
        categories: [{
            name: "Total",
            categories: ["2004", "2008", "2012"]
        }, {
            name: "Lower than 2.50",
            categories: ["2004", "2008", "2012"]
        }]
    },
    yAxis: {
        min: 0,
        title: {
            text: 'Percent (%)'
        }
    },
    series: [{
        name: 'Male',
        data: [42.4, 43.0, 43.0, 50.3, 49.4, 48.4]

    }, {
        name: 'Female',
        data: [57.6, 57.0, 57.0, 49.7, 50.6, 51.6]

    }]

Essentially I need a nest series on the x-axis and I am open to plugins or code from Github to do this.

cdub
  • 24,555
  • 57
  • 174
  • 303

2 Answers2

4

You can make use of the Plugin Core API. It offers different hooks that may be used for executing custom code. In below code, I use the afterDraw hook to draw the category labels and the delimiter lines.

new Chart('myChart', {
  type: 'bar',
  plugins: [{
    afterDraw: chart => {
      let ctx = chart.chart.ctx;
      ctx.save();    
      let xAxis = chart.scales['x-axis-0'];
      let xCenter = (xAxis.left + xAxis.right) / 2;
      let yBottom = chart.scales['y-axis-0'].bottom;
      ctx.textAlign = 'center';
      ctx.font = '12px Arial';
      ctx.fillText(chart.data.categories[0], (xAxis.left + xCenter) / 2, yBottom + 40);
      ctx.fillText(chart.data.categories[1], (xCenter + xAxis.right) / 2, yBottom + 40);
      ctx.strokeStyle  = 'lightgray';
      [xAxis.left, xCenter, xAxis.right].forEach(x => {
        ctx.beginPath();
        ctx.moveTo(x, yBottom);
        ctx.lineTo(x, yBottom + 40);
        ctx.stroke();
      });
      ctx.restore();
    }
  }],
  data: {
    labels: ['2004', '2008', '2012', '2016', '2004', '2008', '2012', '2016'],
    categories: ['Total', 'Lower than 2.50'],
    datasets: [{
        label: 'Male',
        data: [42.4, 43.0, 43.0, 50.3, 49.4, 48.4, 51.2, 51.8],
        backgroundColor: 'rgba(124, 181, 236, 0.9)',
        borderColor: 'rgb(124, 181, 236)',
        borderWidth: 1
      },
      {
        label: 'Female',
        data: [57.6, 57.0, 57.0, 49.7, 50.6, 51.6, 53.7, 54.6],
        backgroundColor: 'rgba(67, 67, 72, 0.9)',
        borderColor: 'rgb(67, 67, 72)',
        borderWidth: 1
      }
    ]
  },
  options: {
    legend: {
      position: 'bottom',
      labels: {
        padding: 30,
        usePointStyle: true
      }
    },
    scales: {
      yAxes: [{
        ticks: {
          min: 0,
          max: 80,
          stepSize: 20
        },
        scaleLabel: {
          display: true,
          labelString: 'Percent (%)'
        }
      }],
      xAxes: [{
        gridLines: {
          drawOnChartArea: false
        }
      }]
    }
  }
});
canvas {
  max-width: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<canvas id="myChart" height="200"></canvas>
uminder
  • 23,831
  • 5
  • 37
  • 72
  • Thanks. You know your stuff. I am going to play with this more and make it dynamic for us. – cdub Mar 16 '21 at 05:16
1

You have to define a second x-axis and play around with the many ticks and gridLines options.

Please take a look at below runnable code and see how it could be done. This is obviously only a draft and needs to optimized and made more generic.

new Chart(document.getElementById('myChart'), {
  type: 'bar',
  data: {
    labels: ['2004', '2008', '2012', '2004', '2008', '2012'],    
    datasets: [{
        label: 'Male',
        data: [42.4, 43.0, 43.0, 50.3, 49.4, 48.4],
        backgroundColor: 'rgba(124, 181, 236, 0.9)',
        borderColor: 'rgb(124, 181, 236)',
        borderWidth: 1
      },
      {
        label: 'Female',
        data: [57.6, 57.0, 57.0, 49.7, 50.6, 51.6],
        backgroundColor: 'rgba(67, 67, 72, 0.9)',
        borderColor: 'rgb(67, 67, 72)',
        borderWidth: 1
      }
    ]
  },
  options: {
    legend: {
      position: 'bottom',
      labels: {
        usePointStyle: true
      }
    },
    scales: {
      yAxes: [{
        ticks: {
          min: 0,
          max: 80,
          stepSize: 20
        },
        scaleLabel: {
          display: true,
          labelString: 'Percent (%)'
        }
      }],
      xAxes: [{
          gridLines: {
            drawOnChartArea: false
          }
        },
        {
          offset: true,
          ticks: {
            autoSkip: false,
            maxRotation: 0,
            padding: -15,
            callback: (v, i) => {
              if (i == 1) {
                return 'Total';
              } else if (i == 4) {
                return 'Lower than 2.50';
              } else {
                return '';
              }
            }
          },
          gridLines: {
            drawOnChartArea: false,
            offsetGridLines: true,
            tickMarkLength: 20,
            color: ['white', 'white', 'white', 'lightgray', 'white', 'white', 'lightgray']
          }
        }
      ]
    }
  }
});
canvas {
  max-width: 400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<canvas id="myChart" height="200"></canvas>
uminder
  • 23,831
  • 5
  • 37
  • 72
  • I find that if I have the chart size 400 by 400 the Total disappears which is weird and the documentation around ticks is subpar online that i can see. – cdub Mar 12 '21 at 20:58
  • Can you explain the issue with the missing total under the 2nd tick line? – cdub Mar 13 '21 at 08:16
  • I made the chart smaller using `max-width` but am unable to reproduce the problem with the missing "Total" on the second x-axis. Are you also using the latest stable version of Chart.js (currently 2.9.4)? – uminder Mar 13 '21 at 15:51
  • It's a firefox vs chrome issue. In firefox the snippet above when run does not have the total. Running this same snippet above in chrome works fine. I don't know why. – cdub Mar 13 '21 at 23:46
  • I had to add `autoSkip: false` on the second x-axis to make it also work in Firefox. – uminder Mar 14 '21 at 08:28
  • awesome thanks. where are you getting all the possible options as I don't see everything listed in the docs? – cdub Mar 14 '21 at 20:17
  • I get most information in the Chart.js documentation but must agree that sometimes it's not easy to find it right away. A good source of further information are the Chart.js samples (https://www.chartjs.org/samples/latest/). Simply select the appropriate sample chart and view its page source. – uminder Mar 15 '21 at 16:10
  • I see. I will keep searching. I also don't know how to center the categories dynamically if there is an even number like categories: ["2004", "2008", "2012", "2016"] ass it is simple to do when its odd. – cdub Mar 15 '21 at 16:22