Skip to main content

Needle Gauge

A rotating needle gauge that displays a single numeric value within a range. Supports alert zones, custom styling, and zone-change callbacks.

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
dataRelayDataPoint—Required. The value and timestamp — see data
minnumber0Minimum scale value
maxnumber100Maximum scale value
labelstring—Label text below the value
unitstring—Unit suffix (e.g., "°C")
formatValuefunction—Custom value formatter — see formatValue
alertZonesAlertZone[]—Color zones on the gauge arc — see Alert Zones
defaultColorstring'#3b82f6'Color used for the value-fill arc, needle, and value text when no alert zone matches
showZoneValuesbooleanfalseShow boundary values on zone edges
stylesobject—Custom fonts and background — see Styles
showLastUpdatedbooleanfalseShow the timestamp from data.timestamp
formatTimestampfunction—Custom timestamp formatter — see formatTimestamp
showLoadingbooleantrueShow skeleton shimmer while data.isLoading is true
onZoneChangefunction—Called when value enters a new zone — see onZoneChange
onErrorfunction—Called on invalid data — see onError

data​

All components accept a data prop with this shape:

data={{
value: 65, // the current reading
timestamp: Date.now(), // when the reading was taken (ms)
isLoading: false, // optional — shows skeleton shimmer
error: null, // optional — triggers onError
}}
FieldTypeDescription
valueanyThe current reading
timestampnumber | nullWhen the reading was taken (Unix ms)
isLoadingboolean(optional) Shows skeleton shimmer when true
errorError | null(optional) Triggers the onError callback

When using hooks, this maps directly:

const { value, timestamp, isLoading, error } = useRelayLatest({
deviceIdent: "sensor_01",
metric: "temperature",
timeRange: { start: Date.now() - 86400000, end: Date.now() },
});

<NeedleGauge data={{ value, timestamp, isLoading, error }} />

Alert Zones​

Color the gauge arc based on value ranges. Each zone needs min, max, color, and an optional label. Zones must not overlap.

Try pasting this into the playground:

alertZones={[
{ min: 0, max: 25, color: "#3b82f6", label: "Cold" },
{ min: 25, max: 50, color: "#22c55e", label: "Normal" },
{ min: 50, max: 75, color: "#f59e0b", label: "Warm" },
{ min: 75, max: 100, color: "#ef4444", label: "Hot" },
]}

When the value is inside a zone, the value-fill arc, needle, and value text all adopt that zone's color, while the zone bands render at 25% opacity behind the fill. When the value is outside every zone — or when no zones are configured — the fill uses the defaultColor prop (blue #3b82f6 by default).

<NeedleGauge
data={{ value: 65, timestamp: Date.now() }}
defaultColor="#7c3aed"
/>

Styles​

Customize fonts and background via the styles prop. Every font field accepts fontFamily, fontSize, fontWeight, and color.

styles={{
value: { fontSize: 28, fontWeight: 700, color: "#1e293b" },
label: { fontSize: 14, color: "#64748b" },
unit: { fontSize: 12, color: "#94a3b8" },
minMax: { fontSize: 10, color: "#94a3b8" },
lastUpdated: { fontSize: 10, color: "#94a3b8" },
background: { color: "#f8fafc" },
}}

Try adding this to the playground to see the effect:

<NeedleGauge
data={{ value: 72, timestamp: Date.now() }}
min={0}
max={100}
label="Styled"
unit="°C"
styles={{
value: { fontSize: 32, fontWeight: 800, color: "#7c3aed" },
label: { fontSize: 16, color: "#a78bfa" },
background: { color: "#faf5ff" },
}}
alertZones={[
{ min: 0, max: 50, color: "#a78bfa" },
{ min: 50, max: 100, color: "#7c3aed" },
]}
/>

Style fields reference​

FieldApplies toProperties
valueThe large number displayfontFamily, fontSize, fontWeight, color, fontFile
labelThe label below the valuefontFamily, fontSize, fontWeight, color, fontFile
unitThe unit suffixfontFamily, fontSize, fontWeight, color, fontFile
minMaxMin/max labels at arc endsfontFamily, fontSize, fontWeight, color, fontFile
lastUpdatedThe timestamp textfontFamily, fontSize, fontWeight, color, fontFile
backgroundThe component backgroundcolor

Custom Fonts​

Use fontFile to load a custom font from a URL. Supports .ttf, .otf, .woff, and .woff2 files. The font is automatically loaded via @font-face — you don't need to add any CSS yourself.

styles={{
value: {
fontFile: "/fonts/CustomFont.woff2",
fontSize: 28,
fontWeight: 700,
},
}}

Additional style options:

FieldTypeDefaultDescription
arcThicknessnumber14Thickness of the gauge arc in pixels
needleThicknessnumber2.5Thickness of the needle in pixels
arcAnglenumber180Sweep angle of the arc in degrees (30–300)
widthnumber—Fixed width override
heightnumber—Fixed height override

formatValue​

Override how the value number is displayed. Receives the raw number, returns a string.

formatValue={(v) => v.toFixed(1)}
formatValue={(v) => `~${Math.round(v)}`}
formatValue={(v) => `${v}%`}

formatTimestamp​

Override how data.timestamp is displayed when showLastUpdated is true.

showLastUpdated={true}
formatTimestamp={(ts) => new Date(ts).toLocaleTimeString()}

onZoneChange​

Called whenever the value moves from one alert zone to another.

onZoneChange={(transition) => {
console.log(transition);
// {
// previousZone: { min: 30, max: 70, color: "#22c55e", label: "Normal" },
// currentZone: { min: 70, max: 100, color: "#ef4444", label: "Hot" },
// value: 72
// }
}}
FieldTypeDescription
transition.previousZoneAlertZone | nullThe zone the value was in
transition.currentZoneAlertZone | nullThe zone the value moved to
transition.valuenumberThe current value

onError​

Called when the component receives invalid data (NaN, non-finite, non-numeric). The component renders the last valid value and fires this callback.

onError={(error) => {
console.error(error);
// {
// type: "invalid_value",
// message: "NeedleGauge received NaN",
// rawValue: NaN,
// component: "NeedleGauge"
// }
}}
FieldTypeDescription
error.typestring"invalid_value", "invalid_data_point", or "invalid_timestamp"
error.messagestringHuman-readable error description
error.rawValueanyThe invalid value that was passed
error.componentstringThe component name

The component never crashes on bad data — it falls back to the last valid state and notifies you via this callback.


With Hooks​

import { useRelayLatest, NeedleGauge } from "@relay-x/ui";

function TemperatureGauge() {
const latest = useRelayLatest({
deviceIdent: "sensor_01",
metric: "temperature",
timeRange: { start: Date.now() - 86400000, end: Date.now() },
});

return (
<NeedleGauge
data={latest}
min={0}
max={100}
label="Temperature"
unit="°C"
showLastUpdated
alertZones={[
{ min: 0, max: 30, color: "#3b82f6", label: "Cold" },
{ min: 30, max: 70, color: "#22c55e", label: "Normal" },
{ min: 70, max: 100, color: "#ef4444", label: "Hot" },
]}
onZoneChange={(t) => console.log(`Zone: ${t.currentZone?.label}`)}
onError={(e) => console.error(e.message)}
/>
);
}