Skip to content

Commit a77f30a

Browse files
committed
Add InsetAxes for floating overlay sub-plots and enhance layout handling
1 parent 7f11f7c commit a77f30a

27 files changed

Lines changed: 982 additions & 161 deletions

anyplotlib/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from anyplotlib.figure import Figure, GridSpec, SubplotSpec, subplots
2-
from anyplotlib.figure_plots import Axes, Plot1D, Plot2D, PlotMesh, Plot3D, PlotBar
2+
from anyplotlib.figure_plots import Axes, InsetAxes, Plot1D, Plot2D, PlotMesh, Plot3D, PlotBar
33
from anyplotlib.callbacks import CallbackRegistry, Event
44
from anyplotlib.widgets import (
55
Widget, RectangleWidget, CircleWidget, AnnularWidget,
@@ -14,7 +14,7 @@
1414

1515
__all__ = [
1616
"Figure", "GridSpec", "SubplotSpec", "subplots",
17-
"Axes", "Plot1D", "Plot2D", "PlotMesh", "Plot3D", "PlotBar",
17+
"Axes", "InsetAxes", "Plot1D", "Plot2D", "PlotMesh", "Plot3D", "PlotBar",
1818
"CallbackRegistry", "Event",
1919
"Widget", "RectangleWidget", "CircleWidget", "AnnularWidget",
2020
"CrosshairWidget", "PolygonWidget", "LabelWidget",

anyplotlib/figure.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
from __future__ import annotations
2626
import json, pathlib
2727
import anywidget, numpy as np, traitlets
28-
from anyplotlib.figure_plots import GridSpec, SubplotSpec, Axes, Plot2D, PlotMesh, Plot3D, PlotBar
28+
from anyplotlib.figure_plots import (GridSpec, SubplotSpec, Axes, Plot2D, PlotMesh,
29+
Plot3D, PlotBar, InsetAxes, _plot_kind)
2930
from anyplotlib.callbacks import Event
3031

3132
__all__ = ["Figure", "GridSpec", "SubplotSpec", "subplots"]
@@ -124,6 +125,7 @@ def __init__(self, nrows=1, ncols=1, figsize=(640, 480),
124125
self._sharey = sharey
125126
self._axes_map: dict = {}
126127
self._plots_map: dict = {}
128+
self._insets_map: dict = {}
127129
with self.hold_trait_notifications():
128130
self.fig_width = figsize[0]
129131
self.fig_height = figsize[1]
@@ -263,10 +265,7 @@ def _mg(flag, key):
263265
plot = self._plots_map.get(pid)
264266
panel_specs.append({
265267
"id": pid,
266-
"kind": ("3d" if isinstance(plot, Plot3D)
267-
else "2d" if isinstance(plot, (Plot2D, PlotMesh))
268-
else "bar" if isinstance(plot, PlotBar)
269-
else "1d"),
268+
"kind": _plot_kind(plot) if plot else "1d",
270269
"row_start": s.row_start,
271270
"row_stop": s.row_stop,
272271
"col_start": s.col_start,
@@ -275,6 +274,23 @@ def _mg(flag, key):
275274
"panel_height": ph,
276275
})
277276

277+
inset_specs = []
278+
for pid, inset_ax in self._insets_map.items():
279+
plot = self._plots_map.get(pid)
280+
pw = max(64, round(self.fig_width * inset_ax.w_frac))
281+
ph = max(64, round(self.fig_height * inset_ax.h_frac))
282+
inset_specs.append({
283+
"id": pid,
284+
"kind": _plot_kind(plot) if plot else "1d",
285+
"w_frac": inset_ax.w_frac,
286+
"h_frac": inset_ax.h_frac,
287+
"corner": inset_ax.corner,
288+
"title": inset_ax.title,
289+
"panel_width": pw,
290+
"panel_height": ph,
291+
"inset_state": inset_ax._inset_state,
292+
})
293+
278294
self.layout_json = json.dumps({
279295
"nrows": self._nrows,
280296
"ncols": self._ncols,
@@ -284,8 +300,48 @@ def _mg(flag, key):
284300
"fig_height": self.fig_height,
285301
"panel_specs": panel_specs,
286302
"share_groups": share_groups,
303+
"inset_specs": inset_specs,
287304
})
288305

306+
# ── inset creation ────────────────────────────────────────────────────────
307+
def add_inset(self, w_frac: float, h_frac: float, *,
308+
corner: str = "top-right", title: str = "") -> "InsetAxes":
309+
"""Create and return a floating inset axes.
310+
311+
The inset overlays the figure at the specified corner. Call
312+
plot-factory methods on the returned :class:`InsetAxes` to attach
313+
data::
314+
315+
inset = fig.add_inset(0.3, 0.25, corner="top-right", title="Zoom")
316+
inset.imshow(data) # returns Plot2D
317+
inset.plot(profile) # returns Plot1D
318+
319+
Parameters
320+
----------
321+
w_frac, h_frac : float
322+
Width and height as fractions of the figure size (0–1).
323+
corner : str, optional
324+
Positioning corner: ``"top-right"`` (default), ``"top-left"``,
325+
``"bottom-right"``, or ``"bottom-left"``.
326+
title : str, optional
327+
Text displayed in the inset title bar.
328+
329+
Returns
330+
-------
331+
InsetAxes
332+
"""
333+
return InsetAxes(self, w_frac, h_frac, corner=corner, title=title)
334+
335+
def _register_inset(self, inset_ax: "InsetAxes", plot) -> None:
336+
"""Register an inset plot, allocating its trait and updating layout."""
337+
pid = plot._id
338+
if not self.has_trait(f"panel_{pid}_json"):
339+
self.add_traits(**{f"panel_{pid}_json": traitlets.Unicode("{}").tag(sync=True)})
340+
self._plots_map[pid] = plot
341+
self._insets_map[pid] = inset_ax
342+
self._push(pid)
343+
self._push_layout()
344+
289345
@traitlets.observe("fig_width", "fig_height")
290346
def _on_resize(self, change) -> None:
291347
self._push_layout()
@@ -313,6 +369,16 @@ def _on_event(self, change) -> None:
313369
data = {k: v for k, v in msg.items()
314370
if k not in ("source", "panel_id", "event_type", "widget_id")}
315371

372+
# Inset state changes are handled before regular plot dispatch
373+
if event_type == "on_inset_state_change":
374+
inset_ax = self._insets_map.get(panel_id)
375+
if inset_ax is not None:
376+
new_state = data.get("new_state", "normal")
377+
if new_state in ("normal", "minimized", "maximized"):
378+
inset_ax._inset_state = new_state
379+
self._push_layout()
380+
return
381+
316382
plot = self._plots_map.get(panel_id)
317383
if plot is None:
318384
return

0 commit comments

Comments
 (0)