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). But you don't need streaming to use it — pass an array and you get a beautiful static chart with sensible defaults. This guide covers both paths.
Installation
Install Semiotic via npm:
Peer dependencies: Semiotic requires React 18+ and ReactDOM 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
Here is a complete example that renders a line chart showing monthly sales data. Just import LineChart, 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 ($)" /> ) }
That is it — Semiotic handles axes, scales, hover interactions, and responsive sizing with sensible defaults. When you need to customize, every aspect can be controlled through props.
Streaming Data
Under the hood, every Semiotic chart is a canvas-rendered streaming pipeline. When you pass a data array, Semiotic ingests it through the same pipeline that handles live push data — chunking large datasets automatically and rendering progressively 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 the data 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" />
Core Concepts: Three Tiers
Semiotic is organized into three tiers of abstraction. Start at the top with Charts and drop down to Frames or Utilities when you need more control.
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 StreamXYFrame. Full creative control over every aspect of rendering, interaction, and layout. Use when Charts are not enough.
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 the frameProps 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] }} />
This means you can start simple with a Chart and progressively customize it. If you eventually outgrow the Chart API entirely, you can graduate to using 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. Only graduate to a Frame when you need full control over 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 |
| 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: