Charts: getting started

How to turn rows into a ChartSpec, write a spec by hand, and set the titles, number format and size of a chart. Start here; the other chart pages assume this one.

The examples on this page import from @svgrid/grid:

<script lang="ts">
  import { SvGrid, SvGridChart } from '@svgrid/grid'
</script>

rowsToChartSpec

Option Meaning
type 'bar' | 'line' | 'area' | 'pie' | 'scatter'
category Field whose distinct values become the x-axis / slices.
value Numeric field, or an array of fields (one series each).
series Pivot field: one series per distinct value of it.
reduce 'sum' (default), 'avg', 'count', 'min', 'max', 'median', 'first', 'last', 'countDistinct', or 'pN' for the Nth percentile ('p90').
bucket Group a date category by 'day', 'week', 'month', 'quarter' or 'year'. Categories come out as ISO dates in order and the x axis becomes 'ordinal-time'.
stacked Stack the series instead of grouping them.
stacked100 Stack to 100% - each category normalized to its total.
sort 'value-desc' | 'value-asc' | 'category' | 'none'.
topN Keep the top N categories, bucket the rest into "Other".
otherLabel Label for the bucket (default 'Other').
width / height SVG viewBox size.

Three multi-series shapes:

rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue' })            // 1 series
rowsToChartSpec(rows, { type: 'bar', category: 'region', value: ['revenue', 'cost'] })  // 2 series
rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'sales', series: 'product' }) // pivot

Rows in, spec out, and the reducer is one option:

<script lang="ts">
  import { SvChart, rowsToChartSpec, type ChartSpec } from '@svgrid/grid'

  const rows = [
    { region: 'EMEA', product: 'Grid', revenue: 1200 },
    { region: 'EMEA', product: 'Charts', revenue: 800 },
    { region: 'EMEA', product: 'Studio', revenue: 450 },
    { region: 'APAC', product: 'Grid', revenue: 900 },
    { region: 'APAC', product: 'Charts', revenue: 300 },
    { region: 'Americas', product: 'Grid', revenue: 1500 },
    { region: 'Americas', product: 'Charts', revenue: 700 },
    { region: 'Americas', product: 'Studio', revenue: 620 },
  ]
  let reduce = $state<'sum' | 'avg' | 'count' | 'max'>('sum')
  let split = $state(false)
  const spec = $derived<ChartSpec>({
    ...rowsToChartSpec(rows, { type: 'bar', category: 'region', value: 'revenue', reduce, series: split ? 'product' : undefined, height: 240 }),
    valueFormat: reduce === 'count' ? 'number' : 'currency',
  })
</script>

<label style="font-size: 12px">
  Reduce
  <select bind:value={reduce}>
    <option value="sum">sum</option>
    <option value="avg">avg</option>
    <option value="count">count</option>
    <option value="max">max</option>
  </select>
</label>
<label style="font-size: 12px; margin-left: 10px"><input type="checkbox" bind:checked={split} /> One series per product</label>
<SvChart {spec} legend={split ? 'bottom' : false} />

The three original reducers run on a running sum and count. The rest keep every observation per group, which is what a median or a percentile needs, so they cost memory in proportion to the rows. An empty group is 0 for sum and count and a gap for the others: a month with no orders draws as a gap in a median line rather than as a fake zero.

bucket is the "monthly" in "monthly signups". It parses the category as a date and files the row under the start of its calendar unit (weeks start on Monday), so a daily log charts as one bar per month with no pre-aggregation in your code:

rowsToChartSpec(rows, { type: 'bar', category: 'day', value: 'signups', bucket: 'month' })
// categories: ['2026-01-01', '2026-02-01', ...], xType: 'ordinal-time'

The chart panel exposes the same two knobs as its Aggregate and Bucket selects, and api.configureChart({ reduce: 'median', bucket: 'week' }) sets them from code.

Building a spec yourself

SvGridChart takes any ChartSpec. Per-series type and axis give you combo charts and a secondary Y axis; stacked stacks bars/areas; innerRadius turns a pie into a donut; yAxisTitle / y2AxisTitle / xAxisTitle label the axes. Negative values drop below a zero baseline automatically, and null / NaN values break the line (a gap) instead of dropping it to zero.

<script lang="ts">
  import { SvChart, type ChartSpec } from '@svgrid/grid'

  const spec: ChartSpec = {
    type: 'bar',
    categories: ['Q1', 'Q2', 'Q3', 'Q4'],
    series: [
      { label: 'Revenue', values: [120, 140, 90, 180] },
      { label: 'Margin', values: [0.31, 0.28, 0.22, 0.35], type: 'line', axis: 'right', marker: 'circle' },
    ],
    yAxis: { title: 'Revenue', format: 'currency' },
    y2Axis: { title: 'Margin', format: 'percent', min: 0, max: 0.5 },
    height: 260,
  }
  // A donut is the same shape with one series: { type: 'pie', innerRadius: 0.6, categories, series: [one] }
</script>

<SvChart {spec} legend="bottom" />

The geometry helper buildChart(spec) is exported too, if you want the raw SVG primitives for a custom renderer.

Titles and captions

title and subtitle sit centred above the plot, caption sits under it, left-aligned, for a source or a note. Each reserves its own room, on every chart type, so a title never lands on a pie or a gauge.

<script lang="ts">
  import { SvChart, type ChartSpec } from '@svgrid/grid'

  const spec: ChartSpec = {
    type: 'line',
    categories: Array.from({ length: 12 }, (_, i) => `W${i + 1}`),
    series: [{ label: 'Signups', values: [310, 342, 335, 390, 412, 398, 450, 471, 466, 502, 540, 558], smooth: true }],
    title: 'Weekly signups',
    subtitle: 'Rolling 12 weeks, all channels',
    caption: 'Source: product analytics, refreshed nightly',
    height: 260,
  }
</script>

<SvChart {spec} legend={false} />

The text takes the theme's --sg-fg and --sg-muted colours; the classes are sv-grid-chart-title, -subtitle and -caption if you want to restyle them.

Number format, currency and locale

valueFormat picks the shape of every number the chart draws - the value axis, tooltips, data labels and reference lines:

{ valueFormat: 'currency' }   // 'number' | 'currency' | 'percent' | 'compact'

By itself that formats in the chart's own compact style, and currency means a $. Add currency (an ISO 4217 code) and locale (BCP-47) to format through Intl.NumberFormat instead:

{ valueFormat: 'currency', currency: 'EUR', locale: 'de-DE' }

Setting either one switches the whole chart over, so separators, the decimal mark and the compact suffixes follow the locale rather than English. Leaving both unset keeps the original output exactly, which is deliberate: Intl's compact form is not the same string even for en-US (1.2K, capitalised), so formatting everything through it would restyle every axis already drawn.

A grid inherits this. localization.locale is the chart's default locale, so a grid that is already localized gets a localized chart without repeating itself, and charting.locale overrides it when the two really should differ:

<SvGrid
  {data} {columns}
  localization={{ locale: 'de-DE' }}
  charting={{ valueFormat: 'currency', currency: 'EUR' }}
/>

Currency has no such default, because only the application knows what the numbers are denominated in. Ask for valueFormat: 'currency' without one and the chart falls back to USD rather than refusing to draw.

Sizing

By default the SVG keeps its width / height aspect ratio and scales to its container's width. autosize lays it out at the container's pixel size instead, measuring the toolbar, legend and brush and giving the plot the rest, and re-lays it out on resize. The height comes from a flex or grid parent that stretches the chart (a display: grid box with a height is the simplest); in a plain block the chart follows the width and keeps its own height. The chart panel and the grid's chart view both use it, so their axis labels are drawn for the pixels they actually get.

<script lang="ts">
  import { SvChart, type ChartSpec } from '@svgrid/grid'

  const spec: ChartSpec = {
    type: 'area',
    stacked: true,
    categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
    series: [
      { label: 'Search', values: [420, 460, 430, 510, 540, 580] },
      { label: 'Social', values: [180, 220, 260, 240, 300, 330] },
      { label: 'Referral', values: [90, 110, 100, 130, 120, 150] },
    ],
  }
  let height = $state(300)
</script>

<label style="font-size: 12px">Height <input type="range" min="180" max="420" bind:value={height} /> {height}px</label>
<div style={`display: grid; height: ${height}px`}>
  <SvChart {spec} autosize legend="right" />
</div>

Notes

See the live Integrated charts demo, or the Chart wizard panel - a pick-a-chart dialog whose type-gallery thumbnails are themselves live SvGridChart previews.

More examples

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.

Open the live example: Integrated charts (no deps) (Charts)

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.

Open the live example: Time-series chart (date axis) (Charts)

Forecast: smooth + confidence band

smooth: true bends the polyline into a monotone cubic curve that still passes through every point but flows between them. upperValues + lowerValues shade a translucent envelope around the forecast for at-a-glance uncertainty. 12 weeks actuals + 8 weeks forecast.

Open the live example: Forecast: smooth + confidence band (Charts)

See also

Live examples

  • 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.
  • 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.
  • Forecast: smooth + confidence band - smooth: true bends the polyline into a monotone cubic curve that still passes through every point but flows between them. upperValues + lowerValues shade a translucent envelope around the forecast for at-a-glance uncertainty. 12 weeks actuals + 8 weeks forecast.
  • Chart wizard (from the grid) - Grid on the left, chart wizard on the right. The wizard builds a chart from the grid rows via rowsToChartSpec: pick the type from a gallery (whose thumbnails are themselves live mini SvGridChart instances), choose group-by + measure + aggregation, and a palette. Column, Bar (horizontal), Line, Area, Pie with stacked / 100% variants. Drag a cell range or tick row checkboxes to chart just the selection; otherwise the whole filtered / sorted set re-aggregates live.

Related articles

  • Render Your First Svelte Data Grid in Under 5 Minutes - How to add a fast, accessible, sortable data grid to a Svelte 5 app with SvGrid - covering data, typed columns, feature composition, and the imperative API.
  • How We Started Building SvGrid - The boundary we drew on day one - between the logic engine and the render layer - is what everything else rests on. Here is how that line was drawn and what it cost to get right.