Skip to content

go-plot v0: matplotlib-model plotting in pure Go, tested against matplotlib - #1

Merged
tannevaled merged 12 commits into
mainfrom
feat/v0
Oct 10, 2026
Merged

tannevaled merged 12 commits into
mainfrom
feat/v0

Conversation

@tannevaled

@tannevaled tannevaled commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

First version of go-plot: a pure-Go (CGO=0) plotting library with matplotlib's object model and defaults, SVG and PNG output, and TeX labels typeset by go-tex/math.

What is in it

  • plot: Figure, Subplots, AddAxes, TightLayout; Plot, Scatter, Bar/Barh, Hist, FillBetween, ErrorBar, Imshow (+ Colorbar), Contour/Contourf, Axhline/Axvline, Text/Annotate, Legend, titles, labels, grid, limits, linear/log scales, TwinX; SaveSVG, SavePNG, Display() (image/svg+xml, image/png). Data: numeric slices or *ndarray.Array of any dtype.
  • ticker: AutoLocator/MaxNLocator, ScalarFormatter (offset, scientific), LogLocator, LogFormatterSciNotation.
  • colors: named colours, tab10 cycle, viridis/plasma/inferno/magma/cividis/gray/coolwarm/RdBu with matplotlib's LUTs (generated by tools/gen_colormaps.py).
  • contour (marching squares), render (raster via go-widgets/painter + go-gfx coverage; vector SVG), text (DejaVu Sans via go-opentype; TeX via go-tex/math).

Verification

  • Ticks: 408 real matplotlib axes + 202 direct locator/formatter calls, exact (base-e log ticks to 1e-15).
  • Colormaps: 8 maps x 1008 samples, floats and bytes, exact; 173 named colours exact.
  • Layout: 30 figures — Axes positions and limits to 1e-9, every tick label and offset exact. TightLayout: 12 figures within 0.005 of the figure (worst 0.0049 = 2.4 px; text widths are measured from advances, matplotlib's from FreeType's hinted box).
  • Rendering vs matplotlib (12 reference figures + 2 perf figures, tools/compare): in 12 of 14 every axes frame and tick mark is on the same pixel, curve overlap 0.994–1.000. Two fail the 1-px tolerance and are reported, not loosened: the TightLayout figure (2 px, text-width measurement) and the 1e6-point line (22% fewer saturated pixels: anti-aliasing of a self-overlapping stroke). Negative controls line_shifted (4 px) and line_1px (2 px tick error) both fail as required.
  • 100.0% statement coverage under -race; test binaries also run locally under qemu on s390x, ppc64le, riscv64 and on 386 (this caught one platform-dependent label, fixed).

Performance (medians, interleaved, loaded host — docs/perf.md)

go-plot matplotlib
1e6-point line → PNG 0.055 s 0.065 s
1e6-point line → SVG 0.019 s 0.053 s
1000² imshow → PNG 0.025 s 0.042 s
1000² imshow → SVG 0.020 s 0.042 s

The API follows a census of 339 notebooks (subplots, plot, set_xlabel/ylabel/title, legend, figure, tight_layout, set_ylim, axhline all present; pyplot.show is Display); differences from matplotlib and what v0 leaves out are listed in docs/design.md.

CI: gofmt, vet, build, the 100% coverage gate on Linux/macOS/Windows, native amd64/arm64, qemu riscv64/loong64/ppc64le/s390x, cross-builds for 386, arm, js/wasm, wasip1/wasm, govulncheck.

main was created by GitHub (initial LICENSE + README); this PR is the first code. Not to be merged by its author.

🤖 Generated with Claude Code

tannevaled and others added 12 commits October 9, 2026 21:16
…FormatterSciNotation

Ported from matplotlib 3.11.2 with Python/NumPy float semantics (floor
division, correctly rounded powers of ten, no fused multiply-add), and
checked against 408 cases generated from real matplotlib axes: locations bit
for bit, labels and offset text character for character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b's exact tables

The lookup tables and named colours are generated from matplotlib 3.11.2 by
tools/gen_colormaps.py; the test checks 1008 samples of every map (floats and
bytes) against an independent fixture from the same matplotlib.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Figure/Subplots/AddAxes with matplotlib's GridSpec placement; Axes with
Plot, Scatter, Bar/Barh, Hist, FillBetween, ErrorBar, Imshow, Contour,
Contourf, Axhline/Axvline, Text/Annotate, Legend (with 'best' placement),
titles and labels, grid, limits, linear and log scales, TwinX; Colorbar.

render/ draws through go-widgets/painter (masks and images) with go-gfx
vector coverage for strokes and fills, or to SVG; text/ sets DejaVu Sans
via go-opentype and TeX math via go-tex/math.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al comparison

main.go and figures.py draw the same eleven figures; compare.py finds the
axes frames, the tick marks and the C0 pixels in both renderings with one
detector and checks them against stated tolerances. Two perturbed figures
are negative controls the comparison must fail.

Also: superscripts in tick labels use the x-height of the x glyph, and
one-colour contour lines dash negative levels, as matplotlib does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arness

A 1e6-point line to PNG goes from 121 ms to 52 ms: data limits are taken
once per artist, long strokes are rasterised in 512-segment pieces (the
coverage sweep sorts crossings by insertion, quadratic in a dense line), and
STIX Two Math is parsed only when a label needs TeX.

tools/compare/perf.py times go-plot against matplotlib interleaved, fresh
processes, medians; tools/gen_layout.py records matplotlib's layout of small
figures for the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tplotlib's

30 figures (15 kinds x 2 sizes) from tools/gen_layout.py; LICENSE copyright
line now names the go-plot/plot authors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erything tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… contour

tools/gen_ticker_units.py records matplotlib's MaxNLocator, LogLocator,
ScalarFormatter, LogFormatterSciNotation, nonsingular and %g called with
non-default parameters; the port matches every one (base-e log ticks to
1e-15, the rest bit for bit). LogLocator gains subs="all". Zero-area band
polygons are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
logNonsingular widened a single value to the decades two apart; matplotlib
takes the decade strictly below and above (3 -> [1, 10], 10 -> [1, 100]).
Dead branches the tests showed unreachable are removed with a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the test binaries on s390x (qemu, locally) caught a platform
difference: LogFormatterSciNotation's coefficient b**(fx - exponent) sits
within an ulp of 1.5 for x = 6, b = 2 and rounds either way depending on
pow; x / b**exponent is exact and gives matplotlib's answer everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rom code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TightLayout ports _auto_adjust_subplotpars and is checked against
matplotlib's positions after fig.tight_layout() for 12 figures (every edge
within 0.005 of the figure, worst 0.0049). Text boxes now start at the pen
and are as wide as the string's advance, as FreeType's box is in matplotlib
within a pixel or two; this halves the mean pixel difference of every
reference figure (line: 2.10 -> 1.01).

docs/design.md records the notebook census (339 notebooks) the API's shape
follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit ea69a8a into main Oct 10, 2026
11 checks passed
@tannevaled
tannevaled deleted the feat/v0 branch October 10, 2026 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant