Chart zoom + brush mini-map
Drag a rectangle over a 180-day series to zoom in; double-click resets. A compact brush below shows the full range with a draggable window - drag the body to pan, edges to resize. Pairs with the crosshair tooltip + PNG/SVG export.
A live, editable Svelte 5 data grid example. Open the interactive demo or read the documentation.
What this example shows
Long time-series get unreadable when every point fights for the same horizontal pixels. `zoomable` lets the user drag a rect over the plot area to zoom in (double-click resets). `brush` adds a compact mini-map below the chart with a draggable window - drag the window body to pan, drag either edge to resize. The two interactions stay in sync via the chart's internal zoom state. Crosshair tooltip + PNG/SVG export round out the interactive surface.
Source code (153-chart-zoom-brush.svelte)
<!-- Documented in: docs/help/charts.md -->
<script lang="ts">
/**
* 153. Chart zoom + brush (mini-map)
* ----------------------------------
* Long time-series get unreadable when every point fights for the same
* horizontal pixels. `zoomable` lets the user drag a rect over the plot
* area to zoom in (double-click resets). `brush` adds a compact mini-map
* below the chart with a draggable window - drag the window body to pan,
* drag either edge to resize. The two interactions stay in sync via the
* chart's internal zoom state. Crosshair tooltip + PNG/SVG export round
* out the interactive surface.
*/
import {
SvGrid,
SvGridChart,
rowsToChartSpec,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type ColumnDef,
type SvGridApi,
type ChartSpec,
} from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
type Row = { id: number; day: string; revenue: number; sessions: number }
// 180 days of synthetic web traffic so zoom + brush actually earn their keep.
let seed = 0xabc123
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
const rows: Row[] = Array.from({ length: 180 }, (_, i) => {
const d = new Date(2026, 0, 1 + i)
const trend = 1200 + i * 6 // slow climb
const weekly = Math.sin((i / 7) * 2 * Math.PI) * 180 // weekly cycle
const noise = (rnd() - 0.5) * 240
const sessions = Math.max(200, Math.round(trend + weekly + noise))
return {
id: i,
day: d.toISOString().slice(0, 10),
sessions,
revenue: Math.round(sessions * (8 + rnd() * 4)),
}
})
const columns: ColumnDef<typeof features, Row>[] = [
{ field: 'day', header: 'Day', width: 130 },
{ field: 'sessions', header: 'Sessions', width: 130, align: 'right' },
{ field: 'revenue', header: 'Revenue', width: 140, align: 'right',
format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } } },
]
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let displayed = $state<Row[]>(rows)
let metric = $state<'sessions' | 'revenue'>('sessions')
function sync() { displayed = (api?.getDisplayedRows() as Row[]) ?? rows }
const compact = (v: number) => (Math.abs(v) >= 1e6 ? (v / 1e6).toFixed(1) + 'M' : Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(v % 1e3 ? 1 : 0) + 'k' : String(Math.round(v)))
const spec = $derived.by<ChartSpec>(() => {
const s = rowsToChartSpec(displayed, {
type: 'line',
category: 'day',
value: metric,
reduce: 'sum',
width: 720,
height: 320,
})
s.xType = 'time'
s.xAxisTitle = 'Day'
s.yAxisTitle = metric === 'revenue' ? 'Revenue (USD)' : 'Sessions'
return s
})
</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);">
Drag-rect zoom + brush mini-map for long time-series
</p>
<p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
Drag a rectangle over the chart to zoom in. Double-click to reset. The brush below shows
the full series with a draggable window - drag the body to pan, drag the edges to resize.
</p>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
<select bind:value={metric} class="ic-sel">
<option value="sessions">Sessions</option>
<option value="revenue">Revenue</option>
</select>
</div>
</div>
<div class="flex flex-1 min-h-0 gap-3">
<div class="flex-1 min-h-0">
<SvGrid responsive={true}
data={rows}
columns={columns}
features={features}
sortable
filterable
selectionMode="none"
enableRowSummaries={false}
rowHeight={28}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => { api = a; sync() }}
onFiltersChange={sync}
onSortingChange={sync}
/>
</div>
<div class="shrink-0 rounded-lg border p-3" style="width: 760px; border-color: var(--sg-border); background: var(--sg-bg);">
<SvGridChart {spec} formatValue={compact} zoomable brush brushHeight={96} />
</div>
</div>
</section>
<style>
.ic-sel {
border: 1px solid var(--sg-input-border, var(--sg-border));
background: var(--sg-input-bg, var(--sg-bg));
color: var(--sg-fg);
border-radius: 6px;
padding: 4px 8px;
font-size: 12px;
}
</style>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.
- Integrated charts (no deps) - Chart the grid data with no external charting library. SvGridChart renders a ChartSpec; rowsToChartSpec aggregates the grid current (filtered/sorted) rows into one. Bar, line, area, pie - plus 100% stacked, top-N + Other, an average reference line, and double-click-to-isolate a series. Filter the grid and the chart re-aggregates live.
- Scatter / bubble chart - A scatter plot maps two numeric measures (x vs y); a bubble chart adds a third via dot radius. type: scatter with series points [{ x, y, r }]. Spend vs revenue, sized by deals, coloured by region, with an average-revenue reference line. Filter the grid and the cloud re-plots.
- Time-series chart (date axis) - xType: time spaces points by ACTUAL time - irregular date gaps render proportionally - and shows real date ticks. A referenceLines target/SLA line spans the plot; toggle 100% stacked to read each day as a share of its total. Line, stacked area, or stacked bar.