Time Series Line Graph
A multi-metric line chart built with D3. Supports legends, grid lines, area fills, alert zones, annotations, zoom/brush selection, and real-time auto-scrolling.
Playground
onZoneChange callbacks work in your local project but not in this interactive playground due to how the editor re-renders components.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, DataPoint[]> | — | Required. Data keyed by device identifier |
metrics | MetricConfig[] | (auto-detected) | Metric definitions. See metrics |
title | string | — | Chart title |
showGrid | boolean | — | Show grid lines |
gridColor | string | — | Grid line color |
gridThickness | number | — | Grid line width |
showLegend | boolean | — | Show legend |
legendPosition | string | — | "top", "bottom", "left", "right" |
area | boolean | false | Fill area under lines |
areaColor | string | — | Area fill color |
alertZones | AlertZone[] | — | Zone bands on the chart (10% opacity) |
zoomEnabled | boolean | false | Enable zoom/brush selection |
autoScroll | boolean | false | Auto-scroll with new data |
timeWindow | number | — | Rolling time window in ms |
start | Date | number | — | Fixed start time |
end | Date | number | — | Fixed end time |
lineThickness | number | — | Line width in px |
pointSize | number | — | Data point dot radius |
annotations | Annotation[] | — | Vertical line/range annotations. See Annotations |
annotationMode | boolean | — | Enable click-to-annotate mode (cursor changes to copy) |
annotationColor | string | — | Default annotation color |
zoomColor | string | — | Zoom brush selection color |
downsample | object | — | Downsampling config for large datasets |
styles | object | — | Custom styling including dimensions — see Styles |
formatValue | function | — | Custom Y-axis formatter: (value) => string |
formatTimestamp | function | — | Custom X-axis and tooltip time formatter: (timestamp) => string |
formatLegend | function | — | Custom legend label: (device, metric) => string. See formatLegend |
renderTooltip | function | — | Custom tooltip: (dataPoint) => ReactNode. Replaces the entire default tooltip |
showLoading | boolean | true | Show skeleton while data is empty |
onHover | function | — | Called on data point hover. See onHover |
onRelease | function | — | Called when hover ends. Same signature as onHover |
onAnnotate | function | — | Called when annotation created. See onAnnotate |
onAnnotationHover | function | — | Called when hovering an annotation: (hover, annotation) => ReactNode | void |
onZoneChange | function | — | Per-series zone transitions. See onZoneChange |
onError | function | — | Called on invalid timestamps: (error) => void |
metrics
Define which metrics to display and their colors. If omitted, metrics are auto-detected from the first device's numeric data keys.
metrics={[
{ key: "temperature", color: "#f97316" },
{ key: "humidity", color: "#3b82f6" },
{ key: "pressure", color: "#22c55e" },
]}
When not provided, colors cycle through D3's schemeCategory10 palette.
Styles
The styles prop controls fonts, background, and chart dimensions.
styles={{
width: 600,
height: 400,
title: { fontSize: 16, fontWeight: 700, color: "#1e293b" },
legend: { fontSize: 12, color: "#64748b" },
tooltip: { fontSize: 12, color: "#1e293b" },
axis: { fontSize: 10, color: "#94a3b8" },
background: { color: "#ffffff" },
}}
| Field | Applies to | Properties |
|---|---|---|
width | Chart container | number | string — explicit width |
height | Chart container | number | string — explicit height |
title | Chart title | fontFamily, fontSize, fontWeight, color |
legend | Legend labels | fontFamily, fontSize, fontWeight, color |
tooltip | Tooltip text | fontFamily, fontSize, fontWeight, color |
axis | Axis labels/ticks | fontFamily, fontSize, fontWeight, color |
background | Chart background | color |
Set styles.width and styles.height to control the chart size directly. Without them, the chart fills its parent container using a ResizeObserver.
onHover
Called when the user hovers over a data point. Receives the nearest point or null when the cursor leaves.
onHover={(point, event) => {
if (point) {
console.log(point);
// { metric: "temperature", value: 22.5, timestamp: 1774690200000 }
}
}}
onAnnotate
Called when the user creates an annotation (requires annotationMode={true}). A click creates a point annotation; a drag creates a range.
annotationMode={true}
onAnnotate={(id, timestamp, type) => {
console.log(id, timestamp, type);
// id: number — auto-incrementing, shared between start_drag and end_drag
// timestamp: number — clamped to chart domain
// type: "click" | "start_drag" | "end_drag"
}}
| Type | When |
|---|---|
"click" | User clicked without dragging (drag < 10px) — creates a point annotation |
"start_drag" | User started dragging (drag >= 10px) — left edge of range |
"end_drag" | User released after dragging — right edge of range |
start_drag and end_drag share the same id. Each new interaction increments the ID.
Annotations
Add vertical markers (points) or highlighted ranges (zones) to the chart. There are two types:
Point annotation — A vertical dashed line at a specific timestamp:
{
timestamp: number;
label?: string;
color?: string; // overrides annotationColor
data?: Record<string, unknown>; // passed to onAnnotationHover
}
Range annotation — A shaded region between two timestamps:
{
start: number;
end: number;
label?: string;
color?: string;
data?: Record<string, unknown>;
}
Example with Annotations
onZoneChange
Unlike other components, the TimeSeries zone transition includes device and metric since there can be multiple series on one chart.
onZoneChange={(transition) => {
console.log(transition);
// {
// device: "sensor_01",
// metric: "temperature",
// previousZone: { min: 30, max: 70, color: "#22c55e" },
// currentZone: { min: 70, max: 100, color: "#ef4444" },
// value: 72
// }
}}
Does not fire on first render — only on subsequent zone transitions.
X-Domain (Time Range)
The chart resolves which time range to show in this priority order:
- Zoom selection — when user brush-selects a range
start+endprops — fixed time rangetimeWindow— rolling window:[now - timeWindow, now]- Data extent — automatically fits all data points
Zoom
Enable with zoomEnabled={true}. Click and drag to select a time range. The chart rescales both X and Y axes to fit the selected data. Cursor changes to crosshair.
<TimeSeries
data={data}
metrics={metrics}
zoomEnabled={true}
zoomColor="#3b82f6"
/>
Zoom is disabled while annotationMode is active.
Auto-Scroll
When timeWindow is set and autoScroll is not false, the chart automatically scrolls to keep the latest data visible. The right edge advances as new data arrives, clamped to 1 second past the latest data point.
<TimeSeries
data={data}
metrics={metrics}
timeWindow={60000}
autoScroll={true}
/>
formatLegend
Customize legend labels. By default:
- Single device: shows the metric name
- Multiple devices: shows
[device]: metric
formatLegend={(device, metric) => `${device} / ${metric}`}
Legend items are clickable — click to solo a series, click again to restore all.
With Hooks
import { useRelayTimeSeries, TimeSeries } from "@relay-x/ui";
function TemperatureChart() {
const { data, isLoading } = useRelayTimeSeries({
deviceIdent: "sensor_01",
metrics: ["temperature", "humidity"],
mode: "both",
timeRange: { start: Date.now() - 3600000, end: Date.now() },
});
return (
<TimeSeries
data={{ sensor_01: data }}
metrics={[
{ key: "temperature", color: "#f97316" },
{ key: "humidity", color: "#3b82f6" },
]}
showGrid
showLegend
autoScroll
showLoading={isLoading}
styles={{ width: 600, height: 400 }}
/>
);
}