Funnel chart (signup conversion)
type: funnel renders strictly-decreasing values as a stack of trapezoids. Each segment shows conversion vs. the top of the funnel inline; hover for the step drop-off. Click a segment to record a drill selection.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
A signup conversion funnel in Svelte 5. type funnel renders strictly decreasing values as a stack of trapezoids where each level is narrower than the one above in proportion to its value; every segment shows its count and the cumulative conversion against the top of the funnel, hovering shows the step drop-off, and clicking a segment records a drill selection through onSelect that filters the grid to that stage.
`type: 'funnel'` renders a series of strictly-decreasing values as a stack of trapezoids - level N+1 is automatically narrower than level N proportional to its value. Each segment shows the absolute count plus the cumulative conversion vs. the top of the funnel; hover for the step drop-off. Click any segment to drill the grid to that stage.
Imports, features and API used
Imports: @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: stage (Stage), users (Users), note (Definition)
SvGridApi methods called: api.getDisplayedRows()
Frequently asked questions
What if the values are not decreasing?
A funnel assumes each stage is a subset of the previous one. Sort or aggregate the rows so the stages descend before building the spec; the chart draws each segment in proportion to its value either way.
What does hover show?
The tooltip lists the stage count, the conversion from the top of the funnel and the drop-off from the previous step.
How does drill work here?
onSelect receives the clicked stage; the demo filters the grid's stage column to it so the rows behind the segment are listed.
Related documentation
Source code (160-chart-funnel.svelte)
<!-- Documented in: docs/help/charts.md -->
<script lang="ts">
/**
* 160. Funnel chart (signup conversion)
* --------------------------------------
* `type: 'funnel'` renders a series of strictly-decreasing values as a
* stack of trapezoids - level N+1 is automatically narrower than level
* N proportional to its value. Each segment shows the absolute count
* plus the cumulative conversion vs. the top of the funnel; hover for
* the step drop-off. Click any segment to drill the grid to that stage.
*/
import {
SvGrid,
SvGridChart,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
type SvGridApi,
type ChartSpec,
type ChartSelection,
} from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
type Row = { id: number; stage: string; users: number; note: string }
const stages = [
{ stage: 'Visited', users: 12_400, note: 'All visits to /pricing' },
{ stage: 'Signed up', users: 3_180, note: 'Free trial start' },
{ stage: 'Verified email',users: 2_640, note: 'Clicked the activation link' },
{ stage: 'Onboarded', users: 1_790, note: 'Finished the 4-step setup' },
{ stage: 'Activated', users: 1_250, note: 'Created their first project' },
{ stage: 'Paid', users: 432, note: 'Upgraded to a paid plan' },
]
const rows: Row[] = stages.map((s, id) => ({ id, ...s }))
const columns: GridColumns<Row> = [
{ field: 'stage', header: 'Stage', width: 160 },
{ field: 'users', header: 'Users', width: 110, align: 'right',
format: { type: 'number', options: { maximumFractionDigits: 0 } } },
{ field: 'note', header: 'Definition', width: 280 },
]
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let displayed = $state<Row[]>(rows)
let highlighted = $state<string | null>(null)
function sync() { displayed = (api?.getDisplayedRows() as Row[]) ?? rows }
const compact = (v: number) => (Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(v % 1e3 ? 1 : 0) + 'k' : String(Math.round(v)))
const spec = $derived.by<ChartSpec>(() => ({
type: 'funnel',
categories: displayed.map((r) => r.stage),
series: [{
label: 'Users',
values: displayed.map((r) => r.users),
}],
width: 540,
height: 360,
palette: ['#2563eb', '#1d4ed8', '#1e40af', '#1e3a8a', '#312e81', '#3730a3'],
}))
function onSelect(sel: ChartSelection) {
highlighted = sel.category
}
/** Pane size, so the chart fills its card rather than a fixed viewBox. */
let paneW = $state(0)
let paneH = $state(0)
</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);">
Funnel: signup conversion across 6 stages
</p>
<p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
Each trapezoid's width is proportional to its value relative to the top. Conversion shown
inline; hover for the step drop-off vs. the previous stage.
{#if highlighted}<span style="color: var(--sg-accent); margin-left: 8px;">Last clicked: {highlighted}</span>{/if}
</p>
</div>
<div class="flex flex-1 min-h-0 gap-3">
<div class="flex-1 min-w-0 min-h-0">
<SvGrid responsive={true}
columnResize
data={rows}
columns={columns}
features={features}
sortable
filterable
selectionMode="none"
rowHeight={32}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => { api = a; sync() }}
onFiltersChange={sync}
onSortingChange={sync}
/>
</div>
<div class="rounded-lg border p-3" style="flex: 0 1 580px; min-width: 0; min-height: 0; border-color: var(--sg-border); background: var(--sg-bg);">
<!-- Measured box, not the card: its height comes from the parent, so the
chart cannot push the thing it is sized against. -->
<div style="width: 100%; height: 100%; min-height: 0;" bind:clientWidth={paneW} bind:clientHeight={paneH}>
{#if paneW > 40 && paneH > 40}
<SvGridChart {spec} formatValue={compact} {onSelect} width={paneW} height={paneH} />
{/if}
</div>
</div>
</div>
</section>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.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- 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.