syd 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syd/__init__.py +3 -3
- syd/flask_deployment/__init__.py +1 -0
- syd/flask_deployment/components.py +376 -363
- syd/flask_deployment/deployer.py +247 -283
- syd/flask_deployment/static/css/viewer.css +82 -0
- syd/flask_deployment/static/js/viewer.js +174 -0
- syd/flask_deployment/templates/base.html +23 -20
- syd/flask_deployment/templates/viewer.html +49 -95
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -1
- syd/notebook_deployment/_ipympl_deployer.py +258 -0
- syd/notebook_deployment/deployer.py +126 -50
- syd/notebook_deployment/widgets.py +142 -69
- syd/parameters.py +93 -180
- syd/plotly_deployment/__init__.py +1 -0
- syd/plotly_deployment/components.py +531 -0
- syd/plotly_deployment/deployer.py +376 -0
- syd/{interactive_viewer.py → viewer.py} +152 -188
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/METADATA +26 -12
- syd-0.1.7.dist-info/RECORD +22 -0
- syd/flask_deployment/static/css/styles.css +0 -39
- syd/flask_deployment/static/js/components.js +0 -51
- syd-0.1.6.dist-info/RECORD +0 -18
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/WHEEL +0 -0
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
import ipywidgets as widgets
|
|
5
|
+
from IPython.display import display
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import warnings
|
|
8
|
+
from ..parameters import ParameterUpdateWarning
|
|
9
|
+
|
|
10
|
+
from ..viewer import Viewer
|
|
11
|
+
from .widgets import BaseWidget, create_widget
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@contextmanager
|
|
15
|
+
def _plot_context():
|
|
16
|
+
plt.ioff()
|
|
17
|
+
try:
|
|
18
|
+
yield
|
|
19
|
+
finally:
|
|
20
|
+
plt.ion()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class LayoutConfig:
|
|
25
|
+
"""Configuration for the viewer layout."""
|
|
26
|
+
|
|
27
|
+
controls_position: str = "left" # Options are: 'left', 'top'
|
|
28
|
+
figure_width: float = 8.0
|
|
29
|
+
figure_height: float = 6.0
|
|
30
|
+
controls_width_percent: int = 30
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
valid_positions = ["left", "top"]
|
|
34
|
+
if self.controls_position not in valid_positions:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_horizontal(self) -> bool:
|
|
41
|
+
return self.controls_position == "left"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotebookDeployer:
|
|
45
|
+
"""
|
|
46
|
+
A deployment system for Viewer in Jupyter notebooks using ipywidgets.
|
|
47
|
+
Built around the parameter widget system for clean separation of concerns.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
viewer: Viewer,
|
|
53
|
+
layout_config: Optional[LayoutConfig] = None,
|
|
54
|
+
continuous: bool = False,
|
|
55
|
+
suppress_warnings: bool = False,
|
|
56
|
+
):
|
|
57
|
+
if not isinstance(viewer, Viewer): # type: ignore
|
|
58
|
+
raise TypeError(f"viewer must be an Viewer, got {type(viewer).__name__}")
|
|
59
|
+
|
|
60
|
+
self.viewer = viewer
|
|
61
|
+
self.config = layout_config or LayoutConfig()
|
|
62
|
+
self.continuous = continuous
|
|
63
|
+
self.suppress_warnings = suppress_warnings
|
|
64
|
+
|
|
65
|
+
# Initialize containers
|
|
66
|
+
self.parameter_widgets: Dict[str, BaseWidget] = {}
|
|
67
|
+
self.layout_widgets = self._create_layout_controls()
|
|
68
|
+
self.plot_output = widgets.Output()
|
|
69
|
+
self._canvas_widget = None
|
|
70
|
+
|
|
71
|
+
# Store current figure
|
|
72
|
+
self._current_figure = None
|
|
73
|
+
# Flag to prevent circular updates
|
|
74
|
+
self._updating = False
|
|
75
|
+
|
|
76
|
+
def _create_layout_controls(self) -> Dict[str, widgets.Widget]:
|
|
77
|
+
"""Create widgets for controlling the layout."""
|
|
78
|
+
controls: Dict[str, widgets.Widget] = {}
|
|
79
|
+
|
|
80
|
+
# Controls width slider for horizontal layouts
|
|
81
|
+
if self.config.is_horizontal:
|
|
82
|
+
controls["controls_width"] = widgets.IntSlider(
|
|
83
|
+
value=self.config.controls_width_percent,
|
|
84
|
+
min=20,
|
|
85
|
+
max=80,
|
|
86
|
+
description="Controls Width %",
|
|
87
|
+
continuous=True,
|
|
88
|
+
layout=widgets.Layout(width="95%"),
|
|
89
|
+
style={"description_width": "initial"},
|
|
90
|
+
)
|
|
91
|
+
controls["controls_width"].observe(
|
|
92
|
+
self._handle_container_width_change, names="value"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return controls
|
|
96
|
+
|
|
97
|
+
def _create_parameter_widgets(self) -> None:
|
|
98
|
+
"""Create widget instances for all parameters."""
|
|
99
|
+
for name, param in self.viewer.parameters.items():
|
|
100
|
+
widget = create_widget(
|
|
101
|
+
param,
|
|
102
|
+
continuous=self.continuous,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Store in widget dict
|
|
106
|
+
self.parameter_widgets[name] = widget
|
|
107
|
+
|
|
108
|
+
def _handle_widget_engagement(self, name: str) -> None:
|
|
109
|
+
"""Handle engagement with an interactive widget."""
|
|
110
|
+
if self._updating:
|
|
111
|
+
print(
|
|
112
|
+
"Already updating -- there's a circular dependency!"
|
|
113
|
+
"This is probably caused by failing to disable callbacks for a parameter."
|
|
114
|
+
"It's a bug --- tell the developer on github issues please."
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
self._updating = True
|
|
120
|
+
|
|
121
|
+
# Optionally suppress warnings during parameter updates
|
|
122
|
+
with warnings.catch_warnings():
|
|
123
|
+
if self.suppress_warnings:
|
|
124
|
+
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
125
|
+
|
|
126
|
+
widget = self.parameter_widgets[name]
|
|
127
|
+
|
|
128
|
+
if widget._is_action:
|
|
129
|
+
parameter = self.viewer.parameters[name]
|
|
130
|
+
parameter.callback(self.viewer.state)
|
|
131
|
+
else:
|
|
132
|
+
self.viewer.set_parameter_value(name, widget.value)
|
|
133
|
+
|
|
134
|
+
# Update any widgets that changed due to dependencies
|
|
135
|
+
self._sync_widgets_with_state(exclude=name)
|
|
136
|
+
|
|
137
|
+
# Update the plot
|
|
138
|
+
self._update_plot()
|
|
139
|
+
|
|
140
|
+
finally:
|
|
141
|
+
self._updating = False
|
|
142
|
+
|
|
143
|
+
def _handle_action(self, name: str) -> None:
|
|
144
|
+
"""Handle actions for parameter widgets."""
|
|
145
|
+
|
|
146
|
+
def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
|
|
147
|
+
"""Sync widget values with viewer state."""
|
|
148
|
+
for name, parameter in self.viewer.parameters.items():
|
|
149
|
+
if name == exclude:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
widget = self.parameter_widgets[name]
|
|
153
|
+
if not widget.matches_parameter(parameter):
|
|
154
|
+
widget.update_from_parameter(parameter)
|
|
155
|
+
|
|
156
|
+
def _handle_figure_size_change(self, change: Dict[str, Any]) -> None:
|
|
157
|
+
"""Handle changes to figure dimensions."""
|
|
158
|
+
if self._current_figure is None:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
self._redraw_plot()
|
|
162
|
+
|
|
163
|
+
def _handle_container_width_change(self, change: Dict[str, Any]) -> None:
|
|
164
|
+
"""Handle changes to container width proportions."""
|
|
165
|
+
width_percent = self.layout_widgets["controls_width"].value
|
|
166
|
+
self.config.controls_width_percent = width_percent
|
|
167
|
+
|
|
168
|
+
# Update container widths
|
|
169
|
+
self.widgets_container.layout.width = f"{width_percent}%"
|
|
170
|
+
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
171
|
+
|
|
172
|
+
def _update_plot(self) -> None:
|
|
173
|
+
"""Update the plot with current state."""
|
|
174
|
+
state = self.viewer.state
|
|
175
|
+
|
|
176
|
+
with _plot_context():
|
|
177
|
+
new_fig = self.viewer.plot(state)
|
|
178
|
+
plt.close(self._current_figure) # Close old figure
|
|
179
|
+
self._current_figure = new_fig
|
|
180
|
+
|
|
181
|
+
# Clear previous output and display new figure
|
|
182
|
+
self.plot_output.clear_output(wait=True)
|
|
183
|
+
with self.plot_output:
|
|
184
|
+
# Make sure the canvas is created and displayed
|
|
185
|
+
if self._canvas_widget is None:
|
|
186
|
+
self._canvas_widget = self._current_figure.canvas
|
|
187
|
+
display(self._current_figure.canvas)
|
|
188
|
+
|
|
189
|
+
def _redraw_plot(self) -> None:
|
|
190
|
+
"""Clear and redraw the plot in the output widget."""
|
|
191
|
+
if self._canvas_widget is not None:
|
|
192
|
+
self._canvas_widget.draw()
|
|
193
|
+
|
|
194
|
+
def _create_layout(self) -> widgets.Widget:
|
|
195
|
+
"""Create the main layout combining controls and plot."""
|
|
196
|
+
# Create layout controls section
|
|
197
|
+
layout_box = widgets.VBox(
|
|
198
|
+
[widgets.HTML("<b>Layout Controls</b>")]
|
|
199
|
+
+ list(self.layout_widgets.values()),
|
|
200
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Set up parameter widgets with their observe callbacks
|
|
204
|
+
for name, widget in self.parameter_widgets.items():
|
|
205
|
+
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
206
|
+
|
|
207
|
+
# Create parameter controls section
|
|
208
|
+
param_box = widgets.VBox(
|
|
209
|
+
[widgets.HTML("<b>Parameters</b>")]
|
|
210
|
+
+ [w.widget for w in self.parameter_widgets.values()],
|
|
211
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Combine all controls
|
|
215
|
+
self.widgets_container = widgets.VBox(
|
|
216
|
+
[param_box, layout_box],
|
|
217
|
+
layout=widgets.Layout(
|
|
218
|
+
width=(
|
|
219
|
+
f"{self.config.controls_width_percent}%"
|
|
220
|
+
if self.config.is_horizontal
|
|
221
|
+
else "100%"
|
|
222
|
+
),
|
|
223
|
+
padding="10px",
|
|
224
|
+
overflow_y="auto",
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Create plot container
|
|
229
|
+
self.plot_container = widgets.VBox(
|
|
230
|
+
[self.plot_output],
|
|
231
|
+
layout=widgets.Layout(
|
|
232
|
+
width=(
|
|
233
|
+
f"{100 - self.config.controls_width_percent}%"
|
|
234
|
+
if self.config.is_horizontal
|
|
235
|
+
else "100%"
|
|
236
|
+
),
|
|
237
|
+
padding="10px",
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Create final layout based on configuration
|
|
242
|
+
if self.config.controls_position == "left":
|
|
243
|
+
return widgets.HBox([self.widgets_container, self.plot_container])
|
|
244
|
+
else:
|
|
245
|
+
return widgets.VBox([self.widgets_container, self.plot_container])
|
|
246
|
+
|
|
247
|
+
def deploy(self) -> None:
|
|
248
|
+
"""Deploy the interactive viewer with proper state management."""
|
|
249
|
+
with self.viewer._deploy_app():
|
|
250
|
+
# Create widgets
|
|
251
|
+
self._create_parameter_widgets()
|
|
252
|
+
|
|
253
|
+
# Create and display layout
|
|
254
|
+
layout = self._create_layout()
|
|
255
|
+
display(layout)
|
|
256
|
+
|
|
257
|
+
# Create initial plot
|
|
258
|
+
self._update_plot()
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
from contextlib import contextmanager
|
|
6
|
+
from time import time
|
|
7
|
+
|
|
4
8
|
import ipywidgets as widgets
|
|
5
9
|
from IPython.display import display
|
|
10
|
+
import matplotlib as mpl
|
|
6
11
|
import matplotlib.pyplot as plt
|
|
7
|
-
import warnings
|
|
8
|
-
from ..parameters import ParameterUpdateWarning
|
|
9
12
|
|
|
10
|
-
from ..
|
|
13
|
+
from ..parameters import ParameterUpdateWarning
|
|
14
|
+
from ..viewer import Viewer
|
|
11
15
|
from .widgets import BaseWidget, create_widget
|
|
12
16
|
|
|
13
17
|
|
|
@@ -20,17 +24,53 @@ def _plot_context():
|
|
|
20
24
|
plt.ion()
|
|
21
25
|
|
|
22
26
|
|
|
27
|
+
def get_backend_type():
|
|
28
|
+
"""
|
|
29
|
+
Determines the current matplotlib backend type and returns relevant info
|
|
30
|
+
"""
|
|
31
|
+
backend = mpl.get_backend().lower()
|
|
32
|
+
|
|
33
|
+
if "inline" in backend:
|
|
34
|
+
return "inline"
|
|
35
|
+
elif "widget" in backend or "ipympl" in backend:
|
|
36
|
+
return "widget"
|
|
37
|
+
elif "qt" in backend:
|
|
38
|
+
return "qt"
|
|
39
|
+
else:
|
|
40
|
+
return "other"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def debounce(wait_time):
|
|
44
|
+
"""
|
|
45
|
+
Decorator to prevent a function from being called more than once every wait_time seconds.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def decorator(fn):
|
|
49
|
+
last_called = [0.0] # Using list to maintain state in closure
|
|
50
|
+
|
|
51
|
+
@wraps(fn)
|
|
52
|
+
def debounced(*args, **kwargs):
|
|
53
|
+
current_time = time()
|
|
54
|
+
if current_time - last_called[0] >= wait_time:
|
|
55
|
+
fn(*args, **kwargs)
|
|
56
|
+
last_called[0] = current_time
|
|
57
|
+
|
|
58
|
+
return debounced
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
|
|
23
63
|
@dataclass
|
|
24
64
|
class LayoutConfig:
|
|
25
65
|
"""Configuration for the viewer layout."""
|
|
26
66
|
|
|
27
|
-
controls_position: str = "left" # Options are: 'left', 'top'
|
|
67
|
+
controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
|
|
28
68
|
figure_width: float = 8.0
|
|
29
69
|
figure_height: float = 6.0
|
|
30
70
|
controls_width_percent: int = 30
|
|
31
71
|
|
|
32
72
|
def __post_init__(self):
|
|
33
|
-
valid_positions = ["left", "top"]
|
|
73
|
+
valid_positions = ["left", "top", "right", "bottom"]
|
|
34
74
|
if self.controls_position not in valid_positions:
|
|
35
75
|
raise ValueError(
|
|
36
76
|
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
@@ -38,42 +78,54 @@ class LayoutConfig:
|
|
|
38
78
|
|
|
39
79
|
@property
|
|
40
80
|
def is_horizontal(self) -> bool:
|
|
41
|
-
return self.controls_position == "left"
|
|
81
|
+
return self.controls_position == "left" or self.controls_position == "right"
|
|
42
82
|
|
|
43
83
|
|
|
44
|
-
class
|
|
84
|
+
class NotebookDeployer:
|
|
45
85
|
"""
|
|
46
|
-
A deployment system for
|
|
86
|
+
A deployment system for Viewer in Jupyter notebooks using ipywidgets.
|
|
47
87
|
Built around the parameter widget system for clean separation of concerns.
|
|
48
88
|
"""
|
|
49
89
|
|
|
50
90
|
def __init__(
|
|
51
91
|
self,
|
|
52
|
-
viewer:
|
|
53
|
-
|
|
92
|
+
viewer: Viewer,
|
|
93
|
+
controls_position: str = "left",
|
|
94
|
+
figure_width: float = 8.0,
|
|
95
|
+
figure_height: float = 6.0,
|
|
96
|
+
controls_width_percent: int = 30,
|
|
54
97
|
continuous: bool = False,
|
|
55
98
|
suppress_warnings: bool = False,
|
|
56
99
|
):
|
|
57
|
-
if not isinstance(viewer, InteractiveViewer): # type: ignore
|
|
58
|
-
raise TypeError(
|
|
59
|
-
f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
100
|
self.viewer = viewer
|
|
63
|
-
self.config =
|
|
101
|
+
self.config = LayoutConfig(
|
|
102
|
+
controls_position=controls_position,
|
|
103
|
+
figure_width=figure_width,
|
|
104
|
+
figure_height=figure_height,
|
|
105
|
+
controls_width_percent=controls_width_percent,
|
|
106
|
+
)
|
|
64
107
|
self.continuous = continuous
|
|
65
108
|
self.suppress_warnings = suppress_warnings
|
|
66
109
|
|
|
67
110
|
# Initialize containers
|
|
111
|
+
self.backend_type = get_backend_type()
|
|
112
|
+
if self.backend_type not in ["inline", "widget"]:
|
|
113
|
+
warnings.warn(
|
|
114
|
+
"The current backend is not supported. Please use %matplotlib widget or %matplotlib inline.\n"
|
|
115
|
+
"The behavior of the viewer will almost definitely not work as expected."
|
|
116
|
+
)
|
|
68
117
|
self.parameter_widgets: Dict[str, BaseWidget] = {}
|
|
69
|
-
self.layout_widgets = self._create_layout_controls()
|
|
70
118
|
self.plot_output = widgets.Output()
|
|
71
119
|
|
|
72
|
-
#
|
|
73
|
-
self.
|
|
120
|
+
# Create layout for controls
|
|
121
|
+
self.layout_widgets = self._create_layout_controls()
|
|
122
|
+
|
|
74
123
|
# Flag to prevent circular updates
|
|
75
124
|
self._updating = False
|
|
76
125
|
|
|
126
|
+
# Last figure to close when new figures are created
|
|
127
|
+
self._last_figure = None
|
|
128
|
+
|
|
77
129
|
def _create_layout_controls(self) -> Dict[str, widgets.Widget]:
|
|
78
130
|
"""Create widgets for controlling the layout."""
|
|
79
131
|
controls: Dict[str, widgets.Widget] = {}
|
|
@@ -89,9 +141,6 @@ class NotebookDeployment:
|
|
|
89
141
|
layout=widgets.Layout(width="95%"),
|
|
90
142
|
style={"description_width": "initial"},
|
|
91
143
|
)
|
|
92
|
-
controls["controls_width"].observe(
|
|
93
|
-
self._handle_container_width_change, names="value"
|
|
94
|
-
)
|
|
95
144
|
|
|
96
145
|
return controls
|
|
97
146
|
|
|
@@ -106,6 +155,7 @@ class NotebookDeployment:
|
|
|
106
155
|
# Store in widget dict
|
|
107
156
|
self.parameter_widgets[name] = widget
|
|
108
157
|
|
|
158
|
+
@debounce(0.1)
|
|
109
159
|
def _handle_widget_engagement(self, name: str) -> None:
|
|
110
160
|
"""Handle engagement with an interactive widget."""
|
|
111
161
|
if self._updating:
|
|
@@ -128,12 +178,12 @@ class NotebookDeployment:
|
|
|
128
178
|
|
|
129
179
|
if widget._is_action:
|
|
130
180
|
parameter = self.viewer.parameters[name]
|
|
131
|
-
parameter.callback(self.viewer.
|
|
181
|
+
parameter.callback(self.viewer.state)
|
|
132
182
|
else:
|
|
133
183
|
self.viewer.set_parameter_value(name, widget.value)
|
|
134
184
|
|
|
135
185
|
# Update any widgets that changed due to dependencies
|
|
136
|
-
self._sync_widgets_with_state(
|
|
186
|
+
self._sync_widgets_with_state()
|
|
137
187
|
|
|
138
188
|
# Update the plot
|
|
139
189
|
self._update_plot()
|
|
@@ -172,30 +222,34 @@ class NotebookDeployment:
|
|
|
172
222
|
|
|
173
223
|
def _update_plot(self) -> None:
|
|
174
224
|
"""Update the plot with current state."""
|
|
175
|
-
state = self.viewer.
|
|
225
|
+
state = self.viewer.state
|
|
176
226
|
|
|
177
227
|
with _plot_context():
|
|
178
|
-
|
|
179
|
-
plt.close(self._current_figure) # Close old figure
|
|
180
|
-
self._current_figure = new_fig
|
|
228
|
+
figure = self.viewer.plot(state)
|
|
181
229
|
|
|
182
|
-
|
|
230
|
+
# Close the last figure if it exists to keep matplotlib clean
|
|
231
|
+
# (just moved this from after clear_output.... noting!)
|
|
232
|
+
if self._last_figure is not None:
|
|
233
|
+
plt.close(self._last_figure)
|
|
183
234
|
|
|
184
|
-
def _redraw_plot(self) -> None:
|
|
185
|
-
"""Clear and redraw the plot in the output widget."""
|
|
186
235
|
self.plot_output.clear_output(wait=True)
|
|
187
236
|
with self.plot_output:
|
|
188
|
-
|
|
237
|
+
if self.backend_type == "inline":
|
|
238
|
+
display(figure)
|
|
239
|
+
|
|
240
|
+
# Also required to make sure a second figure window isn't opened
|
|
241
|
+
plt.close(figure)
|
|
242
|
+
|
|
243
|
+
elif self.backend_type == "widget":
|
|
244
|
+
display(figure.canvas)
|
|
245
|
+
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError(f"Unsupported backend type: {self.backend_type}")
|
|
248
|
+
|
|
249
|
+
self._last_figure = figure
|
|
189
250
|
|
|
190
251
|
def _create_layout(self) -> widgets.Widget:
|
|
191
252
|
"""Create the main layout combining controls and plot."""
|
|
192
|
-
# Create layout controls section
|
|
193
|
-
layout_box = widgets.VBox(
|
|
194
|
-
[widgets.HTML("<b>Layout Controls</b>")]
|
|
195
|
-
+ list(self.layout_widgets.values()),
|
|
196
|
-
layout=widgets.Layout(margin="10px 0px"),
|
|
197
|
-
)
|
|
198
|
-
|
|
199
253
|
# Set up parameter widgets with their observe callbacks
|
|
200
254
|
for name, widget in self.parameter_widgets.items():
|
|
201
255
|
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
@@ -208,8 +262,19 @@ class NotebookDeployment:
|
|
|
208
262
|
)
|
|
209
263
|
|
|
210
264
|
# Combine all controls
|
|
265
|
+
if self.config.is_horizontal:
|
|
266
|
+
# Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
|
|
267
|
+
layout_box = widgets.VBox(
|
|
268
|
+
[widgets.HTML("<b>Layout Controls</b>")]
|
|
269
|
+
+ list(self.layout_widgets.values()),
|
|
270
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
271
|
+
)
|
|
272
|
+
widgets_elements = [param_box, layout_box]
|
|
273
|
+
else:
|
|
274
|
+
widgets_elements = [param_box]
|
|
275
|
+
|
|
211
276
|
self.widgets_container = widgets.VBox(
|
|
212
|
-
|
|
277
|
+
widgets_elements,
|
|
213
278
|
layout=widgets.Layout(
|
|
214
279
|
width=(
|
|
215
280
|
f"{self.config.controls_width_percent}%"
|
|
@@ -217,7 +282,9 @@ class NotebookDeployment:
|
|
|
217
282
|
else "100%"
|
|
218
283
|
),
|
|
219
284
|
padding="10px",
|
|
220
|
-
overflow_y="
|
|
285
|
+
overflow_y="scroll",
|
|
286
|
+
border="1px solid #e5e7eb",
|
|
287
|
+
border_radius="4px 4px 0px 0px",
|
|
221
288
|
),
|
|
222
289
|
)
|
|
223
290
|
|
|
@@ -237,18 +304,27 @@ class NotebookDeployment:
|
|
|
237
304
|
# Create final layout based on configuration
|
|
238
305
|
if self.config.controls_position == "left":
|
|
239
306
|
return widgets.HBox([self.widgets_container, self.plot_container])
|
|
307
|
+
elif self.config.controls_position == "right":
|
|
308
|
+
return widgets.HBox([self.plot_container, self.widgets_container])
|
|
309
|
+
elif self.config.controls_position == "bottom":
|
|
310
|
+
return widgets.VBox([self.plot_container, self.widgets_container])
|
|
240
311
|
else:
|
|
241
312
|
return widgets.VBox([self.widgets_container, self.plot_container])
|
|
242
313
|
|
|
243
314
|
def deploy(self) -> None:
|
|
244
315
|
"""Deploy the interactive viewer with proper state management."""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
316
|
+
self.backend_type = get_backend_type()
|
|
317
|
+
|
|
318
|
+
# We used to use the deploy_app context, but notebook deployment works
|
|
319
|
+
# differently because it's asynchronous and this doesn't really behave
|
|
320
|
+
# as intended. (e.g. with self.viewer._deploy_app() ...)
|
|
321
|
+
|
|
322
|
+
# Create widgets
|
|
323
|
+
self._create_parameter_widgets()
|
|
248
324
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
325
|
+
# Create and display layout
|
|
326
|
+
self.layout = self._create_layout()
|
|
327
|
+
display(self.layout)
|
|
252
328
|
|
|
253
|
-
|
|
254
|
-
|
|
329
|
+
# Create initial plot
|
|
330
|
+
self._update_plot()
|