@ggts-sh/skill 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Liam O'Dea
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @ggts-sh/skill
2
+
3
+ The ggts agent skill: `SKILL.md` plus deep-dive `references/` that teach a
4
+ coding agent the shared grammar of graphics — TypeScript builders, React
5
+ and Svelte components, PortableSpec JSON, and the `ggts check` /
6
+ `ggts render` feedback loop.
7
+
8
+ This package is the one published home of the skill. It versions in lock-step
9
+ with [`@ggts-sh/spec`](https://www.npmjs.com/package/@ggts-sh/spec),
10
+ [`@ggts-sh/core`](https://www.npmjs.com/package/@ggts-sh/core),
11
+ [`@ggts-sh/compose`](https://www.npmjs.com/package/@ggts-sh/compose),
12
+ [`@ggts-sh/react`](https://www.npmjs.com/package/@ggts-sh/react),
13
+ [`@ggts-sh/svelte`](https://www.npmjs.com/package/@ggts-sh/svelte), and
14
+ [`@ggts-sh/cli`](https://www.npmjs.com/package/@ggts-sh/cli): a given version
15
+ number describes the spec, the renderers, and this skill as of the same
16
+ release. Pin it like any other dependency and let dependabot (or
17
+ npm-check-updates) tell you when the bundled skill in your repo is stale.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install --save-dev --save-exact @ggts-sh/skill @ggts-sh/cli
23
+ ```
24
+
25
+ The package root **is** the skill directory: `SKILL.md` sits next to this
26
+ README. Skill loaders key off the frontmatter (`name: ggts`), not the
27
+ directory name.
28
+
29
+ ## Use
30
+
31
+ Point your agent at the skill, or copy/symlink it into your agent's skills
32
+ directory under the name `ggts`:
33
+
34
+ ```sh
35
+ # Claude Code
36
+ cp -R node_modules/@ggts-sh/skill .claude/skills/ggts
37
+
38
+ # pi
39
+ cp -R node_modules/@ggts-sh/skill .pi/agent/skills/ggts
40
+
41
+ # or reference it in place
42
+ node_modules/@ggts-sh/skill/SKILL.md
43
+ ```
44
+
45
+ Re-run the copy on every version bump (a two-line `postinstall` or a sync
46
+ script works); the dependabot PR is the signal that the skill changed.
47
+
48
+ The skill assumes the agent can also run
49
+ [`@ggts-sh/cli`](https://www.npmjs.com/package/@ggts-sh/cli)
50
+ (`ggts check` and `ggts render`) for spec validation and headless SVG rendering — install it
51
+ in every sandbox where an agent authors specs.
52
+
53
+ ## Contents
54
+
55
+ - `SKILL.md` — target selection, shared grammar, authoring workflow.
56
+ - `references/react.md` and `references/svelte.md` — adapter instructions.
57
+ - `references/` — geoms and stats, scales and palettes, themes, interactions,
58
+ composition surfaces, recipes.
59
+
60
+ ## Guarantees
61
+
62
+ CI checks the teaching contracts and shipped examples:
63
+
64
+ 1. **Content contracts** (`scripts/skill-content/*.test.ts`,
65
+ `scripts/skill-package.test.ts`) — inventory completeness for every geom,
66
+ stat, position, theme, and color scheme; every complete JSON fence
67
+ normalizes and validates; pack shape and lock-step version. Packed-consumer
68
+ checks also compile complete TSX and Svelte examples from the installed skill.
69
+ 2. **Trigger / disclosure contracts** (`scripts/skill-trigger.test.ts`) —
70
+ frontmatter description quality (the loader's selection signal), balanced
71
+ positive/negative trigger fixtures under `evals/trigger-cases.json`,
72
+ relative-link integrity, and progressive disclosure of every `references/`
73
+ file from `SKILL.md`.
74
+ 3. **Held-out NL→spec evals** (`tests/evals/`, `bun run evals`) — model
75
+ capability on PortableSpec authoring with deterministic graders. These do
76
+ **not** load this skill today; agent-in-the-loop skill evals (with/without
77
+ skill A/B, multi-trial trigger accuracy) are tracked as follow-up work.
78
+
79
+ `evals/trigger-cases.json` is not packed to npm (`files` is only `SKILL.md` +
80
+ `references/`). It is the living seed for future skill-loaded agent evals.
package/SKILL.md ADDED
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: ggts
3
+ description: Build data visualizations with ggts, a grammar-of-graphics charting library (ggplot2 semantics, React and Svelte 5 components, TypeScript builders, JSON specs, headless SVG rendering). Use whenever creating, editing, validating, or debugging charts, plots, graphs, scatter plots, bar charts, histograms, line charts, boxplots, density plots, violins, heatmaps, maps, faceted/small-multiple charts, or data visualization in a JavaScript/TypeScript/React/Svelte project; when code imports from "@ggts-sh/react", "@ggts-sh/svelte", "@ggts-sh/spec", or "@ggts-sh/core"; when composing GGPlot with Geom*/Scale*/Theme*/Facet*/Coord*/Guide*/Labs children; when emitting a ggts plot spec JSON; or when rendering charts server-side/headless to SVG.
4
+ ---
5
+
6
+ # ggts
7
+
8
+ ggplot2’s layered grammar for TypeScript, with headless rendering and React
9
+ and Svelte adapters. A chart spec carries the same grammar between surfaces.
10
+
11
+ ## Choose the target
12
+
13
+ 1. Follow the user’s explicit framework choice. Otherwise inspect the target
14
+ application’s package.json and imports. In a mixed monorepo, choose the
15
+ application before generating component code.
16
+ 2. For a sandbox or an unspecified framework, author PortableSpec JSON or use
17
+ the TypeScript builder; run the CLI before shipping the chart.
18
+ 3. For React, read [React](references/react.md). For Svelte, read
19
+ [Svelte](references/svelte.md). Keep framework callbacks and host-only
20
+ interactions in that adapter; PortableSpec holds the portable grammar.
21
+ 4. In every agent sandbox, install `@ggts-sh/cli` and `@ggts-sh/skill`
22
+ at matching local versions and keep the lockfile. Expose
23
+ the installed skill to the agent, refreshing any copied skill on upgrades.
24
+ Run `ggts check chart.json`, apply relevant diagnostics, then
25
+ `ggts render chart.json > chart.svg` and inspect the output. A local npm
26
+ install exposes these commands through `npm exec -- ggts`.
27
+
28
+ ## Registration (call these)
29
+
30
+ The `@ggts-sh/core` barrel is side-effect-free. Call `registerAll()` once
31
+ at startup for every agent-authored JSON, builder, `layers`, or `<GGPlot spec>`
32
+ path. It installs the full grammar; both `ggts render` and `ggts check` do
33
+ this for you. Missing opt-ins throw `not registered in this build` and name
34
+ the required registration. `registerBasic()` alone is insufficient for
35
+ specialty stats and Temporal.
36
+
37
+ Framework geom children self-register their own geom and default stat;
38
+ stat overrides still need the matching family. Spec-driven temporal options
39
+ need `installTemporal()` or `registerAll()` because there is no Temporal child.
40
+ Use the full default for agents. If the user requests a lean bundle, read the
41
+ [registration reference](references/registration.md) for the exact family,
42
+ color, theme, and headless entry-point rules before selecting imports.
43
+
44
+ ## Mental model
45
+
46
+ **Everything that composes a plot is a layer in component composition.** Marks _and_ the
47
+ seven grammar families (scale, theme, coord, facet, labs, guides, legend)
48
+ register as `Layer` kinds via `createPlotLayer` / geom factories. Never call
49
+ Scale/Theme/Guide/Labs/Coord/Facet/Legend “non-layers.”
50
+
51
+ Two serializations of the same grammar:
52
+
53
+ ```text fragment
54
+ # React / Svelte composition
55
+ plot children = mark layers + grammar layers
56
+ mark = Geom* → Layer.kind "mark"
57
+ grammar = Scale* | Theme* | Coord* | Facet* | Labs | Guide* | Legend
58
+ → Layer.kind "scale"|"theme"|"coord"|"facet"|"labs"|"guides"|"legend"
59
+
60
+ # PortableSpec JSON (agent / headless wire format)
61
+ spec = data + aes + layers[] # layers[] = MARKS ONLY
62
+ + scales? coord? facet? labs? theme? guides? legend?
63
+ one mark layer = { geom, stat?, position?, positionParams?, aes?, params?, render?, data? }
64
+ ```
65
+
66
+ PortableSpec puts grammar pieces in top-level keys because that is how the
67
+ JSON schema folds them — **not** because they are outside the layer model.
68
+ “Non-mark layer” / “grammar layer” is correct; “non-layer grammar component”
69
+ is wrong and must never appear in issues, docs, or comments.
70
+
71
+ - **data**: `{"values": [rows]}`, `{"columns": {name: [...]}}`, or
72
+ `{"name": "dataset"}` (resolved from `spec.datasets` or runtime data).
73
+ - **mark `layers[]`** draw in order — later marks on top (z-order).
74
+ - Geom defaults: bar → count+stack, histogram → bin+stack, col/area →
75
+ identity+stack, boxplot → boxplot+dodge, violin → ydensity+dodge,
76
+ jitter → point+jitter, freqpoly → bin, smooth → smooth, count → sum,
77
+ density → density, hex → bin_hex, everything else identity+identity.
78
+
79
+ ## Aes: JSON specs vs component props
80
+
81
+ The one rule that differs between the two skins:
82
+
83
+ | Surface | `x: "displ"` bare string | canonical |
84
+ | ------------------------------------------- | ------------------------------------- | ------------------------ |
85
+ | JSON `PortableSpec` (incl. `spec` prop) | INVALID | `{"field": "displ"}` |
86
+ | React / Svelte `aes` prop (plot or `Geom*`) | valid shorthand, expands to `{field}` | same object form also OK |
87
+
88
+ Canonical channel forms: `{"field": "col"}` maps a column, `{"value": "red"}`
89
+ is a constant, `{"stat": "count"}` reads a stat output, `null` unsets an
90
+ inherited channel. Layer aes merges over plot aes per channel.
91
+ Channels (25): x, y, color, fill, size, linewidth, alpha, shape, linetype,
92
+ group, label, weight, ymin, ymax, xmin, xmax, xend, yend, width, height, z,
93
+ map_id, angle, radius, sample. Mapped size/linewidth/alpha/shape/linetype work
94
+ only on the geoms in `STYLE_AESTHETIC_GEOMS`.
95
+
96
+ ## The validation contract (use it!)
97
+
98
+ `validate(spec)` = schema shape. `validate(spec, { profile })` adds data-aware
99
+ checks against a `DataProfile` —
100
+ `{"fields": [{"name": "displ", "type": "quantitative"}], "rowCount": 234}`
101
+ (types: quantitative | temporal | ordinal | nominal). Every error is:
102
+
103
+ ```json fragment
104
+ {
105
+ "code": "unknown-field",
106
+ "path": "/layers/0/aes/x",
107
+ "message": "Unknown field \"dsipl\" (available: displ, hwy). Did you mean \"displ\"?",
108
+ "allowed": ["displ", "hwy"],
109
+ "fix": {
110
+ "description": "Map \"x\" to \"displ\".",
111
+ "example": { "field": "displ" }
112
+ }
113
+ }
114
+ ```
115
+
116
+ **Errors include `fix.example` — apply it at `path` and re-validate.**
117
+ `validate(spec, { lint: true })` additionally returns advisories for
118
+ valid-but-questionable specs (line over unordered categories, >10 discrete
119
+ colors, stacked negative areas, discrete×discrete scatter, transform-domain
120
+ mixed-sign data, fractional calendar years on a linear scale); `lintSpec(spec)`
121
+ is the standalone equivalent. Advisories never block; fix them when they match
122
+ intent. `normalize(input)` canonicalizes
123
+ authoring sugar into a `PortableSpec`; `isPortable`/`toPortable` check and
124
+ strip runtime-only fields. CLIs: `ggts render spec.json > out.svg`
125
+ (from `@ggts-sh/cli`; JSON-line diagnostics on stderr — exit 3 means
126
+ validation errors, exit 0 with stderr output means quality warnings worth
127
+ fixing) and `ggts-codemod [--write] src` (ships with `@ggts-sh/svelte`).
128
+
129
+ **CLI coverage for interaction:** the CLI always covers PortableSpec validation,
130
+ pipeline warnings/advisories, scale diagnostics (`source: "scale"`), and
131
+ spec-lint (`source: "spec-lint"`). Host inspect **mode** is not a PortableSpec
132
+ field — without intent the agent loop never sees inspect×geom bar/col guide
133
+ codes such as `INTERACTION_INSPECT_X_ON_COL`. When the host will enable inspect,
134
+ declare it:
135
+
136
+ ```sh fragment
137
+ ggts render --inspect xy spec.json > out.svg
138
+ # stderr: kind advisory|warning, source "interaction", bar/col x-guide codes
139
+ ```
140
+
141
+ Modes match the host enum (`auto|exact|x|y|xy`). This path covers the pure
142
+ bar/col axis-guide collectors only (same codes as host for that slice).
143
+ High-cardinality discrete and runtime key/lineage/wiring diagnostics still
144
+ require a mounted host's `ondiagnostic`.
145
+
146
+ ## Interactions (host props / children, not PortableSpec fields)
147
+
148
+ Opt-in host capabilities: `<Inspect />` for tooltips and crosshairs;
149
+ `select` / `zoom` on `<GGPlot>`; legend emphasis and data-changing filter via
150
+ `<GuideLegend channel="color" focus />` / `filter` (per aesthetic; host-only,
151
+ not in `guideLegend()` / PortableSpec). Row identity for selection, legend
152
+ focus, and linked views **defaults** to an `id` column when present, else
153
+ the row index (order-stable only) — ordinary charts omit custom identity.
154
+ Override with `identity` on `<Inspect>`, object-form `select`, or
155
+ `createPlotInteraction` when a non-`id` durable field or accessor is needed
156
+ (for example `<Inspect identity="year" />`). Plot-level `key` is deprecated
157
+ since 0.21. Link plots by sharing one `createPlotInteraction()` controller
158
+ and matching `interactionScope` channels; observe everything through
159
+ `oninteraction` or per-capability handlers.
160
+
161
+ **Inspect mode and hit hygiene:** prefer `mode="auto"` when library auto matches
162
+ the mark; pin `mode="exact"` (or leave auto) on violin, boxplot, and discrete-axis
163
+ error bars — never freescrolling `x`/`y`/`xy` on those band geoms; use
164
+ `x`/`y`/`xy` only for continuous shared-axis or free 2d hits; set
165
+ `inspect={false}` on decorative layers in multi-layer stories. The CLI SVG
166
+ loop cannot validate host inspect mode — hover/pin in a browser after edits.
167
+ Read [references/interactions.md](references/interactions.md) (Choosing inspect
168
+ mode, Multi-layer hit hygiene) before enabling Inspect or writing interactive
169
+ or linked-view code.
170
+
171
+ ## Pointers
172
+
173
+ - JSON Schema (constrained decoding): `packages/spec/schema/v0.json` in the
174
+ repo, `/schema/v0.json` on the docs site, or
175
+ `import schema from "@ggts-sh/spec/schema/v0.json"`.
176
+ - Full corpus for models: `/llms-full.txt` on the docs site (all guide prose
177
+ plus every example with spec JSON and Svelte source); index at `/llms.txt`.
178
+ - Error catalog: `/guide/errors`; advisories: `/guide/advisories`;
179
+ lifecycle/editions: `/guide/lifecycle` (specs are stamped with the current
180
+ appearance edition, currently 2). Upgrading off deprecated props:
181
+ `/guide/upgrading`.
182
+ - References in this skill — read the one that matches the task:
183
+ [geoms-and-stats.md](references/geoms-and-stats.md) (any geom/stat/position
184
+ beyond the everyday set, annotations),
185
+ [scales-and-palettes.md](references/scales-and-palettes.md) (scale options,
186
+ palettes, temporal),
187
+ [themes.md](references/themes.md) (every product theme name, shells,
188
+ role overrides),
189
+ [composition-surfaces.md](references/composition-surfaces.md) (coords,
190
+ facets, guides, merge semantics),
191
+ [interactions.md](references/interactions.md) (tooltips, selection, linking),
192
+ [recipes.md](references/recipes.md) (long-tail chart recipes).
193
+ Registration calls live in the [registration reference](references/registration.md).
194
+
195
+ - [Grammar and recipes](references/grammar.md): choose charts, scales, and themes;
196
+ the same recipes work in every adapter.
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@ggts-sh/skill",
3
+ "version": "0.43.0",
4
+ "description": "Agent skill for the shared grammar of graphics: TypeScript, React, Svelte, portable JSON, and the ggts check/render feedback loop. Versions with all framework adapters.",
5
+ "keywords": [
6
+ "agent-skill",
7
+ "agents",
8
+ "charts",
9
+ "claude-code",
10
+ "dataviz",
11
+ "ggplot2",
12
+ "grammar-of-graphics",
13
+ "llm",
14
+ "plot",
15
+ "react",
16
+ "skill",
17
+ "svelte",
18
+ "typescript",
19
+ "visualization"
20
+ ],
21
+ "homepage": "https://ggts.sh/",
22
+ "bugs": "https://github.com/ggts-sh/ggts/issues",
23
+ "license": "MIT",
24
+ "author": "Liam O'Dea",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/ggts-sh/ggts.git",
28
+ "directory": "packages/skill"
29
+ },
30
+ "files": [
31
+ "SKILL.md",
32
+ "references"
33
+ ],
34
+ "type": "module",
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ }
39
+ }
@@ -0,0 +1,199 @@
1
+ <!-- Source of truth: packages/svelte/src/lib/index.ts (component exports), packages/svelte/src/lib/plot-props.ts, packages/svelte/src/lib/layers/ (merge semantics, grammar-families). Theme roster lives in themes.md (asserted complete by scripts/skill-content/inventory.test.ts). -->
2
+
3
+ # Composition surfaces: coords, facets, guides, labs
4
+
5
+ Every surface here is a **grammar layer** (non-mark `Layer` kind). Each exists
6
+ in three equivalent forms: a key in the JSON `PortableSpec`, a removed
7
+ `<GGPlot>` prop (gone since 0.13.0; codemod rewrites old sources), and a
8
+ declaration-only child component
9
+ (canonical in Svelte). Child components emit no markup, register on init via
10
+ `createPlotLayer` / `registerPlotLayer`, unregister on destroy, and are inert
11
+ without a `<GGPlot>` ancestor.
12
+
13
+ **Ontology:** scale, theme, coord, facet, labs, guides, and legend **are
14
+ layers**. PortableSpec’s top-level `layers[]` array holds marks only; these
15
+ families fold into sibling keys. That is wire format, not “not a layer.”
16
+
17
+ ## Themes
18
+
19
+ Full product roster (37 names), shells, and role overrides:
20
+ [themes.md](themes.md). Theme is a REPLACE family (last registration wins) —
21
+ see replace-vs-merge below.
22
+
23
+ ## Coords
24
+
25
+ Components: `Coord` (escape hatch, `value: CoordSpec | "flip"`), `CoordFlip`,
26
+ `CoordCartesian`, `CoordTransform`, `CoordFixed`, `CoordEqual` (alias of
27
+ `CoordFixed`), `CoordSf`, `CoordRadial`, `CoordPolar` (ggplot2 `coord_polar`
28
+ alias shell — prefer `CoordRadial` for new work). `<CoordCartesian/>`
29
+ registers `{"type":"cartesian"}`, which `normalize()` drops when bare — use
30
+ it to clear an earlier coord under REPLACE. JSON forms:
31
+
32
+ ```json fragment
33
+ "coord": {"type": "flip"}
34
+ ```
35
+
36
+ ```json fragment
37
+ "coord": {
38
+ "type": "transform",
39
+ "x": {"transform": "log10", "limits": [1, 1000], "reverse": false, "expand": true},
40
+ "y": {"transform": "sqrt"},
41
+ "clip": true
42
+ }
43
+ ```
44
+
45
+ ```json fragment
46
+ "coord": {
47
+ "type": "radial",
48
+ "theta": "y",
49
+ "innerRadius": 0.4,
50
+ "expand": false
51
+ }
52
+ ```
53
+
54
+ - Coord transform runs AFTER stats and scale training (scale `transform` runs
55
+ before stats). It preserves stat inputs; coordinate `limits` are a viewport —
56
+ they never remove rows or recompute statistics. `expand` (default true) adds
57
+ a 5% display expansion to explicit limits; `clip` (default true) clips marks
58
+ to the panel.
59
+ - Non-identity coordinate transforms require continuous, non-temporal
60
+ position scales on the transformed axes.
61
+ - `{"type": "fixed", "ratio": 1}`: ratio is physical y-unit length divided by
62
+ physical x-unit length (default 1). Layout fits the largest centered data
63
+ rectangle after chart chrome. Fixed-aspect coords reject free positional
64
+ facet scales (`coord-fixed-free-scales` error).
65
+ - `{"type": "sf"}` (`<CoordSf/>`): fixed-aspect coords for already-projected
66
+ map data; no CRS reprojection in v1. Accepts `ratio` like fixed.
67
+ - `{"type": "radial", …}` (`<CoordRadial/>`, helpers `coordRadial` /
68
+ `coord_radial`): polar coordinates (ggplot2 `coord_radial`). Maps one
69
+ aesthetic to angle and the other to radius — pie, coxcomb, polar scatter.
70
+ Options: `theta` (`"x"` default | `"y"` for pie), `start` / `end` (radians
71
+ from 12 o'clock; default full circle), `innerRadius` (0–1 fraction, donut
72
+ hole), `expand` (default true; false for pie/coxcomb), `clip` (default
73
+ off), `reverse` (`"none"` | `"theta"` | `"r"` | `"thetar"`),
74
+ `thetaLimits` / `rLimits`. Full-circle panels use a square data rectangle.
75
+ Equivalent skins: JSON ≡ `coordRadial({…})` ≡ `.coordRadial({…})` ≡
76
+ `<CoordRadial …/>`.
77
+ - `CoordPolar` / `coordPolar` / `coord_polar`: superseded ggplot2
78
+ `coord_polar` alias. Emits `{ type: "radial", clip: true, … }`;
79
+ `direction: -1` maps to `reverse: "theta"`. Prefer `<CoordRadial/>` for
80
+ partial arcs, inner radius, and reverse.
81
+
82
+ ## Facets
83
+
84
+ Components: `Facet` (full `FacetInput` surface), `FacetWrap` (`field`,
85
+ `ncol?`, `scales?`, `strip?`), `FacetGrid` (`rows?`, `cols?`, `scales?`,
86
+ `strip?`). `field`/`rows`/`cols` accept a bare string or
87
+ `{ field, levels?, labels? }` for authored panel order and strip text. Wrap
88
+ form is mutually exclusive with rows/cols; a bare `<Facet/>` fails validate
89
+ with `facet-form-missing`.
90
+
91
+ ```json fragment
92
+ "facet": {"wrap": {"field": "region"}, "ncol": 3, "scales": "free_y",
93
+ "strip": {"position": "top", "show": true}}
94
+ ```
95
+
96
+ - Panels partition the data BEFORE stats and positions run — each panel
97
+ computes its own counts, bins, and stacks.
98
+ - `scales`: `"fixed"` (default — panels share both positional scales) |
99
+ `"free"` | `"free_x"` | `"free_y"`. Discrete color/fill assignments are
100
+ ALWAYS global (one legend), whatever `scales` says.
101
+ - `strip`: `position` `"top"` (default) | `"bottom"` | `"left"` | `"right"`
102
+ (left/right strips take layout space rather than overlaying the panel);
103
+ `show` (default true) — set false when direct labels are authored elsewhere.
104
+
105
+ ## Guides, legend, labs
106
+
107
+ Guides are keyed by aesthetic channel: `x`, `y`, `color`, `fill`, `size`,
108
+ `linewidth`, `alpha`, `shape`, `linetype`. Components: `Guides`
109
+ (escape hatch, `value: GuidesSpec`), plus one shell per guide type carrying a
110
+ `channel` prop — `GuideAxis` (channels `x`/`y` only), `GuideLegend`,
111
+ `GuideColorbar`, `GuideColorsteps` (non-position channels), `GuideNone` (any
112
+ channel). `<GuideLegend channel="color" position="bottom"/>` assembles
113
+ `guides: {"color": {"type": "legend", "position": "bottom"}}`.
114
+
115
+ - Top-level `guides` override a scale-local `guide`.
116
+ - Shared options: `title`, `theme` (bounded size/gap overrides), `collision`;
117
+ non-position guides add integer `order` (placement rank), `position`
118
+ `"auto"|"right"|"bottom"`, `direction` `"auto"|"vertical"|"horizontal"`,
119
+ and `force` (show a guide suppressed by default, e.g. identity scales).
120
+ - Responsive rule: `auto`-positioned non-position guides move below the panel
121
+ when the viewport is 480px wide or less, or when a right guide would leave
122
+ under 320px of panel (after an estimated 80px of other chrome).
123
+ - `<Legend order="stable-domain"|"present-first-seen"|"sorted"/>` is the
124
+ plot-wide legend ENTRY-SORT enum (default `stable-domain`; ordering never
125
+ changes color assignments). Distinct from `<GuideLegend order={2}/>`, a
126
+ per-aesthetic integer placement rank — same word, unrelated concepts.
127
+ - `Labs` takes flat string props: `title`, `subtitle`, `caption`, `x`, `y`,
128
+ `color`, `fill`, `size`, `linewidth`, `alpha`, `shape`, `linetype`. The
129
+ spec type is re-exported from `@ggts-sh/svelte` as `LabsSpec`.
130
+
131
+ ```svelte fragment
132
+ <GGPlot
133
+ data={rows}
134
+ aes={{ x: "cat", y: "val", fill: "group" }}
135
+ layers={[{ geom: "col" }]}
136
+ >
137
+ <CoordFlip />
138
+ <FacetWrap field="region" ncol={2} />
139
+ <ThemeMinimal />
140
+ <Labs title="Sales by category" x="Category" y="Sales" />
141
+ <GuideLegend channel="fill" position="bottom" />
142
+ <Legend order="sorted" />
143
+ </GGPlot>
144
+ ```
145
+
146
+ Equivalent complete JSON spec:
147
+
148
+ ```json complete
149
+ {
150
+ "data": {
151
+ "values": [
152
+ { "cat": "a", "val": 3, "group": "g1", "region": "west" },
153
+ { "cat": "b", "val": 5, "group": "g2", "region": "west" },
154
+ { "cat": "a", "val": 2, "group": "g1", "region": "east" },
155
+ { "cat": "b", "val": 4, "group": "g2", "region": "east" }
156
+ ]
157
+ },
158
+ "layers": [
159
+ {
160
+ "geom": "col",
161
+ "aes": {
162
+ "x": { "field": "cat" },
163
+ "y": { "field": "val" },
164
+ "fill": { "field": "group" }
165
+ }
166
+ }
167
+ ],
168
+ "coord": { "type": "flip" },
169
+ "facet": { "wrap": { "field": "region" }, "ncol": 2 },
170
+ "theme": "minimal",
171
+ "labs": { "title": "Sales by category", "x": "Category", "y": "Sales" },
172
+ "guides": { "fill": { "type": "legend", "position": "bottom" } },
173
+ "legend": { "order": "sorted" }
174
+ }
175
+ ```
176
+
177
+ ## Replace-vs-merge semantics (child layers)
178
+
179
+ - The `spec` prop wins over everything — when set, all other props and all
180
+ children are ignored.
181
+ - Mark layers: the `layers` prop, when set, is used INSTEAD of geom children.
182
+ Otherwise geom children register in document order, and registration order
183
+ is z-order (first = bottom).
184
+ - Grammar (non-mark) families fold onto the builder in registration order —
185
+ later siblings beat earlier ones.
186
+ - REPLACE families — `coord`, `facet`, `theme`: the last registration fully
187
+ replaces earlier ones (and any matching prop). Two or more children of one
188
+ kind emit a `DUPLICATE_PLOT_LAYER` advisory.
189
+ - Scales merge per-channel; two scale children configuring the same channel
190
+ emit `DUPLICATE_SCALE_CHANNEL` (note British/American spellings write the
191
+ same channel: `<ScaleColorDiscrete/>` and `<ScaleColourContinuous/>` both
192
+ configure `color`).
193
+ - `labs`, `guides`, `legend` are keyed-MERGE families: siblings touching
194
+ different keys all survive; a colliding key emits `DUPLICATE_MERGE_KEY` and
195
+ the later child's value wins. The value at a key is replaced whole — a
196
+ guide child never field-merges into another guide object for that channel.
197
+ - All three advisory codes arrive through `ondiagnostic` with severity
198
+ `"advisory"`; the chart still renders.
199
+ - Grammar children emit no markup and are inert without a `<GGPlot>` ancestor.