top of page

Introducing Vistral: A Grammar of Graphics for Streaming Data

  • Writer: Gang Tao
    Gang Tao
  • 12 hours ago
  • 7 min read

We live in a world of streaming data. Kafka topics, IoT sensors, clickstreams, live trades — data doesn't sit still anymore. And yet, when it comes to visualizing that data in real-time, we're still stuck with tools designed for a static world.


Today, I'm excited to announce the open-source of Vistral — a TypeScript library that brings the Grammar of Graphics to streaming data. The name is a portmanteau of Visual + Stream. It's what we've been building internally at Timeplus to power our streaming dashboards, and now it's available for every developer under open source Apache 2.0 license.




Why Streaming Data Needs Its Own Visualization Layer


Here's the thing about real-time data: it never stops. A traditional chart takes a complete dataset, computes scales, renders pixels, and you're done. But streaming data keeps arriving. Your axes need to shift. Old points need to expire. Aggregations need to update incrementally.


Most developers deal with this by wrapping a setInterval around a static charting library — fetch new data every N seconds, destroy the chart, rebuild it. It works, sort of. But it's wasteful, janky, and forces you to manage all the streaming complexity in application code: how much data to keep, when to expire it, how to handle late-arriving events.


I've spent 26+ years working on data systems, from SAP to Splunk to building Timeplus. One thing became clear to me: the gap between streaming processing (which is mature — Flink, Kafka Streams, our own Proton) and streaming visualization (which is not) keeps growing. We solved streaming SQL. Now it's time to bring it to streaming charts.



What’s Missing from Existing Solutions


I've evaluated practically every visualization library out there. They fall into two camps, and neither solves the problem.


Camp 1: Great grammar, no streaming. 


  • Vega-Lite is brilliant — a declarative JSON spec rooted in the Grammar of Graphics. But try pushing 50,000 data points through its view.change() API with a setInterval and it slows to a crawl. 

  • Observable Plot, Mike Bostock's elegant successor to D3 for Grammar of Graphics, has no incremental update at all — the docs tell you to remove the entire SVG and re-render. 

  • ggplot2, the most influential Grammar of Graphics implementation, is entirely batch-oriented in R. no streaming support


Camp 2: Handles streaming, no grammar. 


  • FINOS Perspective (originally from J.P. Morgan) is impressive for streaming — WebAssembly engine, Apache Arrow, virtual scroll. But it's a pivot-table widget, not a composable grammar. 

  • Apache ECharts can handle millions of points with its incremental rendering, but its API is a monolithic setOption() blob with no streaming abstractions. 

  • Grafana Live pushes real-time via WebSocket, but it's a platform, not a library you can embed.


The result? If you want a composable, declarative way to describe a streaming visualization — the way ggplot2 lets you describe a static one — nothing exists. That's the gap Vistral fills.



The Grammar of Graphics: A Quick Primer


In 1999, Leland Wilkinson published The Grammar of Graphics, which decomposed any chart into independent components: data, transformations, scales, coordinate systems, geometric marks, and guides (axes/legends). The insight was that a bar chart and a pie chart aren't fundamentally different chart "types" — they're the same grammar with a different coordinate system (Cartesian vs. polar).


This idea was transformative. Hadley Wickham built ggplot2 on it and changed how a generation of data scientists thinks about visualization. Vega-Lite brought it to the web as a portable JSON spec. Observable Plot simplified it for JavaScript.


But all of these implementations share one assumption: the data is complete and bounded at render time. The entire pipeline — data → transform → scale → render — assumes you have everything upfront.


Streaming data breaks this assumption in fundamental ways. Your scale domains are infinite and shifting. Aggregations run over windows, not the full dataset. Data has a lifecycle — it arrives, ages, and expires. And there's the whole question of event-time vs. processing-time that stream processing engines solved years ago but visualization grammars have never addressed.



How Vistral Extends the Grammar: Temporal Binding


This is the core design idea behind Vistral. We introduce a concept called temporal binding — a new grammar primitive that defines how streaming data maps to the visual frame. It answers the question: given an infinite stream, what finite slice does this chart show right now?


Vistral provides three temporal binding modes:


1) Axis-bound 

Time is mapped to the X-axis with a sliding window. You declare something like "show the last 5 minutes" and the chart automatically advances as new data arrives and old data scrolls off the left edge. This is your classic time-series trend.



The grammar:

temporal: {
  mode: 'axis',
  field: 'timestamp',
  range: 5, // last 5 minutes
}

Design principles:

  • Recency Priority: Recent data is more important. The visible window keeps the most recent N minutes/seconds.

  • Smooth Scrolling: Data appears to flow across the canvas as time progresses, creating intuitive temporal understanding.

  • Configurable Window: Users specify the time range (e.g., 30 seconds, 5 minutes, 1 hour) based on their monitoring needs.

  • Direction Flexibility: While left-to-right (X-axis) is most common, the grammar supports:

    • X-axis: left-to-right or right-to-left

    • Y-axis: bottom-to-top or top-to-bottom



2) Frame-bound

Only the latest timestamp is visible. Every time new data arrives at a new timestamp, the previous frame is replaced. In frame-bound mode, time acts as a playback controller rather than a visual dimension. The visualization displays only data from the current timestamp, like frames in a movie. As time advances, the entire canvas updates to show the new frame.



The grammar:

temporal: {
  mode: 'frame',
  field: 'timestamp',
}

Design principles:

  • Complete Replacement: Each new timestamp completely replaces the previous visualization. No blending or transition between frames.

  • Temporal Filtering: Data is filtered by timestamp before any other processing. Only rows matching the current frame are rendered.

  • Animation Potential: Frame-bound mode naturally supports animation—replaying historical data as a sequence of frames.

  • Snapshot Semantics: Each frame represents a complete snapshot of the system state at that moment.



3) Key-bound 

The chart keeps the latest value for each unique key, with support for composite keys. This is like a Timeplus mutable stream: as updates stream in for a given entity, the chart reflects current state. Perfect for live dashboards showing per-device metrics, per-region stats, or geo maps tracking moving objects.



The grammar:

temporal: {
  mode: 'key',
  field: ['region', 'vehicle_id'], // composite key
}

Design principles:

  • Entity Identity: Each key represents a distinct entity whose state evolves over time.

  • State Replacement: New data for a key completely replaces old data—no history accumulation per key.

  • Bounded Growth: The canvas size is bounded by the number of unique keys, not by time or data volume.

  • Eventual Consistency: The visualization always reflects the latest known state of each entity.


These aren't just API options — they're grammar-level primitives that compose with everything else: chart type, scales, colors, legends. The same temporal binding works whether you're rendering a line chart, a geo map, or a data table.


If you've worked with streaming SQL, this should feel familiar. Axis-bound is analogous to a tumble/hop window. Frame-bound is like LATEST_BY. Key-bound is a changelog stream or mutable stream. We're bringing the same temporal semantics that streaming databases understand to the visualization layer.



What Vistral Looks Like in Practice


Vistral is a React component library built on top of AntV G2 (a Grammar of Graphics engine). 


Vistral provides two API levels:

  • Grammar API (VistralSpec + VistralChart) — composable, low-level, full control over marks, scales, transforms, coordinates, and streaming behavior.

  • Chart Config API (TimeSeriesConfig, BarColumnConfig, etc. + StreamChart) — high-level, opinionated presets that compile down to VistralSpec internally.


You can get a streaming chart up in a few lines:

import { StreamChart } from '@timeplus/vistral';

<StreamChart
  config={{
    chartType: 'line',
    xAxis: 'timestamp',
    yAxis: 'cpu_usage',
    color: 'host',
    temporal: { mode: 'axis', field: 'timestamp', range: 5 },
    legend: true,
  }}
  data={streamData}
  theme="dark"
/>

It supports line, area, bar, column, single-value metrics (with sparkline and delta indicators), data tables with conditional coloring and mini-charts, and geo maps with dynamic point sizing. The useStreamingData hook manages the data buffer for you:

const { data, append, clear } = useStreamingData([], 1000);

// In your WebSocket handler:
ws.onmessage = (event) => append(JSON.parse(event.data));

Or you can just create a lower level streaming visualization using the grammar API:

import { VistralChart, type VistralSpec } from '@timeplus/vistral';

const spec: VistralSpec = {
  marks: [{ type: 'line', encode: { x: 'time', y: 'value' } }],
  scales: { x: { type: 'time' }, y: { type: 'linear', nice: true } },
  temporal: { mode: 'axis', field: 'time', range: 5 },
  streaming: { maxItems: 2000 },
  theme: 'dark',
  animate: false,
};

<VistralChart spec={spec} source={dataSource} height={400} />

Try them all at the live playground!





What This Means for Data Application Developers


If you're building any application that shows real-time data — monitoring dashboards, IoT control panels, live analytics, trading UIs — Vistral gives you three things:


A declarative API instead of imperative hacks. Stop writing setInterval + destroy + rebuild loops. Declare what your chart should look like with temporal semantics, feed it data, and let the library handle the rest.


Streaming semantics at the visualization layer. Window types, data expiration, key-based deduplication — concepts that previously lived only in your stream processing engine or application code now live in the chart spec itself.


An embeddable library, not a platform. npm install @timeplus/vistral and you're done. It's a React component that fits into your existing app. No separate Grafana instance, no iframe hacks, no vendor lock-in.


Vistral works with any data source. It's designed to pair naturally with Proton (our open-source streaming SQL engine) via Server-Sent Events, but there's zero coupling — use it with WebSockets, Kafka consumers, polling, or any other data transport you have.



Get Involved


Vistral open sourced under Apache 2.0 license, and we're just getting started. We are looking for developers who want to join us for this open source project.



If you're building real-time data applications and have opinions about how streaming visualization should work, I'd love to hear from you. Open an issue, submit a PR, or just star the repo and try the playground.


The Grammar of Graphics changed how we think about static charts. It's time to do the same for streaming.

 
 
bottom of page