Skip to content

Feat/image layers#31

Merged
CSSFrancis merged 7 commits into
mainfrom
feat/image-layers
Jul 13, 2026
Merged

Feat/image layers#31
CSSFrancis merged 7 commits into
mainfrom
feat/image-layers

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

No description provided.

CSSFrancis and others added 7 commits July 12, 2026 11:01
plot2d.add_layer(data, cmap=, alpha=, clim=, visible=) returns a Layer
handle (set/set_data/remove); layers draw bottom-up above the base image
with the base's exact fit-rect + zoom/pan transform, under axes, markers,
and widgets, and composite in Canvas2D even over a WebGPU base. Per-layer
LUT bitmaps are cached by (pixels, cmap, clim) so unchanged layers cost
nothing on repaint; exportPNG captures layers for free.

Heavy pixels ride dynamic geom keys (layer_<id>_b64): _GEOM_KEYS is now a
property that unions the fixed base set with live layer keys, the Electron
binary route ships each layer as its own PLOTBIN frame, and
_spliceBinaryBytes scans by geom prefix instead of a hardcoded key list.
resolve_pixel_tokens materialises layer base64 so save_html snapshots stay
self-contained.

Layers and tile mode exclude each other (enforced both directions).
Layer data must match the base (H, W).

30 unit + 5 Playwright tests (incl. alpha-blend pixel assertions and the
WebGPU-base + Canvas2D-layer readback case).
add_inset gains anchor=(x_frac, y_frac) as an alternative to the four
corners (inset top-left in figure fraction, clamped inside the figure;
minimize/maximize/restore still work). inset.indicate_region(parent_plot,
(x, y, w, h)) draws the mark_inset look: a dashed rectangle around the
region in the parent's DATA coords plus two leader lines from the rect's
facing corners to the inset's nearest corners (deterministic loc1/loc2
pairing). clear_indication() removes it; one indication per inset.

Indications live in layout_json (round-trip through figure_state /
save_html) and render on a figure-level calloutCanvas above panels and
insets: the rect re-maps through the parent transform every draw (tracks
zoom/pan, clips to the image area), leaders follow the inset's live DOM
rect and hide while minimized, and exportPNG composites them last after a
forced fresh draw. The parent may be any 2-D panel in the figure, so
cross-panel navigator-to-DP callouts work.

19 tests (13 unit + 6 Playwright visual/tracking).
exportPNG now draws inset title bars (they are DOM divs, previously
dropped from every export) and snaps element edges to the output pixel
grid (rounded left/right edges, not rounded widths) so fractional DPR
no longer produces 1px seams between grid panels.

Layers: a shape-changing set_data on a layered plot raises a clear
ValueError instead of silently stretching stale-sized layers over the
new fit rect (mirrors the tile-mode guards); Layer.set accepts
clim='auto' to return to data min/max after an explicit clim (None
still means unchanged — the docstring now matches the behavior).

indicate_region validates its parent belongs to the same figure and
that the region is four finite numbers with positive extent;
out-of-bounds regions remain allowed (clipping is by design).
FIGURE_ESM.md documents all of the above plus a maintainer note that
layout indications are rebuilt from _insets_map on every push (no
stale-indication path exists until a remove_inset API is added).
…andles

- ArrowWidget: draggable arrow in image-px coords (body move, head re-aim),
  add_arrow_widget + add_widget('arrow', ...) dispatch
- show_handles option on every 2-D widget (hides handle dots; drag unchanged)
- Figure edit_chrome / selected_panel traits: JS-local dashed hover outline
  per panel + persistent solid selection outline
- figure-background pointer_down (edit mode, clicks outside all panels) via a
  new Figure-level CallbackRegistry
- figure_markers_json: figure-fraction annotation layer (text/circle/rect/
  arrow) drawn over all panels, draggable under edit_chrome with pointer_up
  write-back; always composited into exportPNG while edit chrome/handles
  never export (edit-mode export is pixel-identical to non-edit)
…push fixes

- circle gains a visible center node (radius node existed); arrow tail handle
  becomes a reshape node (tail moves, head anchored: x,y+=d, u,v-=d); shaft
  still moves the whole arrow; endpoint cursors
- edit-chrome hover/selection outlines drawn fully inset (outline-offset =
  -width) so edge panels are no longer clipped right/bottom
- panel drag-swap: per-panel move grip under edit_chrome; dropping on another
  panel fires figure-level callbacks with source/target panel ids (host swaps
  and rebuilds); source/empty drops cancel cleanly
- mount() gains an onResize hook (rAF-debounced ResizeObserver) so hosts can
  relayout on container-only resizes
- exportPNG(includeWidgets) redraws panel overlay widgets handle-free on a
  scratch canvas (mirrors the figure-marker export path) — visible resize
  nodes never bake into harvested PNGs; live canvas untouched
- standalone page makeModel(): save_changes() after a parent awi_state push
  now fires ONLY the listeners of keys actually set (per-key dirty set) —
  a targeted event_json widget update was being clobbered by the stale
  panel_<id>_json listener re-applying old panel state in the same cascade
  (Python-pushed widget.set(color/geometry) never repainted in Electron)
The figure-level callout overlay (calloutCanvas, added in the inset-callout
work) and the figure-marker overlay (figMarkerCanvas) sized their backing
store to the FULL figure even when there was nothing to draw — both draw
functions set width/height to fig_width/fig_height and only THEN returned
early on an empty indication/marker list.

An empty full-figure transparent canvas spans the whole figure, so it is
wider than any single panel and becomes the LARGEST <canvas> in the DOM.
Any consumer/test that samples "the largest canvas" then reads the
transparent overlay instead of the panel content — which is exactly why
the tiled-image render tests (test_detail_tile / test_tiled_imshow,
"0 pixels rendered") regressed: the panel drew its overview/detail tile
correctly, but the probe sampled the empty overlay on top.

Fix: when there is nothing to draw, collapse the overlay to a 0x0 backing
store (which also clears any prior drawing) and return, instead of sizing
it to the full figure. The overlay grows back to full-figure size the moment
an indication / marker exists (covered by the existing TestCalloutRendering
and edit-chrome marker suites). Add a regression test pinning that empty
overlays stay 0x0 and never shadow panel content.
feat(inset): callout connectors + arbitrary inset placement
@CSSFrancis CSSFrancis merged commit 24df988 into main Jul 13, 2026
10 of 11 checks passed
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49682% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.00%. Comparing base (0b74e7c) to head (00e6666).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
anyplotlib/plot2d/_plot2d.py 96.27% 6 Missing ⚠️
anyplotlib/plot2d/_layer.py 91.30% 4 Missing ⚠️
anyplotlib/figure/_figure.py 98.14% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #31      +/-   ##
==========================================
+ Coverage   88.97%   90.00%   +1.02%     
==========================================
  Files          38       39       +1     
  Lines        3511     3801     +290     
==========================================
+ Hits         3124     3421     +297     
+ Misses        387      380       -7     

☔ 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.

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