Streaming chart (rolling window)
Hit Start - prices stream in at 4 Hz. The buffer holds the last 60 ticks; older points drop off the left as new ones appear on the right. Re-aggregates via rowsToChartSpec so smoothing, brush, zoom all stay in play.
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 streaming chart with a rolling window in Svelte 5. Live data needs no special chart API: the spec is a $derived, so appending a tick to the rows re-runs rowsToChartSpec and re-renders the chart, and capping the array at 60 ticks keeps the last window on screen with older points dropping off the left. Press Start and prices stream at 4 Hz while smoothing, brush and zoom stay available.
A feed hands the chart one tick every 250 ms. The spec is state, and appendPoints(spec, point, { window }) returns the next one: the new category and value appended, the oldest dropped past the window, and every per-category array (row ids, bands, candles if there were any) kept in step. live on the chart skips the data-update tween and the enter effect, which would otherwise restart on every tick and never settle.
The grid keeps its own rows array for the table; the chart no longer rebuilds from it. Hit Start - prices stream in at 4 Hz. The window holds the last 60 ticks; older points drop off the left as new ones appear on the right.
Imports, features and API used
Imports: svelte, @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature
Columns: t (Tick), price (Price)
Frequently asked questions
How is the window kept fixed?
After each tick the rows array is sliced to its last 60 entries before being assigned, so the derived spec always covers the same number of points.
Does re-aggregating every tick cost much?
Not at this size. rowsToChartSpec over 60 rows is a few microseconds, and the chart re-renders only the paths that changed.
Can I keep the grid and chart in sync?
Yes. Both read the same rows array, so the table scrolls the same window the chart draws.
Related documentation
Related articles
- Sparkline Cells in a Svelte Data Grid - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
- Virtual Scrolling Explained - A practical look at how windowed rendering keeps large grids fast, what makes it break, and how to verify it's actually working.
Source code (159-chart-streaming.svelte)
<!-- Documented in: docs/help/charts/interaction.md -->
<script lang="ts">
/**
* 159. Streaming chart with a rolling window
* -------------------------------------------
* A feed hands the chart one tick every 250 ms. The spec is state, and
* `appendPoints(spec, point, { window })` returns the next one: the new
* category and value appended, the oldest dropped past the window, and
* every per-category array (row ids, bands, candles if there were any) kept
* in step. `live` on the chart skips the data-update tween and the enter
* effect, which would otherwise restart on every tick and never settle.
*
* The grid keeps its own rows array for the table; the chart no longer
* rebuilds from it. Hit Start - prices stream in at 4 Hz. The window holds
* the last 60 ticks; older points drop off the left as new ones appear on
* the right.
*/
import { onDestroy } from 'svelte'
import {
SvGrid,
SvGridChart,
appendPoints,
rowsToChartSpec,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
type GridColumns,
type SvGridApi,
type ChartSpec,
} from '@svgrid/grid'
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
type Row = { id: number; t: string; price: number }
const WINDOW = 60
// Seed with a stable starting walk so the chart isn't empty on mount.
let seed = 0x42
const rnd = () => ((seed = (seed * 1103515245 + 12345) >>> 0) / 0xffffffff)
let nextId = 0
let price = 100
function tick(at = Date.now()): Row {
price = Math.max(50, price + (rnd() - 0.5) * 1.8)
const t = new Date(at).toISOString().slice(11, 21) // HH:MM:SS.ms, unique per tick
return { id: nextId++, t, price: Math.round(price * 100) / 100 }
}
// Twenty seed ticks, spaced like the feed will be, so the chart starts with
// twenty categories rather than twenty rows in one instant.
const seedAt = Date.now()
let rows = $state<Row[]>(Array.from({ length: 20 }, (_, i) => tick(seedAt - (20 - i) * 250)))
const columns: GridColumns<Row> = [
{ field: 't', header: 'Tick', width: 110 },
{ field: 'price', header: 'Price', width: 110, align: 'right',
format: { type: 'number', options: { minimumFractionDigits: 2, maximumFractionDigits: 2 } } },
]
let api = $state<SvGridApi<typeof features, Row> | null>(null)
let running = $state(false)
let intervalId: ReturnType<typeof setInterval> | null = null
function start() {
if (intervalId) return
running = true
intervalId = setInterval(() => {
const row = tick()
const next = [...rows, row]
// Roll: drop the oldest when above WINDOW so the table slides too.
rows = next.length > WINDOW ? next.slice(next.length - WINDOW) : next
// The chart takes the one point; appendPoints keeps the window.
spec = appendPoints(spec, { category: row.t, values: [row.price], rowIds: [[row.id]] }, { window: WINDOW })
}, 250)
}
function stop() {
running = false
if (intervalId) { clearInterval(intervalId); intervalId = null }
}
onDestroy(stop)
// Seeded once from the starting rows; every tick appends to it.
let spec = $state<ChartSpec>((() => {
const s = rowsToChartSpec(rows, { type: 'line', category: 't', value: 'price', reduce: 'sum', width: 720, height: 320 })
s.xAxisTitle = 'Tick'
s.yAxisTitle = 'Price'
s.series[0]!.smooth = true
return s
})())
/** 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);">
Streaming chart with a {WINDOW}-tick rolling window
</p>
<p class="mt-0.5 text-xs" style="color: var(--sg-muted);">
Hit Start. Prices stream in at 4 Hz; the line slides left as new ticks arrive. Smoothing is
on so the curve flows between samples. The grid mirrors the same rolling buffer.
</p>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs">
{#if !running}
<button type="button" class="ic-btn" onclick={start}>▶ Start streaming</button>
{:else}
<button type="button" class="ic-btn" onclick={stop}>■ Stop</button>
{/if}
<span style="color: var(--sg-muted);">Buffered: {rows.length} / {WINDOW} ticks</span>
</div>
</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={26}
containerHeight="100%"
fitColumns={true}
onApiReady={(a) => { api = a }}
/>
</div>
<div class="rounded-lg border p-3" style="flex: 0 1 760px; 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} width={paneW} height={paneH} live />
{/if}
</div>
</div>
</div>
</section>
<style>
.ic-btn {
border: 1px solid var(--sg-border);
background: var(--sg-bg);
color: var(--sg-fg);
border-radius: 6px;
padding: 4px 12px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.ic-btn:hover { background: var(--sg-row-hover-bg); }
</style>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.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.