syd 0.1.3__py3-none-any.whl → 0.1.5__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 +15 -1
- syd/interactive_viewer.py +1139 -81
- syd/notebook_deploy/__init__.py +1 -0
- syd/notebook_deploy/deployer.py +237 -0
- syd/notebook_deploy/widgets.py +471 -0
- syd/parameters.py +1135 -154
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/METADATA +10 -2
- syd-0.1.5.dist-info/RECORD +10 -0
- syd/notebook_deploy.py +0 -277
- syd-0.1.3.dist-info/RECORD +0 -8
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/WHEEL +0 -0
- {syd-0.1.3.dist-info → syd-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .deployer import NotebookDeployment
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional, cast
|
|
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
|
+
|
|
8
|
+
from ..interactive_viewer import InteractiveViewer
|
|
9
|
+
from .widgets import BaseParameterWidget, create_parameter_widget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@contextmanager
|
|
13
|
+
def _plot_context():
|
|
14
|
+
plt.ioff()
|
|
15
|
+
try:
|
|
16
|
+
yield
|
|
17
|
+
finally:
|
|
18
|
+
plt.ion()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class LayoutConfig:
|
|
23
|
+
"""Configuration for the viewer layout."""
|
|
24
|
+
|
|
25
|
+
controls_position: str = "left" # Options are: 'left', 'top'
|
|
26
|
+
figure_width: float = 8.0
|
|
27
|
+
figure_height: float = 6.0
|
|
28
|
+
controls_width_percent: int = 30
|
|
29
|
+
continuous_update: bool = False
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
valid_positions = ["left", "top"]
|
|
33
|
+
if self.controls_position not in valid_positions:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_horizontal(self) -> bool:
|
|
40
|
+
return self.controls_position == "left"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotebookDeployment:
|
|
44
|
+
"""
|
|
45
|
+
A deployment system for InteractiveViewer in Jupyter notebooks using ipywidgets.
|
|
46
|
+
Built around the parameter widget system for clean separation of concerns.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
viewer: InteractiveViewer,
|
|
52
|
+
layout_config: Optional[LayoutConfig] = None,
|
|
53
|
+
continuous_update: bool = False,
|
|
54
|
+
):
|
|
55
|
+
if not isinstance(viewer, InteractiveViewer): # type: ignore
|
|
56
|
+
raise TypeError(
|
|
57
|
+
f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.viewer = viewer
|
|
61
|
+
self.config = layout_config or LayoutConfig()
|
|
62
|
+
self.continuous_update = continuous_update
|
|
63
|
+
|
|
64
|
+
# Initialize containers
|
|
65
|
+
self.parameter_widgets: Dict[str, BaseParameterWidget] = {}
|
|
66
|
+
self.layout_widgets = self._create_layout_controls()
|
|
67
|
+
self.plot_output = widgets.Output()
|
|
68
|
+
|
|
69
|
+
# Store current figure
|
|
70
|
+
self._current_figure = None
|
|
71
|
+
# Flag to prevent circular updates
|
|
72
|
+
self._updating = False
|
|
73
|
+
|
|
74
|
+
def _create_layout_controls(self) -> Dict[str, widgets.Widget]:
|
|
75
|
+
"""Create widgets for controlling the layout."""
|
|
76
|
+
controls: Dict[str, widgets.Widget] = {}
|
|
77
|
+
|
|
78
|
+
# Controls width slider for horizontal layouts
|
|
79
|
+
if self.config.is_horizontal:
|
|
80
|
+
controls["controls_width"] = widgets.IntSlider(
|
|
81
|
+
value=self.config.controls_width_percent,
|
|
82
|
+
min=20,
|
|
83
|
+
max=80,
|
|
84
|
+
description="Controls Width %",
|
|
85
|
+
continuous_update=True,
|
|
86
|
+
layout=widgets.Layout(width="95%"),
|
|
87
|
+
style={"description_width": "initial"},
|
|
88
|
+
)
|
|
89
|
+
controls["controls_width"].observe(
|
|
90
|
+
self._handle_container_width_change, names="value"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return controls
|
|
94
|
+
|
|
95
|
+
def _create_parameter_widgets(self) -> None:
|
|
96
|
+
"""Create widget instances for all parameters."""
|
|
97
|
+
for name, param in self.viewer.parameters.items():
|
|
98
|
+
widget = create_parameter_widget(
|
|
99
|
+
param,
|
|
100
|
+
continuous_update=self.continuous_update,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Store in widget dict
|
|
104
|
+
self.parameter_widgets[name] = widget
|
|
105
|
+
|
|
106
|
+
def _handle_parameter_change(self, name: str) -> None:
|
|
107
|
+
"""Handle changes to parameter widgets."""
|
|
108
|
+
if self._updating:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
self._updating = True
|
|
113
|
+
widget = self.parameter_widgets[name]
|
|
114
|
+
|
|
115
|
+
if hasattr(widget, "_is_button") and widget._is_button:
|
|
116
|
+
parameter = self.viewer.parameters[name]
|
|
117
|
+
parameter.callback(parameter)
|
|
118
|
+
else:
|
|
119
|
+
self.viewer.set_parameter_value(name, widget.value)
|
|
120
|
+
|
|
121
|
+
# Update any widgets that changed due to dependencies
|
|
122
|
+
self._sync_widgets_with_state(exclude=name)
|
|
123
|
+
|
|
124
|
+
# Update the plot
|
|
125
|
+
self._update_plot()
|
|
126
|
+
|
|
127
|
+
finally:
|
|
128
|
+
self._updating = False
|
|
129
|
+
|
|
130
|
+
def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
|
|
131
|
+
"""Sync widget values with viewer state."""
|
|
132
|
+
for name, parameter in self.viewer.parameters.items():
|
|
133
|
+
if name == exclude:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
widget = self.parameter_widgets[name]
|
|
137
|
+
if not widget.matches_parameter(parameter):
|
|
138
|
+
widget.update_from_parameter(parameter)
|
|
139
|
+
|
|
140
|
+
def _handle_figure_size_change(self, change: Dict[str, Any]) -> None:
|
|
141
|
+
"""Handle changes to figure dimensions."""
|
|
142
|
+
if self._current_figure is None:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
self._redraw_plot()
|
|
146
|
+
|
|
147
|
+
def _handle_container_width_change(self, change: Dict[str, Any]) -> None:
|
|
148
|
+
"""Handle changes to container width proportions."""
|
|
149
|
+
width_percent = self.layout_widgets["controls_width"].value
|
|
150
|
+
self.config.controls_width_percent = width_percent
|
|
151
|
+
|
|
152
|
+
# Update container widths
|
|
153
|
+
self.widgets_container.layout.width = f"{width_percent}%"
|
|
154
|
+
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
155
|
+
|
|
156
|
+
def _update_plot(self) -> None:
|
|
157
|
+
"""Update the plot with current state."""
|
|
158
|
+
state = self.viewer.get_state()
|
|
159
|
+
|
|
160
|
+
with _plot_context():
|
|
161
|
+
new_fig = self.viewer.plot(state)
|
|
162
|
+
plt.close(self._current_figure) # Close old figure
|
|
163
|
+
self._current_figure = new_fig
|
|
164
|
+
|
|
165
|
+
self._redraw_plot()
|
|
166
|
+
|
|
167
|
+
def _redraw_plot(self) -> None:
|
|
168
|
+
"""Clear and redraw the plot in the output widget."""
|
|
169
|
+
self.plot_output.clear_output(wait=True)
|
|
170
|
+
with self.plot_output:
|
|
171
|
+
display(self._current_figure)
|
|
172
|
+
|
|
173
|
+
def _create_layout(self) -> widgets.Widget:
|
|
174
|
+
"""Create the main layout combining controls and plot."""
|
|
175
|
+
# Create layout controls section
|
|
176
|
+
layout_box = widgets.VBox(
|
|
177
|
+
[widgets.HTML("<b>Layout Controls</b>")]
|
|
178
|
+
+ list(self.layout_widgets.values()),
|
|
179
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Set up parameter widgets with their observe callbacks
|
|
183
|
+
for name, widget in self.parameter_widgets.items():
|
|
184
|
+
widget.observe(lambda change, n=name: self._handle_parameter_change(n))
|
|
185
|
+
|
|
186
|
+
# Create parameter controls section
|
|
187
|
+
param_box = widgets.VBox(
|
|
188
|
+
[widgets.HTML("<b>Parameters</b>")]
|
|
189
|
+
+ [w.widget for w in self.parameter_widgets.values()],
|
|
190
|
+
layout=widgets.Layout(margin="10px 0px"),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Combine all controls
|
|
194
|
+
self.widgets_container = widgets.VBox(
|
|
195
|
+
[param_box, layout_box],
|
|
196
|
+
layout=widgets.Layout(
|
|
197
|
+
width=(
|
|
198
|
+
f"{self.config.controls_width_percent}%"
|
|
199
|
+
if self.config.is_horizontal
|
|
200
|
+
else "100%"
|
|
201
|
+
),
|
|
202
|
+
padding="10px",
|
|
203
|
+
overflow_y="auto",
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Create plot container
|
|
208
|
+
self.plot_container = widgets.VBox(
|
|
209
|
+
[self.plot_output],
|
|
210
|
+
layout=widgets.Layout(
|
|
211
|
+
width=(
|
|
212
|
+
f"{100 - self.config.controls_width_percent}%"
|
|
213
|
+
if self.config.is_horizontal
|
|
214
|
+
else "100%"
|
|
215
|
+
),
|
|
216
|
+
padding="10px",
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Create final layout based on configuration
|
|
221
|
+
if self.config.controls_position == "left":
|
|
222
|
+
return widgets.HBox([self.widgets_container, self.plot_container])
|
|
223
|
+
else:
|
|
224
|
+
return widgets.VBox([self.widgets_container, self.plot_container])
|
|
225
|
+
|
|
226
|
+
def deploy(self) -> None:
|
|
227
|
+
"""Deploy the interactive viewer with proper state management."""
|
|
228
|
+
with self.viewer.deploy_app():
|
|
229
|
+
# Create widgets
|
|
230
|
+
self._create_parameter_widgets()
|
|
231
|
+
|
|
232
|
+
# Create and display layout
|
|
233
|
+
layout = self._create_layout()
|
|
234
|
+
display(layout)
|
|
235
|
+
|
|
236
|
+
# Create initial plot
|
|
237
|
+
self._update_plot()
|