Getting started
ggts is ggplot2's layered grammar for TypeScript, React, and Svelte 5. A plot is data + an aesthetic mapping + one or more layers, and every plot normalizes to a PortableSpec: strict JSON, no functions, no closures. That JSON is the surface to generate, validate, and correct against.
Choose your surface
For an agent sandbox, start with the CLI and skill setup. For a browser application, install its adapter. React and Svelte use the same PortableSpec and grammar; the rest of your application chooses the framework.
React
npm install @ggts-sh/react
React DOM 18.2 and 19 are supported. In a server-component application, keep interactive charts behind a client component boundary. Save SalesChart.tsx:
"use client";
import { GGPlot, registerAll, type PortableSpec } from "@ggts-sh/react";
registerAll();
const spec: PortableSpec = {
"data": {
"values": [
{
"year": "2023",
"sales": 12
},
{
"year": "2024",
"sales": 18
},
{
"year": "2025",
"sales": 25
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "sales"
}
},
"layers": [
{
"geom": "col"
}
],
"scales": {
"x": {
"type": "band"
}
},
"labs": {
"title": "Annual sales",
"x": "Year",
"y": "Sales"
}
};
export default function SalesChart() {
return <GGPlot spec={spec} height={400} />;
}
Svelte
<script lang="ts">
import { GGPlot, registerAll, type PortableSpec } from "@ggts-sh/svelte";
registerAll();
const spec: PortableSpec = {
"data": {
"values": [
{
"year": "2023",
"sales": 12
},
{
"year": "2024",
"sales": 18
},
{
"year": "2025",
"sales": 25
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "sales"
}
},
"layers": [
{
"geom": "col"
}
],
"scales": {
"x": {
"type": "band"
}
},
"labs": {
"title": "Annual sales",
"x": "Year",
"y": "Sales"
}
};
</script>
<GGPlot {spec} height={400} />
TypeScript
For a script without a browser or framework, install the core and spec packages:
npm install @ggts-sh/core @ggts-sh/spec
Save this complete program as chart.ts:
import { registerAll, renderToSVGString } from "@ggts-sh/core";
import type { PortableSpec } from "@ggts-sh/spec";
registerAll();
const spec: PortableSpec = {
"data": {
"values": [
{
"year": "2023",
"sales": 12
},
{
"year": "2024",
"sales": 18
},
{
"year": "2025",
"sales": 25
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "sales"
}
},
"layers": [
{
"geom": "col"
}
],
"scales": {
"x": {
"type": "band"
}
},
"labs": {
"title": "Annual sales",
"x": "Year",
"y": "Sales"
}
};
console.log(renderToSVGString(spec, { width: 640, height: 400 }));
Run it with Bun to write the SVG:
bun chart.ts > chart.svg
Svelte install and composition
bun add @ggts-sh/svelte @ggts-sh/core
# or: npm install @ggts-sh/svelte @ggts-sh/core
# or: pnpm add @ggts-sh/svelte @ggts-sh/core
@ggts-sh/spec (schema, validate, builder) and @ggts-sh/core (pipeline, headless render) are dependencies of the Svelte package. Install them directly when importing their APIs or bundled data. The ggts CLI is its own package — install @ggts-sh/cli in every sandbox where an agent authors specs, so validation errors and chart-quality warnings surface before a chart ships. The agent skill is also its own package: @ggts-sh/skill (SKILL.md + references/ at the package root) — install it and copy/symlink node_modules/@ggts-sh/skill into the agent's skills directory as ggts/ (or point the agent at node_modules/@ggts-sh/skill/SKILL.md directly). Bundled teaching data lives at @ggts-sh/core/data.
A complete Svelte file
src/routes/+page.svelte:
<script lang="ts">
import { GeomPoint, GGPlot, Labs, ScaleXContinuous, ScaleYMonthDay } from "@ggts-sh/svelte";
import { kyotoSakura } from "@ggts-sh/core/data";
</script>
<GGPlot
data={kyotoSakura}
aes={{ x: "year", y: "bloomDate" }}
>
<GeomPoint />
<ScaleYMonthDay reverse />
<ScaleXContinuous labels="d" domain={[800, 2030]} />
<Labs x="Year" y="Bloom date (earlier ↑)" />
</GGPlot>
Omitted width follows the container; default height is 400px. No chart CSS is required. During server rendering the plot uses a deterministic 832 x 400 fallback, then measures the real container after hydration; inside display: none or a zero-width track it stays not-ready until the container has positive width.
The PortableSpec contract
The same chart as JSON. This is the canonical form — the Svelte component and the TypeScript builder both normalize to it.
{
"data": {
"name": "kyotoSakura"
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "bloomDate"
}
},
"layers": [
{
"geom": "rect",
"data": {
"values": [
{
"epoch": "Medieval warm period",
"year": 950,
"until": 1250,
"top": "03-18",
"bottom": "05-10"
},
{
"epoch": "Little Ice Age",
"year": 1300,
"until": 1850,
"top": "03-18",
"bottom": "05-10"
},
{
"epoch": "Industrial era",
"year": 1850,
"until": 2026,
"top": "03-18",
"bottom": "05-10"
}
]
},
"aes": {
"x": null,
"y": null,
"xmin": {
"field": "year"
},
"xmax": {
"field": "until"
},
"ymin": {
"field": "top"
},
"ymax": {
"field": "bottom"
},
"fill": {
"field": "epoch"
}
},
"params": {
"alpha": 0.55
},
"inspect": false
},
{
"geom": "text",
"data": {
"values": [
{
"epoch": "Medieval warm period",
"midYear": 1100,
"nameDate": "03-14"
},
{
"epoch": "Little Ice Age",
"midYear": 1575,
"nameDate": "03-14"
},
{
"epoch": "Industrial era",
"midYear": 1938,
"nameDate": "03-14"
}
]
},
"aes": {
"x": {
"field": "midYear"
},
"y": {
"field": "nameDate"
},
"label": {
"field": "epoch"
},
"color": {
"value": "#6b7075"
}
},
"params": {
"size": 11
},
"inspect": false
},
{
"geom": "rule",
"aes": {
"color": {
"value": "#b7c1cd"
},
"linetype": {
"value": "dotted"
}
},
"params": {
"yintercept": "04-05",
"linewidth": 0.75
},
"inspect": false
},
{
"geom": "rule",
"aes": {
"color": {
"value": "#b7c1cd"
},
"linetype": {
"value": "dotted"
}
},
"params": {
"yintercept": "04-25",
"linewidth": 0.75
},
"inspect": false
},
{
"geom": "point",
"aes": {
"color": {
"value": "#4a5568"
}
},
"params": {
"alpha": 0.55,
"size": 1.4
}
},
{
"geom": "rule",
"aes": {
"color": {
"value": "#6b7075"
}
},
"params": {
"yintercept": "04-15",
"linewidth": 1
},
"inspect": false
},
{
"geom": "text",
"data": {
"values": [
{
"year": 812,
"bloomDate": "04-15",
"label": "median"
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "bloomDate"
},
"label": {
"field": "label"
},
"color": {
"value": "#6b7075"
}
},
"params": {
"size": 9,
"anchor": "start",
"dy": 22
},
"inspect": false
},
{
"geom": "line",
"stat": "summary_rolling",
"aes": {
"color": {
"value": "#262626"
}
},
"params": {
"fun": "median",
"window": 30,
"curve": "linear",
"linewidth": 1.8
}
},
{
"geom": "point",
"data": {
"values": [
{
"year": 1323,
"bloomDate": "05-04"
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "bloomDate"
},
"color": {
"value": "#2c5282"
}
},
"params": {
"shape": "circle-open",
"size": 3.5
}
},
{
"geom": "point",
"data": {
"values": [
{
"year": 1409,
"bloomDate": "03-27"
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "bloomDate"
},
"color": {
"value": "#c53030"
}
},
"params": {
"shape": "circle-open",
"size": 3.5
}
},
{
"geom": "point",
"data": {
"values": [
{
"year": 2023,
"bloomDate": "03-25"
}
]
},
"aes": {
"x": {
"field": "year"
},
"y": {
"field": "bloomDate"
},
"color": {
"value": "#c53030"
}
},
"params": {
"size": 3
}
},
{
"geom": "segment",
"data": {
"values": [
{
"year": 1323,
"bloomDate": "05-04",
"label": "1323 · May 4, latest on record",
"labelYear": 1305,
"labelDate": "05-07"
},
{
"year": 1409,
"bloomDate": "03-27",
"label": "1409 · March 27, earliest for six centuries",
"labelYear": 1400,
"labelDate": "03-22"
},
{
"year": 2023,
"bloomDate": "03-25",
"label": "2023 · March 25, earliest in 1,200 years",
"labelYear": 2014,
"labelDate": "03-20"
}
]
},
"aes": {
"x": {
"field": "labelYear"
},
"y": {
"field": "labelDate"
},
"xend": {
"field": "year"
},
"yend": {
"field": "bloomDate"
},
"color": {
"value": "#b3452f"
}
},
"params": {
"linewidth": 0.7,
"alpha": 0.9
}
},
{
"geom": "text",
"data": {
"values": [
{
"year": 1323,
"bloomDate": "05-04",
"label": "1323 · May 4, latest on record",
"labelYear": 1305,
"labelDate": "05-07"
},
{
"year": 1409,
"bloomDate": "03-27",
"label": "1409 · March 27, earliest for six centuries",
"labelYear": 1400,
"labelDate": "03-22"
},
{
"year": 2023,
"bloomDate": "03-25",
"label": "2023 · March 25, earliest in 1,200 years",
"labelYear": 2014,
"labelDate": "03-20"
}
]
},
"aes": {
"x": {
"field": "labelYear"
},
"y": {
"field": "labelDate"
},
"label": {
"field": "label"
},
"color": {
"value": "#b3452f"
}
},
"params": {
"size": 11,
"anchor": "end",
"dx": -4
}
}
],
"scales": {
"x": {
"type": "linear",
"labels": "d",
"domain": [
800,
2030
]
},
"y": {
"type": "time",
"temporalKind": "monthDay",
"reverse": true,
"breaks": [
"04-05",
"04-15",
"04-25"
],
"dateLabels": "%b %e",
"domain": [
"05-10",
"03-10"
]
},
"fill": {
"type": "manual",
"domain": [
"Medieval warm period",
"Little Ice Age",
"Industrial era"
],
"range": [
"#f5edc4",
"#dce8f2",
"#f3dcda"
]
}
},
"guides": {
"fill": {
"type": "none"
}
},
"labs": {
"x": "Year",
"y": "Bloom date (earlier ↑)"
},
"theme": "tufte"
}
Rules that matter when generating specs:
- Channels are objects, never bare strings:
{"field": "year"}maps a column,{"value": "#777777"}sets a constant,nullunsets a channel inherited from the plot-levelaes. - Data has three forms.
{"values": [...]}inlines rows;{"columns": {...}}is the columnar form;{"name": "..."}refers to adatasetsentry. Inlinevaluesfor data small enough to read,datasets+columnsfor anything large or shared between layers. Never truncate rows silently — say so, or point at the full source. layersis ordered bottom to top and must hold at least one layer. A layer may carry its owndata, which then replaces the plot's for that layer.- Stats are declarative.
{"geom": "smooth", "params": {"method": "loess"}}fits in the pipeline; do not precompute a trend column and pass it off as raw data.
The full machine-readable contract is /schema/v0.json.
The validate loop
validate(spec) checks schema shape; validate(spec, { profile }) adds data-aware checks without shipping data; { lint: true } also returns advisories for valid-but-questionable specs.
Every error carries a stable code, a JSON path into the spec, a message, and a fix naming the change to make. That is the correction loop: emit, validate, apply the fix at the path, re-emit. Do not guess, and do not fall back to a different chart — the fix says what is wrong.
import { validate } from "@ggts-sh/spec";
const result = validate(spec);
if (!result.ok) {
for (const error of result.errors) {
console.error(error.code, error.path, error.fix);
}
}
The complete error catalog, with the fix for each code, is at /guide/errors; advisories are at /guide/advisories.
Headless rendering
No browser, no DOM. renderToSVGString is pure:
import { registerAll, renderToSVGString } from "@ggts-sh/core";
// Headless/spec-driven rendering opts into the full grammar explicitly (#1420).
registerAll();
const svg = renderToSVGString(spec, { width: 900, height: 360 });
The installed CLI writes SVG to stdout and JSON Lines diagnostics to stderr, with exit classes documented at /reference/cli:
ggts render spec.json > chart.svg 2> diagnostics.jsonl
Building specs in TypeScript
The fluent builder produces the same PortableSpec, with types:
import { aes, gg } from "@ggts-sh/svelte";
import { kyotoSakura } from "@ggts-sh/core/data";
const spec = gg(kyotoSakura, aes({ x: "year", y: "bloomDate" }))
.geomPoint()
.geomLine({
stat: "summary_rolling",
fun: "median",
window: 30,
curve: "linear",
})
.spec();
Bundled data
@ggts-sh/core/data exports seven cited teaching tables (each also served as JSON under the same name on the docs site):
kyotoSakura— 838 peak cherry-blossom dates for Kyoto, 812-2026 CE (year,bloomDate,bloomDoy). Time series. Data copyright Yasuyuki Aono; citeKYOTO_SAKURA_CITATION.palmerPenguins— 333 complete Palmer Archipelago penguin measurements (species,island, bill/flipper/mass,sex,year, stableid). Distribution and categorical groups. CC0; citePALMER_PENGUINS_CITATION.mpg— 234 EPA fuel-economy rows for 38 popular models, 1999/2008 (manufacturer,model,displ,class,drv,cty,hwy, …). Categorical comparison. CiteMPG_CITATION.
chocolateBars— 2,530 Flavors of Cacao bar reviews (cocoaPercent,rating, company location, bean origin). Dense scatter and heatmaps. Via TidyTuesday 2022-01-18; citeCHOCOLATE_BARS_CITATION.coffeeRatings— 1,338 Coffee Quality Institute cupping lots (totalCupPoints, aroma/flavor, origin, processing). Distributions and continuous scatter. Via TidyTuesday 2020-07-07; citeCOFFEE_RATINGS_CITATION.beerProduction— 36 US national beer-production totals by package type, 2008–2019 (year,package,barrelsMillions). Dodged multi-series bars. Via TidyTuesday 2020-03-31; citeBEER_PRODUCTION_CITATION.fastfoodMenu— 515 US fast-food entrée nutrition rows (restaurant,calories, fat/protein/sodium). Categorical scatter and jitter. Via TidyTuesday 2018-09-04; citeFASTFOOD_MENU_CITATION.
Grammar vocabulary
- Geoms — every mark, defaults, stats, positions, and params
- Guides and legends — GuideLegend, colorbar, colorsteps, axis, none
- Labs — title, subtitle, caption, axis/legend titles
- Axes and ticks — GuideAxis, breaks/labels, collision, grids
- Labels — chrome vs ticks vs GeomText/GeomLabel/SF labels
- Statistics and positions — stats, jitter, stacking
- Scales — every Scale* component (position, color, style)
- Scales and guides — continuous, discrete, manual, temporal
- Facets and coordinates — small multiples, flip, fixed aspect
- Chart themes and palettes — paper/ink chrome and data color
- Themes reference and palettes reference — props, tokens, scheme → scale helpers
- Interactions — inspect, pin, selection, zoom, linked views
- Production — sizing, SVG/canvas, SSR, export, support matrix
- Lifecycle — what is stable and what is not