syd 0.1.6__py3-none-any.whl → 0.2.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>Syd Viewer</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>
@@ -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=0, max=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=0, max=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=0, max=2)
222
+ viewer.add_float('frequency', value=1.0, min=0.1, max=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.
@@ -1 +1 @@
1
- from .deployer import NotebookDeployment
1
+ from .deployer import NotebookDeployer
@@ -1,13 +1,17 @@
1
- from typing import Dict, Any, Optional, cast
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 ..interactive_viewer import InteractiveViewer
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
- controls_width_percent: int = 30
70
+ controls_width_percent: int = 20
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 NotebookDeployment:
84
+ class NotebookDeployer:
45
85
  """
46
- A deployment system for InteractiveViewer in Jupyter notebooks using ipywidgets.
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: InteractiveViewer,
53
- layout_config: Optional[LayoutConfig] = None,
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 = 20,
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 = layout_config or LayoutConfig()
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
+ 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."
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
- # Store current figure
73
- self._current_figure = None
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] = {}
@@ -82,16 +134,13 @@ class NotebookDeployment:
82
134
  if self.config.is_horizontal:
83
135
  controls["controls_width"] = widgets.IntSlider(
84
136
  value=self.config.controls_width_percent,
85
- min=20,
86
- max=80,
137
+ min=10,
138
+ max=50,
87
139
  description="Controls Width %",
88
140
  continuous=True,
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.get_state())
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(exclude=name)
186
+ self._sync_widgets_with_state()
137
187
 
138
188
  # Update the plot
139
189
  self._update_plot()
@@ -172,30 +222,37 @@ class NotebookDeployment:
172
222
 
173
223
  def _update_plot(self) -> None:
174
224
  """Update the plot with current state."""
175
- state = self.viewer.get_state()
225
+ state = self.viewer.state
176
226
 
177
227
  with _plot_context():
178
- new_fig = self.viewer.plot(state)
179
- plt.close(self._current_figure) # Close old figure
180
- self._current_figure = new_fig
228
+ figure = self.viewer.plot(state)
181
229
 
182
- self._redraw_plot()
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)
183
237
 
184
- def _redraw_plot(self) -> None:
185
- """Clear and redraw the plot in the output widget."""
186
238
  self.plot_output.clear_output(wait=True)
187
239
  with self.plot_output:
188
- display(self._current_figure)
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
189
253
 
190
254
  def _create_layout(self) -> widgets.Widget:
191
255
  """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
256
  # Set up parameter widgets with their observe callbacks
200
257
  for name, widget in self.parameter_widgets.items():
201
258
  widget.observe(lambda change, n=name: self._handle_widget_engagement(n))
@@ -208,8 +265,26 @@ class NotebookDeployment:
208
265
  )
209
266
 
210
267
  # Combine all controls
268
+ if self.config.is_horizontal:
269
+ # Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
270
+ layout_box = widgets.VBox(
271
+ [widgets.HTML("<b>Layout Controls</b>")]
272
+ + list(self.layout_widgets.values()),
273
+ layout=widgets.Layout(margin="10px 0px"),
274
+ )
275
+
276
+ # Register the controls_width slider's observer
277
+ if "controls_width" in self.layout_widgets:
278
+ self.layout_widgets["controls_width"].observe(
279
+ self._handle_container_width_change, names="value"
280
+ )
281
+
282
+ widgets_elements = [param_box, layout_box]
283
+ else:
284
+ widgets_elements = [param_box]
285
+
211
286
  self.widgets_container = widgets.VBox(
212
- [param_box, layout_box],
287
+ widgets_elements,
213
288
  layout=widgets.Layout(
214
289
  width=(
215
290
  f"{self.config.controls_width_percent}%"
@@ -217,7 +292,9 @@ class NotebookDeployment:
217
292
  else "100%"
218
293
  ),
219
294
  padding="10px",
220
- overflow_y="auto",
295
+ overflow_y="scroll",
296
+ border="1px solid #e5e7eb",
297
+ border_radius="4px 4px 0px 0px",
221
298
  ),
222
299
  )
223
300
 
@@ -237,18 +314,27 @@ class NotebookDeployment:
237
314
  # Create final layout based on configuration
238
315
  if self.config.controls_position == "left":
239
316
  return widgets.HBox([self.widgets_container, self.plot_container])
317
+ elif self.config.controls_position == "right":
318
+ return widgets.HBox([self.plot_container, self.widgets_container])
319
+ elif self.config.controls_position == "bottom":
320
+ return widgets.VBox([self.plot_container, self.widgets_container])
240
321
  else:
241
322
  return widgets.VBox([self.widgets_container, self.plot_container])
242
323
 
243
324
  def deploy(self) -> None:
244
325
  """Deploy the interactive viewer with proper state management."""
245
- with self.viewer._deploy_app():
246
- # Create widgets
247
- self._create_parameter_widgets()
326
+ self.backend_type = get_backend_type()
327
+
328
+ # We used to use the deploy_app context, but notebook deployment works
329
+ # differently because it's asynchronous and this doesn't really behave
330
+ # as intended. (e.g. with self.viewer._deploy_app() ...)
331
+
332
+ # Create widgets
333
+ self._create_parameter_widgets()
248
334
 
249
- # Create and display layout
250
- layout = self._create_layout()
251
- display(layout)
335
+ # Create and display layout
336
+ self.layout = self._create_layout()
337
+ display(self.layout)
252
338
 
253
- # Create initial plot
254
- self._update_plot()
339
+ # Create initial plot
340
+ self._update_plot()