Fix the audit's 21 P1s across kernel, API, client, exports and pyplot

AL
4d ago
+5701-287

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 type returns None instead of raising TypeError in the dispatcher.
  • pick/click bound-check against the rows the columns actually hold (histogram: bins, not samples; hexbin likewise); IndexError is also caught as defense in depth.
  • density_view with a subnormal span is dropped by normalize_window (below sys.float_info.min) and aligned_window no longer overflows on tiny normal spans (math.ldexp, non-finite guards).
  • view_change records only finite, ordered, non-empty ranges, so view_state() always round-trips into state_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 RuntimeWarning naming the axis, the count dropped, and the nonpositive= 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 of KeyError; a 22-kind sweep test asserts build-or-ValueError.
  • Bare pyarrow Array/ChunkedArray/DictionaryArray strings become categorical axes like the pandas string[pyarrow] path.
  • facet_chart(by='missing') raises ValueError, not KeyError.

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 fmtLinear labels; _log_ticks does 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 < 0 rows 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), and opacity (ExtGState) on text, as the capability matrix claimed.
  • escape drops 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_format configures the axis ScalarFormatter (plain/sci/mathtext, scilimits, useOffset as 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, and label= produces legend rows.
  • series.plot(ax=)/df.plot(ax=): pandas' TimeSeries_* tickers are no-ops on both tiers; a locator without tick_values falls back instead of raising.
  • set_xlim/set_ylim, set_xticks/set_yticks, fill_between, fill_betweenx accept datetime, date, datetime64, Timestamp.
  • legend(handles=, labels=), integer loc codes, tuple loc; Line2D/Patch/Rectangle legend proxies.
  • zorder, clip_on, rasterized, antialiased, snap, gid, url, picker, ... are documented compat-noops; visible=False hides.
  • 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, callable plt.cm.<name> colormaps (+ tab10/Set1/Paired…), BarPatch.get_x/get_height/..., categorical bar + yerr limits.

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)
log axis before/after

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

PDF text properties (rasterized from the PDF; v0.0.7 raised)
pdf text before/after

pyplot spans and rules: autoscale and legend labels
spans before/after

pyplot plt.subplot(221) then plt.subplot(224)
subplot before/after

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 (view bounds and px, density_view bounds and w, both view_change shapes, 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=) with rwidth: patch positions match matplotlib 3.11 to at most 8.9e-16 across all six combinations.
  • Stale _fullN after append: a trace cannot be both appendable and category-filtered — append_data rejects 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.