SvGrid Studio · chart
A live chart panel beside the grid, driven by the same data source: rowsToChartSpec aggregates the rows (group by tier / active, reduce MRR by sum / avg / count) and SvGridChart renders bar / pie / line - no external chart library. The chart doubles as a filter control: click a bar or slice to filter the grid to that category. Create / edit / delete and the chart updates. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Studio). See the SvGrid documentation for the full API.
What this example shows
Data-app Studio · chart - a grid + a live chart panel side by side.
`SvSchemaChart` is bound to the same `EntitySchema` + data source as the grid. It picks chart-able fields from the schema, renders its own controls, and aggregates via the source's `getAggregate` (a GROUP BY - server-side, so it scales past a page). Click a bar / slice to filter the grid; create / edit / delete and bump `refreshKey` and the chart re-aggregates.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise, ../shared/SvGridStudio.svelte
Source code (197-studio-chart.svelte)
<script lang="ts">
/**
* Data-app Studio · chart - a grid + a live chart panel side by side.
*
* `SvSchemaChart` is bound to the same `EntitySchema` + data source as the
* grid. It picks chart-able fields from the schema, renders its own controls,
* and aggregates via the source's `getAggregate` (a GROUP BY - server-side, so
* it scales past a page). Click a bar / slice to filter the grid; create / edit
* / delete and bump `refreshKey` and the chart re-aggregates.
*/
import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
import { SvSchemaChart, SvGridEditPanel, createInMemoryDataSource, schemaToColumns, type EntitySchema } from '@svgrid/enterprise'
import SvGridStudio from '../shared/SvGridStudio.svelte'
type Customer = { id: string; name: string; email: string; tier: string; mrr: number; active: boolean }
const schema: EntitySchema<Customer> = {
name: 'customers', label: 'Customer', idField: 'id',
fields: [
{ field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: true } },
{ field: 'name', type: 'text', required: true, minLength: 2 },
{ field: 'email', type: 'text', label: 'Email', required: true, format: 'email' },
{ field: 'tier', type: 'enum', options: [
{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }, { value: 'enterprise', label: 'Enterprise' },
] },
{ field: 'mrr', type: 'number', label: 'MRR ($)', min: 0 },
{ field: 'active', type: 'boolean' },
],
}
const seed: Customer[] = [
{ id: 'c1', name: 'Ada Lovelace', email: '[email protected]', tier: 'enterprise', mrr: 1200, active: true },
{ id: 'c2', name: 'Alan Turing', email: '[email protected]', tier: 'pro', mrr: 240, active: true },
{ id: 'c3', name: 'Grace Hopper', email: '[email protected]', tier: 'enterprise', mrr: 980, active: true },
{ id: 'c4', name: 'Edsger Dijkstra', email: '[email protected]', tier: 'free', mrr: 0, active: false },
{ id: 'c5', name: 'Barbara Liskov', email: '[email protected]', tier: 'pro', mrr: 300, active: true },
{ id: 'c6', name: 'Donald Knuth', email: '[email protected]', tier: 'enterprise', mrr: 1500, active: true },
{ id: 'c7', name: 'Margaret Hamilton', email: '[email protected]', tier: 'pro', mrr: 420, active: true },
{ id: 'c8', name: 'Tim Berners-Lee', email: '[email protected]', tier: 'enterprise', mrr: 1100, active: true },
{ id: 'c9', name: 'Linus Torvalds', email: '[email protected]', tier: 'pro', mrr: 360, active: true },
{ id: 'c10', name: 'Katherine Johnson', email: '[email protected]', tier: 'enterprise', mrr: 890, active: true },
{ id: 'c11', name: 'Dennis Ritchie', email: '[email protected]', tier: 'pro', mrr: 275, active: false },
{ id: 'c12', name: 'Frances Allen', email: '[email protected]', tier: 'free', mrr: 0, active: false },
]
const columns = schemaToColumns(schema)
const source = createInMemoryDataSource(seed, schema)
let view = $state<ServerState<Customer>>({
rows: [], total: 0, loading: false, saving: false, error: null,
pageIndex: 0, pageSize: 8, pageCount: 1, sortModel: [], filterModel: {},
})
const controller = createServerDataSource(source, {
pageSize: 8, optimistic: true, getRowId: (r) => r.id, onChange: (s) => (view = s),
})
controller.refresh()
let chartRev = $state(0) // bump to re-aggregate after a mutation
const fmt = (v: number) => (v >= 1000 ? '$' + (v / 1000).toFixed(1) + 'k' : String(Math.round(v)))
// Click a chart category -> filter the grid to it (the chart stays an overview).
let activeFilter = $state<{ dim: string; value: string } | null>(null)
function drill(category: string, dimension: string) {
activeFilter = { dim: dimension, value: category }
controller.setFilter({ columns: { [dimension]: { operator: 'equals', value: category } } })
}
function clearFilter() {
activeFilter = null
controller.setFilter({})
}
let editing = $state<Customer | null | undefined>(undefined)
let genId = 13
async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<Customer> }) {
if (mode === 'create') { await controller.createRow({ id: `c${genId++}`, ...values }); controller.setPage(view.pageCount - 1) }
else if (id) { await controller.updateRow(id, values) }
editing = undefined
chartRev++
}
</script>
<SvGridStudio
title="Chart"
subtitle="A live <code>SvSchemaChart</code> beside the grid, aggregating the same source server-side (<code>getAggregate</code>). Click a bar / slice to filter the grid."
>
{#snippet toolbar()}
<button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New customer</button>
{#if activeFilter}
<button class="st-chip" onclick={clearFilter}>{activeFilter.dim} = {activeFilter.value} ✕</button>
{/if}
<span class="st-hint" style="margin-left:auto">Double-click a row to edit</span>
{/snippet}
<div class="chart-demo__split">
<div class="chart-demo__grid">
<SvGrid responsive={true}
data={view.rows}
{columns}
loading={view.loading}
fitColumns
enableRowSummaries={false}
showRowSelection
sortable
externalSort
onSortingChange={(s) => controller.setSort(s)}
onRowDoubleClick={(e) => (editing = e.row)}
showPagination
externalPagination
rowCount={view.total}
pageIndex={view.pageIndex}
pageSize={view.pageSize}
onPaginationChange={({ pageIndex, pageSize }) => (pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex))}
containerHeight={340}
/>
</div>
<aside class="chart-demo__panel">
<SvSchemaChart
{schema}
getAggregate={(req) => source.getAggregate(req)}
refreshKey={chartRev}
formatValue={fmt}
onDrill={drill}
/>
</aside>
</div>
{#if editing !== undefined}
<SvGridEditPanel {schema} row={editing} presentation="modal" persistKey="studio" onSubmit={save} onCancel={() => (editing = undefined)} />
{/if}
</SvGridStudio>
<style>
.chart-demo__split { display: flex; flex-wrap: wrap; gap: 14px; align-items: flex-start; }
.chart-demo__grid { flex: 1 1 420px; min-width: 0; }
.chart-demo__panel { flex: 0 0 500px; max-width: 100%; border: 1px solid var(--sg-border, #e6e8ec); border-radius: 16px; padding: 16px; background: var(--sg-bg, #fff); box-shadow: 0 1px 2px rgba(15,23,42,.05), 0 12px 28px -14px rgba(15,23,42,.22); }
</style>Related documentation
Related articles
- SvGrid Studio Tips and Tricks: Build Svelte Data Apps, Fast - Practical SvGrid Studio tips - AI app generation, a designer that emits real SvelteKit, schema-driven edit forms, code-behind with ctx.grid, production auth/data/deploy toggles, and real data-source binding - each with a snippet.
- Reducing Re-Renders in SvGrid with $derived - Svelte 5's $derived memoizes your row pipeline so the grid recomputes only when filter inputs actually change - not on every unrelated state update.
- Progress and Percentage Bar Cells in SvGrid - Build in-cell progress bars in your Svelte 5 data grid - with color thresholds, accessible markup, and sorting that still works.
More Studio examples
- SvGrid Studio · live SQL - The Studio stack backed by a REAL Postgres running in the browser via PGlite (WASM), no server. createSqlDataSource turns the grid's sort / filter / page requests into parameterized SQL run through PGlite; the executed query is shown live under the toolbar. Full CRUD with optimistic updates against actual Postgres.
- Northwind on PGlite - The classic Northwind sample DB (categories, customers, products, orders, order_details) seeded into a real in-browser Postgres (PGlite, WASM). Switch between a five-table JOIN exposed as a read-only VIEW and the editable base tables - each via createSqlDataSource. Sort / filter / page / edit all run as parameterized SQL; new rows get an auto id from a Postgres IDENTITY sequence.
- SvGrid Studio · Supabase - The Studio stack over hosted Postgres on Supabase, straight from the browser via supabase-js (PostgREST) and your project's public anon key. createSupabaseDataSource maps the grid's sort / filter / page / CRUD onto the query builder, introspectSupabaseTable adapts to any table AND detects foreign keys (a FK column auto-becomes a searchable lookup in the form), and createSupabaseRealtime makes it LIVE - change a row in the Supabase dashboard and it flashes in the grid (toggle the Live pill). Paste your URL + anon key, run the one-time setup SQL, done. RLS keeps the anon key safe.
- SvGrid Studio · relations - Foreign-key lookup fields end to end. Contacts belong to a Company: the contact form renders a searchable Company picker (SvLookupInput + createRelationLookup) that queries the Companies source and stores the id, while the grid shows the resolved company NAME. The lookup runs over the same ServerDataSource contract, so related options can come from Supabase / REST / SQL / in-memory (here both entities are in-memory).
- SvGrid Studio · secured - A secured screen: SvAuthGate requires a signed-in user (Supabase Auth via createSupabaseAuth) before showing the Studio grid, with a login / sign-up form and a signed-in bar. Uses a mock auth client so any email + password works here; swap for your supabase-js client and Row-Level Security scopes each user to their own rows (auth = who, RLS = what).