Skip to content

feat(2.0): inset axes — qv.Inset as a structural element + pane [D152]–[D154] - #9

Merged
jawjay merged 6 commits into
mainfrom
feat/inset-axes
Aug 5, 2026
Merged

feat(2.0): inset axes — qv.Inset as a structural element + pane [D152]–[D154]#9
jawjay merged 6 commits into
mainfrom
feat/inset-axes

Conversation

@jawjay

@jawjay jawjay commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Implements I1–I4 of design/inset-axes.md Part II — matplotlib-style inset axes, done the qtviz way:

overview * qv.Inset(zoom, rect=(0.58, 0.55, 0.4, 0.42), label="zoom", indicate=True)
view.pane("zoom").set_range(x=(20, 22))     # move the zoom window
view.on(qv.RangeEvent, cb, pane="zoom")      # events from inside the inset

inset zoom

What landed

  • I1 — the node ([D152]): qv.Inset(child, rect=, label=, indicate=), composed into an overlay like an annotation. New Element.STRUCTURAL_CHILD declared marker ([D124] discipline): resolve_node/node_is_lazy recurse into the child, _elements_of yields through it, and both negotiation paths enforce same-surface ⇒ same-backend (explicit negotiate recursion + auto intersect). Depth-1 + rect validation. Freeze triple in one commit (FROZEN_2_0 + api.md + CHANGELOG); element census 28 → 29.
  • I2 — renderers: interception in the backends' surface loop (beside the y2 branch). matplotlib: ax.inset_axes + _render_cell re-entry (~15 lines — theming, surface config, events, PaneBus all inherited). pyqtgraph: _render_cell split into _surface_target/_populate_plot shared by grid cells and insets; a child PlotItem placed from rect (mpl bottom-origin semantics) on the parent ViewBox's sigResized (spiked offscreen first), opaque via autoFillBackground + themed palette. webengine: warns-and-skips insets until I5 (headless-tested via the figure dict); supports() accepts so negotiation proceeds. Labels thread as a deque in traversal order, matching flat_pane_labels' depth-first walk. Also fixes a latent [D146] interaction: the mpl sharex leader lookup now maps child→surface index (insets shift the surfaces list).
  • I3 — insets are panes ([D153]): zero production changes beyond I2 — the pane machinery carried them as designed. Pinned by tests: pane lists, per-pane element scoping, the inset window surviving pg⇄mpl backend switches via label-keyed LayoutState, RangeEvent(pane="zoom") + view.on(pane=), per-pane export of just the inset.
  • I4 — static zoom indicator ([D154]): indicate=True synthesizes the existing Rect annotation at the child's declared x/y lims — zero new drawing code on any backend; missing lims warn-and-skip.
  • Docs: gallery example 38_inset_zoom.py + screenshot; CLAUDE.md element census updated.

Not in this PR

I5 — webengine domain-axes support (replaces the warn-skip); the live follow-the-pan indicator (I4b). Both gated per the design doc.

Verification

+16 tests in test_inset.py (tier-1 node/pipeline/negotiation + tier-2 across both native backends). Suite 1074 green; ruff/mypy/mkdocs --strict clean; both backends visually verified (opaque panels, correct indicator placement).

🤖 Generated with Claude Code

jawjay added 6 commits August 5, 2026 14:12
qv.Inset(child, rect=, label=, indicate=) composes into an overlay like an
annotation (parent * Inset(...)). Element gains the declared
STRUCTURAL_CHILD marker ([D124] discipline): resolve_node/node_is_lazy
recurse into the child, _elements_of yields through it (negotiation's
intersect-first rule covers inset contents), and series_index_map treats
structural elements as chrome. Depth-1 and rect validation at construction.
Freeze triple: FROZEN_2_0 + api.md + CHANGELOG.

Design: design/inset-axes.md Part II (I1).
…bengine warn-skip

Interception is the surface loop (where the y2 branch lives), not the
renderer registry — supports()/honored_options gain a structural clause.
matplotlib: ax.inset_axes + _render_cell re-entry (theming, surface apply,
events, PaneBus stamping all inherited). pyqtgraph: _render_cell splits
into _surface_target/_populate_plot shared by grid cells and insets; a
child PlotItem parents onto the parent plot, placed from rect (mpl
bottom-origin semantics, Qt y-down conversion) on the parent ViewBox's
sigResized (spiked offscreen). webengine: the figure builder warns and
skips insets until I5; supports() accepts so negotiation proceeds.
Labels thread as a deque in traversal order, matching flat_pane_labels'
depth-first walk (which now includes insets). negotiate/auto_negotiate
recurse into structural children — same-surface ⇒ same-backend enforced.
mpl [D146] leader lookup gains a child→surface index map (insets shift
the surfaces list). Element census 28→29 (CLAUDE.md + vocabulary test).

Design: design/inset-axes.md Part II (I2).
No production changes needed beyond I2: flat_pane_labels already walks
insets and the backends append inset surfaces in depth-first order, so the
pane protocol carries them. Pins: pane lists ['0','zoom']; per-pane
element scoping; the inset window surviving pg⇄mpl backend switches via
label-keyed LayoutState; RangeEvent(pane='zoom') + view.on(pane=) scoping;
per-pane export of just the inset.
…es a Rect

indicate=True draws the parent-side rectangle at the child surface's
declared x/y lims — the synthesized wave-1 Rect annotation rendered
through the parent's element path, so it lowers everywhere with zero new
drawing code. Both lims required; missing lims warn-and-skip (the live
follow-the-pan indicator is a gated follow-on, design/inset-axes.md I4b).

Design: design/inset-axes.md Part II (I4).
…ed palette

The parent's curves/grid showed through the inset's margins (ViewBox
background covers only the data area). autoFillBackground over the whole
PlotItem rect matches matplotlib's opaque inset (spiked visually).
examples/38_inset_zoom.py: an overview curve with a labeled zoom inset and
the indicator rectangle; main() demos the pane-scoped RangeEvent
subscription. Wired into the capture tool, the gallery, and the
example-mains regression list. design/inset-axes.md Part II gains the
I1-I4-shipped status note (I5 webengine domain axes stays gated).
@jawjay
jawjay merged commit 9e77ffc into main Aug 5, 2026
5 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