syd 1.1.0__py3-none-any.whl → 1.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.
- syd/__init__.py +2 -1
- syd/flask_deployment/deployer.py +107 -23
- syd/flask_deployment/static/css/styles.css +77 -62
- syd/flask_deployment/static/css/viewer.css +0 -42
- syd/flask_deployment/static/js/modules/api.js +89 -0
- syd/flask_deployment/static/js/modules/config.js +22 -0
- syd/flask_deployment/static/js/modules/plot.js +75 -0
- syd/flask_deployment/static/js/modules/state.js +89 -0
- syd/flask_deployment/static/js/modules/system_controls.js +191 -0
- syd/flask_deployment/static/js/modules/ui_controls.js +812 -0
- syd/flask_deployment/static/js/modules/utils.js +49 -0
- syd/flask_deployment/static/js/old_viewer.js +1195 -0
- syd/flask_deployment/static/js/viewer.js +40 -1177
- syd/flask_deployment/templates/index.html +1 -1
- syd/support.py +25 -0
- syd/viewer.py +4 -0
- {syd-1.1.0.dist-info → syd-1.2.0.dist-info}/METADATA +18 -5
- syd-1.2.0.dist-info/RECORD +28 -0
- syd-1.1.0.dist-info/RECORD +0 -20
- {syd-1.1.0.dist-info → syd-1.2.0.dist-info}/WHEEL +0 -0
- {syd-1.1.0.dist-info → syd-1.2.0.dist-info}/licenses/LICENSE +0 -0
syd/__init__.py
CHANGED
syd/flask_deployment/deployer.py
CHANGED
|
@@ -6,7 +6,6 @@ from dataclasses import dataclass
|
|
|
6
6
|
import matplotlib as mpl
|
|
7
7
|
import matplotlib.pyplot as plt
|
|
8
8
|
import io
|
|
9
|
-
import time
|
|
10
9
|
import webbrowser
|
|
11
10
|
import threading
|
|
12
11
|
import socket
|
|
@@ -20,7 +19,7 @@ from flask import (
|
|
|
20
19
|
jsonify,
|
|
21
20
|
render_template,
|
|
22
21
|
)
|
|
23
|
-
from werkzeug.serving import
|
|
22
|
+
from werkzeug.serving import make_server
|
|
24
23
|
|
|
25
24
|
# Use Deployer base class
|
|
26
25
|
from ..viewer import Viewer
|
|
@@ -43,6 +42,55 @@ from ..support import ParameterUpdateWarning, plot_context
|
|
|
43
42
|
mpl.use("Agg")
|
|
44
43
|
|
|
45
44
|
|
|
45
|
+
class ServerManager:
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.servers: dict[int, "ServerThread"] = {}
|
|
48
|
+
|
|
49
|
+
def register_server(self, server: "ServerThread", port: int):
|
|
50
|
+
self.servers[port] = server
|
|
51
|
+
|
|
52
|
+
def close_app(self, port: int | None = None):
|
|
53
|
+
if port is None:
|
|
54
|
+
for server in self.servers.values():
|
|
55
|
+
server.shutdown()
|
|
56
|
+
self.servers.clear()
|
|
57
|
+
else:
|
|
58
|
+
if port in self.servers:
|
|
59
|
+
self.servers[port].shutdown()
|
|
60
|
+
del self.servers[port]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
server_manager = ServerManager()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ServerThread(threading.Thread):
|
|
67
|
+
def __init__(self, host: str, port: int, app, debug: bool):
|
|
68
|
+
super().__init__(daemon=True)
|
|
69
|
+
self.server = make_server(host, port, app, threaded=True)
|
|
70
|
+
self.port = port
|
|
71
|
+
self.debug = debug
|
|
72
|
+
self.ready = threading.Event()
|
|
73
|
+
|
|
74
|
+
num_open_servers = len(server_manager.servers)
|
|
75
|
+
if num_open_servers >= 10:
|
|
76
|
+
open_servers = "\n".join([f"{port}" for port in server_manager.servers])
|
|
77
|
+
print(
|
|
78
|
+
f"\nYou have {num_open_servers} open servers!\n"
|
|
79
|
+
f"Open servers:\n{open_servers}\n"
|
|
80
|
+
"You can close them with syd.close_servers() or a particular one with syd.close_servers(port).\n"
|
|
81
|
+
"To see a list, use: syd.show_open_servers()."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def run(self):
|
|
85
|
+
server_manager.register_server(self, self.port)
|
|
86
|
+
self.ready.set()
|
|
87
|
+
self.server.serve_forever()
|
|
88
|
+
|
|
89
|
+
def shutdown(self):
|
|
90
|
+
# Call this to stop the server cleanly
|
|
91
|
+
self.server.shutdown()
|
|
92
|
+
|
|
93
|
+
|
|
46
94
|
@dataclass
|
|
47
95
|
class FlaskLayoutConfig:
|
|
48
96
|
"""Configuration for the Flask viewer layout."""
|
|
@@ -80,6 +128,7 @@ class FlaskDeployer:
|
|
|
80
128
|
port: Optional[int] = None,
|
|
81
129
|
open_browser: bool = True,
|
|
82
130
|
update_threshold: float = 1.0,
|
|
131
|
+
timeout_threshold: float = 10.0,
|
|
83
132
|
):
|
|
84
133
|
"""
|
|
85
134
|
Initialize the Flask deployer.
|
|
@@ -110,11 +159,14 @@ class FlaskDeployer:
|
|
|
110
159
|
Whether to open the web application in a browser tab (default: True).
|
|
111
160
|
update_threshold : float, optional
|
|
112
161
|
Time in seconds to wait before showing the loading indicator (default: 1.0)
|
|
162
|
+
timeout_threshold : float, optional
|
|
163
|
+
Time in seconds to wait for the browser to open (default: 10.0).
|
|
113
164
|
"""
|
|
114
165
|
self.viewer = viewer
|
|
115
166
|
self.suppress_warnings = suppress_warnings
|
|
116
167
|
self._updating = False # Flag to check circular updates
|
|
117
168
|
self.update_threshold = update_threshold # Store update threshold
|
|
169
|
+
self.timeout_threshold = timeout_threshold # Store timeout threshold
|
|
118
170
|
|
|
119
171
|
# Flask specific configurations
|
|
120
172
|
self.config = FlaskLayoutConfig(
|
|
@@ -324,7 +376,6 @@ class FlaskDeployer:
|
|
|
324
376
|
host: str = "127.0.0.1",
|
|
325
377
|
port: Optional[int] = None,
|
|
326
378
|
open_browser: bool = True,
|
|
327
|
-
**kwargs,
|
|
328
379
|
) -> None:
|
|
329
380
|
"""Starts the Flask development server."""
|
|
330
381
|
if not self.app:
|
|
@@ -338,26 +389,59 @@ class FlaskDeployer:
|
|
|
338
389
|
self.url = f"http://{self.host}:{self.port}"
|
|
339
390
|
print(f" * Syd Flask server running on {self.url}")
|
|
340
391
|
|
|
341
|
-
if open_browser:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
392
|
+
# if open_browser:
|
|
393
|
+
|
|
394
|
+
# def wait_until_responsive(url, timeout=self.timeout_threshold):
|
|
395
|
+
# start_time = time.time()
|
|
396
|
+
# while time.time() - start_time < timeout:
|
|
397
|
+
# try:
|
|
398
|
+
# r = requests.get(url, timeout=0.5)
|
|
399
|
+
# if r.status_code == 200:
|
|
400
|
+
# return True
|
|
401
|
+
# except requests.exceptions.RequestException:
|
|
402
|
+
# pass
|
|
403
|
+
# time.sleep(0.1)
|
|
404
|
+
# return False
|
|
405
|
+
|
|
406
|
+
# def open_browser_tab_when_ready():
|
|
407
|
+
# if wait_until_responsive(self.url):
|
|
408
|
+
# out = webbrowser.open(self.url, new=1, autoraise=True)
|
|
409
|
+
# else:
|
|
410
|
+
# print(
|
|
411
|
+
# f"Could not open browser: server at {self.url} not responding."
|
|
412
|
+
# f"Increase the timeout_threshold to fix this! It's set to {self.timeout_threshold} seconds."
|
|
413
|
+
# "You can do this from viewer.show(timeout_threshold=...) or in the FlaskDeployer constructor."
|
|
414
|
+
# "Also, this is unexpected so please report this issue on GitHub."
|
|
415
|
+
# )
|
|
416
|
+
|
|
417
|
+
# threading.Thread(target=open_browser_tab_when_ready, daemon=True).start()
|
|
418
|
+
|
|
419
|
+
# # Run the Flask server using Werkzeug's run_simple
|
|
420
|
+
# # Pass debug status to run_simple for auto-reloading
|
|
421
|
+
# run_simple(
|
|
422
|
+
# self.host,
|
|
423
|
+
# self.port,
|
|
424
|
+
# self.app,
|
|
425
|
+
# use_reloader=False,
|
|
426
|
+
# use_debugger=self.debug,
|
|
427
|
+
# )
|
|
428
|
+
|
|
429
|
+
# 1) Spin up the server thread
|
|
430
|
+
srv_thread = ServerThread(self.host, self.port, self.app, debug=self.debug)
|
|
431
|
+
srv_thread.start()
|
|
432
|
+
|
|
433
|
+
# 2) Wait for the socket‐bind event (not for an HTTP 200)
|
|
434
|
+
if not srv_thread.ready.wait(timeout=self.timeout_threshold):
|
|
435
|
+
print(
|
|
436
|
+
f"[!] Server did not bind within {self.timeout_threshold:.1f}s; it may already be in use."
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
# 3) Now we know the app is truly listening; open a focused window
|
|
440
|
+
if open_browser:
|
|
441
|
+
webbrowser.open(self.url, new=1, autoraise=True)
|
|
359
442
|
|
|
360
|
-
|
|
443
|
+
# 4) Keep the thread handle around so you can call srv_thread.shutdown()
|
|
444
|
+
self._server_thread = srv_thread
|
|
361
445
|
|
|
362
446
|
def deploy(self) -> None:
|
|
363
447
|
"""
|
|
@@ -607,7 +691,7 @@ class FlaskDeployer:
|
|
|
607
691
|
)
|
|
608
692
|
|
|
609
693
|
|
|
610
|
-
def _find_available_port(start_port=5000, max_attempts=
|
|
694
|
+
def _find_available_port(start_port=5000, max_attempts=1000):
|
|
611
695
|
"""
|
|
612
696
|
Find an available port starting from start_port.
|
|
613
697
|
(Identical to original)
|
|
@@ -92,33 +92,8 @@ input[type="number"] {
|
|
|
92
92
|
box-sizing: border-box;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/* Range inputs */
|
|
96
95
|
input[type="range"] {
|
|
97
96
|
width: 100%;
|
|
98
|
-
height: 12px;
|
|
99
|
-
background: #ddd;
|
|
100
|
-
border-radius: 3px;
|
|
101
|
-
outline: none;
|
|
102
|
-
margin: 5px 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
input[type="range"]::-webkit-slider-thumb {
|
|
106
|
-
-webkit-appearance: none;
|
|
107
|
-
width: 18px;
|
|
108
|
-
height: 18px;
|
|
109
|
-
border-radius: 50%;
|
|
110
|
-
background: #3f51b5;
|
|
111
|
-
cursor: pointer;
|
|
112
|
-
border: 1px solid #2c3e90;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
input[type="range"]::-moz-range-thumb {
|
|
116
|
-
width: 18px;
|
|
117
|
-
height: 18px;
|
|
118
|
-
border-radius: 50%;
|
|
119
|
-
background: #3f51b5;
|
|
120
|
-
cursor: pointer;
|
|
121
|
-
border: 1px solid #2c3e90;
|
|
122
97
|
}
|
|
123
98
|
|
|
124
99
|
/* Checkbox styling */
|
|
@@ -184,6 +159,49 @@ button.active {
|
|
|
184
159
|
font-style: italic;
|
|
185
160
|
}
|
|
186
161
|
|
|
162
|
+
/* Style all numeric controls consistently */
|
|
163
|
+
.numeric-control {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.numeric-control input[type="range"] {
|
|
169
|
+
flex: 1;
|
|
170
|
+
-webkit-appearance: none;
|
|
171
|
+
appearance: none;
|
|
172
|
+
height: 6px;
|
|
173
|
+
background: #ddd;
|
|
174
|
+
outline: none;
|
|
175
|
+
border-radius: 3px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.numeric-control input[type="range"]::-webkit-slider-thumb {
|
|
179
|
+
-webkit-appearance: none;
|
|
180
|
+
appearance: none;
|
|
181
|
+
width: 16px;
|
|
182
|
+
height: 16px;
|
|
183
|
+
background: #4a90e2;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
border-radius: 50%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.numeric-control input[type="range"]::-moz-range-thumb {
|
|
189
|
+
width: 16px;
|
|
190
|
+
height: 16px;
|
|
191
|
+
background: #4a90e2;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
border-radius: 50%;
|
|
194
|
+
border: none;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.numeric-control input[type="number"] {
|
|
198
|
+
width: 60px;
|
|
199
|
+
padding: 4px 1px;
|
|
200
|
+
border: 1px solid #ddd;
|
|
201
|
+
border-radius: 1px;
|
|
202
|
+
margin-left: 6px;
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
/* Range slider styles */
|
|
188
206
|
.range-container {
|
|
189
207
|
display: flex;
|
|
@@ -204,79 +222,76 @@ button.active {
|
|
|
204
222
|
|
|
205
223
|
.range-slider-container {
|
|
206
224
|
position: relative;
|
|
207
|
-
margin:
|
|
208
|
-
background: linear-gradient(
|
|
225
|
+
margin: 10px 0 15px 0;
|
|
226
|
+
background: linear-gradient(
|
|
227
|
+
to right,
|
|
209
228
|
#ddd 0%,
|
|
210
229
|
#ddd var(--min-pos, 0%),
|
|
211
|
-
#
|
|
212
|
-
#
|
|
230
|
+
#4a90e2 var(--min-pos, 0%),
|
|
231
|
+
#4a90e2 var(--max-pos, 100%),
|
|
213
232
|
#ddd var(--max-pos, 100%),
|
|
214
233
|
#ddd 100%);
|
|
215
|
-
border-radius:
|
|
216
|
-
height:
|
|
234
|
+
border-radius: 4px;
|
|
235
|
+
height: 6px;
|
|
236
|
+
width: 100%;
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
.range-slider {
|
|
220
240
|
position: absolute;
|
|
221
|
-
top:
|
|
222
|
-
transform: translateY(-50%);
|
|
241
|
+
top: 0;
|
|
223
242
|
left: 0;
|
|
224
243
|
width: 100%;
|
|
225
|
-
|
|
226
|
-
-webkit-appearance: none;
|
|
244
|
+
height: 100%;
|
|
227
245
|
appearance: none;
|
|
228
|
-
|
|
246
|
+
-webkit-appearance: none;
|
|
229
247
|
cursor: pointer;
|
|
248
|
+
background: none;
|
|
230
249
|
margin: 0;
|
|
231
|
-
|
|
250
|
+
padding: 0;
|
|
251
|
+
pointer-events: none;
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
/* Transparent Track for Webkit */
|
|
235
|
-
.range-slider::-webkit-slider-runnable-track {
|
|
255
|
+
.range-slider-container .range-slider::-webkit-slider-runnable-track {
|
|
236
256
|
background: transparent;
|
|
237
|
-
border:
|
|
238
|
-
|
|
257
|
+
border-radius: 2px;
|
|
258
|
+
height: 8px;
|
|
239
259
|
}
|
|
240
260
|
|
|
241
261
|
/* Transparent Track for Firefox */
|
|
242
|
-
.range-slider::-moz-range-track {
|
|
262
|
+
.range-slider-container .range-slider::-moz-range-track {
|
|
243
263
|
background: transparent;
|
|
244
|
-
border:
|
|
245
|
-
|
|
264
|
+
border-radius: 2px;
|
|
265
|
+
height: 8px;
|
|
246
266
|
}
|
|
247
267
|
|
|
248
|
-
.range-slider.
|
|
249
|
-
z-index: 2;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.range-slider::-webkit-slider-thumb {
|
|
268
|
+
.range-slider-container .range-slider::-webkit-slider-thumb {
|
|
253
269
|
pointer-events: auto;
|
|
254
270
|
-webkit-appearance: none;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
border-radius: 50%;
|
|
259
|
-
background: #3f51b5;
|
|
271
|
+
width: 16px;
|
|
272
|
+
height: 16px;
|
|
273
|
+
background: #4a90e2;
|
|
260
274
|
cursor: pointer;
|
|
261
|
-
border:
|
|
275
|
+
border-radius: 50%;
|
|
276
|
+
margin-top: -4px; /* center on track */
|
|
262
277
|
}
|
|
263
278
|
|
|
264
|
-
.range-slider::-moz-range-thumb {
|
|
279
|
+
.range-slider-container .range-slider::-moz-range-thumb {
|
|
265
280
|
pointer-events: auto;
|
|
266
|
-
width:
|
|
267
|
-
height:
|
|
268
|
-
|
|
269
|
-
background: #3f51b5;
|
|
281
|
+
width: 16px;
|
|
282
|
+
height: 16px;
|
|
283
|
+
background: #4a90e2;
|
|
270
284
|
cursor: pointer;
|
|
271
|
-
border:
|
|
285
|
+
border-radius: 50%;
|
|
286
|
+
margin-top: -4px; /* center on track */
|
|
272
287
|
}
|
|
273
288
|
|
|
274
289
|
.min-slider {
|
|
275
|
-
z-index:
|
|
290
|
+
z-index: 5;
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
.max-slider {
|
|
279
|
-
z-index:
|
|
294
|
+
z-index: 5;
|
|
280
295
|
}
|
|
281
296
|
|
|
282
297
|
#status-display {
|
|
@@ -45,46 +45,4 @@
|
|
|
45
45
|
.section-header {
|
|
46
46
|
margin-bottom: 15px;
|
|
47
47
|
font-size: 16px;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* Style all numeric controls consistently */
|
|
51
|
-
.numeric-control {
|
|
52
|
-
display: flex;
|
|
53
|
-
align-items: center;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.numeric-control input[type="range"] {
|
|
57
|
-
flex: 1;
|
|
58
|
-
-webkit-appearance: none;
|
|
59
|
-
appearance: none;
|
|
60
|
-
height: 6px;
|
|
61
|
-
background: #ddd;
|
|
62
|
-
outline: none;
|
|
63
|
-
border-radius: 3px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.numeric-control input[type="range"]::-webkit-slider-thumb {
|
|
67
|
-
-webkit-appearance: none;
|
|
68
|
-
appearance: none;
|
|
69
|
-
width: 16px;
|
|
70
|
-
height: 16px;
|
|
71
|
-
background: #4a90e2;
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
border-radius: 50%;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.numeric-control input[type="range"]::-moz-range-thumb {
|
|
77
|
-
width: 16px;
|
|
78
|
-
height: 16px;
|
|
79
|
-
background: #4a90e2;
|
|
80
|
-
cursor: pointer;
|
|
81
|
-
border-radius: 50%;
|
|
82
|
-
border: none;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.numeric-control input[type="number"] {
|
|
86
|
-
width: 60px;
|
|
87
|
-
padding: 1px 1px;
|
|
88
|
-
border: 1px solid #ddd;
|
|
89
|
-
border-radius: 1px;
|
|
90
48
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { updateStatus } from './utils.js';
|
|
2
|
+
import { initializeState, updateStateFromServer } from './state.js';
|
|
3
|
+
import { updatePlot } from './plot.js';
|
|
4
|
+
import { setUpdateThreshold } from './config.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetch initial parameter information from the server.
|
|
8
|
+
* Initializes the state and gets configuration.
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchInitialData() {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('/init-data');
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
15
|
+
}
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
|
|
18
|
+
setUpdateThreshold(data.config.update_threshold); // Set initial threshold
|
|
19
|
+
initializeState(data); // Initialize state
|
|
20
|
+
|
|
21
|
+
return data; // Return data in case the caller needs it
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error initializing viewer:', error);
|
|
24
|
+
updateStatus('Error initializing viewer');
|
|
25
|
+
throw error; // Re-throw the error to signal failure
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send parameter update to the server.
|
|
31
|
+
* @param {string} name - The name of the parameter.
|
|
32
|
+
* @param {*} value - The new value of the parameter.
|
|
33
|
+
* @param {boolean} [action=false] - Whether this is a button action.
|
|
34
|
+
*/
|
|
35
|
+
export async function updateParameterOnServer(name, value, action = false) {
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch('/update-param', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
name: name,
|
|
44
|
+
value: value,
|
|
45
|
+
action: action
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
return await response.json(); // Return the server response (likely includes updated state)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error updating parameter:', error);
|
|
54
|
+
updateStatus('Error updating parameter');
|
|
55
|
+
throw error; // Re-throw error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handles button click actions by sending to the server.
|
|
61
|
+
* @param {string} name - The name of the button parameter.
|
|
62
|
+
*/
|
|
63
|
+
export function handleButtonClick(name) {
|
|
64
|
+
const button = document.getElementById(`${name}-button`);
|
|
65
|
+
if (!button) return;
|
|
66
|
+
|
|
67
|
+
button.classList.add('active'); // Show button as active
|
|
68
|
+
updateStatus(`Processing ${name}...`);
|
|
69
|
+
|
|
70
|
+
updateParameterOnServer(name, null, true) // Send action=true
|
|
71
|
+
.then(data => {
|
|
72
|
+
button.classList.remove('active');
|
|
73
|
+
if (data.error) {
|
|
74
|
+
console.error('Error:', data.error);
|
|
75
|
+
updateStatus(`Error processing ${name}`);
|
|
76
|
+
} else {
|
|
77
|
+
// Update state with any changes from callbacks
|
|
78
|
+
updateStateFromServer(data.state, data.params);
|
|
79
|
+
// Update plot if needed (plot.js handles this now)
|
|
80
|
+
updatePlot();
|
|
81
|
+
updateStatus('Ready!');
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(error => {
|
|
85
|
+
button.classList.remove('active');
|
|
86
|
+
console.error('Error during button action:', error);
|
|
87
|
+
updateStatus(`Error processing ${name}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export let updateThreshold = 1.0; // Default update threshold
|
|
2
|
+
|
|
3
|
+
// Config object parsed from HTML data attributes
|
|
4
|
+
export const config = {
|
|
5
|
+
controlsPosition: document.getElementById('viewer-config')?.dataset.controlsPosition || 'left',
|
|
6
|
+
controlsWidthPercent: parseInt(document.getElementById('viewer-config')?.dataset.controlsWidthPercent || 20),
|
|
7
|
+
plotMarginPercent: parseInt(document.getElementById('viewer-config')?.dataset.plotMarginPercent || 15)
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Function to update threshold, needed by system controls
|
|
11
|
+
export function setUpdateThreshold(value) {
|
|
12
|
+
updateThreshold = value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Function to update config values, needed by system controls
|
|
16
|
+
export function setConfigValue(key, value) {
|
|
17
|
+
if (key in config) {
|
|
18
|
+
config[key] = value;
|
|
19
|
+
} else {
|
|
20
|
+
console.warn(`Attempted to set unknown config key: ${key}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { state } from './state.js';
|
|
2
|
+
import { updateThreshold } from './config.js';
|
|
3
|
+
import { updateStatus, createSlowLoadingImage } from './utils.js';
|
|
4
|
+
|
|
5
|
+
let loadingTimeout = null; // Timeout for showing loading state
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update the plot with current state
|
|
9
|
+
*/
|
|
10
|
+
export function updatePlot() {
|
|
11
|
+
const plotImage = document.getElementById('plot-image');
|
|
12
|
+
if (!plotImage) {
|
|
13
|
+
console.warn("Plot image element not found");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Clear any existing loading timeout
|
|
18
|
+
if (loadingTimeout) {
|
|
19
|
+
clearTimeout(loadingTimeout);
|
|
20
|
+
loadingTimeout = null; // Reset timeout variable
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Show loading state after threshold
|
|
24
|
+
loadingTimeout = setTimeout(() => {
|
|
25
|
+
const slowLoadingDataURL = createSlowLoadingImage(); // Get cached or create new
|
|
26
|
+
plotImage.src = slowLoadingDataURL;
|
|
27
|
+
plotImage.style.opacity = '0.5';
|
|
28
|
+
updateStatus('Generating plot...'); // Update status during loading
|
|
29
|
+
}, updateThreshold * 1000);
|
|
30
|
+
|
|
31
|
+
// Build query string from state
|
|
32
|
+
const queryParams = new URLSearchParams();
|
|
33
|
+
for (const [name, value] of Object.entries(state)) {
|
|
34
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
35
|
+
// Ensure complex objects/arrays are properly stringified for URL
|
|
36
|
+
queryParams.append(name, JSON.stringify(value));
|
|
37
|
+
} else {
|
|
38
|
+
queryParams.append(name, value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Set the image source to the plot endpoint with parameters
|
|
43
|
+
const url = `/plot?${queryParams.toString()}`;
|
|
44
|
+
|
|
45
|
+
// Use an Image object to preload and handle load/error events
|
|
46
|
+
const newImage = new Image();
|
|
47
|
+
|
|
48
|
+
newImage.onload = function() {
|
|
49
|
+
// Clear loading timeout if it hasn't fired yet
|
|
50
|
+
if (loadingTimeout) {
|
|
51
|
+
clearTimeout(loadingTimeout);
|
|
52
|
+
loadingTimeout = null;
|
|
53
|
+
}
|
|
54
|
+
// Update the actual image source and reset opacity
|
|
55
|
+
plotImage.src = url;
|
|
56
|
+
plotImage.style.opacity = 1;
|
|
57
|
+
// Don't necessarily set status to Ready here, as state updates might happen
|
|
58
|
+
// Let the calling function (updateParameter or initial load) handle final status
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
newImage.onerror = function() {
|
|
62
|
+
// Clear loading timeout
|
|
63
|
+
if (loadingTimeout) {
|
|
64
|
+
clearTimeout(loadingTimeout);
|
|
65
|
+
loadingTimeout = null;
|
|
66
|
+
}
|
|
67
|
+
updateStatus('Error loading plot');
|
|
68
|
+
plotImage.style.opacity = 1; // Reset opacity even on error
|
|
69
|
+
// Optionally display an error image/message
|
|
70
|
+
// plotImage.src = 'path/to/error/image.png';
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Start loading the new image
|
|
74
|
+
newImage.src = url;
|
|
75
|
+
}
|