syd 0.1.5__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 +7 -11
- syd/flask_deployment/__init__.py +1 -0
- syd/flask_deployment/components.py +510 -0
- syd/flask_deployment/deployer.py +302 -0
- syd/flask_deployment/static/css/viewer.css +82 -0
- syd/flask_deployment/static/js/viewer.js +174 -0
- syd/flask_deployment/templates/base.html +29 -0
- syd/flask_deployment/templates/viewer.html +51 -0
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -0
- syd/{notebook_deploy/deployer.py → notebook_deployment/_ipympl_deployer.py} +57 -36
- syd/notebook_deployment/deployer.py +330 -0
- syd/{notebook_deploy → notebook_deployment}/widgets.py +192 -112
- syd/parameters.py +390 -194
- 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} +309 -176
- syd-0.1.7.dist-info/METADATA +120 -0
- syd-0.1.7.dist-info/RECORD +22 -0
- syd/notebook_deploy/__init__.py +0 -1
- syd-0.1.5.dist-info/METADATA +0 -41
- syd-0.1.5.dist-info/RECORD +0 -10
- {syd-0.1.5.dist-info → syd-0.1.7.dist-info}/WHEEL +0 -0
- {syd-0.1.5.dist-info → syd-0.1.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Testing Principles for SYD Flask Deployment
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Testing an interactive GUI application like the SYD Flask deployment presents unique challenges. While traditional test suites focus on programmatic API testing, interactive GUIs require a different approach that combines several testing methodologies. This document outlines a comprehensive testing strategy for the SYD Flask deployment.
|
|
6
|
+
|
|
7
|
+
## Testing Pyramid for Interactive GUIs
|
|
8
|
+
|
|
9
|
+
For the SYD Flask deployment, we recommend implementing a modified testing pyramid with the following layers:
|
|
10
|
+
|
|
11
|
+
1. **Unit Tests** - Test isolated components and functions
|
|
12
|
+
2. **Integration Tests** - Test interactions between components
|
|
13
|
+
3. **API Tests** - Test Flask API endpoints
|
|
14
|
+
4. **Headless Browser Tests** - Test GUI interactions programmatically
|
|
15
|
+
5. **Visual Regression Tests** - Ensure UI appearance remains consistent
|
|
16
|
+
6. **Manual Testing Checklists** - Structured human verification
|
|
17
|
+
|
|
18
|
+
## 1. Unit Tests
|
|
19
|
+
|
|
20
|
+
### What to Test
|
|
21
|
+
- Individual Flask routes
|
|
22
|
+
- Component HTML generation functions
|
|
23
|
+
- Parameter update handlers
|
|
24
|
+
- State synchronization logic
|
|
25
|
+
- Plot generation utilities
|
|
26
|
+
|
|
27
|
+
### Implementation Strategy
|
|
28
|
+
```python
|
|
29
|
+
import unittest
|
|
30
|
+
from unittest import mock
|
|
31
|
+
from syd.flask_deployment.deployer import FlaskDeployer
|
|
32
|
+
from syd.flask_deployment.components import create_component
|
|
33
|
+
|
|
34
|
+
class TestFlaskDeployerComponents(unittest.TestCase):
|
|
35
|
+
def setUp(self):
|
|
36
|
+
self.mock_viewer = mock.MagicMock()
|
|
37
|
+
self.deployer = FlaskDeployer(self.mock_viewer)
|
|
38
|
+
|
|
39
|
+
def test_create_float_component(self):
|
|
40
|
+
mock_param = mock.MagicMock()
|
|
41
|
+
mock_param.name = "test_param"
|
|
42
|
+
mock_param.value = 5.0
|
|
43
|
+
component = create_component(mock_param)
|
|
44
|
+
self.assertIn("test_param", component.html)
|
|
45
|
+
self.assertIn("5.0", component.html)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 2. Integration Tests
|
|
49
|
+
|
|
50
|
+
### What to Test
|
|
51
|
+
- Parameter creation to HTML rendering pipeline
|
|
52
|
+
- Parameter update → state sync → component update flow
|
|
53
|
+
- Plot generation and serving workflow
|
|
54
|
+
|
|
55
|
+
### Implementation Strategy
|
|
56
|
+
```python
|
|
57
|
+
def test_parameter_update_state_sync():
|
|
58
|
+
# Create real viewer with test parameters
|
|
59
|
+
viewer = Viewer()
|
|
60
|
+
viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
|
|
61
|
+
|
|
62
|
+
# Create deployer with this viewer
|
|
63
|
+
deployer = FlaskDeployer(viewer)
|
|
64
|
+
|
|
65
|
+
# Update parameter and check if components reflect the change
|
|
66
|
+
deployer._handle_parameter_update('test_param', 5.0)
|
|
67
|
+
|
|
68
|
+
# Verify component has updated
|
|
69
|
+
component = deployer.parameter_components['test_param']
|
|
70
|
+
self.assertEqual(component.value, 5.0)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 3. API Tests
|
|
74
|
+
|
|
75
|
+
### What to Test
|
|
76
|
+
- All Flask endpoints (`/`, `/update/<name>`, `/state`, `/plot`)
|
|
77
|
+
- Response formats and status codes
|
|
78
|
+
- Error handling
|
|
79
|
+
- Race conditions with concurrent requests
|
|
80
|
+
|
|
81
|
+
### Implementation Strategy
|
|
82
|
+
```python
|
|
83
|
+
def test_update_parameter_endpoint():
|
|
84
|
+
viewer = Viewer()
|
|
85
|
+
viewer.add_float('test_param', value=1.0, min_value=0, max_value=10)
|
|
86
|
+
deployer = FlaskDeployer(viewer)
|
|
87
|
+
app = deployer.app
|
|
88
|
+
|
|
89
|
+
with app.test_client() as client:
|
|
90
|
+
# Test successful update
|
|
91
|
+
response = client.post('/update/test_param',
|
|
92
|
+
json={'value': 5.0})
|
|
93
|
+
self.assertEqual(response.status_code, 200)
|
|
94
|
+
self.assertEqual(viewer.state['test_param'], 5.0)
|
|
95
|
+
|
|
96
|
+
# Test parameter not found
|
|
97
|
+
response = client.post('/update/nonexistent',
|
|
98
|
+
json={'value': 5.0})
|
|
99
|
+
self.assertEqual(response.status_code, 404)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 4. Headless Browser Tests
|
|
103
|
+
|
|
104
|
+
### What to Test
|
|
105
|
+
- User interactions with the web interface
|
|
106
|
+
- Parameter widget interactions
|
|
107
|
+
- UI updates in response to parameter changes
|
|
108
|
+
- Plot refreshes
|
|
109
|
+
|
|
110
|
+
### Implementation Strategy
|
|
111
|
+
|
|
112
|
+
Use Selenium or Playwright to automate browser interactions:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from selenium import webdriver
|
|
116
|
+
from selenium.webdriver.common.by import By
|
|
117
|
+
|
|
118
|
+
def test_slider_interaction():
|
|
119
|
+
# Start the Flask app in a separate thread
|
|
120
|
+
viewer = create_test_viewer()
|
|
121
|
+
deployer = FlaskDeployer(viewer)
|
|
122
|
+
thread = threading.Thread(target=deployer.app.run)
|
|
123
|
+
thread.daemon = True
|
|
124
|
+
thread.start()
|
|
125
|
+
|
|
126
|
+
# Use Selenium to interact with the web interface
|
|
127
|
+
driver = webdriver.Chrome()
|
|
128
|
+
driver.get('http://localhost:5000')
|
|
129
|
+
|
|
130
|
+
# Find a slider and interact with it
|
|
131
|
+
slider = driver.find_element(By.ID, 'param_amplitude')
|
|
132
|
+
slider.send_keys(webdriver.Keys.ARROW_RIGHT * 5) # Increase value
|
|
133
|
+
|
|
134
|
+
# Verify the plot updates
|
|
135
|
+
# This requires some way to detect that the plot has changed
|
|
136
|
+
# Could check the src attribute of the image element changes
|
|
137
|
+
time.sleep(1) # Allow time for update
|
|
138
|
+
plot_src = driver.find_element(By.ID, 'plot').get_attribute('src')
|
|
139
|
+
self.assertTrue(len(plot_src) > 0)
|
|
140
|
+
|
|
141
|
+
driver.quit()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 5. Visual Regression Tests
|
|
145
|
+
|
|
146
|
+
### What to Test
|
|
147
|
+
- UI appearance remains consistent across changes
|
|
148
|
+
- Components render correctly with different values
|
|
149
|
+
- Layout adapts properly to different screen sizes
|
|
150
|
+
|
|
151
|
+
### Implementation Strategy
|
|
152
|
+
|
|
153
|
+
Use tools like Percy or BackstopJS to capture and compare screenshots:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
def test_visual_appearance():
|
|
157
|
+
# Set up test app with standard parameters
|
|
158
|
+
app = create_standard_test_app()
|
|
159
|
+
|
|
160
|
+
# Use a tool like Percy
|
|
161
|
+
percy_snapshot(driver, 'Main view')
|
|
162
|
+
|
|
163
|
+
# Change to different layout
|
|
164
|
+
driver.get('http://localhost:5000?controls_position=right')
|
|
165
|
+
percy_snapshot(driver, 'Right controls layout')
|
|
166
|
+
|
|
167
|
+
# Test responsive views
|
|
168
|
+
driver.set_window_size(400, 800) # Mobile size
|
|
169
|
+
percy_snapshot(driver, 'Mobile view')
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 6. Manual Testing Checklists
|
|
173
|
+
|
|
174
|
+
For some aspects of interactive GUI applications, manual testing remains necessary. Create structured checklists for testers:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
# Manual Testing Checklist
|
|
178
|
+
|
|
179
|
+
## Parameter Interactions
|
|
180
|
+
- [ ] Sliders respond smoothly to mouse drag
|
|
181
|
+
- [ ] Number inputs accept keyboard input
|
|
182
|
+
- [ ] Range sliders correctly display and update both values
|
|
183
|
+
- [ ] Toggles switch state when clicked
|
|
184
|
+
- [ ] Dropdowns show all options and select correctly
|
|
185
|
+
|
|
186
|
+
## Plot Updates
|
|
187
|
+
- [ ] Plot updates immediately after parameter change when continuous=True
|
|
188
|
+
- [ ] Plot updates only after interaction ends when continuous=False
|
|
189
|
+
- [ ] Plot maintains aspect ratio when window is resized
|
|
190
|
+
- [ ] No visual glitches during plot transitions
|
|
191
|
+
|
|
192
|
+
## Layouts
|
|
193
|
+
- [ ] Test all layouts: left, right, top, bottom
|
|
194
|
+
- [ ] Verify responsive behavior on different screen sizes
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Automated Testing Framework
|
|
198
|
+
|
|
199
|
+
To bring this all together, we recommend implementing a pytest-based testing framework:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# conftest.py
|
|
203
|
+
import pytest
|
|
204
|
+
from syd import make_viewer
|
|
205
|
+
import numpy as np
|
|
206
|
+
import matplotlib.pyplot as plt
|
|
207
|
+
|
|
208
|
+
@pytest.fixture
|
|
209
|
+
def standard_test_viewer():
|
|
210
|
+
"""Create a standard viewer for testing."""
|
|
211
|
+
viewer = make_viewer()
|
|
212
|
+
|
|
213
|
+
def plot(state):
|
|
214
|
+
fig, ax = plt.subplots()
|
|
215
|
+
x = np.linspace(0, 10, 1000)
|
|
216
|
+
y = state['amplitude'] * np.sin(state['frequency'] * x)
|
|
217
|
+
ax.plot(x, y)
|
|
218
|
+
return fig
|
|
219
|
+
|
|
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)
|
|
223
|
+
|
|
224
|
+
return viewer
|
|
225
|
+
|
|
226
|
+
@pytest.fixture
|
|
227
|
+
def flask_app(standard_test_viewer):
|
|
228
|
+
"""Create a Flask app for testing."""
|
|
229
|
+
from syd.flask_deployment.deployer import FlaskDeployer
|
|
230
|
+
deployer = FlaskDeployer(standard_test_viewer)
|
|
231
|
+
return deployer.app
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Continuous Integration Setup
|
|
235
|
+
|
|
236
|
+
Integrate these tests into a CI pipeline:
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
# .github/workflows/flask-tests.yml
|
|
240
|
+
name: Flask Deployment Tests
|
|
241
|
+
|
|
242
|
+
on:
|
|
243
|
+
push:
|
|
244
|
+
paths:
|
|
245
|
+
- 'syd/flask_deployment/**'
|
|
246
|
+
- 'tests/flask/**'
|
|
247
|
+
|
|
248
|
+
jobs:
|
|
249
|
+
test:
|
|
250
|
+
runs-on: ubuntu-latest
|
|
251
|
+
steps:
|
|
252
|
+
- uses: actions/checkout@v2
|
|
253
|
+
- name: Set up Python
|
|
254
|
+
uses: actions/setup-python@v2
|
|
255
|
+
with:
|
|
256
|
+
python-version: '3.9'
|
|
257
|
+
- name: Install dependencies
|
|
258
|
+
run: |
|
|
259
|
+
python -m pip install --upgrade pip
|
|
260
|
+
pip install -e ".[test]"
|
|
261
|
+
- name: Run unit and integration tests
|
|
262
|
+
run: pytest tests/flask/test_unit_integration.py
|
|
263
|
+
- name: Run API tests
|
|
264
|
+
run: pytest tests/flask/test_api.py
|
|
265
|
+
- name: Set up Chrome WebDriver
|
|
266
|
+
uses: browser-actions/setup-chrome@latest
|
|
267
|
+
- name: Run headless browser tests
|
|
268
|
+
run: pytest tests/flask/test_browser.py
|
|
269
|
+
- name: Upload visual test artifacts
|
|
270
|
+
uses: actions/upload-artifact@v2
|
|
271
|
+
with:
|
|
272
|
+
name: visual-test-results
|
|
273
|
+
path: tests/flask/visual/results
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Practical Implementation Plan
|
|
277
|
+
|
|
278
|
+
For the SYD Flask deployment, we recommend implementing tests in this order:
|
|
279
|
+
|
|
280
|
+
1. **Start with Unit Tests** - Focus on individual components and functions
|
|
281
|
+
2. **Add API Tests** - Ensure all endpoints work correctly
|
|
282
|
+
3. **Implement Integration Tests** - Test component interactions
|
|
283
|
+
4. **Add Visual Testing** - Simple screenshot comparisons
|
|
284
|
+
5. **Create Browser Tests** - For critical UI workflows
|
|
285
|
+
|
|
286
|
+
## Testing Challenges and Solutions
|
|
287
|
+
|
|
288
|
+
| Challenge | Solution |
|
|
289
|
+
|-----------|----------|
|
|
290
|
+
| Testing real-time updates | Use JavaScript triggers and MutationObserver in browser tests |
|
|
291
|
+
| Asynchronous plot generation | Implement wait utilities with timeouts |
|
|
292
|
+
| Visual inconsistencies across platforms | Use Percy or similar to handle cross-platform rendering |
|
|
293
|
+
| Slow browser tests | Run only critical UI tests in CI, more comprehensive ones nightly |
|
|
294
|
+
| Matplotlib backend differences | Mock or standardize backends during testing |
|
|
295
|
+
|
|
296
|
+
## Conclusion
|
|
297
|
+
|
|
298
|
+
Testing an interactive GUI like the SYD Flask deployment requires a multi-layered approach. By combining traditional unit and integration tests with specialized GUI testing tools, we can achieve good test coverage while still accommodating the unique challenges of interactive web applications. The most effective strategy is to automate as much as possible while maintaining a structured approach to manual testing for aspects that are difficult to verify programmatically.
|
|
299
|
+
|
|
300
|
+
For the initial implementation of the testing framework, focus on establishing solid unit and API tests, as these will provide the foundation for more complex tests and help catch the most common issues early in the development process.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .deployer import NotebookDeployer
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
import ipywidgets as widgets
|
|
5
5
|
from IPython.display import display
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
|
+
import warnings
|
|
8
|
+
from ..parameters import ParameterUpdateWarning
|
|
7
9
|
|
|
8
|
-
from ..
|
|
9
|
-
from .widgets import
|
|
10
|
+
from ..viewer import Viewer
|
|
11
|
+
from .widgets import BaseWidget, create_widget
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@contextmanager
|
|
@@ -26,7 +28,6 @@ class LayoutConfig:
|
|
|
26
28
|
figure_width: float = 8.0
|
|
27
29
|
figure_height: float = 6.0
|
|
28
30
|
controls_width_percent: int = 30
|
|
29
|
-
continuous_update: bool = False
|
|
30
31
|
|
|
31
32
|
def __post_init__(self):
|
|
32
33
|
valid_positions = ["left", "top"]
|
|
@@ -40,31 +41,32 @@ class LayoutConfig:
|
|
|
40
41
|
return self.controls_position == "left"
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
class
|
|
44
|
+
class NotebookDeployer:
|
|
44
45
|
"""
|
|
45
|
-
A deployment system for
|
|
46
|
+
A deployment system for Viewer in Jupyter notebooks using ipywidgets.
|
|
46
47
|
Built around the parameter widget system for clean separation of concerns.
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
50
|
def __init__(
|
|
50
51
|
self,
|
|
51
|
-
viewer:
|
|
52
|
+
viewer: Viewer,
|
|
52
53
|
layout_config: Optional[LayoutConfig] = None,
|
|
53
|
-
|
|
54
|
+
continuous: bool = False,
|
|
55
|
+
suppress_warnings: bool = False,
|
|
54
56
|
):
|
|
55
|
-
if not isinstance(viewer,
|
|
56
|
-
raise TypeError(
|
|
57
|
-
f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
|
|
58
|
-
)
|
|
57
|
+
if not isinstance(viewer, Viewer): # type: ignore
|
|
58
|
+
raise TypeError(f"viewer must be an Viewer, got {type(viewer).__name__}")
|
|
59
59
|
|
|
60
60
|
self.viewer = viewer
|
|
61
61
|
self.config = layout_config or LayoutConfig()
|
|
62
|
-
self.
|
|
62
|
+
self.continuous = continuous
|
|
63
|
+
self.suppress_warnings = suppress_warnings
|
|
63
64
|
|
|
64
65
|
# Initialize containers
|
|
65
|
-
self.parameter_widgets: Dict[str,
|
|
66
|
+
self.parameter_widgets: Dict[str, BaseWidget] = {}
|
|
66
67
|
self.layout_widgets = self._create_layout_controls()
|
|
67
68
|
self.plot_output = widgets.Output()
|
|
69
|
+
self._canvas_widget = None
|
|
68
70
|
|
|
69
71
|
# Store current figure
|
|
70
72
|
self._current_figure = None
|
|
@@ -82,7 +84,7 @@ class NotebookDeployment:
|
|
|
82
84
|
min=20,
|
|
83
85
|
max=80,
|
|
84
86
|
description="Controls Width %",
|
|
85
|
-
|
|
87
|
+
continuous=True,
|
|
86
88
|
layout=widgets.Layout(width="95%"),
|
|
87
89
|
style={"description_width": "initial"},
|
|
88
90
|
)
|
|
@@ -95,38 +97,52 @@ class NotebookDeployment:
|
|
|
95
97
|
def _create_parameter_widgets(self) -> None:
|
|
96
98
|
"""Create widget instances for all parameters."""
|
|
97
99
|
for name, param in self.viewer.parameters.items():
|
|
98
|
-
widget =
|
|
100
|
+
widget = create_widget(
|
|
99
101
|
param,
|
|
100
|
-
|
|
102
|
+
continuous=self.continuous,
|
|
101
103
|
)
|
|
102
104
|
|
|
103
105
|
# Store in widget dict
|
|
104
106
|
self.parameter_widgets[name] = widget
|
|
105
107
|
|
|
106
|
-
def
|
|
107
|
-
"""Handle
|
|
108
|
+
def _handle_widget_engagement(self, name: str) -> None:
|
|
109
|
+
"""Handle engagement with an interactive widget."""
|
|
108
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
|
+
)
|
|
109
116
|
return
|
|
110
117
|
|
|
111
118
|
try:
|
|
112
119
|
self._updating = True
|
|
113
|
-
widget = self.parameter_widgets[name]
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
self.viewer.set_parameter_value(name, widget.value)
|
|
121
|
+
# Optionally suppress warnings during parameter updates
|
|
122
|
+
with warnings.catch_warnings():
|
|
123
|
+
if self.suppress_warnings:
|
|
124
|
+
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
self._sync_widgets_with_state(exclude=name)
|
|
126
|
+
widget = self.parameter_widgets[name]
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
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()
|
|
126
139
|
|
|
127
140
|
finally:
|
|
128
141
|
self._updating = False
|
|
129
142
|
|
|
143
|
+
def _handle_action(self, name: str) -> None:
|
|
144
|
+
"""Handle actions for parameter widgets."""
|
|
145
|
+
|
|
130
146
|
def _sync_widgets_with_state(self, exclude: Optional[str] = None) -> None:
|
|
131
147
|
"""Sync widget values with viewer state."""
|
|
132
148
|
for name, parameter in self.viewer.parameters.items():
|
|
@@ -155,20 +171,25 @@ class NotebookDeployment:
|
|
|
155
171
|
|
|
156
172
|
def _update_plot(self) -> None:
|
|
157
173
|
"""Update the plot with current state."""
|
|
158
|
-
state = self.viewer.
|
|
174
|
+
state = self.viewer.state
|
|
159
175
|
|
|
160
176
|
with _plot_context():
|
|
161
177
|
new_fig = self.viewer.plot(state)
|
|
162
178
|
plt.close(self._current_figure) # Close old figure
|
|
163
179
|
self._current_figure = new_fig
|
|
164
180
|
|
|
165
|
-
|
|
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)
|
|
166
188
|
|
|
167
189
|
def _redraw_plot(self) -> None:
|
|
168
190
|
"""Clear and redraw the plot in the output widget."""
|
|
169
|
-
self.
|
|
170
|
-
|
|
171
|
-
display(self._current_figure)
|
|
191
|
+
if self._canvas_widget is not None:
|
|
192
|
+
self._canvas_widget.draw()
|
|
172
193
|
|
|
173
194
|
def _create_layout(self) -> widgets.Widget:
|
|
174
195
|
"""Create the main layout combining controls and plot."""
|
|
@@ -181,7 +202,7 @@ class NotebookDeployment:
|
|
|
181
202
|
|
|
182
203
|
# Set up parameter widgets with their observe callbacks
|
|
183
204
|
for name, widget in self.parameter_widgets.items():
|
|
184
|
-
widget.observe(lambda change, n=name: self.
|
|
205
|
+
widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
|
|
185
206
|
|
|
186
207
|
# Create parameter controls section
|
|
187
208
|
param_box = widgets.VBox(
|
|
@@ -225,7 +246,7 @@ class NotebookDeployment:
|
|
|
225
246
|
|
|
226
247
|
def deploy(self) -> None:
|
|
227
248
|
"""Deploy the interactive viewer with proper state management."""
|
|
228
|
-
with self.viewer.
|
|
249
|
+
with self.viewer._deploy_app():
|
|
229
250
|
# Create widgets
|
|
230
251
|
self._create_parameter_widgets()
|
|
231
252
|
|