Gauge dial (KPI dashboard)
type: gauge renders a semicircle with track + value arcs, optional red/amber/green range bands, and a target tick. Click any row in the KPI grid to drive the dial; bands auto-flip direction based on whether higher or lower is better.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
A gauge dial for a KPI dashboard in Svelte 5. type gauge renders a semicircle with a grey track arc and a coloured fill arc for gaugeValue, optional red, amber and green range bands through gaugeRanges and a target tick through gaugeTarget. The grid holds the KPIs and the dial reads one row at a time: click any row to display it, and the bands flip direction depending on whether higher or lower is better for that metric.
type: 'gauge' renders a semicircle dial with:
- a grey track arc
- a coloured fill arc representing the current
gaugeValue - optional red/amber/green range bands via
gaugeRanges - an optional target tick via
gaugeTarget
The grid holds the data; the dial reads one row at a time. Click any row to display that KPI on the dial.
Imports, features and API used
Imports: @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: metric (Metric), value (Value), target (Target), max (Scale)
Frequently asked questions
How do I set the ranges?
gaugeRanges is an array of { from, to, color } within the gauge's max; the demo generates green, amber and red bands from the row's target and flips their order for metrics where lower is better.
What does the target tick show?
gaugeTarget draws a small marker on the arc at that value, so the needle's distance from the target is readable without numbers.
How does the dial follow the grid?
onRowClick sets the selected row, and the spec is a $derived over it, so the dial redraws for whichever KPI is clicked.
Related documentation
Related articles
- Sparkline Cells in a Svelte Data Grid - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
- Building an Order Management Dashboard in Svelte - How to wire up an order management grid with master-detail line items, status workflows, server-side data, and bulk fulfillment actions using SvGrid.
- Building an IoT Sensor Dashboard in Svelte - How to build a live IoT sensor dashboard with SvGrid - high-frequency updates, sparkline trends, threshold alerts, and stale-device detection that all stay smooth at scale.
Source code (163-chart-gauge.svelte)
<!-- Documented in: docs/help/charts/types.md -->
<script lang="ts">
/**
* 163. Gauge dial (KPI dashboard)
* --------------------------------
* `type: 'gauge'` renders a semicircle dial with:
* - a grey track arc
* - a coloured fill arc representing the current `gaugeValue`
* - optional red/amber/green range bands via `gaugeRanges`
* - an optional target tick via `gaugeTarget`
*
* The grid holds the data; the dial reads one row at a time. Click any
* row to display that KPI on the dial.
*/
import {
SvGrid,
SvGridChart,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
type SvGridApi,
type ChartSpec,
} from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
type Row = {
id: number; metric: string;
value: number; target: number; max: number; unit: string;
}
const rows: Row[] = [
{ id: 0, metric: 'Uptime', value: 99.82, target: 99.9, max: 100, unit: '%' },
{ id: 1, metric: 'p95 latency', value: 180, target: 150, max: 400, unit: 'ms' },
{ id: 2, metric: 'Error rate', value: 0.45, target: 0.30, max: 5, unit: '%' },
{ id: 3, metric: 'NPS', value: 62, target: 55, max: 100, unit: '' },
{ id: 4, metric: 'Trial -> paid conversion',value: 8.4, target: 12.0, max: 25, unit: '%' },
]
/**
* Every row is a different KPI, so the numbers are in different units: a
* percentage, milliseconds, an unitless NPS score. A bare "180" next to a
* bare "99.82" tells the reader nothing, so each cell carries its own unit
* the same way the dial's readout does.
*/
const withUnit = (v: unknown, row: Row) =>
v == null || v === '' ? '' : row.unit ? `${v} ${row.unit}` : String(v)
const columns: GridColumns<Row> = [
{ field: 'metric', header: 'Metric', width: 190 },
{ field: 'value', header: 'Value', width: 104, align: 'right', cell: (ctx) => withUnit(ctx.getValue(), ctx.row.original) },
{ field: 'target', header: 'Target', width: 104, align: 'right', cell: (ctx) => withUnit(ctx.getValue(), ctx.row.original) },
{ field: 'max', header: 'Scale', width: 96, align: 'right', cell: (ctx) => withUnit(ctx.getValue(), ctx.row.original) },
]
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let selected = $state<Row>(rows[0]!)
/** Pick range bands based on whether higher or lower is better. p95
* latency + error rate are "lower better"; the rest are "higher better". */
function ranges(r: Row): ChartSpec['gaugeRanges'] {
const higherBetter = !['p95 latency', 'Error rate'].includes(r.metric)
if (higherBetter) {
return [
{ from: 0, to: r.target * 0.7, color: '#ef4444' },
{ from: r.target * 0.7, to: r.target, color: '#f59e0b' },
{ from: r.target, to: r.max, color: '#16a34a' },
]
} else {
return [
{ from: 0, to: r.target, color: '#16a34a' },
{ from: r.target, to: r.target * 1.5, color: '#f59e0b' },
{ from: r.target * 1.5,to: r.max, color: '#ef4444' },
]
}
}
/** Pane size, so the dial fills its card rather than a fixed viewBox. */
let paneW = $state(0)
let paneH = $state(0)
const spec = $derived.by<ChartSpec>(() => ({
type: 'gauge',
categories: [],
series: [],
gaugeValue: selected.value,
gaugeTarget: selected.target,
gaugeMin: 0,
gaugeMax: selected.max,
gaugeUnit: selected.unit,
gaugeRanges: ranges(selected),
width: 340,
height: 230,
}))
</script>
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div class="shrink-0 rounded-lg border px-4 py-3" style="border-color: var(--sg-border); background: var(--sg-header-bg);">
<p class="text-sm font-semibold" style="color: var(--sg-fg);">
Gauge dial - one KPI at a time
</p>
<p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
Click any row to display that KPI on the dial. The red/amber/green bands flip direction
based on whether higher or lower is better. The target tick shows the goal value; the
filled arc is the current reading.
</p>
</div>
<div class="flex flex-1 min-h-0 gap-3">
<div class="flex-1 min-w-0 min-h-0">
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
sortable
filterable
selectionMode="row"
rowHeight={36}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => { api = a }}
onRowClick={(e: { row: Row }) => { if (e.row) selected = e.row }}
/>
</div>
<div class="rounded-lg border p-3 flex flex-col" style="flex: 0 1 380px; min-width: 0; min-height: 0; border-color: var(--sg-border); background: var(--sg-bg);">
<div class="mb-2 shrink-0 text-xs font-semibold" style="color: var(--sg-muted);">{selected.metric}</div>
<!-- A dial is a semicircle, so unlike the other charts it should NOT be
stretched to fill: given a tall card it just sits at the bottom with
dead space above the arc. Keep its own ~0.68 aspect and centre it. -->
<div
class="flex-1 min-h-0 flex items-center justify-center"
style="width: 100%;"
bind:clientWidth={paneW}
bind:clientHeight={paneH}
>
{#if paneW > 40 && paneH > 40}
<SvGridChart {spec} width={paneW} height={Math.min(paneH, Math.round(paneW * 0.68))} />
{/if}
</div>
</div>
</div>
</section>More Charts examples
- Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
- Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.