Instruction file imported from pablofrommars/GGNet (
.github/instructions/dsl.instructions.md). Copyright stays with the author.
DSL / Grammar-of-Graphics Guide
Scope: GGNet's public fluent surface — the grammar of graphics — implemented in src/GGNet. Assumes csharp.instructions.md applies. This is the library's crown jewel: the conventions here are also read by the MCP server (mcp.instructions.md) and the packaged skill (skill.instructions.md), so drift is expensive.
1. The Grammar — One Fluent Chain
A plot is a single chain, terminated by .Style():
PlotContext.Build(source, x, y) // data + default selectors
.Geom_*(...) // layers, configured in place (repeatable)
.Scale_*(...) // axes, legends, transforms
.Facet_*(...) / .Coord_Polar() / .Flip()
.Title("...") .XLab("...") // labels (Markdown)
.Style(); // terminal call → IPlotContext
How it is wired (know this before editing the surface):
PlotContextis a non-genericpublic partial classholding the staticBuild<…>entry points; the genericpublic partial class PlotContext<T, TX, TY> : IPlotContextholds instance state.Buildpicks default scales by dispatching on the axis typesTX/TY(including NodaTimeLocalDate/LocalDateTime/Instantoverloads).Geom_*,Scale_*,Facet_*,Styleare static extension methods inpublic static partial class BuilderExtensions, split across 22 partial files —BuilderExtensions.<Geom>.cs(one per geom) plus the baseBuilderExtensions.cs(holdsScale_*/Style).Geom_*extendsPanelFactory<…>, constructs the internal geom, and registers it via the internalpanel.AddTyped(() => new <Geom>(...)).Scale_*/StyleextendPlotContext<T, TX, TY>and return it for chaining.
2. The Four DSL Conventions (non-negotiable — they define the surface)
xxxBymeans data-driven; the unsuffixed twin is a per-layer constant.colorBy/fillBy/sizeBy/lineTypeBytake an aesthetic mapping (built byScale_Color_Discrete,Scale_Fill_Continuous, …): computed per item, trains a scale, feeds the legend.color/fill/size/lineTypeare constants painting the whole layer. Setting both is meaningful (the mapping wins for its own aesthetic; the constant still colors other aesthetics' legend swatches) — not an error.- Positional arguments stop at the selectors. Source and selector params (
x,y,ymin,open, …) may be positional; every aesthetic, event, or option after them is passed by name. The signatures are intentionally wide — all configuration in one call — and named arguments keep call sites readable and stable. - The vocabulary is SVG's.
strokeWidth,opacity,fillOpacity,strokeOpacity,strokeColormean exactly what they mean in SVG.width/heightare reserved for geometric extent in data units (Geom_Bar,Geom_Tile,Geom_Violin). - Interactivity is a uniform block. Every data-mark geom takes
onclick,onmouseover,onmouseout, and (where hover makes sense)tooltip. Annotation geoms (Geom_ABLine/HLine/VLine/Text) and statistical summaries (Geom_Boxplot/Violin/RidgeLine) deliberately take no event block.
3. Stats Are Sources, Not Layers
Stat.* calls return a typed source (public readonly record struct — Bin, DensityPoint, Count<TKey>, Summary) that any geom draws unchanged, recomputed every render pass so streaming data stays current. There is no Histogram geom — a histogram is Stat.Bin + Geom_Bar.
- Grouped variants add a
groupBy:parameter and prependGroupto the output; per-facet statistics are grouped statistics — compute withgroupBy:and facet the output on the same key (the key is stated twice by design; a mismatch is almost certainly a bug).
4. The Overload-Partial Discipline (this is where regressions hide)
The wide per-geom signatures are hand-copied across overloads — source generation of these families was tried and retired by decision in favor of verification (ROADMAP.md). The guarantee is a test battery, so consistency is on you when you touch a family:
OverloadConsistencyTests(tests/GGNet.Headless.Tests/OverloadConsistencyTests.cs) reflects overBuilderExtensions/PlotContext/Statand asserts four invariants per name-family:- Defaults agree — the same parameter name has an identical default value across every overload.
- Parameter shapes agree — parameter types agree within a
(name, receiver)sub-family (generics erased;source/palette/polygonsexcluded as legitimate dispatch variance). - Sugar overloads preserve canonical order — a shorter overload keeps the parameter order of its longest sibling.
- Docs agree — each
<param>'s XML-doc text is identical across the family (loaded fromGGNet.xml).
BuilderForwardingTestsguards that overloads forward to the canonical implementation correctly (a positional-forwarding bug shipped for years before the gates caught it —ROADMAP.md).
When you add or edit a geom overload: keep parameter names, defaults, order, and <param> docs consistent across the entire family, or these tests fail. Every public method on this surface must carry full /// docs (§C# guide).
5. Adding a Geom (the checklist)
- Add
src/GGNet/Geoms/<Geom>/<Geom>.cs—internal sealed class <Geom><T, TX, TY> : Geom<T, TX, TY>(guards throwGGNetUserExceptionfor missing required selectors). - Add
src/GGNet/BuilderExtensions.<Geom>.cs— theGeom_<Geom>extension family onPanelFactory, registering viaAddTyped, with full///docs and family-consistent signatures (§4). - Update the skill: a
skills/ggnet/examples/<chart>.mdand the relevantskills/ggnet/reference/*.md(signatures extracted from source — skill.instructions.md). - Add a pinned gallery golden (
tests/GGNet.Headless.Tests/Gallery/GalleryTests.<Name>.verified.svg) — rendering.instructions.md. - The MCP
list_geomstool reflects the new method automatically — no manual list to update (mcp.instructions.md).
6. Guard at the Surface
DSL misuse is caught early with GGNetUserException and a clear message — null required selectors (BuilderExtensions.<Geom>.cs), incompatible combinations (Flip() + polar in PlotContext), uninferrable types ("Type could not be inferred"), unsupported coordinate systems. Put the guard at the top of the Build/Geom_*/Style entry point; never let bad DSL input reach the render pipeline as a NullReferenceException.