feat(inset): callout connectors + arbitrary inset placement#30
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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))— themark_insetequivalent: 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).exportPNGcomposites indications last after a forced fresh draw. State lives inlayout_jsonsofigure_state/save_htmlround-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.