No description
Find a file
2026-06-11 18:07:05 +08:00
agent-specs feat: add position system docs coverage 2026-06-11 18:07:05 +08:00
docs/superpowers docs(parity): document clean verification order 2026-06-07 22:06:47 +08:00
packages feat: add position system docs coverage 2026-06-11 18:07:05 +08:00
stories feat: add position system docs coverage 2026-06-11 18:07:05 +08:00
.editorconfig chore: scaffold goodplots workspace 2026-06-07 19:24:09 +08:00
.gitignore feat(examples): add Storybook catalog 2026-06-08 23:34:07 +08:00
.nvmrc chore: scaffold goodplots workspace 2026-06-07 19:24:09 +08:00
AGENTS.md feat: add statistical geoms pack 2026-06-11 10:34:12 +08:00
eslint.config.mjs feat(examples): add Storybook catalog 2026-06-08 23:34:07 +08:00
package.json feat: add position system docs coverage 2026-06-11 18:07:05 +08:00
pnpm-lock.yaml feat: add statistical geoms pack 2026-06-11 10:34:12 +08:00
pnpm-workspace.yaml refactor(stories): replace browser examples 2026-06-09 14:23:25 +08:00
README.md feat: add facet layout 2026-06-11 11:50:41 +08:00
tsconfig.base.json chore: scaffold goodplots workspace 2026-06-07 19:24:09 +08:00
tsconfig.typedoc.json feat(renderer-d3): add browser svg runtime 2026-06-07 20:37:00 +08:00
typedoc.json feat(renderer-d3): add browser svg runtime 2026-06-07 20:37:00 +08:00
vitest.config.ts fix(storybook): load vitest config in UI 2026-06-09 15:05:46 +08:00
vitest.workspace.ts fix(storybook): load vitest config in UI 2026-06-09 15:05:46 +08:00

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 compatibility
  • geomPointrange()
  • 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 limits
  • guides({ position, color, fill, size, alpha, shape }) for global guide placement and per-aesthetic legend, colorbar, or none
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 Reference sidebar 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.R
  • test-layer.R
  • test-plot.R
  • test-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.