syd 0.1.6__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 +3 -3
- syd/flask_deployment/__init__.py +1 -0
- syd/flask_deployment/components.py +376 -363
- syd/flask_deployment/deployer.py +247 -283
- syd/flask_deployment/static/css/viewer.css +82 -0
- syd/flask_deployment/static/js/viewer.js +174 -0
- syd/flask_deployment/templates/base.html +23 -20
- syd/flask_deployment/templates/viewer.html +49 -95
- syd/flask_deployment/testing_principles.md +300 -0
- syd/notebook_deployment/__init__.py +1 -1
- syd/notebook_deployment/_ipympl_deployer.py +258 -0
- syd/notebook_deployment/deployer.py +126 -50
- syd/notebook_deployment/widgets.py +142 -69
- syd/parameters.py +93 -180
- 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} +152 -188
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/METADATA +26 -12
- syd-0.1.7.dist-info/RECORD +22 -0
- syd/flask_deployment/static/css/styles.css +0 -39
- syd/flask_deployment/static/js/components.js +0 -51
- syd-0.1.6.dist-info/RECORD +0 -18
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/WHEEL +0 -0
- {syd-0.1.6.dist-info → syd-0.1.7.dist-info}/licenses/LICENSE +0 -0
syd/flask_deployment/deployer.py
CHANGED
|
@@ -1,133 +1,81 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional
|
|
1
|
+
from typing import Dict, Any, Optional, List
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
2
4
|
from dataclasses import dataclass
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from time import time
|
|
7
|
+
import base64
|
|
8
|
+
from io import BytesIO
|
|
3
9
|
import threading
|
|
4
|
-
|
|
5
|
-
import
|
|
10
|
+
import webbrowser
|
|
11
|
+
from pathlib import Path
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
from flask import Flask, render_template, jsonify, request, Response
|
|
14
|
+
import matplotlib as mpl
|
|
8
15
|
import matplotlib.pyplot as plt
|
|
9
|
-
import io
|
|
10
|
-
import base64
|
|
11
|
-
from contextlib import contextmanager
|
|
12
|
-
import webbrowser
|
|
13
16
|
|
|
14
|
-
from ..
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{% endfor %}
|
|
62
|
-
|
|
63
|
-
<script>
|
|
64
|
-
function updateParameter(name, value) {
|
|
65
|
-
fetch('/update_parameter', {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
},
|
|
70
|
-
body: JSON.stringify({name, value})
|
|
71
|
-
})
|
|
72
|
-
.then(response => response.json())
|
|
73
|
-
.then(data => {
|
|
74
|
-
if (data.error) {
|
|
75
|
-
console.error(data.error);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// Update plot
|
|
79
|
-
document.getElementById('plot').src = data.plot;
|
|
80
|
-
// Apply any parameter updates
|
|
81
|
-
for (const [param, js] of Object.entries(data.updates)) {
|
|
82
|
-
eval(js);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function buttonClick(name) {
|
|
88
|
-
fetch('/button_click', {
|
|
89
|
-
method: 'POST',
|
|
90
|
-
headers: {
|
|
91
|
-
'Content-Type': 'application/json',
|
|
92
|
-
},
|
|
93
|
-
body: JSON.stringify({name})
|
|
94
|
-
})
|
|
95
|
-
.then(response => response.json())
|
|
96
|
-
.then(data => {
|
|
97
|
-
if (data.error) {
|
|
98
|
-
console.error(data.error);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
// Update plot
|
|
102
|
-
document.getElementById('plot').src = data.plot;
|
|
103
|
-
// Apply any parameter updates
|
|
104
|
-
for (const [param, js] of Object.entries(data.updates)) {
|
|
105
|
-
eval(js);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Initialize components
|
|
111
|
-
{{ components_init | safe }}
|
|
112
|
-
</script>
|
|
113
|
-
</body>
|
|
114
|
-
</html>
|
|
115
|
-
"""
|
|
17
|
+
from ..parameters import ParameterUpdateWarning
|
|
18
|
+
from ..viewer import Viewer
|
|
19
|
+
from .components import BaseComponent, ComponentStyle, create_component
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def _plot_context():
|
|
24
|
+
plt.ioff()
|
|
25
|
+
try:
|
|
26
|
+
yield
|
|
27
|
+
finally:
|
|
28
|
+
plt.ion()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_backend_type():
|
|
32
|
+
"""
|
|
33
|
+
Determines the current matplotlib backend type and returns relevant info
|
|
34
|
+
"""
|
|
35
|
+
backend = mpl.get_backend().lower()
|
|
36
|
+
if "agg" in backend:
|
|
37
|
+
return "agg"
|
|
38
|
+
elif "inline" in backend:
|
|
39
|
+
return "inline"
|
|
40
|
+
else:
|
|
41
|
+
# Force Agg backend for Flask
|
|
42
|
+
mpl.use("Agg")
|
|
43
|
+
return "agg"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def debounce(wait_time):
|
|
47
|
+
"""
|
|
48
|
+
Decorator to prevent a function from being called more than once every wait_time seconds.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def decorator(fn):
|
|
52
|
+
last_called = [0.0] # Using list to maintain state in closure
|
|
53
|
+
|
|
54
|
+
@wraps(fn)
|
|
55
|
+
def debounced(*args, **kwargs):
|
|
56
|
+
current_time = time()
|
|
57
|
+
if current_time - last_called[0] >= wait_time:
|
|
58
|
+
fn(*args, **kwargs)
|
|
59
|
+
last_called[0] = current_time
|
|
60
|
+
|
|
61
|
+
return debounced
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
116
64
|
|
|
117
65
|
|
|
118
66
|
@dataclass
|
|
119
|
-
class
|
|
120
|
-
"""Configuration for the viewer layout
|
|
67
|
+
class LayoutConfig:
|
|
68
|
+
"""Configuration for the viewer layout."""
|
|
121
69
|
|
|
122
|
-
controls_position: str = "left" # Options are: 'left', 'top'
|
|
70
|
+
controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
|
|
123
71
|
figure_width: float = 8.0
|
|
124
72
|
figure_height: float = 6.0
|
|
125
73
|
controls_width_percent: int = 30
|
|
126
|
-
|
|
127
|
-
|
|
74
|
+
template_path: Optional[str] = None
|
|
75
|
+
static_path: Optional[str] = None
|
|
128
76
|
|
|
129
77
|
def __post_init__(self):
|
|
130
|
-
valid_positions = ["left", "top"]
|
|
78
|
+
valid_positions = ["left", "top", "right", "bottom"]
|
|
131
79
|
if self.controls_position not in valid_positions:
|
|
132
80
|
raise ValueError(
|
|
133
81
|
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
@@ -135,204 +83,220 @@ class FlaskLayoutConfig:
|
|
|
135
83
|
|
|
136
84
|
@property
|
|
137
85
|
def is_horizontal(self) -> bool:
|
|
138
|
-
return self.controls_position == "left"
|
|
86
|
+
return self.controls_position == "left" or self.controls_position == "right"
|
|
139
87
|
|
|
140
88
|
|
|
141
|
-
class
|
|
142
|
-
"""
|
|
89
|
+
class FlaskDeployer:
|
|
90
|
+
"""
|
|
91
|
+
A deployment system for Viewer in Flask web applications.
|
|
92
|
+
Built around the parameter component system for clean separation of concerns.
|
|
93
|
+
"""
|
|
143
94
|
|
|
144
95
|
def __init__(
|
|
145
96
|
self,
|
|
146
|
-
viewer:
|
|
147
|
-
|
|
97
|
+
viewer: Viewer,
|
|
98
|
+
controls_position: str = "left",
|
|
99
|
+
figure_width: float = 8.0,
|
|
100
|
+
figure_height: float = 6.0,
|
|
101
|
+
controls_width_percent: int = 30,
|
|
102
|
+
continuous: bool = False,
|
|
103
|
+
suppress_warnings: bool = False,
|
|
104
|
+
host: str = "127.0.0.1",
|
|
105
|
+
port: int = 5000,
|
|
106
|
+
template_path: Optional[str] = None,
|
|
107
|
+
static_path: Optional[str] = None,
|
|
148
108
|
):
|
|
149
|
-
if not isinstance(viewer, InteractiveViewer):
|
|
150
|
-
raise TypeError(
|
|
151
|
-
f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
|
|
152
|
-
)
|
|
153
|
-
|
|
154
109
|
self.viewer = viewer
|
|
155
|
-
self.config =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
self.
|
|
164
|
-
self.
|
|
110
|
+
self.config = LayoutConfig(
|
|
111
|
+
controls_position=controls_position,
|
|
112
|
+
figure_width=figure_width,
|
|
113
|
+
figure_height=figure_height,
|
|
114
|
+
controls_width_percent=controls_width_percent,
|
|
115
|
+
template_path=template_path,
|
|
116
|
+
static_path=static_path,
|
|
117
|
+
)
|
|
118
|
+
self.continuous = continuous
|
|
119
|
+
self.suppress_warnings = suppress_warnings
|
|
120
|
+
self.host = host
|
|
121
|
+
self.port = port
|
|
122
|
+
|
|
123
|
+
# Initialize containers
|
|
124
|
+
self.backend_type = get_backend_type()
|
|
125
|
+
self.parameter_components: Dict[str, BaseComponent] = {}
|
|
126
|
+
self.app = self._create_flask_app()
|
|
127
|
+
|
|
128
|
+
# Flag to prevent circular updates
|
|
129
|
+
self._updating = False
|
|
130
|
+
|
|
131
|
+
# Last figure to close when new figures are created
|
|
132
|
+
self._last_figure = None
|
|
133
|
+
|
|
134
|
+
def _create_flask_app(self) -> Flask:
|
|
135
|
+
"""Create and configure the Flask application."""
|
|
136
|
+
template_path = self.config.template_path or str(
|
|
137
|
+
Path(__file__).parent / "templates"
|
|
138
|
+
)
|
|
139
|
+
static_path = self.config.static_path or str(Path(__file__).parent / "static")
|
|
165
140
|
|
|
166
|
-
|
|
167
|
-
"""Set up the Flask routes for the application."""
|
|
141
|
+
app = Flask(__name__, template_folder=template_path, static_folder=static_path)
|
|
168
142
|
|
|
169
|
-
|
|
143
|
+
# Register routes
|
|
144
|
+
@app.route("/")
|
|
170
145
|
def index():
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
146
|
+
# Generate initial plot
|
|
147
|
+
initial_plot = self._generate_plot()
|
|
148
|
+
if initial_plot is not None:
|
|
149
|
+
buffer = BytesIO()
|
|
150
|
+
initial_plot.savefig(buffer, format="png", bbox_inches="tight")
|
|
151
|
+
buffer.seek(0)
|
|
152
|
+
initial_plot_data = base64.b64encode(buffer.getvalue()).decode()
|
|
153
|
+
else:
|
|
154
|
+
initial_plot_data = ""
|
|
155
|
+
|
|
156
|
+
return render_template(
|
|
157
|
+
"viewer.html",
|
|
158
|
+
components=self._get_component_html(),
|
|
159
|
+
controls_position=self.config.controls_position,
|
|
160
|
+
controls_width=self.config.controls_width_percent,
|
|
161
|
+
figure_width=self.config.figure_width,
|
|
162
|
+
figure_height=self.config.figure_height,
|
|
163
|
+
continuous=self.continuous,
|
|
164
|
+
initial_plot=initial_plot_data,
|
|
165
|
+
)
|
|
180
166
|
|
|
167
|
+
@app.route("/update/<name>", methods=["POST"])
|
|
168
|
+
def update_parameter(name: str):
|
|
181
169
|
if name not in self.viewer.parameters:
|
|
182
|
-
print(f"Parameter {name} not found in viewer parameters") # Debug log
|
|
183
170
|
return jsonify({"error": f"Parameter {name} not found"}), 404
|
|
184
171
|
|
|
185
172
|
try:
|
|
186
|
-
|
|
187
|
-
self.
|
|
188
|
-
|
|
189
|
-
# Update the plot with new parameter values
|
|
190
|
-
print("Updating plot with new parameters...") # Debug log
|
|
191
|
-
self._update_plot()
|
|
192
|
-
|
|
193
|
-
updates = self._get_parameter_updates()
|
|
194
|
-
plot_data = self._get_current_plot_data()
|
|
195
|
-
# Debug log
|
|
196
|
-
print(f"Generated updates for parameters: {list(updates.keys())}")
|
|
197
|
-
|
|
198
|
-
return jsonify(
|
|
199
|
-
{
|
|
200
|
-
"success": True,
|
|
201
|
-
"updates": updates,
|
|
202
|
-
"plot": plot_data,
|
|
203
|
-
}
|
|
204
|
-
)
|
|
173
|
+
value = request.json["value"]
|
|
174
|
+
self._handle_parameter_update(name, value)
|
|
175
|
+
return jsonify({"success": True})
|
|
205
176
|
except Exception as e:
|
|
206
|
-
print(f"Error updating parameter: {str(e)}") # Debug log
|
|
207
177
|
return jsonify({"error": str(e)}), 400
|
|
208
178
|
|
|
209
|
-
@
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
name = data.get("name")
|
|
179
|
+
@app.route("/state")
|
|
180
|
+
def get_state():
|
|
181
|
+
return jsonify(self.viewer.state)
|
|
213
182
|
|
|
214
|
-
|
|
183
|
+
@app.route("/plot")
|
|
184
|
+
def get_plot():
|
|
185
|
+
# Generate plot and convert to base64 PNG
|
|
186
|
+
figure = self._generate_plot()
|
|
187
|
+
if figure is None:
|
|
188
|
+
return jsonify({"error": "Failed to generate plot"}), 500
|
|
215
189
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
190
|
+
# Save plot to bytes buffer
|
|
191
|
+
buffer = BytesIO()
|
|
192
|
+
figure.savefig(buffer, format="png", bbox_inches="tight")
|
|
193
|
+
buffer.seek(0)
|
|
194
|
+
image_base64 = base64.b64encode(buffer.getvalue()).decode()
|
|
219
195
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
updates = self._get_parameter_updates()
|
|
233
|
-
plot_data = self._get_current_plot_data()
|
|
234
|
-
# Debug log
|
|
235
|
-
print(f"Generated updates for parameters: {list(updates.keys())}")
|
|
236
|
-
|
|
237
|
-
return jsonify(
|
|
238
|
-
{
|
|
239
|
-
"success": True,
|
|
240
|
-
"updates": updates,
|
|
241
|
-
"plot": plot_data,
|
|
242
|
-
}
|
|
243
|
-
)
|
|
244
|
-
except Exception as e:
|
|
245
|
-
print(f"Error handling button click: {str(e)}") # Debug log
|
|
246
|
-
return jsonify({"error": str(e)}), 400
|
|
196
|
+
return jsonify({"image": image_base64})
|
|
197
|
+
|
|
198
|
+
return app
|
|
199
|
+
|
|
200
|
+
def _create_parameter_components(self) -> None:
|
|
201
|
+
"""Create component instances for all parameters."""
|
|
202
|
+
style = ComponentStyle(
|
|
203
|
+
width="100%",
|
|
204
|
+
margin="10px 0",
|
|
205
|
+
description_width="auto",
|
|
206
|
+
)
|
|
247
207
|
|
|
248
|
-
def _create_components(self):
|
|
249
|
-
"""Create web components for all parameters."""
|
|
250
208
|
for name, param in self.viewer.parameters.items():
|
|
251
|
-
|
|
209
|
+
component = create_component(
|
|
210
|
+
param,
|
|
211
|
+
continuous=self.continuous,
|
|
212
|
+
style=style,
|
|
213
|
+
)
|
|
214
|
+
self.parameter_components[name] = component
|
|
215
|
+
|
|
216
|
+
def _get_component_html(self) -> List[str]:
|
|
217
|
+
"""Get HTML for all components."""
|
|
218
|
+
if not self.parameter_components:
|
|
219
|
+
self._create_parameter_components()
|
|
220
|
+
return [comp.html for comp in self.parameter_components.values()]
|
|
221
|
+
|
|
222
|
+
@debounce(0.2)
|
|
223
|
+
def _handle_parameter_update(self, name: str, value: Any) -> None:
|
|
224
|
+
"""Handle updates to parameter values."""
|
|
225
|
+
if self._updating:
|
|
226
|
+
print(
|
|
227
|
+
"Already updating -- there's a circular dependency!"
|
|
228
|
+
"This is probably caused by failing to disable callbacks for a parameter."
|
|
229
|
+
"It's a bug --- tell the developer on github issues please."
|
|
230
|
+
)
|
|
231
|
+
return
|
|
252
232
|
|
|
253
|
-
@contextmanager
|
|
254
|
-
def _plot_context(self):
|
|
255
|
-
"""Context manager for thread-safe plotting."""
|
|
256
|
-
plt.ioff()
|
|
257
233
|
try:
|
|
258
|
-
|
|
234
|
+
self._updating = True
|
|
235
|
+
|
|
236
|
+
# Optionally suppress warnings during parameter updates
|
|
237
|
+
with warnings.catch_warnings():
|
|
238
|
+
if self.suppress_warnings:
|
|
239
|
+
warnings.filterwarnings("ignore", category=ParameterUpdateWarning)
|
|
240
|
+
|
|
241
|
+
component = self.parameter_components[name]
|
|
242
|
+
if component._is_action:
|
|
243
|
+
parameter = self.viewer.parameters[name]
|
|
244
|
+
parameter.callback(self.viewer.state)
|
|
245
|
+
else:
|
|
246
|
+
self.viewer.set_parameter_value(name, value)
|
|
247
|
+
|
|
248
|
+
# Update any components that changed due to dependencies
|
|
249
|
+
self._sync_components_with_state(exclude=name)
|
|
250
|
+
|
|
259
251
|
finally:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def
|
|
263
|
-
"""
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
with self._plot_context(), self._figure_lock:
|
|
268
|
-
new_fig = self.viewer.plot(state)
|
|
269
|
-
plt.close(self._current_figure) # Close old figure
|
|
270
|
-
self._current_figure = new_fig
|
|
271
|
-
print("Plot updated successfully") # Debug log
|
|
272
|
-
|
|
273
|
-
def _get_current_plot_data(self) -> str:
|
|
274
|
-
"""Get the current plot as a base64 encoded PNG."""
|
|
275
|
-
with self._figure_lock:
|
|
276
|
-
if self._current_figure is None:
|
|
277
|
-
return ""
|
|
278
|
-
|
|
279
|
-
buffer = io.BytesIO()
|
|
280
|
-
self._current_figure.savefig(
|
|
281
|
-
buffer,
|
|
282
|
-
format="png",
|
|
283
|
-
bbox_inches="tight",
|
|
284
|
-
dpi=300,
|
|
285
|
-
)
|
|
286
|
-
buffer.seek(0)
|
|
287
|
-
image_png = buffer.getvalue()
|
|
288
|
-
buffer.close()
|
|
289
|
-
|
|
290
|
-
graphic = base64.b64encode(image_png).decode("utf-8")
|
|
291
|
-
return f"data:image/png;base64,{graphic}"
|
|
292
|
-
|
|
293
|
-
def _get_parameter_updates(self) -> Dict[str, Any]:
|
|
294
|
-
"""Get JavaScript updates for all parameters."""
|
|
295
|
-
updates = {}
|
|
296
|
-
state = self.viewer.get_state()
|
|
297
|
-
for name, value in state.items():
|
|
298
|
-
# Skip button parameters since they don't have a meaningful value to update
|
|
299
|
-
if (
|
|
300
|
-
hasattr(self.viewer.parameters[name], "_is_button")
|
|
301
|
-
and self.viewer.parameters[name]._is_button
|
|
302
|
-
):
|
|
252
|
+
self._updating = False
|
|
253
|
+
|
|
254
|
+
def _sync_components_with_state(self, exclude: Optional[str] = None) -> None:
|
|
255
|
+
"""Sync component values with viewer state."""
|
|
256
|
+
for name, parameter in self.viewer.parameters.items():
|
|
257
|
+
if name == exclude:
|
|
303
258
|
continue
|
|
304
|
-
updates[name] = self.components.get_update_js(name, value)
|
|
305
|
-
return updates
|
|
306
|
-
|
|
307
|
-
def _render_page(self) -> str:
|
|
308
|
-
"""Render the complete HTML page."""
|
|
309
|
-
# Create initial plot
|
|
310
|
-
self._update_plot()
|
|
311
|
-
|
|
312
|
-
return render_template_string(
|
|
313
|
-
PAGE_TEMPLATE,
|
|
314
|
-
config=self.config,
|
|
315
|
-
components_html=self.components.get_all_html(),
|
|
316
|
-
components_init=self.components.get_init_js(),
|
|
317
|
-
initial_plot=self._get_current_plot_data(),
|
|
318
|
-
required_css=self.components.get_required_css(),
|
|
319
|
-
required_js=self.components.get_required_js(),
|
|
320
|
-
custom_styles=self.components.get_custom_styles(),
|
|
321
|
-
)
|
|
322
259
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# Create components
|
|
327
|
-
self._create_components()
|
|
260
|
+
component = self.parameter_components[name]
|
|
261
|
+
if not component.matches_parameter(parameter):
|
|
262
|
+
component.update_from_parameter(parameter)
|
|
328
263
|
|
|
329
|
-
|
|
330
|
-
|
|
264
|
+
def _generate_plot(self) -> Optional[plt.Figure]:
|
|
265
|
+
"""Generate the current plot."""
|
|
266
|
+
try:
|
|
267
|
+
with _plot_context():
|
|
268
|
+
figure = self.viewer.plot(self.viewer.state)
|
|
269
|
+
|
|
270
|
+
# Close the last figure if it exists to keep matplotlib clean
|
|
271
|
+
if self._last_figure is not None:
|
|
272
|
+
plt.close(self._last_figure)
|
|
273
|
+
|
|
274
|
+
self._last_figure = figure
|
|
275
|
+
return figure
|
|
276
|
+
except Exception as e:
|
|
277
|
+
print(f"Error generating plot: {e}")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def deploy(self, open_browser: bool = True) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Deploy the viewer as a Flask web application.
|
|
283
|
+
|
|
284
|
+
Parameters
|
|
285
|
+
----------
|
|
286
|
+
open_browser : bool, optional
|
|
287
|
+
Whether to automatically open the browser when deploying (default: True)
|
|
288
|
+
"""
|
|
289
|
+
with self.viewer._deploy_app():
|
|
290
|
+
if open_browser:
|
|
291
|
+
# Open browser in a separate thread to not block
|
|
292
|
+
threading.Timer(
|
|
293
|
+
1.0, lambda: webbrowser.open(f"http://{self.host}:{self.port}")
|
|
294
|
+
).start()
|
|
331
295
|
|
|
332
296
|
# Start Flask app
|
|
333
297
|
self.app.run(
|
|
334
|
-
host=self.
|
|
335
|
-
port=self.
|
|
298
|
+
host=self.host,
|
|
299
|
+
port=self.port,
|
|
336
300
|
debug=False, # Debug mode doesn't work well with matplotlib
|
|
337
|
-
use_reloader=False, #
|
|
301
|
+
use_reloader=False, # Reloader causes issues with matplotlib
|
|
338
302
|
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* General layout */
|
|
2
|
+
html, body {
|
|
3
|
+
height: 100vh;
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.viewer-container {
|
|
10
|
+
height: 100vh;
|
|
11
|
+
width: 100vw;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Controls container */
|
|
15
|
+
.controls-container {
|
|
16
|
+
background-color: #f8f9fa;
|
|
17
|
+
border-right: 1px solid #dee2e6;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.w-controls {
|
|
22
|
+
width: var(--controls-width);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Plot container */
|
|
26
|
+
.plot-container {
|
|
27
|
+
background-color: white;
|
|
28
|
+
min-height: 300px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.plot-container img {
|
|
32
|
+
max-width: var(--figure-width);
|
|
33
|
+
max-height: var(--figure-height);
|
|
34
|
+
object-fit: contain;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Form controls */
|
|
38
|
+
.form-control {
|
|
39
|
+
margin-bottom: 1rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.form-label {
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
margin-bottom: 0.5rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Range inputs */
|
|
48
|
+
input[type="range"] {
|
|
49
|
+
width: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
output {
|
|
53
|
+
margin-left: 0.5rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Multiple select */
|
|
57
|
+
select[multiple] {
|
|
58
|
+
height: auto;
|
|
59
|
+
min-height: 100px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Button styling */
|
|
63
|
+
.btn-primary {
|
|
64
|
+
width: 100%;
|
|
65
|
+
margin-bottom: 1rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Responsive adjustments */
|
|
69
|
+
@media (max-width: 768px) {
|
|
70
|
+
.viewer-container {
|
|
71
|
+
flex-direction: column !important;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.controls-container {
|
|
75
|
+
width: 100% !important;
|
|
76
|
+
max-height: 40vh;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.plot-container {
|
|
80
|
+
height: 60vh;
|
|
81
|
+
}
|
|
82
|
+
}
|