syd 0.1.4__py3-none-any.whl → 0.1.6__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 +11 -1
- syd/flask_deployment/__init__.py +0 -0
- syd/flask_deployment/components.py +497 -0
- syd/flask_deployment/deployer.py +338 -0
- syd/flask_deployment/static/css/styles.css +39 -0
- syd/flask_deployment/static/js/components.js +51 -0
- syd/flask_deployment/static/js/viewer.js +0 -0
- syd/flask_deployment/templates/base.html +26 -0
- syd/flask_deployment/templates/viewer.html +97 -0
- syd/interactive_viewer.py +1314 -87
- syd/notebook_deployment/__init__.py +1 -0
- syd/notebook_deployment/deployer.py +254 -0
- syd/notebook_deployment/widgets.py +478 -0
- syd/parameters.py +1421 -157
- syd-0.1.6.dist-info/METADATA +106 -0
- syd-0.1.6.dist-info/RECORD +18 -0
- syd/notebook_deploy.py +0 -277
- syd-0.1.4.dist-info/METADATA +0 -33
- syd-0.1.4.dist-info/RECORD +0 -8
- {syd-0.1.4.dist-info → syd-0.1.6.dist-info}/WHEEL +0 -0
- {syd-0.1.4.dist-info → syd-0.1.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import threading
|
|
4
|
+
from flask import Flask, render_template_string, jsonify, request
|
|
5
|
+
import matplotlib
|
|
6
|
+
|
|
7
|
+
matplotlib.use("Agg") # Use Agg backend for thread safety
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import io
|
|
10
|
+
import base64
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
import webbrowser
|
|
13
|
+
|
|
14
|
+
from ..interactive_viewer import InteractiveViewer
|
|
15
|
+
from .components import WebComponentCollection
|
|
16
|
+
|
|
17
|
+
# Create a template constant to hold our HTML template
|
|
18
|
+
PAGE_TEMPLATE = """
|
|
19
|
+
<!DOCTYPE html>
|
|
20
|
+
<html>
|
|
21
|
+
<head>
|
|
22
|
+
<title>Interactive Viewer</title>
|
|
23
|
+
{% for css in required_css %}
|
|
24
|
+
<link rel="stylesheet" href="{{ css }}">
|
|
25
|
+
{% endfor %}
|
|
26
|
+
<style>
|
|
27
|
+
{{ custom_styles | safe }}
|
|
28
|
+
.controls {
|
|
29
|
+
{% if config.is_horizontal %}
|
|
30
|
+
width: {{ config.controls_width_percent }}%;
|
|
31
|
+
float: left;
|
|
32
|
+
padding-right: 20px;
|
|
33
|
+
{% endif %}
|
|
34
|
+
}
|
|
35
|
+
.plot-container {
|
|
36
|
+
{% if config.is_horizontal %}
|
|
37
|
+
width: {{ 100 - config.controls_width_percent }}%;
|
|
38
|
+
float: left;
|
|
39
|
+
{% endif %}
|
|
40
|
+
}
|
|
41
|
+
#plot {
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: auto;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<div class="container-fluid">
|
|
49
|
+
<div class="row">
|
|
50
|
+
<div class="controls">
|
|
51
|
+
{{ components_html | safe }}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="plot-container">
|
|
54
|
+
<img id="plot" src="{{ initial_plot }}">
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{% for js in required_js %}
|
|
60
|
+
<script src="{{ js }}"></script>
|
|
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
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class FlaskLayoutConfig:
|
|
120
|
+
"""Configuration for the viewer layout in Flask deployment."""
|
|
121
|
+
|
|
122
|
+
controls_position: str = "left" # Options are: 'left', 'top'
|
|
123
|
+
figure_width: float = 8.0
|
|
124
|
+
figure_height: float = 6.0
|
|
125
|
+
controls_width_percent: int = 30
|
|
126
|
+
port: int = 5000
|
|
127
|
+
host: str = "localhost"
|
|
128
|
+
|
|
129
|
+
def __post_init__(self):
|
|
130
|
+
valid_positions = ["left", "top"]
|
|
131
|
+
if self.controls_position not in valid_positions:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Invalid controls position: {self.controls_position}. Must be one of {valid_positions}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def is_horizontal(self) -> bool:
|
|
138
|
+
return self.controls_position == "left"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FlaskDeployment:
|
|
142
|
+
"""A deployment system for InteractiveViewer using Flask to create a web interface."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
viewer: InteractiveViewer,
|
|
147
|
+
layout_config: Optional[FlaskLayoutConfig] = None,
|
|
148
|
+
):
|
|
149
|
+
if not isinstance(viewer, InteractiveViewer):
|
|
150
|
+
raise TypeError(
|
|
151
|
+
f"viewer must be an InteractiveViewer, got {type(viewer).__name__}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self.viewer = viewer
|
|
155
|
+
self.config = layout_config or FlaskLayoutConfig()
|
|
156
|
+
self.components = WebComponentCollection()
|
|
157
|
+
|
|
158
|
+
# Initialize Flask app
|
|
159
|
+
self.app = Flask(__name__)
|
|
160
|
+
self._setup_routes()
|
|
161
|
+
|
|
162
|
+
# Store current figure
|
|
163
|
+
self._current_figure = None
|
|
164
|
+
self._figure_lock = threading.Lock()
|
|
165
|
+
|
|
166
|
+
def _setup_routes(self):
|
|
167
|
+
"""Set up the Flask routes for the application."""
|
|
168
|
+
|
|
169
|
+
@self.app.route("/")
|
|
170
|
+
def index():
|
|
171
|
+
return self._render_page()
|
|
172
|
+
|
|
173
|
+
@self.app.route("/update_parameter", methods=["POST"])
|
|
174
|
+
def update_parameter():
|
|
175
|
+
data = request.json
|
|
176
|
+
name = data.get("name")
|
|
177
|
+
value = data.get("value")
|
|
178
|
+
|
|
179
|
+
print(f"Received parameter update: {name} = {value}") # Debug log
|
|
180
|
+
|
|
181
|
+
if name not in self.viewer.parameters:
|
|
182
|
+
print(f"Parameter {name} not found in viewer parameters") # Debug log
|
|
183
|
+
return jsonify({"error": f"Parameter {name} not found"}), 404
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
print(f"Setting parameter value: {name} = {value}") # Debug log
|
|
187
|
+
self.viewer.set_parameter_value(name, value)
|
|
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
|
+
)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print(f"Error updating parameter: {str(e)}") # Debug log
|
|
207
|
+
return jsonify({"error": str(e)}), 400
|
|
208
|
+
|
|
209
|
+
@self.app.route("/button_click", methods=["POST"])
|
|
210
|
+
def button_click():
|
|
211
|
+
data = request.json
|
|
212
|
+
name = data.get("name")
|
|
213
|
+
|
|
214
|
+
print(f"Received button click: {name}") # Debug log
|
|
215
|
+
|
|
216
|
+
if name not in self.viewer.parameters:
|
|
217
|
+
print(f"Button {name} not found in viewer parameters") # Debug log
|
|
218
|
+
return jsonify({"error": f"Parameter {name} not found"}), 404
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
parameter = self.viewer.parameters[name]
|
|
222
|
+
if hasattr(parameter, "callback"):
|
|
223
|
+
print(f"Executing callback for button: {name}") # Debug log
|
|
224
|
+
parameter.callback(self.viewer.get_state())
|
|
225
|
+
else:
|
|
226
|
+
print(f"No callback found for button: {name}") # Debug log
|
|
227
|
+
|
|
228
|
+
# Update the plot after button click
|
|
229
|
+
print("Updating plot after button click...") # Debug log
|
|
230
|
+
self._update_plot()
|
|
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
|
|
247
|
+
|
|
248
|
+
def _create_components(self):
|
|
249
|
+
"""Create web components for all parameters."""
|
|
250
|
+
for name, param in self.viewer.parameters.items():
|
|
251
|
+
self.components.add_component(name, param)
|
|
252
|
+
|
|
253
|
+
@contextmanager
|
|
254
|
+
def _plot_context(self):
|
|
255
|
+
"""Context manager for thread-safe plotting."""
|
|
256
|
+
plt.ioff()
|
|
257
|
+
try:
|
|
258
|
+
yield
|
|
259
|
+
finally:
|
|
260
|
+
plt.ion()
|
|
261
|
+
|
|
262
|
+
def _update_plot(self) -> None:
|
|
263
|
+
"""Update the plot with current state."""
|
|
264
|
+
state = self.viewer.get_state()
|
|
265
|
+
print(f"Updating plot with state: {state}") # Debug log
|
|
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
|
+
):
|
|
303
|
+
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
|
+
|
|
323
|
+
def deploy(self) -> None:
|
|
324
|
+
"""Deploy the interactive viewer as a web application."""
|
|
325
|
+
with self.viewer._deploy_app():
|
|
326
|
+
# Create components
|
|
327
|
+
self._create_components()
|
|
328
|
+
|
|
329
|
+
# Open browser
|
|
330
|
+
webbrowser.open(f"http://{self.config.host}:{self.config.port}")
|
|
331
|
+
|
|
332
|
+
# Start Flask app
|
|
333
|
+
self.app.run(
|
|
334
|
+
host=self.config.host,
|
|
335
|
+
port=self.config.port,
|
|
336
|
+
debug=False, # Debug mode doesn't work well with matplotlib
|
|
337
|
+
use_reloader=False, # Prevent double startup
|
|
338
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.controls {
|
|
2
|
+
padding: 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.controls.horizontal {
|
|
6
|
+
width: var(--controls-width);
|
|
7
|
+
float: left;
|
|
8
|
+
padding-right: 20px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.plot-container {
|
|
12
|
+
padding: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.plot-container.horizontal {
|
|
16
|
+
width: var(--plot-width);
|
|
17
|
+
float: left;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#plot {
|
|
21
|
+
width: 100%;
|
|
22
|
+
height: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.slider, .range-slider {
|
|
26
|
+
margin: 10px 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.value-display {
|
|
30
|
+
display: block;
|
|
31
|
+
text-align: center;
|
|
32
|
+
margin-top: 5px;
|
|
33
|
+
font-size: 0.9em;
|
|
34
|
+
color: #666;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.form-group {
|
|
38
|
+
margin-bottom: 1rem;
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function updateParameter(name, value) {
|
|
2
|
+
console.log(`Sending parameter update: ${name} = ${value}`); // Debug log
|
|
3
|
+
fetch('/update_parameter', {
|
|
4
|
+
method: 'POST',
|
|
5
|
+
headers: {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
},
|
|
8
|
+
body: JSON.stringify({name, value})
|
|
9
|
+
})
|
|
10
|
+
.then(response => response.json())
|
|
11
|
+
.then(data => {
|
|
12
|
+
if (data.error) {
|
|
13
|
+
console.error(`Error updating parameter: ${data.error}`); // Debug log
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log('Update successful, applying updates'); // Debug log
|
|
17
|
+
// Update plot
|
|
18
|
+
document.getElementById('plot').src = data.plot;
|
|
19
|
+
// Apply any parameter updates
|
|
20
|
+
for (const [param, js] of Object.entries(data.updates)) {
|
|
21
|
+
console.log(`Applying update for ${param}`); // Debug log
|
|
22
|
+
eval(js);
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.catch(error => {
|
|
26
|
+
console.error('Error in updateParameter:', error); // Debug log
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buttonClick(name) {
|
|
31
|
+
fetch('/button_click', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({name})
|
|
37
|
+
})
|
|
38
|
+
.then(response => response.json())
|
|
39
|
+
.then(data => {
|
|
40
|
+
if (data.error) {
|
|
41
|
+
console.error(data.error);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Update plot
|
|
45
|
+
document.getElementById('plot').src = data.plot;
|
|
46
|
+
// Apply any parameter updates
|
|
47
|
+
for (const [param, js] of Object.entries(data.updates)) {
|
|
48
|
+
eval(js);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>{% block title %}Interactive Viewer{% endblock %}</title>
|
|
5
|
+
{% for css in required_css %}
|
|
6
|
+
<link rel="stylesheet" href="{{ css }}">
|
|
7
|
+
{% endfor %}
|
|
8
|
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--controls-width: {{ config.controls_width_percent }}%;
|
|
12
|
+
--plot-width: {{ 100 - config.controls_width_percent }}%;
|
|
13
|
+
}
|
|
14
|
+
{{ custom_styles | safe }}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
{% block content %}{% endblock %}
|
|
19
|
+
|
|
20
|
+
{% for js in required_js %}
|
|
21
|
+
<script src="{{ js }}"></script>
|
|
22
|
+
{% endfor %}
|
|
23
|
+
<script src="{{ url_for('static', filename='js/components.js') }}"></script>
|
|
24
|
+
{% block scripts %}{% endblock %}
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# templates/viewer.html
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<title>Interactive Viewer</title>
|
|
6
|
+
{% for css in required_css %}
|
|
7
|
+
<link rel="stylesheet" href="{{ css }}">
|
|
8
|
+
{% endfor %}
|
|
9
|
+
<style>
|
|
10
|
+
{{ custom_styles | safe }}
|
|
11
|
+
.controls {
|
|
12
|
+
{% if config.is_horizontal %}
|
|
13
|
+
width: {{ config.controls_width_percent }}%;
|
|
14
|
+
float: left;
|
|
15
|
+
padding-right: 20px;
|
|
16
|
+
{% endif %}
|
|
17
|
+
}
|
|
18
|
+
.plot-container {
|
|
19
|
+
{% if config.is_horizontal %}
|
|
20
|
+
width: {{ 100 - config.controls_width_percent }}%;
|
|
21
|
+
float: left;
|
|
22
|
+
{% endif %}
|
|
23
|
+
}
|
|
24
|
+
#plot {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div class="container-fluid">
|
|
32
|
+
<div class="row">
|
|
33
|
+
<div class="controls">
|
|
34
|
+
{{ components_html | safe }}
|
|
35
|
+
</div>
|
|
36
|
+
<div class="plot-container">
|
|
37
|
+
<img id="plot" src="{{ initial_plot }}">
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{% for js in required_js %}
|
|
43
|
+
<script src="{{ js }}"></script>
|
|
44
|
+
{% endfor %}
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
function updateParameter(name, value) {
|
|
48
|
+
fetch('/update_parameter', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({name, value})
|
|
54
|
+
})
|
|
55
|
+
.then(response => response.json())
|
|
56
|
+
.then(data => {
|
|
57
|
+
if (data.error) {
|
|
58
|
+
console.error(data.error);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Update plot
|
|
62
|
+
document.getElementById('plot').src = data.plot;
|
|
63
|
+
// Apply any parameter updates
|
|
64
|
+
for (const [param, js] of Object.entries(data.updates)) {
|
|
65
|
+
eval(js);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buttonClick(name) {
|
|
71
|
+
fetch('/button_click', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({name})
|
|
77
|
+
})
|
|
78
|
+
.then(response => response.json())
|
|
79
|
+
.then(data => {
|
|
80
|
+
if (data.error) {
|
|
81
|
+
console.error(data.error);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Update plot
|
|
85
|
+
document.getElementById('plot').src = data.plot;
|
|
86
|
+
// Apply any parameter updates
|
|
87
|
+
for (const [param, js] of Object.entries(data.updates)) {
|
|
88
|
+
eval(js);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Initialize components
|
|
94
|
+
{{ components_init | safe }}
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|