Charting from the grid
The chart panel on the grid: charting the displayed rows with no spec, cross-filtering, the builder with its type gallery and Format tab, linked and unlinked charts, lifecycle events, and export to PNG, SVG, PDF and CSV.
The examples on this page import from @svgrid/grid:
<script lang="ts">
import { SvGrid, SvGridChart } from '@svgrid/grid'
</script>
Charting from the grid, without a spec
charting puts the whole thing behind one prop: the grid grows a Chart button
and a panel that derives its spec from the displayed rows.
<SvGrid {data} {columns} charting />
<SvGrid {data} {columns} charting={{ position: 'right', crossFilter: true }} />
The panel lets a reader pick the chart type, the group-by, an optional
split-by, the measure, the aggregate and the number format, plus stacking,
100%, horizontal bars, donut, data labels, a log axis, a date axis and the
series palette. Everything it offers is a ChartingConfig field, so anything
a reader can reach you can also preset. The Build button opens the
chart builder: a type gallery, the pickers, and a Format tab
for titles, axes, legend and per-series style.
Cross-filtering
crossFilter: true turns a click on a chart category into a grid filter, and
the panel grows a Clear filter button. It applies to the types where a clicked
mark maps back to exactly one value of the charted dimension, so it is off for
gauge, scatter and sankey - on a sankey a clicked target would otherwise
filter the source column and empty the grid.
Transcript
Add the charting prop and the grid grows a chart panel. It charts the rows on screen, so filtering or sorting redraws it.
Pick a type, a group-by column and the aggregate.
Click a bar and the grid filters to that category. Clear filter brings the rows back.
Build opens a gallery with a live thumbnail per type, and a Format tab for titles, axes and series.
Everything here is a config field or an API call, and it round-trips through the saved view.
Chart builder
The grid panel's Build button opens the builder: a modal with three tabs. Type is a gallery with a live thumbnail per chart type, so a reader picks by shape rather than by name. Data is the panel's pickers laid out as a form (the same component, so the two cannot disagree about what a type needs) plus the link toggle (below). Format is everything the derived spec does not carry: title, subtitle and caption; legend placement; data-label placement; series labels at the line ends and the crosshair's axis pills; a compact rule for narrow widths; value-axis title, min, max, format and grid lines; category-axis title, grid lines and label angle; per series, colour, type (bar / line / area), axis, marker, line width and, on a bar or area chart, the stack group; and the chart's font size, font family, background and text colour.
Format edits are plain data (ChartFormatState) on the chart tab, applied by
the engine to every spec the panel derives, so they keep applying when the
rows change and they round-trip through getState() / setState(). They are
also reachable from code:
api.configureChart({ format: { title: 'Revenue by region', legend: 'right', yAxis: { max: 50000, format: 'currency' }, series: { EMEA: { color: '#0ea5e9', type: 'line', axis: 'right' } } } })
api.configureChart({ format: { seriesLabels: true, crosshairLabels: false, compactBelow: 480, style: { fontSize: 13 } } })
api.configureChart({ format: null }) // back to the derived look
compactBelow appends CHART_RESPONSIVE_PRESETS.compact(width) to the
spec's responsive rules: under that
width the series and data labels go, the category labels turn vertical and
the legend hides. applyChartFormat(spec, format) is the pure function
behind the tab, exported for charts outside the grid.
Linked and unlinked charts
A panel chart follows the grid: filter, sort, edit a cell, add rows, and it
redraws. The link button in the panel header (and the checkbox on the
builder's Data tab) unlinks it: the chart keeps the spec it has at that
moment and stops following, which is what a reader wants when they have
found the picture and are about to change the filter to look for another.
The button relinks. configureChart({ frozen: true | false }) does the same
from code, and the frozen spec is part of the saved view.
Saved charts
A reader who has set up a chart they like keeps it: the panel's save button
names the active tab's whole configuration (type, columns, aggregate, every
switch, indicators, zoom window, format) and lists the saved ones, and
picking one applies it to whichever tab is active, which keeps its own title.
From code the same is api.saveChart(name), api.applySavedChart(name),
api.removeSavedChart(name) and api.getSavedCharts();
configureChart({ saved: name, ...more }) applies a saved chart first and
the other keys on top. The list travels with getState() / setState() as
savedCharts, present only when there is one, so older snapshots do not
change.
<script lang="ts">
import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'
type Row = { id: number; region: string; product: string; revenue: number; units: number }
const rows: Row[] = [
{ id: 1, region: 'EMEA', product: 'Grid', revenue: 1200, units: 40 },
{ id: 2, region: 'EMEA', product: 'Charts', revenue: 800, units: 30 },
{ id: 3, region: 'APAC', product: 'Grid', revenue: 900, units: 25 },
{ id: 4, region: 'APAC', product: 'Charts', revenue: 300, units: 12 },
{ id: 5, region: 'Americas', product: 'Grid', revenue: 1500, units: 52 },
{ id: 6, region: 'Americas', product: 'Charts', revenue: 700, units: 28 },
]
const columns: GridColumns<Row> = [
{ field: 'region', header: 'Region', width: 120 },
{ field: 'product', header: 'Product', width: 120 },
{ field: 'revenue', header: 'Revenue', width: 110, cellDataType: 'number' },
{ field: 'units', header: 'Units', width: 90, cellDataType: 'number' },
]
let api = $state<SvGridApi<{}, Row> | null>(null)
let saved = $state<string[]>([])
const refresh = () => (saved = api?.getSavedCharts().map((s) => s.name) ?? [])
function keep(name: string, config: Parameters<SvGridApi<{}, Row>['configureChart']>[0]) {
api?.configureChart(config)
api?.saveChart(name)
refresh()
}
</script>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px">
<button type="button" onclick={() => keep('Revenue by region', { type: 'bar', dimension: 'region', measure: 'revenue', reduce: 'sum', format: { title: 'Revenue by region' } })}>Save "Revenue by region"</button>
<button type="button" onclick={() => keep('Units by product', { type: 'pie', dimension: 'product', measure: 'units', reduce: 'sum', format: { title: 'Units by product' } })}>Save "Units by product"</button>
{#each saved as name (name)}
<button type="button" onclick={() => api?.applySavedChart(name)}>Apply {name}</button>
{/each}
</div>
<SvGrid data={rows} {columns} charting={{ defaultOpen: true, position: 'right', width: 380 }} containerHeight={360} onApiReady={(a) => { api = a; refresh() }} />
Localizing the panel
Every string the panel and the builder show (the head and its buttons, the
export menu, the pickers with their type, aggregate, bucket, palette and
indicator names, the sentences that say why a type cannot draw yet, the
Format fields, the saved-charts popover) is a key on localization.text,
the same map the grid's own chrome reads. The keys start with chart; the
localization page lists the groups. The
charts the panel draws take their own strings from charting.localeText.
<script lang="ts">
import { SvGrid, type GridColumns } from '@svgrid/grid'
type Row = { id: number; region: string; product: string; revenue: number; units: number }
const rows: Row[] = [
{ id: 1, region: 'EMEA', product: 'Grid', revenue: 1200, units: 40 },
{ id: 2, region: 'EMEA', product: 'Charts', revenue: 800, units: 30 },
{ id: 3, region: 'APAC', product: 'Grid', revenue: 900, units: 25 },
{ id: 4, region: 'APAC', product: 'Charts', revenue: 300, units: 12 },
{ id: 5, region: 'Americas', product: 'Grid', revenue: 1500, units: 52 },
{ id: 6, region: 'Americas', product: 'Charts', revenue: 700, units: 28 },
]
const columns: GridColumns<Row> = [
{ field: 'region', header: 'Region', width: 120 },
{ field: 'product', header: 'Product', width: 120 },
{ field: 'revenue', header: 'Revenue', width: 110, cellDataType: 'number' },
{ field: 'units', header: 'Units', width: 90, cellDataType: 'number' },
]
</script>
<SvGrid
data={rows}
{columns}
charting={{ defaultOpen: true, position: 'right', width: 380, localeText: { chartLabel: '{type}-Diagramm' } }}
containerHeight={360}
localization={{
locale: 'de-DE',
text: {
chartPanelTitle: 'Diagramm', chartAdd: 'Diagramm hinzufügen', chartBuild: 'Erstellen', chartClose: 'Diagramm schließen',
chartType: 'Typ', chartGroupBy: 'Gruppieren nach', chartSplitBy: 'Aufteilen nach', chartValue: 'Wert', chartAggregate: 'Aggregat',
chartReduceSum: 'Summe', chartReduceAvg: 'Durchschnitt', chartReduceCount: 'Anzahl',
chartTypeBar: 'Balken', chartTypeLine: 'Linie', chartTypeArea: 'Fläche', chartTypePie: 'Kreis',
chartGroupCompare: 'Vergleichen', chartGroupPartOfWhole: 'Anteile', chartColours: 'Farben', chartStacked: 'Gestapelt', chartLabels: 'Beschriftungen',
chartBuilderTitle: 'Diagramm-Editor', chartBuilderTabType: 'Typ', chartBuilderTabData: 'Daten', chartBuilderTabFormat: 'Format',
},
}}
/>
Lifecycle events
charting: { onChartCreated, onChartChanged } report what the panel does:
onChartCreated fires once for the first chart when the panel opens and
again for every tab added, onChartChanged once per burst of changes to the
active chart's spec (a picker, a format edit, a filter, an edit, new rows),
debounced. Both carry { index, title, type, spec }.
The chart as a grid
chartSpecToTable(spec) is the other direction: the rows and columns a
spec amounts to, in the shape SvGrid takes. A category chart comes back as
one row per category with a column per series (a grouped axis adds a Group
column, a histogram From and To columns, a candlestick Open / High / Low /
Close, a box plot its five numbers); a scatter as one row per point, a sankey
or chord per link, a tree map or sunburst per leaf with a column per level,
a calendar per day, a gauge as one row. Number columns carry
cellDataType: 'number' and the chart's valueFormat (a right-axis series
its axis's), a date axis a date column, so the grid reads the numbers the way
the chart does. The gallery demos' Chart | Grid switch is this behind a
sortable grid:
<script lang="ts">
import { SvChart, SvGrid, chartSpecToTable, type ChartSpec, type GridColumns } from '@svgrid/grid'
const spec: ChartSpec = {
type: 'bar',
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
series: [
{ label: 'Americas', values: [55, 58, 64, 78] },
{ label: 'EMEA', values: [39, 42, 45, 57] },
],
valueFormat: 'currency',
height: 240,
}
let view = $state<'chart' | 'grid'>('chart')
const table = $derived(chartSpecToTable(spec))
</script>
<div style="display: inline-flex; gap: 6px; margin-bottom: 6px">
<button type="button" onclick={() => (view = 'chart')} aria-pressed={view === 'chart'}>Chart</button>
<button type="button" onclick={() => (view = 'grid')} aria-pressed={view === 'grid'}>Grid</button>
</div>
{#if view === 'chart'}
<SvChart {spec} legend="bottom" />
{:else}
<SvGrid data={table.rows} columns={table.columns as GridColumns<Record<string, unknown>>} sortable fitColumns showRowSelection={false} containerHeight={240} />
{/if}
Export
Download the rendered chart as a standalone SVG, a PNG or a one-page PDF, or
print it. Pass the chart's wrapper element (or its <svg>):
<div bind:this={chartEl}><SvGridChart {spec} /></div>
<button onclick={() => downloadChartSvg(chartEl, 'chart.svg')}>SVG</button>
<button onclick={() => downloadChartPng(chartEl, 'chart.png', { scale: 2 })}>PNG</button>
<button onclick={() => downloadChartPdf(chartEl, 'chart.pdf', { title: spec.title, caption: 'Source: ledger' })}>PDF</button>
<button onclick={() => printChart(chartEl, { title: spec.title })}>Print</button>
chartToSvgString / chartToPngBlob / chartToPdfBlob return the data if
you want to upload it instead. The export inlines the live theme colors, so it
matches what's on screen. The chart's toolbar, its context menu and the grid
panel's export menu offer all of them.
PDF and print
The PDF needs no library: the chart is rasterised through the PNG path and
placed on an A4 or Letter page (page), landscape or portrait (whichever
fits the chart's aspect, or orientation), with the title and
subtitle above it and the caption at the foot. quality sets the
JPEG quality (default 0.92); background paints behind the chart, since a
JPEG has no transparency. buildChartPdf(jpeg, options) is the writer
itself, for a caller that already has the image bytes.
printChart(el, { title }) opens a window with the standalone SVG sized to
the page and calls print() once it has drawn; it returns false when
the popup was blocked, so the caller can say so.
CSV
chartSpecToCsv(spec) writes the data behind the picture: one row per category,
one column per series. A series carrying more than one number per category
widens rather than losing them:
| Series shape | Columns |
|---|---|
plain values |
<label> |
ohlc |
<label> Open, High, Low, Close |
boxes |
<label> Min, Q1, Median, Q3, Max, Outliers |
errors |
<label>, <label> Low, <label> High |
Scatter has no categories, so it exports one row per point instead
(Series, X, Y, Size, Label).
chartCsvExportable(spec) answers whether there is anything to write, without
building it - which is what the panel's CSV menu item is disabled on. Sankey,
treemap, gauge and calendar have no rectangular data and still export nothing.
More examples
AI: chart this
Describe the chart in words and the panel builds it; Explain reads the chart
back in two or three insights grounded on chartSummary. Both come from
enableAiCharting(api) on the AI toolkit.
Chart view of the grid
See also
- Charts: getting started
- Charting a pivot: the panel in pivot mode, the pivot designer's Chart view and
pivotToChartSpec - Export
- AI toolkit
Live examples
- Built-in charting (one prop) - Turn on the built-in Chart panel with a single charting prop - no external library. Pick Group by / Value, choose a type, filter a column or click a bar, and the chart re-aggregates over the grid's current (filtered / sorted) rows live.
- AI: chart this - Open the Chart panel, press the AI button, and describe the chart in words - the model reads the grid's column schema and returns a ChartSpec the built-in panel renders. Ships with a deterministic mock provider; swap in your own via setAIProvider.
- 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.