v0.0.20 · Apache 2.0 · no telemetry

Charts for the
agent-native era.

An agent-native, deterministic chart-and-compute library. Every spec is a JSON value the LLM can author, diff, and patch. Every render is byte-identical across runs. Every primitive — render, drill, audit, forecast, narrate — is an MCP verb. Embedded DuckDB. Apache 2.0. No telemetry.

# As an MCP server (Claude / Codex / Cursor / Gemini):
npx -y @glyph/mcp

# As a TypeScript library:
npm install @glyph/core @glyph/duckdb
Rides by pickup hour 7am 10am 1pm 4pm 6pm 100 200 300
50
MCP tools
679
tests passing
11
mark types
SVG + Canvas
renderers

Why Glyph exists

Every charting library predates LLMs. D3 (2011), Tableau (2003), Plotly (2014), Vega-Lite (2017) — all assume a human writes the code, reads the chart, and maintains the dashboard. When an agent tries to use them, the cracks show: imperative APIs that won't compose, non-deterministic output that can't be snapshot-tested, opaque internals that block lineage + audit, no programmatic verb surface to chain reliably. Glyph was built from scratch for the agent-native era — chart specs as JSON values, compilation as pure function, byte-stable SVG output, 50 MCP verbs as the primary interface.

Who it's for

  • Agents — Claude, Codex, Cursor, Copilot CLI, Gemini authoring data analyses
  • Analysts who want their work auditable + version-controlled like code
  • Engineers building agent-driven reporting / dashboards / pipelines
  • Researchers who need figures that survive Node upgrades + decades of reruns

What it solves

  • Charts agents can't reliably write — too many lines of imperative JS, too many runtime deps
  • Charts you can't reproduce — float drift, browser variation, library updates
  • Charts you can't audit — no built-in lineage, uncertainty, or critique
  • Charts that don't compose — no diff/patch/replay verbs, no cross-process handles

What we optimize for

  • Determinism — same spec → same SVG bytes, always
  • Agent affordance — 50 MCP verbs are the primary API, not a wrapper
  • Trust — audit, uncertainty, lineage built into every render
  • Composability — charts are values you diff, patch, replay
  • Portability — plain SVG ships in email, PR, slide, paper

What Glyph is — and what it isn't

Scope matters. Glyph picks a side on several axes that other tools straddle. If your use case lives on the other side of these lines, Glyph is not the right tool — and we'll tell you which tool is.

Glyph IS

  • A TypeScript chart-and-compute library with embedded DuckDB
  • An MCP server (50 verbs) for agent-driven analytics
  • Deterministic — byte-identical SVG across runs / OSes / Node versions
  • Spec-first — charts are JSON values you diff, patch, version, replay
  • Audit-aware — uncertainty, lineage, critique are built in by default
  • Static-output friendly — SVG ships in email, PR, slide, paper
  • Open source (Apache 2.0), zero telemetry, self-hostable
  • Multi-IDE — first-class skills for Claude, Cursor, Copilot CLI, Gemini

Glyph IS NOT

  • A drag-and-drop dashboard tool — use Tableau, Power BI, or Hex
  • A 3D / WebGL renderer — use Three.js or deck.gl
  • A SaaS platform — runs entirely on your machine
  • A pure-JS browser-only library — DuckDB is embedded server-side
  • A real-time streaming engine — use Grafana or Apache Superset
  • A 1:1 D3 replacement — fewer mark types (11), by design
  • Tied to one IDE, one agent, or one LLM vendor
  • Free of trade-offs — determinism costs some runtime flexibility

Use cases — agent-native computing

Concrete scenarios where Glyph is the right tool. Each one maps to one or a few MCP verbs; every output carries a gdf:// handle, lineage, audit findings, and is reproducible byte-for-byte.

1 · Single-agent analysis loop

Agent receives data, renders, drills, narrates

User pastes a CSV. The agent calls glyph_describeglyph_clarifyglyph_renderglyph_drillglyph_explain. Every step deterministic; every handle addressable.

glyph_describe("rides.csv")
glyph_render(spec, modalities)
glyph_drill(handle, selector)
glyph_explain(handle)
2 · Multi-agent collaboration

Agent A renders, B drills, C narrates

One gdf:// URI flows between processes. Each agent's transformation is captured in lineage; any later agent can walk back to the source via glyph_lineage(uri).

// agent A
const { uri } = await glyph_render(spec)
// agent B (different process)
await glyph_subscribe(uri)
const { uri: drill } =
  await glyph_drill(uri, sel)
3 · Audit-grade reporting

Every chart carries its own critique

Compile, audit, score, render — one verb. Truncated axes, dual-y mismatch, small-n, log-zero all flagged with severity + fix hint. Investor decks ship with footnotes by default.

const { svg, audit, trust } =
  await glyph_audit({ spec })
// trust.score: 47 / 100
// audit.findings: 3
4 · Reproducible research

The same figure in 2026 and 2030

Check the spec into git alongside the paper. Snapshot-test SVG output in CI. Across Node versions, OSes, and rerun dates, the bytes don't drift. Reviewers rerun your analysis with one command.

vitest run __snapshots__/fig3.svg
# 679 tests — byte-identical
# 6-cell CI matrix (Node × OS)
5 · Interactive notebooks

Live preview in Cursor / Jupyter / VS Code

glyph_preview spins up a local preview server. Charts hydrate browser-side via @glyph/live for click / zoom / lasso, all gated by declarative spec flags. No JS unless you opt in.

glyph_preview(spec)
// → http://localhost:5174
// → SVG hydrates with declared interactions
6 · PR review for data

Spec diffs as HTML in pull requests

glyph diff a.json b.json --format=html emits a structured diff you can drop into a PR comment. A reviewer sees exactly which fields changed — no pixel scanning, no "spot the difference."

$ glyph diff a.json b.json
[ replace /layers/0/mark
    "bar" → "area" ]
[ add /encoding/opacity 0.75 ]

Examples

Same JSON spec on the left, the rendered chart on the right.

Bar chart

Categorical x + quantitative y; the canonical first-pass viz.

Q1 Q2 Q3 Q4

Line chart

Time series with smoothed connectors. Add animation: { kind: "stage" } for entrance.

Heatmap

2D categorical × categorical with quantitative color. New in PR49.

Mon Tue Wed Thu

Choropleth

Project GeoJSON polygons through naturalEarth / mercator / albers.

Boxplot

Tukey quartiles + whiskers + outliers, per categorical group. New in PR50.

A B C

Bar race (animated)

SMIL-animated bar widths per frame; attachScrub wires a slider in the browser.

Pie / donut PR66 · Polar

Set coordinates: { type: "polar" }. Bar + polar → arcs. Same Scene IR, byte-stable SVG.

marketing 25% · ops 30% · eng 45%

Treemap PR67 · Hierarchy

data.hierarchy: {...} + mark: "treemap". Squarified algorithm. No DuckDB.

eng 45% sales 22% ops 13% mkt 6% hr 5%

Sunburst PR67 · Partition

Radial partition of the same hierarchy. Each ring = one depth level.

Force graph PR68 · Seeded

data.graph: { nodes, edges } + spec.seed: 42. Velocity-Verlet, byte-stable across runs.

Contour PR75 · Marching squares

data.grid: { rows, cols, values } + thresholds. 16 marching-squares cases including saddles.

4 isovalue thresholds

Morph PR74 · SMIL

glyph_morph_render(specA, specB) emits one SVG with <animate from to>. Smooth transitions between scene states.

Uncertainty PR61 · Auto

Low-sample data → hatched bars + n-badge. No spec change — driven by provenance.confidence.

n=8 · confidence: low

Auditor PR63 · 8 rules

glyph_audit_spec(spec) catches misleading charts. Truncated y-axis, undisclosed log, dual-axis, more.

100 → AUDIT-01 high y-axis starts at 100, not 0 — bar heights misrepresent magnitude.

Radial line PR66 · Polar

Line mark + polar coords = radar/star chart. Same encoding API, different coordinate system.

Key innovations, visualized

Nine innovations that show how Glyph differs from D3 / Vega-Lite / Plotly / Tableau / Power BI. Each is one MCP verb call. None requires an LLM at runtime — every algorithm is deterministic + pure. For the full taxonomy see 55 capabilities and INNOVATION.md.

v0.0.1 · 2024 v0.0.20 · 2030
2.8 · determinism

Byte-stable SVG forever

Same spec → identical bytes across runs, OSes, Node versions, decades. Snapshot-tested in CI on a 6-cell matrix.

n=8 · low
2.3 · trust

Uncertainty rendering by default

Low-n samples auto-hatch and emit a confidence badge. Charts ship their own honest footnotes; no opt-in required.

- "/layers/0/mark": "bar", + "/layers/0/mark": "area", + "/encoding/opacity": 0.75, "/data/source": "rides.csv", "/encoding/x": "hour", "/encoding/y": "rides"
1.7 / 1.8 · edit

Spec diff & patch (RFC 6902)

Charts are JSON values you diff and patch. Review them in a PR like code. Replay edits across years of data via glyph_macro_replay.

62% 5 of 8 frames · notifications/progress
1.2 · MCP

Streaming progress

Long-running verbs emit notifications/progress milestones. Agents stream partial state instead of blocking on a 30-second forecast.

glyph_clarify Include weekends in "rides per hour"? weekday only all 7 days
1.4 · clarify

Disambiguation before rendering

Before guessing, the planner asks explicit clarification_questions. Agents pin answers via glyph_story_clarify — no silent assumptions.

local only ~/.glyph/engagement never phones home
1.5 · privacy

Engagement signals — local only

Hovers, clicks, zooms tallied in ~/.glyph/. Aggregates feed planner-bias; nothing leaves your machine. Audit-safe by default.

gdf://a/1 38m gdf://b/2 12m gdf://c/3 2m gdf://d/4 TTL countdown · reaper sweeps expired
1.6 · GC

Handle TTL + lineage-aware reaper

Every gdf:// handle carries a TTL. The reaper sweeps stale ones — but parents with live descendants are spared automatically.

2024 2025 2026 macro replay × 3 glyph_macro_replay(macro, params)
2.5 · replay

Macro capture & replay

Record a sequence of MCP verb calls as a portable JSON macro. Replay against new data with {{params.X}} substitution — same analysis, same SVG.

intent "why ↓?" planner.llm { Claude | Codex | Gemini } glyph_story_plan(intent, planner_hint: "llm")
1.1 · plan

LLM-pluggable story agent

Story plans are deterministic; the planner is your choice of LLM. Swap Claude, Codex, or Gemini without changing the protocol.

55 capabilities only Glyph ships

Every row below is a thing Glyph does today that no other charting tool ships — D3, Vega-Lite, Plotly, Tableau, Power BI, Observable Plot, Apache ECharts. Grouped into 11 categories and 3 uniqueness tiers. Architectural = nobody else could ship this without rewriting their core. Capability = nobody else has built it. Ergonomics = could be copied, but isn't.

Tier 1 — Architectural advantages

Things competitors can't ship without rewriting their compiler, runtime, or data model.

A
Protocol layer (GDF — Glyph Data Flow)
Charts as addressable, lineage-tracked, multi-agent data flow.
#
Capability
Verbs / code
Why competitors can't
1
Every render mints a stable gdf://session/<handle> URI for the materialized dataset.
gdf://session/<h>glyph_render
Others: no concept of a chart having an addressable identity outside the page.
2
Cross-agent publish/subscribe: Agent A renders, Agent B subscribes to the handle, Agent C drills.
glyph_publishglyph_subscribeglyph_linked_publish
Others: single-process libraries with no inter-agent surface.
3
Walk-back lineage: every handle records its parents → reconstruct the full transformation graph.
glyph_lineageparents[]
Others: the chart is a leaf, not a node in a DAG.
4
Lineage-aware garbage collection: parents are protected while descendants live.
TTL reaperrefcount on parents
Others: no shared state → no GC concept.
5
Modality-tagged link bus: chart, table, narrative all flow through the same address space.
modality: "chart"|"table"|"text"
Others: different libraries for each modality, no shared bus.
B
Deterministic compilation
Same input → byte-identical output. Forever. Provable in CI.
#
Capability
Verbs / code
Why competitors can't
6
Byte-identical SVG across runs / OSes / Node versions. 8-decimal pixel rounding everywhere.
roundPx()snapshot tests
Others: float arithmetic + browser rendering = drift between runs.
7
Seeded force simulation via mulberry32 PRNG → force-directed graphs are reproducible.
seed: numbermulberry32
Others: Math.random() inside the layout, snapshots impossible.
8
Deterministic Voronoi via integer-sign-preserving Bowyer-Watson + Sutherland-Hodgman clip.
scenegraph/voronoi.ts
Others: floating-point degeneracies near cocircular points → orientation flips.
9
Spec-as-code CI gate: a chart's spec is checked in; a diff in pixels means a diff in JSON.
glyph diff__snapshots__/*.svg
Others: charts are emitted by imperative code, not declared.
10
Regression-proof rendering: 679 snapshot tests guarantee no silent visual drift across PRs.
vitest --update679 tests
Others: rasterized diffs are too noisy to gate merges.
C
Embedded compute
SQL engine lives inside the chart library, not next to it.
#
Capability
Verbs / code
Why competitors can't
11
In-process SQL via embedded DuckDB — query a handle without round-tripping to a server.
glyph_query@duckdb/node-api
Others: assume the data is already in JS — no SQL surface.
12
Click → SQL: glyph_drill turns a clicked mark into a WHERE clause against the materialized view.
glyph_drilldata-* keys
Others: interactions are JS callbacks; no engine on the other end.
13
Named-metric layer — define metrics once, reuse across charts. Anomaly/forecast/decompose all consume named metrics.
glyph_define_metricglyph_list_metrics
Others: no shared semantic layer; each chart re-derives its expressions.
14
DuckDB embedded — Parquet, CSV, JSON, Arrow all queryable from the same spec.
data.source: "file.parquet"
Others: only JS arrays / objects accepted.
15
Inline-data shortcut bypasses DuckDB for tiny specs → no engine startup cost.
synthesizeInlineDataHandle
Others: n/a — they don't have a compute engine to bypass.
G
Editability as a primitive
Charts are values you can diff, patch, and replay.
#
Capability
Verbs / code
Why competitors can't
31
Spec diff: glyph_spec_diff returns the structural delta between two specs.
glyph_spec_diff
Others: chart is the output of imperative code; "diff the chart" is meaningless.
32
RFC 6902 JSON Patch: glyph_spec_patch applies a patch to a spec deterministically.
glyph_spec_patchJSON Pointer
Others: no canonical spec to patch.
33
Macro replay: capture a sequence of spec edits, replay against a different dataset.
glyph_macro_recordglyph_macro_replay
Others: no spec → no macro primitives.
34
CLI diff: glyph diff a.json b.json outputs human-readable HTML/MD report.
glyph diff --format html|md
Others: no spec on disk to diff.
35
Scale suggester: glyph_suggest_scale returns the optimal scale type given a column.
glyph_suggest_scale
Others: agents have to reason about scales themselves.

Tier 2 — Capability advantages

Things nobody else has built. A competitor could build them, but hasn't.

D
Agent-native interface
LLM agents are the primary user. 50 MCP verbs, no GUI required.
#
Capability
Verbs / code
Why competitors can't
16
50 MCP tools covering data, render, analyze, story, audit, memory, lineage.
50 verbstools/list
Others: MCP wrappers around JS APIs exist, but verbs are not the design.
17
Deterministic narrative: glyph_explain emits a pure-fn description of a chart — no LLM call.
glyph_explain
Others: rely on an LLM to caption charts → nondeterministic.
18
Analytic verbs as primitives: glyph_anomaly, glyph_forecast, glyph_decompose, glyph_change_point.
glyph_anomalyglyph_forecast
Others: the agent has to chain a stats library + a chart library.
19
Versioned capability manifest: glyph_capabilities returns the verb surface for THIS version → agents can self-discover.
glyph_capabilities
Others: no versioned tool manifest; agents hardcode against APIs.
20
LLM-pluggable story agent: storytelling verbs accept any model via spec; default is local.
glyph_story_planmodel: "..."
Others: opinionated about which LLM, or none.
E
Story & multi-agent collaboration
Multi-step analytic narratives as first-class artifacts.
#
Capability
Verbs / code
Why competitors can't
21
Story planner: glyph_story_plan outputs a multi-chart narrative DAG.
glyph_story_plan
Others: charts are isolated; no story container.
22
Whyboard: an analytic argument graph — claim → evidence → counter-evidence.
glyph_whyboard_*
Others: nothing like this anywhere.
23
Whyboard diff: track how an argument evolves between two checkpoints.
glyph_whyboard_diff
Others: no whyboard → no whyboard diff.
24
Clarification questions: glyph_clarify returns explicit unknowns before rendering.
glyph_clarify
Others: agents make assumptions silently.
25
Streamable checkpoints via notifications/progress — long-running plans stream partial state.
notifications/progress
Others: request/response only.
F
Trust & governance
Every chart carries enough metadata to audit, score, and contest.
#
Capability
Verbs / code
Why competitors can't
26
Uncertainty rendering by default — small n hatches bars, dims points, adds a confidence badge.
provenance.confidenceSceneUncertainty
Others: uncertainty is your problem; the chart looks confident.
27
Chart auditor: 8 rules (truncated axis, dual-y mismatch, log-zero, pie>7, etc.) → glyph_audit.
glyph_audit8 rules
Others: rendering ≠ critique.
28
Causal-aware viz with cycle detection — DAG layout via toposort; cycles refuse to render.
data.shape: "graph"cycle = error
Others: no concept of causality at the chart layer.
29
Trust scorecard: glyph_trust returns a 0-100 score with itemized contributors.
glyph_trust
Others: nobody scores their charts.
30
Persistent audit log: every verb call is logged with handle, args, timestamp.
glyph_audit_log
Others: no shared state, no audit trail.
H
Renderer & animation
One IR drives multiple backends; animation is spec-declared.
#
Capability
Verbs / code
Why competitors can't
36
Multiple renderers, same IR: SVG (server), Canvas (browser), more pluggable. Byte-equivalent output.
renderSvg(scene)renderCanvas(scene)
Others: renderer and library are inseparable.
37
Spec-declared morph: glyph_morph_render animates between two spec states via SMIL — zero JS.
glyph_morph_renderanimation.kind: "morph"
Others: animation = imperative JS, not a spec field.
38
4 spec-declared animation kinds: stage, stage-stagger, race, scrub, morph — all via SMIL.
animation.kind
Others: ad-hoc per-call animation; not declarative.
39
renderFrames: deterministic per-frame array of scenes for film/PNG export.
renderFrames(spec)
Others: animation lives in the DOM, not the IR.
40
TopoJSON + 4 projections (Mercator, Albers, Equirectangular, EqualEarth) built in.
data.shape: "topojson"projection
Others: ship maps as a separate package or not at all.
I
Memory & persistence
Agents remember; chart state survives across sessions, locally.
#
Capability
Verbs / code
Why competitors can't
41
Save / recall: glyph_memory_save + glyph_memory_recall persist named specs across runs.
glyph_memory_saveglyph_memory_recall
Others: no persistent layer below the library.
42
Local-only engagement signals: viewers' interactions are tallied LOCALLY → never phones home.
~/.glyph/engagement
Others: either no analytics or cloud-coupled telemetry.
43
Engagement aggregates: glyph_engagement shows which charts were hovered/zoomed/clicked.
glyph_engagement
Others: n/a — no local telemetry pipeline.
44
TTL + reaper: handles auto-expire; named saves are exempt.
ttl_msreaper.ts
Others: no handle layer to expire.
45
Persistent audit log at ~/.glyph/audit.log across runs, surveys, and machines.
~/.glyph/audit.log
Others: stateless libraries.
J
Multi-modal & streaming
Chart, table, narrative co-emitted from one verb.
#
Capability
Verbs / code
Why competitors can't
46
One verb → chart + table + narrative. glyph_render returns all three modalities.
modalities: ["chart","table","text"]
Others: chart libs return SVG / Canvas only.
47
Echo-filter by modality: subscribe to only the modalities you want via glyph_linked_publish.
modality: "chart"
Others: no pub/sub.
48
MCP notifications/progress on long-running verbs (forecast, story) — agents stream partial.
notifications/progress
Others: blocking request/response.
49
Zero-cost backward compatibility: omitted optional spec fields produce byte-identical output to prior versions.
byte-identity tests
Others: new features change pixel output silently.
50
glyph_close_preview: clean shutdown of the preview-server, including released handles.
glyph_close_preview
Others: n/a — no preview server.

Tier 3 — Ergonomic advantages

Things that could be copied — but reflect deliberate choices nobody else makes.

K
Distribution & ergonomics
Built for how analysts, engineers, and agents actually work.
#
Capability
Verbs / code
Why competitors can't
51
4-IDE skill files: Claude Code, Cursor, Copilot CLI, Gemini CLI all ship skills out of the box.
skills/4 platforms
Others: no IDE-aware affordance layer.
52
Role-aware skills: analyst / engineer / agent each get a different skill surface.
role.md
Others: one-size-fits-all docs.
53
glyph diff CLI outputs HTML or Markdown spec-diff reports for PR comments.
glyph diff --format html|md
Others: no CLI, no spec → no diff.
54
Apache 2.0 + zero telemetry. Self-hostable. No phone-home. Audit-safe by default.
LICENSEno analytics
Others: SaaS-coupled or telemetry-enabled defaults.
55
6-cell CI matrix: { Node 20 / 22 } × { Linux / macOS / Windows } — byte-identity proven on every push.
.github/workflows6 cells
Others: nondeterministic output makes cross-platform CI meaningless.

Architectural (20)

Items in categories A, B, C, G. Glyph could not retrofit these into a non-deterministic, runtime-only chart library — the protocol layer, byte-identity, and embedded SQL engine demand a different core.

Capability (30)

Items in categories D, E, F, H, I, J. Competitors could in principle build these — they are buildable features — but none have. Whyboard, trust scorecards, lineage-aware GC, multi-modal echoes: not on anyone else's roadmap.

Ergonomics (5)

Items in category K. Anyone can ship a CLI or skill files. The choice not to is itself revealing — Glyph treats agents and PR reviewers as first-class users.

Eight things no other charting library can do

Each of the demos below is a single MCP verb call on Glyph and a multi-week engineering project on every competitor. The reason isn't "more features" — it's the architecture: deterministic spec-only JSON + an embedded compute engine + an agent-native protocol layer.

1

Streamgraph that morphs offset modes

deterministic morph between two compiled scenes

D3's most-recognizable interactive demo: flip the stack offset between zero, expand, and wiggle; watch the streams smoothly morph between layouts. In D3, this is ~120 lines of imperative SVG path interpolation. In Glyph, it's glyph_morph_render(specA, specB) — and the SVG bytes are byte-identical across runs.

offset: zero ⇄ expand (4-second loop, single <animate> per layer) Jan Apr Jul Oct Dec

One MCP call

glyph_morph_render({
  spec_a: { data: {source}, layers: [
    { mark: "area", stat: { type:"stack",
                            params:{offset:"zero"} } }
  ]},
  spec_b: { /* same, but offset:"expand" */ },
  duration_ms: 4000
})

Both specs compile to scenes whose marks align by index; morphScenes() pairs them and emits <animate from to> on each path. No path-string interpolation needed — Glyph's deterministic compiler guarantees the alignment.

Other tools: D3 needs hand-rolled d3.interpolate + Flubber for path morphing; Vega-Lite has no morph primitive; Tableau/Power BI can't animate spec changes at all.
2

Three agents, one chart, full lineage

gdf:// protocol — cross-agent data flow

Agent A renders a chart. Agent B subscribes via glyph_subscribe(uri), drills into an outlier, mints a new handle. Agent C subscribes to that handle, runs anomaly detection. Every step's gdf:// URI carries the full lineage chain. Any later agent can walk back to the source with one glyph_lineage(uri) call.

Agent A Explorer glyph_render(spec) gdf://abc/src subscribe(uri) Agent B Diagnostician glyph_drill(uri, ">p99") gdf://abc/drill subscribe(drill_uri) Agent C Narrator glyph_explain(drill_uri) gdf://abc/expl glyph_lineage(gdf://abc/expl) → src → drill → expl · 3 hops, 3 agents, full audit

Three agents, one wire format

// Agent A
const { uri } = await glyph_render(spec)
// uri = "gdf://abc/src"

// Agent B (different process / IDE)
await glyph_subscribe(uri)
const { uri: drill } = await glyph_drill(uri,
  { selector: { kind: "between", field: "z",
                from: 3, to: 99 } })

// Agent C (yet a third process)
await glyph_subscribe(drill)
const story = await glyph_explain(drill)
// walks lineage back to "uri" automatically

Every handle is globally addressable + immutable. No "export to JSON" or "copy this dashboard" step — the URI is the data.

Other tools: D3 and Vega-Lite are process-local. Tableau and Power BI have data sources but no chart-level handles. Plotly's Dash apps don't cross processes. There is no industry standard for cross-agent chart handles.
3

Chart + table + narrative, one envelope

modalities: ["chart", "table", "narrative"]

One glyph_render call returns all three modalities. A click in any pane broadcasts through the link_group bus with a modality tag — subscribers filter out their own (echo prevention) and update the rest. The narrative is auto-generated by the same glyph_explain pipeline.

chart 10am ← table hour rides 7am 60 8am 100 9am 140 10am 160 11am 120 12pm 80 1pm 40 narrative
Rides peak at 10am

The 10am bar hits 160 — the daily max. Morning commute (7–9am) ramps from 60 → 140, then drops sharply after lunch.

glyph_explain headline · 3 highlights
↳ via modalities: ["narrative"]
link_group: "rides-q4" · modality: table → broadcasts → chart, narrative

One verb returns all three

glyph_render({
  spec: { data:{source:"rides.csv"},
          layers:[{mark:"bar",
            encoding:{x:"hour",y:"rides"}}],
          link_group: "rides-q4" },
  modalities: ["chart","table","narrative"]
})
→ { svg, modalities: {
    table:     { columns, rows },
    narrative: { headline, highlights[] }
  }}

Click any pane → call glyph_linked_publish({ modality, predicate }). Subscribers in the same link_group receive the event and filter by event.modality to skip their own surface (echo prevention).

Other tools: Tableau dashboards do cross-pane filtering but you build each pane separately + manage state in tableau-script. Plotly Dash requires writing every callback by hand. Glyph composes the triptych from one render call.
4

Racing bar chart, one spec

animation.kind: "race" · SMIL, no JS

Hans Rosling's iconic format: bars swap positions and lengths through time, with a giant year ticker. In D3 it's ~300 lines of imperative animation, plus a runtime. In Glyph it's one spec fieldanimation: { kind: "race", frame_field: "year" } — and the compiler emits a single SVG with SMIL <animate> elements. No JS at runtime. Bytewise stable across renders.

1990 2000 2010 2020 Top-5 economies by GDP (race, 8s loop) USA Japan China Germany India 0 10T 20T 30T (USD)

One spec, one verb

glyph_render({
  data: { source: "gdp.parquet" },
  layers: [{
    mark: "bar",
    encoding: { x:"gdp", y:"country", color:"country" }
  }],
  animation: {
    kind: "race",
    frame_field: "year",
    duration_ms: 8000
  }
})

The compiler emits the per-frame x/y/width values, then injects a single SMIL <animate> on each rect. The SVG plays everywhere — server email, GitHub README, slide deck — no JS runtime required.

Other tools: D3 needs ~300 lines of imperative transition() chains. Tableau "page" filters animate but require Tableau Server. Plotly's animation_frame needs Plotly.js loaded. None ship a pure-SVG race chart.
5

Chart auditor — render with its own critique

glyph_audit · 8 rules, deterministic

Render a chart AND its critique with the same toolchain. The auditor inspects the compiled spec for known deception patterns (truncated axes, dual-y mismatch, log-zero, small-n, pie>7, etc.) and emits findings inline. Most charts ship without their footnotes; Glyph ships them by default.

Q1–Q4 revenue ($M) — looks like 2× growth 1.12 1.08 1.04 1.00 Q1 Q2 Q3 Q4 ← baseline ≠ 0 glyph_audit → 3 findings ✗ truncated_y_axis y starts at $1.00M (real baseline = $0) ✗ visual_inflation Q4 looks 2.4× Q1; actual ratio = 1.07× ✗ small_sample n=4 quarters — uncertainty not rendered glyph_trust → 47 / 100

Critique is a verb

const { svg, audit, trust } =
  await glyph_audit({
    spec,
    rules: ["axis", "scale",
            "encoding", "size"]
  })
// audit.findings: [...]
// trust.score: 47 / 100

Eight built-in rules run as pure functions on the compiled scene — no LLM judgment, no flakiness. They inspect the spec, the bound data, and the scene tree. Findings carry severity, rule_id, and a fix hint.

Other tools: D3 / Vega-Lite / Plotly all let you make deceptive charts; none audit them. Tableau has visual-best-practice tips but no programmatic gate. Glyph treats critique as a first-class output.
6

Git for charts — RFC 6902 spec patches

glyph_spec_patch · JSON Patch in, animation out

Charts are values. glyph_spec_diff(a, b) returns the structural delta. glyph_spec_patch(spec, patch) applies it deterministically. Combine with glyph_morph_render and a patch becomes a visible animation — Git diffs that move.

// RFC 6902 patch — applied to /layers/0 [ { "op": "replace" , "path": "/layers/0/mark" , "value": "area" }, { "op": "add" , "path": "/layers/0/encoding/opacity" , "value": 0.75 } ] M T W T F S S glyph_spec_patch(spec, patch) → spec' → glyph_morph_render(spec, spec')

Charts as patchable values

// diff two saved specs
const patch = await glyph_spec_diff(a, b)

// apply elsewhere
const next = await glyph_spec_patch(a, patch)

// animate the transition
await glyph_morph_render({
  spec_a: a, spec_b: next,
  duration_ms: 5000
})

Because the spec is canonical JSON, patches travel like code: review them in a PR, apply them in CI, replay them across datasets via glyph_macro_replay.

Other tools: there's no canonical chart spec on D3 or Plotly to patch — charts are the side-effects of imperative code. Vega-Lite has a spec but no diff/patch/replay primitives. Glyph treats chart edits like Git treats text edits.
7

Geo map + anomaly overlay, one envelope

TopoJSON · 4 projections · glyph_anomaly

data.shape: "topojson" + one of 4 built-in projections (Mercator, Albers, Equirectangular, EqualEarth) ships a world map with no external geo library. Layer glyph_anomaly on top and outliers pulse automatically. Every other library requires a separate map package; Glyph composes both from one spec.

✗ anomaly detected BRA: shipping_p99 = 4.2σ above mean shipping_p99 by country — projection: equalEarth glyph_render({ data.shape: "topojson", projection: "equalEarth", layers: [anomaly] }) > 3σ 1.5–3σ in band

Geo + analytics, one spec

glyph_render({
  data: {
    shape: "topojson",
    source: "world.json"
  },
  projection: "equalEarth",
  layers: [
    { mark: "geoshape",
      encoding: { color: "value" } },
    { mark: "anomaly",  // built-in
      encoding: { value: "shipping_p99",
                  by: "country" } }
  ]
})

One spec, one verb. The compiler runs the projection (deterministic), the anomaly detector (named-metric layer), and the renderer — all in process. No D3-geo or Mapbox dependency.

Other tools: D3 ships geo separately (d3-geo + topojson-client). Plotly Mapbox needs an API token. Tableau maps are proprietary. Glyph: spec field flip.
8

Causal DAG — refuses to render cycles

data.shape: "graph" · cycle ⇒ audit error

Most charts let you draw anything you want, regardless of whether it's semantically valid. Glyph's graph shape with kind: "causal" runs a toposort first — and emits an audit error when it finds a cycle, instead of silently laying it out as a ball of yarn. Visualization with a conscience.

✓ acyclic — rendered A B C D toposort: A → B → D → C ✓ ✗ cycle detected — refused A B C RuleAuditFailure: rule: causal.cycle cycle: A → B → C → A hint: break edge or set kind: "flow"

Visualization with a conscience

glyph_render({
  data: { shape: "graph",
          nodes, edges },
  layers: [{
    mark: "node-link",
    kind: "causal"   // enables cycle audit
  }]
})
// throws RuleAuditFailure on cycle
// → agent gets actionable feedback,
//   not a bowl of spaghetti

Cycles in a causal graph are always a modeling bug — the chart shouldn't paper over them. Use kind: "flow" when cycles are intentional (Sankey, state machines); use "causal" for Bayes nets, dependency graphs, SEMs.

Other tools: D3-force happily renders cycles as visual knots. Graphviz arranges them but doesn't flag them. No charting library distinguishes causal from flow graphs at the spec level. Only Glyph refuses to lie.

The Whyboard — agents tell stories, not just charts

One chart answers "what does this look like?". The Whyboard answers "why?" When the user asks "why did rides spike at 8am?", Glyph doesn't return a single chart — it returns a deterministic tree of diagnostics: a root, then one branch each for anomaly detection, variance decomposition, and forecast. Every leaf is a real DataHandle with chained lineage — click it, query it, drill in.

User: “Why did rides spike at 8am yesterday?”
root

Rides peaked at 8 (260), 6.5× the 3 low (40).

Source chart · 12 rows · confidence: high · gdf://abc/source
8am ↑
anomaly

Hour=17 is +2.4σ from the mean (240 rides).

2 outliers · threshold 2σ · z=2.4 strongest
17h ↑
relation: filter gdf://abc/a1
decompose

day-of-week explains 73% of the variance.

Top factor: weekday (η² 0.73) · weekend 0.18
weekday weekend 73% 18%
relation: agg gdf://abc/d1
forecast

Next 7-step forecast averages 232 (±18).

Holt-Winters · season=7 · residual σ 9.0
forecast →
relation: transform gdf://abc/f1

One MCP call. Returns the whole tree.

glyph_whyboard({
  handle_id: "rides_today",
  question: "Why did rides spike at 8am?",
  link_group: "investigate-q4"
})

→ {
  root: { kind: "root", explanation: {...},
    children: [
      { kind: "anomaly",   uri: "gdf://abc/a1", ... },
      { kind: "decompose", uri: "gdf://abc/d1", ... },
      { kind: "forecast",  uri: "gdf://abc/f1", ... }
    ]
  }
}

Every leaf is a real, queryable DataHandle.

Each branch's uri is a chained DataHandle with full lineage — glyph_lineage(uri) walks it back to the source file. glyph_query / glyph_drill work directly against it. glyph_render with data.source: "gdf://abc/a1" re-renders the outlier slice.

Pass link_group and a click in any card propagates through the linked-view bus to siblings — the canonical "agents tell a story, humans drive the next step" loop.

Deterministic. Auditable. Same source rows → same tree, every time.

Playground — try a spec

Five preset chart types, each with its full Glyph spec and the byte-stable SVG it compiles to. Click a preset to swap both panes. The specs are valid — paste any of them into glyph_render and you'll get back the exact SVG shown.

Open the live playground → Paste a CSV, edit a spec, see the audit panel update live, share via URL or Gist.

Spec

{
  "data": { "source": "rides.csv" },
  "layers": [{
    "mark": "bar",
    "encoding": {
      "x": "hour",
      "y": "rides"
    }
  }]
}
{
  "data": { "source": "rides.csv" },
  "layers": [{
    "mark": "line",
    "encoding": {
      "x": "hour",
      "y": "rides"
    }
  }]
}
{
  "data": { "source": "rides.csv" },
  "layers": [{
    "mark": "area",
    "encoding": {
      "x": "hour",
      "y": "rides"
    },
    "opacity": 0.75
  }]
}
{
  "data": { "source": "iris.csv" },
  "layers": [{
    "mark": "point",
    "encoding": {
      "x": "sepal_length",
      "y": "sepal_width",
      "color": "species"
    }
  }]
}
{
  "data": { "source": "browsers.csv" },
  "coordinates": { "kind": "polar" },
  "layers": [{
    "mark": "arc",
    "encoding": {
      "theta": "share",
      "color": "browser"
    }
  }]
}
via:  glyph_render(spec) → { svg, handle }

Compiled SVG

rides by hour 7a 10a 1p 4p rides by hour 7a 10a 1p 4p rides by hour 7a 10a 1p 4p iris: sepal width vs length setosa versicolor virginica browser share Q1 2026 Chrome 60% Safari 20% Firefox 20%
→  byte-stable, snapshot-tested SVG

Get started

Install @glyph/mcp once. Register it with your agent's MCP config. Then ask the agent to render a chart. The same flow works in Claude Code, Codex CLI, Cursor, Copilot CLI, and Gemini CLI — anything that speaks MCP.

Claude Claude Code

Anthropic's CLI. Drops Glyph into the agent's tool surface via MCP — no config file editing required.

STEP 1 — VERIFY
npx -y @glyph/mcp --version
STEP 2 — REGISTER MCP SERVER
claude mcp add glyph -- npx -y @glyph/mcp
STEP 3 — USE IN A SESSION
$ claude
> render a bar chart of rides.csv by hour
[Claude calls glyph_describe, glyph_render]
[returns SVG handle: gdf://abc/src]

Codex OpenAI Codex CLI

OpenAI's terminal coding agent. Speaks MCP via a TOML config block.

STEP 1 — VERIFY
npx -y @glyph/mcp --version
STEP 2 — EDIT ~/.codex/config.toml
[mcp_servers.glyph]
command = "npx"
args    = ["-y", "@glyph/mcp"]
STEP 3 — USE IN A SESSION
$ codex
> read rides.csv and render rides per hour
[Codex calls glyph_render, returns SVG]
[handle: gdf://abc/src]

Inline TypeScript use (no agent host)

For pipelines, services, or notebooks that want Glyph as a library rather than an MCP server. Both packages ship as standalone npm modules.

import { compileSpec, renderSvg } from "@glyph/core";
import { createDuckDBEngine, materializeSpec } from "@glyph/duckdb";

const engine = await createDuckDBEngine();
const m = await materializeSpec(engine, {
  data: { source: "rides.csv", format: "csv" },
  layers: [{ mark: "bar", encoding: { x: "hour", y: "rides" } }]
});
const scene = compileSpec({
  spec: m.effectiveSpec, rows: m.result.rows, schema: m.handle.schema
});
const svg = renderSvg(scene);

Comparison — objective feature matrix

No scoring, no totals — just yes / partial / no across 16 features. Glyph picks different trade-offs from each competitor; rows where Glyph differs are where it earns its keep. Sources: each tool's official documentation as of 2026-05. Readers should weight the rows that matter for their use case.

Feature D3 Vega-Lite Plotly Tableau Power BI Glyph
Deterministic byte-stable output no partial no no no yes
Embedded SQL engine (DuckDB) no no no proprietary proprietary yes
Spec as canonical JSON no (imperative) yes partial no no yes
MCP server (agent-native) no no no no no yes (50 verbs)
Built-in chart auditor no no no no no yes (8 rules)
Uncertainty rendering by default no no error bars no no yes (auto)
Cross-process chart lineage (URI) no no no in-product in-product yes (gdf://)
Spec diff / patch (RFC 6902) no no no no no yes
TopoJSON + projections built in via d3-geo no yes yes yes yes (4)
Animation as declarative spec field no no animation_frame pages pages yes (4 kinds)
Multi-modal (chart + table + narrative) no no no in-product in-product yes
License BSD-3 BSD-3 MIT Proprietary Proprietary Apache 2.0
Zero telemetry by default yes yes opt-in no no yes
Mark types built in primitives (any) 14 30+ 25+ 30+ 11
Server-side render → static SVG via jsdom yes (vega-cli) via kaleido no no yes (native)
Canvas / WebGL renderer (10k+ marks) yes no yes (WebGL) yes yes yes (Canvas)

yes — built in  ·  partial — via plugin, separate package, or in-product only  ·  no — not supported.
Tableau and Power BI have proprietary compute engines that are not embeddable in your own code. Vega-Lite is deterministic only when rendered headless (vega-cli); browser-side it inherits browser float drift. D3 is a primitive toolkit, so “mark types” is not a meaningful count.