Skip to main content

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

PropTypeDefaultDescription
dataRecord<string, DataPoint[]>Required. Time-series data keyed by device identifier
stateMapperfunctionRequired. Maps a metric value to a state name string. See stateMapper
metricKeystringKey to extract from each data point before passing to stateMapper
stateColorsRecord<string, string>Color for each state name. See stateColors
formatTooltipfunctionCustom tooltip text: (entry, deviceName) => string
renderTooltipfunctionCustom tooltip JSX: (entry, deviceName) => ReactNode. Takes priority over formatTooltip
stylesobjectCustom styling including dimensions — see Styles
rowHeightnumber28Height of each device row in px
labelAlignstring"left""left" or "right" — position of device labels
showLoadingbooleantrueShow skeleton while data is empty
onErrorfunctionCalled 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

StateColor
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",
}}
FieldApplies toProperties
widthChart containernumber | string — explicit width
heightChart containernumber | string — explicit height
labelX-axis tick labelsfontFamily, fontSize, fontWeight, color
rowLabelDevice name labelsfontFamily, fontSize, fontWeight, color
tooltipHover tooltipfontFamily, fontSize, fontWeight, color
backgroundComponent backgroundcolor
emptyRowColorNo-data row fillstring — color for rows with no state entries
tip

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:

  1. renderTooltip — custom JSX (takes priority)
  2. formatTooltip — custom text string
  3. Default — shows device — state with 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:

FieldTypeDescription
statestringThe state name (output of stateMapper)
startnumberStart timestamp in ms
endnumberEnd 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",
}}
/>
);
}