GG.Net lets Data Scientists and Developers create interactive and flexible charts for .NET and Blazor Web Apps.
Taking its inspiration from the highly popular ggpplot2 R package, GG.Net provides natively rich features for your Data Analysis Workflow. Build publication quality charts with just a few lines of code in C# and F#.
A plot is one fluent chain: PlotContext.Build(source, x, y) establishes the data source and default selectors, each Geom_* call adds a layer configured in place, Scale_* calls shape the axes and legends, and .Style() finishes the plot.
var plot = PlotContext.Build(points, o => o.X, o => o.Y)
.Geom_Line(strokeWidth: 2, color: "#23d0fc")
.Geom_HLine([1.0], y: o => o, label: o => "Baseline", lineType: LineType.Dashed)
.Scale_Y_Continuous(formatter: new DoubleFormatter("N2"))
.Style();xxxBymeans data-driven.colorBy,fillBy,sizeBy,lineTypeBytake an aesthetic mapping (built byScale_Color_Discrete,Scale_Fill_Continuous, …): the value is computed per item, trains a scale, and feeds the legend. The unsuffixed twin (color,fill,size,lineType) is a constant applied to the whole layer. When a mapping is present it wins for its own aesthetic; the constant then still serves as the base for other aesthetics' legend swatches (a line-type legend draws its swatches in the constant color), so setting both is meaningful rather than an error.- Positional arguments stop at the selectors. Source and selector parameters (
x,y,ymin,open, …) may be passed positionally; every aesthetic, event, or option after them is passed by name. The signatures are wide by design — configuration lives in one call — and named arguments are what keep call sites readable and stable. - The vocabulary is SVG's.
strokeWidth,opacity,fillOpacity,strokeOpacity,strokeColormean exactly what they mean in SVG.widthandheightare reserved for geometric extent in data units (Geom_Bar,Geom_Tile,Geom_Violin,Geom_RidgeLine). - Interactivity is a uniform block. Every data-mark geom takes
onclick,onmouseover,onmouseout, and (where a hover surface makes sense)tooltip. Whentooltipis set and no explicit hover handlers are given, the default hover shows it. Annotation geoms (Geom_ABLine,Geom_HLine,Geom_VLine,Geom_Text) and statistical summaries (Geom_Boxplot,Geom_Violin,Geom_RidgeLine) deliberately take no event block.
Stats are sources, not layers: each Stat.* call returns a typed source that any geom draws unchanged, recomputed on every render pass so streaming data stays current.
// a histogram is Stat.Bin + Geom_Bar — there is no Histogram geom
PlotContext.Build(Stat.Bin(readings, r => r.Value, bins: 20), b => b.Mid, b => b.Count)
.Geom_Bar(width: 1.0)
.Style();| Stat | Output | Draw with |
|---|---|---|
Stat.Bin |
Bin / Bin<TKey> (min, mid, max, count, density) |
Geom_Bar(x: b => b.Mid, y: b => b.Count) |
Stat.Density |
DensityPoint / DensityPoint<TKey> (at, density) |
Geom_Area, Geom_Line, Geom_Violin(width: d => d.Density) |
Stat.Count |
Count<TKey> (key, n) |
Geom_Bar over categories |
Stat.Summary |
Summary / Summary<TKey> (x, center, lower, upper) |
Geom_ErrorBar(y: s => s.Center, ymin: s => s.Lower, ymax: s => s.Upper) |
Per-facet statistics are grouped statistics. Compute with groupBy: and facet the output on the same key — the key is deliberately stated twice; a mismatch between them is almost certainly a bug:
PlotContext.Build(Stat.Bin(readings, r => r.Value, r => r.Tank, bins: 10), b => b.Mid, b => b.Count)
.Geom_Bar(width: 1.0)
.Facet_Wrap(b => b.Group)
.Style();Statistics run over the whole source (per group when grouped). Stats that would depend on panel-trained state — a function traced over each panel's free-scale range — are out of scope by design.
| Geom | Selectors | Mappings | Constants | Events | Tooltip |
|---|---|---|---|---|---|
Geom_Point |
x, y |
sizeBy, colorBy |
size, color, opacity |
✓ | ✓ |
Geom_Line |
x, y |
colorBy, lineTypeBy |
strokeWidth, color, opacity, lineType, piecewise |
✓ | ✓ |
Geom_Bar |
x, y |
fillBy |
fill, fillOpacity, strokeColor, strokeOpacity, strokeWidth, position, width |
✓ | ✓ |
Geom_Area |
x, y |
fillBy |
fill, fillOpacity, position |
✓ | ✓ |
Geom_Ribbon |
x, ymin, ymax |
fillBy |
fill, fillOpacity |
✓ | ✓ |
Geom_ErrorBar |
x, y, ymin, ymax |
colorBy |
strokeWidth, color, opacity, lineType, radius, position |
✓ | ✓ |
Geom_Segment |
x, xend, y, yend |
— | strokeWidth, color, opacity, lineType |
✓ | ✓ |
Geom_Tile |
x, y, width, height |
fillBy |
fill, fillOpacity, strokeColor, strokeOpacity, strokeWidth |
✓ | ✓ |
Geom_Hex |
x, y, dx, dy |
fillBy |
fill, opacity |
✓ | ✓ |
Geom_Radar |
x, y |
fillBy |
fill, fillOpacity, strokeWidth |
✓ | ✓ |
Geom_Map |
polygons |
fillBy |
fill, fillOpacity, strokeColor, strokeWidth |
✓ | ✓ |
Geom_Candlestick |
x, open, high, low, close |
— | strokeWidth, color, opacity, lineType |
✓ | — |
Geom_OHLC |
x, open, high, low, close |
— | strokeWidth, color, opacity, lineType |
✓ | — |
Geom_Volume |
x, volume |
— | fill, opacity |
✓ | — |
Geom_Boxplot |
x, y |
fillBy |
size, fill, fillOpacity, strokeWidth |
— | — |
Geom_Violin |
x, y, width |
fillBy |
fill, fillOpacity, strokeColor, position |
— | — |
Geom_RidgeLine |
x, y, height |
fillBy |
fill, fillOpacity |
— | — |
Geom_Text |
x, y, angleBy, text |
colorBy |
size, anchor, weight, style, color, angle |
— | — |
Geom_ABLine |
a, b, label |
— | strokeWidth, color, opacity, lineType, size, anchor, weight, style |
— | — |
Geom_HLine |
y, label |
— | strokeWidth, color, opacity, lineType, size, anchor, weight, style |
— | — |
Geom_VLine |
x, label |
— | strokeWidth, color, opacity, lineType, size, anchor, weight, style |
— | — |
Styling is split by one rule: if it moves layout it's C# (Style — font sizes, margins, positions, because the server measures them); if it's paint it's CSS. Paint targets stable semantic classes (panel, x-break, legend-title, …) scoped under .ggnet[theme=name], selected by the Theme parameter on the Plot component.
A theme is a block of variable overrides, not a stylesheet fork:
.ggnet[theme=mytheme] {
--ggnet-bg: #1e1e1e;
--ggnet-grid: #333;
--ggnet-break-label: #9ca3af;
}The base rules in Themes/Default.css read every paint through a --ggnet-* variable (backgrounds, grid, labels, titles, strips, legend, spinner — the file documents the full set), so a theme overrides only what it changes, anything it omits degrades to the default instead of rendering unstyled, and classes added by future GGNet versions are painted automatically. A test (ThemeContractTests) enforces the contract: every emitted class painted, every referenced variable defined, theme files only setting known variables.
Notes:
- Geom parameters accept css custom properties —
color: "var(--color-temperature)"wires a layer to your design tokens. - Changing
--ggnet-fontaffects rendering only: server-side text measurement assumes Inter until font metrics ship with the theme. - Self-contained export:
plot.AsStringAsync(selfContained: true)/SaveAsync(..., selfContained: true)embeds the bundled theme as a<style>element so the SVG renders standalone; off by default — app-hosted output is styled by the app's stylesheet.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |


















