3232)
3333from ._tensor_elements_support import _TensorRecord
3434from ._typing import root_figure
35- from ._ui_utils import _reserve_figure_bottom , _set_axes_visible
35+ from ._ui_utils import _set_axes_visible , _set_figure_bottom_reserved
3636from .config import EngineName , PlotConfig , ViewName
37+ from .contraction_viewer import _MAIN_FIGURE_BOTTOM_RESERVED , _PLAYBACK_DETAILS_TOP
3738from .einsum_module .trace import EinsumTrace
3839from .tensor_elements import _show_tensor_records
3940from .tensor_elements_config import TensorElementsConfig
4041
4142RenderedAxes = Axes | Axes3D
4243
43- # 2d/3d: width 0.053 (= 60% of 0.088); height 0.063; bottom lowered ~0.03 vs earlier slider-row alignment.
44- _VIEW_SELECTOR_BOUNDS : tuple [float , float , float , float ] = (0.213 , 0.025 , 0.053 , 0.063 )
45- _BASE_INTERACTIVE_CHECKBOX_BOUNDS : tuple [float , float , float , float ] = (0.02 , 0.028 , 0.19 , 0.09 )
46- _SCHEME_INTERACTIVE_CHECKBOX_BOUNDS : tuple [float , float , float , float ] = (0.02 , 0.028 , 0.19 , 0.142 )
47- _SCHEME_INSPECTOR_INTERACTIVE_CHECKBOX_BOUNDS : tuple [float , float , float , float ] = (
44+ # Menu column: fixed bottom = tallest stack (inspector + scheme). Without playback, checkboxes/radio
45+ # stay as low as when the bottom row exists. Top aligned with cost-details top.
46+ _VIEW_SELECTOR_LEFT : float = 0.213
47+ _VIEW_SELECTOR_WIDTH : float = 0.053
48+ _VIEW_SELECTOR_HEIGHT : float = 0.063
49+ # Manual axes positions: 2D extends slightly below *base*, 3D starts higher (base + lift).
50+ _INTERACTIVE_2D_BOTTOM_EXTRA : float = 0.022
51+ _INTERACTIVE_3D_BOTTOM_LIFT : float = 0.084
52+ _BASE_INTERACTIVE_HEIGHT : float = 0.09
53+ _SCHEME_INSPECTOR_INTERACTIVE_HEIGHT : float = 0.172
54+ _INTERACTIVE_MENU_COLUMN_HEIGHT : float = _SCHEME_INSPECTOR_INTERACTIVE_HEIGHT
55+ _INTERACTIVE_MENU_COLUMN_BOTTOM : float = _PLAYBACK_DETAILS_TOP - _INTERACTIVE_MENU_COLUMN_HEIGHT
56+ _INTERACTIVE_CHECKBOX_AXES_BOUNDS : tuple [float , float , float , float ] = (
4857 0.02 ,
49- 0.028 ,
58+ _INTERACTIVE_MENU_COLUMN_BOTTOM ,
5059 0.19 ,
51- 0.172 ,
60+ _INTERACTIVE_MENU_COLUMN_HEIGHT ,
61+ )
62+ # When Scheme is off, main axes bottom (not tied to menu column bottom after unifying menus).
63+ _SCHEME_OFF_FIGURE_BOTTOM_PAD : float = 0.02
64+ _MAIN_FIGURE_BOTTOM_SCHEME_OFF : float = (
65+ _PLAYBACK_DETAILS_TOP - _BASE_INTERACTIVE_HEIGHT + _SCHEME_OFF_FIGURE_BOTTOM_PAD
5266)
53- _INTERACTIVE_CONTROLS_BOTTOM : float = 0.26
5467_BASE_TOGGLE_LABELS : tuple [str , str , str ] = ("Hover" , "Tensor labels" , "Edge labels" )
5568_SCHEME_TOGGLE_LABELS : tuple [str , str , str ] = ("Scheme" , "Playback" , "Costs" )
5669_TENSOR_INSPECTOR_LABEL : str = "Tensor inspector"
5770_INTERACTIVE_LABEL_PROPS : dict [str , Sequence [Any ]] = {"fontsize" : [9.5 ]}
5871_INTERACTIVE_CHECK_FRAME_PROPS : dict [str , float ] = {"s" : 44.0 , "linewidth" : 0.9 }
5972_INTERACTIVE_CHECK_MARK_PROPS : dict [str , float ] = {"s" : 34.0 , "linewidth" : 1.0 }
6073_INTERACTIVE_RADIO_PROPS : dict [str , float ] = {"s" : 38.0 , "linewidth" : 0.9 }
74+ _CONTROL_TRAY_FACE : tuple [float , float , float ] = (0.97 , 0.97 , 0.99 )
75+ _CONTROL_TRAY_FRAME : tuple [float , float , float ] = (0.78 , 0.78 , 0.82 )
76+
77+
78+ def _style_interactive_control_axes (ax : Axes ) -> None :
79+ ax .set_xticks ([])
80+ ax .set_yticks ([])
81+ ax .set_navigate (False )
82+ ax .patch .set_facecolor (_CONTROL_TRAY_FACE )
83+ ax .patch .set_alpha (0.88 )
84+ ax .patch .set_edgecolor (_CONTROL_TRAY_FRAME )
85+ ax .patch .set_linewidth (0.6 )
86+ for spine in ax .spines .values ():
87+ spine .set_visible (True )
88+ spine .set_linewidth (0.6 )
89+ spine .set_color (_CONTROL_TRAY_FRAME )
6190
6291
6392@dataclass
@@ -222,11 +251,8 @@ def _interactive_checkbox_bounds(
222251 include_scheme_toggles : bool ,
223252 include_tensor_inspector : bool ,
224253) -> tuple [float , float , float , float ]:
225- if include_scheme_toggles and include_tensor_inspector :
226- return _SCHEME_INSPECTOR_INTERACTIVE_CHECKBOX_BOUNDS
227- if include_scheme_toggles :
228- return _SCHEME_INTERACTIVE_CHECKBOX_BOUNDS
229- return _BASE_INTERACTIVE_CHECKBOX_BOUNDS
254+ _ = include_scheme_toggles , include_tensor_inspector
255+ return _INTERACTIVE_CHECKBOX_AXES_BOUNDS
230256
231257
232258class _InteractiveTensorFigureController :
@@ -290,8 +316,6 @@ def initialize(self) -> tuple[Figure, RenderedAxes]:
290316 self .figure = figure
291317 if self ._view_caches [self .current_view ].scene is None :
292318 return figure , ax
293- if not self ._external_ax :
294- _reserve_figure_bottom (figure , _INTERACTIVE_CONTROLS_BOTTOM )
295319 self ._build_controls ()
296320 self ._apply_scene_state (self .current_scene )
297321 set_interactive_controls (figure , self )
@@ -346,6 +370,60 @@ def _build_view(
346370 scene .contraction_controls = get_contraction_controls (rendered_ax )
347371 return fig , rendered_ax
348372
373+ def _shared_data_axes_top (self ) -> float :
374+ ax3 = self ._view_caches ["3d" ].ax
375+ if ax3 is not None :
376+ p = ax3 .get_position ()
377+ return float (p .y0 + p .height )
378+ ax2 = self ._view_caches ["2d" ].ax
379+ if ax2 is not None :
380+ p = ax2 .get_position ()
381+ return float (p .y0 + p .height )
382+ return 0.9
383+
384+ def _interactive_scheme_chrome_on (self ) -> bool :
385+ return self .current_scene .contraction_controls is not None and self .scheme_on
386+
387+ def _interactive_main_axes_bottom (self ) -> float :
388+ return float (
389+ _MAIN_FIGURE_BOTTOM_RESERVED
390+ if self ._interactive_scheme_chrome_on ()
391+ else _MAIN_FIGURE_BOTTOM_SCHEME_OFF
392+ )
393+
394+ def _figure_bottom_margin (self ) -> float :
395+ base = self ._interactive_main_axes_bottom ()
396+ lows : list [float ] = []
397+ if self ._view_caches ["2d" ].ax is not None :
398+ lows .append (base - float (_INTERACTIVE_2D_BOTTOM_EXTRA ))
399+ if self ._view_caches ["3d" ].ax is not None :
400+ lows .append (base + float (_INTERACTIVE_3D_BOTTOM_LIFT ))
401+ return min (lows ) if lows else base
402+
403+ def _apply_interactive_figure_layout (self ) -> None :
404+ if self .figure is None or self ._external_ax :
405+ return
406+ _set_figure_bottom_reserved (self .figure , self ._figure_bottom_margin ())
407+ self ._sync_data_axes_vertical_layout ()
408+
409+ def _sync_data_axes_vertical_layout (self ) -> None :
410+ if self .figure is None or self ._external_ax :
411+ return
412+ base = self ._interactive_main_axes_bottom ()
413+ top = self ._shared_data_axes_top ()
414+ ax2 = self ._view_caches ["2d" ].ax
415+ ax3 = self ._view_caches ["3d" ].ax
416+ if ax2 is not None :
417+ bottom_2d = base - float (_INTERACTIVE_2D_BOTTOM_EXTRA )
418+ pos = ax2 .get_position ()
419+ height = max (top - bottom_2d , 0.08 )
420+ ax2 .set_position ([pos .x0 , bottom_2d , pos .width , height ])
421+ if ax3 is not None :
422+ bottom_3d = base + float (_INTERACTIVE_3D_BOTTOM_LIFT )
423+ pos = ax3 .get_position ()
424+ height = max (top - bottom_3d , 0.08 )
425+ ax3 .set_position ([pos .x0 , bottom_3d , pos .width , height ])
426+
349427 def _build_controls (self ) -> None :
350428 assert self .figure is not None
351429 labels = list (_BASE_TOGGLE_LABELS )
@@ -355,8 +433,23 @@ def _build_controls(self) -> None:
355433 labels .extend (_SCHEME_TOGGLE_LABELS )
356434 if has_tensor_inspector :
357435 labels .append (_TENSOR_INSPECTOR_LABEL )
436+ cb_bounds = _interactive_checkbox_bounds (
437+ include_scheme_toggles = has_scheme_toggles ,
438+ include_tensor_inspector = has_tensor_inspector ,
439+ )
440+ cb_bottom = float (cb_bounds [1 ])
441+ check_ax = self .figure .add_axes (cb_bounds )
442+ _style_interactive_control_axes (check_ax )
443+ self ._check_ax = check_ax
358444 if not self ._external_ax :
359- radio_ax = self .figure .add_axes (_VIEW_SELECTOR_BOUNDS )
445+ radio_bounds : tuple [float , float , float , float ] = (
446+ _VIEW_SELECTOR_LEFT ,
447+ cb_bottom ,
448+ _VIEW_SELECTOR_WIDTH ,
449+ _VIEW_SELECTOR_HEIGHT ,
450+ )
451+ radio_ax = self .figure .add_axes (radio_bounds )
452+ _style_interactive_control_axes (radio_ax )
360453 self ._radio_ax = radio_ax
361454 active_index = 0 if self .current_view == "2d" else 1
362455 self ._radio = RadioButtons (
@@ -367,13 +460,6 @@ def _build_controls(self) -> None:
367460 radio_props = _INTERACTIVE_RADIO_PROPS ,
368461 )
369462 self ._radio .on_clicked (self ._on_view_clicked )
370- check_ax = self .figure .add_axes (
371- _interactive_checkbox_bounds (
372- include_scheme_toggles = has_scheme_toggles ,
373- include_tensor_inspector = has_tensor_inspector ,
374- )
375- )
376- self ._check_ax = check_ax
377463 statuses = [
378464 self .hover_on ,
379465 self .tensor_labels_on ,
@@ -489,6 +575,8 @@ def _apply_scene_state(self, scene: _InteractiveSceneState) -> None:
489575 self ._tensor_inspector .set_enabled (self .tensor_inspector_on )
490576 _apply_scene_hover_state (scene , hover_on = self .hover_on )
491577 self ._sync_checkbuttons ()
578+ if not self ._external_ax :
579+ self ._apply_interactive_figure_layout ()
492580 scene .ax .figure .canvas .draw_idle ()
493581
494582 def set_view (self , view : ViewName ) -> None :
0 commit comments