syd 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
syd/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import Callable, Optional
2
2
  from .viewer import Viewer
3
3
 
4
- __version__ = "1.0.0"
4
+ __version__ = "1.0.1"
5
5
 
6
6
 
7
7
  def make_viewer(plot_func: Optional[Callable] = None):
@@ -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
- [widgets.HTML("<b>Syd Controls</b>")] + list(self.controls.values()),
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
- try:
168
- self._updating = True
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
- # Close the last figure if it exists to keep matplotlib clean
215
- if self._last_figure is not None:
216
- plt.close(self._last_figure)
279
+ self._display_figure(figure)
280
+
281
+ self._showing_new_figure = True
217
282
 
218
- self.plot_output.clear_output(wait=True)
219
- with self.plot_output:
220
- if self.backend_type == "inline":
221
- display(figure)
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
- # Also required to make sure a second figure window isn't opened
224
- plt.close(figure)
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
- elif self.backend_type == "widget":
227
- display(figure.canvas)
294
+ # Also required to make sure a second figure window isn't opened
295
+ plt.close(figure)
228
296
 
229
- else:
230
- raise ValueError(f"Unsupported backend type: {self.backend_type}")
297
+ elif self.backend_type == "widget":
298
+ display(figure.canvas)
231
299
 
232
- self._last_figure = figure
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syd
3
- Version: 1.0.0
3
+ Version: 1.0.1
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
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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 _**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 do you know what that means? It 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!
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.
@@ -55,28 +59,35 @@ pip install syd
55
59
  The full documentation is available at [shareyourdata.readthedocs.io](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)!
56
60
 
57
61
  ## Quick Start
62
+ <!-- <div style="float: right; margin-left: 100px; margin-bottom: 10px;">
63
+ <img src="./docs/assets/viewer_screenshots/readme_example_gif.gif" alt="Syd" width="300" align="right"/>
64
+ </div> -->
65
+
58
66
  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!).
67
+
59
68
  ```python
60
- import matplotlib.pyplot as plt
61
69
  import numpy as np
70
+ import matplotlib.pyplot as plt
62
71
  from syd import make_viewer
63
- def plot(viewer, state):
64
- fig, ax = plt.subplots()
65
- x = np.linspace(0, 10, 1000)
66
- y = state['amplitude'] * np.sin(state['frequency'] * x)
67
- ax.plot(x, y)
72
+ def plot(state):
73
+ # Here's a simple plot function that plots a sine wave
74
+ fig = plt.figure()
75
+ t = np.linspace(0, 2 * np.pi, 1000)
76
+ ax = plt.gca()
77
+ ax.plot(t, state["amplitude"] * np.sin(state["frequency"] * t), color=state["color"])
68
78
  return fig
69
-
70
- viewer = make_viewer()
71
- viewer.set_plot(plot)
72
- viewer.add_float('amplitude', value=1.0, min=0, max=2)
73
- viewer.add_float('frequency', value=1.0, min=0.1, max=5)
74
79
 
75
- # env = "browser" # for viewing in a web browser
80
+ viewer = make_viewer(plot)
81
+ viewer.add_float("amplitude", value=1.0, min=0.1, max=2.0)
82
+ viewer.add_float("frequency", value=1.0, min=0.1, max=5.0)
83
+ viewer.add_selection("color", value="red", options=["red", "blue", "green", "black"])
84
+
85
+ # env = "browser" # for viewing in a web browser (accessible via an IP address)
76
86
  env = "notebook" # for viewing within a jupyter notebook
77
- viewer.deploy(env=env)
87
+ viewer = viewer.deploy(env=env)
78
88
  ```
79
89
 
90
+ ![Quick Start Viewer](./docs/assets/viewer_screenshots/readme_example_gif.gif)
80
91
  ### More Examples
81
92
  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
93
 
@@ -197,8 +208,11 @@ Contributions are welcome! Here's how you can help:
197
208
  6. Push to the branch (`git push origin feature/amazing-feature`)
198
209
  7. Open a Pull Request online
199
210
 
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
-
211
+ 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:
212
+ ```bash
213
+ pip install black
214
+ black . # from the root directory of the repo
215
+ ```
202
216
 
203
217
  ## To-Do List
204
218
  - Layout controls
@@ -208,12 +222,7 @@ Please make sure to update tests as appropriate and adhere to the existing codin
208
222
  - [ ] Add a "freeze" button that allows the user to update state variables without updating the plot until unfreezing
209
223
  - [ ] 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
224
  - [ ] 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
225
  - Export options:
218
226
  - [ ] 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
- - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
227
+ - [ ] Export full: export the viewer in a way that contains the data to give full functionality.
228
+ - [ ] Keep deploy() for backwards compatibility, but deprecate it in favor of show() and share() (for notebook and browser, respectively)
@@ -1,4 +1,4 @@
1
- syd/__init__.py,sha256=LSf7L9CZ7WVB6qb88vX6YgnCffcQLsDdhIggNMEGd10,250
1
+ syd/__init__.py,sha256=Lx1We_TNKsV2Co9v5RB05WgqXpFqieSBh-y3zF0q1LE,250
2
2
  syd/parameters.py,sha256=dlnYOVsi1CDtC2toVECf0kNBRipVrtUjr6XVX86b5MA,42886
3
3
  syd/support.py,sha256=7wztPMaL750opyBDnaYYRVyBR5QUJtVspDzQWpu50rk,6106
4
4
  syd/viewer.py,sha256=UYDfH9TNds0CiC3vThINLrMNZE8ikSFqJHgjM8yMD94,49323
@@ -11,9 +11,9 @@ syd/flask_deployment/static/js/viewer.js,sha256=kSY24VGjlQLe-jtPOEU1nE7U0ALC_ZaV
11
11
  syd/flask_deployment/templates/__init__.py,sha256=ieWE8NKR-APw7h4Ge0ooZGk6wZrneSSs_1cMyTPbQSA,65
12
12
  syd/flask_deployment/templates/index.html,sha256=fr1g9IOwNttULhQCIcw_fo0sNpmdgznSGfPStQllR_E,1594
13
13
  syd/notebook_deployment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- syd/notebook_deployment/deployer.py,sha256=LcKq0Jy_znrVFF7S0uBJrQUWf1hB-NKmDYiq1OG0Jr8,8930
14
+ syd/notebook_deployment/deployer.py,sha256=sf3f9STRXpU1ac-OGENYv1g5yBpUaiIcREBKtKWvIlA,12324
15
15
  syd/notebook_deployment/widgets.py,sha256=UbkasRf8wY9beUwpwJYjv9X0Lus3DvgAEIORHwaC-zA,20058
16
- syd-1.0.0.dist-info/METADATA,sha256=xEpq6sJo2eE6uMZDCwgLwS1yMQd1ZRoz--SWDE4t0F0,13043
17
- syd-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- syd-1.0.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
19
- syd-1.0.0.dist-info/RECORD,,
16
+ syd-1.0.1.dist-info/METADATA,sha256=pHDBBxQoLRmHRBsWPtF5VlYI0hpd_tX6zcmYBNhdIJQ,13668
17
+ syd-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ syd-1.0.1.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
19
+ syd-1.0.1.dist-info/RECORD,,
File without changes