Skip to content

chore(release): v0.7.0 — land the plot-key overlays that missed main - #51

Merged
CSSFrancis merged 8 commits into
mainfrom
release/v0.7.0
Jul 31, 2026
Merged

chore(release): v0.7.0 — land the plot-key overlays that missed main#51
CSSFrancis merged 8 commits into
mainfrom
release/v0.7.0

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Releases the plot-keys feature that missed the 0.6.0 boat, and explains how it
went missing.

What happened

#49 was stacked on
#48 and merged into
feat/3d-colormap-surface — but that branch had already merged to main:

#48main 2026-07-31 06:50
#49feat/3d-colormap-surface 2026-07-31 09:12 (2 h 22 m later)

So #49's six commits landed on a branch nobody merges again. GitHub reports
#49 as MERGED, which is true and misleading — it merged into its base, and
its base was spent. The visible symptom: v0.6.0 shipped set_texture
without add_key
, and anyplotlib/keys.py is absent from main.

Nothing is wrong with the code — it just never got to main.

What this PR does

  1. Merges the six stranded commits onto main, unchanged. No conflicts,
    no rebase, no cherry-pick — so the history and authorship of feat(keys): floating image keys pinned over a panel (IPF, colour wheels) #49 are
    preserved exactly as reviewed.
  2. Prepares 0.7.0, mirroring what Prepare Release does for
    bump=minor, stable: version in pyproject.toml and docs/conf.py,
    towncrier build (one fragment — the keys feature), the switcher.json
    entry, and the root redirect.

0.7.0 rather than 0.6.1: add_key is a new feature, and 0.6.0 is already
published with a changelog that does not mention it.

Verification

  • 2000 passed, 58 skipped on the merge result (full suite).
  • anyplotlib/tests/test_keys — 40 passed, run 4x.
  • Confirmed at 0.7.0: add_key and set_texture both present.

Two notes for the reviewer:

  • test_keys.py::TestRender::test_a_bare_key_draws_no_card failed once on
    the very first run in a cold worktree, then passed 4/4. It looks like a
    first-browser-launch timing flake rather than a real fault — flagging it
    because CI cold-starts every time, so it may resurface there.
  • uv.lock still records version = "0.5.0" while main's pyproject.toml
    says 0.6.0. That drift predates this PR (the release workflow never
    touches the lockfile), so it is deliberately not fixed here — worth a
    separate one-liner.

Why now

SpyDE wants add_key(..., hover_only=True) for the IPF colour key over an
orientation map — the exact case #49 was written for. It currently draws that
key as an entire separate figure pinned by the renderer, which add_key
collapses to one call. SpyDE 0.3.0 would rather depend on a PyPI release than
carry a git pin, so this unblocks it.

CSSFrancis and others added 8 commits July 30, 2026 23:42
A key is the scale bar's sibling: a small picture that floats in screen
space over the plot area and does not pan or zoom with the data. It is for
a colour legend a colorbar cannot express — an inverse pole figure triangle
over an orientation map, a hue wheel over a polarization field, a phase key
over a segmentation.

Deliberately NOT an inset axes. Figure.add_inset gives a draggable window
with a title bar, a theme border and a full canvas stack; Python exposes no
way to turn any of that off. That is right when the overlay is a live plot
you interact with, and much too heavy when it is a static picture that
should read as part of the figure. A key has no chrome unless asked for, no
event wiring, and (once the renderer lands) one canvas shared by every key
on the panel.

Lives on _BasePlot, so every panel type has it — the same picture makes
sense over a 2-D map and a 3-D scatter.

The picture and the styling travel on separate channels. `keys` carries
placement and style on the light view trait; `key_images` carries the data:
URLs and is declared in _GEOM_KEYS wherever the panel class has one, so
restyling a key — or a hover toggle, once that is wired — never
re-transmits the image. Plot1D has no geom trait by design, and needs no
special case: `key_images` simply rides its single trait as any other state
field does.

This is the Python layer only: state, validation and lifecycle. The
renderer does not draw keys yet, so add_key currently has no visible
effect; the JS follows in the next commit.
The renderer half of add_key. Every key on a panel shares ONE canvas
(p.keyCanvas, z 7, built generically in _buildCanvasStack for all three
panel kinds — each branch already ends with a positioned wrap).

Because that canvas is separate from plotCanvas, an ordinary data redraw
does not erase it. Keys are therefore redrawn only when the key state, the
panel size, or the hover flag changes, which is what makes `hover_only`
cheap enough to hang off mouseenter/mouseleave. Those two listeners are
attached in _attachPanelEvents rather than in each per-kind handler: it is
the one panel behaviour identical across all four, and it must not depend
on a kind remembering to wire it.

Keys pin to the panel's IMAGE / plot box — the same four numbers
_resizePanelDOM drives the scale bar off — not the letterbox fit-rect, so a
key stays put when the data's aspect ratio changes underneath it.

hover_only keys are kept OUT of PNG export, and that needed real work
rather than the comment I first wrote claiming it fell out for free. The
key canvas is persistent: if the pointer happens to be over the panel when
exportPNG runs, the key is already painted and composites straight through.
Verified by rendering, hovering, and re-exporting — max channel delta was
210 before the fix, 0 after. _drawPanelKeysNoHover re-renders onto a
scratch canvas with the flag suppressed, mirroring the existing
_drawPanelWidgetsNoHandles precedent.

A caption on a card defaults to near-white rather than theme.tickText: the
usual card is a translucent dark slab and tickText is near-black under a
light theme, so the default put dark text on a dark pill. With no card the
label sits on the figure and takes the tick colour as before.

FIGURE_ESM.md anchors re-verified against all 9,466 lines.
Reverses the export rule: a saved figure now shows every key. The export
renders the panel as though the pointer were over it, so what you save is
what you see while reading the plot.

Fixing this surfaced a real bug that was not about export at all. Images
were decoded lazily from inside the DRAW loop, which only ever visits the
keys shown this frame — so a hover_only key that had never been on screen
had never begun decoding. The first hover would reveal nothing until some
later redraw happened to come along, and an export could not wait for
onload at all, so it silently omitted the key. Every DECLARED key now
starts decoding, whether or not it is drawn.

Second trap, in the same area: a panel whose only keys are hover_only kept
a display:none canvas while the pointer was away, and the export path
measures against that canvas's bounding rect — which is 0x0 when hidden, so
_drawEl dropped the key on the floor. The canvas is now sized and shown
whenever any key is declared, and merely left empty when none are currently
visible. `declared` and `shown` are now separate filters for exactly this
reason.

Verified both ways round: with mixed keys and with hover_only keys alone,
the cold export (pointer away) and the warm export (pointer over the panel)
are byte-identical and both contain the key.
33 tests. The Python half covers the lifecycle and validation; the render
half covers what only the browser can answer.

Notable cases, each pinned to a decision rather than to current output:

  * a bare RGBA key draws NO card — alpha 0 stays transparent, which is the
    whole reason a triangle or a disc can sit on an image
  * keys pin to the AXES BOX, not the letterboxed picture. The panel is made
    much wider than its square image so the two differ, and the key must
    track imgX. This is the scale bar's rule and the test says so.
  * hover_only stays dark until the pointer arrives, and a plain key is
    unaffected by hovering
  * three export regressions: a hover_only key must appear in an export
    taken with the pointer away; cold and hovered exports must be
    byte-identical; and a LONE hover_only key must still export — that last
    one is the display:none / 0x0-rect trap, which no other key is around to
    mask.

Adds a shared `mount_page` fixture to tests/conftest.py. `interact_page`
calls renderFn directly and exposes no mount handle, so it can drive a
pointer but cannot export or reach the panels map; these tests need both at
once. test_export_png.py and test_texture.py predate it and keep their own
local copies.
`labels=` draws text INSIDE the key, positioned in fractions of the key
image so it tracks the picture through a resize. This is what an IPF
triangle actually needs — its corner indices belong on the corners, not in
the caption underneath — and it reuses the existing mini-TeX `_drawTex`, so
`$[1\bar{1}0]$` works as in any other label.

Entries are `(x, y, text)` or a dict adding `size` / `color` / `align`.
`align` matters more than it looks: centring text on a corner hangs half of
it off the edge, where the panel clips it, which is exactly what the first
draft of the example did.

Glyphs are drawn white over a dark halo rather than in a theme colour. A
key's own colours are arbitrary — an IPF triangle runs through the entire
hue circle — so there is no background to key the text off, and any single
flat colour is illegible against part of it.

Adds Examples/PlotTypes/plot_key_overlays.py: an IPF colour key over a
synthetic orientation map (grains coloured by reading the key image itself,
so map and legend agree by construction) and a hue wheel over a vortex
polarization field. Plus the changelog fragment and a docs/api/keys.rst
page.

Tests: 40, up from 33. The label pair is a positive/control pair, and the
control earned its place — it caught the first version of the positive
test, which counted "near-white pixels in the red mask's bounding box" and
so was really counting the page background showing through the disc's
corners. Both now crop to the disc interior and detect glyphs by "strong
green AND blue over a pure-red key", which does not depend on antialiasing.
feat(keys): floating image keys pinned over a panel (IPF, colour wheels)
anyplotlib #49 was merged into feat/3d-colormap-surface at 09:12, but that
branch had already merged to main at 06:50 as #48 — so the six keys commits
were left on a branch nobody merges again, and v0.6.0 shipped set_texture
WITHOUT add_key. GitHub reports #49 as MERGED, which is true and misleading:
it merged into its base, and its base was already spent.

This brings them onto main unchanged.
0.7.0 is the plot-keys feature that missed the 0.6.0 boat: #49 merged into
feat/3d-colormap-surface at 09:12, but that branch had already merged to main
at 06:50 as #48, so its six commits were stranded and v0.6.0 shipped
set_texture without add_key.

Mirrors what the Prepare Release workflow does for bump=minor, stable:
version in pyproject.toml and docs/conf.py, towncrier build, switcher.json
entry, and the root redirect.

NB uv.lock still records version 0.5.0 — it was already stale on main (the
workflow never touches it), so it is left alone here rather than mixed into a
release commit.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.77%. Comparing base (322029e) to head (37439e4).

Files with missing lines Patch % Lines
anyplotlib/keys.py 95.72% 5 Missing ⚠️
anyplotlib/_base_plot.py 97.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #51      +/-   ##
==========================================
+ Coverage   90.58%   90.77%   +0.19%     
==========================================
  Files          39       40       +1     
  Lines        4345     4500     +155     
==========================================
+ Hits         3936     4085     +149     
- Misses        409      415       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis
CSSFrancis merged commit 78b6819 into main Jul 31, 2026
11 of 12 checks passed
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.

2 participants