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
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.
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.
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.
User pastes a CSV. The agent calls glyph_describe → glyph_clarify
→ glyph_render → glyph_drill → glyph_explain.
Every step deterministic; every handle addressable.
glyph_describe("rides.csv")
glyph_render(spec, modalities)
glyph_drill(handle, selector)
glyph_explain(handle)
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)
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
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)
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
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 ]
Same JSON spec on the left, the rendered chart on the right.
Categorical x + quantitative y; the canonical first-pass viz.
Time series with smoothed connectors. Add animation: { kind: "stage" } for entrance.
2D categorical × categorical with quantitative color. New in PR49.
Project GeoJSON polygons through naturalEarth / mercator / albers.
Tukey quartiles + whiskers + outliers, per categorical group. New in PR50.
SMIL-animated bar widths per frame; attachScrub wires a slider in the browser.
Set coordinates: { type: "polar" }. Bar + polar → arcs. Same Scene IR, byte-stable SVG.
data.hierarchy: {...} + mark: "treemap". Squarified algorithm. No DuckDB.
Radial partition of the same hierarchy. Each ring = one depth level.
data.graph: { nodes, edges } + spec.seed: 42. Velocity-Verlet, byte-stable across runs.
data.grid: { rows, cols, values } + thresholds. 16 marching-squares cases including saddles.
glyph_morph_render(specA, specB) emits one SVG with <animate from to>. Smooth transitions between scene states.
Low-sample data → hatched bars + n-badge. No spec change — driven by provenance.confidence.
glyph_audit_spec(spec) catches misleading charts. Truncated y-axis, undisclosed log, dual-axis, more.
Line mark + polar coords = radar/star chart. Same encoding API, different coordinate system.
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.
Same spec → identical bytes across runs, OSes, Node versions, decades. Snapshot-tested in CI on a 6-cell matrix.
Low-n samples auto-hatch and emit a confidence badge. Charts ship their own honest footnotes; no opt-in required.
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.
Long-running verbs emit notifications/progress milestones. Agents stream partial state instead of blocking on a 30-second forecast.
Before guessing, the planner asks explicit clarification_questions. Agents pin answers via glyph_story_clarify — no silent assumptions.
Hovers, clicks, zooms tallied in ~/.glyph/. Aggregates feed planner-bias; nothing leaves your machine. Audit-safe by default.
Every gdf:// handle carries a TTL. The reaper sweeps stale ones — but parents with live descendants are spared automatically.
Record a sequence of MCP verb calls as a portable JSON macro. Replay against new data with {{params.X}} substitution — same analysis, same SVG.
Story plans are deterministic; the planner is your choice of LLM. Swap Claude, Codex, or Gemini without changing the protocol.
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.
Things competitors can't ship without rewriting their compiler, runtime, or data model.
gdf://session/<handle> URI for the materialized dataset.gdf://session/<h>glyph_renderglyph_publishglyph_subscribeglyph_linked_publishglyph_lineageparents[]TTL reaperrefcount on parentsmodality: "chart"|"table"|"text"roundPx()snapshot testsseed: numbermulberry32scenegraph/voronoi.tsglyph diff__snapshots__/*.svgvitest --update679 testsglyph_query@duckdb/node-apiglyph_drill turns a clicked mark into a WHERE clause against the materialized view.glyph_drilldata-* keysglyph_define_metricglyph_list_metricsdata.source: "file.parquet"synthesizeInlineDataHandleglyph_spec_diff returns the structural delta between two specs.glyph_spec_diffglyph_spec_patch applies a patch to a spec deterministically.glyph_spec_patchJSON Pointerglyph_macro_recordglyph_macro_replayglyph diff a.json b.json outputs human-readable HTML/MD report.glyph diff --format html|mdglyph_suggest_scale returns the optimal scale type given a column.glyph_suggest_scaleThings nobody else has built. A competitor could build them, but hasn't.
50 verbstools/listglyph_explain emits a pure-fn description of a chart — no LLM call.glyph_explainglyph_anomaly, glyph_forecast, glyph_decompose, glyph_change_point.glyph_anomalyglyph_forecastglyph_capabilities returns the verb surface for THIS version → agents can self-discover.glyph_capabilitiesglyph_story_planmodel: "..."glyph_story_plan outputs a multi-chart narrative DAG.glyph_story_planglyph_whyboard_*glyph_whyboard_diffglyph_clarify returns explicit unknowns before rendering.glyph_clarifynotifications/progress — long-running plans stream partial state.notifications/progressprovenance.confidenceSceneUncertaintyglyph_audit.glyph_audit8 rulesdata.shape: "graph"cycle = errorglyph_trust returns a 0-100 score with itemized contributors.glyph_trustglyph_audit_logrenderSvg(scene)renderCanvas(scene)glyph_morph_render animates between two spec states via SMIL — zero JS.glyph_morph_renderanimation.kind: "morph"animation.kindrenderFrames: deterministic per-frame array of scenes for film/PNG export.renderFrames(spec)data.shape: "topojson"projectionglyph_memory_save + glyph_memory_recall persist named specs across runs.glyph_memory_saveglyph_memory_recall~/.glyph/engagementglyph_engagement shows which charts were hovered/zoomed/clicked.glyph_engagementttl_msreaper.ts~/.glyph/audit.log across runs, surveys, and machines.~/.glyph/audit.logglyph_render returns all three modalities.modalities: ["chart","table","text"]glyph_linked_publish.modality: "chart"notifications/progress on long-running verbs (forecast, story) — agents stream partial.notifications/progressbyte-identity testsglyph_close_preview: clean shutdown of the preview-server, including released handles.glyph_close_previewThings that could be copied — but reflect deliberate choices nobody else makes.
skills/4 platformsrole.mdglyph diff CLI outputs HTML or Markdown spec-diff reports for PR comments.glyph diff --format html|mdLICENSEno analytics.github/workflows6 cellsItems 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.
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.
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.
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.
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.
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.
d3.interpolate + Flubber for path morphing;
Vega-Lite has no morph primitive; Tableau/Power BI can't
animate spec changes at all.
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
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.
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.
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).
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 field — animation: { kind: "race", frame_field: "year" } —
and the compiler emits a single SVG with SMIL <animate> elements.
No JS at runtime. Bytewise stable across renders.
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.
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.
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.
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.
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.
// 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.
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.
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.
d3-geo + topojson-client).
Plotly Mapbox needs an API token. Tableau maps are
proprietary. Glyph: spec field flip.
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.
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.
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.
gdf://abc/source
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", ... }
]
}
}
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.
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.
{
"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"
}
}]
}
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.
Anthropic's CLI. Drops Glyph into the agent's tool surface via MCP — no config file editing required.
npx -y @glyph/mcp --version
claude mcp add glyph -- npx -y @glyph/mcp
$ claude
> render a bar chart of rides.csv by hour
[Claude calls glyph_describe, glyph_render]
[returns SVG handle: gdf://abc/src]
OpenAI's terminal coding agent. Speaks MCP via a TOML config block.
npx -y @glyph/mcp --version
[mcp_servers.glyph]
command = "npx"
args = ["-y", "@glyph/mcp"]
$ codex
> read rides.csv and render rides per hour
[Codex calls glyph_render, returns SVG]
[handle: gdf://abc/src]
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);
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.