2525from __future__ import annotations
2626import json , pathlib
2727import 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 )
2930from 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