react-timeseries-charts
From Zero to Dashboard
Everything you need to install, configure, customize, and ship
production-ready React time series visualizations —
without spending three hours reading sparse documentation.
📌 Semantic Keyword Core
React time series
React time series library
react-timeseries-charts installation
react-timeseries-charts setup
react-timeseries-charts getting started
react-timeseries-charts tutorial
react-timeseries-charts example
react-timeseries-charts dashboard
react-timeseries-charts customization
React time-based charts
React temporal charts
React time data charts
React time series visualization
React chart component
ESnet Pond.js TimeSeries
D3 React time charts
real-time data visualization React
interactive time series React
time series data React
Time series data is everywhere. Network traffic, server CPU metrics, stock prices,
IoT sensor readings, user activity heatmaps — all of it is timestamped, sequential,
and deeply unpleasant to visualize with a generic bar chart library that was clearly
designed for quarterly sales reports. If you’ve ever tried to force Recharts
or Victory into handling irregular time intervals, pan-and-zoom brushing,
and multi-axis overlays simultaneously, you already know the pain.
Enter react-timeseries-charts —
an open-source React time series library developed by ESnet (the
high-performance network connecting US research institutions). It was built precisely
for this class of problem: temporal data at scale, with interactive exploration,
annotation support, and a composable chart architecture that actually makes sense.
Under the hood it leans on D3 for rendering and Pond.js
for the immutable time series data model, which means you get serious computational
muscle without reinventing the wheel.
This guide walks you through everything: installation,
project setup, core concepts,
real-world examples,
customization, and
building a dashboard.
By the end you’ll have a working, styled, interactive
React time data chart in production — not just a CodeSandbox toy.
Why react-timeseries-charts Over the Alternatives
The React charting ecosystem is crowded. Recharts is beginner-friendly
and has excellent documentation. Victory is composable and testable.
Nivo is beautiful out of the box. Chart.js via
react-chartjs-2 is familiar to anyone who’s touched JavaScript in the last decade.
So why choose react-timeseries-charts?
The answer is specificity. Every one of those libraries treats time as just another
dimension on a linear axis. react-timeseries-charts treats time as
the primary organizing principle of its entire data model. The
TimeSeries object from Pond.js carries metadata about time zones,
supports both regular and irregular intervals natively, handles missing data
gracefully, and provides built-in aggregation, rollup, and slicing methods.
You’re not manually computing axis tick positions or formatting UTC offsets — the
library handles that contract for you.
The second differentiator is the synchronized multi-chart architecture. The
ChartContainer and ChartRow components share a common
time axis, which means stacking a network traffic chart above a latency chart
above a packet-loss chart — all synchronized on the same time range with a shared
brush selector — takes about thirty lines of JSX, not three hundred lines of
manual D3 event delegation. For anyone building an operational
dashboard with React temporal charts, this alone is
worth the library-specific learning curve.
react-timeseries-charts is mature but not under active daily development.
It targets React 16–18 and works well in production. Check the GitHub issues
before starting if you need a specific niche feature — community PRs are the
path forward for new capabilities.
Installation and Environment Setup
The react-timeseries-charts installation
involves two packages, not one. The charting library itself is a thin
declarative layer; the Pond.js library (pondjs)
provides the core TimeSeries, TimeRange, and
Event data structures that the chart components consume as props.
Attempting to use one without the other produces cryptic runtime errors that
will send you to Stack Overflow for no good reason.
# npm npm install react-timeseries-charts pondjs --save # yarn yarn add react-timeseries-charts pondjs # pnpm pnpm add react-timeseries-charts pondjs
After installing, import the base stylesheet. The library ships its own CSS
for default chart chrome — track backgrounds, grid lines, crosshair cursors,
and tooltip positioning. Skipping this import won’t break your charts, but
they’ll look like they were designed in 2003 and hosted on GeoCities.
// Import the default styles once, globally import "react-timeseries-charts/lib/css/style.css";
If your project uses Webpack 5 or Vite and throws module resolution errors
about ESM vs CJS boundaries, add the
pondjs package to your bundler’s transpilation list. For Vite,
include it in optimizeDeps.include inside vite.config.js.
For Create React App, if you haven’t ejected, a CRACO override or a
react-app-rewired config is your cleanest path. These are
one-time configuration steps — not ongoing maintenance headaches.
react-timeseries-charts expects
react and react-dom≥16.8 and uses internal D3 v4/v5 via bundled dependencies. Do not install
D3 separately unless you need it for other purposes — version conflicts
between D3 builds will produce silent rendering bugs.
Core Concepts: TimeSeries, ChartContainer, and ChartRow
Before writing a single line of JSX, you need to internalize the Pond.js
TimeSeries data model. A TimeSeries is an immutable,
ordered collection of Event objects, each carrying a timestamp
(or time range) and a set of named, typed values. You construct it from
a plain JavaScript object with a name, columns array,
and a points array of [timestamp, ...values] tuples.
This is the only format the chart components understand — passing raw
arrays or objects directly will get you a blank chart and a confusing prop-types
warning.
import { TimeSeries } from "pondjs"; const cpuData = { name: "cpu_usage", columns: ["time", "usage"], points: [ [new Date("2024-01-01T00:00:00Z").getTime(), 12.4], [new Date("2024-01-01T00:05:00Z").getTime(), 15.1], [new Date("2024-01-01T00:10:00Z").getTime(), 9.8], [new Date("2024-01-01T00:15:00Z").getTime(), 22.6], // ...more data points ], }; export const cpuTimeSeries = new TimeSeries(cpuData);
With your TimeSeries in hand, the chart architecture follows
a container–row–chart hierarchy. <ChartContainer> owns
the time axis and the overall width. <ChartRow> defines
a horizontal band with its own Y-axis and height. <Charts>
is a grouping wrapper inside a row, and the actual visual primitives —
<LineChart>, <AreaChart>,
<BarChart>, <ScatterChart> — live
inside it. This hierarchy is rigid by design: breaking out of it means
fighting the library instead of using it.
import React from "react"; import { ChartContainer, ChartRow, Charts, LineChart, YAxis, Resizable } from "react-timeseries-charts"; import { TimeRange } from "pondjs"; import { cpuTimeSeries } from "./data"; const timeRange = cpuTimeSeries.timerange(); export default function CpuChart() { return ( <Resizable> <ChartContainer timeRange={timeRange} format="HH:mm" enablePanZoom={true} > <ChartRow height="200"> <YAxis id="cpu" label="CPU %" min={0} max={100} width="60" type="linear" /> <Charts> <LineChart axis="cpu" series={cpuTimeSeries} column={["usage"]} /> </Charts> </ChartRow> </ChartContainer> </Resizable> ); }
The <Resizable> wrapper is the unsung hero here.
Without it, you must hardcode a pixel width for ChartContainer.
Resizable uses a ResizeObserver internally to keep the chart
fluid — essential for any responsive React chart component
inside a flexbox or CSS Grid layout. Wrap every top-level chart with it and
you’ll never manually compute container widths again.
A Real-World Example: Network Monitoring Dashboard
Let’s build something that would actually survive a code review: a
network monitoring panel showing inbound and outbound traffic on two
ChartRows, synchronized on the same time axis, with a
brush-selector that lets the user zoom into a sub-range. This is the
canonical react-timeseries-charts example — the kind ESnet
themselves run in production to monitor transcontinental fiber links.
import React, { useState, useCallback } from "react"; import { ChartContainer, ChartRow, Charts, AreaChart, LineChart, YAxis, Resizable, Brush } from "react-timeseries-charts"; import { TimeSeries, TimeRange } from "pondjs"; // ── Simulated 24h traffic data (1-min intervals) ── const generatePoints = (hours = 24) => { const points = []; const base = new Date("2024-01-01T00:00:00Z").getTime(); for (let i = 0; i < hours * 60; i++) { points.push([ base + i * 60000, parseFloat((Math.random() * 900 + 100).toFixed(2)), parseFloat((Math.random() * 400 + 50).toFixed(2)), ]); } return points; }; const trafficSeries = new TimeSeries({ name: "network_traffic", columns: ["time", "in", "out"], points: generatePoints(), }); export default function NetworkDashboard() { const fullRange = trafficSeries.timerange(); const [selectedRange, setSelectedRange] = useState(fullRange); const [brushRange, setBrushRange] = useState(null); const handleTimeRangeChange = useCallback( (tr) => setSelectedRange(tr), [] ); return ( <div style={{ padding: "24px" }}> {/* ── Main Chart: Inbound ── */} <Resizable> <ChartContainer timeRange={selectedRange} onTimeRangeChanged={handleTimeRangeChange} enablePanZoom={true} format="%H:%M" > {/* Row 1: Inbound traffic */} <ChartRow height="200"> <YAxis id="in" label="Inbound Mbps" min={0} max={1200} width="70" /> <Charts> <AreaChart axis="in" series={trafficSeries} columns={{ up: ["in"], down: [] }} style={{ in: { up: [{ fill: "#539afa", opacity: 0.3, stroke: "#0f3460", strokeWidth: 1.5 }] } }} /> </Charts> </ChartRow> {/* Row 2: Outbound traffic */} <ChartRow height="200"> <YAxis id="out" label="Outbound Mbps" min={0} max={600} width="70" /> <Charts> <LineChart axis="out" series={trafficSeries} column={["out"]} style={{ out: { stroke: "#f59e0b", strokeWidth: 2 } }} /> </Charts> </ChartRow> {/* Row 3: Brush selector (navigator) */} <ChartRow height="40"> <Brush timeRange={brushRange || fullRange} onTimeRangeChanged={(tr) => { setBrushRange(tr); setSelectedRange(tr); }} /> </ChartRow> </ChartContainer> </Resizable> </div> ); }
The key insight in this pattern is that selectedRange state
is the single source of truth for what time window both rows display.
When the user pans or zooms on either row, onTimeRangeChanged
fires and updates that state, causing both rows to re-render in sync.
The brush row at the bottom acts as a global navigator — drag its handles
to select a sub-range, and the upper charts zoom in accordingly. This is
a complete, interactive React time series dashboard pattern
that you can drop into any monitoring application.
Notice the AreaChart columns prop structure:
{ up: ["in"], down: [] }. This reflects the library’s
convention for bidirectional area charts (think traffic in/out with a shared
baseline). For a simple filled area, just populate up and leave
down empty. It’s a slightly surprising API until you’ve seen it
once — after that it clicks immediately.
Customization: Styles, Tooltips, and Event Markers
Out-of-the-box styling in react-timeseries-charts is functional
but deliberately minimal. The library intentionally avoids opinionated color
palettes so it doesn’t clash with your design system. All visual customization
flows through the style prop, which accepts an object keyed by
column name with sub-keys for different rendering states
(normal, highlighted, selected,
muted). This four-state model is powerful because it enables
interactive highlighting without any additional event-handling code on your part.
const lineStyle = { usage: { normal: { stroke: "#539afa", strokeWidth: 2, opacity: 1 }, highlighted: { stroke: "#ffffff", strokeWidth: 3, opacity: 1 }, selected: { stroke: "#f59e0b", strokeWidth: 2, opacity: 1 }, muted: { stroke: "#539afa", strokeWidth: 1, opacity: 0.3 }, }, }; // Pass it to any chart primitive: <LineChart axis="cpu" series={cpuTimeSeries} column={["usage"]} style={lineStyle} highlight={highlight} onHighlightChange={(h) => setHighlight(h)} />
Tooltips are handled through the <EventMarker> component,
which you position based on mouse-over events from the chart. The pattern
involves tracking a tracker state (the timestamp under the cursor)
and a trackerValue (the interpolated series value at that time).
ChartContainer exposes onTrackerChanged for this purpose.
You then render <EventMarker> conditionally inside your
Charts group to show a vertical line and a value label wherever
the user is hovering. It’s more verbose than a tooltip={true}
boolean, but the explicit API means you can render anything in the tooltip —
formatted numbers, delta comparisons, external data lookups, whatever you need.
const [tracker, setTracker] = useState(null); // Inside ChartContainer: <ChartContainer timeRange={timeRange} onTrackerChanged={(t) => setTracker(t)} trackerPosition={tracker} > <ChartRow height="200"> <YAxis id="cpu" min={0} max={100} width="60" /> <Charts> <LineChart axis="cpu" series={cpuTimeSeries} /> {tracker && ( <EventMarker type="flag" axis="cpu" event={cpuTimeSeries.atTime(tracker)} column="usage" info={[{ label: "CPU", value: `${val}%` }]} infoWidth={120} markerRadius={3} markerStyle={{ fill: "#f59e0b" }} /> )} </Charts> </ChartRow> </ChartContainer>
For baseline annotations, threshold markers, or event flags (deployments,
incidents, maintenance windows), the <Baseline> and
<TimeMarker> components slot directly into
<Charts> alongside your data series. A
<Baseline> at value={95} on your CPU axis
renders a labeled horizontal dashed line — an instant visual boundary for
your SLA threshold, zero extra math required. These small ergonomic wins
are where react-timeseries-charts customization quietly
earns its keep compared to general-purpose React chart components.
Key Props Reference
| Prop | Type | Component | Description |
|---|---|---|---|
| timeRange | TimeRange | ChartContainer | Visible time window. Drive with state for pan/zoom. |
| enablePanZoom | bool | ChartContainer | Enables drag-to-pan and scroll-to-zoom interactions. |
| onTimeRangeChanged | func | ChartContainer | Fires with new TimeRange on pan/zoom. Update state here. |
| onTrackerChanged | func | ChartContainer | Fires with timestamp on mouse move. Use for tooltips. |
| series | TimeSeries | LineChart, AreaChart, etc. | The Pond.js TimeSeries data source. |
| column | string[] | LineChart | Column names from the TimeSeries to render. |
| style | object | All chart primitives | Keyed by column name, sub-keyed by interaction state. |
| interpolation | string | LineChart, AreaChart | «linear», «curveBasis», «curveStep», «curveMonotoneX». |
| min / max | number | YAxis | Fixed Y-axis domain bounds. |
| height | string | ChartRow | Row height in pixels (string). Multiple rows stack vertically. |
Building a Production Dashboard
A single-chart demo is fine for learning. A production
react-timeseries-charts dashboard introduces three concerns
that demos never address: data loading state, error boundaries, and
performance with large datasets. Let’s handle all three systematically.
For data loading, wrap your chart in a component that tracks
isLoading and error states separately from the
series state. During loading, render a skeleton placeholder
at the same pixel dimensions as the chart — this prevents layout shift
when the chart mounts. A simple approach is a div with
a background: linear-gradient shimmer animation at
height: 200px. Using Suspense with lazy-loaded chart components
also works well in React 18 if your data fetching uses a Suspense-compatible
library like SWR or React Query.
Performance is the more interesting challenge. TimeSeries
objects are immutable, which means every update creates a new instance.
If you’re polling an API every 5 seconds and naively constructing a new
TimeSeries from the full dataset each time, you’re doing
unnecessary work. The correct pattern is to keep your raw points array
in a useRef, append new points to it, and then reconstruct
the TimeSeries only for state updates — or use
useMemo with the raw array as a dependency.
For datasets exceeding ~50,000 points, consider using Pond.js’s built-in
TimeSeries.prototype.crop() and
TimeSeries.prototype.fixedWindowRollup() methods to
downsample before rendering. The chart doesn’t need 50,000 pixel-accurate
points if the canvas is 1,200 pixels wide.
Memoize your
TimeSeries construction with useMemoand your style objects with
useMemo or as module-level constants.Both are structural props that React’s shallow comparison will treat as changed
if recreated on every render, triggering unnecessary D3 re-computations.
For error boundaries, wrap the entire chart section in a React
ErrorBoundary class component. D3 rendering errors inside
react-timeseries-charts don’t always propagate cleanly to the React error
event system — an uncaught error inside a D3 selection callback will
occasionally crash the entire component tree silently in development builds.
An error boundary catches these, renders a fallback UI
(«Chart unavailable — try refreshing»), and logs the error to your
observability stack. This is not optional for production systems where
your on-call engineer needs to know what failed at 3 AM.
Handling Real-Time Streaming Data
One of the most common questions about React time series
visualization libraries is whether they can handle real-time
data from WebSockets or Server-Sent Events. The answer for
react-timeseries-charts is yes, with a clear pattern. Because
TimeSeries is immutable, you maintain a mutable buffer of
raw data points in a ref, append incoming events to it, and then
replace the state-held TimeSeries on each update. The
chart re-renders only when the TimeSeries prop reference
changes — which it will, because you’re creating a new instance —
but D3 is smart enough to diff the rendered paths and update
incrementally rather than redrawing from scratch.
import { useEffect, useRef, useState, useMemo } from "react"; import { TimeSeries } from "pondjs"; const MAX_POINTS = 500; // rolling window export function useRealtimeSeries(wsUrl) { const bufferRef = useRef([]); const [points, setPoints] = useState([]); useEffect(() => { const ws = new WebSocket(wsUrl); let rafId; ws.onmessage = (evt) => { const { timestamp, value } = JSON.parse(evt.data); bufferRef.current.push([timestamp, value]); // Trim to rolling window if (bufferRef.current.length > MAX_POINTS) { bufferRef.current.shift(); } // Batch DOM updates via rAF cancelAnimationFrame(rafId); rafId = requestAnimationFrame(() => { setPoints([...bufferRef.current]); }); }; return () => { ws.close(); cancelAnimationFrame(rafId); }; }, [wsUrl]); const series = useMemo(() => { if (!points.length) return null; return new TimeSeries({ name: "realtime", columns: ["time", "value"], points: points, }); }, [points]); return series; }
The requestAnimationFrame batching is critical for
high-frequency streams. Without it, a WebSocket sending 100 messages
per second will trigger 100 state updates per second, which will
schedule 100 re-renders per second. With rAF debouncing,
you’re capped at the browser’s refresh rate (60–120fps) and React can
batch the state updates efficiently. Your chart will feel fluid rather
than jittery, and your CPU usage will stay reasonable.
For SSE (Server-Sent Events), the pattern is identical — replace
new WebSocket(url) with
new EventSource(url) and
ws.onmessage with es.onmessage.
SSE is often preferable for one-directional server-push data because
it uses standard HTTP, survives proxy caching layers gracefully,
and reconnects automatically on network interruption without any
client-side reconnection logic.
Pre-Launch Checklist for Any react-timeseries-charts Integration
Before shipping your React time series visualization
to production, run through these verification points. Most of them
cost five minutes to check and each one has burned someone badly
at some point in their career.
- TimeSeries immutability: Confirm you’re not mutating the
pointsarray in place. Create new arrays, newTimeSeriesobjects. The library assumes immutability and will not re-render if the reference is unchanged. - TimeRange validity: Ensure the
timeRangeprop’s begin is strictly less than its end. An invalid range causes a silent render abort with no console error — deeply frustrating to debug. - CSS import: Confirm the base stylesheet is imported once, globally, not inside a component that might unmount.
- Resizable wrapper: Every top-level chart should be wrapped in
<Resizable>. Missing this on a responsive layout will cause the chart to overflow its container or render at 0px width. - Error boundary: Wrap the entire chart section. Test it by temporarily passing an invalid
TimeSeriesand confirm the boundary catches it. - Accessibility: react-timeseries-charts renders SVG without ARIA attributes. Add
role="img"andaria-labelto the container div, and consider a visually-hidden data table as an accessible alternative.
🚀 The bottom line
react-timeseries-charts is the right choice when your
data is fundamentally temporal and you need synchronized multi-row charts,
brush navigation, real-time updates, or precise time-axis control.
It’s not the right choice if you need a quick bar chart for a marketing
dashboard — Recharts will get you there faster. Know your use case,
choose accordingly, and you’ll spend your time building features instead
of fighting rendering edge cases.
Frequently Asked Questions
npm install react-timeseries-charts pondjs --save.Both packages are required:
pondjs provides theTimeSeries and TimeRange data structuresthat the chart components consume as props. After installing,
add
import "react-timeseries-charts/lib/css/style.css"once in your root file (
index.js or App.jsx)to load the base chart styles. For Vite projects, add
pondjs to optimizeDeps.include invite.config.js if you encounter ESM resolution errors.
data with native support for irregular time intervals, synchronized
multi-row charts, brush-based zooming, and a rich immutable data model
via Pond.js. Recharts is more general-purpose —
easier to get started with, better documented, and more widely adopted —
but requires significant custom code to replicate features like
cross-chart synchronization, tracker-based tooltips, or event annotations.
If time is the primary dimension of your data, react-timeseries-charts
wins on precision and built-in temporal ergonomics. For simpler charts
where a time axis is just one of many options, Recharts is faster to ship.
raw data points in a
useRef, append incoming events froma WebSocket or EventSource, batch state updates using
requestAnimationFrame to cap re-renders at the displayrefresh rate, and reconstruct the
TimeSeries object insidea
useMemo keyed on the points array. For high-frequencystreams, limit the buffer to a fixed window (e.g., 500 points) using
Array.prototype.shift() to drop the oldest point on eachappend. This keeps memory stable and rendering fast regardless of
how long the stream runs.
