syd 1.0.0__tar.gz → 1.0.2__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.0 → syd-1.0.2}/PKG-INFO +48 -36
- {syd-1.0.0 → syd-1.0.2}/README.md +47 -35
- {syd-1.0.0 → syd-1.0.2}/syd/__init__.py +1 -1
- {syd-1.0.0 → syd-1.0.2}/syd/notebook_deployment/deployer.py +106 -22
- {syd-1.0.0 → syd-1.0.2}/syd/viewer.py +51 -3
- {syd-1.0.0 → syd-1.0.2}/.gitignore +0 -0
- {syd-1.0.0 → syd-1.0.2}/LICENSE +0 -0
- {syd-1.0.0 → syd-1.0.2}/pyproject.toml +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/__init__.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/deployer.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/static/__init__.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/static/css/styles.css +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/static/js/viewer.js +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/templates/__init__.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/templates/index.html +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/flask_deployment/testing_principles.md +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/notebook_deployment/__init__.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/notebook_deployment/widgets.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/parameters.py +0 -0
- {syd-1.0.0 → syd-1.0.2}/syd/support.py +0 -0
{syd-1.0.0 → syd-1.0.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syd
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
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>
|
|
@@ -37,13 +37,17 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
[](https://github.com/psf/black)
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
<div>
|
|
41
|
+
<img src="./docs/assets/syd-logo-white.png" alt="Syd" width="350" align="right"/>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
40
44
|
A package to help you share your data!
|
|
41
45
|
|
|
42
46
|
Have you ever wanted to look through all your data really quickly interactively? Of course you have. Mo data mo problems, but only if you don't know what to do with it. And that's why Syd stands for show your data!
|
|
43
47
|
|
|
44
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!
|
|
45
49
|
|
|
46
|
-
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
|
|
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!
|
|
47
51
|
|
|
48
52
|
## Installation
|
|
49
53
|
It's easy, just use pip install. The dependencies are light so it should work in most environments.
|
|
@@ -56,27 +60,31 @@ The full documentation is available at [shareyourdata.readthedocs.io](https://sh
|
|
|
56
60
|
|
|
57
61
|
## Quick Start
|
|
58
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!).
|
|
63
|
+
|
|
59
64
|
```python
|
|
60
|
-
import matplotlib.pyplot as plt
|
|
61
65
|
import numpy as np
|
|
66
|
+
import matplotlib.pyplot as plt
|
|
62
67
|
from syd import make_viewer
|
|
63
|
-
def plot(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ax.
|
|
68
|
+
def plot(state):
|
|
69
|
+
# Here's a simple plot function that plots a sine wave
|
|
70
|
+
fig = plt.figure()
|
|
71
|
+
t = np.linspace(0, 2 * np.pi, 1000)
|
|
72
|
+
ax = plt.gca()
|
|
73
|
+
ax.plot(t, state["amplitude"] * np.sin(state["frequency"] * t), color=state["color"])
|
|
68
74
|
return fig
|
|
69
|
-
|
|
70
|
-
viewer = make_viewer()
|
|
71
|
-
viewer.
|
|
72
|
-
viewer.add_float(
|
|
73
|
-
viewer.
|
|
75
|
+
|
|
76
|
+
viewer = make_viewer(plot)
|
|
77
|
+
viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
|
|
78
|
+
viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
|
|
79
|
+
viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
|
|
74
80
|
|
|
75
81
|
# env = "browser" # for viewing in a web browser
|
|
76
82
|
env = "notebook" # for viewing within a jupyter notebook
|
|
77
|
-
viewer.
|
|
83
|
+
viewer.show()
|
|
78
84
|
```
|
|
79
85
|
|
|
86
|
+

|
|
87
|
+
|
|
80
88
|
### More Examples
|
|
81
89
|
We have several examples of more complex viewers with detailed explanations in the comments. Here are the links and descriptions to each of them:
|
|
82
90
|
|
|
@@ -90,7 +98,7 @@ We have several examples of more complex viewers with detailed explanations in t
|
|
|
90
98
|
|
|
91
99
|
|
|
92
100
|
### Data loading
|
|
93
|
-
Thinking about how to get data into a Syd viewer can be non-intuitive. For some examples that showcase different ways to get your data into a Syd viewer, check out the [data loading example](examples/3-data_loading.ipynb). Or, if you just want a quick example, check this out:
|
|
101
|
+
Thinking about how to get data into a Syd viewer can be non-intuitive. For some examples that showcase different ways to get your data into a Syd viewer, check out the [data loading example](examples/3-data_loading.ipynb). Or, if you just want a quick and fast example, check this one out:
|
|
94
102
|
```python
|
|
95
103
|
import numpy as np
|
|
96
104
|
from matplotlib import pyplot as plt
|
|
@@ -111,15 +119,15 @@ def plot(state):
|
|
|
111
119
|
# Since plot "knows" about the data variable, all you need to do is pass the plot
|
|
112
120
|
# function to the syd viewer and it'll be able to access the data once deployed!
|
|
113
121
|
viewer = make_viewer(plot)
|
|
114
|
-
viewer.
|
|
122
|
+
viewer.show()
|
|
115
123
|
```
|
|
116
124
|
|
|
117
125
|
### Handling Hierarchical Callbacks
|
|
118
|
-
Syd dramatically reduces the amount of work you need to do to build a GUI for viewing your data. However, it can still be a bit complicated to think about callbacks. Below is a quick demonstration
|
|
126
|
+
Syd dramatically reduces the amount of work you need to do to build a GUI for viewing your data. However, it can still be a bit complicated to think about callbacks. Below is a quick demonstration. To try it yourself, check out the full example [here](examples/4-hierarchical_callbacks.ipynb) or open it in colab [](https://colab.research.google.com/github/landoskape/syd/blob/main/examples/4-hierarchical_callbacks.ipynb).
|
|
119
127
|
|
|
120
|
-
For example, suppose your dataset is composed of electrophysiology recordings from 3 mice, where each mouse has a different number of sesssions, and each session has a different number of neurons. You want to build a viewer to
|
|
128
|
+
For example, suppose your dataset is composed of electrophysiology recordings from 3 mice, where each mouse has a different number of sesssions, and each session has a different number of neurons. You want to build a viewer to view a particular neuron from a particular session from a particular mouse. But the viewer will break if you try to index to session 5 for mouse 2 when mouse 2 has less than 5 sessions!
|
|
121
129
|
|
|
122
|
-
This is where hierarchical callbacks come in. There's a straightforward pattern to handling this kind of situation that you can follow. You can write a callback for each **level** of the hierarchy. Then, each callback can call the next callback in the hierarchy. It looks like this:
|
|
130
|
+
This is where hierarchical callbacks come in. There's a straightforward pattern to handling this kind of situation that you can follow. You can write a callback for each **level** of the hierarchy. Then, each callback can **update** the state and call the next callback in the hierarchy once it's finished. It looks like this:
|
|
123
131
|
```python
|
|
124
132
|
import numpy as np
|
|
125
133
|
from syd import Viewer # Much easier to build a Viewer class for hierarchical callbacks
|
|
@@ -130,19 +138,22 @@ class MouseViewer(Viewer):
|
|
|
130
138
|
|
|
131
139
|
self.add_selection("mouse", options=list(mice_names))
|
|
132
140
|
|
|
133
|
-
# We don't know how many sessions or neurons to pick from yet
|
|
141
|
+
# We don't know how many sessions or neurons to pick from yet,
|
|
142
|
+
# so just set the max to 1 for now.
|
|
134
143
|
self.add_integer("session", min=0, max=1)
|
|
135
144
|
self.add_integer("neuron", min=0, max=1)
|
|
136
145
|
|
|
137
|
-
# Any time the mouse changes, update the sessions to pick from
|
|
146
|
+
# Any time the mouse changes, update the sessions to pick from!
|
|
138
147
|
self.on_change("mouse", self.update_mouse)
|
|
139
148
|
|
|
140
|
-
# Any time the session changes, update the neurons to pick from
|
|
149
|
+
# Any time the session changes, update the neurons to pick from!
|
|
141
150
|
self.on_change("session", self.update_session)
|
|
142
151
|
|
|
143
152
|
# Since we built callbacks for setting the range of the session
|
|
144
|
-
# and neuron parameters, we can use them here
|
|
145
|
-
#
|
|
153
|
+
# and neuron parameters, we can use them here so the viewer is
|
|
154
|
+
# fully ready and up to date.
|
|
155
|
+
|
|
156
|
+
# To get the state, use self.state, which is the current
|
|
146
157
|
# state of the viewer (in the init function, it'll just be the
|
|
147
158
|
# default value for each parameter you've added already).
|
|
148
159
|
self.update_mouse(self.state)
|
|
@@ -155,9 +166,13 @@ class MouseViewer(Viewer):
|
|
|
155
166
|
self.update_integer("session", max=num_sessions - 1)
|
|
156
167
|
|
|
157
168
|
# Now we need to update the neurons to choose from ....
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
#
|
|
169
|
+
|
|
170
|
+
# But! Updating the session parameter's max value might trigger a change
|
|
171
|
+
# to the current session value. This ~won't be reflected~ in the state
|
|
172
|
+
# dictionary that was passed to this function.
|
|
173
|
+
|
|
174
|
+
# So, we need to load the ~NEW~ state dictionary, which is always
|
|
175
|
+
# accessible as self.state (or viewer.state if you're not using a class).
|
|
161
176
|
new_state = self.state
|
|
162
177
|
|
|
163
178
|
# Then perform the session update callback!
|
|
@@ -178,7 +193,7 @@ class MouseViewer(Viewer):
|
|
|
178
193
|
|
|
179
194
|
# Now we can create a viewer and deploy it
|
|
180
195
|
viewer = MouseViewer(["Mouse 1", "Mouse 2", "Mouse 3"])
|
|
181
|
-
viewer.
|
|
196
|
+
viewer.show()
|
|
182
197
|
```
|
|
183
198
|
|
|
184
199
|
## License
|
|
@@ -197,8 +212,11 @@ Contributions are welcome! Here's how you can help:
|
|
|
197
212
|
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
198
213
|
7. Open a Pull Request online
|
|
199
214
|
|
|
200
|
-
Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines).
|
|
201
|
-
|
|
215
|
+
Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). I don't have any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
|
|
216
|
+
```bash
|
|
217
|
+
pip install black
|
|
218
|
+
black . # from the root directory of the repo
|
|
219
|
+
```
|
|
202
220
|
|
|
203
221
|
## To-Do List
|
|
204
222
|
- Layout controls
|
|
@@ -208,12 +226,6 @@ Please make sure to update tests as appropriate and adhere to the existing codin
|
|
|
208
226
|
- [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
|
|
209
227
|
- [ ] Add a window for capturing any error messages that might be thrown by the plot function. Maybe we could have a little interface for looking at each one (up to a point) and the user could press a button to throw an error for the traceback.
|
|
210
228
|
- [ ] Consider "app_deployed" context for each deployer...
|
|
211
|
-
- [ ] Consider adding a step to the integer parameters...
|
|
212
|
-
- Idea for figure management:
|
|
213
|
-
- [ ] We could make fig=?, ax=? arguments optional for the plot function and add a
|
|
214
|
-
"recycle_figure: bool = False" flag be part of the deploy API. This way, an
|
|
215
|
-
advanced user that wants snappy responsivity or complex figure management can
|
|
216
|
-
do so, but the default is for the user to generate a new figure object each time.
|
|
217
229
|
- Export options:
|
|
218
230
|
- [ ] 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.
|
|
219
231
|
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
[](https://github.com/psf/black)
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
<div>
|
|
11
|
+
<img src="./docs/assets/syd-logo-white.png" alt="Syd" width="350" align="right"/>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
10
14
|
A package to help you share your data!
|
|
11
15
|
|
|
12
16
|
Have you ever wanted to look through all your data really quickly interactively? Of course you have. Mo data mo problems, but only if you don't know what to do with it. And that's why Syd stands for show your data!
|
|
13
17
|
|
|
14
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!
|
|
15
19
|
|
|
16
|
-
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
|
|
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!
|
|
17
21
|
|
|
18
22
|
## Installation
|
|
19
23
|
It's easy, just use pip install. The dependencies are light so it should work in most environments.
|
|
@@ -26,27 +30,31 @@ The full documentation is available at [shareyourdata.readthedocs.io](https://sh
|
|
|
26
30
|
|
|
27
31
|
## Quick Start
|
|
28
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!).
|
|
33
|
+
|
|
29
34
|
```python
|
|
30
|
-
import matplotlib.pyplot as plt
|
|
31
35
|
import numpy as np
|
|
36
|
+
import matplotlib.pyplot as plt
|
|
32
37
|
from syd import make_viewer
|
|
33
|
-
def plot(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ax.
|
|
38
|
+
def plot(state):
|
|
39
|
+
# Here's a simple plot function that plots a sine wave
|
|
40
|
+
fig = plt.figure()
|
|
41
|
+
t = np.linspace(0, 2 * np.pi, 1000)
|
|
42
|
+
ax = plt.gca()
|
|
43
|
+
ax.plot(t, state["amplitude"] * np.sin(state["frequency"] * t), color=state["color"])
|
|
38
44
|
return fig
|
|
39
|
-
|
|
40
|
-
viewer = make_viewer()
|
|
41
|
-
viewer.
|
|
42
|
-
viewer.add_float(
|
|
43
|
-
viewer.
|
|
45
|
+
|
|
46
|
+
viewer = make_viewer(plot)
|
|
47
|
+
viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
|
|
48
|
+
viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
|
|
49
|
+
viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
|
|
44
50
|
|
|
45
51
|
# env = "browser" # for viewing in a web browser
|
|
46
52
|
env = "notebook" # for viewing within a jupyter notebook
|
|
47
|
-
viewer.
|
|
53
|
+
viewer.show()
|
|
48
54
|
```
|
|
49
55
|
|
|
56
|
+

|
|
57
|
+
|
|
50
58
|
### More Examples
|
|
51
59
|
We have several examples of more complex viewers with detailed explanations in the comments. Here are the links and descriptions to each of them:
|
|
52
60
|
|
|
@@ -60,7 +68,7 @@ We have several examples of more complex viewers with detailed explanations in t
|
|
|
60
68
|
|
|
61
69
|
|
|
62
70
|
### Data loading
|
|
63
|
-
Thinking about how to get data into a Syd viewer can be non-intuitive. For some examples that showcase different ways to get your data into a Syd viewer, check out the [data loading example](examples/3-data_loading.ipynb). Or, if you just want a quick example, check this out:
|
|
71
|
+
Thinking about how to get data into a Syd viewer can be non-intuitive. For some examples that showcase different ways to get your data into a Syd viewer, check out the [data loading example](examples/3-data_loading.ipynb). Or, if you just want a quick and fast example, check this one out:
|
|
64
72
|
```python
|
|
65
73
|
import numpy as np
|
|
66
74
|
from matplotlib import pyplot as plt
|
|
@@ -81,15 +89,15 @@ def plot(state):
|
|
|
81
89
|
# Since plot "knows" about the data variable, all you need to do is pass the plot
|
|
82
90
|
# function to the syd viewer and it'll be able to access the data once deployed!
|
|
83
91
|
viewer = make_viewer(plot)
|
|
84
|
-
viewer.
|
|
92
|
+
viewer.show()
|
|
85
93
|
```
|
|
86
94
|
|
|
87
95
|
### Handling Hierarchical Callbacks
|
|
88
|
-
Syd dramatically reduces the amount of work you need to do to build a GUI for viewing your data. However, it can still be a bit complicated to think about callbacks. Below is a quick demonstration
|
|
96
|
+
Syd dramatically reduces the amount of work you need to do to build a GUI for viewing your data. However, it can still be a bit complicated to think about callbacks. Below is a quick demonstration. To try it yourself, check out the full example [here](examples/4-hierarchical_callbacks.ipynb) or open it in colab [](https://colab.research.google.com/github/landoskape/syd/blob/main/examples/4-hierarchical_callbacks.ipynb).
|
|
89
97
|
|
|
90
|
-
For example, suppose your dataset is composed of electrophysiology recordings from 3 mice, where each mouse has a different number of sesssions, and each session has a different number of neurons. You want to build a viewer to
|
|
98
|
+
For example, suppose your dataset is composed of electrophysiology recordings from 3 mice, where each mouse has a different number of sesssions, and each session has a different number of neurons. You want to build a viewer to view a particular neuron from a particular session from a particular mouse. But the viewer will break if you try to index to session 5 for mouse 2 when mouse 2 has less than 5 sessions!
|
|
91
99
|
|
|
92
|
-
This is where hierarchical callbacks come in. There's a straightforward pattern to handling this kind of situation that you can follow. You can write a callback for each **level** of the hierarchy. Then, each callback can call the next callback in the hierarchy. It looks like this:
|
|
100
|
+
This is where hierarchical callbacks come in. There's a straightforward pattern to handling this kind of situation that you can follow. You can write a callback for each **level** of the hierarchy. Then, each callback can **update** the state and call the next callback in the hierarchy once it's finished. It looks like this:
|
|
93
101
|
```python
|
|
94
102
|
import numpy as np
|
|
95
103
|
from syd import Viewer # Much easier to build a Viewer class for hierarchical callbacks
|
|
@@ -100,19 +108,22 @@ class MouseViewer(Viewer):
|
|
|
100
108
|
|
|
101
109
|
self.add_selection("mouse", options=list(mice_names))
|
|
102
110
|
|
|
103
|
-
# We don't know how many sessions or neurons to pick from yet
|
|
111
|
+
# We don't know how many sessions or neurons to pick from yet,
|
|
112
|
+
# so just set the max to 1 for now.
|
|
104
113
|
self.add_integer("session", min=0, max=1)
|
|
105
114
|
self.add_integer("neuron", min=0, max=1)
|
|
106
115
|
|
|
107
|
-
# Any time the mouse changes, update the sessions to pick from
|
|
116
|
+
# Any time the mouse changes, update the sessions to pick from!
|
|
108
117
|
self.on_change("mouse", self.update_mouse)
|
|
109
118
|
|
|
110
|
-
# Any time the session changes, update the neurons to pick from
|
|
119
|
+
# Any time the session changes, update the neurons to pick from!
|
|
111
120
|
self.on_change("session", self.update_session)
|
|
112
121
|
|
|
113
122
|
# Since we built callbacks for setting the range of the session
|
|
114
|
-
# and neuron parameters, we can use them here
|
|
115
|
-
#
|
|
123
|
+
# and neuron parameters, we can use them here so the viewer is
|
|
124
|
+
# fully ready and up to date.
|
|
125
|
+
|
|
126
|
+
# To get the state, use self.state, which is the current
|
|
116
127
|
# state of the viewer (in the init function, it'll just be the
|
|
117
128
|
# default value for each parameter you've added already).
|
|
118
129
|
self.update_mouse(self.state)
|
|
@@ -125,9 +136,13 @@ class MouseViewer(Viewer):
|
|
|
125
136
|
self.update_integer("session", max=num_sessions - 1)
|
|
126
137
|
|
|
127
138
|
# Now we need to update the neurons to choose from ....
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
#
|
|
139
|
+
|
|
140
|
+
# But! Updating the session parameter's max value might trigger a change
|
|
141
|
+
# to the current session value. This ~won't be reflected~ in the state
|
|
142
|
+
# dictionary that was passed to this function.
|
|
143
|
+
|
|
144
|
+
# So, we need to load the ~NEW~ state dictionary, which is always
|
|
145
|
+
# accessible as self.state (or viewer.state if you're not using a class).
|
|
131
146
|
new_state = self.state
|
|
132
147
|
|
|
133
148
|
# Then perform the session update callback!
|
|
@@ -148,7 +163,7 @@ class MouseViewer(Viewer):
|
|
|
148
163
|
|
|
149
164
|
# Now we can create a viewer and deploy it
|
|
150
165
|
viewer = MouseViewer(["Mouse 1", "Mouse 2", "Mouse 3"])
|
|
151
|
-
viewer.
|
|
166
|
+
viewer.show()
|
|
152
167
|
```
|
|
153
168
|
|
|
154
169
|
## License
|
|
@@ -167,8 +182,11 @@ Contributions are welcome! Here's how you can help:
|
|
|
167
182
|
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
168
183
|
7. Open a Pull Request online
|
|
169
184
|
|
|
170
|
-
Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines).
|
|
171
|
-
|
|
185
|
+
Please make sure to update tests as appropriate and adhere to the existing coding style (black, line-length=88, other style guidelines not capture by black, generally following pep8 guidelines). Try to make the code coverage report improve or stay the same rather than decrease (right now the deployment system isn't covered by tests). I don't have any precommit hooks or anything so you're responsible for checking this yourself. You can process the code with black as follows:
|
|
186
|
+
```bash
|
|
187
|
+
pip install black
|
|
188
|
+
black . # from the root directory of the repo
|
|
189
|
+
```
|
|
172
190
|
|
|
173
191
|
## To-Do List
|
|
174
192
|
- Layout controls
|
|
@@ -178,12 +196,6 @@ Please make sure to update tests as appropriate and adhere to the existing codin
|
|
|
178
196
|
- [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
|
|
179
197
|
- [ ] Add a window for capturing any error messages that might be thrown by the plot function. Maybe we could have a little interface for looking at each one (up to a point) and the user could press a button to throw an error for the traceback.
|
|
180
198
|
- [ ] Consider "app_deployed" context for each deployer...
|
|
181
|
-
- [ ] Consider adding a step to the integer parameters...
|
|
182
|
-
- Idea for figure management:
|
|
183
|
-
- [ ] We could make fig=?, ax=? arguments optional for the plot function and add a
|
|
184
|
-
"recycle_figure: bool = False" flag be part of the deploy API. This way, an
|
|
185
|
-
advanced user that wants snappy responsivity or complex figure management can
|
|
186
|
-
do so, but the default is for the user to generate a new figure object each time.
|
|
187
199
|
- Export options:
|
|
188
200
|
- [ ] 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.
|
|
189
201
|
- [ ] Export full: export the viewer in a way that contains the data to give full functionality.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Literal, Optional
|
|
2
2
|
import warnings
|
|
3
|
+
import threading
|
|
4
|
+
from contextlib import contextmanager
|
|
3
5
|
import ipywidgets as widgets
|
|
4
6
|
from IPython.display import display
|
|
5
7
|
import matplotlib as mpl
|
|
@@ -39,6 +41,7 @@ class NotebookDeployer:
|
|
|
39
41
|
controls_width_percent: int = 20,
|
|
40
42
|
continuous: bool = False,
|
|
41
43
|
suppress_warnings: bool = True,
|
|
44
|
+
update_threshold: float = 1.0,
|
|
42
45
|
):
|
|
43
46
|
self.viewer = viewer
|
|
44
47
|
self.components: dict[str, BaseWidget] = {}
|
|
@@ -56,12 +59,58 @@ class NotebookDeployer:
|
|
|
56
59
|
"The behavior of the viewer will almost definitely not work as expected!"
|
|
57
60
|
)
|
|
58
61
|
self._last_figure = None
|
|
62
|
+
self._update_event = threading.Event()
|
|
63
|
+
self.update_threshold = update_threshold
|
|
64
|
+
self._slow_loading_figure = None
|
|
65
|
+
self._display_lock = threading.Lock() # Lock for synchronizing display updates
|
|
66
|
+
|
|
67
|
+
def _show_slow_loading(self):
|
|
68
|
+
if self.backend_type == "inline":
|
|
69
|
+
if not self._update_event.wait(self.update_threshold):
|
|
70
|
+
if self._slow_loading_figure is None:
|
|
71
|
+
fig = plt.figure()
|
|
72
|
+
ax = fig.add_subplot(111)
|
|
73
|
+
ax.text(
|
|
74
|
+
0.5,
|
|
75
|
+
0.5,
|
|
76
|
+
"waiting for next figure...",
|
|
77
|
+
ha="center",
|
|
78
|
+
va="center",
|
|
79
|
+
fontsize=12,
|
|
80
|
+
weight="bold",
|
|
81
|
+
color="black",
|
|
82
|
+
)
|
|
83
|
+
ax.axis("off")
|
|
84
|
+
self._slow_loading_figure = fig
|
|
85
|
+
if not self._showing_new_figure:
|
|
86
|
+
self._display_figure(self._slow_loading_figure, store_figure=False)
|
|
87
|
+
self._showing_slow_loading_figure = True
|
|
88
|
+
|
|
89
|
+
@contextmanager
|
|
90
|
+
def _perform_update(self):
|
|
91
|
+
self._updating = True
|
|
92
|
+
self._showing_new_figure = False
|
|
93
|
+
self._showing_slow_loading_figure = False
|
|
94
|
+
self._update_event.clear()
|
|
95
|
+
|
|
96
|
+
thread = threading.Thread(target=self._show_slow_loading, daemon=True)
|
|
97
|
+
thread.start()
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
yield
|
|
101
|
+
finally:
|
|
102
|
+
self._updating = False
|
|
103
|
+
self._update_event.set()
|
|
104
|
+
thread.join()
|
|
105
|
+
if self._showing_slow_loading_figure:
|
|
106
|
+
self._display_figure(self._last_figure)
|
|
107
|
+
self._update_status("Ready!")
|
|
59
108
|
|
|
60
109
|
def deploy(self) -> None:
|
|
61
110
|
"""Deploy the viewer."""
|
|
111
|
+
self.backend_type = get_backend_type()
|
|
62
112
|
self.build_components()
|
|
63
113
|
self.build_layout()
|
|
64
|
-
self.backend_type = get_backend_type()
|
|
65
114
|
display(self.layout)
|
|
66
115
|
self.update_plot()
|
|
67
116
|
|
|
@@ -80,6 +129,10 @@ class NotebookDeployer:
|
|
|
80
129
|
|
|
81
130
|
# Controls width slider for horizontal layouts
|
|
82
131
|
self.controls = {}
|
|
132
|
+
self.controls["status"] = widgets.HTML(
|
|
133
|
+
value="<b>Syd Controls</b>",
|
|
134
|
+
layout=widgets.Layout(width="95%"),
|
|
135
|
+
)
|
|
83
136
|
if self.controls_position in ["left", "right"]:
|
|
84
137
|
self.controls["controls_width"] = widgets.IntSlider(
|
|
85
138
|
value=self.controls_width_percent,
|
|
@@ -90,6 +143,15 @@ class NotebookDeployer:
|
|
|
90
143
|
layout=widgets.Layout(width="95%"),
|
|
91
144
|
style={"description_width": "initial"},
|
|
92
145
|
)
|
|
146
|
+
if self.backend_type == "inline":
|
|
147
|
+
self.controls["update_threshold"] = widgets.FloatSlider(
|
|
148
|
+
value=self.update_threshold,
|
|
149
|
+
min=0.1,
|
|
150
|
+
max=10.0,
|
|
151
|
+
description="Update Threshold",
|
|
152
|
+
layout=widgets.Layout(width="95%"),
|
|
153
|
+
style={"description_width": "initial"},
|
|
154
|
+
)
|
|
93
155
|
|
|
94
156
|
# Create parameter controls section
|
|
95
157
|
param_box = widgets.VBox(
|
|
@@ -102,7 +164,7 @@ class NotebookDeployer:
|
|
|
102
164
|
if self.controls_position in ["left", "right"]:
|
|
103
165
|
# Create layout controls section if horizontal (might include for vertical later when we have more permanent controls...)
|
|
104
166
|
layout_box = widgets.VBox(
|
|
105
|
-
|
|
167
|
+
list(self.controls.values()),
|
|
106
168
|
layout=widgets.Layout(margin="10px 0px"),
|
|
107
169
|
)
|
|
108
170
|
|
|
@@ -112,6 +174,11 @@ class NotebookDeployer:
|
|
|
112
174
|
self._handle_container_width_change, names="value"
|
|
113
175
|
)
|
|
114
176
|
|
|
177
|
+
if "update_threshold" in self.controls:
|
|
178
|
+
self.controls["update_threshold"].observe(
|
|
179
|
+
self._handle_update_threshold_change, names="value"
|
|
180
|
+
)
|
|
181
|
+
|
|
115
182
|
widgets_elements = [param_box, layout_box]
|
|
116
183
|
else:
|
|
117
184
|
widgets_elements = [param_box]
|
|
@@ -154,6 +221,8 @@ class NotebookDeployer:
|
|
|
154
221
|
else:
|
|
155
222
|
self.layout = widgets.VBox([self.widgets_container, self.plot_container])
|
|
156
223
|
|
|
224
|
+
self._update_status("Ready!")
|
|
225
|
+
|
|
157
226
|
def handle_component_engagement(self, name: str) -> None:
|
|
158
227
|
"""Handle engagement with an interactive component."""
|
|
159
228
|
if self._updating:
|
|
@@ -164,9 +233,8 @@ class NotebookDeployer:
|
|
|
164
233
|
)
|
|
165
234
|
return
|
|
166
235
|
|
|
167
|
-
|
|
168
|
-
self.
|
|
169
|
-
|
|
236
|
+
with self._perform_update():
|
|
237
|
+
self._update_status(f"Updating {name}")
|
|
170
238
|
# Optionally suppress warnings during parameter updates
|
|
171
239
|
with warnings.catch_warnings():
|
|
172
240
|
if self.suppress_warnings:
|
|
@@ -188,9 +256,6 @@ class NotebookDeployer:
|
|
|
188
256
|
# Update the plot
|
|
189
257
|
self.update_plot()
|
|
190
258
|
|
|
191
|
-
finally:
|
|
192
|
-
self._updating = False
|
|
193
|
-
|
|
194
259
|
def sync_components_with_state(self, exclude: Optional[str] = None) -> None:
|
|
195
260
|
"""Sync component values with viewer state."""
|
|
196
261
|
for name, parameter in self.viewer.parameters.items():
|
|
@@ -211,25 +276,32 @@ class NotebookDeployer:
|
|
|
211
276
|
# Update components if plot function updated a parameter
|
|
212
277
|
self.sync_components_with_state()
|
|
213
278
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
279
|
+
self._display_figure(figure)
|
|
280
|
+
|
|
281
|
+
self._showing_new_figure = True
|
|
217
282
|
|
|
218
|
-
|
|
219
|
-
with self.
|
|
220
|
-
if
|
|
221
|
-
|
|
283
|
+
def _display_figure(self, figure: plt.Figure, store_figure: bool = True) -> None:
|
|
284
|
+
with self._display_lock:
|
|
285
|
+
# Close the last figure if it exists to keep matplotlib clean
|
|
286
|
+
if self._last_figure is not None:
|
|
287
|
+
plt.close(self._last_figure)
|
|
222
288
|
|
|
223
|
-
|
|
224
|
-
|
|
289
|
+
self.plot_output.clear_output(wait=True)
|
|
290
|
+
with self.plot_output:
|
|
291
|
+
if self.backend_type == "inline":
|
|
292
|
+
display(figure)
|
|
225
293
|
|
|
226
|
-
|
|
227
|
-
|
|
294
|
+
# Also required to make sure a second figure window isn't opened
|
|
295
|
+
plt.close(figure)
|
|
228
296
|
|
|
229
|
-
|
|
230
|
-
|
|
297
|
+
elif self.backend_type == "widget":
|
|
298
|
+
display(figure.canvas)
|
|
231
299
|
|
|
232
|
-
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError(f"Unsupported backend type: {self.backend_type}")
|
|
302
|
+
|
|
303
|
+
if store_figure:
|
|
304
|
+
self._last_figure = figure
|
|
233
305
|
|
|
234
306
|
def _handle_container_width_change(self, _) -> None:
|
|
235
307
|
"""Handle changes to container width proportions."""
|
|
@@ -239,3 +311,15 @@ class NotebookDeployer:
|
|
|
239
311
|
# Update container widths
|
|
240
312
|
self.widgets_container.layout.width = f"{width_percent}%"
|
|
241
313
|
self.plot_container.layout.width = f"{100 - width_percent}%"
|
|
314
|
+
|
|
315
|
+
def _handle_update_threshold_change(self, _) -> None:
|
|
316
|
+
"""Handle changes to update threshold."""
|
|
317
|
+
self.update_threshold = self.controls["update_threshold"].value
|
|
318
|
+
|
|
319
|
+
def _update_status(self, status: str) -> None:
|
|
320
|
+
"""Update the status text."""
|
|
321
|
+
value = "<b>Syd Controls</b> "
|
|
322
|
+
value += "<span style='background-color: #e0e0e0; color: #000; padding: 2px 6px; border-radius: 4px; font-size: 90%;'>"
|
|
323
|
+
value += f"Status: {status}"
|
|
324
|
+
value += "</span>"
|
|
325
|
+
self.controls["status"].value = value
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Any, Callable, Dict, Tuple, Union, Optional
|
|
1
|
+
from typing import List, Any, Callable, Dict, Tuple, Union, Optional, Literal
|
|
2
2
|
from functools import wraps, partial
|
|
3
3
|
import inspect
|
|
4
4
|
from contextlib import contextmanager
|
|
@@ -212,6 +212,54 @@ class Viewer:
|
|
|
212
212
|
"""Set the plot method for the viewer"""
|
|
213
213
|
self.plot = self._prepare_function(func, context="Setting plot:")
|
|
214
214
|
|
|
215
|
+
def show(
|
|
216
|
+
self,
|
|
217
|
+
controls_position: Literal["left", "top", "right", "bottom"] = "left",
|
|
218
|
+
controls_width_percent: int = 20,
|
|
219
|
+
continuous: bool = False,
|
|
220
|
+
suppress_warnings: bool = True,
|
|
221
|
+
update_threshold: float = 1.0,
|
|
222
|
+
):
|
|
223
|
+
"""Show the viewer in a notebook
|
|
224
|
+
|
|
225
|
+
Same as deploy(env="notebook") except it doesn't return the viewer object.
|
|
226
|
+
"""
|
|
227
|
+
_ = self.deploy(
|
|
228
|
+
env="notebook",
|
|
229
|
+
controls_position=controls_position,
|
|
230
|
+
controls_width_percent=controls_width_percent,
|
|
231
|
+
continuous=continuous,
|
|
232
|
+
suppress_warnings=suppress_warnings,
|
|
233
|
+
update_threshold=update_threshold,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def share(
|
|
237
|
+
self,
|
|
238
|
+
controls_position: str = "left",
|
|
239
|
+
fig_dpi: int = 300,
|
|
240
|
+
controls_width_percent: int = 20,
|
|
241
|
+
suppress_warnings: bool = True,
|
|
242
|
+
debug: bool = False,
|
|
243
|
+
host: str = "127.0.0.1",
|
|
244
|
+
port: Optional[int] = None,
|
|
245
|
+
open_browser: bool = True,
|
|
246
|
+
):
|
|
247
|
+
"""Share the viewer on a web browser using Flask
|
|
248
|
+
|
|
249
|
+
Same as deploy(env="browser") except it doesn't return the viewer object.
|
|
250
|
+
"""
|
|
251
|
+
_ = self.deploy(
|
|
252
|
+
env="browser",
|
|
253
|
+
controls_position=controls_position,
|
|
254
|
+
fig_dpi=fig_dpi,
|
|
255
|
+
controls_width_percent=controls_width_percent,
|
|
256
|
+
suppress_warnings=suppress_warnings,
|
|
257
|
+
debug=debug,
|
|
258
|
+
host=host,
|
|
259
|
+
port=port,
|
|
260
|
+
open_browser=open_browser,
|
|
261
|
+
)
|
|
262
|
+
|
|
215
263
|
def deploy(self, env: str = "notebook", **kwargs):
|
|
216
264
|
"""Deploy the app in a notebook or standalone environment"""
|
|
217
265
|
env = env.lower()
|
|
@@ -223,7 +271,7 @@ class Viewer:
|
|
|
223
271
|
deployer.deploy()
|
|
224
272
|
return self
|
|
225
273
|
|
|
226
|
-
elif env == "browser"
|
|
274
|
+
elif env == "browser":
|
|
227
275
|
# On demand import because the deployers need to import the viewer
|
|
228
276
|
from .flask_deployment.deployer import FlaskDeployer
|
|
229
277
|
|
|
@@ -238,7 +286,7 @@ class Viewer:
|
|
|
238
286
|
|
|
239
287
|
else:
|
|
240
288
|
raise ValueError(
|
|
241
|
-
f"Unsupported environment: {env}, only 'notebook', '
|
|
289
|
+
f"Unsupported environment: {env}, only 'notebook', 'browser' are supported right now."
|
|
242
290
|
)
|
|
243
291
|
|
|
244
292
|
@contextmanager
|
|
File without changes
|
{syd-1.0.0 → syd-1.0.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|