Skip to main content

Step 4: Render telemetry

Create dashboard/src/components/LiveChart.jsx. This component wraps the UI Kit's TimeSeries component with a title, a live latest-value readout, and a 5-minute rolling window. The highlighted lines are the RelayX integration. Everything else is styling.

import { useMemo } from "react";
import { TimeSeries, useRelayTimeSeries } from "@relay-x/ui";

import { DEVICE_IDENT } from "../relayx.js";

export function LiveChart({ title, subtitle, metric, color, unit, decimals = 2 }) {
const timeRange = useMemo(() => {
const end = new Date();
const start = new Date(end.getTime() - 5 * 60 * 1000);
return { start, end };
}, []);

const { data: tsData, isLoading } = useRelayTimeSeries({
deviceIdent: DEVICE_IDENT,
metrics: [metric],
timeRange,
mode: "both",
maxPoints: 2000,
});

const chartData = useMemo(
() => ({ [DEVICE_IDENT]: tsData ?? [] }),
[tsData],
);

const latest =
tsData && tsData.length > 0 ? Number(tsData[tsData.length - 1][metric] ?? 0) : 0;

return (
<div
style={{
background: "var(--bg-1)",
border: "1px solid var(--line)",
borderRadius: 12,
padding: 20,
flex: 1,
boxShadow: "0 1px 2px rgba(21,17,10,0.03)",
}}
>
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 8 }}>
<div>
<div style={{ fontFamily: "Manrope", fontSize: 14, fontWeight: 700, color: "var(--ink)" }}>
{title}
</div>
<div style={{ fontSize: 11, color: "var(--fg-2)", marginTop: 2 }}>{subtitle}</div>
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 4 }}>
<span
style={{
fontFamily: "Manrope",
fontSize: 22,
fontWeight: 700,
fontVariantNumeric: "tabular-nums",
color: "var(--ink)",
letterSpacing: "-0.02em",
}}
>
{isLoading && !tsData?.length ? "--" : latest.toFixed(decimals)}
</span>
<span style={{ fontSize: 11, color: "var(--fg-2)", fontWeight: 500 }}>{unit}</span>
</div>
</div>

<div style={{ height: 180 }}>
<TimeSeries
data={chartData}
metrics={[{ key: metric, label: unit, color }]}
timeWindow={5 * 60 * 1000}
autoScroll
area
showLoading={isLoading}
/>
</div>
</div>
);
}

Two things to notice in the RelayX wiring:

  • mode: "both" fetches the 5-minute backfill on mount, then streams subsequent points. The chart is populated on first render, not after a delay.
  • TimeSeries expects a Record<deviceIdent, DataPoint[]> shape, not a flat array. That's why chartData wraps the result under the device ident key.

The rest of the component is styling: a card wrapper, a header row with title and latest value, and a fixed-height container for the chart.

Test it

Make sure the device is running and publishing (Device Step 5). Update dashboard/src/App.jsx to render three charts:

import { LiveChart } from "./components/LiveChart.jsx";

export default function App() {
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12, padding: 24 }}>
<LiveChart title="Power draw" subtitle="power · mW · 5 min (live)" metric="power" color="#157A3E" unit="mW" />
<LiveChart title="Current draw" subtitle="current · mA · 5 min (live)" metric="current" color="#E09E00" unit="mA" />
<LiveChart title="Bus voltage" subtitle="volt · V · 5 min (live)" metric="volt" color="#1E5FAE" unit="V" />
</div>
);
}

Reload the page. Three chart cards render side by side, each with a title, a latest value in the top right, and a live chart below. Values should tick every 10 seconds, matching the device's default sample rate. Readings hover near zero until the relay is closed in Step 4.