This guide walks you through upgrading an existing Semiotic 1.x or 2.x codebase to Semiotic 3. The legacy frame names (XYFrame, OrdinalFrame,NetworkFrame) are not exported in v3 — start with a mechanical rename to the Stream* names. Past that, the work is incremental: pick up the chart HOCs as you touch each chart, switch to sub-path imports for smaller bundles, and adopt streaming or SSR features when you need them.
TipIf you only have a handful of charts, start with
the TL;DR. If you have a large legacy app, read
What's changed first so the prop diffs below make sense in context.
TL;DR
For most apps, three commands and one Frame rename are enough:
BASH
# React 18.1+ or 19 — Semiotic v3's peer-dep range is ^18.1.0 || ^19.0.0 npm install semiotic@latest react@^18.1.0 react-dom@^18.1.0 npm uninstall @types/semiotic # built-in types ship with v3
TipRun the codemods.Most of the mechanical rewrites below — pure renames, JSX prop additions, and the sub-path import split — are automated by
semiotic-codemod. Run the full recipe before reading the rest of this guide:
BASH
npx semiotic-codemod migration-recipe ./src
See
the codemod section below for the individual transforms and what's left to do by hand.
The legacy XYFrame, OrdinalFrame, and NetworkFramenames still resolve, so existing imports compile:
JSX
// v3 renamed the frames — the legacy names are NOT exported. // v2: import { XYFrame } from "semiotic" import { StreamXYFrame } from "semiotic"
New code should use the chart HOCs (LineChart, BarChart,SankeyDiagram, etc.). They wrap the Stream Frames and ship with sensible defaults; you'll write a fraction of the boilerplate you wrote against legacy frames.
Breaking changeThree frame APIs and a handful of components were removed in v3:
RealtimeSankey,
RealtimeNetworkFrame,
FacetController,
ProcessViz,
Mark,
SpanOrDiv, and the
baseMarkProps prop. See
Removed APIs for the migration path on each.
What's changed
| Area | v1 / v2 | v3 |
|---|
| React | 16+ (v1) / 17+ (v2-rc) | 18.1+ or 19 (peer-dep range ^18.1.0 || ^19.0.0) |
| Frames | SVG-based XYFrame, OrdinalFrame, NetworkFrame | Canvas-first StreamXYFrame, StreamOrdinalFrame,StreamNetworkFrame (rename required — legacy names removed) |
| Rendering | SVG marks + SVG chrome | Canvas marks + SVG overlay (axes, labels, annotations) |
| Chart catalog | Build everything from frames | 40+ chart HOCs (LineChart, BarChart, …) |
| Streaming | Bespoke realtime components (RealtimeSankey, …) | Ref-based push() on every frame and HOC |
| SSR | Not officially supported | First-class — "use client" on every interactive component, plus asemiotic/server static-render entry point |
| TypeScript | Community @types/semiotic | Built-in types |
| Bundle | ~440KB minified, no tree-shaking | ~165KB full bundle, ~50–80KB sub-path bundles |
| Coordination | FacetController | LinkedCharts + linkedHover / selection props |
The most consequential change is the move from SVG marks to canvas marks. Axes, labels, legends, and annotations still render as SVG (so they remain inspectable, themable via CSS, and accessible), but data marks are painted on a canvas. This is what makes streaming, large datasets, and 60fps interaction feasible — but it does mean a few SVG-only patterns (CSS selectors targeting marks, MutationObserver, screenshot pipelines that ignored canvas) need adjustment.
Step-by-step upgrade
1. Update React to 18.1+ or 19
Semiotic 3 uses concurrent rendering features (transitions, deferred values) and ships with"use client" directives recognized by Next.js App Router and similar frameworks. The peer-dep range is ^18.1.0 || ^19.0.0; React 16 and 17 are not supported, and 18.0.x is below the floor.
BASH
npm install react@^18.1.0 react-dom@^18.1.0
See theReact 18 upgrade guideif you're coming from 16/17.
2. Install Semiotic 3
BASH
npm install semiotic@latest
If you previously installed @types/semiotic, remove it — types now ship with the package.
BASH
npm uninstall @types/semiotic
3. Re-run your app
Once the Frame imports are renamed to the Stream* names, most v1/v2 codebases compile and render at this point. Prop signatures are largely backwards compatible and existing chart code continues to work.
TipIf you hit an error referencing
RealtimeSankey,
FacetController,
baseMarkProps,
ProcessViz,
Mark, or
SpanOrDiv — those were removed. Skip to
Removed APIs for the replacement.
4. Adopt chart HOCs (recommended)
v3 ships 40+ chart HOC components that wrap the Stream Frames with sensible defaults, automatic legends, hover handling, and TypeScript generics. New code is dramatically shorter than the equivalent legacy Frame:
Before — legacy XYFrame with lines array andlineType:
JSX
import { XYFrame } from "semiotic" import { curveMonotoneX } from "d3-shape" <XYFrame lines={[{ coordinates: salesData }]} xAccessor="month" yAccessor="revenue" lineDataAccessor="coordinates" lineType={{ type: "line", interpolator: curveMonotoneX }} hoverAnnotation={true} size={[600, 400]} />
After — LineChart HOC with flat data and a stringcurve:
JSX
import { LineChart } from "semiotic/xy" <LineChart data={salesData} xAccessor="month" yAccessor="revenue" curve="monotoneX" width={600} height={400} />
If you'd rather stay closer to the frame API (full control, no HOC defaults), useStreamXYFrame directly:
JSX
import { StreamXYFrame } from "semiotic/xy" <StreamXYFrame chartType="line" data={salesData} xAccessor="month" yAccessor="revenue" curve="monotoneX" size={[600, 400]} enableHover />
The same pattern applies to network charts — NetworkFrame with anetworkType object becomes a single-purpose HOC likeForceDirectedGraph:
Before:
JSX
import { NetworkFrame } from "semiotic" <NetworkFrame nodes={nodes} edges={edges} networkType={{ type: "force", iterations: 300 }} nodeIDAccessor="id" sourceAccessor="source" targetAccessor="target" hoverAnnotation size={[800, 500]} />
After:
JSX
import { ForceDirectedGraph } from "semiotic/network" <ForceDirectedGraph nodes={nodes} edges={edges} nodeIDAccessor="id" sourceAccessor="source" targetAccessor="target" iterations={300} width={800} height={500} />
Each HOC accepts a frameProps escape hatch for advanced configuration that doesn't fit the prop API. See the Charts catalog for the full list of HOCs and their props.
5. Switch to sub-path imports
v3 ships entry points per chart family. If you only render XY charts, importing fromsemiotic/xy drops the ordinal and network bundles entirely.
DIFF
- import { LineChart } from "semiotic" // full bundle (~165KB gz) + import { LineChart } from "semiotic/xy" // XY only (~77KB gz) - import { BarChart } from "semiotic" // full bundle + import { BarChart } from "semiotic/ordinal" // ordinal only (~64KB gz) - import { SankeyDiagram } from "semiotic" // full bundle + import { SankeyDiagram } from "semiotic/network" // network only (~51KB gz)
Available entry points: semiotic/xy, semiotic/ordinal,semiotic/network, semiotic/geo, semiotic/realtime,semiotic/server, semiotic/recipes, semiotic/utils,semiotic/themes, semiotic/data.
Codemods
semiotic-codemodships automated transforms for the mechanical parts of the upgrade. Run the full recipe, or apply individual transforms one at a time. Every transform is idempotent — re-running produces no further changes — so it's safe to re-apply after manual edits.
BASH
# Run every transform, in order npx semiotic-codemod migration-recipe ./src # Or pick individual transforms npx semiotic-codemod realtime-network-frame ./src npx semiotic-codemod realtime-sankey ./src npx semiotic-codemod subpath-imports ./src # Preview without writing npx semiotic-codemod migration-recipe ./src --dry --print
| Transform | What it does |
|---|
realtime-network-frame | Pure rename: RealtimeNetworkFrame → StreamNetworkFrameacross imports, JSX, and identifier references. |
realtime-sankey | Renames RealtimeSankey → StreamNetworkFrame and addschartType="sankey" to each JSX usage. |
subpath-imports | Splits bare from "semiotic" named imports into the appropriate sub-path entry points (semiotic/xy, semiotic/ordinal,semiotic/network, semiotic/realtime,semiotic/geo, semiotic/themes, semiotic/recipes). Symbols not in the manifest stay on the original "semiotic" import. |
After running, format with your usual tool — codemods emit jscodeshift's default style on touched lines (semicolons on new statements; original style preserved on untouched ones):
BASH
npx semiotic-codemod migration-recipe ./src npx prettier --write ./src
TipSome migrations aren't automated because they're context-dependent — replacingbaseMarkProps with the right per-mark style prop varies per chart, and rewriting FacetController children needs new linkedHover /selection props that the codemod can't infer. Those are documented below.
Removed APIs
These components and props are not exported in v3. Each has a direct replacement.
RealtimeSankey → StreamNetworkFrame
The bespoke realtime sankey was folded into StreamNetworkFrame's sankey chart type. The push API and particle behavior are identical:
DIFF
- import { RealtimeSankey } from "semiotic" - <RealtimeSankey ref={chartRef} size={[800, 400]} showParticles /> + import { StreamNetworkFrame } from "semiotic/network" + <StreamNetworkFrame ref={chartRef} chartType="sankey" size={[800, 400]} showParticles />
RealtimeNetworkFrame → StreamNetworkFrame
Direct rename. The RealtimeNetworkFrame export no longer exists in v3, so update the import to StreamNetworkFrame.
FacetController → LinkedCharts
The cross-chart coordination model was rebuilt around explicit linkedHover,linkedBrush, and selection props that name what's being shared, plus a LinkedCharts wrapper that hosts the shared state.
DIFF
- import { FacetController } from "semiotic" - <FacetController> - <XYFrame {...} /> - <OrdinalFrame {...} /> - </FacetController> + import { LinkedCharts } from "semiotic" + <LinkedCharts> + <LineChart {...} linkedHover={{ name: "hl", fields: ["id"] }} selection={{ name: "hl" }} /> + <BarChart {...} selection={{ name: "hl" }} /> + </LinkedCharts>
baseMarkProps (removed)
Two replacements depending on what you used baseMarkProps for:
- For per-mark styling (fill, stroke, opacity), use the per-mark style props —
lineStyle, pointStyle, pieceStyle, etc. Each accepts either a static style object or a function returning one. - For transition / animation timing, use the
animate prop on the HOC. v3 marks render to canvas, so CSS transitions on style objects do not animate mark updates — animate wires the duration/easing into the canvas transition pipeline, which is what produces the actual smooth interpolation.
DIFF
- <XYFrame baseMarkProps={{ transitionDuration: { fill: 500 } }} /> + <LineChart animate={{ duration: 500 }} />
animate accepts true, a number of milliseconds, or{ duration, easing, intro }. Thechart API reference lists per-chart defaults.
ProcessViz, Mark, SpanOrDiv (removed)
These low-level building blocks are gone. ProcessViz was a debugging aid;Mark and SpanOrDiv were primitives no longer needed once frames switched to canvas. If you depended on them, replace with direct SVG/HTML elements or scene primitives via the custom layout APIs.
Behavioral changes
Canvas marks (not SVG)
Data marks are painted on a canvas in v3. Existing code that reaches into the DOM to inspect or manipulate marks (CSS selectors, MutationObserver, screenshot tools that walk the SVG tree) won't see marks anymore. Style overrides should move to the per-mark style props (lineStyle, pieceStyle, pointStyle, etc.) or to theme-level CSS variables.
TipAxes, labels, legends, annotations, and tooltips remain SVG/HTML in v3. Theming and accessibility tools targeting those layers continue to work without changes.
Network style callbacks see RealtimeNode
On StreamNetworkFrame (and the network HOCs), nodeStyle andedgeStyle functions receive a RealtimeNode/RealtimeEdge wrapper. Your original data lives on d.data instead of directly on d.
Before:
JSX
<NetworkFrame nodes={nodes} edges={edges} nodeStyle={d => ({ fill: d.cluster === "core" ? "#3b82f6" : "#94a3b8" })} />
After:
JSX
<StreamNetworkFrame chartType="force" nodes={nodes} edges={edges} // d.data is the user-supplied node — d itself is a RealtimeNode wrapper nodeStyle={d => ({ fill: d.data.cluster === "core" ? "#3b82f6" : "#94a3b8" })} />
Responsive sizing is built in
ResponsiveXYFrame, ResponsiveOrdinalFrame, andResponsiveNetworkFrame are removed (not aliased). PassresponsiveWidth / responsiveHeight on a base Stream*Frame, or omit dimensions on an HOC — both measure the container automatically.
Adding streaming (optional)
Every chart HOC and Stream Frame in v3 accepts a forwarded ref. Callref.current.push() to ingest live data; the frame handles windowing, decay, transitions, and re-paint. Omit the data prop entirely when streaming — passing an empty array clears the chart on every render.
JSX
import { useRef, useEffect } from "react" import { RealtimeLineChart } from "semiotic/realtime" function LiveMetrics() { const ref = useRef() useEffect(() => { const interval = setInterval(() => { ref.current?.push({ time: Date.now(), value: Math.random() }) }, 100) return () => clearInterval(interval) }, []) return <RealtimeLineChart ref={ref} stroke="#6366f1" windowSize={200} /> }
See the Push API page for the full ref shape (push, pushMany, remove, update,clear, getData, getScales) and the streaming encodings (decay, pulse, staleness,transition) you can compose on top.
TypeScript
v3 ships its own type definitions. Remove any @types/semiotic from your dependencies. Every chart HOC accepts a generic for the row type so accessor strings are validated against your data:
TSX
import { LineChart } from "semiotic/xy" import type { LineChartProps } from "semiotic/xy" interface SalesPoint { month: number revenue: number } <LineChart<SalesPoint> data={salesData} xAccessor="month" // typechecked against keys of SalesPoint yAccessor="revenue" />
Frame and HOC prop types are exported from each entry point — e.g.import type { LineChartProps } from "semiotic/xy".
Next.js, Remix, and SSR frameworks
v3 charts include a "use client" directive at the top of each module, so the App Router treats them as client components automatically. The standard pattern is a thin client wrapper that the server component imports:
TSX
// app/dashboard/Chart.tsx — client component "use client" import { LineChart } from "semiotic/xy" export default function Chart({ data }) { return <LineChart data={data} xAccessor="month" yAccessor="revenue" /> } // app/dashboard/page.tsx — server component import Chart from "./Chart" export default async function DashboardPage() { const data = await fetchMetrics() return <Chart data={data} /> }
For static SVG output that doesn't require a browser at all (email, OG images, PDFs, dashboards rendered into static sites), use the semiotic/server entry point:
TS
import { renderChart } from "semiotic/server" const svg = renderChart("LineChart", { data: salesData, xAccessor: "month", yAccessor: "revenue", width: 600, height: 400, theme: "tufte", })
See Server-side rendering for the full SSR story.
FAQ
Do I have to rename the legacy Frame imports?
Yes. XYFrame, OrdinalFrame, NetworkFrame, and the responsive variants are not exported in v3, so those imports fail to resolve until you rename them to the Stream* names — a mechanical find-and-replace. Once renamed, prop names largely keep working and accessors still resolve. Past that, switch to the chart HOCs as you touch each chart rather than doing a big-bang rewrite.
Can I mix Stream Frames and v3 HOCs?
Yes. The HOCs wrap Stream Frames internally, and Stream Frames can live alongside HOCs on the same page. There's no runtime conflict.
What happened to v2?
v2 was a series of release candidates (up to 2.0.0-rc.12) that began the internal refactoring to functional components and TypeScript. It never promoted to a stable release. v3 completes that work and adds the chart HOCs, the stream-first Frames, SSR, sub-path imports, and built-in types.
Where do I file bugs about migration issues?
Open an issue atgithub.com/nteract/semiotic/issues. Include the legacy snippet you're migrating from and what the v3 equivalent should produce.