Semiotic is a streaming-first visualization library for React. Every chart is backed by a canvas rendering engine with a push API, ring buffer windowing, and visual encodings for live data (decay, pulse, staleness). Streaming is optional: pass an array and the same pipeline renders a static chart. This guide covers both paths.
Installation
Install Semiotic via npm:
Peer dependencies: Semiotic requires React 18+ andReactDOM 18+. Make sure your project already has these installed.
Semiotic ships with built-in TypeScript type definitions, so no additional@types packages are needed. You get full autocomplete and type checking out of the box.
Your First Chart
This example renders a line chart of monthly sales. ImportLineChart, pass your data, and specify which fields map to the x and y axes:
JSX
import { LineChart } from "semiotic/xy" const data = [ { month: "Jan", sales: 4200 }, { month: "Feb", sales: 5100 }, { month: "Mar", sales: 6800 }, { month: "Apr", sales: 5900 }, { month: "May", sales: 7200 }, { month: "Jun", sales: 8100 }, ] function App() { return ( <LineChart data={data} xAccessor="month" yAccessor="sales" xLabel="Month" yLabel="Sales ($)" /> ) }
Semiotic supplies the axes, scales, hover interactions, and responsive sizing. Props expose each of those choices when the defaults no longer fit.
Streaming Data
Every Semiotic chart uses the same canvas-rendered streaming pipeline. A dataarray enters the pipeline used for live pushes, with automatic chunking and progressive rendering to keep the UI responsive.
For live data, use a ref-based push API. Data is microtask-batched (thousands of pushes per second coalesce into single render frames) and rendered at 60fps with optional visual encodings:
JSX
import { useRef, useEffect } from "react" import { RealtimeLineChart } from "semiotic/realtime" function LiveMetrics() { const ref = useRef() useEffect(() => { const ws = new WebSocket("wss://metrics.example.com") ws.onmessage = (e) => { ref.current?.push(JSON.parse(e.data)) } return () => ws.close() }, []) return ( <RealtimeLineChart ref={ref} timeAccessor="time" valueAccessor="latency" windowSize={500} decay={{ type: "exponential", halfLife: 200 }} pulse={{ duration: 300, color: "#22c55e" }} /> ) }
Decay
Older data fades out. Linear, exponential, or step modes. Per-vertex opacity on lines and areas — not just uniform dimming.
Pulse
New data glows briefly. Configurable duration and color. Three visual modes: circle glow, rect overlay, and path fill.
Staleness
Charts dim when the feed pauses. Configurable threshold and badge position. Automatic recovery when data resumes.
Transitions
Identity-based animation. Nodes matched by stable keys across rebuilds — not array index. Respects prefers-reduced-motion.
The push API works on most HOC charts too — not just Realtime* charts. Omit thedata prop and push via refs:
JSX
// Any HOC chart supports push via refs — just omit the data prop const ref = useRef() // Push from effects, event handlers, or WebSocket callbacks ref.current?.push({ x: 1, y: 42 }) // single point ref.current?.pushMany([...points]) // batch ref.current?.clear() // reset <Scatterplot ref={ref} xAccessor="x" yAccessor="y" />
Choose the Right Layer
Start with Charts. Move to Frames or Utilities when the design needs control that the chart component does not expose.
Charts
20 ready-to-use components like LineChart, BarChart, and Scatterplot. Simple props, instant results. This is the best starting point for most visualizations.
Frames
StreamXYFrame, StreamOrdinalFrame, StreamNetworkFrame, and StreamGeoFrame.Frames expose rendering, interaction, and layout directly. Use one when a Chart keeps a decision you need to make for yourself.
Utilities
Shared infrastructure like ThemeProvider,ChartContainer, and LinkedCharts. Compose them to build coordinated dashboards and themed applications.
The frameProps Escape Hatch
Every Chart component is built on top of a Frame. When you need advanced functionality that a Chart does not directly expose, you can pass additional Frame-level props through theframeProps prop without having to rewrite your entire component:
JSX
// Every Chart accepts a frameProps escape hatch <LineChart data={salesData} xAccessor="month" yAccessor="sales" frameProps={{ annotations: [ { type: "x", month: "Mar", label: "Q1 End" } ], hoverAnnotation: true, size: [800, 400] }} />
Start with a Chart and customize it as the design develops. If the Chart API becomes the constraint, use the underlying Frame directly.
Choosing the Right Component
Use this decision matrix to find the right component. Start with your data shape, then pick based on what you want to show:
| Data Shape | Goal | Component |
|---|
Flat array
[{x, y}] | Trends over time | LineChart,AreaChart |
| Part-to-whole over time | StackedAreaChart |
| Correlations | Scatterplot,BubbleChart |
| Compare categories | BarChart,DotPlot |
| Part-to-whole (categorical) | StackedBarChart,PieChart,DonutChart |
| Distributions | BoxPlot,SwarmPlot |
Hierarchical
{ children: [...] } | Tree/org structure | TreeDiagram |
| Proportional sizing | Treemap,CirclePack |
| Matrix / density | Heatmap |
Nodes + edges
[{id}], [{source, target}] | Relationships | ForceDirectedGraph |
| Flows and budgets | SankeyDiagram |
| Inter-group connections | ChordDiagram |
Streaming
ref.push({ time, value }) | Live trends | RealtimeLineChart |
| Live aggregates | RealtimeHistogram,RealtimeSwarmChart |
Chart vs Frame: When to Graduate
| Chart | Frame |
|---|
| Lines of code | 5-15 | 20-80+ |
| Custom marks | No | Yes |
| Annotations | Via frameProps | Direct prop |
| Custom tooltips | Yes | Yes |
| Custom rendering | No | Full SVG/Canvas control |
| Best for | Standard charts, dashboards, quick prototypes | Bespoke visualizations, novel encodings |
Tip: Start with a Chart. Use frameProps for one-off customizations. Reach for a Frame when you need to control the marks, layout, or rendering.
Bundle Size
Semiotic ships 11 entry points so you only load the chart types you use. Don't import from "semiotic" unless you need everything — use the sub-path that matches your chart category:
JS
// Instead of this (278 KB gzip — full library): import { LineChart } from "semiotic" // Do this (143 KB gzip — XY charts only): import { LineChart } from "semiotic/xy" // Or this (109 KB gzip — categorical charts only): import { BarChart } from "semiotic/ordinal" // Mixing is fine — each sub-path is independent: import { LineChart } from "semiotic/xy" import { BarChart } from "semiotic/ordinal" // Total: ~252 KB gzip (less than the full bundle)
| Entry Point | gzip | Charts |
|---|
| semiotic/xy | 143 KB | LineChart, AreaChart, Scatterplot, Heatmap, + 7 more |
| semiotic/ordinal | 109 KB | BarChart, PieChart, BoxPlot, Histogram, + 11 more |
| semiotic/network | 98 KB | ForceDirectedGraph, SankeyDiagram, Treemap, + 4 more |
| semiotic/geo | 93 KB | ChoroplethMap, FlowMap, DistanceCartogram, + 1 more |
| semiotic/realtime | 145 KB | RealtimeLineChart, RealtimeHistogram, + 3 streaming charts |
| semiotic/server | 100 KB | renderChart, renderDashboard, renderToImage, renderToAnimatedGif, renderPhysicsToAnimatedGif |
| semiotic/utils | 31 KB | ThemeProvider, validators, serialization — no chart components |
| semiotic/themes | 5 KB | Theme preset constants only |
| semiotic/data | 5 KB | bin, rollup, groupBy, pivot, fromVegaLite |
Tree-shaking: Each sub-path bundle has sideEffects: false. Modern bundlers (webpack, Vite, Rollup, esbuild) will tree-shake unused exports automatically. Sub-path imports give the most reliable size reduction since they're pre-split at the package level.
Next Steps
Now that you have the basics, dive into the component documentation: