Svelte Trading Grid

A blotter is a grid sorted by something that ticks. Last price, percent change, P&L: the column the desk watches is the column the feed moves, so every update has to land in sorted position and the grid has to do that at the row count a desk actually has, not a demo's dozen.

SvGrid takes a tick feed on one path: the messages wait in a map keyed by instrument, the newest price per id wins the frame, and one applyTransaction per animation frame hands the grid a new array in which only the ticked rows are new objects. The row model reuses every row that did not change and repairs the sort around the ones that did instead of re-sorting 100,000 rows; the cost of that tick is measured and published on the benchmarks page, with the harness that produced it in the repository, next to the number a full sort would have cost.

The rest of the blotter is the grid as it comes: cellFlash restarts a colour fade only when that row's value changes, number formats with negative sections and a currency, sparklines from a rolling buffer, pinned symbol, row virtualization so 100,000 rows keep a few dozen elements in the DOM. For books larger than a browser tab should hold, the Enterprise server row model streams the visible window from your service and applies transactions to it.

Install

npm i @svgrid/grid

Free and MIT-licensed in @svgrid/grid: no license key, no row cap, no watermark.

The code

<script lang="ts">
  import { SvGrid, tableFeatures, rowSortingFeature, type GridColumns, type SvGridApi } from '@svgrid/grid'

  const features = tableFeatures({ rowSortingFeature })
  let rows = $state.raw<Quote[]>(initialQuotes)
  let api = $state<SvGridApi | null>(null)

  // Ticks wait in a map (newest price per id wins the frame) and go through
  // one applyTransaction per animation frame: one data change, one pipeline
  // run, however many messages arrived.
  const pending = new Map<string, number>()
  let frame: number | null = null
  socket.onmessage = (e) => {
    for (const [id, last] of JSON.parse(e.data).t) pending.set(id, last)
    if (frame === null) frame = requestAnimationFrame(flush)
  }
  function flush() {
    frame = null
    const byId = new Map(api!.getData().map((r) => [r.id, r]))
    const update = [...pending].flatMap(([id, last]) => {
      const row = byId.get(id)
      return row ? [{ ...row, last, direction: last > row.last ? 'up' : 'down' }] : []
    })
    pending.clear()
    api!.applyTransaction({ update })
  }

  const columns: GridColumns<Quote> = [
    { field: 'symbol', header: 'Symbol' },
    { field: 'last', header: 'Last', editorType: 'number',
      format: { type: 'number', options: { minimumFractionDigits: 2, maximumFractionDigits: 2 } },
      cellFlash: { className: 'tick' },
      cellClass: (ctx) => (ctx.row.original.direction === 'up' ? 'tick-up' : 'tick-down') },
    { field: 'pct', header: 'Chg %', editorType: 'number' },
  ]
</script>

<SvGrid data={rows} {columns} {features} getRowId={(r) => r.id}
  initialSorting={[{ id: 'pct', desc: true }]} sortable onApiReady={(a) => (api = a)} />

What you get

Live examples

Documentation

Related articles

Frequently asked questions

How many rows can a Svelte data grid keep sorted while prices tick?

The market blotter demo keeps 100,000 rows sorted by percent change with the feed pushing tens of thousands of updates a second, batched per animation frame. The engine cost of a tick, and how it compares to a full sort, is measured on the benchmarks page with the script that produced it.

Should I mutate rows in place or replace them?

For a sorted blotter, replace them: a proxy write updates one cell but the row model never runs, so the sort goes stale. applyTransaction replaces the ticked rows in a new array and the grid repairs the order around them. Mutate in place only when you will refresh with a new array afterwards.

Why batch ticks per animation frame?

The screen paints once per frame, so applying ticks faster than that runs the pipeline for results nobody sees. A map keyed by instrument merges a symbol that ticked five times in a frame into one update with its latest price.

Can I connect it to my own WebSocket feed?

Yes. The blotter demo reads any WebSocket URL you pass, and tools/tick-server.mjs in the repository is a forty-line feed with no dependency that shows the message format, so it doubles as a template for adapting yours.

Is the benchmark comparable to other grids?

The comparison harness in the repository runs the same tick against several grids, on each grid's own update path, and checks after the ticks that every grid kept its sort; a grid that answered a data change by dropping the order is marked, not credited. The results and the method are on the comparison page.

Build something else

Get started · Browse the gallery · Pricing