syd 1.0.2__tar.gz → 1.1.0__tar.gz
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-1.0.2 → syd-1.1.0}/PKG-INFO +9 -8
- {syd-1.0.2 → syd-1.1.0}/README.md +8 -7
- syd-1.1.0/syd/__init__.py +3 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/deployer.py +12 -3
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/static/css/styles.css +26 -8
- syd-1.1.0/syd/flask_deployment/static/css/viewer.css +90 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/static/js/viewer.js +394 -30
- {syd-1.0.2 → syd-1.1.0}/syd/notebook_deployment/deployer.py +1 -3
- {syd-1.0.2 → syd-1.1.0}/syd/notebook_deployment/widgets.py +45 -27
- {syd-1.0.2 → syd-1.1.0}/syd/viewer.py +31 -4
- syd-1.0.2/syd/__init__.py +0 -11
- {syd-1.0.2 → syd-1.1.0}/.gitignore +0 -0
- {syd-1.0.2 → syd-1.1.0}/LICENSE +0 -0
- {syd-1.0.2 → syd-1.1.0}/pyproject.toml +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/__init__.py +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/static/__init__.py +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/templates/__init__.py +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/templates/index.html +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/flask_deployment/testing_principles.md +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/notebook_deployment/__init__.py +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/parameters.py +0 -0
- {syd-1.0.2 → syd-1.1.0}/syd/support.py +0 -0
{syd-1.0.2 → syd-1.1.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syd
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A Python package for making GUIs for data science easy.
|
|
5
5
|
Project-URL: Homepage, https://github.com/landoskape/syd
|
|
6
6
|
Author-email: Andrew Landau <andrew+tyler+landau+getridofthisanddtheplusses@gmail.com>
|
|
@@ -47,7 +47,7 @@ Have you ever wanted to look through all your data really quickly interactively?
|
|
|
47
47
|
|
|
48
48
|
Syd is a system for creating a data viewing GUI that you can view in a jupyter notebook or in a web browser. And guess what? Since it can open in a web browser, you can even open it on any other computer on your local network! For example, your PI's computer. Gone are the days of single random examples that they make infinitely stubborn conclusions about. Now, you can look at all the examples, quickly and easily, on their computer. And that's why Syd stands for share your data!
|
|
49
49
|
|
|
50
|
-
Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about what you want to
|
|
50
|
+
Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about _**what you want to plot**_ and _**which parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to _**spend your time thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
53
|
It's easy, just use pip install. The dependencies are light so it should work in most environments.
|
|
@@ -56,7 +56,7 @@ pip install syd
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Documentation
|
|
59
|
-
The full documentation is available
|
|
59
|
+
The full documentation is available [here](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
|
|
60
60
|
|
|
61
61
|
## Quick Start
|
|
62
62
|
This is an example of a sine wave viewer which is about as simple as it gets. You can choose which env to use - if you use ``env="notebook"`` then the GUI will deploy as the output of a jupyter cell (this only works in jupyter!). If you use ``env="browser"`` then the GUI will open a page in your default web browser and you can interact with the data there (works in jupyter notebooks and also from python scripts!).
|
|
@@ -78,9 +78,8 @@ viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
|
|
|
78
78
|
viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
|
|
79
79
|
viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
viewer.show()
|
|
81
|
+
viewer.show() # for viewing in a jupyter notebook
|
|
82
|
+
# viewer.share() # for viewing in a web browser
|
|
84
83
|
```
|
|
85
84
|
|
|
86
85
|

|
|
@@ -220,7 +219,6 @@ black . # from the root directory of the repo
|
|
|
220
219
|
|
|
221
220
|
## To-Do List
|
|
222
221
|
- Layout controls
|
|
223
|
-
- [ ] Improve the display and make it look better
|
|
224
222
|
- [ ] Add a "save" button that saves the current state of the viewer to a json file
|
|
225
223
|
- [ ] Add a "load" button that loads the viewer state from a json file
|
|
226
224
|
- [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
|
|
@@ -228,4 +226,7 @@ black . # from the root directory of the repo
|
|
|
228
226
|
- [ ] Consider "app_deployed" context for each deployer...
|
|
229
227
|
- Export options:
|
|
230
228
|
- [ ] Export lite: export the viewer as a HTML/Java package that contains an incomplete set of renderings of figures -- using a certain set of parameters.
|
|
231
|
-
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
229
|
+
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
230
|
+
- [ ] Idea for sharing: https://github.com/analyticalmonk/awesome-neuroscience, https://github.com/fasouto/awesome-dataviz
|
|
231
|
+
- [ ] The handling of value in Selection parameters is kind of weird.... I think we need to think more about what to do for fails!!!!
|
|
232
|
+
- [ ] Range parameters render poorly in browser mode.
|
|
@@ -17,7 +17,7 @@ Have you ever wanted to look through all your data really quickly interactively?
|
|
|
17
17
|
|
|
18
18
|
Syd is a system for creating a data viewing GUI that you can view in a jupyter notebook or in a web browser. And guess what? Since it can open in a web browser, you can even open it on any other computer on your local network! For example, your PI's computer. Gone are the days of single random examples that they make infinitely stubborn conclusions about. Now, you can look at all the examples, quickly and easily, on their computer. And that's why Syd stands for share your data!
|
|
19
19
|
|
|
20
|
-
Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about what you want to
|
|
20
|
+
Okay, so what is it? Syd is an automated system to convert some basic python plotting code into an interactive GUI. This means you only have to think about _**what you want to plot**_ and _**which parameters**_ you want to be interactive. Syd handles all the behind-the-scenes action required to make an interface. And guess what? That means you get to _**spend your time thinking**_ about your data, rather than writing code to look at it. And that's why Syd stands for Science, Yes! Dayummmm!
|
|
21
21
|
|
|
22
22
|
## Installation
|
|
23
23
|
It's easy, just use pip install. The dependencies are light so it should work in most environments.
|
|
@@ -26,7 +26,7 @@ pip install syd
|
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Documentation
|
|
29
|
-
The full documentation is available
|
|
29
|
+
The full documentation is available [here](https://shareyourdata.readthedocs.io/). It includes a quick start guide, a comprehensive tutorial, and an API reference for the different elements of Syd. If you have any questions or want to suggest improvements to the docs, please let us know on the [github issues page](https://github.com/landoskape/syd/issues)!
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
This is an example of a sine wave viewer which is about as simple as it gets. You can choose which env to use - if you use ``env="notebook"`` then the GUI will deploy as the output of a jupyter cell (this only works in jupyter!). If you use ``env="browser"`` then the GUI will open a page in your default web browser and you can interact with the data there (works in jupyter notebooks and also from python scripts!).
|
|
@@ -48,9 +48,8 @@ viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
|
|
|
48
48
|
viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
|
|
49
49
|
viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
viewer.show()
|
|
51
|
+
viewer.show() # for viewing in a jupyter notebook
|
|
52
|
+
# viewer.share() # for viewing in a web browser
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|

|
|
@@ -190,7 +189,6 @@ black . # from the root directory of the repo
|
|
|
190
189
|
|
|
191
190
|
## To-Do List
|
|
192
191
|
- Layout controls
|
|
193
|
-
- [ ] Improve the display and make it look better
|
|
194
192
|
- [ ] Add a "save" button that saves the current state of the viewer to a json file
|
|
195
193
|
- [ ] Add a "load" button that loads the viewer state from a json file
|
|
196
194
|
- [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
|
|
@@ -198,4 +196,7 @@ black . # from the root directory of the repo
|
|
|
198
196
|
- [ ] Consider "app_deployed" context for each deployer...
|
|
199
197
|
- Export options:
|
|
200
198
|
- [ ] Export lite: export the viewer as a HTML/Java package that contains an incomplete set of renderings of figures -- using a certain set of parameters.
|
|
201
|
-
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
199
|
+
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
200
|
+
- [ ] Idea for sharing: https://github.com/analyticalmonk/awesome-neuroscience, https://github.com/fasouto/awesome-dataviz
|
|
201
|
+
- [ ] The handling of value in Selection parameters is kind of weird.... I think we need to think more about what to do for fails!!!!
|
|
202
|
+
- [ ] Range parameters render poorly in browser mode.
|
|
@@ -48,7 +48,7 @@ class FlaskLayoutConfig:
|
|
|
48
48
|
"""Configuration for the Flask viewer layout."""
|
|
49
49
|
|
|
50
50
|
controls_position: str = "left" # Options are: 'left', 'top', 'right', 'bottom'
|
|
51
|
-
controls_width_percent: int =
|
|
51
|
+
controls_width_percent: int = 15
|
|
52
52
|
|
|
53
53
|
def __post_init__(self):
|
|
54
54
|
valid_positions = ["left", "top", "right", "bottom"]
|
|
@@ -73,12 +73,13 @@ class FlaskDeployer:
|
|
|
73
73
|
viewer: Viewer,
|
|
74
74
|
controls_position: str = "left",
|
|
75
75
|
fig_dpi: int = 300,
|
|
76
|
-
controls_width_percent: int =
|
|
76
|
+
controls_width_percent: int = 15,
|
|
77
77
|
suppress_warnings: bool = True,
|
|
78
78
|
debug: bool = False,
|
|
79
79
|
host: str = "127.0.0.1",
|
|
80
80
|
port: Optional[int] = None,
|
|
81
81
|
open_browser: bool = True,
|
|
82
|
+
update_threshold: float = 1.0,
|
|
82
83
|
):
|
|
83
84
|
"""
|
|
84
85
|
Initialize the Flask deployer.
|
|
@@ -107,10 +108,13 @@ class FlaskDeployer:
|
|
|
107
108
|
Port for the server. If None, finds an available port (default: None).
|
|
108
109
|
open_browser : bool, optional
|
|
109
110
|
Whether to open the web application in a browser tab (default: True).
|
|
111
|
+
update_threshold : float, optional
|
|
112
|
+
Time in seconds to wait before showing the loading indicator (default: 1.0)
|
|
110
113
|
"""
|
|
111
114
|
self.viewer = viewer
|
|
112
115
|
self.suppress_warnings = suppress_warnings
|
|
113
116
|
self._updating = False # Flag to check circular updates
|
|
117
|
+
self.update_threshold = update_threshold # Store update threshold
|
|
114
118
|
|
|
115
119
|
# Flask specific configurations
|
|
116
120
|
self.config = FlaskLayoutConfig(
|
|
@@ -167,12 +171,17 @@ class FlaskDeployer:
|
|
|
167
171
|
}
|
|
168
172
|
# Get the order of parameters
|
|
169
173
|
param_order = list(self.viewer.parameters.keys())
|
|
170
|
-
# Also include the initial state
|
|
174
|
+
# Also include the initial state and configuration
|
|
171
175
|
return jsonify(
|
|
172
176
|
{
|
|
173
177
|
"params": param_info,
|
|
174
178
|
"param_order": param_order,
|
|
175
179
|
"state": self.viewer.state,
|
|
180
|
+
"config": {
|
|
181
|
+
"controls_position": self.config.controls_position,
|
|
182
|
+
"controls_width_percent": self.config.controls_width_percent,
|
|
183
|
+
"update_threshold": self.update_threshold,
|
|
184
|
+
},
|
|
176
185
|
}
|
|
177
186
|
)
|
|
178
187
|
|
|
@@ -60,14 +60,14 @@ body {
|
|
|
60
60
|
#controls-container {
|
|
61
61
|
display: grid;
|
|
62
62
|
grid-template-columns: 1fr;
|
|
63
|
-
gap:
|
|
63
|
+
gap: 5px;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/* Control groups */
|
|
67
67
|
.control-group {
|
|
68
68
|
display: flex;
|
|
69
69
|
flex-direction: column;
|
|
70
|
-
padding:
|
|
70
|
+
padding: 7px;
|
|
71
71
|
border: 1px solid #eee;
|
|
72
72
|
border-radius: 4px;
|
|
73
73
|
background-color: white;
|
|
@@ -76,7 +76,7 @@ body {
|
|
|
76
76
|
|
|
77
77
|
.control-label {
|
|
78
78
|
font-weight: 600;
|
|
79
|
-
margin-bottom:
|
|
79
|
+
margin-bottom: 0px;
|
|
80
80
|
color: #333;
|
|
81
81
|
text-transform: capitalize;
|
|
82
82
|
}
|
|
@@ -87,7 +87,7 @@ input[type="number"] {
|
|
|
87
87
|
padding: 8px 12px;
|
|
88
88
|
border: 1px solid #ddd;
|
|
89
89
|
border-radius: 4px;
|
|
90
|
-
font-size:
|
|
90
|
+
font-size: 12px;
|
|
91
91
|
width: 100%;
|
|
92
92
|
box-sizing: border-box;
|
|
93
93
|
}
|
|
@@ -95,11 +95,11 @@ input[type="number"] {
|
|
|
95
95
|
/* Range inputs */
|
|
96
96
|
input[type="range"] {
|
|
97
97
|
width: 100%;
|
|
98
|
-
height:
|
|
98
|
+
height: 12px;
|
|
99
99
|
background: #ddd;
|
|
100
100
|
border-radius: 3px;
|
|
101
101
|
outline: none;
|
|
102
|
-
margin:
|
|
102
|
+
margin: 5px 0;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
input[type="range"]::-webkit-slider-thumb {
|
|
@@ -194,7 +194,7 @@ button.active {
|
|
|
194
194
|
.range-inputs {
|
|
195
195
|
display: flex;
|
|
196
196
|
justify-content: space-between;
|
|
197
|
-
margin-bottom:
|
|
197
|
+
margin-bottom: 5px;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
.range-input {
|
|
@@ -204,7 +204,7 @@ button.active {
|
|
|
204
204
|
|
|
205
205
|
.range-slider-container {
|
|
206
206
|
position: relative;
|
|
207
|
-
margin:
|
|
207
|
+
margin: 5px 0;
|
|
208
208
|
background: linear-gradient(to right,
|
|
209
209
|
#ddd 0%,
|
|
210
210
|
#ddd var(--min-pos, 0%),
|
|
@@ -277,4 +277,22 @@ button.active {
|
|
|
277
277
|
|
|
278
278
|
.max-slider {
|
|
279
279
|
z-index: 2;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#status-display {
|
|
283
|
+
margin-top: 10px;
|
|
284
|
+
margin-bottom: 3px;
|
|
285
|
+
padding: 8px;
|
|
286
|
+
border-radius: 4px;
|
|
287
|
+
background-color: #ffffff;
|
|
288
|
+
border: 1px solid #e5e7eb;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.status-message {
|
|
292
|
+
background-color: #e0e0e0;
|
|
293
|
+
color: #000;
|
|
294
|
+
padding: 2px 6px;
|
|
295
|
+
border-radius: 4px;
|
|
296
|
+
font-size: 90%;
|
|
297
|
+
margin-left: 8px;
|
|
280
298
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#viewer-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 100%;
|
|
4
|
+
margin: 0;
|
|
5
|
+
padding: 0;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
display: flex;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#controls-container {
|
|
11
|
+
padding: 15px;
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
overflow-y: auto;
|
|
14
|
+
max-height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#plot-container {
|
|
18
|
+
padding: 15px;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#plot-container img {
|
|
26
|
+
max-width: 100%;
|
|
27
|
+
height: auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.system-controls {
|
|
31
|
+
margin: 10px 0px;
|
|
32
|
+
padding: 10px;
|
|
33
|
+
background-color: #ffffff;
|
|
34
|
+
border: 1px solid #e5e7eb;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.parameter-controls {
|
|
39
|
+
padding: 10px;
|
|
40
|
+
background-color: #ffffff;
|
|
41
|
+
border: 1px solid #e5e7eb;
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.section-header {
|
|
46
|
+
margin-bottom: 15px;
|
|
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
|
+
}
|
|
@@ -2,21 +2,56 @@ let state = {};
|
|
|
2
2
|
let paramInfo = {};
|
|
3
3
|
let paramOrder = [];
|
|
4
4
|
let isUpdating = false;
|
|
5
|
+
let updateThreshold = 1.0; // Default update threshold
|
|
6
|
+
let loadingTimeout = null; // Timeout for showing loading state
|
|
7
|
+
let slowLoadingImage = null; // Cache for slow loading image
|
|
5
8
|
|
|
6
9
|
// Config object parsed from HTML data attributes
|
|
7
10
|
const config = {
|
|
8
11
|
controlsPosition: document.getElementById('viewer-config').dataset.controlsPosition || 'left',
|
|
9
|
-
controlsWidthPercent: parseInt(document.getElementById('viewer-config').dataset.controlsWidthPercent || 20)
|
|
12
|
+
controlsWidthPercent: parseInt(document.getElementById('viewer-config').dataset.controlsWidthPercent || 20),
|
|
13
|
+
plotMarginPercent: parseInt(document.getElementById('viewer-config').dataset.plotMarginPercent || 0)
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
// Initialize the viewer
|
|
13
17
|
document.addEventListener('DOMContentLoaded', function() {
|
|
18
|
+
// Create main controls container if it doesn't exist
|
|
19
|
+
const mainContainer = document.getElementById('controls-container');
|
|
20
|
+
|
|
21
|
+
// Create parameter controls section first
|
|
22
|
+
const paramControls = document.createElement('div');
|
|
23
|
+
paramControls.id = 'parameter-controls';
|
|
24
|
+
paramControls.className = 'parameter-controls';
|
|
25
|
+
|
|
26
|
+
// Add Parameters header
|
|
27
|
+
const paramHeader = document.createElement('div');
|
|
28
|
+
paramHeader.className = 'section-header';
|
|
29
|
+
paramHeader.innerHTML = '<b>Parameters</b>';
|
|
30
|
+
paramControls.appendChild(paramHeader);
|
|
31
|
+
|
|
32
|
+
// Create system controls section
|
|
33
|
+
const systemControls = document.createElement('div');
|
|
34
|
+
systemControls.id = 'system-controls';
|
|
35
|
+
systemControls.className = 'system-controls';
|
|
36
|
+
|
|
37
|
+
// Create status element
|
|
38
|
+
const statusElement = document.createElement('div');
|
|
39
|
+
statusElement.id = 'status-display';
|
|
40
|
+
statusElement.className = 'status-display';
|
|
41
|
+
systemControls.appendChild(statusElement);
|
|
42
|
+
updateStatus('Initializing...');
|
|
43
|
+
|
|
44
|
+
// Add sections to main container in the desired order
|
|
45
|
+
mainContainer.appendChild(paramControls);
|
|
46
|
+
mainContainer.appendChild(systemControls);
|
|
47
|
+
|
|
14
48
|
// Fetch initial parameter information from server
|
|
15
49
|
fetch('/init-data')
|
|
16
50
|
.then(response => response.json())
|
|
17
51
|
.then(data => {
|
|
18
52
|
paramInfo = data.params;
|
|
19
53
|
paramOrder = data.param_order;
|
|
54
|
+
updateThreshold = data.config.update_threshold;
|
|
20
55
|
|
|
21
56
|
// Initialize state from parameter info
|
|
22
57
|
for (const [name, param] of Object.entries(paramInfo)) {
|
|
@@ -25,23 +60,271 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
25
60
|
|
|
26
61
|
// Create UI controls for each parameter
|
|
27
62
|
createControls();
|
|
63
|
+
|
|
64
|
+
// Create system controls if horizontal layout
|
|
65
|
+
if (config.controlsPosition === 'left' || config.controlsPosition === 'right') {
|
|
66
|
+
createSystemControls(systemControls);
|
|
67
|
+
}
|
|
28
68
|
|
|
29
69
|
// Generate initial plot
|
|
30
70
|
updatePlot();
|
|
71
|
+
updateStatus('Ready!');
|
|
31
72
|
})
|
|
32
73
|
.catch(error => {
|
|
33
74
|
console.error('Error initializing viewer:', error);
|
|
75
|
+
updateStatus('Error initializing viewer');
|
|
34
76
|
});
|
|
35
77
|
});
|
|
36
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Create system controls (width and threshold)
|
|
81
|
+
*/
|
|
82
|
+
function createSystemControls(container) {
|
|
83
|
+
// Create controls width slider
|
|
84
|
+
const widthControl = createFloatController('controls_width', {
|
|
85
|
+
type: 'float',
|
|
86
|
+
value: config.controlsWidthPercent,
|
|
87
|
+
min: 10,
|
|
88
|
+
max: 50,
|
|
89
|
+
step: 1
|
|
90
|
+
});
|
|
91
|
+
widthControl.className = 'numeric-control system-control';
|
|
92
|
+
|
|
93
|
+
// Add label for width control
|
|
94
|
+
const widthLabel = document.createElement('span');
|
|
95
|
+
widthLabel.className = 'control-label';
|
|
96
|
+
widthLabel.textContent = 'Controls Width %';
|
|
97
|
+
|
|
98
|
+
const widthGroup = document.createElement('div');
|
|
99
|
+
widthGroup.className = 'control-group';
|
|
100
|
+
widthGroup.appendChild(widthLabel);
|
|
101
|
+
widthGroup.appendChild(widthControl);
|
|
102
|
+
|
|
103
|
+
// Create update threshold slider
|
|
104
|
+
const thresholdControl = createFloatController('update_threshold', {
|
|
105
|
+
type: 'float',
|
|
106
|
+
value: updateThreshold,
|
|
107
|
+
min: 0.1,
|
|
108
|
+
max: 10.0,
|
|
109
|
+
step: 0.1
|
|
110
|
+
});
|
|
111
|
+
thresholdControl.className = 'numeric-control system-control';
|
|
112
|
+
|
|
113
|
+
// Add label for threshold control
|
|
114
|
+
const thresholdLabel = document.createElement('span');
|
|
115
|
+
thresholdLabel.className = 'control-label';
|
|
116
|
+
thresholdLabel.textContent = 'Update Threshold';
|
|
117
|
+
|
|
118
|
+
const thresholdGroup = document.createElement('div');
|
|
119
|
+
thresholdGroup.className = 'control-group';
|
|
120
|
+
thresholdGroup.appendChild(thresholdLabel);
|
|
121
|
+
thresholdGroup.appendChild(thresholdControl);
|
|
122
|
+
|
|
123
|
+
// Create plot margin slider
|
|
124
|
+
const plotMarginControl = createFloatController('plot_margin', {
|
|
125
|
+
type: 'float',
|
|
126
|
+
value: config.plotMarginPercent,
|
|
127
|
+
min: 0,
|
|
128
|
+
max: 50,
|
|
129
|
+
step: 1
|
|
130
|
+
});
|
|
131
|
+
plotMarginControl.className = 'numeric-control system-control';
|
|
132
|
+
|
|
133
|
+
// Add label for margin control
|
|
134
|
+
const marginLabel = document.createElement('span');
|
|
135
|
+
marginLabel.className = 'control-label';
|
|
136
|
+
marginLabel.textContent = 'Plot Margin %';
|
|
137
|
+
|
|
138
|
+
const marginGroup = document.createElement('div');
|
|
139
|
+
marginGroup.className = 'control-group';
|
|
140
|
+
marginGroup.appendChild(marginLabel);
|
|
141
|
+
marginGroup.appendChild(plotMarginControl);
|
|
142
|
+
|
|
143
|
+
// Add custom event listeners
|
|
144
|
+
// Width Control Listeners
|
|
145
|
+
const widthSlider = widthControl.querySelector('input[type="range"]');
|
|
146
|
+
const widthInput = widthControl.querySelector('input[type="number"]');
|
|
147
|
+
|
|
148
|
+
widthSlider.addEventListener('input', function() { // Real-time update for number input
|
|
149
|
+
widthInput.value = this.value;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
widthSlider.addEventListener('change', function() {
|
|
153
|
+
const width = parseFloat(this.value);
|
|
154
|
+
config.controlsWidthPercent = width;
|
|
155
|
+
|
|
156
|
+
// Update the root containers using querySelector for classes
|
|
157
|
+
const rootContainer = document.querySelector('.viewer-container');
|
|
158
|
+
const controlsContainer = document.querySelector('.controls-container'); // Select the outer div by class
|
|
159
|
+
const plotContainer = document.querySelector('.plot-container');
|
|
160
|
+
|
|
161
|
+
if (rootContainer && controlsContainer && plotContainer) {
|
|
162
|
+
if (config.controlsPosition === 'left' || config.controlsPosition === 'right') {
|
|
163
|
+
controlsContainer.style.width = `${width}%`;
|
|
164
|
+
plotContainer.style.width = `${100 - width}%`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Update the slider to match
|
|
169
|
+
widthSlider.value = width;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Threshold Control Listeners
|
|
173
|
+
const thresholdSlider = thresholdControl.querySelector('input[type="range"]');
|
|
174
|
+
const thresholdInput = thresholdControl.querySelector('input[type="number"]');
|
|
175
|
+
|
|
176
|
+
thresholdSlider.addEventListener('input', function() { // Real-time update for number input
|
|
177
|
+
thresholdInput.value = this.value;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
thresholdSlider.addEventListener('change', function() {
|
|
181
|
+
updateThreshold = parseFloat(this.value);
|
|
182
|
+
thresholdInput.value = updateThreshold; // Ensure input matches final value
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Plot Margin Control Listeners
|
|
186
|
+
const marginSlider = plotMarginControl.querySelector('input[type="range"]');
|
|
187
|
+
const marginInput = plotMarginControl.querySelector('input[type="number"]');
|
|
188
|
+
const plotContainer = document.querySelector('.plot-container');
|
|
189
|
+
|
|
190
|
+
// Function to apply margin and adjust size of the plot image
|
|
191
|
+
function applyPlotMargin(marginPercent) {
|
|
192
|
+
const plotImage = document.getElementById('plot-image'); // Get the image element
|
|
193
|
+
if (plotImage) {
|
|
194
|
+
const effectiveMargin = parseFloat(marginPercent); // Ensure it's a number
|
|
195
|
+
// Apply margin to the image
|
|
196
|
+
plotImage.style.margin = `${effectiveMargin}%`;
|
|
197
|
+
// Adjust width and height to account for the margin
|
|
198
|
+
plotImage.style.width = `calc(100% - ${2 * effectiveMargin}%)`;
|
|
199
|
+
plotImage.style.height = `calc(100% - ${2 * effectiveMargin}%)`;
|
|
200
|
+
// Reset container padding just in case
|
|
201
|
+
if (plotContainer) {
|
|
202
|
+
plotContainer.style.padding = '0';
|
|
203
|
+
}
|
|
204
|
+
config.plotMarginPercent = effectiveMargin; // Update config
|
|
205
|
+
} else {
|
|
206
|
+
console.warn('Plot image element not found when applying margin.');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
marginSlider.addEventListener('input', function() { // Real-time update for number input
|
|
211
|
+
marginInput.value = this.value;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
marginSlider.addEventListener('change', function() {
|
|
215
|
+
const margin = parseFloat(this.value);
|
|
216
|
+
marginInput.value = margin; // Ensure input matches final value
|
|
217
|
+
applyPlotMargin(margin);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Add wheel event listener to plot container for margin control
|
|
221
|
+
if (plotContainer) {
|
|
222
|
+
plotContainer.addEventListener('wheel', function(event) {
|
|
223
|
+
event.preventDefault(); // Prevent page scrolling
|
|
224
|
+
|
|
225
|
+
const currentValue = parseFloat(marginSlider.value);
|
|
226
|
+
const step = parseFloat(marginSlider.step) || 1;
|
|
227
|
+
const min = parseFloat(marginSlider.min);
|
|
228
|
+
const max = parseFloat(marginSlider.max);
|
|
229
|
+
|
|
230
|
+
let newValue;
|
|
231
|
+
if (event.deltaY < 0) {
|
|
232
|
+
// Scrolling up (or zoom in) -> decrease margin
|
|
233
|
+
newValue = currentValue - step;
|
|
234
|
+
} else {
|
|
235
|
+
// Scrolling down (or zoom out) -> increase margin
|
|
236
|
+
newValue = currentValue + step;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clamp the value within min/max bounds
|
|
240
|
+
newValue = Math.max(min, Math.min(max, newValue));
|
|
241
|
+
|
|
242
|
+
// Only update if the value actually changed
|
|
243
|
+
if (newValue !== currentValue) {
|
|
244
|
+
marginSlider.value = newValue;
|
|
245
|
+
marginInput.value = newValue;
|
|
246
|
+
applyPlotMargin(newValue);
|
|
247
|
+
}
|
|
248
|
+
}, { passive: false }); // Need passive: false to call preventDefault()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Apply initial margin
|
|
252
|
+
applyPlotMargin(config.plotMarginPercent);
|
|
253
|
+
|
|
254
|
+
container.appendChild(widthGroup);
|
|
255
|
+
container.appendChild(thresholdGroup);
|
|
256
|
+
container.appendChild(marginGroup);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create update threshold control
|
|
261
|
+
*/
|
|
262
|
+
function createUpdateThresholdControl() {
|
|
263
|
+
const container = document.createElement('div');
|
|
264
|
+
container.className = 'control-group';
|
|
265
|
+
|
|
266
|
+
const label = document.createElement('span');
|
|
267
|
+
label.className = 'control-label';
|
|
268
|
+
label.textContent = 'Update Threshold';
|
|
269
|
+
|
|
270
|
+
const input = document.createElement('input');
|
|
271
|
+
input.type = 'range';
|
|
272
|
+
input.min = '0.1';
|
|
273
|
+
input.max = '10.0';
|
|
274
|
+
input.step = '0.1';
|
|
275
|
+
input.value = updateThreshold;
|
|
276
|
+
input.className = 'update-threshold-slider';
|
|
277
|
+
|
|
278
|
+
input.addEventListener('change', function() {
|
|
279
|
+
updateThreshold = parseFloat(this.value);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
container.appendChild(label);
|
|
283
|
+
container.appendChild(input);
|
|
284
|
+
return container;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Update the status display
|
|
289
|
+
*/
|
|
290
|
+
function updateStatus(message) {
|
|
291
|
+
const statusElement = document.getElementById('status-display');
|
|
292
|
+
if (statusElement) {
|
|
293
|
+
statusElement.innerHTML = `<b>Syd Controls</b> <span class="status-message">Status: ${message}</span>`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create and cache the slow loading image
|
|
299
|
+
*/
|
|
300
|
+
function createSlowLoadingImage() {
|
|
301
|
+
const canvas = document.createElement('canvas');
|
|
302
|
+
canvas.width = 1200;
|
|
303
|
+
canvas.height = 900;
|
|
304
|
+
const ctx = canvas.getContext('2d');
|
|
305
|
+
|
|
306
|
+
// Fill background
|
|
307
|
+
ctx.fillStyle = '#ffffff';
|
|
308
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
309
|
+
|
|
310
|
+
// Add loading text
|
|
311
|
+
ctx.fillStyle = '#000000';
|
|
312
|
+
ctx.font = 'bold 16px Arial';
|
|
313
|
+
ctx.textAlign = 'center';
|
|
314
|
+
ctx.textBaseline = 'middle';
|
|
315
|
+
ctx.fillText('waiting for next figure...', canvas.width/2, canvas.height/2);
|
|
316
|
+
|
|
317
|
+
return canvas.toDataURL();
|
|
318
|
+
}
|
|
319
|
+
|
|
37
320
|
/**
|
|
38
321
|
* Create UI controls based on parameter types
|
|
39
322
|
*/
|
|
40
323
|
function createControls() {
|
|
41
|
-
const
|
|
324
|
+
const paramControls = document.getElementById('parameter-controls');
|
|
42
325
|
|
|
43
|
-
// Clear any existing controls
|
|
44
|
-
|
|
326
|
+
// Clear any existing parameter controls
|
|
327
|
+
paramControls.innerHTML = '';
|
|
45
328
|
|
|
46
329
|
// Create controls for each parameter in the order specified by the viewer
|
|
47
330
|
paramOrder.forEach(name => {
|
|
@@ -56,7 +339,7 @@ function createControls() {
|
|
|
56
339
|
|
|
57
340
|
// Add to container
|
|
58
341
|
if (controlGroup) {
|
|
59
|
-
|
|
342
|
+
paramControls.appendChild(controlGroup);
|
|
60
343
|
}
|
|
61
344
|
});
|
|
62
345
|
}
|
|
@@ -186,6 +469,10 @@ function createIntegerControl(name, param) {
|
|
|
186
469
|
input.value = param.value;
|
|
187
470
|
|
|
188
471
|
// Add event listeners
|
|
472
|
+
slider.addEventListener('input', function() {
|
|
473
|
+
input.value = this.value;
|
|
474
|
+
});
|
|
475
|
+
|
|
189
476
|
slider.addEventListener('change', function() {
|
|
190
477
|
const value = parseInt(this.value, 10);
|
|
191
478
|
input.value = value;
|
|
@@ -198,7 +485,7 @@ function createIntegerControl(name, param) {
|
|
|
198
485
|
slider.value = value;
|
|
199
486
|
updateParameter(name, value);
|
|
200
487
|
} else {
|
|
201
|
-
this.value = state[name];
|
|
488
|
+
this.value = state[name];
|
|
202
489
|
}
|
|
203
490
|
});
|
|
204
491
|
|
|
@@ -211,6 +498,40 @@ function createIntegerControl(name, param) {
|
|
|
211
498
|
* Create float control with slider and number input
|
|
212
499
|
*/
|
|
213
500
|
function createFloatControl(name, param) {
|
|
501
|
+
// create a container with the slider and input
|
|
502
|
+
const container = createFloatController(name, param);
|
|
503
|
+
const slider = container.querySelector('input[type="range"]');
|
|
504
|
+
const input = container.querySelector('input[type="number"]');
|
|
505
|
+
|
|
506
|
+
// Add event listeners
|
|
507
|
+
slider.addEventListener('input', function() {
|
|
508
|
+
input.value = this.value;
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
slider.addEventListener('change', function() {
|
|
512
|
+
const value = parseFloat(this.value);
|
|
513
|
+
input.value = value;
|
|
514
|
+
updateParameter(name, value);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
input.addEventListener('change', function() {
|
|
518
|
+
const value = parseFloat(this.value);
|
|
519
|
+
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
520
|
+
slider.value = value;
|
|
521
|
+
updateParameter(name, value);
|
|
522
|
+
} else {
|
|
523
|
+
this.value = state[name]; // Revert to current state
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return container;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Create float object with slider and number input
|
|
532
|
+
* Without the elements specific to "parameters"
|
|
533
|
+
*/
|
|
534
|
+
function createFloatController(name, param) {
|
|
214
535
|
const container = document.createElement('div');
|
|
215
536
|
container.className = 'numeric-control';
|
|
216
537
|
|
|
@@ -232,23 +553,7 @@ function createFloatControl(name, param) {
|
|
|
232
553
|
input.step = param.step || 0.01;
|
|
233
554
|
input.value = param.value;
|
|
234
555
|
|
|
235
|
-
//
|
|
236
|
-
slider.addEventListener('change', function() {
|
|
237
|
-
const value = parseFloat(this.value);
|
|
238
|
-
input.value = value;
|
|
239
|
-
updateParameter(name, value);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
input.addEventListener('change', function() {
|
|
243
|
-
const value = parseFloat(this.value);
|
|
244
|
-
if (!isNaN(value) && value >= param.min && value <= param.max) {
|
|
245
|
-
slider.value = value;
|
|
246
|
-
updateParameter(name, value);
|
|
247
|
-
} else {
|
|
248
|
-
this.value = state[name]; // Revert to current state
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
|
|
556
|
+
// return the container with the slider and input
|
|
252
557
|
container.appendChild(slider);
|
|
253
558
|
container.appendChild(input);
|
|
254
559
|
return container;
|
|
@@ -414,6 +719,34 @@ function createRangeControl(name, param, converter) {
|
|
|
414
719
|
maxInput.value = param.value[1];
|
|
415
720
|
|
|
416
721
|
// Add event listeners
|
|
722
|
+
// Input listeners for real-time updates of number inputs and gradient
|
|
723
|
+
minSlider.addEventListener('input', function() {
|
|
724
|
+
const minVal = converter(this.value);
|
|
725
|
+
const maxVal = converter(maxSlider.value);
|
|
726
|
+
if (minVal <= maxVal) {
|
|
727
|
+
minInput.value = minVal;
|
|
728
|
+
} else {
|
|
729
|
+
// Prevent slider crossing visually, update input to maxVal
|
|
730
|
+
this.value = maxVal;
|
|
731
|
+
minInput.value = maxVal;
|
|
732
|
+
}
|
|
733
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
maxSlider.addEventListener('input', function() {
|
|
737
|
+
const minVal = converter(minSlider.value);
|
|
738
|
+
const maxVal = converter(this.value);
|
|
739
|
+
if (maxVal >= minVal) {
|
|
740
|
+
maxInput.value = maxVal;
|
|
741
|
+
} else {
|
|
742
|
+
// Prevent slider crossing visually, update input to minVal
|
|
743
|
+
this.value = minVal;
|
|
744
|
+
maxInput.value = minVal;
|
|
745
|
+
}
|
|
746
|
+
updateSliderGradient(minSlider, maxSlider, sliderContainer); // Update gradient
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Change listeners for updating state and triggering backend calls
|
|
417
750
|
minSlider.addEventListener('change', function() {
|
|
418
751
|
const minVal = converter(this.value);
|
|
419
752
|
const maxVal = converter(maxSlider.value);
|
|
@@ -596,7 +929,9 @@ function updateParameter(name, value) {
|
|
|
596
929
|
if (isUpdating) {
|
|
597
930
|
return;
|
|
598
931
|
}
|
|
599
|
-
|
|
932
|
+
// Indicate status update
|
|
933
|
+
updateStatus('Updating ' + name + '...');
|
|
934
|
+
|
|
600
935
|
// Update local state
|
|
601
936
|
state[name] = value;
|
|
602
937
|
|
|
@@ -626,6 +961,9 @@ function updateParameter(name, value) {
|
|
|
626
961
|
.catch(error => {
|
|
627
962
|
console.error('Error:', error);
|
|
628
963
|
});
|
|
964
|
+
|
|
965
|
+
// Indicate status update
|
|
966
|
+
updateStatus('Ready!');
|
|
629
967
|
}
|
|
630
968
|
|
|
631
969
|
/**
|
|
@@ -802,11 +1140,27 @@ function formatLabel(name) {
|
|
|
802
1140
|
* Update the plot with current state
|
|
803
1141
|
*/
|
|
804
1142
|
function updatePlot() {
|
|
1143
|
+
const plotImage = document.getElementById('plot-image');
|
|
1144
|
+
if (!plotImage) return;
|
|
1145
|
+
|
|
1146
|
+
// Clear any existing loading timeout
|
|
1147
|
+
if (loadingTimeout) {
|
|
1148
|
+
clearTimeout(loadingTimeout);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Show loading state after threshold
|
|
1152
|
+
loadingTimeout = setTimeout(() => {
|
|
1153
|
+
// Create slow loading image if not cached
|
|
1154
|
+
if (!slowLoadingImage) {
|
|
1155
|
+
slowLoadingImage = createSlowLoadingImage();
|
|
1156
|
+
}
|
|
1157
|
+
plotImage.src = slowLoadingImage;
|
|
1158
|
+
plotImage.style.opacity = '0.5';
|
|
1159
|
+
}, updateThreshold * 1000);
|
|
1160
|
+
|
|
805
1161
|
// Build query string from state
|
|
806
1162
|
const queryParams = new URLSearchParams();
|
|
807
|
-
|
|
808
1163
|
for (const [name, value] of Object.entries(state)) {
|
|
809
|
-
// Handle arrays and special types by serializing to JSON
|
|
810
1164
|
if (Array.isArray(value) || typeof value === 'object') {
|
|
811
1165
|
queryParams.append(name, JSON.stringify(value));
|
|
812
1166
|
} else {
|
|
@@ -816,16 +1170,26 @@ function updatePlot() {
|
|
|
816
1170
|
|
|
817
1171
|
// Set the image source to the plot endpoint with parameters
|
|
818
1172
|
const url = `/plot?${queryParams.toString()}`;
|
|
819
|
-
const plotImage = document.getElementById('plot-image');
|
|
820
|
-
|
|
821
|
-
// Show loading indicator
|
|
822
|
-
plotImage.style.opacity = 0.5;
|
|
823
1173
|
|
|
824
1174
|
// Create a new image object
|
|
825
1175
|
const newImage = new Image();
|
|
826
1176
|
newImage.onload = function() {
|
|
1177
|
+
// Clear loading timeout
|
|
1178
|
+
if (loadingTimeout) {
|
|
1179
|
+
clearTimeout(loadingTimeout);
|
|
1180
|
+
loadingTimeout = null;
|
|
1181
|
+
}
|
|
827
1182
|
plotImage.src = url;
|
|
828
1183
|
plotImage.style.opacity = 1;
|
|
829
1184
|
};
|
|
1185
|
+
newImage.onerror = function() {
|
|
1186
|
+
// Clear loading timeout
|
|
1187
|
+
if (loadingTimeout) {
|
|
1188
|
+
clearTimeout(loadingTimeout);
|
|
1189
|
+
loadingTimeout = null;
|
|
1190
|
+
}
|
|
1191
|
+
updateStatus('Error loading plot');
|
|
1192
|
+
plotImage.style.opacity = 1;
|
|
1193
|
+
};
|
|
830
1194
|
newImage.src = url;
|
|
831
1195
|
}
|
|
@@ -39,7 +39,6 @@ class NotebookDeployer:
|
|
|
39
39
|
viewer: Viewer,
|
|
40
40
|
controls_position: Literal["left", "top", "right", "bottom"] = "left",
|
|
41
41
|
controls_width_percent: int = 20,
|
|
42
|
-
continuous: bool = False,
|
|
43
42
|
suppress_warnings: bool = True,
|
|
44
43
|
update_threshold: float = 1.0,
|
|
45
44
|
):
|
|
@@ -49,7 +48,6 @@ class NotebookDeployer:
|
|
|
49
48
|
self._updating = False # Flag to check circular updates
|
|
50
49
|
self.controls_position = controls_position
|
|
51
50
|
self.controls_width_percent = controls_width_percent
|
|
52
|
-
self.continuous = continuous
|
|
53
51
|
|
|
54
52
|
# Initialize containers
|
|
55
53
|
self.backend_type = get_backend_type()
|
|
@@ -117,7 +115,7 @@ class NotebookDeployer:
|
|
|
117
115
|
def build_components(self) -> None:
|
|
118
116
|
"""Create widget instances for all parameters and equip callbacks."""
|
|
119
117
|
for name, param in self.viewer.parameters.items():
|
|
120
|
-
widget = create_widget(param
|
|
118
|
+
widget = create_widget(param)
|
|
121
119
|
self.components[name] = widget
|
|
122
120
|
callback = lambda _, n=name: self.handle_component_engagement(n)
|
|
123
121
|
widget.observe(callback)
|
|
@@ -36,13 +36,15 @@ class BaseWidget(Generic[T, W], ABC):
|
|
|
36
36
|
def __init__(
|
|
37
37
|
self,
|
|
38
38
|
parameter: T,
|
|
39
|
-
continuous: bool = False,
|
|
40
39
|
width: str = "auto",
|
|
41
40
|
margin: str = "3px 0px",
|
|
42
41
|
description_width: str = "initial",
|
|
43
42
|
):
|
|
44
43
|
self._widget = self._create_widget(
|
|
45
|
-
parameter,
|
|
44
|
+
parameter,
|
|
45
|
+
width,
|
|
46
|
+
margin,
|
|
47
|
+
description_width,
|
|
46
48
|
)
|
|
47
49
|
self._updating = False # Flag to prevent circular updates
|
|
48
50
|
# List of callbacks to remember for quick disabling/enabling
|
|
@@ -52,7 +54,6 @@ class BaseWidget(Generic[T, W], ABC):
|
|
|
52
54
|
def _create_widget(
|
|
53
55
|
self,
|
|
54
56
|
parameter: T,
|
|
55
|
-
continuous: bool,
|
|
56
57
|
width: str = "auto",
|
|
57
58
|
margin: str = "3px 0px",
|
|
58
59
|
description_width: str = "initial",
|
|
@@ -123,7 +124,6 @@ class TextWidget(BaseWidget[TextParameter, widgets.Text]):
|
|
|
123
124
|
def _create_widget(
|
|
124
125
|
self,
|
|
125
126
|
parameter: TextParameter,
|
|
126
|
-
continuous: bool,
|
|
127
127
|
width: str = "auto",
|
|
128
128
|
margin: str = "3px 0px",
|
|
129
129
|
description_width: str = "initial",
|
|
@@ -131,7 +131,7 @@ class TextWidget(BaseWidget[TextParameter, widgets.Text]):
|
|
|
131
131
|
return widgets.Text(
|
|
132
132
|
value=parameter.value,
|
|
133
133
|
description=parameter.name,
|
|
134
|
-
continuous=
|
|
134
|
+
continuous=False,
|
|
135
135
|
layout=widgets.Layout(width=width, margin=margin),
|
|
136
136
|
style={"description_width": description_width},
|
|
137
137
|
)
|
|
@@ -143,7 +143,6 @@ class BooleanWidget(BaseWidget[BooleanParameter, widgets.ToggleButton]):
|
|
|
143
143
|
def _create_widget(
|
|
144
144
|
self,
|
|
145
145
|
parameter: BooleanParameter,
|
|
146
|
-
continuous: bool,
|
|
147
146
|
width: str = "auto",
|
|
148
147
|
margin: str = "3px 0px",
|
|
149
148
|
description_width: str = "initial",
|
|
@@ -162,7 +161,6 @@ class SelectionWidget(BaseWidget[SelectionParameter, widgets.Dropdown]):
|
|
|
162
161
|
def _create_widget(
|
|
163
162
|
self,
|
|
164
163
|
parameter: SelectionParameter,
|
|
165
|
-
continuous: bool,
|
|
166
164
|
width: str = "auto",
|
|
167
165
|
margin: str = "3px 0px",
|
|
168
166
|
description_width: str = "initial",
|
|
@@ -198,7 +196,6 @@ class MultipleSelectionWidget(
|
|
|
198
196
|
def _create_widget(
|
|
199
197
|
self,
|
|
200
198
|
parameter: MultipleSelectionParameter,
|
|
201
|
-
continuous: bool,
|
|
202
199
|
width: str = "auto",
|
|
203
200
|
margin: str = "3px 0px",
|
|
204
201
|
description_width: str = "initial",
|
|
@@ -235,7 +232,6 @@ class IntegerWidget(BaseWidget[IntegerParameter, widgets.IntSlider]):
|
|
|
235
232
|
def _create_widget(
|
|
236
233
|
self,
|
|
237
234
|
parameter: IntegerParameter,
|
|
238
|
-
continuous: bool,
|
|
239
235
|
width: str = "auto",
|
|
240
236
|
margin: str = "3px 0px",
|
|
241
237
|
description_width: str = "initial",
|
|
@@ -247,7 +243,7 @@ class IntegerWidget(BaseWidget[IntegerParameter, widgets.IntSlider]):
|
|
|
247
243
|
max=parameter.max,
|
|
248
244
|
step=1,
|
|
249
245
|
description=parameter.name,
|
|
250
|
-
continuous_update=
|
|
246
|
+
continuous_update=False,
|
|
251
247
|
style={"description_width": description_width},
|
|
252
248
|
layout=widgets.Layout(width=width, margin=margin),
|
|
253
249
|
)
|
|
@@ -264,6 +260,10 @@ class IntegerWidget(BaseWidget[IntegerParameter, widgets.IntSlider]):
|
|
|
264
260
|
def extra_updates_from_parameter(self, parameter: IntegerParameter) -> None:
|
|
265
261
|
"""Update the widget attributes from the parameter."""
|
|
266
262
|
current_value = self._widget.value
|
|
263
|
+
if parameter.min > self._widget.max:
|
|
264
|
+
self._widget.max = parameter.min + 1
|
|
265
|
+
if parameter.max < self._widget.min:
|
|
266
|
+
self._widget.min = parameter.max - 1
|
|
267
267
|
self._widget.min = parameter.min
|
|
268
268
|
self._widget.max = parameter.max
|
|
269
269
|
self.value = max(parameter.min, min(parameter.max, current_value))
|
|
@@ -275,7 +275,6 @@ class FloatWidget(BaseWidget[FloatParameter, widgets.FloatSlider]):
|
|
|
275
275
|
def _create_widget(
|
|
276
276
|
self,
|
|
277
277
|
parameter: FloatParameter,
|
|
278
|
-
continuous: bool,
|
|
279
278
|
width: str = "auto",
|
|
280
279
|
margin: str = "3px 0px",
|
|
281
280
|
description_width: str = "initial",
|
|
@@ -287,7 +286,7 @@ class FloatWidget(BaseWidget[FloatParameter, widgets.FloatSlider]):
|
|
|
287
286
|
max=parameter.max,
|
|
288
287
|
step=parameter.step,
|
|
289
288
|
description=parameter.name,
|
|
290
|
-
continuous_update=
|
|
289
|
+
continuous_update=False,
|
|
291
290
|
style={"description_width": description_width},
|
|
292
291
|
layout=widgets.Layout(width=width, margin=margin),
|
|
293
292
|
)
|
|
@@ -305,6 +304,10 @@ class FloatWidget(BaseWidget[FloatParameter, widgets.FloatSlider]):
|
|
|
305
304
|
def extra_updates_from_parameter(self, parameter: FloatParameter) -> None:
|
|
306
305
|
"""Update the widget attributes from the parameter."""
|
|
307
306
|
current_value = self._widget.value
|
|
307
|
+
if parameter.min > self._widget.max:
|
|
308
|
+
self._widget.max = parameter.min + 1
|
|
309
|
+
if parameter.max < self._widget.min:
|
|
310
|
+
self._widget.min = parameter.max - 1
|
|
308
311
|
self._widget.min = parameter.min
|
|
309
312
|
self._widget.max = parameter.max
|
|
310
313
|
self._widget.step = parameter.step
|
|
@@ -317,7 +320,6 @@ class IntegerRangeWidget(BaseWidget[IntegerRangeParameter, widgets.IntRangeSlide
|
|
|
317
320
|
def _create_widget(
|
|
318
321
|
self,
|
|
319
322
|
parameter: IntegerRangeParameter,
|
|
320
|
-
continuous: bool,
|
|
321
323
|
width: str = "auto",
|
|
322
324
|
margin: str = "3px 0px",
|
|
323
325
|
description_width: str = "initial",
|
|
@@ -330,7 +332,7 @@ class IntegerRangeWidget(BaseWidget[IntegerRangeParameter, widgets.IntRangeSlide
|
|
|
330
332
|
max=parameter.max,
|
|
331
333
|
step=1,
|
|
332
334
|
description=parameter.name,
|
|
333
|
-
continuous_update=
|
|
335
|
+
continuous_update=False,
|
|
334
336
|
style={"description_width": description_width},
|
|
335
337
|
layout=widgets.Layout(width=width, margin=margin),
|
|
336
338
|
)
|
|
@@ -349,6 +351,10 @@ class IntegerRangeWidget(BaseWidget[IntegerRangeParameter, widgets.IntRangeSlide
|
|
|
349
351
|
def extra_updates_from_parameter(self, parameter: IntegerRangeParameter) -> None:
|
|
350
352
|
"""Update the widget attributes from the parameter."""
|
|
351
353
|
low, high = self._widget.value
|
|
354
|
+
if parameter.min > self._widget.max:
|
|
355
|
+
self._widget.max = parameter.min + 1
|
|
356
|
+
if parameter.max < self._widget.min:
|
|
357
|
+
self._widget.min = parameter.max - 1
|
|
352
358
|
self._widget.min = parameter.min
|
|
353
359
|
self._widget.max = parameter.max
|
|
354
360
|
# Ensure values stay within bounds
|
|
@@ -363,7 +369,6 @@ class FloatRangeWidget(BaseWidget[FloatRangeParameter, widgets.FloatRangeSlider]
|
|
|
363
369
|
def _create_widget(
|
|
364
370
|
self,
|
|
365
371
|
parameter: FloatRangeParameter,
|
|
366
|
-
continuous: bool,
|
|
367
372
|
width: str = "auto",
|
|
368
373
|
margin: str = "3px 0px",
|
|
369
374
|
description_width: str = "initial",
|
|
@@ -376,7 +381,7 @@ class FloatRangeWidget(BaseWidget[FloatRangeParameter, widgets.FloatRangeSlider]
|
|
|
376
381
|
max=parameter.max,
|
|
377
382
|
step=parameter.step,
|
|
378
383
|
description=parameter.name,
|
|
379
|
-
continuous_update=
|
|
384
|
+
continuous_update=False,
|
|
380
385
|
style={"description_width": description_width},
|
|
381
386
|
layout=widgets.Layout(width=width, margin=margin),
|
|
382
387
|
)
|
|
@@ -396,6 +401,10 @@ class FloatRangeWidget(BaseWidget[FloatRangeParameter, widgets.FloatRangeSlider]
|
|
|
396
401
|
def extra_updates_from_parameter(self, parameter: FloatRangeParameter) -> None:
|
|
397
402
|
"""Update the widget attributes from the parameter."""
|
|
398
403
|
low, high = self._widget.value
|
|
404
|
+
if parameter.min > self._widget.max:
|
|
405
|
+
self._widget.max = parameter.min + 1
|
|
406
|
+
if parameter.max < self._widget.min:
|
|
407
|
+
self._widget.min = parameter.max - 1
|
|
399
408
|
self._widget.min = parameter.min
|
|
400
409
|
self._widget.max = parameter.max
|
|
401
410
|
self._widget.step = parameter.step
|
|
@@ -411,7 +420,6 @@ class UnboundedIntegerWidget(BaseWidget[UnboundedIntegerParameter, widgets.IntTe
|
|
|
411
420
|
def _create_widget(
|
|
412
421
|
self,
|
|
413
422
|
parameter: UnboundedIntegerParameter,
|
|
414
|
-
continuous: bool,
|
|
415
423
|
width: str = "auto",
|
|
416
424
|
margin: str = "3px 0px",
|
|
417
425
|
description_width: str = "initial",
|
|
@@ -440,7 +448,6 @@ class UnboundedFloatWidget(BaseWidget[UnboundedFloatParameter, widgets.FloatText
|
|
|
440
448
|
def _create_widget(
|
|
441
449
|
self,
|
|
442
450
|
parameter: UnboundedFloatParameter,
|
|
443
|
-
continuous: bool,
|
|
444
451
|
width: str = "auto",
|
|
445
452
|
margin: str = "3px 0px",
|
|
446
453
|
description_width: str = "initial",
|
|
@@ -470,7 +477,6 @@ class ButtonWidget(BaseWidget[ButtonAction, widgets.Button]):
|
|
|
470
477
|
def _create_widget(
|
|
471
478
|
self,
|
|
472
479
|
parameter: ButtonAction,
|
|
473
|
-
continuous: bool,
|
|
474
480
|
width: str = "auto",
|
|
475
481
|
margin: str = "3px 0px",
|
|
476
482
|
description_width: str = "initial",
|
|
@@ -514,19 +520,32 @@ class ButtonWidget(BaseWidget[ButtonAction, widgets.Button]):
|
|
|
514
520
|
|
|
515
521
|
def create_widget(
|
|
516
522
|
parameter: Union[Parameter[Any], ButtonAction],
|
|
517
|
-
continuous: bool = False,
|
|
518
523
|
width: str = "auto",
|
|
519
524
|
margin: str = "3px 0px",
|
|
520
525
|
description_width: str = "initial",
|
|
521
526
|
) -> BaseWidget[Union[Parameter[Any], ButtonAction], widgets.Widget]:
|
|
522
527
|
"""Create and return the appropriate widget for the given parameter.
|
|
523
528
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
parameter : Union[Parameter[Any], ButtonAction]
|
|
532
|
+
The parameter to create a widget for.
|
|
533
|
+
width : str, optional
|
|
534
|
+
Width of the widget. Default is 'auto'.
|
|
535
|
+
margin : str, optional
|
|
536
|
+
Margin of the widget. Default is '3px 0px'.
|
|
537
|
+
description_width : str, optional
|
|
538
|
+
Width of the description label. Default is 'initial'.
|
|
539
|
+
|
|
540
|
+
Returns
|
|
541
|
+
-------
|
|
542
|
+
BaseWidget[Union[Parameter[Any], ButtonAction], widgets.Widget]
|
|
543
|
+
The appropriate widget instance for the given parameter type.
|
|
544
|
+
|
|
545
|
+
Raises
|
|
546
|
+
------
|
|
547
|
+
ValueError
|
|
548
|
+
If no widget implementation exists for the given parameter type.
|
|
530
549
|
"""
|
|
531
550
|
widget_map = {
|
|
532
551
|
TextParameter: TextWidget,
|
|
@@ -562,7 +581,6 @@ def create_widget(
|
|
|
562
581
|
|
|
563
582
|
return widget_class(
|
|
564
583
|
parameter,
|
|
565
|
-
continuous,
|
|
566
584
|
width=width,
|
|
567
585
|
margin=margin,
|
|
568
586
|
description_width=description_width,
|
|
@@ -12,6 +12,36 @@ NO_UPDATE = NoUpdate()
|
|
|
12
12
|
NO_INITIAL_VALUE = NoInitialValue()
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def make_viewer(plot_func: Optional[Callable] = None) -> "Viewer":
|
|
16
|
+
"""Create an empty viewer object.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
plot_func : Callable, optional
|
|
21
|
+
A function that takes a state dictionary and returns a matplotlib figure.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
viewer : Viewer
|
|
26
|
+
A new viewer object
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> from syd import make_viewer
|
|
31
|
+
>>> def plot(state):
|
|
32
|
+
>>> ... generate figure, plot stuff ...
|
|
33
|
+
>>> return fig
|
|
34
|
+
>>> viewer = make_viewer(plot)
|
|
35
|
+
>>> viewer.add_float('x', value=1.0, min=0, max=10)
|
|
36
|
+
>>> viewer.on_change('x', viewer.update_based_on_x)
|
|
37
|
+
>>> viewer.show()
|
|
38
|
+
"""
|
|
39
|
+
viewer = Viewer()
|
|
40
|
+
if plot_func is not None:
|
|
41
|
+
viewer.set_plot(plot_func)
|
|
42
|
+
return viewer
|
|
43
|
+
|
|
44
|
+
|
|
15
45
|
def validate_parameter_operation(
|
|
16
46
|
operation: str,
|
|
17
47
|
parameter_type: Union[ParameterType, ActionType],
|
|
@@ -216,7 +246,6 @@ class Viewer:
|
|
|
216
246
|
self,
|
|
217
247
|
controls_position: Literal["left", "top", "right", "bottom"] = "left",
|
|
218
248
|
controls_width_percent: int = 20,
|
|
219
|
-
continuous: bool = False,
|
|
220
249
|
suppress_warnings: bool = True,
|
|
221
250
|
update_threshold: float = 1.0,
|
|
222
251
|
):
|
|
@@ -228,7 +257,6 @@ class Viewer:
|
|
|
228
257
|
env="notebook",
|
|
229
258
|
controls_position=controls_position,
|
|
230
259
|
controls_width_percent=controls_width_percent,
|
|
231
|
-
continuous=continuous,
|
|
232
260
|
suppress_warnings=suppress_warnings,
|
|
233
261
|
update_threshold=update_threshold,
|
|
234
262
|
)
|
|
@@ -427,9 +455,8 @@ class Viewer:
|
|
|
427
455
|
try:
|
|
428
456
|
self._in_callbacks = True
|
|
429
457
|
if name in self.callbacks:
|
|
430
|
-
state = self.state
|
|
431
458
|
for callback in self.callbacks[name]:
|
|
432
|
-
callback(state)
|
|
459
|
+
callback(self.state)
|
|
433
460
|
finally:
|
|
434
461
|
self._in_callbacks = False
|
|
435
462
|
|
syd-1.0.2/syd/__init__.py
DELETED
|
File without changes
|
{syd-1.0.2 → syd-1.1.0}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|