Skip to main content

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

info

onZoneChange callbacks work in your local project but not in this interactive playground due to how the editor re-renders components.

Props

PropTypeDefaultDescription
dataRecord<string, DataPoint[]>Required. Data keyed by device identifier
metricsMetricConfig[](auto-detected)Metric definitions. See metrics
titlestringChart title
showGridbooleanShow grid lines
gridColorstringGrid line color
gridThicknessnumberGrid line width
showLegendbooleanShow legend
legendPositionstring"top", "bottom", "left", "right"
areabooleanfalseFill area under lines
areaColorstringArea fill color
alertZonesAlertZone[]Zone bands on the chart (10% opacity)
zoomEnabledbooleanfalseEnable zoom/brush selection
autoScrollbooleanfalseAuto-scroll with new data
timeWindownumberRolling time window in ms
startDate | numberFixed start time
endDate | numberFixed end time
lineThicknessnumberLine width in px
pointSizenumberData point dot radius
annotationsAnnotation[]Vertical line/range annotations. See Annotations
annotationModebooleanEnable click-to-annotate mode (cursor changes to copy)
annotationColorstringDefault annotation color
zoomColorstringZoom brush selection color
downsampleobjectDownsampling config for large datasets
stylesobjectCustom styling including dimensions — see Styles
formatValuefunctionCustom Y-axis formatter: (value) => string
formatTimestampfunctionCustom X-axis and tooltip time formatter: (timestamp) => string
formatLegendfunctionCustom legend label: (device, metric) => string. See formatLegend
renderTooltipfunctionCustom tooltip: (dataPoint) => ReactNode. Replaces the entire default tooltip
showLoadingbooleantrueShow skeleton while data is empty
onHoverfunctionCalled on data point hover. See onHover
onReleasefunctionCalled when hover ends. Same signature as onHover
onAnnotatefunctionCalled when annotation created. See onAnnotate
onAnnotationHoverfunctionCalled when hovering an annotation: (hover, annotation) => ReactNode | void
onZoneChangefunctionPer-series zone transitions. See onZoneChange
onErrorfunctionCalled 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" },
}}
FieldApplies toProperties
widthChart containernumber | string — explicit width
heightChart containernumber | string — explicit height
titleChart titlefontFamily, fontSize, fontWeight, color
legendLegend labelsfontFamily, fontSize, fontWeight, color
tooltipTooltip textfontFamily, fontSize, fontWeight, color
axisAxis labels/ticksfontFamily, fontSize, fontWeight, color
backgroundChart backgroundcolor
tip

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"
}}
TypeWhen
"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:

  1. Zoom selection — when user brush-selects a range
  2. start + end props — fixed time range
  3. timeWindow — rolling window: [now - timeWindow, now]
  4. 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"
/>
info

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 }}
/>
);
}