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>

View this example on GitHub

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.