Exactness
go-plot’s model and defaults are matplotlib’s so that a figure can be checked against matplotlib’s rendering of the same figure, not only look like it.
All fixtures are generated by scripts in
tools/ from matplotlib
itself (3.11.2), committed, //go:embed-ded in the tests, and compared
exactly unless stated.
Ticks
| what | how it is checked |
|---|---|
| Tick locations, labels, offset text on linear and log axes | ticker/testdata/ticks.json: 408 axes of real figures (4 sizes, 37 linear and 14 log ranges, x and y): every location bit for bit, every label and offset character for character, minor log ticks and their labels included. |
| Locator and formatter parameters a default figure never uses | ticker/testdata/units.json: 45 MaxNLocator (nbins, steps, integer, symmetric, prune, min_n_ticks), 42 LogLocator (base 2, e, 10; subs list/auto/all; numticks), 72 ScalarFormatter (math text, no offset, fixed power limits, no scientific, ASCII minus, one or no location), 25 LogFormatterSciNotation, nonsingular, and %g. Exact, except base-e log locations, compared to 1e-15: NumPy’s e**n and Go’s math.Pow(e, n) differ in the last bit. |
Colours
| what | how it is checked |
|---|---|
| Colormaps | viridis, plasma, inferno, magma, cividis, gray, coolwarm, RdBu (and _r): the 256-entry tables are generated from matplotlib, and an independent fixture of 1008 samples per map (including the ends and out-of-range values) is compared as floats and as bytes; the test also checks that all 256 entries were sampled. |
| Named colours and the cycle | all 148 CSS4, 10 tab:, 8 single-letter colours, C0–C9, grey levels, short and long hex: exact. |
Layout
| what | how it is checked |
|---|---|
| Layout | testdata/layout.json: 15 kinds of figure at two sizes — line, bar, barh, hist, imshow, imshow + colorbar, scatter, log-log, fill + errorbar, contourf, 2x2 grid, twinx, offsets, axhline/axvline, inverted and singular limits: every Axes position (after aspect and colorbar) and view limits (after margins and sticky edges) to 1e-9 relative, and every tick label and offset text exactly. |
TightLayout | matplotlib’s _auto_adjust_subplotpars, ported; testdata/layout.json holds matplotlib’s positions after fig.tight_layout() for 6 kinds of figure at two sizes (single Axes, labelled, 2x2 labelled, twinx with two labels, offsets, log-log). Every Axes edge is within 0.005 of the figure (worst measured 0.0049, 2.4 pixels): the algorithm is the same, the measured width of text is not — matplotlib’s is FreeType’s hinted box, every glyph a whole number of pixels; go-plot’s is the string’s advance. |
| Defaults | figure 6.4 x 4.8 in at 100 dpi, subplot parameters, font sizes, tick sizes and pads, line widths, marker sizes, dash patterns (scaled by line width), patch and grid colours, legend geometry, colorbar fraction/pad/aspect: transcribed from matplotlibrc and checked by the rendering comparison below. |
Algorithms
| what | how it is checked |
|---|---|
| Histogram binning | NumPy’s algorithm (equal bins by arithmetic, corrected against the edges; explicit edges by searchsorted). |
| Contour levels | ContourSet._autolev (MaxNLocator(n+1, min_n_ticks=1), trimmed). |
| Dense lines | matplotlib’s PathSimplifier (threshold 1/9 pixel), ported from path_converters.h. |
Rendering comparison
tools/compare draws twelve reference figures with go-plot and with
matplotlib — same data, size and dpi — and measures each pair with one
detector applied to both images: the axes frames (columns and rows of long
dark runs), the tick marks (dark for the three pixels just outside each bottom
and left spine, within the frame), and the pixels of the first cycle colour
inside the frames. Tolerances: frame and ticks within 1 pixel, C0 pixel count
within 10 %, overlap of C0 pixels (each side dilated by 1 pixel) at least 0.90.
| figure | frames | ticks (go / mpl) | tick error | C0 pixels (go / mpl) | overlap | mean abs diff (0–255) |
|---|---|---|---|---|---|---|
| line | equal | 15 / 15 | 0 px | 1695 / 1736 | 1.000 | 1.01 |
| scatter + colorbar | equal | 11 / 11 | 0 px | – | – | 0.53 |
| bars (1x2) | equal | 26 / 26 | 0 px | 76024 / 76034 | 1.000 | 0.67 |
| hist | equal | 17 / 17 | 0 px | 49515 / 49515 | 1.000 | 0.63 |
| imshow + colorbar | equal | 16 / 16 | 0 px | 38429 / 38683 | 1.000 | 0.77 |
| contourf + contour + colorbar | equal | 16 / 16 | 0 px | – | – | 0.75 |
| log-log with grid | equal | 11 / 11 | 0 px | 770 / 775 | 1.000 | 0.64 |
| errorbar, fill, axhline/axvline, text, annotate | equal | 14 / 14 | 0 px | 1688 / 1558 | 0.994 | 1.03 |
| twinx | equal | 11 / 11 | 0 px | 881 / 891 | 1.000 | 0.94 |
| 2x2 grid, offsets | equal | 41 / 41 | 0 px | 4991 / 4944 | 0.997 | 1.79 |
| int32 ndarray with markers | equal | 14 / 14 | 0 px | 2380 / 2465 | 1.000 | 0.32 |
| 1000² imshow (perf figure) | equal | 10 / 10 | 0 px | 17426 / 17629 | 1.000 | 1.59 |
2x2 labelled, TightLayout | ±2 px | 14 / 14 | 2 px | 713 / 700 | 0.970 | 6.71 |
| 1e6-point line (perf figure) | equal | 13 / 13 | 0 px | 12032 / 15405 | 1.000 | 2.07 |
In twelve of the fourteen, every frame lands on the same pixel rows and columns and every tick on the same pixel. The images were also looked at, side by side.
Two fail the tolerances, and are reported rather than loosened:
TightLayout: frames and ticks within 2 pixels, not 1, for the reason given above — the width of tick labels, which tight layout makes room for, is measured differently. Side by side the figures are not distinguishable by eye.- The dense line: go-plot paints 22 % fewer saturated C0 pixels. Every go-plot pixel is within one pixel of a matplotlib one (overlap 1.000); the difference is how dark the edges of a stroke that crosses itself thousands of times come out — Agg’s scanline accumulates such a stroke differently from go-gfx’s coverage sweep. It is not the simplification: drawn without it, go-plot’s count changes by 0.3 %, while matplotlib’s own simplified and unsimplified renderings differ by 9 %.
Negative controls
A comparison that cannot fail proves nothing. Two deliberately perturbed
go-plot figures are compared with matplotlib’s line, and both must fail:
line_shifted, the x limits widened by 0.1 data units at the left: tick error 4 px, overlap 0.436;line_1px, the view slid by 0.03 data units, 1.35 pixels: tick error 2 px, overlap 0.779.
Both are caught; the smaller one by the tick detector and by the overlap, so a one-pixel slip of the data does not pass.
Reproduce
git clone https://github.com/go-plot/plot && cd plot
go test ./... # includes the matplotlib fixtures
uv run --with matplotlib --with numpy python tools/gen_ticks.py # regenerate a fixture
cd tools/compare && go run . -out DIR && uv run --with matplotlib --with numpy python figures.py DIR \
&& uv run --with matplotlib --with numpy python compare.py DIR line=line ...Write rendered images to a directory outside the repository.
What does not equal matplotlib is on Differences.
Created on by tannevaled
Modified on by tannevaled