syd 0.2.0__py3-none-any.whl → 1.0.1__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 +1 -1
- syd/flask_deployment/__init__.py +0 -6
- syd/flask_deployment/deployer.py +446 -460
- syd/flask_deployment/static/css/styles.css +50 -16
- syd/flask_deployment/static/js/viewer.js +154 -67
- syd/flask_deployment/templates/index.html +1 -1
- syd/notebook_deployment/__init__.py +0 -1
- syd/notebook_deployment/deployer.py +212 -227
- syd/notebook_deployment/widgets.py +2 -2
- syd/parameters.py +69 -104
- syd/support.py +27 -0
- syd/viewer.py +10 -6
- syd-1.0.1.dist-info/METADATA +228 -0
- syd-1.0.1.dist-info/RECORD +19 -0
- syd-0.2.0.dist-info/METADATA +0 -126
- syd-0.2.0.dist-info/RECORD +0 -19
- {syd-0.2.0.dist-info → syd-1.0.1.dist-info}/WHEEL +0 -0
- {syd-0.2.0.dist-info → syd-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,27 +1,15 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Literal, Optional
|
|
2
2
|
import warnings
|
|
3
|
-
|
|
4
|
-
from dataclasses import dataclass
|
|
3
|
+
import threading
|
|
5
4
|
from contextlib import contextmanager
|
|
6
|
-
from time import time
|
|
7
|
-
|
|
8
5
|
import ipywidgets as widgets
|
|
9
6
|
from IPython.display import display
|
|
10
7
|
import matplotlib as mpl
|
|
11
8
|
import matplotlib.pyplot as plt
|
|
12
9
|
|
|
13
|
-
from ..
|
|
10
|
+
from ..support import ParameterUpdateWarning, plot_context
|
|
14
11
|
from ..viewer import Viewer
|
|
15
|
-
from .widgets import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@contextmanager
|
|
19
|
-
def _plot_context():
|
|
20
|
-
plt.ioff()
|
|
21
|
-
try:
|
|
22
|
-
yield
|
|
23
|
-
finally:
|
|
24
|
-
plt.ion()
|
|
12
|
+
from .widgets import create_widget, BaseWidget
|
|
25
13
|
|
|
26
14
|
|
|
27
15
|
def get_backend_type():
|
|
@@ -40,47 +28,6 @@ def get_backend_type():
|
|
|
40
28
|
return "other"
|
|
41
29
|
|
|
42
30
|
|
|
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 = 20
|
|
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
31
|
class NotebookDeployer:
|
|
85
32
|
"""
|
|
86
33
|
A deployment system for Viewer in Jupyter notebooks using ipywidgets.
|
|
@@ -90,50 +37,105 @@ class NotebookDeployer:
|
|
|
90
37
|
def __init__(
|
|
91
38
|
self,
|
|
92
39
|
viewer: Viewer,
|
|
93
|
-
controls_position:
|
|
94
|
-
figure_width: float = 8.0,
|
|
95
|
-
figure_height: float = 6.0,
|
|
40
|
+
controls_position: Literal["left", "top", "right", "bottom"] = "left",
|
|
96
41
|
controls_width_percent: int = 20,
|
|
97
42
|
continuous: bool = False,
|
|
98
|
-
suppress_warnings: bool =
|
|
43
|
+
suppress_warnings: bool = True,
|
|
44
|
+
update_threshold: float = 1.0,
|
|
99
45
|
):
|
|
100
46
|
self.viewer = viewer
|
|
101
|
-
self.
|
|
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
|
|
47
|
+
self.components: dict[str, BaseWidget] = {}
|
|
108
48
|
self.suppress_warnings = suppress_warnings
|
|
49
|
+
self._updating = False # Flag to check circular updates
|
|
50
|
+
self.controls_position = controls_position
|
|
51
|
+
self.controls_width_percent = controls_width_percent
|
|
52
|
+
self.continuous = continuous
|
|
109
53
|
|
|
110
54
|
# Initialize containers
|
|
111
55
|
self.backend_type = get_backend_type()
|
|
112
56
|
if self.backend_type not in ["inline", "widget"]:
|
|
113
57
|
warnings.warn(
|
|
114
58
|
f"The current backend ({self.backend_type}) is not supported. Please use %matplotlib widget or %matplotlib inline.\n"
|
|
115
|
-
"The behavior of the viewer will almost definitely not work as expected
|
|
59
|
+
"The behavior of the viewer will almost definitely not work as expected!"
|
|
116
60
|
)
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
61
|
+
self._last_figure = None
|
|
62
|
+
self._update_event = threading.Event()
|
|
63
|
+
self.update_threshold = update_threshold
|
|
64
|
+
self._slow_loading_figure = None
|
|
65
|
+
self._display_lock = threading.Lock() # Lock for synchronizing display updates
|
|
66
|
+
|
|
67
|
+
def _show_slow_loading(self):
|
|
68
|
+
if self.backend_type == "inline":
|
|
69
|
+
if not self._update_event.wait(self.update_threshold):
|
|
70
|
+
if self._slow_loading_figure is None:
|
|
71
|
+
fig = plt.figure()
|
|
72
|
+
ax = fig.add_subplot(111)
|
|
73
|
+
ax.text(
|
|
74
|
+
0.5,
|
|
75
|
+
0.5,
|
|
76
|
+
"waiting for next figure...",
|
|
77
|
+
ha="center",
|
|
78
|
+
va="center",
|
|
79
|
+
fontsize=12,
|
|
80
|
+
weight="bold",
|
|
81
|
+
color="black",
|
|
82
|
+
)
|
|
83
|
+
ax.axis("off")
|
|
84
|
+
self._slow_loading_figure = fig
|
|
85
|
+
if not self._showing_new_figure:
|
|
86
|
+
self._display_figure(self._slow_loading_figure, store_figure=False)
|
|
87
|
+
self._showing_slow_loading_figure = True
|
|
88
|
+
|
|
89
|
+
@contextmanager
|
|
90
|
+
def _perform_update(self):
|
|
91
|
+
self._updating = True
|
|
92
|
+
self._showing_new_figure = False
|
|
93
|
+
self._showing_slow_loading_figure = False
|
|
94
|
+
self._update_event.clear()
|
|
95
|
+
|
|
96
|
+
thread = threading.Thread(target=self._show_slow_loading, daemon=True)
|
|
97
|
+
thread.start()
|
|
119
98
|
|
|
120
|
-
|
|
121
|
-
|
|
99
|
+
try:
|
|
100
|
+
yield
|
|
101
|
+
finally:
|
|
102
|
+
self._updating = False
|
|
103
|
+
self._update_event.set()
|
|
104
|
+
thread.join()
|
|
105
|
+
if self._showing_slow_loading_figure:
|
|
106
|
+
self._display_figure(self._last_figure)
|
|
107
|
+
self._update_status("Ready!")
|
|
108
|
+
|
|
109
|
+
def deploy(self) -> None:
|
|
110
|
+
"""Deploy the viewer."""
|
|
111
|
+
self.backend_type = get_backend_type()
|
|
112
|
+
self.build_components()
|
|
113
|
+
self.build_layout()
|
|
114
|
+
display(self.layout)
|
|
115
|
+
self.update_plot()
|
|
122
116
|
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
def build_components(self) -> None:
|
|
118
|
+
"""Create widget instances for all parameters and equip callbacks."""
|
|
119
|
+
for name, param in self.viewer.parameters.items():
|
|
120
|
+
widget = create_widget(param, continuous=self.continuous)
|
|
121
|
+
self.components[name] = widget
|
|
122
|
+
callback = lambda _, n=name: self.handle_component_engagement(n)
|
|
123
|
+
widget.observe(callback)
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
def build_layout(self) -> None:
|
|
126
|
+
"""Create the main layout combining controls and plot."""
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
"""Create widgets for controlling the layout."""
|
|
131
|
-
controls: Dict[str, widgets.Widget] = {}
|
|
128
|
+
self.plot_output = widgets.Output()
|
|
132
129
|
|
|
133
130
|
# Controls width slider for horizontal layouts
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
self.controls = {}
|
|
132
|
+
self.controls["status"] = widgets.HTML(
|
|
133
|
+
value="<b>Syd Controls</b>",
|
|
134
|
+
layout=widgets.Layout(width="95%"),
|
|
135
|
+
)
|
|
136
|
+
if self.controls_position in ["left", "right"]:
|
|
137
|
+
self.controls["controls_width"] = widgets.IntSlider(
|
|
138
|
+
value=self.controls_width_percent,
|
|
137
139
|
min=10,
|
|
138
140
|
max=50,
|
|
139
141
|
description="Controls Width %",
|
|
@@ -141,144 +143,42 @@ class NotebookDeployer:
|
|
|
141
143
|
layout=widgets.Layout(width="95%"),
|
|
142
144
|
style={"description_width": "initial"},
|
|
143
145
|
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
continuous=self.continuous,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
# Store in widget dict
|
|
156
|
-
self.parameter_widgets[name] = widget
|
|
157
|
-
|
|
158
|
-
@debounce(0.1)
|
|
159
|
-
def _handle_widget_engagement(self, name: str) -> None:
|
|
160
|
-
"""Handle engagement with an interactive widget."""
|
|
161
|
-
if self._updating:
|
|
162
|
-
print(
|
|
163
|
-
"Already updating -- there's a circular dependency!"
|
|
164
|
-
"This is probably caused by failing to disable callbacks for a parameter."
|
|
165
|
-
"It's a bug --- tell the developer on github issues please."
|
|
146
|
+
if self.backend_type == "inline":
|
|
147
|
+
self.controls["update_threshold"] = widgets.FloatSlider(
|
|
148
|
+
value=self.update_threshold,
|
|
149
|
+
min=0.1,
|
|
150
|
+
max=10.0,
|
|
151
|
+
description="Update Threshold",
|
|
152
|
+
layout=widgets.Layout(width="95%"),
|
|
153
|
+
style={"description_width": "initial"},
|
|
166
154
|
)
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
self._updating = True
|
|
171
|
-
|
|
172
|
-
# Optionally suppress warnings during parameter updates
|
|
173
|
-
with warnings.catch_warnings():
|
|
174
|
-
if self.suppress_warnings:
|
|
175
|
-
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
176
|
-
|
|
177
|
-
widget = self.parameter_widgets[name]
|
|
178
|
-
|
|
179
|
-
if widget._is_action:
|
|
180
|
-
parameter = self.viewer.parameters[name]
|
|
181
|
-
parameter.callback(self.viewer.state)
|
|
182
|
-
else:
|
|
183
|
-
self.viewer.set_parameter_value(name, widget.value)
|
|
184
|
-
|
|
185
|
-
# Update any widgets that changed due to dependencies
|
|
186
|
-
self._sync_widgets_with_state()
|
|
187
|
-
|
|
188
|
-
# Update the plot
|
|
189
|
-
self._update_plot()
|
|
190
|
-
|
|
191
|
-
finally:
|
|
192
|
-
self._updating = False
|
|
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."""
|
|
199
|
-
for name, parameter in self.viewer.parameters.items():
|
|
200
|
-
if name == exclude:
|
|
201
|
-
continue
|
|
202
|
-
|
|
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
|
|
218
|
-
|
|
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:
|
|
224
|
-
"""Update the plot with current state."""
|
|
225
|
-
state = self.viewer.state
|
|
226
|
-
|
|
227
|
-
with _plot_context():
|
|
228
|
-
figure = self.viewer.plot(state)
|
|
229
|
-
|
|
230
|
-
# Update widgets if plot function updated a parameter
|
|
231
|
-
self._sync_widgets_with_state()
|
|
232
|
-
|
|
233
|
-
# Close the last figure if it exists to keep matplotlib clean
|
|
234
|
-
# (just moved this from after clear_output.... noting!)
|
|
235
|
-
if self._last_figure is not None:
|
|
236
|
-
plt.close(self._last_figure)
|
|
237
|
-
|
|
238
|
-
self.plot_output.clear_output(wait=True)
|
|
239
|
-
with self.plot_output:
|
|
240
|
-
if self.backend_type == "inline":
|
|
241
|
-
display(figure)
|
|
242
|
-
|
|
243
|
-
# Also required to make sure a second figure window isn't opened
|
|
244
|
-
plt.close(figure)
|
|
245
|
-
|
|
246
|
-
elif self.backend_type == "widget":
|
|
247
|
-
display(figure.canvas)
|
|
248
|
-
|
|
249
|
-
else:
|
|
250
|
-
raise ValueError(f"Unsupported backend type: {self.backend_type}")
|
|
251
|
-
|
|
252
|
-
self._last_figure = figure
|
|
253
|
-
|
|
254
|
-
def _create_layout(self) -> widgets.Widget:
|
|
255
|
-
"""Create the main layout combining controls and plot."""
|
|
256
|
-
# Set up parameter widgets with their observe callbacks
|
|
257
|
-
for name, widget in self.parameter_widgets.items():
|
|
258
|
-
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
259
155
|
|
|
260
156
|
# Create parameter controls section
|
|
261
157
|
param_box = widgets.VBox(
|
|
262
158
|
[widgets.HTML("<b>Parameters</b>")]
|
|
263
|
-
+ [w.widget for w in self.
|
|
159
|
+
+ [w.widget for w in self.components.values()],
|
|
264
160
|
layout=widgets.Layout(margin="10px 0px"),
|
|
265
161
|
)
|
|
266
162
|
|
|
267
163
|
# Combine all controls
|
|
268
|
-
if self.
|
|
164
|
+
if self.controls_position in ["left", "right"]:
|
|
269
165
|
# Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
|
|
270
166
|
layout_box = widgets.VBox(
|
|
271
|
-
|
|
272
|
-
+ list(self.layout_widgets.values()),
|
|
167
|
+
list(self.controls.values()),
|
|
273
168
|
layout=widgets.Layout(margin="10px 0px"),
|
|
274
169
|
)
|
|
275
170
|
|
|
276
171
|
# Register the controls_width slider's observer
|
|
277
|
-
if "controls_width" in self.
|
|
278
|
-
self.
|
|
172
|
+
if "controls_width" in self.controls:
|
|
173
|
+
self.controls["controls_width"].observe(
|
|
279
174
|
self._handle_container_width_change, names="value"
|
|
280
175
|
)
|
|
281
176
|
|
|
177
|
+
if "update_threshold" in self.controls:
|
|
178
|
+
self.controls["update_threshold"].observe(
|
|
179
|
+
self._handle_update_threshold_change, names="value"
|
|
180
|
+
)
|
|
181
|
+
|
|
282
182
|
widgets_elements = [param_box, layout_box]
|
|
283
183
|
else:
|
|
284
184
|
widgets_elements = [param_box]
|
|
@@ -287,8 +187,8 @@ class NotebookDeployer:
|
|
|
287
187
|
widgets_elements,
|
|
288
188
|
layout=widgets.Layout(
|
|
289
189
|
width=(
|
|
290
|
-
f"{self.
|
|
291
|
-
if self.
|
|
190
|
+
f"{self.controls_width_percent}%"
|
|
191
|
+
if self.controls_position in ["left", "right"]
|
|
292
192
|
else "100%"
|
|
293
193
|
),
|
|
294
194
|
padding="10px",
|
|
@@ -303,8 +203,8 @@ class NotebookDeployer:
|
|
|
303
203
|
[self.plot_output],
|
|
304
204
|
layout=widgets.Layout(
|
|
305
205
|
width=(
|
|
306
|
-
f"{100 - self.
|
|
307
|
-
if self.
|
|
206
|
+
f"{100 - self.controls_width_percent}%"
|
|
207
|
+
if self.controls_position in ["left", "right"]
|
|
308
208
|
else "100%"
|
|
309
209
|
),
|
|
310
210
|
padding="10px",
|
|
@@ -312,29 +212,114 @@ class NotebookDeployer:
|
|
|
312
212
|
)
|
|
313
213
|
|
|
314
214
|
# Create final layout based on configuration
|
|
315
|
-
if self.
|
|
316
|
-
|
|
317
|
-
elif self.
|
|
318
|
-
|
|
319
|
-
elif self.
|
|
320
|
-
|
|
215
|
+
if self.controls_position == "left":
|
|
216
|
+
self.layout = widgets.HBox([self.widgets_container, self.plot_container])
|
|
217
|
+
elif self.controls_position == "right":
|
|
218
|
+
self.layout = widgets.HBox([self.plot_container, self.widgets_container])
|
|
219
|
+
elif self.controls_position == "bottom":
|
|
220
|
+
self.layout = widgets.VBox([self.plot_container, self.widgets_container])
|
|
321
221
|
else:
|
|
322
|
-
|
|
222
|
+
self.layout = widgets.VBox([self.widgets_container, self.plot_container])
|
|
323
223
|
|
|
324
|
-
|
|
325
|
-
"""Deploy the interactive viewer with proper state management."""
|
|
326
|
-
self.backend_type = get_backend_type()
|
|
224
|
+
self._update_status("Ready!")
|
|
327
225
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
226
|
+
def handle_component_engagement(self, name: str) -> None:
|
|
227
|
+
"""Handle engagement with an interactive component."""
|
|
228
|
+
if self._updating:
|
|
229
|
+
print(
|
|
230
|
+
"Already updating -- there's a circular dependency!"
|
|
231
|
+
"This is probably caused by failing to disable callbacks for a parameter."
|
|
232
|
+
"It's a bug --- tell the developer on github issues please."
|
|
233
|
+
)
|
|
234
|
+
return
|
|
331
235
|
|
|
332
|
-
|
|
333
|
-
|
|
236
|
+
with self._perform_update():
|
|
237
|
+
self._update_status(f"Updating {name}")
|
|
238
|
+
# Optionally suppress warnings during parameter updates
|
|
239
|
+
with warnings.catch_warnings():
|
|
240
|
+
if self.suppress_warnings:
|
|
241
|
+
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
334
242
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
243
|
+
# Get the component
|
|
244
|
+
component = self.components[name]
|
|
245
|
+
if component.is_action:
|
|
246
|
+
# If the component is an action, call the callback
|
|
247
|
+
parameter = self.viewer.parameters[name]
|
|
248
|
+
parameter.callback(self.viewer.state)
|
|
249
|
+
else:
|
|
250
|
+
# Otherwise, update the parameter value
|
|
251
|
+
self.viewer.set_parameter_value(name, component.value)
|
|
252
|
+
|
|
253
|
+
# Update any components that changed due to dependencies
|
|
254
|
+
self.sync_components_with_state()
|
|
255
|
+
|
|
256
|
+
# Update the plot
|
|
257
|
+
self.update_plot()
|
|
258
|
+
|
|
259
|
+
def sync_components_with_state(self, exclude: Optional[str] = None) -> None:
|
|
260
|
+
"""Sync component values with viewer state."""
|
|
261
|
+
for name, parameter in self.viewer.parameters.items():
|
|
262
|
+
if name == exclude:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
component = self.components[name]
|
|
266
|
+
if not component.matches_parameter(parameter):
|
|
267
|
+
component.update_from_parameter(parameter)
|
|
268
|
+
|
|
269
|
+
def update_plot(self) -> None:
|
|
270
|
+
"""Update the plot with current state."""
|
|
271
|
+
state = self.viewer.state
|
|
272
|
+
|
|
273
|
+
with plot_context():
|
|
274
|
+
figure = self.viewer.plot(state)
|
|
275
|
+
|
|
276
|
+
# Update components if plot function updated a parameter
|
|
277
|
+
self.sync_components_with_state()
|
|
278
|
+
|
|
279
|
+
self._display_figure(figure)
|
|
280
|
+
|
|
281
|
+
self._showing_new_figure = True
|
|
282
|
+
|
|
283
|
+
def _display_figure(self, figure: plt.Figure, store_figure: bool = True) -> None:
|
|
284
|
+
with self._display_lock:
|
|
285
|
+
# Close the last figure if it exists to keep matplotlib clean
|
|
286
|
+
if self._last_figure is not None:
|
|
287
|
+
plt.close(self._last_figure)
|
|
288
|
+
|
|
289
|
+
self.plot_output.clear_output(wait=True)
|
|
290
|
+
with self.plot_output:
|
|
291
|
+
if self.backend_type == "inline":
|
|
292
|
+
display(figure)
|
|
293
|
+
|
|
294
|
+
# Also required to make sure a second figure window isn't opened
|
|
295
|
+
plt.close(figure)
|
|
296
|
+
|
|
297
|
+
elif self.backend_type == "widget":
|
|
298
|
+
display(figure.canvas)
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(f"Unsupported backend type: {self.backend_type}")
|
|
302
|
+
|
|
303
|
+
if store_figure:
|
|
304
|
+
self._last_figure = figure
|
|
305
|
+
|
|
306
|
+
def _handle_container_width_change(self, _) -> None:
|
|
307
|
+
"""Handle changes to container width proportions."""
|
|
308
|
+
width_percent = self.controls["controls_width"].value
|
|
309
|
+
self.controls_width_percent = width_percent
|
|
310
|
+
|
|
311
|
+
# Update container widths
|
|
312
|
+
self.widgets_container.layout.width = f"{width_percent}%"
|
|
313
|
+
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
338
314
|
|
|
339
|
-
|
|
340
|
-
|
|
315
|
+
def _handle_update_threshold_change(self, _) -> None:
|
|
316
|
+
"""Handle changes to update threshold."""
|
|
317
|
+
self.update_threshold = self.controls["update_threshold"].value
|
|
318
|
+
|
|
319
|
+
def _update_status(self, status: str) -> None:
|
|
320
|
+
"""Update the status text."""
|
|
321
|
+
value = "<b>Syd Controls</b> "
|
|
322
|
+
value += "<span style='background-color: #e0e0e0; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 90%;'>"
|
|
323
|
+
value += f"Status: {status}"
|
|
324
|
+
value += "</span>"
|
|
325
|
+
self.controls["status"].value = value
|
|
@@ -31,7 +31,7 @@ class BaseWidget(Generic[T, W], ABC):
|
|
|
31
31
|
|
|
32
32
|
_widget: W
|
|
33
33
|
_callbacks: List[Dict[str, Union[Callable, Union[str, List[str]]]]]
|
|
34
|
-
|
|
34
|
+
is_action: bool = False
|
|
35
35
|
|
|
36
36
|
def __init__(
|
|
37
37
|
self,
|
|
@@ -465,7 +465,7 @@ class UnboundedFloatWidget(BaseWidget[UnboundedFloatParameter, widgets.FloatText
|
|
|
465
465
|
class ButtonWidget(BaseWidget[ButtonAction, widgets.Button]):
|
|
466
466
|
"""Widget for button parameters."""
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
is_action: bool = True
|
|
469
469
|
|
|
470
470
|
def _create_widget(
|
|
471
471
|
self,
|