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
onZoneChange callbacks work in your local project but not in this interactive playground due to how the editor re-renders components.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | RelayDataPoint | — | Required. The value and timestamp — see data |
min | number | 0 | Minimum scale value |
max | number | 100 | Maximum scale value |
label | string | — | Label text below the value |
unit | string | — | Unit suffix (e.g., "°C") |
formatValue | function | — | Custom value formatter — see formatValue |
alertZones | AlertZone[] | — | Color zones on the gauge arc — see Alert Zones |
showZoneValues | boolean | false | Show boundary values on zone edges |
styles | object | — | Custom fonts and background — see Styles |
showLastUpdated | boolean | false | Show the timestamp from data.timestamp |
formatTimestamp | function | — | Custom timestamp formatter — see formatTimestamp |
showLoading | boolean | true | Show skeleton shimmer while data.isLoading is true |
onZoneChange | function | — | Called when value enters a new zone — see onZoneChange |
onError | function | — | 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
}}
| Field | Type | Description |
|---|---|---|
value | any | The current reading |
timestamp | number | null | When the reading was taken (Unix ms) |
isLoading | boolean | (optional) Shows skeleton shimmer when true |
error | Error | 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" },
]}
The needle color automatically matches the zone it's in. Without alert zones, the gauge renders a single grey arc.
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
| Field | Applies to | Properties |
|---|---|---|
value | The large number display | fontFamily, fontSize, fontWeight, color, fontFile |
label | The label below the value | fontFamily, fontSize, fontWeight, color, fontFile |
unit | The unit suffix | fontFamily, fontSize, fontWeight, color, fontFile |
minMax | Min/max labels at arc ends | fontFamily, fontSize, fontWeight, color, fontFile |
lastUpdated | The timestamp text | fontFamily, fontSize, fontWeight, color, fontFile |
background | The component background | color |
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:
| Field | Type | Default | Description |
|---|---|---|---|
arcThickness | number | 14 | Thickness of the gauge arc in pixels |
needleThickness | number | 2.5 | Thickness of the needle in pixels |
arcAngle | number | 180 | Sweep angle of the arc in degrees (30–300) |
width | number | — | Fixed width override |
height | number | — | 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
// }
}}
| Field | Type | Description |
|---|---|---|
transition.previousZone | AlertZone | null | The zone the value was in |
transition.currentZone | AlertZone | null | The zone the value moved to |
transition.value | number | The 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"
// }
}}
| Field | Type | Description |
|---|---|---|
error.type | string | "invalid_value", "invalid_data_point", or "invalid_timestamp" |
error.message | string | Human-readable error description |
error.rawValue | any | The invalid value that was passed |
error.component | string | The 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)}
/>
);
}