frontend
How to Cross Link Plotly.js Charts in a React App
A simple custom implementation to cross link JavaScript Plotly charts.
Introduction
Plotly is a powerful, highly customizable set of open source, browser-based graphing libraries for JavaScript and Python. It's used by the likes of companies as big as Intuit and Grafana, so when I needed to build a data visualization heavy dashboard for a project at work, I looked to Plotly for help with the charts, graphs, and maps the app required.
Especially appealing to me is the fact that not only does Plotly have a JavaScript version of its library, but it also has a React-specific version, which is one of the JavaScript frameworks I use most day-to-day.
Unfortunately, although Plotly has implementations in multiple languages, they don't have complete feature parity. The feature I was most interested in was the ability to zoom in on a section of one timeseries-based chart, and have any other related charts on the page automatically show the same time selection - a functionality known as cross linking or cross filtering. The Python version of Plotly has the ability to cross link charts right out of the box, but the JavaScript and React versions do not.
Lucky for me, I'm a web developer, and if something's not already available via a library, I'll figure out a way to build it myself.
This article will demonstrate how you can to make your own cross linked Plotly.js charts inside of a Next.js application by using a shared parent component to manage the date ranges.
Here's what the cross linked charts look like and a link to a live demo in CodeSandbox you can interact with.
Install Plotly.js and React-Plotly.js
To use Plotly data visualizations inside of a React-based application, you'll need to install the Plotly.js and React-Plotly.js libraries first.
Run this command in the terminal in the root of your project to add them both.
npm install react-plotly.js plotly.js
As I said earlier, Plotly is a flexible, well documented, open source data visualization library, and the amount of customization it offers for even very complex charts and graphs makes it a good choice for dashboards that need to display this type of information.
Create a Reusable Chart Component
The first thing to do after installing the Plotly libraries inside of a React-based application is to create a reusable chart component that can display different values passed to it from the parent component.
For me, a Plotly line chart was one of the chart types that made the most sense to have on hand, but this sort of reusable React component structure should translate pretty well to many different Plotly chart types.
Here is the full code for my <PlotlyLineChart/>
component, I'll go through each of the pieces in it below.
NOTE: All of my JavaScript-related code is actually written in TypeScript, so if you prefer to use JavaScript, just know that it can be fairly easily adapted to plain JS by removing things like types from the components and variable declarations.
import dynamic from "next/dynamic";
const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
const PlotlyLineChart = ({
title,
data,
property,
displayValue,
startDate,
endDate,
setDate,
maxStartDate,
maxEndDate,
}: {
title: string;
data: any[];
property: string;
displayValue?: string;
startDate: Date;
endDate: Date;
setDate: ({ startDate, endDate }: { startDate: Date; endDate: Date }) => void;
maxStartDate: Date;
maxEndDate: Date;
}) => {
const layout = {
width: 640,
height: 480,
title: title,
xaxis: {
autorange: false, // determines whether or not the range of the axis is computed in relation to the input data
range: [startDate, endDate],
rangeselector: {
buttons: [
{
step: "day" as "day",
stepmode: "backward" as "backward",
count: 1,
label: "1d",
},
{
step: "day" as "day",
stepmode: "backward" as "backward",
count: 2,
label: "2d",
},
{
step: "week" as "week",
stepmode: "backward" as "backward",
count: 1,
label: "1w",
},
{
step: "month" as "month",
stepmode: "backward" as "backward",
count: 1,
label: "1m",
},
{
step: "year" as "year",
stepmode: "backward" as "backward",
count: 1,
label: "1y",
},
{
step: "all" as "all",
},
],
},
rangeslider: { range: [maxStartDate, maxEndDate] as [Date, Date] },
type: "date" as "date",
},
yaxis: { title: displayValue },
};
const prepareChartData = () => {
if (data.length === 0) return [];
// loop through each array of device events and create a trace for each device
const traces = data.map((device: any) => {
return {
type: "scatter",
mode: "lines+markers",
name: device[0].device,
x: device.map((event: { when: number }) => new Date(event.when * 1000)),
y: device.map(
(event: { [x: string]: number }) =>
Math.round(event[property] * 10) / 10
),
};
});
return traces;
};
const handleRelayout = (event: any) => {
if (event["xaxis.autorange"]) {
setDate({ startDate: maxStartDate, endDate: maxEndDate });
} else {
let rangeLeft = event["xaxis.range[0]"] || event["xaxis.range"][0];
let rangeRight = event["xaxis.range[1]"] || event["xaxis.range"][1];
if (rangeLeft && rangeRight) {
setDate({
startDate: rangeLeft,
endDate: rangeRight,
});
}
}
};
return (
<Plot
data={prepareChartData()}
layout={layout}
onRelayout={handleRelayout}
/>
);
};
export default PlotlyLineChart;
There is a lot of code here, I know, but it's not actually as complicated as it might seem at first.
First: the many props being passed to the component, which serve to make it reusable in the app for many different line charts.
title
- the title text to display above the line chartdata
- the list of objects and their values to display inside of the chartproperty
- the name of the property inside of each object to get the value for and display in the graph (for instance, the property ofvoltage
to show device voltage over time)displayValue
- the title to display on the Y-axis of the graph (the X-axis will always show dates)startDate
- the currently selected beginning of the date rangeendDate
- the currently selected end date of the date rangesetDate
- a function to update the currentstartDate
andendDate
variablesmaxStartDate
- the earliest date timestamp based on the data present in the parent componentmaxEndDate
- the latest date timestamp based on the data present in the parent component
Next is the layout
object, which is mostly boilerplate for the Plotly component. The layout takes in some of the props like title
, the range
for the x-axis (startDate
and endDate
), the maximum date range of maxStartDate
and maxEndDate
for the rangeslider
(the mini version of the line chart underneath the main chart), and the displayValue
string for the y-axis.
The rangeselector
code which takes up a fair amount of space is actually just directions for a series of buttons that allow users to quickly choose different time ranges to view in the chart. It can accept values like "month", "year", "day", "hour", "minute", or "second".
The prepareChartData()
function is where the array of data passed to the chart is transformed into "traces" that the Plotly chart requires. For my scenario, each item in the array has a device
property which is a unique ID, a when
property that is a timestamp for the x-axis of the graph in Unix time, and whatever numeric value the property
prop passed into the component matches. The appropriate data is pulled from each item and the final list of traces
data ends up being organized by device ID.
Finally, the handleRelayout()
function helps the chart's parent component knows when a user zooms in or out on a particular section of a chart. It uses the setDate()
function prop passed from the parent component to ensure that the parent's view of the currently selected dates stays accurate. This is one of the keys to cross linking multiple charts in the project.
These pieces of set up code: the layout
variable, prepareChartData()
, and handleRelayout()
are all passed to the <Plot/>
component, and then it can render a JavaScript version of the appropriate data visualization.
Ok, that's enough time spent on this reusable chart component. Let's move on to tracking the date state next.
Leverage React's useState() to track date selections in the charts
The other key to ensuring that all Plotly charts in a React application are showing the same selected date range relies on React's useState()
hook in a shared parent component. For my particular Next.js application, it made sense for me to import my <PlotlyLineChart/>
component directly into my pages/index.tsx
file, so when you view the code below, you'll see a few placeholders for the Next pages boilerplate code mixed in with the code specific to cross linking the charts.
import { useState, useEffect } from "react";
import dayjs from "dayjs";
import {
convertObjectToArray,
groupEventsByDevice,
} from "@/components/utils/helpers";
import PlotlyLineChart from "@/components/PlotlyLineChart";
type HomeProps = {
data: any;
};
export default function Home({ data }: HomeProps) {
const rawData = data;
const [initialDate, setInitialDateRange] = useState({
startDate: dayjs(),
endDate: dayjs(),
});
const [selectedDate, setSelectedDateRange] = useState({
startDate: dayjs(),
endDate: dayjs(),
});
const [devices, setDevices] = useState<any>([]);
const [mostActiveDevices, setMostActiveDevices] = useState<any>([]);
useEffect(() => {
if (rawData.length > 0) {
getInitialDateRange(rawData);
}
}, [rawData]);
function getInitialDateRange(data: any[]) {
data.sort((a, b) => {
return Number(new Date(a.when * 1000)) - Number(new Date(b.when * 1000));
});
// set initial start date and end date for charts
const startDate = new Date(data[0].when * 1000);
const endDate = new Date(data[data.length - 1].when * 1000);
setInitialDateRange({ startDate: startDate, endDate: endDate });
setSelectedDateRange({ startDate: startDate, endDate: endDate });
}
// group all the events together by device ID
useEffect(() => {
const groupedEventsByDevice = groupEventsByDevice(rawData);
const deviceList = convertObjectToArray(groupedEventsByDevice);
setDevices(deviceList);
}, [rawData, selectedDate]);
// set the 5 most active devices
useEffect(() => {
if (devices.length > 0) {
setMostActiveDevices(
devices.sort((a, b) => b.length - a.length).slice(0, 5)
);
}
}, [devices]);
return (
<>
{/* Next.js head boilerplate */}
<main>
<div>
{/* page tile and date range display components */}
<div>
<PlotlyLineChart
title={"Most Active Device Temperature"}
data={mostActiveDevices}
property={"temp"}
displayValue="Celsius"
startDate={selectedDate.startDate}
endDate={selectedDate.endDate}
setDate={setSelectedDateRange}
maxStartDate={initialDate.startDate}
maxEndDate={initialDate.endDate}
/>
<PlotlyLineChart
title={"Most Active Device Voltage"}
data={mostActiveDevices}
property={"voltage"}
displayValue="Voltage"
startDate={selectedDate.startDate}
endDate={selectedDate.endDate}
setDate={setSelectedDateRange}
maxStartDate={initialDate.startDate}
maxEndDate={initialDate.endDate}
/>
<PlotlyLineChart
title={"Most Active Device RSSI"}
data={mostActiveDevices}
property={"rssi"}
displayValue={""}
startDate={selectedDate.startDate}
endDate={selectedDate.endDate}
setDate={setSelectedDateRange}
maxStartDate={initialDate.startDate}
maxEndDate={initialDate.endDate}
/>
</div>
</div>
</main>
</>
);
}
export async function getStaticProps() {
{/* fetch data to display in charts here */}
return {
props: {
data,
},
};
}
As I mentioned, I put in some placeholders for code that's not really relevant to the cross linking, so bits like data fetching, CSS styling, and some of the other HTML elements like the date range display components are omitted to keep the code cleaner. If you'd like to see the full code, you can check out this CodeSandbox demo.
Let's discuss some of the variables being tracked in this <Home/>
component.
initialDate
- the maximum starting and ending date range for the charts (determined by the data fetched from the server)selectedDate
- the user selected starting and ending date range being viewed on the charts at any time (on page load these dates are the same as theinitialDate
object's values)devices
- after data is fetched from the server, it's grouped by device IDsmostActiveDevices
- once the data is grouped by device ID, the devices are sorted by how many data events each one has associated with it, and the top 5 devices are passed to the chart components
Once the data is fetched from the server (in this case, I'm using a CSV of air quality data produced by Blues Airnote devices located all around the world), the <Home/>
component uses that data to determine the starting and ending date ranges and sets the initialDate
and selectedDate
objects via the getInitialDates()
function. Each row of data from the CSV has a Unix timestamp associated with it, hence the date manipulations inside the function. These date objects will then get passed to the <PlotlyLineChart/>
components further down in the code.
After the first useEffect()
that sets the dates, a second useEffect()
is called to group all of the data from the CSV by device ID (each row of data from the server has a device ID included in it), and the devices
variable is set.
And last, the devices
array sorts all the objects in it by how many events each device has, and the top 5 devices are chosen to be set as the mostActiveDevices
, which will then be passed to the <PlotlyLineChart/>
component as well.
Now all the data needed by the chart components is ready to be supplied to them. The initialDate
and selectedDate
data gets passed in as startDate/endDate
and maxStartDate/maxEndDate
. The setSelectedDate()
function gets passed to the setDate()
function within the component. The mostActiveDevices
list is passed in as the data
for the chart to render. And the title
, property
, and displayValue
are all strings passed to each chart. For the data I was using, I had values like temp
, voltage
, and rssi
(received signal strength indicator - used to describe wireless signal strength) available to me, so I chose to display those in the line charts.
And that's basically all you need to do cross link your charts in your React application.
The thing to understand about this component is that it controls the state for the dates that gets passed to all the chart components. As the selectedDate
is updated via setSelectedDate()
by the user interacting with any of the charts, it sends the new selectedDate
state down to all the charts to display.
Test out the cross linked charts
If you'd like to see a working demo of these cross linked Plotly charts in action, I've got a CodeSandbox for you to try out.
Open a terminal and type npm run dev
to get it started.
In addition to the <PlotlyLineChart/>
component, I also included a couple of Ant Design date picker components to display the entire date range of the data from the CSV (July 16, 2024 to July 18, 2024).
Feel free to zoom on in on different parts of different charts or on the range sliders below the charts, or click the range selector buttons above each chart. Every time you interact with one chart, all the other charts should update accordingly. Pretty cool!
Conclusion
When I saw how the Python implementation of Plotly cross linked charts so when a user interacted with one on the page all of them adjusted, I knew I wanted that same functionality in my JavaScript application. Unfortunately, Plotly didn't have feature in the React version of the library, so I had to build it myself.
It turned out to be fairly simple just by having a parent component keep track of the selected date range that was passed to all the chart components, and reacting accordingly when a user updated the date range in one chart by passing that same date range to all the charts.
Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.
Thanks for reading. I hope if you want to cross link multiple charts inside of your own dashboard (whether they're Plotly.js charts or another data viz library) you'll consider giving this solution a try and just lifting the date state up one level so it can be passed to all the charts at once. Enjoy!
References & Further Resources
Want to be notified first when I publish new content? Subscribe to my newsletter.