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,302 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional, List
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from time import time
|
|
7
|
+
import base64
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
import threading
|
|
10
|
+
import webbrowser
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from flask import Flask, render_template, jsonify, request, Response
|
|
14
|
+
import matplotlib as mpl
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
|
|
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
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class LayoutConfig:
|
|
68
|
+
"""Configuration for the viewer layout."""
|
|
69
|
+
|
|
70
|
+
controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
|
|
71
|
+
figure_width: float = 8.0
|
|
72
|
+
figure_height: float = 6.0
|
|
73
|
+
controls_width_percent: int = 30
|
|
74
|
+
template_path: Optional[str] = None
|
|
75
|
+
static_path: Optional[str] = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
valid_positions = ["left", "top", "right", "bottom"]
|
|
79
|
+
if self.controls_position not in valid_positions:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_horizontal(self) -> bool:
|
|
86
|
+
return self.controls_position == "left" or self.controls_position == "right"
|
|
87
|
+
|
|
88
|
+
|
|
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
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
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,
|
|
108
|
+
):
|
|
109
|
+
self.viewer = viewer
|
|
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")
|
|
140
|
+
|
|
141
|
+
app = Flask(__name__, template_folder=template_path, static_folder=static_path)
|
|
142
|
+
|
|
143
|
+
# Register routes
|
|
144
|
+
@app.route("/")
|
|
145
|
+
def index():
|
|
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
|
+
)
|
|
166
|
+
|
|
167
|
+
@app.route("/update/<name>", methods=["POST"])
|
|
168
|
+
def update_parameter(name: str):
|
|
169
|
+
if name not in self.viewer.parameters:
|
|
170
|
+
return jsonify({"error": f"Parameter {name} not found"}), 404
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
value = request.json["value"]
|
|
174
|
+
self._handle_parameter_update(name, value)
|
|
175
|
+
return jsonify({"success": True})
|
|
176
|
+
except Exception as e:
|
|
177
|
+
return jsonify({"error": str(e)}), 400
|
|
178
|
+
|
|
179
|
+
@app.route("/state")
|
|
180
|
+
def get_state():
|
|
181
|
+
return jsonify(self.viewer.state)
|
|
182
|
+
|
|
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
|
|
189
|
+
|
|
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()
|
|
195
|
+
|
|
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
|
+
)
|
|
207
|
+
|
|
208
|
+
for name, param in self.viewer.parameters.items():
|
|
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
|
|
232
|
+
|
|
233
|
+
try:
|
|
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
|
+
|
|
251
|
+
finally:
|
|
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:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
component = self.parameter_components[name]
|
|
261
|
+
if not component.matches_parameter(parameter):
|
|
262
|
+
component.update_from_parameter(parameter)
|
|
263
|
+
|
|
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()
|
|
295
|
+
|
|
296
|
+
# Start Flask app
|
|
297
|
+
self.app.run(
|
|
298
|
+
host=self.host,
|
|
299
|
+
port=self.port,
|
|
300
|
+
debug=False, # Debug mode doesn't work well with matplotlib
|
|
301
|
+
use_reloader=False, # Reloader causes issues with matplotlib
|
|
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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
class SydViewer {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.config = {
|
|
4
|
+
controlsPosition: 'left',
|
|
5
|
+
continuous: false,
|
|
6
|
+
updateInterval: 200,
|
|
7
|
+
...config
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
this.form = document.getElementById('controls-form');
|
|
11
|
+
this.plot = document.getElementById('plot');
|
|
12
|
+
this.updateTimer = null;
|
|
13
|
+
this.setupEventListeners();
|
|
14
|
+
this.updatePlot();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setupEventListeners() {
|
|
18
|
+
// Handle all form input changes
|
|
19
|
+
this.form.addEventListener('input', (event) => {
|
|
20
|
+
const input = event.target;
|
|
21
|
+
if (input.dataset.continuous === 'true' || this.config.continuous) {
|
|
22
|
+
this.debounceUpdate(() => this.handleInputChange(input));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Handle form changes for non-continuous updates
|
|
27
|
+
this.form.addEventListener('change', (event) => {
|
|
28
|
+
const input = event.target;
|
|
29
|
+
if (input.dataset.continuous !== 'true' && !this.config.continuous) {
|
|
30
|
+
this.handleInputChange(input);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Handle button clicks
|
|
35
|
+
this.form.querySelectorAll('button[type="button"]').forEach(button => {
|
|
36
|
+
button.addEventListener('click', () => this.handleButtonClick(button));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
debounceUpdate(callback) {
|
|
41
|
+
if (this.updateTimer) {
|
|
42
|
+
clearTimeout(this.updateTimer);
|
|
43
|
+
}
|
|
44
|
+
this.updateTimer = setTimeout(callback, this.config.updateInterval);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async handleInputChange(input) {
|
|
48
|
+
let value = this.getInputValue(input);
|
|
49
|
+
const name = input.name;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`/update/${name}`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ value }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.updatePlot();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Error updating parameter:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async handleButtonClick(button) {
|
|
71
|
+
const name = button.name;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(`/update/${name}`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ value: null }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await this.updatePlot();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error handling button click:', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getInputValue(input) {
|
|
92
|
+
switch (input.type) {
|
|
93
|
+
case 'checkbox':
|
|
94
|
+
return input.checked;
|
|
95
|
+
case 'number':
|
|
96
|
+
return parseFloat(input.value);
|
|
97
|
+
case 'range':
|
|
98
|
+
if (input.name.endsWith('_low')) {
|
|
99
|
+
const high = document.getElementById(input.id.replace('_low', '_high')).value;
|
|
100
|
+
return [parseFloat(input.value), parseFloat(high)];
|
|
101
|
+
} else if (input.name.endsWith('_high')) {
|
|
102
|
+
const low = document.getElementById(input.id.replace('_high', '_low')).value;
|
|
103
|
+
return [parseFloat(low), parseFloat(input.value)];
|
|
104
|
+
}
|
|
105
|
+
return parseFloat(input.value);
|
|
106
|
+
case 'select-multiple':
|
|
107
|
+
return Array.from(input.selectedOptions).map(option => {
|
|
108
|
+
const value = option.value;
|
|
109
|
+
return !isNaN(value) ? parseFloat(value) : value;
|
|
110
|
+
});
|
|
111
|
+
default:
|
|
112
|
+
const value = input.value;
|
|
113
|
+
return !isNaN(value) ? parseFloat(value) : value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async updatePlot() {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch('/plot');
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = await response.json();
|
|
125
|
+
if (data.error) {
|
|
126
|
+
throw new Error(data.error);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.plot.src = `data:image/png;base64,${data.image}`;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Error updating plot:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async updateState() {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch('/state');
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const state = await response.json();
|
|
143
|
+
this.syncFormWithState(state);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error updating state:', error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
syncFormWithState(state) {
|
|
150
|
+
for (const [name, value] of Object.entries(state)) {
|
|
151
|
+
const input = this.form.elements[name];
|
|
152
|
+
if (!input) continue;
|
|
153
|
+
|
|
154
|
+
if (input.type === 'checkbox') {
|
|
155
|
+
input.checked = value;
|
|
156
|
+
} else if (input.type === 'select-multiple') {
|
|
157
|
+
Array.from(input.options).forEach(option => {
|
|
158
|
+
option.selected = value.includes(option.value);
|
|
159
|
+
});
|
|
160
|
+
} else if (input.type === 'range' && Array.isArray(value)) {
|
|
161
|
+
const [low, high] = value;
|
|
162
|
+
document.getElementById(`param_${name}_low`).value = low;
|
|
163
|
+
document.getElementById(`param_${name}_high`).value = high;
|
|
164
|
+
document.querySelector(`output[for="param_${name}_low"]`).value = low;
|
|
165
|
+
document.querySelector(`output[for="param_${name}_high"]`).value = high;
|
|
166
|
+
} else {
|
|
167
|
+
input.value = value;
|
|
168
|
+
if (input.type === 'range') {
|
|
169
|
+
document.querySelector(`output[for="${input.id}"]`).value = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% block title %}Syd Viewer{% endblock %}</title>
|
|
7
|
+
|
|
8
|
+
<!-- Bootstrap CSS -->
|
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<!-- Custom CSS -->
|
|
12
|
+
<link href="{{ url_for('static', filename='css/viewer.css') }}" rel="stylesheet">
|
|
13
|
+
|
|
14
|
+
{% block extra_head %}{% endblock %}
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container-fluid p-0">
|
|
18
|
+
{% block content %}{% endblock %}
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Bootstrap Bundle with Popper -->
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
23
|
+
|
|
24
|
+
<!-- Custom JavaScript -->
|
|
25
|
+
<script src="{{ url_for('static', filename='js/viewer.js') }}"></script>
|
|
26
|
+
|
|
27
|
+
{% block extra_scripts %}{% endblock %}
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block extra_head %}
|
|
4
|
+
<style>
|
|
5
|
+
:root {
|
|
6
|
+
--controls-width: {{ controls_width }}%;
|
|
7
|
+
--figure-width: {{ figure_width }}px;
|
|
8
|
+
--figure-height: {{ figure_height }}px;
|
|
9
|
+
}
|
|
10
|
+
</style>
|
|
11
|
+
{% endblock %}
|
|
12
|
+
|
|
13
|
+
{% block content %}
|
|
14
|
+
<div class="viewer-container {% if controls_position in ['left', 'right'] %}d-flex flex-row{% else %}d-flex flex-column{% endif %} h-100"
|
|
15
|
+
data-controls-position="{{ controls_position }}"
|
|
16
|
+
data-continuous="{{ continuous|default(false)|tojson }}">
|
|
17
|
+
{% if controls_position in ['left', 'top'] %}
|
|
18
|
+
<div class="controls-container {% if controls_position == 'left' %}w-controls{% else %}w-100{% endif %}">
|
|
19
|
+
<form id="controls-form" class="p-3">
|
|
20
|
+
{{ components|join('')|safe }}
|
|
21
|
+
</form>
|
|
22
|
+
</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
<div class="plot-container {% if controls_position in ['left', 'right'] %}flex-grow-1{% else %}w-100{% endif %} d-flex justify-content-center align-items-center p-3">
|
|
26
|
+
<img id="plot" src="{% if initial_plot %}data:image/png;base64,{{ initial_plot }}{% endif %}" alt="Plot" class="img-fluid">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{% if controls_position in ['right', 'bottom'] %}
|
|
30
|
+
<div class="controls-container {% if controls_position == 'right' %}w-controls{% else %}w-100{% endif %}">
|
|
31
|
+
<form id="controls-form" class="p-3">
|
|
32
|
+
{{ components|join('')|safe }}
|
|
33
|
+
</form>
|
|
34
|
+
</div>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endblock %}
|
|
38
|
+
|
|
39
|
+
{% block extra_scripts %}
|
|
40
|
+
<script>
|
|
41
|
+
// Initialize the viewer with configuration
|
|
42
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
43
|
+
const container = document.querySelector('.viewer-container');
|
|
44
|
+
window.viewer = new SydViewer({
|
|
45
|
+
controlsPosition: container.dataset.controlsPosition,
|
|
46
|
+
continuous: JSON.parse(container.dataset.continuous),
|
|
47
|
+
updateInterval: 200
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
{% endblock %}
|