| name | vistral |
| description | Use when building React streaming data visualizations with @timeplus/vistral — creating charts, writing VistralSpec, using VistralChart, configuring streaming data sources, or wiring up live data via ChartHandle. |
Vistral — Streaming Data Visualization
Overview
@timeplus/vistral is a React library for real-time streaming charts built on AntV G2. Two APIs exist — pick the right one before writing any code.
Which API to Use
Need custom marks / full G2 control?
→ Grammar API: VistralChart + VistralSpec
Want a quick chart with minimal config?
→ Config API: StreamChart + TimeSeriesConfig / BarColumnConfig / etc.
AI agents should default to the Grammar API — it's more expressive and maps cleanly to spec-driven generation.
Grammar API (VistralChart + VistralSpec)
Component
import { VistralChart, type ChartHandle } from '@timeplus/vistral';
import type { VistralSpec } from '@timeplus/vistral';
const ref = useRef<ChartHandle>(null);
<VistralChart
ref={ref}
spec={spec}
source={dataSource} // optional: declarative initial data
height={400}
theme="dark" // string | VistralTheme — 'dark', 'light', registered name, or VistralTheme object
onReady={(handle) => {
handle.append(rows); // add rows, re-render
handle.replace(rows); // replace all data, re-render
handle.clear(); // empty buffer, re-render
handle.g2; // raw G2 instance
}}
/>
VistralSpec Fields
| Field | Type | Description |
|---|
marks | MarkSpec[] | Required. Visual elements: line, area, interval, point, rect, text, etc. |
scales | Record<string, ScaleSpec> | Scale config per channel (x, y, color). Per-mark scales override. |
transforms | TransformSpec[] | Visual transforms: stackY, dodgeX, bin, group |
temporal | TemporalSpec | Streaming time control (see below) |
streaming | StreamingSpec | maxItems, mode, throttle |
axes | AxesSpec | { x?, y? } — title, grid, label format/rotate |
legend | LegendSpec | false | { position?, interactive? } or false to hide |
tooltip | TooltipSpec | false | { title?, items[] } — items[].format maps to G2 valueFormatter |
coordinate | CoordinateSpec | polar, theta, transpose, etc. |
annotations | AnnotationSpec[] | Reference lines, ranges, text overlays |
interactions | InteractionSpec[] | { type: 'tooltip' | 'brush' | ... } |
theme | string | VistralTheme | Spec-level theme — overridden by component theme prop. Prefer setting theme on the component. |
animate | boolean | Disable for streaming: animate: false |
g2Overrides | Record<string, unknown> | Raw G2 options deep-merged last — override anything |
Common Mark Pattern
const spec: VistralSpec = {
marks: [{
type: 'line',
encode: { x: 'time', y: 'value', color: 'series' },
scales: { x: { type: 'time' } },
style: { shape: 'smooth' },
}],
temporal: { mode: 'axis', field: 'time', range: 5 },
axes: {
y: { labels: { format: (v) => `${Number(v).toFixed(1)}` } }
},
tooltip: {
items: [{ field: 'value', name: 'Value', format: (v) => `${v}` }]
},
animate: false,
theme: 'dark',
};
Temporal Modes
| Mode | When to use | Key fields |
|---|
axis | Sliding time window on X | field: 'time', range: 5 (minutes) |
frame | Latest timestamp only | field: 'time' |
key | Latest value per entity | field: 'id' or field: ['region','server'] |
g2Overrides — Escape Hatch
Use for any G2 option not modeled by VistralSpec:
g2Overrides: {
axis: { y: { tickCount: 5 } },
paddingLeft: 60,
tooltip: {
items: [{ channel: 'y', valueFormatter: (v) => formatBps(v) }]
}
}
Custom Themes
import { registerTheme, type VistralTheme } from '@timeplus/vistral';
registerTheme('corporate', {
extends: 'light',
palette: ['#0066CC', '#FF6600', '#00AA44'],
font: { family: 'Roboto, sans-serif', size: 12 },
axis: { grid: { color: '#E0E0E0', dash: [4, 4] }, label: { color: '#333' } },
tooltip: { background: '#FFF', text: { color: '#111' }, border: { color: '#E0E0E0' } },
legend: { label: { color: '#333' } },
} satisfies VistralTheme);
<VistralChart spec={spec} theme="corporate" />
<StreamChart config={config} data={data} theme="corporate" />
<VistralChart spec={spec} theme={{ palette: ['#FF73B6', '#8890FF'], axis: { grid: { color: '#1A1A2E' } } }} />
Built-in themes: 'dark' (default) and 'light'. Custom themes deep-merge onto the base.
StreamDataSource Format
import type { StreamDataSource } from '@timeplus/vistral';
const source: StreamDataSource = {
columns: [
{ name: 'time', type: 'datetime64' },
{ name: 'value', type: 'float64' },
{ name: 'series', type: 'string' },
],
data: [
{ time: '2024-01-01T00:00:00Z', value: 42.5, series: 'A' },
],
};
Column types: string, number, datetime, datetime64, float32, float64, int32, int64, boolean
Streaming Pattern (Grammar API)
function LiveChart() {
const handleRef = useRef<ChartHandle>(null);
const loadedRef = useRef(false);
useEffect(() => {
if (!loadedRef.current && handleRef.current) {
loadedRef.current = true;
handleRef.current.append(historicalRows);
}
}, []);
useEffect(() => {
const id = setInterval(() => {
handleRef.current?.append(newRows);
}, 1000);
return () => clearInterval(id);
}, []);
return <VistralChart ref={handleRef} spec={spec} height={400} theme="dark" />;
}
Config API (StreamChart) — Quick Reference
import { StreamChart, useStreamingData } from '@timeplus/vistral';
import type { TimeSeriesConfig, StreamDataSource } from '@timeplus/vistral';
function QuickChart() {
const { data, append } = useStreamingData<Record<string, unknown>>([], 1000);
const config: TimeSeriesConfig = {
chartType: 'line',
xAxis: 'time',
yAxis: 'value',
color: 'series',
lineStyle: 'curve',
legend: true,
temporal: { mode: 'axis', field: 'time', range: 5 },
};
return <StreamChart config={config} data={{ columns, data }} theme="dark" />;
}
Critical Gotchas
| Mistake | Fix |
|---|
useStreamingData<Row[]> (wrong generic) | Use useStreamingData<Row> — element type, not array |
<StreamChart ref={...}> | StreamChart has NO ref/ChartHandle. Use VistralChart for imperative append/replace/clear. |
append([singleRow]) when row is already an array | Wrap: append([[singleRow]]) — append treats top-level array as multiple items |
No loadedRef guard when seeding history | React 18 Strict Mode runs effects twice → doubled rows |
tooltip.items[].format not working | format IS supported — translated to G2's valueFormatter internally (v0.1.6+). Use g2Overrides.tooltip only for raw G2 tooltip features beyond what TooltipSpec models. |
legend.interactive silently ignored | Use legend: { interactive: true } — now translated correctly |
Axis Label Formatting
axes: {
y: {
title: 'Throughput',
labels: {
format: (v) => {
const n = Number(v);
if (n >= 1e9) return `${(n/1e9).toFixed(1)} Gbps`;
if (n >= 1e6) return `${(n/1e6).toFixed(1)} Mbps`;
if (n >= 1e3) return `${(n/1e3).toFixed(1)} Kbps`;
return `${n.toFixed(0)} bps`;
}
}
}
}