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