syd 0.1.7__py3-none-any.whl → 1.0.0__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.
@@ -0,0 +1 @@
1
+ # This file exists to make the directory a proper Python package
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>{{ title }}</title>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
8
+ </head>
9
+ <body>
10
+ <div class="viewer-container" data-controls-position="{{ config.controls_position }}">
11
+ <div class="controls-container" data-width-percent="{{ config.controls_width_percent }}"
12
+ {% if config.is_horizontal %}style="width: {{ config.controls_width_percent }}%;"{% else %}style="height: {{ config.controls_width_percent }}%;"{% endif %}>
13
+ <div id="controls-container">
14
+ <!-- Controls will be dynamically generated via JavaScript -->
15
+ </div>
16
+ </div>
17
+
18
+ <div class="plot-container"
19
+ {% if config.is_horizontal %}style="width: calc(100% - {{ config.controls_width_percent }}%);"{% else %}style="height: calc(100% - {{ config.controls_width_percent }}%);"{% endif %}>
20
+ <img id="plot-image" width="100%" height="100%">
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Store config as data attributes for JS to access -->
25
+ <div id="viewer-config"
26
+ data-figure-width="{{ config.figure_width }}"
27
+ data-figure-height="{{ config.figure_height }}"
28
+ data-controls-position="{{ config.controls_position }}"
29
+ data-controls-width-percent="{{ config.controls_width_percent }}"
30
+ style="display:none;"></div>
31
+
32
+ <script src="{{ url_for('static', filename='js/viewer.js') }}"></script>
33
+ </body>
34
+ </html>
@@ -57,7 +57,7 @@ class TestFlaskDeployerComponents(unittest.TestCase):
57
57
  def test_parameter_update_state_sync():
58
58
  # Create real viewer with test parameters
59
59
  viewer = Viewer()
60
- viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
60
+ viewer.add_float('test_param', value=1.0, min=0, max=10)
61
61
 
62
62
  # Create deployer with this viewer
63
63
  deployer = FlaskDeployer(viewer)
@@ -82,7 +82,7 @@ def test_parameter_update_state_sync():
82
82
  ```python
83
83
  def test_update_parameter_endpoint():
84
84
  viewer = Viewer()
85
- viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
85
+ viewer.add_float('test_param', value=1.0, min=0, max=10)
86
86
  deployer = FlaskDeployer(viewer)
87
87
  app = deployer.app
88
88
 
@@ -218,8 +218,8 @@ def standard_test_viewer():
218
218
  return fig
219
219
 
220
220
  viewer.set_plot(plot)
221
- viewer.add_float('amplitude', value=1.0, min_value=0, max_value=2)
222
- viewer.add_float('frequency', value=1.0, min_value=0.1, max_value=5)
221
+ viewer.add_float('amplitude', value=1.0, min=0, max=2)
222
+ viewer.add_float('frequency', value=1.0, min=0.1, max=5)
223
223
 
224
224
  return viewer
225
225
 
@@ -1 +0,0 @@
1
- from .deployer import NotebookDeployer
@@ -1,27 +1,13 @@
1
- from typing import Dict, Any, Optional
1
+ from typing import Literal, Optional
2
2
  import warnings
3
- from functools import wraps
4
- from dataclasses import dataclass
5
- from contextlib import contextmanager
6
- from time import time
7
-
8
3
  import ipywidgets as widgets
9
4
  from IPython.display import display
10
5
  import matplotlib as mpl
11
6
  import matplotlib.pyplot as plt
12
7
 
13
- from ..parameters import ParameterUpdateWarning
8
+ from ..support import ParameterUpdateWarning, plot_context
14
9
  from ..viewer import Viewer
15
- from .widgets import BaseWidget, create_widget
16
-
17
-
18
- @contextmanager
19
- def _plot_context():
20
- plt.ioff()
21
- try:
22
- yield
23
- finally:
24
- plt.ion()
10
+ from .widgets import create_widget, BaseWidget
25
11
 
26
12
 
27
13
  def get_backend_type():
@@ -40,47 +26,6 @@ def get_backend_type():
40
26
  return "other"
41
27
 
42
28
 
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
-
63
- @dataclass
64
- class LayoutConfig:
65
- """Configuration for the viewer layout."""
66
-
67
- controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
68
- figure_width: float = 8.0
69
- figure_height: float = 6.0
70
- controls_width_percent: int = 30
71
-
72
- def __post_init__(self):
73
- valid_positions = ["left", "top", "right", "bottom"]
74
- if self.controls_position not in valid_positions:
75
- raise ValueError(
76
- f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
77
- )
78
-
79
- @property
80
- def is_horizontal(self) -> bool:
81
- return self.controls_position == "left" or self.controls_position == "right"
82
-
83
-
84
29
  class NotebookDeployer:
85
30
  """
86
31
  A deployment system for Viewer in Jupyter notebooks using ipywidgets.
@@ -90,74 +35,127 @@ class NotebookDeployer:
90
35
  def __init__(
91
36
  self,
92
37
  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,
38
+ controls_position: Literal["left", "top", "right", "bottom"] = "left",
39
+ controls_width_percent: int = 20,
97
40
  continuous: bool = False,
98
- suppress_warnings: bool = False,
41
+ suppress_warnings: bool = True,
99
42
  ):
100
43
  self.viewer = viewer
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
- )
107
- self.continuous = continuous
44
+ self.components: dict[str, BaseWidget] = {}
108
45
  self.suppress_warnings = suppress_warnings
46
+ self._updating = False # Flag to check circular updates
47
+ self.controls_position = controls_position
48
+ self.controls_width_percent = controls_width_percent
49
+ self.continuous = continuous
109
50
 
110
51
  # Initialize containers
111
52
  self.backend_type = get_backend_type()
112
53
  if self.backend_type not in ["inline", "widget"]:
113
54
  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."
55
+ f"The current backend ({self.backend_type}) is not supported. Please use %matplotlib widget or %matplotlib inline.\n"
56
+ "The behavior of the viewer will almost definitely not work as expected!"
116
57
  )
117
- self.parameter_widgets: Dict[str, BaseWidget] = {}
118
- self.plot_output = widgets.Output()
58
+ self._last_figure = None
119
59
 
120
- # Create layout for controls
121
- self.layout_widgets = self._create_layout_controls()
60
+ def deploy(self) -> None:
61
+ """Deploy the viewer."""
62
+ self.build_components()
63
+ self.build_layout()
64
+ self.backend_type = get_backend_type()
65
+ display(self.layout)
66
+ self.update_plot()
122
67
 
123
- # Flag to prevent circular updates
124
- self._updating = False
68
+ def build_components(self) -> None:
69
+ """Create widget instances for all parameters and equip callbacks."""
70
+ for name, param in self.viewer.parameters.items():
71
+ widget = create_widget(param, continuous=self.continuous)
72
+ self.components[name] = widget
73
+ callback = lambda _, n=name: self.handle_component_engagement(n)
74
+ widget.observe(callback)
125
75
 
126
- # Last figure to close when new figures are created
127
- self._last_figure = None
76
+ def build_layout(self) -> None:
77
+ """Create the main layout combining controls and plot."""
128
78
 
129
- def _create_layout_controls(self) -> Dict[str, widgets.Widget]:
130
- """Create widgets for controlling the layout."""
131
- controls: Dict[str, widgets.Widget] = {}
79
+ self.plot_output = widgets.Output()
132
80
 
133
81
  # Controls width slider for horizontal layouts
134
- if self.config.is_horizontal:
135
- controls["controls_width"] = widgets.IntSlider(
136
- value=self.config.controls_width_percent,
137
- min=20,
138
- max=80,
82
+ self.controls = {}
83
+ if self.controls_position in ["left", "right"]:
84
+ self.controls["controls_width"] = widgets.IntSlider(
85
+ value=self.controls_width_percent,
86
+ min=10,
87
+ max=50,
139
88
  description="Controls Width %",
140
89
  continuous=True,
141
90
  layout=widgets.Layout(width="95%"),
142
91
  style={"description_width": "initial"},
143
92
  )
144
93
 
145
- return controls
94
+ # Create parameter controls section
95
+ param_box = widgets.VBox(
96
+ [widgets.HTML("<b>Parameters</b>")]
97
+ + [w.widget for w in self.components.values()],
98
+ layout=widgets.Layout(margin="10px 0px"),
99
+ )
146
100
 
147
- def _create_parameter_widgets(self) -> None:
148
- """Create widget instances for all parameters."""
149
- for name, param in self.viewer.parameters.items():
150
- widget = create_widget(
151
- param,
152
- continuous=self.continuous,
101
+ # Combine all controls
102
+ if self.controls_position in ["left", "right"]:
103
+ # Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
104
+ layout_box = widgets.VBox(
105
+ [widgets.HTML("<b>Syd Controls</b>")] + list(self.controls.values()),
106
+ layout=widgets.Layout(margin="10px 0px"),
153
107
  )
154
108
 
155
- # Store in widget dict
156
- self.parameter_widgets[name] = widget
109
+ # Register the controls_width slider's observer
110
+ if "controls_width" in self.controls:
111
+ self.controls["controls_width"].observe(
112
+ self._handle_container_width_change, names="value"
113
+ )
114
+
115
+ widgets_elements = [param_box, layout_box]
116
+ else:
117
+ widgets_elements = [param_box]
118
+
119
+ self.widgets_container = widgets.VBox(
120
+ widgets_elements,
121
+ layout=widgets.Layout(
122
+ width=(
123
+ f"{self.controls_width_percent}%"
124
+ if self.controls_position in ["left", "right"]
125
+ else "100%"
126
+ ),
127
+ padding="10px",
128
+ overflow_y="scroll",
129
+ border="1px solid #e5e7eb",
130
+ border_radius="4px 4px 0px 0px",
131
+ ),
132
+ )
133
+
134
+ # Create plot container
135
+ self.plot_container = widgets.VBox(
136
+ [self.plot_output],
137
+ layout=widgets.Layout(
138
+ width=(
139
+ f"{100 - self.controls_width_percent}%"
140
+ if self.controls_position in ["left", "right"]
141
+ else "100%"
142
+ ),
143
+ padding="10px",
144
+ ),
145
+ )
146
+
147
+ # Create final layout based on configuration
148
+ if self.controls_position == "left":
149
+ self.layout = widgets.HBox([self.widgets_container, self.plot_container])
150
+ elif self.controls_position == "right":
151
+ self.layout = widgets.HBox([self.plot_container, self.widgets_container])
152
+ elif self.controls_position == "bottom":
153
+ self.layout = widgets.VBox([self.plot_container, self.widgets_container])
154
+ else:
155
+ self.layout = widgets.VBox([self.widgets_container, self.plot_container])
157
156
 
158
- @debounce(0.1)
159
- def _handle_widget_engagement(self, name: str) -> None:
160
- """Handle engagement with an interactive widget."""
157
+ def handle_component_engagement(self, name: str) -> None:
158
+ """Handle engagement with an interactive component."""
161
159
  if self._updating:
162
160
  print(
163
161
  "Already updating -- there's a circular dependency!"
@@ -174,61 +172,46 @@ class NotebookDeployer:
174
172
  if self.suppress_warnings:
175
173
  warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
176
174
 
177
- widget = self.parameter_widgets[name]
178
-
179
- if widget._is_action:
175
+ # Get the component
176
+ component = self.components[name]
177
+ if component.is_action:
178
+ # If the component is an action, call the callback
180
179
  parameter = self.viewer.parameters[name]
181
180
  parameter.callback(self.viewer.state)
182
181
  else:
183
- self.viewer.set_parameter_value(name, widget.value)
182
+ # Otherwise, update the parameter value
183
+ self.viewer.set_parameter_value(name, component.value)
184
184
 
185
- # Update any widgets that changed due to dependencies
186
- self._sync_widgets_with_state()
185
+ # Update any components that changed due to dependencies
186
+ self.sync_components_with_state()
187
187
 
188
188
  # Update the plot
189
- self._update_plot()
189
+ self.update_plot()
190
190
 
191
191
  finally:
192
192
  self._updating = False
193
193
 
194
- def _handle_action(self, name: str) -> None:
195
- """Handle actions for parameter widgets."""
196
-
197
- def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
198
- """Sync widget values with viewer state."""
194
+ def sync_components_with_state(self, exclude: Optional[str] = None) -> None:
195
+ """Sync component values with viewer state."""
199
196
  for name, parameter in self.viewer.parameters.items():
200
197
  if name == exclude:
201
198
  continue
202
199
 
203
- widget = self.parameter_widgets[name]
204
- if not widget.matches_parameter(parameter):
205
- widget.update_from_parameter(parameter)
206
-
207
- def _handle_figure_size_change(self, change: Dict[str, Any]) -> None:
208
- """Handle changes to figure dimensions."""
209
- if self._current_figure is None:
210
- return
211
-
212
- self._redraw_plot()
213
-
214
- def _handle_container_width_change(self, change: Dict[str, Any]) -> None:
215
- """Handle changes to container width proportions."""
216
- width_percent = self.layout_widgets["controls_width"].value
217
- self.config.controls_width_percent = width_percent
200
+ component = self.components[name]
201
+ if not component.matches_parameter(parameter):
202
+ component.update_from_parameter(parameter)
218
203
 
219
- # Update container widths
220
- self.widgets_container.layout.width = f"{width_percent}%"
221
- self.plot_container.layout.width = f"{100 - width_percent}%"
222
-
223
- def _update_plot(self) -> None:
204
+ def update_plot(self) -> None:
224
205
  """Update the plot with current state."""
225
206
  state = self.viewer.state
226
207
 
227
- with _plot_context():
208
+ with plot_context():
228
209
  figure = self.viewer.plot(state)
229
210
 
211
+ # Update components if plot function updated a parameter
212
+ self.sync_components_with_state()
213
+
230
214
  # Close the last figure if it exists to keep matplotlib clean
231
- # (just moved this from after clear_output.... noting!)
232
215
  if self._last_figure is not None:
233
216
  plt.close(self._last_figure)
234
217
 
@@ -248,83 +231,11 @@ class NotebookDeployer:
248
231
 
249
232
  self._last_figure = figure
250
233
 
251
- def _create_layout(self) -> widgets.Widget:
252
- """Create the main layout combining controls and plot."""
253
- # Set up parameter widgets with their observe callbacks
254
- for name, widget in self.parameter_widgets.items():
255
- widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
256
-
257
- # Create parameter controls section
258
- param_box = widgets.VBox(
259
- [widgets.HTML("<b>Parameters</b>")]
260
- + [w.widget for w in self.parameter_widgets.values()],
261
- layout=widgets.Layout(margin="10px 0px"),
262
- )
263
-
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
-
276
- self.widgets_container = widgets.VBox(
277
- widgets_elements,
278
- layout=widgets.Layout(
279
- width=(
280
- f"{self.config.controls_width_percent}%"
281
- if self.config.is_horizontal
282
- else "100%"
283
- ),
284
- padding="10px",
285
- overflow_y="scroll",
286
- border="1px solid #e5e7eb",
287
- border_radius="4px 4px 0px 0px",
288
- ),
289
- )
290
-
291
- # Create plot container
292
- self.plot_container = widgets.VBox(
293
- [self.plot_output],
294
- layout=widgets.Layout(
295
- width=(
296
- f"{100 - self.config.controls_width_percent}%"
297
- if self.config.is_horizontal
298
- else "100%"
299
- ),
300
- padding="10px",
301
- ),
302
- )
303
-
304
- # Create final layout based on configuration
305
- if self.config.controls_position == "left":
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])
311
- else:
312
- return widgets.VBox([self.widgets_container, self.plot_container])
313
-
314
- def deploy(self) -> None:
315
- """Deploy the interactive viewer with proper state management."""
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()
324
-
325
- # Create and display layout
326
- self.layout = self._create_layout()
327
- display(self.layout)
234
+ def _handle_container_width_change(self, _) -> None:
235
+ """Handle changes to container width proportions."""
236
+ width_percent = self.controls["controls_width"].value
237
+ self.controls_width_percent = width_percent
328
238
 
329
- # Create initial plot
330
- self._update_plot()
239
+ # Update container widths
240
+ self.widgets_container.layout.width = f"{width_percent}%"
241
+ self.plot_container.layout.width = f"{100 - width_percent}%"