Floating glass panels showing a line chart, network graph, Sankey diagram, and world map in a warm study, with a gray cat watching from a windowsill.

Semiotic: The Grammar-of-Graphics Chart Library With an AI-Era Comeback

The Gray Cat
The Gray Cat
0 views

If you have ever needed a chart that is not a line, bar, or pie, you already know the wall most React charting libraries hit. The popular ones are wonderful at business dashboards and quietly helpless the moment you ask for a force-directed network, a Sankey flow, a choropleth map, or a violin plot. Semiotic is the library that refused to draw that line. It pairs React with D3 and applies the grammar of graphics to the whole problem, so a single toolkit can render continuous data, categorical comparisons, relational networks, geographic maps, and now realtime streams.

The semiotic package has an unusual history. Built in 2017 by Elijah Meeks under the nteract organization, it became the go-to choice for unusual visualizations, went semi-dormant for years, and was then revived in 2026 with a streaming-first, AI-assisted rewrite. This article covers the version that exists today, version 3.7.2, including the renamed frames that trip up anyone copying old tutorials.

Why Semiotic Thinks in Frames

Most charting libraries give you one component per chart shape: a LineChart, a BarChart, a PieChart. Semiotic's original idea was different. Instead of naming components after shapes, it named them after the shape of your data. The name "Semiotic" comes from semiotics, the study of signs, and the core thesis is that a chart is just a system mapping data to visual marks.

That gave three conceptual frames that made the library famous:

  • XYFrame for continuous X/Y data, producing line charts, scatterplots, and area charts.
  • OrdinalFrame for categorical data, producing bar charts, violin plots, box plots, and parallel coordinates.
  • NetworkFrame for relational data, producing force-directed graphs, Sankey diagrams, hierarchical trees, treemaps, and circle packing.

You picked the frame that matched your data, then flipped a prop to switch chart variants. Turning a bar chart into a violin plot was a prop change on the same frame, not a different component.

Keep that mental model, because it still explains how the library is organized. But there is an important catch.

The v3 Rename You Must Know About

This is the single most important thing to know before you write a line of code. In version 3, the classic XYFrame, OrdinalFrame, and NetworkFrame names were removed. Any tutorial, Stack Overflow answer, or Medium post that imports XYFrame from semiotic will not work against the current package.

The low-level frames are now StreamXYFrame, StreamOrdinalFrame, StreamNetworkFrame, and StreamGeoFrame. On top of them sits a brand-new high-level Charts layer with sensible defaults. For most everyday work, the Charts layer is where you want to be, and it is what the rest of this guide leans on. Treat the old frame names as historical context, not working imports.

Getting It Installed

Semiotic supports React 18.1+ and React 19, ships full TypeScript definitions built with strict: true, and is licensed Apache-2.0.

npm install semiotic
yarn add semiotic

One word of warning up front: Semiotic is feature-heavy and pulls in around fifteen D3 packages, so its bundle is large compared to leaner alternatives. The library mitigates this with twelve sub-path entry points, each marked side-effect-free for tree-shaking. Import from the sub-path that matches your chart family rather than the barrel semiotic import when bundle size matters. We will return to that in the performance section.

Drawing Your First Charts

The Charts layer is opinionated and pleasant. A line chart is exactly as short as you would hope, and its accessors are generic, so TypeScript can validate that the keys you reference actually exist on your data.

import { LineChart } from "semiotic"

interface Sale {
  month: number
  revenue: number
}

const sales: Sale[] = [
  { month: 1, revenue: 4200 },
  { month: 2, revenue: 5100 },
  { month: 3, revenue: 4800 },
]

function RevenueChart() {
  return (
    <LineChart<Sale>
      data={sales}
      xAccessor="month"
      yAccessor="revenue"
    />
  )
}

The <Sale> generic ties the accessor strings to the data type. If you typo revenu, the compiler complains before the chart ever renders. The catalog spans roughly 47 chart types across families: XY charts like AreaChart, Scatterplot, Heatmap, and CandlestickChart; categorical ones like BarChart, PieChart, BoxPlot, Histogram, and SwarmPlot; and more exotic members we will reach shortly.

Networks and Flows in One Import

This is where Semiotic earns its keep. Relational and flow visualizations that would require a separate library elsewhere are first-class members of the same catalog.

import { ForceDirectedGraph, SankeyDiagram } from "semiotic"

function TeamGraph() {
  return (
    <ForceDirectedGraph
      nodes={people}
      edges={friendships}
      colorBy="team"
      nodeSize={8}
      showLabels
    />
  )
}

function BudgetFlow() {
  return (
    <SankeyDiagram
      edges={budgetFlows}
      sourceAccessor="from"
      targetAccessor="to"
      valueAccessor="amount"
    />
  )
}

The force simulation, the Sankey layout math, and the link routing all come from D3 under the hood, but React owns the DOM. You describe nodes and edges declaratively, and the component handles the rest. The network family also covers tree diagrams, treemaps, and circle packing through the same data-topology approach.

Maps Without a Separate Map Library

Geographic support is built in. Semiotic depends on topojson-client and world-atlas, which is why choropleths and flow maps work without bolting on a dedicated mapping stack.

import { ChoroplethMap } from "semiotic"

function PopulationMap() {
  return (
    <ChoroplethMap
      data={countryStats}
      idAccessor="iso3"
      valueAccessor="population"
    />
  )
}

Alongside ChoroplethMap you get FlowMap, DistanceCartogram, and ProportionalSymbolMap. For a React charting library to ship usable maps out of the box is genuinely rare.

Going Deeper

The high-level charts cover most needs, but the v3 additions are where Semiotic feels current rather than nostalgic.

Realtime Streams With Decay and Staleness

The 2026 revival reframed the library around streaming data, and the realtime charts show why. You push points imperatively through a ref, and the chart manages decay and staleness for you.

import { useRef } from "react"
import { RealtimeLineChart } from "semiotic"

function CpuMonitor() {
  const chartRef = useRef<any>(null)

  function recordSample(cpuLoad: number) {
    chartRef.current?.push({ time: Date.now(), value: cpuLoad })
  }

  return (
    <RealtimeLineChart
      ref={chartRef}
      timeAccessor="time"
      valueAccessor="value"
      decay={{ type: "exponential", halfLife: 100 }}
      staleness={{ threshold: 5000, showBadge: true }}
    />
  )
}

The decay prop controls how older samples fade, and staleness shows a badge when fresh data stops arriving past a threshold. There are matching RealtimeHistogram and RealtimeSwarmChart components for other streaming shapes. For dashboards wired to a websocket or polling loop, this removes a surprising amount of plumbing.

Coordinated Dashboards With Linked Charts

Cross-highlighting between charts is the kind of feature that usually means lifting state, wiring callbacks, and a lot of careful bookkeeping. Semiotic packages it as LinkedCharts. Charts that share a named selection automatically synchronize their hover and selection state.

import { LinkedCharts, Scatterplot, BarChart } from "semiotic"

function RegionDashboard() {
  return (
    <LinkedCharts>
      <Scatterplot
        data={people}
        xAccessor="age"
        yAccessor="income"
        colorBy="region"
        linkedHover={{ name: "hl", fields: ["region"] }}
        selection={{ name: "hl" }}
      />
      <BarChart
        data={regionSummary}
        categoryAccessor="region"
        valueAccessor="total"
        selection={{ name: "hl" }}
      />
    </LinkedCharts>
  )
}

Hovering a region in the scatterplot lights up the matching bar, and vice versa, because both charts subscribe to the selection named hl. Brush filtering and synchronized selection work through the same mechanism.

Server Rendering and Static Image Export

Semiotic renders SVG by default and does so automatically in server environments, so server-side rendering needs no special configuration. For images outside the browser, the server entry point produces SVG strings or PNGs, which is handy for emails, Open Graph images, and PDF generation.

import { renderChart, renderToImage } from "semiotic/server"

const svg = await renderChart(<LineChart data={sales} xAccessor="month" yAccessor="revenue" />)
const png = await renderToImage(<BarChart data={regionSummary} categoryAccessor="region" valueAccessor="total" />)

For very large datasets, the underlying frames historically supported a Canvas render mode, useful when you are plotting tens of thousands of points and SVG nodes become the bottleneck.

Charts Built to Be Generated by AI

The most distinctive part of v3 is tooling aimed at AI-assisted development, and it is unlike anything in the competing libraries. The semiotic/ai entry point ships a machine-readable capability catalog, and ai/schema.json exposes prop schemas so a model can generate correct code without examples. There is an MCP server you can run with npx semiotic-mcp that gives coding assistants tools for rendering, schema discovery, config diagnosis, and chart recommendations.

There is also a programmatic guardrail worth highlighting:

import { diagnoseConfig } from "semiotic/ai"

const issues = diagnoseConfig({
  chart: "BarChart",
  yAxis: { startAtZero: false },
})

diagnoseConfig flags anti-patterns across validation, encoding, accessibility, and even misleading-design checks, so a truncated axis or a deceptive encoding can be caught before it ships. Beyond that, every chart ships a built-in error boundary, dev-mode warnings with typo suggestions, and accessibility features like ARIA labels, keyboard-navigable legends, and live tooltips. The framing is that AI-generated charts should fail gracefully and refuse to mislead.

If you import existing specs, fromVegaLite in semiotic/data converts a Vega-Lite definition into a Semiotic config, so you are not locked out of that ecosystem.

Watching the Bundle

Honesty matters here. Even the sub-path bundles are sizeable: the XY entry is around 90 KB gzipped, realtime around 95 KB, ordinal around 74 KB, network around 68 KB, and geo around 55 KB. Always import from the specific sub-path rather than the barrel so tree-shaking can do its job.

import { LineChart } from "semiotic/xy"
import { SankeyDiagram } from "semiotic/network"

If your app is extremely size-sensitive and only needs standard business charts, a lighter library will serve you better. Semiotic's value proposition is breadth, not gram weight. It makes sense precisely when you need networks, maps, statistical plots, and streaming visuals living together in one dependency rather than four.

When Semiotic Is the Right Call

Reach for Semiotic when your visualization needs spill past the standard chart vocabulary. If a single screen has to show a network and a choropleth and a violin plot, very few libraries can cover all three, and Semiotic does it with a coherent frame model and strong TypeScript types. The v3 additions, realtime streams, linked charts, server rendering, and the AI tooling, make it feel like a library with momentum again rather than a relic.

The trade-offs are real: a heavier bundle, a steeper learning curve than the most beginner-friendly options, and documentation that has historically been uneven. But for the specific job of rendering an unusually broad range of chart types from one toolkit, with type safety and a genuinely novel approach to AI-generated visualizations, Semiotic remains uniquely complete. After years in the quiet, that is a comeback worth noticing.