React Chart.js Stacked Bar Chart Data Table Click Event

Overview

This post will go over how to display a stacked bar chart that changes the data in a table. Basically, drilling down and show the detailed data behind an individual stacked bar on the chart.

Source Code

Please get the source files for this demo.

GitHub Repo

Prerequisites

Chart.JS version 3.6.0

Chart.JS and React Chart.JS a wrapper for Chart.JS. This is a great open source chart library that is downloaded over 300k times per week as of March 2022

npm i chart.js react-chartjs-2

Material – (Optional)

MUI – Material UI v5.4.3

Material is only used for the table and layout of the demo.

npm install @mui/material @mui/lab @emotion/react @emotion/styled

Raw Data for the Chart and Table

Raw data for the Chart Component. You can put this in a separate file and export it or just drop it in the Parent Component App.js.


const rawData = [
  {id: 1,  day: 1,  type: 1, airline: 'Southwest',  passengers: 162, parkedHours: 24},
  {id: 2,  day: 1,  type: 1, airline: 'Southwest',  passengers: 208, parkedHours: 8},
  {id: 3,  day: 2,  type: 1, airline: 'Southwest',  passengers: 176, parkedHours: 16},
  {id: 4,  day: 2,  type: 1, airline: 'Southwest',  passengers: 204, parkedHours: 24},
  {id: 5,  day: 3,  type: 1, airline: 'Southwest',  passengers: 158, parkedHours: 58},
  {id: 6,  day: 4,  type: 1, airline: 'Southwest',  passengers: 202, parkedHours: 22},
  {id: 7,  day: 0,  type: 1, airline: 'Southwest',  passengers: 218, parkedHours: 38},
  {id: 8,  day: 4,  type: 1, airline: 'Southwest',  passengers: 136, parkedHours: 6},
  {id: 9,  day: 3,  type: 1, airline: 'Southwest',  passengers: 124, parkedHours: 24},
  {id: 10,  day: 3, type: 1, airline: 'Southwest',  passengers: 158, parkedHours: 2},
  {id: 11,  day: 1, type: 1, airline: 'American',  passengers: 162, parkedHours: 24},
  {id: 12,  day: 1, type: 1, airline: 'American',  passengers: 208, parkedHours: 8},
  {id: 13,  day: 2, type: 1, airline: 'American',  passengers: 176, parkedHours: 16},
  {id: 14,  day: 2, type: 1, airline: 'American',  passengers: 204, parkedHours: 24},
  {id: 15,  day: 3, type: 1, airline: 'American',  passengers: 158, parkedHours: 58},
  {id: 16,  day: 4, type: 1, airline: 'American',  passengers: 202, parkedHours: 22},
  {id: 17,  day: 0, type: 1, airline: 'American',  passengers: 218, parkedHours: 38},
  {id: 18,  day: 4, type: 1, airline: 'American',  passengers: 136, parkedHours: 6},
  {id: 19,  day: 3, type: 1, airline: 'American',  passengers: 124, parkedHours: 24},
  {id: 20,  day: 3, type: 1, airline: 'American',  passengers: 158, parkedHours: 2},
  {id: 21,  day: 1, type: 1, airline: 'Allegiant',  passengers: 162, parkedHours: 24},
  {id: 22,  day: 1, type: 1, airline: 'Allegiant',  passengers: 208, parkedHours: 8},
  {id: 23,  day: 2, type: 1, airline: 'Allegiant',  passengers: 176, parkedHours: 16},
  {id: 24,  day: 2, type: 1, airline: 'Allegiant',  passengers: 204, parkedHours: 24},
  {id: 25,  day: 3, type: 1, airline: 'Allegiant',  passengers: 158, parkedHours: 58},
  {id: 26,  day: 4, type: 1, airline: 'Allegiant',  passengers: 202, parkedHours: 22},
  {id: 27,  day: 0, type: 1, airline: 'Allegiant',  passengers: 218, parkedHours: 38},
  {id: 28,  day: 4, type: 1, airline: 'Allegiant',  passengers: 136, parkedHours: 6},
  {id: 29,  day: 3, type: 1, airline: 'Allegiant',  passengers: 124, parkedHours: 24},
  {id: 30,  day: 3, type: 1, airline: 'Allegiant',  passengers: 158, parkedHours: 2},
  {id: 31,  day: 1, type: 2, airline: 'Capital One',  passengers: 12, parkedHours: 4},
  {id: 32,  day: 1, type: 2, airline: 'Bank of America',  passengers: 28, parkedHours: 8},
  {id: 33,  day: 2, type: 2, airline: 'Charles Schwab',  passengers: 16, parkedHours: 6},
  {id: 34,  day: 2, type: 2, airline: 'Iron Maiden Corp',  passengers: 24, parkedHours: 24},
  {id: 35,  day: 3, type: 2, airline: 'Scorpions Corp',  passengers: 18, parkedHours: 8},
  {id: 36,  day: 4, type: 2, airline: 'Fed Ex',  passengers: 22, parkedHours: 2},
  {id: 37,  day: 0, type: 2, airline: 'Amazon',  passengers: 8, parkedHours: 8},
  {id: 38,  day: 4, type: 2, airline: 'M Hotel',  passengers: 16, parkedHours: 6},
  {id: 39,  day: 3, type: 2, airline: 'Wells Corp',  passengers: 4, parkedHours: 24},
  {id: 40,  day: 3, type: 2, airline: 'Smith Corp', passengers: 18, parkedHours: 2},
  {id: 41,  day: 3, type: 2, airline: 'Walton Corp', passengers: 8, parkedHours: 5},
];

Short Answer – How to Enable the Click Event

From the chart component, import useRef hook from ‘react’

import React, { useRef } from 'react';
import {  
  Bar,  
  getElementAtEvent,  
} from 'react-chartjs-2';

Create a Reference to the Chart

Create an instance of the a reference and add it in the chart component.

export default function ChartComp(props) {
  const chartRef = useRef();
...
return <Bar options={options} data={data} onClick={onClick}  ref={chartRef}/>;

How to Get the Index and Stack Bar Location.

Passing the reference chartRef and event to getElementAtEvent from ‘react-chartjs-2’, we can extract the index and dataSetIndex. The index will have the label (1 to 5) value and the dataSetIndex is the location of the stacked bar (0 for bottom, 1 for top in this case).

  const onClick = (event) => {
    const elem = getElementAtEvent(chartRef.current, event)
    
    props.onHandleBarClickEvent(elem[0].index, elem[0].datasetIndex)
  }

The Long Answer

Create the Chart Component

Under the component folder create a new file “components\ChartComp.js“.

Import Libraries

Add the following imports.

import { useState, useEffect } from 'react';
import React, { useRef } from 'react';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
import {  
  Bar,  
  getElementAtEvent,  
} from 'react-chartjs-2';

Register the Chart.JS Components

Remember, every single component from chart.js import your chart will use must be registered.


ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

Chart Options and Data

To enable a stacked bar chart, set stacked to true under options -> scales -> x & y.

const options = {
    plugins: {
      title: {
        display: true,
        text: '- Stacked',
      },
    },
    responsive: true,
    scales: {
      x: {
        stacked: true,
      },
      y: {
        stacked: true,
      },
    },
  };

  const labels= props.data.map(c => c.label);

  const data = {
    labels,
    datasets: [
      {
        label: 'Commercial Flights',
        data: props.data.map(c => c.commercialCount),
        backgroundColor: '#58508d',
      },
      {
        label: 'General Aviation',
        data: props.data.map(c => c.generalCount),
        backgroundColor: '#ff6361',
      },
    ],
    tooltips: {
  
    },
  };

The Entire Chart Component – All Together

All together

import React, { useRef } from 'react';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
import {  
  Bar,  
  getElementAtEvent,  
} from 'react-chartjs-2';

ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

export default function ChartCompBak(props) {
  const chartRef = useRef();

  const options = {
    plugins: {
      title: {
        display: true,
        text: '- Stacked',
      },
    },
    responsive: true,
    scales: {
      x: {
        stacked: true,
      },
      y: {
        stacked: true,
      },
    },
  };

  const labels= props.data.map(c => c.label);

  const data = {
    labels,
    datasets: [
      {
        label: 'Commercial Flights',
        data: props.data.map(c => c.commercialCount),
        backgroundColor: '#58508d',
      },
      {
        label: 'General Aviation',
        data: props.data.map(c => c.generalCount),
        backgroundColor: '#ff6361',
      },
    ],
    tooltips: {
  
    },
  };

  const onClick = (event) => {
    const elem = getElementAtEvent(chartRef.current, event)
    props.onHandleBarClickEvent(elem[0].index, elem[0].datasetIndex)    
  }

  return <Bar options={options} data={data} onClick={onClick}  ref={chartRef}/>;
}

Data Table Component

Under the component folder create a new file “components\MatDataTable.js“.

Copy and paste the code below

import { useEffect, useState } from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';

export default function MatDataTable(props) {
  const [ tableData, setTableData ] = useState(props.data)

  useEffect(() => {    
    setTableData(props.data)
  }, [props.data]);

  return (
    <TableContainer component={Paper}>
      <Table sx={{ minWidth: 350 }} size="small" aria-label="a dense table">
        <TableHead>
          <TableRow>
            <TableCell>Airline</TableCell>
            <TableCell align="right">Hours Parked</TableCell>
            <TableCell align="right">Passengers</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {tableData.map((row, index) => (
            <TableRow              
              key={index}
              sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
            >
              <TableCell component="th" scope="row">
                {row.airline}
              </TableCell>
              <TableCell align="right">{row.parkedHours}</TableCell>
              <TableCell align="right">{row.passengers}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

The Parent App.js Component

The App.js component converts the raw data to chart data as explained above. Then renders the ChartComp and MatDataTable components. MatDataTable will initially be empty or hidden. When a chart bar is click then the parent will modify the table data that populates the MatDataTable component

How to Generate Chart Data from Raw Data

From my experience building charts for custom web applications, the raw API data must be converted/massaged to work with a chart.


Loop the raw data to create a JSON array by getting the daily counts for type 1 & 2 flights. This fake data has flights for Monday to Friday using values 1 to 5 from an array. Values for days Monday = 1, Tuesday = 2 etc.
Use the JavaScript filter to get the count for each type on each day.

rawData.filter(c => c.day === i && c.type === 1).length

The chart data will have {label : day[i], commercialCount: cfCnt, generalCount: gaCnt: } pushed into an array.

  const [ chartData, setChartData ] = useState([])  
  
  const [ isLoaded, setIsLoaded ] =  useState(false);
...
  useEffect(() => {
      setIsLoaded(false)
      const day = ['Mon', 'Tues', 'Wed', 'Thu', 'Fri']
      var chartData = []
      for(let i=0; i <= 5; i++) {
        const cfCnt = rawData.filter(c => c.day === i && c.type === 1).length
        const gaCnt = rawData.filter(c => c.day === i && c.type === 2).length
        chartData.push({label: day[i], commercialCount: cfCnt, generalCount: gaCnt})
      }
      setChartData(chartData)
      
      setTimeout(() => {
        setIsLoaded(true)    
    }, 100);
  }, []);

The whole App.js component.

import { useState, useEffect } from 'react';
import ChartComp from './components/ChartComp'
import MatDataTable from './components/MatDataTable';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';

const rawData = [
  {id: 1,  day: 1,  type: 1, airline: 'Southwest',  passengers: 162, parkedHours: 24},
  {id: 2,  day: 1,  type: 1, airline: 'Southwest',  passengers: 208, parkedHours: 8},
  {id: 3,  day: 2,  type: 1, airline: 'Southwest',  passengers: 176, parkedHours: 16},
  {id: 4,  day: 2,  type: 1, airline: 'Southwest',  passengers: 204, parkedHours: 24},
  {id: 5,  day: 3,  type: 1, airline: 'Southwest',  passengers: 158, parkedHours: 58},
  {id: 6,  day: 4,  type: 1, airline: 'Southwest',  passengers: 202, parkedHours: 22},
  {id: 7,  day: 0,  type: 1, airline: 'Southwest',  passengers: 218, parkedHours: 38},
  {id: 8,  day: 4,  type: 1, airline: 'Southwest',  passengers: 136, parkedHours: 6},
  {id: 9,  day: 3,  type: 1, airline: 'Southwest',  passengers: 124, parkedHours: 24},
  {id: 10,  day: 3, type: 1, airline: 'Southwest',  passengers: 158, parkedHours: 2},
  {id: 11,  day: 1, type: 1, airline: 'American',  passengers: 162, parkedHours: 24},
  {id: 12,  day: 1, type: 1, airline: 'American',  passengers: 208, parkedHours: 8},
  {id: 13,  day: 2, type: 1, airline: 'American',  passengers: 176, parkedHours: 16},
  {id: 14,  day: 2, type: 1, airline: 'American',  passengers: 204, parkedHours: 24},
  {id: 15,  day: 3, type: 1, airline: 'American',  passengers: 158, parkedHours: 58},
  {id: 16,  day: 4, type: 1, airline: 'American',  passengers: 202, parkedHours: 22},
  {id: 17,  day: 0, type: 1, airline: 'American',  passengers: 218, parkedHours: 38},
  {id: 18,  day: 4, type: 1, airline: 'American',  passengers: 136, parkedHours: 6},
  {id: 19,  day: 3, type: 1, airline: 'American',  passengers: 124, parkedHours: 24},
  {id: 20,  day: 3, type: 1, airline: 'American',  passengers: 158, parkedHours: 2},
  {id: 21,  day: 1, type: 1, airline: 'Allegiant',  passengers: 162, parkedHours: 24},
  {id: 22,  day: 1, type: 1, airline: 'Allegiant',  passengers: 208, parkedHours: 8},
  {id: 23,  day: 2, type: 1, airline: 'Allegiant',  passengers: 176, parkedHours: 16},
  {id: 24,  day: 2, type: 1, airline: 'Allegiant',  passengers: 204, parkedHours: 24},
  {id: 25,  day: 3, type: 1, airline: 'Allegiant',  passengers: 158, parkedHours: 58},
  {id: 26,  day: 4, type: 1, airline: 'Allegiant',  passengers: 202, parkedHours: 22},
  {id: 27,  day: 0, type: 1, airline: 'Allegiant',  passengers: 218, parkedHours: 38},
  {id: 28,  day: 4, type: 1, airline: 'Allegiant',  passengers: 136, parkedHours: 6},
  {id: 29,  day: 3, type: 1, airline: 'Allegiant',  passengers: 124, parkedHours: 24},
  {id: 30,  day: 3, type: 1, airline: 'Allegiant',  passengers: 158, parkedHours: 2},
  {id: 31,  day: 1, type: 2, airline: 'Capital One',  passengers: 12, parkedHours: 4},
  {id: 32,  day: 1, type: 2, airline: 'Bank of America',  passengers: 28, parkedHours: 8},
  {id: 33,  day: 2, type: 2, airline: 'Charles Schwab',  passengers: 16, parkedHours: 6},
  {id: 34,  day: 2, type: 2, airline: 'Iron Maiden Corp',  passengers: 24, parkedHours: 24},
  {id: 35,  day: 3, type: 2, airline: 'Scorpions Corp',  passengers: 18, parkedHours: 8},
  {id: 36,  day: 4, type: 2, airline: 'Fed Ex',  passengers: 22, parkedHours: 2},
  {id: 37,  day: 0, type: 2, airline: 'Amazon',  passengers: 8, parkedHours: 8},
  {id: 38,  day: 4, type: 2, airline: 'M Hotel',  passengers: 16, parkedHours: 6},
  {id: 39,  day: 3, type: 2, airline: 'Wells Corp',  passengers: 4, parkedHours: 24},
  {id: 40,  day: 3, type: 2, airline: 'Smith Corp', passengers: 18, parkedHours: 2},
  {id: 41,  day: 3, type: 2, airline: 'Walton Corp', passengers: 8, parkedHours: 5},
];

export default function App() {    
  const [ chartData, setChartData ] = useState([])  
  const [ tableData, setTableData ] = useState([])  
  

  useEffect(() => {
  
      const day = ['Mon', 'Tues', 'Wed', 'Thu', 'Fri']
      var chartData = []
      for(let i=0; i <= 5; i++) {
        const cfCnt = rawData.filter(c => c.day === i && c.type === 1).length
        const gaCnt = rawData.filter(c => c.day === i && c.type === 2).length
        chartData.push({label: day[i], commercialCount: cfCnt, generalCount: gaCnt})
      }
      setChartData(chartData)
      
  }, []);

  const onHandleBarClickEvent = (barIndex, stackIndex) => {        
    if(stackIndex === 0) {
      setTableData(rawData.filter(c => c.day === barIndex && c.type === 1))
    }
    else {
      setTableData(rawData.filter(c => c.day === barIndex && c.type === 2))
    }
  }

  return (
    <div className="App">      
      <Box sx={{ flexGrow: 1, marginTop: 12 }}>
        <Grid container spacing={2}>
          <Grid item xs={12} md={1}></Grid>
          <Grid item xs={6} md={6}>
            
              <ChartComp  data={chartData}                           
                          onHandleBarClickEvent={onHandleBarClickEvent}/>
            
          </Grid>
          <Grid item xs={6} md={4}>            
            <MatDataTable data={tableData}/>            
          </Grid>
        </Grid>
      </Box>            
    </div>
  );
}