Skip to content

feat(inset): callout connectors + arbitrary inset placement#30

Merged
CSSFrancis merged 5 commits into
feat/image-layersfrom
feat/inset-callouts
Jul 13, 2026
Merged

feat(inset): callout connectors + arbitrary inset placement#30
CSSFrancis merged 5 commits into
feat/image-layersfrom
feat/inset-callouts

Conversation

@CSSFrancis

Copy link
Copy Markdown
Owner

Summary

Stacked on #29 (retarget after it merges).

  • add_inset(..., anchor=(x_frac, y_frac)) — place an inset anywhere, not just the four corners (top-left in figure fraction, clamped; minimize/maximize/restore preserved).
  • inset.indicate_region(parent_plot, (x, y, w, h)) — the mark_inset equivalent: dashed rectangle around a DATA-coord region of the parent + two leader lines from the rect's facing corners to the inset's nearest corners (deterministic pairing). clear_indication() removes; one indication per inset; the parent can be any 2-D panel in the figure (cross-panel navigator→DP callouts).
  • Renders on a figure-level calloutCanvas: the rect tracks the parent's zoom/pan every draw and clips to the image area; leaders follow the inset's live DOM rect and hide while minimized; exportPNG composites indications last after a forced fresh draw. State lives in layout_json so figure_state/save_html round-trip it.

Motivation: SpyDE report-figure callouts (drop a signal onto its navigator's figure → inset with a connector to the source region).

Tests

19 new (13 unit + 6 Playwright visual/zoom-tracking). Regression: test_layouts/ + test_embed/ 241 passed / 6 pre-existing baseline skips; test_markers/ 119 passed.

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.
@CSSFrancis CSSFrancis merged commit 00e6666 into feat/image-layers Jul 13, 2026
10 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.

1 participant