State Timeline
A canvas-based horizontal bar chart that visualizes state transitions over time. Each row represents a device, colored by its current state.
Playground
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | Record<string, DataPoint[]> | — | Required. Time-series data keyed by device identifier |
stateMapper | function | — | Required. Maps a metric value to a state name string. See stateMapper |
metricKey | string | — | Key to extract from each data point before passing to stateMapper |
stateColors | Record<string, string> | — | Color for each state name. See stateColors |
formatTooltip | function | — | Custom tooltip text: (entry, deviceName) => string |
renderTooltip | function | — | Custom tooltip JSX: (entry, deviceName) => ReactNode. Takes priority over formatTooltip |
styles | object | — | Custom styling including dimensions — see Styles |
rowHeight | number | 28 | Height of each device row in px |
labelAlign | string | "left" | "left" or "right" — position of device labels |
showLoading | boolean | true | Show skeleton while data is empty |
onError | function | — | Called on invalid timestamps — see onError |
stateMapper
A function that converts a raw metric value into a state name string. The state name is then looked up in stateColors for coloring.
// direct passthrough (value is already a string)
stateMapper={(value) => value}
// numeric thresholds
stateMapper={(value) => {
if (value > 80) return "critical";
if (value > 50) return "warning";
return "normal";
}}
// boolean
stateMapper={(value) => value ? "online" : "offline"}
The metricKey prop determines which field is extracted from each data point and passed to stateMapper. For example, with metricKey="status" and data { timestamp: 123, status: "ok" }, stateMapper receives "ok".
stateColors
Map state names to colors. States not in this map fall back to built-in defaults, then to a rotating palette.
stateColors={{
normal: "#22c55e",
warning: "#f59e0b",
critical: "#ef4444",
offline: "#6b7280",
maintenance: "#8b5cf6",
}}
Built-in defaults
| State | Color |
|---|---|
normal | #22c55e |
warning | #f59e0b |
critical | #ef4444 |
error | #ef4444 |
offline | #6b7280 |
online | #22c55e |
States not matched by custom colors or built-in defaults cycle through a fallback palette: #3b82f6, #8b5cf6, #ec4899, #f97316, #14b8a6, #6366f1.
Styles
The styles prop controls fonts, background, empty row color, and chart dimensions.
styles={{
width: 660,
height: 200,
label: { fontSize: 11, color: "#6b7280" },
rowLabel: { fontSize: 12, fontWeight: 500, color: "#1e293b" },
tooltip: { fontSize: 12, color: "#ffffff" },
background: { color: "#ffffff" },
emptyRowColor: "#f1f5f9",
}}
| Field | Applies to | Properties |
|---|---|---|
width | Chart container | number | string — explicit width |
height | Chart container | number | string — explicit height |
label | X-axis tick labels | fontFamily, fontSize, fontWeight, color |
rowLabel | Device name labels | fontFamily, fontSize, fontWeight, color |
tooltip | Hover tooltip | fontFamily, fontSize, fontWeight, color |
background | Component background | color |
emptyRowColor | No-data row fill | string — color for rows with no state entries |
Set styles.width and styles.height to control the chart size directly. Without them, the chart fills its parent container using a ResizeObserver.
Tooltip
Hover over a state bar to see a tooltip. The content is resolved in this order:
renderTooltip— custom JSX (takes priority)formatTooltip— custom text string- Default — shows
device — statewith start/end times
// custom JSX tooltip
renderTooltip={(entry, deviceName) => (
<div>
<strong>{deviceName}</strong>: {entry.state}
<br />
{new Date(entry.start).toLocaleTimeString()} — {new Date(entry.end).toLocaleTimeString()}
</div>
)}
// custom text tooltip
formatTooltip={(entry, deviceName) =>
`${deviceName}: ${entry.state} (${((entry.end - entry.start) / 1000).toFixed(0)}s)`
}
The StateEntry object passed to both functions:
| Field | Type | Description |
|---|---|---|
state | string | The state name (output of stateMapper) |
start | number | Start timestamp in ms |
end | number | End timestamp in ms |
With Hooks
import { useRelayTimeSeries, StateTimeline } from "@relay-x/ui";
function DeviceStateView() {
const { data } = useRelayTimeSeries({
deviceIdent: "sensor_01",
metrics: ["status"],
mode: "historical",
timeRange: { start: Date.now() - 86400000, end: Date.now() },
});
return (
<StateTimeline
data={{ sensor_01: data }}
stateMapper={(value) => value}
metricKey="status"
styles={{ width: 660, height: 200 }}
stateColors={{
normal: "#22c55e",
warning: "#f59e0b",
critical: "#ef4444",
offline: "#6b7280",
}}
/>
);
}