React Chart.js Draggable with Material UI Table

Overview

This post is a how-to for installing ChartJS, ChartJS Draggable Plugin, and updating Data in a table. There are two components in addition to App.js, one for the chart and another for the data table. When the chart is update, the data in the table will reflect the changes made after the drag event. With the source code the onDrag() can update the data in real-time if you choose to uncomment the code inside the event.

Download Source from GitHub https://github.com/fullstacksoup

GitHub

Prerequisites

Create a New React-App

npx create react-app react-app

Installing ChartJS and ChartJS Draggable Plugin

IMPORTANT! As of the time of this article, the versions for ChartJS and the Draggable Plugin must be a the following in order for this to work. The latest of each as of 1/28/2022 does not work together possibly due to an unsupported dependency.

Below are the dependencies from the package.json file. This may be easier the installing individually,

  "dependencies": {
    "@emotion/react": "^11.7.1",
    "@emotion/styled": "^11.6.0",
    "@mui/material": "^5.3.1",
    "chart.js": "^3.6.0",
    "chartjs-plugin-dragdata": "^2.1.0",
    "@testing-library/jest-dom": "^5.16.1",
    "@testing-library/react": "^12.1.2",
    "@testing-library/user-event": "^13.5.0",
    "react": "^17.0.2",
    "react-chartjs-2": "^3.3.0",
    "react-dom": "^17.0.2",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.2"
  },

Install Libraries Individually

Chart.JS version 3.6.0

npm i chart.js@3.6.0

React Chart.JS version 3.3.0 – React wrapper for Chart.JS

npm i react-chartjs-2@3.3.0

Chart.JS Draggable Plugin version 2.1.0

npm i chartjs-plugin-dragdata@2.1.0

Material UIVersion 5

NOTE: React-Material Version 5 was was released in October of 2021.

For the documentation click here

For the data table only.

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

Chart Component

Create a new component labeled ChartComp.js under “./.src/components“. This component will have the data passed to it from App.js to render the ChartJS Bar Chart.

Component – Source Code

import React, { useState, useEffect } from 'react'
import { Bar } from "react-chartjs-2";
import 'chartjs-plugin-dragdata'
  

export default function ChartComp(props) {
  const [ shouldRedraw ] = useState(false);  
  const [ isLoaded, setIsLoaded ] =  useState(false);
  
  //  Build Data Set for the Bar Chart
  const buildDataSet = (data) => {
        
    let labels = data.map(c => c.label);

    let options = {
      type: 'line',
      data: {
        labels: labels,
        datasets: [
        {
          label: '# of Pears',
          data: data.map(c => c.aValue),
            //datasetIndex: data.map(c => c.Id),
            fill: true,
            tension: 0.4,
            borderWidth: 1,
            borderColor: 'darkred',
            backgroundColor: 'rgb(255, 230, 230)',
            pointHitRadius: 25
          },
          {
            label: '# of Apples',
            data: data.map(c => c.bValue),
              //datasetIndex: data.map(c => c.Id),
              fill: true,
              tension: 0.4,
              borderWidth: 1,
              borderColor: 'darkblue',
              backgroundColor: 'rgb(230, 230, 255)',
              pointHitRadius: 25
            }
          ]
      },
      options: {
        scales: {
          y: {
            min: 0,
            max: 200
          }
        },
        onHover: function(e) {
          const point = e.chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)
          if (point.length) e.native.target.style.cursor = 'grab'
          else e.native.target.style.cursor = 'default'
        },
        plugins: {
          dragData: {
            round: 1,
            showTooltip: true,
            onDragStart: function(e, element) {
              // console.log('On Drag Start ', element)
            },
            // Change while dragging 
            onDrag: function(e, datasetIndex, index, value) {              
              e.target.style.cursor = 'grabbing' 
              // console.log('On Dragging ', datasetIndex, index, value)
              // if(datasetIndex == 0) {
              //   data[index].aValue = value
              // }

              // if(datasetIndex == 1) {
              //   data[index].bValue = value
              // }

              // props.onHandleChange(data);

            },
            // Only change when finished dragging 
            onDragEnd: function(e, datasetIndex, index, value) {  
              // console.log('On Drag End ', datasetIndex, index, value)
              e.target.style.cursor = 'default' 
                            
              if(datasetIndex == 0) {
                data[index].aValue = value
              }

              if(datasetIndex == 1) {
                data[index].bValue = value
              }

              props.onHandleChange(data);
            },
          }
        }
      }
    }
    
    return options;
  }


  let localOption = buildDataSet(props.data);


  useEffect(() => {        
    setTimeout(() => {
      setIsLoaded(true)
    }, 200);
  }, [])

  return (
    <div>
      {isLoaded &&
        <Bar
            redraw={shouldRedraw}
            data={localOption.data}
            options={localOption.options}
            plugins={localOption.plugins}          
        />
      }

    </div>
  );
}

Breakdown of ChartComp.js

Building the Chart Dataset

The component calls the buildDataSet() function and passes the chart data as props.data from App.js. This function configures the following chart options:

  • Defines the label and data sets from props.data with the map() method.
  • Border and Background colors for the bars
  • Fill property to fill in the bar or just show an outline.
  • Min and Max scale.
  • On hover popover properties
  • Plugins: This is where the chartjs-plugin-dragdata is configured
    • showTooltip is enabled to display a popover with the current bar data value
    • onDragStart event for the initial starting point
    • onDrag while the user is dragging the chart bar
      • I commented out the props.onHandleChange() call to update in real time because it will the latency will not be as smooth as updating after the dragging is over. This might be remedied with Redux or useContext(). If I have more time….. 🙂
    • onDragEnd when the user finished dragging the chart bar. This is where the props.onHandleChange() which is passed from App.js is called to update the data table.

onDrag and onDragEnd have 4 parameters that are accessible.

e – Event – Mouse coordinates etc..

value– the dataset element value
datasetIndex – Which data set

index – This is the row or ID.

Example datsetIndex = 1 and index = 1

Build Data Set function

const buildDataSet = (data) => {
        
    let labels = data.map(c => c.label);

    let options = {
      type: 'line',
      data: {
        labels: labels,
        datasets: [
        {
          label: '# of Pears',
          data: data.map(c => c.aValue),
            //datasetIndex: data.map(c => c.Id),
            fill: true,
            tension: 0.4,
            borderWidth: 1,
            borderColor: 'darkred',
            backgroundColor: 'rgb(255, 230, 230)',
            pointHitRadius: 25
          },
          {
            label: '# of Apples',
            data: data.map(c => c.bValue),
              //datasetIndex: data.map(c => c.Id),
              fill: true,
              tension: 0.4,
              borderWidth: 1,
              borderColor: 'darkblue',
              backgroundColor: 'rgb(230, 230, 255)',
              pointHitRadius: 25
            }
          ]
      },
      options: {
        scales: {
          y: {
            min: 0,
            max: 200
          }
        },
        onHover: function(e) {
          const point = e.chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)
          if (point.length) e.native.target.style.cursor = 'grab'
          else e.native.target.style.cursor = 'default'
        },
        plugins: {
          dragData: {
            round: 1,
            showTooltip: true,
            onDragStart: function(e, element) {
              // console.log('On Drag Start ', element)
            },
            // Change while dragging 
            onDrag: function(e, datasetIndex, index, value) {              
              e.target.style.cursor = 'grabbing' 
              // console.log('On Dragging ', datasetIndex, index, value)
              // if(datasetIndex == 0) {
              //   data[index].aValue = value
              // }

              // if(datasetIndex == 1) {
              //   data[index].bValue = value
              // }

              // props.onHandleChange(data);

            },
            // Only change when finished dragging 
            onDragEnd: function(e, datasetIndex, index, value) {  
              // console.log('On Drag End ', datasetIndex, index, value)
              e.target.style.cursor = 'default' 
                            
              if(datasetIndex == 0) {
                data[index].aValue = value
              }

              if(datasetIndex == 1) {
                data[index].bValue = value
              }

              props.onHandleChange(data);
            },
          }
        }
      }
    }

Table Component

Simple table that is showing the same data that is in ChartComp.js.

The table data is passed from App.js.

  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';


function createData(name, calories, fat, carbs, protein) {
  return { name, calories, fat, carbs, protein };
}

const rows = [
  createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
  createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
  createData('Eclair', 262, 16.0, 24, 6.0),
  createData('Cupcake', 305, 3.7, 67, 4.3),
  createData('Gingerbread', 356, 16.0, 49, 3.9),
];

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

  useEffect(() => {
    console.log('MatDataTable', props.data)
    setTableData(props.data)
  }, [props.data]);

  return (
    <TableContainer component={Paper}>
      <Table sx={{ minWidth: 350 }} size="small" aria-label="a dense table">
        <TableHead>
          <TableRow>
            <TableCell>Label</TableCell>
            <TableCell align="right">Pears</TableCell>
            <TableCell align="right">Apples</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {tableData.map((row) => (
            <TableRow
              key={row.value}
              sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
            >
              <TableCell component="th" scope="row">
                {row.label}
              </TableCell>
              <TableCell align="right">{row.aValue}</TableCell>
              <TableCell align="right">{row.bValue}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

App Component

The parent component that renders the Chart and Table components and handles the data updates.

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

const rawData = [
  {label: 'Mon', aValue: 40, bValue: 62},
  {label: 'Tue', aValue: 14, bValue: 68},
  {label: 'Wed', aValue: 22, bValue: 76},
  {label: 'Thu', aValue: 43, bValue: 54},
  {label: 'Fri', aValue: 33, bValue: 58},
];

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: 'center',
  color: theme.palette.text.secondary,
}));


export default function App() {  
  const [ username, setUsername ] = useState('danny123');
  const [ chartData, setChartData ] = useState([])  
  const [ isLoaded, setIsLoaded ] =  useState(false);
  const [ isDataChanged, setIsDataChanged ] =  useState(true);

  useEffect(() => {
      setIsLoaded(false)
      setChartData(rawData)

      setTimeout(() => {
          setIsLoaded(true)    
      }, 100);
    
  }, []);

  const onHandleChange  = (data) => {
      setIsDataChanged(false)
  setChartData(data)

    setTimeout(() => {
      setIsDataChanged(true)    
    }, 100);

  }

  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}>
              {isLoaded &&
                  <ChartComp data={chartData} onHandleChange={onHandleChange}/>
              }
            </Grid>
            <Grid item xs={6} md={4}>
              
                <MatDataTable data={chartData}/>
              
            </Grid>
          </Grid>
        </Box>
      
      
    </div>
  );
}