| agent-specs | ||
| docs/superpowers | ||
| packages | ||
| stories | ||
| .editorconfig | ||
| .gitignore | ||
| .nvmrc | ||
| AGENTS.md | ||
| eslint.config.mjs | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
| tsconfig.typedoc.json | ||
| typedoc.json | ||
| vitest.config.ts | ||
| vitest.workspace.ts | ||
Goodplots
Goodplots is a TypeScript grammar-of-graphics workspace for typed data frames, immutable plot specifications, diagnostics, interaction controllers, and browser SVG rendering.
The current implementation includes the data frame package, theme package, core grammar builder, D3/SVG renderer, a Storybook examples/docs/test package, and the first ggplot2 parity test package.
Install
pnpm install
The workspace targets Node 26. Use the pinned package manager from package.json.
node --version
pnpm --version
Quick Start
import { plot, render } from '@goodplots/core'
import { fromRows } from '@goodplots/data'
import { d3Renderer } from '@goodplots/renderer-d3'
const frame = fromRows([
{ month: 1, revenue: 24, segment: 'Consumer' },
{ month: 2, revenue: 29, segment: 'Consumer' },
{ month: 3, revenue: 36, segment: 'Consumer' },
])
const chart = plot(frame)
.aes({
x: frame.field('month'),
y: frame.field('revenue'),
color: frame.field('segment'),
})
.geomPoint({ size: 5 })
.geomLine({ strokeWidth: 3 })
.labs({ x: 'Month', y: 'Revenue', color: 'Segment' })
.build()
render(d3Renderer(), chart, '#chart', { width: 640, height: 360 })
Data Frames
Goodplots data frames are immutable and typed from row or column inputs.
import { fromRows } from '@goodplots/data'
const sales = fromRows([
{ segment: 'Consumer', revenue: 24, active: true },
{ segment: 'Enterprise', revenue: 38, active: true },
{ segment: 'Platform', revenue: 31, active: false },
])
const activeSales = sales
.filter(row => row.active)
.select(['segment', 'revenue'])
Use frame.field('name') for aesthetic mappings. Field references are bound to the frame that created them, so build diagnostics can catch mismatched mappings.
Grammar
The core package uses an immutable builder. Each call returns a new plot builder.
const base = plot(sales).aes({
x: sales.field('segment'),
y: sales.field('revenue'),
})
const bars = base
.geomBar({ opacity: 0.84 })
.labs({ x: 'Segment', y: 'Revenue' })
.build()
Implemented geoms:
geomPoint()geomLine()geomPath()geomBar()geomCol()geomSegment()geomCurve()geomHline()geomVline()geomAbline()geomRect()geomTile()geomRaster()geomText()geomLabel()geomLinerange()geomErrorbar()geomErrorbarh()for deprecated horizontal errorbar compatibilitygeomPointrange()geomCrossbar()geomRug()geomPolygon()geomRibbon()geomArea()geomHistogram()geomFreqpoly()geomDensity()geomBoxplot()geomViolin()geomSmooth()
Implemented geom behavior includes bar width/orientation controls, stack and dodge metadata, horizontal bars, identity columns, ggplot2 point shapes 0-25, . pixel glyphs, no-draw shape removal, line ordering by x, input-order paths, missing-value path splits, line segments and curves, rule lines, rect/tile/raster-compatible rectangles, SVG text and labels, interval marks, rug ticks, grouped polygons, ribbons, identity areas, histograms, frequency polygons, density paths, boxplots with outliers, violins, and smooth lines/ribbons. Remaining stat-heavy gaps include exact ggplot2 loess/gam/KDE parity, after_stat() computed aesthetic syntax, stat_bin2d, hex bins, density 2d, summary 2d, full summary stat APIs, contour, sf/map, quantile geoms, exact warning text, and advanced position/coord combinations.
Statistical Transforms
@goodplots/data owns the reusable statistical transforms used by the grammar:
binValues()densityValues()boxplotValues()violinValues()smoothValues()
These helpers filter missing and non-finite numeric values, return deterministic row objects, and keep the simple-statistics dependency scoped to the data package. Core calls these transforms to build temporary stat frames for histogram, frequency polygon, density, boxplot, violin, and smooth layers before normal scale training and renderer mark construction.
Implemented scale hooks:
scaleX()scaleY()scaleColor()scaleFill()scaleSize()scaleAlpha()scaleShape()scaleShapeIdentity()
Scale specs support explicit continuous limits, manual discrete palettes through values, continuous visual range, guide breaks, guide labels, and naValue metadata. Continuous color/fill, size, and alpha mappings resolve to renderer-ready visual values; discrete color/fill mappings can use keyed or ordered manual palettes. Mapped shape uses a six-shape discrete palette by default, scaleShape({ values }) supports keyed and ordered manual values, and scaleShapeIdentity() treats data values as ggplot2 point shapes.
Implemented coordinate and guide hooks:
coordCartesian({ xlim, ylim, clip })for numeric continuous x/y viewport limitsguides({ position, color, fill, size, alpha, shape })for global guide placement and per-aestheticlegend,colorbar, ornone
const zoomed = plot(sales)
.aes({
x: sales.field('segment'),
y: sales.field('revenue'),
fill: sales.field('segment'),
})
.coordCartesian({ ylim: [0, 65], clip: 'on' })
.guides({ position: 'bottom' })
.geomBar()
.build()
const scaled = plot(sales)
.aes({
x: sales.field('revenue'),
y: sales.field('activation'),
color: sales.field('revenue'),
fill: sales.field('segment'),
size: sales.field('accounts'),
alpha: sales.field('confidence'),
})
.scaleColor({ type: 'continuous', range: ['#e0f2fe', '#7c2d12'], breaks: [24, 40, 57] })
.scaleFill({ type: 'discrete', values: { Consumer: '#2563eb', Enterprise: '#0f766e', Platform: '#d97706' } })
.scaleSize({ type: 'continuous', range: [3, 9], breaks: [8, 16, 23] })
.scaleAlpha({ type: 'continuous', range: [0.4, 1], breaks: [0.4, 0.7, 1] })
.scaleShape({ type: 'discrete', values: { Consumer: 21, Enterprise: 22, Platform: 24 } })
.guides({ color: 'colorbar', fill: 'legend', size: 'legend', alpha: 'legend', shape: 'legend' })
.geomPoint()
.build()
Implemented facet hooks:
facetWrap(fieldOrFields, { nrow, ncol, dir, scales, labeller })facetGrid(rowFieldOrFields, colFieldOrFields, { scales, labeller })
Facets build panel-local layers and statistical transforms, support fixed/free position scale modes (fixed, free_x, free_y, free), keep visual guides global, and render SVG panel grids with strips, axes, panel backgrounds, marks, and per-panel clipping.
const faceted = plot(mpg)
.aes({
x: mpg.field('displ'),
y: mpg.field('hwy'),
color: mpg.field('drv'),
})
.facetWrap(mpg.field('class'), { ncol: 3, scales: 'free_y' })
.geomPoint()
.build()
const groupedBars = plot(sales)
.aes({
x: sales.field('segment'),
y: sales.field('revenue'),
fill: sales.field('segment'),
})
.geomBar({ width: 0.72, position: 'dodge' })
.geomPoint({ shape: 'diamond', size: 6 })
.build()
Diagnostics
Use validate() for non-throwing checks and build() for renderer-ready output.
const diagnostics = plot(sales)
.geomPoint()
.validate()
const chart = plot(sales)
.aes({ x: sales.field('segment'), y: sales.field('revenue') })
.geomBar()
.build({ warningsAsErrors: true })
formatDiagnostics() returns stable human-readable messages for logs and tests.
Themes
Themes live in @goodplots/theme and can be applied through the core builder.
import { themeMinimal } from '@goodplots/theme'
const chart = plot(sales)
.aes({ x: sales.field('segment'), y: sales.field('revenue') })
.geomBar()
.theme(themeMinimal())
.build()
Interactions
The core controller forwards renderer events without coupling application state to the renderer implementation.
import { createChartController, render } from '@goodplots/core'
const controller = createChartController()
.onHover((event) => {
console.warn('hover', event.datum)
})
render(d3Renderer(), chart, '#chart', {
width: 640,
height: 360,
controller,
})
Storybook Examples
Run the Storybook examples package:
pnpm --filter @goodplots/stories dev
Storybook is the examples, docs, and browser component test surface. It includes complete interactive stories for:
- scatter with segment filtering and hover readout
- renderer polish with continuous axis titles, major grid lines, compact ticks, and discrete color/fill legend swatches
- coordinate zoom with discrete x bars, clipping controls, and bottom/hidden guide layout controls
- multi-series line chart
- category bar chart with metric switching
- layered line/point overview with layer toggling
- geom behavior controls for stacked/dodged/horizontal bars and point shape rendering
- facet stories for wrap, grid, free scales, custom rows/columns, repeated layers, and faceted statistical geoms
- grouped foundation geom pages for paths/rules, segments/curves, rects/tiles/rasters, text/labels, intervals, and rugs/polygons/ribbons
- grouped statistical geom pages for histograms/frequency polygons, density, boxplots/violins, and smooths
- scale guide controls for manual palettes, continuous colorbars, size legends, alpha legends, shape legends, and hidden color guides
- ggplot2 reference documentation examples under the Storybook
Referencesidebar group
The stories/reference/ files port ggplot2 reference documentation examples into runnable Goodplots charts or explicit blocked-reference panels. Migration progress is tracked in agent-specs/0013-ggplot2-reference-examples/reference-examples.md.
Automated Chromium coverage for Storybook stories:
pnpm --filter @goodplots/stories test:browser
API Docs
Generate public API reference:
pnpm docs:api
TypeDoc writes generated files under docs/api/.
Tests
Run root verification in this order in a clean worktree:
pnpm lint
pnpm build
pnpm typecheck
pnpm test
pnpm test:browser
pnpm test:coverage
pnpm docs:api
pnpm generate:parity-manifest
pnpm build comes before root pnpm typecheck because workspace package exports resolve declarations from package dist/ folders.
ggplot2 Parity Status
The private @goodplots/ggplot2-parity-tests package tracks the local ggplot2 test suite from ../ggplot2/tests/testthat.
Current manifest status:
- total entries: 140
- ported: 4
- mapped: 20
- deferred: 2
- unreviewed: 114
Current ported source areas:
test-aes.Rtest-layer.Rtest-plot.Rtest-stat-count.R
Mapped source areas include test-scale-continuous.R, test-scale-discrete.R, test-scale-hue.R, test-scale-manual.R, test-guide-colourbar.R, test-geom-bar.R, test-geom-point.R, test-geom-path.R, test-stat-bin.R, test-geom-freqpoly.R, test-stat-density.R, test-geom-boxplot.R, test-stat-boxplot.R, test-stat-ydensity.R, test-geom-violin.R, test-geom-smooth.R, test-facet-.R, test-facet-grid-.R, test-facet-labeller.R, test-facet-wrap.R, and the supported test-coord-cartesian.R subset.
Deferred scale areas include binned scales and brewer palettes. Remaining geom/stat/facet gaps include exact ggplot2 loess/gam parity, after_stat() computed aesthetic syntax, stat_bin2d, hex bins, density 2d, summary 2d, full summary stat APIs, position: 'fill', advanced position/coord combinations, facet margins/free-space/switch placement/axis controls, character glyph point shapes, guide legend override aesthetics, and exact warning text.