Fix the audit's 21 P1s across kernel, API, client, exports and pyplot
Fixes the 21 open P1 findings from the 0.0.7 framework audit (the 22nd, duplicate tick labels past 1e6, shipped in #507). Every finding was reproduced on origin/main before the change and has a regression test after it. UI-visible fixes carry before/after screenshots below, captured from the pristine v0.0.7 build and this branch with the same chart and gesture.
Kernel (channel.py, interaction.py, lod.py)
- Non-string message
typereturnsNoneinstead of raisingTypeErrorin the dispatcher. pick/clickbound-check against the rows the columns actually hold (histogram: bins, not samples; hexbin likewise);IndexErroris also caught as defense in depth.density_viewwith a subnormal span is dropped bynormalize_window(belowsys.float_info.min) andaligned_windowno longer overflows on tiny normal spans (math.ldexp, non-finite guards).view_changerecords only finite, ordered, non-empty ranges, soview_state()always round-trips intostate_patch_message(was P2).- 70 new fuzz-style tests: 42 malformed messages across every kind, each asserting no exception, no callback, and no state mutation.
API (components.py, _figure.py, _payload.py, channels.py, columns.py, facets.py)
- Log axis emits a
RuntimeWarningnaming the axis, the count dropped, and thenonpositive=remedy instead of silently dropping non-positive points (§28). - Non-finite continuous
color=/size=values exclude the row from geometry,shipped_sel, and static exports (matplotlib's "bad" colour) instead of painting as the colormap floor. sankey_chart(xy.sankey(...))accepts the mark like every other*_chart.- A hand-built
xy.Mark(kind=...)is completed with the factory's defaults (one source of truth, cached factory call) instead ofKeyError; a 22-kind sweep test asserts build-or-ValueError. - Bare pyarrow
Array/ChunkedArray/DictionaryArraystrings become categorical axes like the pandasstring[pyarrow]path. facet_chart(by='missing')raisesValueError, notKeyError.
Client (30_ticks.ts, 50_chartview.ts) with matching exporters (_svg.py)
- A log axis with fewer than two decade ticks in view falls back to linear ticks over the span, with
fmtLinearlabels;_log_ticksdoes the same so SVG/PNG/PDF agree (TS↔Python parity test over 12 ranges). - Legend-hidden category points are out of the hover pipeline: the CPU-nearest fallback skips
_visInv < 0rows and scans the full pre-filter row range, so the nearest visible row wins or nothing does. The keyboard a11y walk is unchanged.
Exports (_pdf.py, _svg.py)
- PDF honors
font-style(Oblique/Italic faces),font-family(serif→Times, monospace→Courier, everything else→Helvetica; AFM widths for anchoring),letter-spacing(Tc), andopacity(ExtGState) on text, as the capability matrix claimed. escapedrops XML-illegal code points (C0 except\t\n\r, surrogates, U+FFFE/FFFF) so SVG parses and PDF exports for any label text.
pyplot (python/xy/pyplot/)
ticklabel_formatconfigures the axisScalarFormatter(plain/sci/mathtext,scilimits,useOffsetas a documented no-op) instead of crashing export.- Spans and rules autoscale like matplotlib's
dataLim(verified against matplotlib 3.11 in-test), reach the exported chart as the axis domain, andlabel=produces legend rows. series.plot(ax=)/df.plot(ax=): pandas'TimeSeries_*tickers are no-ops on both tiers; a locator withouttick_valuesfalls back instead of raising.set_xlim/set_ylim,set_xticks/set_yticks,fill_between,fill_betweenxaccept datetime, date, datetime64, Timestamp.legend(handles=, labels=), integerloccodes, tupleloc;Line2D/Patch/Rectanglelegend proxies.zorder,clip_on,rasterized,antialiased,snap,gid,url,picker, ... are documented compat-noops;visible=Falsehides.plt.subplot(n, m, i)creates only cell i and returns the existing axes on reuse; mixed grids work.scatter(facecolors='none'),bar(tick_label=, log=),hist(bottom=, align=, log=),text(alpha=),hlines/vlines(linestyle=),errorbar(capthick=, mfc=, mec=, mew=),colorbar(fraction=),plt.tick_params/margins/locator_params, callableplt.cm.<name>colormaps (+ tab10/Set1/Paired…),BarPatch.get_x/get_height/..., categorical bar +yerrlimits.
Spec
wire-protocol.md, interaction.md §10, renderer-architecture.md §6, chart-grammar.md, styling.md, chart-kind-contract.md, export.md §2/§9, capability-matrix.md (regenerated), compat.md/compat-matrix.md/shim-todo.md/compat-changelog.md.
Before / after
Left: pristine v0.0.7 build. Right: this branch. Same chart, same synthetic gesture, headless Chrome (client) or the exporter itself (PDF, pyplot PNG).
Log axis zoomed inside one decade (client; SVG/PNG/PDF share the generator)

Hover on a legend-hidden category point (the red ring marks the pointer; the badge prints the view's hover target)

PDF text properties (rasterized from the PDF; v0.0.7 raised)

pyplot spans and rules: autoscale and legend labels

pyplot plt.subplot(221) then plt.subplot(224)

Tests
Full tests/ 4510 passed locally on the pushed branch (ruff check/ruff format --check, ty check with no new diagnostics vs main, sync_matplotlib_compat.py --check, gen_capability_matrix.py --check, docs-site tests 119 passed). The first CI round caught three things, fixed in 7ce900ac: the regenerated docs/styling/capabilities.md had not been committed, the new API test imported pyarrow at module level (the 3.11 floor job has none), and the unconditional native finite scan over continuous channels cost test_first_payload_scatter_continuous_channels 17% — channels are now probed with NumPy's isfinite().all() first, which restores parity with main.
Summary by CodeRabbit
-
New Features
- Expanded Matplotlib-style plotting support for legends, subplots, colormaps, tick formatting, datetime values, bars, histograms, and styling options.
- Sankey charts accept explicit mark inputs; manually constructed marks receive appropriate defaults.
- PDF exports support additional font families, styles, letter spacing, and opacity.
- Added pyplot helpers and callable qualitative colormaps.
-
Bug Fixes
- Narrow log-axis ranges now display usable ticks and labels.
- Hidden legend categories no longer respond to hover.
- Non-finite values, malformed inputs, and XML-unsafe text are handled safely.
- PyArrow categorical data and missing facet columns produce clearer results and errors.
-
Documentation
- Updated compatibility, export, styling, and interaction guidance.
Review round
Two bots filed 36 threads; all are answered and resolved — 33 fixed, 3 answered with evidence as not valid. Three fixes were wider than reported:
- The
float()overflow was on eight message paths, not one (viewbounds andpx,density_viewbounds andw, bothview_changeshapes,select,select_polygon), fixed at the root in the window coercion helpers. - The non-finite colour/size exclusion had to cover the whole density path — binning, mean-colour plane, sample overlay, pyramid, and padded drill window — not just the sample overlay.
- The tiny-span test's six parameters were five no-ops: at base 50.0,
50.0 + 3e-308 == 50.0, so the guarded path was never reached. Re-anchored, with a spy asserting it now is.
Answered as not valid, with evidence rather than argument:
hist(align=)withrwidth: patch positions match matplotlib 3.11 to at most 8.9e-16 across all six combinations.- Stale
_fullNafter append: a trace cannot be both appendable and category-filtered —append_datarejects the categorical colour channels filtering requires. Hardened anyway and tripwired. - Hidden categorical bars still drawn: no bar-emitting mark builds a categorical colour channel, so the filter never runs for bars. Dead guard removed, limit documented in spec §10, tripwire test added.
Full suite 4567 passed after the round.