React Chart.js Draggable with Material UI Table
Table of Contents
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
GitHubPrerequisites
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 UI – Version 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>
);
}
You must be logged in to post a comment.