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. Import LineChart, pass your data, and specify which fields map to the x and y axes:
JSX
import { LineChart } from "semiotic/line" 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 32 stable JavaScript entry points so you can choose the smallest public boundary that fits the route. Don't import from"semiotic" unless you need everything — use the sub-path that matches your chart category:
JS
// Instead of this (359 KB first-party gzip — full library): import { LineChart } from "semiotic" // Do this (135 KB first-party gzip — LineChart boundary): import { LineChart } from "semiotic/line" // Or this (166 KB first-party gzip — all XY charts): import { LineChart } from "semiotic/xy" // Or this (130 KB first-party gzip — categorical charts): import { BarChart } from "semiotic/ordinal" // Mixing is fine — shared runtime is deduplicated: import { LineChart } from "semiotic/line" import { BarChart } from "semiotic/ordinal"
| Entry Point | gzip | Charts |
|---|
| semiotic/line | 135 KB | LineChart only — one-chart micro boundary |
| semiotic/xy | 166 KB | LineChart, AreaChart, Scatterplot, Heatmap, + 8 more XY charts |
| semiotic/ordinal | 130 KB | BarChart, PieChart, BoxPlot, Histogram, + 11 more categorical charts |
| semiotic/network | 157 KB | ForceDirectedGraph, SankeyDiagram, ProcessSankey, Treemap, + 4 more |
| semiotic/geo | 113 KB | ChoroplethMap, FlowMap, DistanceCartogram, ProportionalSymbolMap |
| semiotic/realtime | 162 KB | RealtimeLineChart, RealtimeHistogram, + 4 streaming charts |
| semiotic/server | 216 KB | renderChart, renderDashboard, renderToImage, renderToAnimatedGif |
| semiotic/utils | 102 KB | ThemeProvider, numeric/accessibility audits, serialization — no chart components |
| semiotic/themes | 12 KB | Theme presets only (tufte, carbon, etc.) |
| semiotic/data | 4 KB | bin, rollup, groupBy, pivot, fromVegaLite |
How to read these numbers: They measure Semiotic's generated first-party artifacts, not a complete application bundle. Shared runtime and dependencies are deduplicated by modern bundlers; consult the checked cold-consumer table in the README before making a route-level size decision.
Next Steps
Now that you have the basics, dive into the component documentation: