Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Real-time dashboards over WebSocket

A real-time dashboard is a set of tables and charts which stay current as the data behind them changes, without the user reloading. Perspective is built for this: a Table accepts streaming update() calls, every View over it — grouped, pivoted, filtered or sorted — is maintained incrementally, and <perspective-viewer> repaints only what changed.

There is no polling and no query re-execution. An update of 50 rows to a 10 million row table costs work proportional to the 50 rows.

Architecture

  1. A server process owns the Table and writes to it as new data arrives — from a message queue, a market data feed, a database change stream, or a timer.
  2. The server exposes that Table by name on a WebSocket endpoint.
  3. Each browser opens the Table by name and loads it into a <perspective-viewer>. The user configures their own grouping, filters and chart type; each browser gets its own View.

Perspective offers two ways to split this work between server and browser, covered in Data Architecture:

  • Client/server replicated — the browser keeps a synchronized copy of the table in WebAssembly. Queries run locally, so interaction is instant and the server only ships deltas. Best when the dataset fits in browser memory.
  • Server only — queries run on the server and the browser receives only the visible window of rows. Best for very large tables or thin clients.

A Python server

import threading
import time

import tornado.ioloop
import tornado.web
from perspective import Server
from perspective.handlers.tornado import PerspectiveTornadoHandler

server = Server()
client = server.new_local_client()
table = client.table(
    {"symbol": "string", "price": "float", "time": "datetime"},
    name="prices",
)

def feed():
    while True:
        table.update(next_batch())
        time.sleep(0.05)

threading.Thread(target=feed, daemon=True).start()

app = tornado.web.Application([
    (r"/websocket", PerspectiveTornadoHandler, {"perspective_server": server}),
])

app.listen(8080)
tornado.ioloop.IOLoop.current().start()

Perspective’s Python API is thread-safe and releases the GIL, so the feed can run on its own thread; see Multithreading. Handlers are also provided for Starlette/FastAPI and aiohttp.

The browser

<perspective-viewer id="viewer"></perspective-viewer>

<script type="module">
    import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer/dist/cdn/perspective-viewer.js";
    import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-datagrid/dist/cdn/perspective-viewer-datagrid.js";
    import "https://cdn.jsdelivr.net/npm/@perspective-dev/viewer-charts/dist/cdn/perspective-viewer-charts.js";
    import perspective from "https://cdn.jsdelivr.net/npm/@perspective-dev/client/dist/cdn/perspective.js";

    const websocket = await perspective.websocket("ws://localhost:8080/websocket");
    const table = await websocket.open_table("prices");
    const viewer = document.getElementById("viewer");
    await viewer.load(table);
    await viewer.restore({
        plugin: "Y Line",
        group_by: ["time"],
        split_by: ["symbol"],
        columns: ["price"],
    });
</script>

This is server-only mode. For replicated mode, create a View on the server table and build a local table from it — worker.table(server_view) — as shown in Hosting a WebSocket server.

Keeping a rolling window

For feeds which never end, bound the table. An index makes updates replace rows by key (latest price per symbol); a limit keeps only the most recent n rows (a rolling tick history).

A Node.js server

The same server can be written in Node.js with WebSocketServer, or in Rust — see the rust-axum example.

See it running

  • Market — a simulated order book streaming into a blotter, depth chart and candlestick chart.
  • python-tornado-streaming — the complete version of the server above.