spyre2 0.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.
- spyre2-0.1.0/.claude/settings.json +33 -0
- spyre2-0.1.0/PKG-INFO +226 -0
- spyre2-0.1.0/README.md +187 -0
- spyre2-0.1.0/examples/grid_example.py +72 -0
- spyre2-0.1.0/examples/plotly_example.py +63 -0
- spyre2-0.1.0/examples/sine_example.py +36 -0
- spyre2-0.1.0/examples/site_example.py +22 -0
- spyre2-0.1.0/examples/slider_examples/slider_site.py +19 -0
- spyre2-0.1.0/examples/slider_examples/sliders_altair.py +62 -0
- spyre2-0.1.0/examples/slider_examples/sliders_migrate_from_spyre1.py +54 -0
- spyre2-0.1.0/examples/slider_examples/sliders_plotly.py +50 -0
- spyre2-0.1.0/examples/us_heatmap_example.py +106 -0
- spyre2-0.1.0/examples/us_heatmap_examples/unemployment_altair.py +79 -0
- spyre2-0.1.0/examples/us_heatmap_examples/unemployment_bokeh.py +88 -0
- spyre2-0.1.0/examples/us_heatmap_examples/unemployment_plotly.py +94 -0
- spyre2-0.1.0/examples/us_heatmap_examples/unemployment_site.py +19 -0
- spyre2-0.1.0/pyproject.toml +52 -0
- spyre2-0.1.0/spyre-demo-heatmaps/pyproject.toml +29 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/__init__.py +35 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/app.py +80 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/compat.py +158 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/components/__init__.py +0 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/components/inputs.py +86 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/components/outputs.py +40 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/layout.py +92 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/migrate.py +366 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/__init__.py +0 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/altair_renderer.py +10 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/base.py +47 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/matplotlib_renderer.py +17 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/pandas_renderer.py +12 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/renderers/plotly_renderer.py +10 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/server.py +236 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/static/alpine.min.js +5 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/static/spyre2.css +285 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/static/spyre2.js +165 -0
- spyre2-0.1.0/spyre-demo-heatmaps/spyre2/templates/base.html.jinja2 +155 -0
- spyre2-0.1.0/spyre-demo-sliders/pyproject.toml +29 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/__init__.py +35 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/app.py +80 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/compat.py +158 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/components/__init__.py +0 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/components/inputs.py +86 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/components/outputs.py +40 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/layout.py +92 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/migrate.py +366 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/__init__.py +0 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/altair_renderer.py +10 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/base.py +47 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/matplotlib_renderer.py +17 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/pandas_renderer.py +12 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/renderers/plotly_renderer.py +10 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/server.py +236 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/static/alpine.min.js +5 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/static/spyre2.css +285 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/static/spyre2.js +165 -0
- spyre2-0.1.0/spyre-demo-sliders/spyre2/templates/base.html.jinja2 +155 -0
- spyre2-0.1.0/spyre2/__init__.py +35 -0
- spyre2-0.1.0/spyre2/app.py +80 -0
- spyre2-0.1.0/spyre2/compat.py +158 -0
- spyre2-0.1.0/spyre2/components/__init__.py +0 -0
- spyre2-0.1.0/spyre2/components/inputs.py +86 -0
- spyre2-0.1.0/spyre2/components/outputs.py +40 -0
- spyre2-0.1.0/spyre2/layout.py +92 -0
- spyre2-0.1.0/spyre2/migrate.py +366 -0
- spyre2-0.1.0/spyre2/renderers/__init__.py +0 -0
- spyre2-0.1.0/spyre2/renderers/altair_renderer.py +10 -0
- spyre2-0.1.0/spyre2/renderers/base.py +47 -0
- spyre2-0.1.0/spyre2/renderers/matplotlib_renderer.py +17 -0
- spyre2-0.1.0/spyre2/renderers/pandas_renderer.py +12 -0
- spyre2-0.1.0/spyre2/renderers/plotly_renderer.py +10 -0
- spyre2-0.1.0/spyre2/server.py +236 -0
- spyre2-0.1.0/spyre2/static/alpine.min.js +5 -0
- spyre2-0.1.0/spyre2/static/spyre2.css +285 -0
- spyre2-0.1.0/spyre2/static/spyre2.js +165 -0
- spyre2-0.1.0/spyre2/templates/base.html.jinja2 +155 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read(//Users/adam/dev/spyre/**)",
|
|
5
|
+
"Bash(find /Users/adam/dev/spyre/tests -type f -name *.py)",
|
|
6
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
7
|
+
"WebFetch(domain:github.com)",
|
|
8
|
+
"Bash(curl -sL \"https://cdn.jsdelivr.net/npm/alpinejs@3.13.10/dist/cdn.min.js\" -o /Users/adam/dev/spyre2/spyre2/static/alpine.min.js)",
|
|
9
|
+
"Bash(pip install:*)",
|
|
10
|
+
"Bash(python -c \"import spyre2; print\\(''import OK''\\); a = spyre2.App\\(\\); print\\(''App\\(\\) OK''\\)\")",
|
|
11
|
+
"Bash(python -c \":*)",
|
|
12
|
+
"Bash(MPLBACKEND=Agg python -c \":*)",
|
|
13
|
+
"Bash(.venv/bin/python -c \"import vegafusion\")",
|
|
14
|
+
"Bash(.venv/bin/python -c \":*)",
|
|
15
|
+
"Bash(.venv/bin/python -c \"import altair; print\\(altair.__version__\\)\")",
|
|
16
|
+
"Bash(curl -s \"https://cdn.jsdelivr.net/npm/vega-embed@6/package.json\")",
|
|
17
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''vega-embed:'''', d[''''version'''']\\); print\\(''''peerDeps:'''', d.get\\(''''peerDependencies'''',{}\\)\\)\")",
|
|
18
|
+
"Bash(open /tmp/unemp_test.html)",
|
|
19
|
+
"Read(//private/tmp/**)",
|
|
20
|
+
"Read(//Users/adam/dev/apps/huggingface/spyre-demo/**)",
|
|
21
|
+
"Read(//Users/adam/dev/apps/huggingface/**)",
|
|
22
|
+
"Bash(chmod +x ~/dev/apps/huggingface/sync_spyre2.sh)",
|
|
23
|
+
"Bash(~/dev/apps/huggingface/sync_spyre2.sh)",
|
|
24
|
+
"Bash(pip show:*)",
|
|
25
|
+
"Bash(python -m build)"
|
|
26
|
+
],
|
|
27
|
+
"additionalDirectories": [
|
|
28
|
+
"/Users/adam/dev/apps/huggingface/spyre-demo-heatmaps",
|
|
29
|
+
"/Users/adam/dev/apps/huggingface/spyre-demo-sliders",
|
|
30
|
+
"/Users/adam/dev/apps/huggingface"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
spyre2-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spyre2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build interactive data web apps in pure Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/adamhajari/spyre2
|
|
6
|
+
Project-URL: Repository, https://github.com/adamhajari/spyre2
|
|
7
|
+
Project-URL: Issues, https://github.com/adamhajari/spyre2/issues
|
|
8
|
+
Author-email: Adam Hajari <adamhajari@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: dashboard,data,interactive,visualization,web
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: fastapi>=0.110.0
|
|
23
|
+
Requires-Dist: jinja2>=3.1.0
|
|
24
|
+
Requires-Dist: numpy>=1.23.0
|
|
25
|
+
Requires-Dist: pandas>=1.5.0
|
|
26
|
+
Requires-Dist: pydantic>=2.0.0
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: altair>=5.0.0; extra == 'all'
|
|
30
|
+
Requires-Dist: matplotlib>=3.6.0; extra == 'all'
|
|
31
|
+
Requires-Dist: plotly>=5.0.0; extra == 'all'
|
|
32
|
+
Provides-Extra: altair
|
|
33
|
+
Requires-Dist: altair>=5.0.0; extra == 'altair'
|
|
34
|
+
Provides-Extra: matplotlib
|
|
35
|
+
Requires-Dist: matplotlib>=3.6.0; extra == 'matplotlib'
|
|
36
|
+
Provides-Extra: plotly
|
|
37
|
+
Requires-Dist: plotly>=5.0.0; extra == 'plotly'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# spyre2
|
|
41
|
+
|
|
42
|
+
Build interactive data web apps in pure Python — no HTML, CSS, or JavaScript required.
|
|
43
|
+
|
|
44
|
+
Spyre2 is a ground-up rewrite of [spyre](https://github.com/adamhajari/spyre) using a modern stack: **FastAPI**, **Alpine.js**, and native support for **matplotlib**, **Plotly**, and **Altair**.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install spyre2
|
|
52
|
+
pip install spyre2[matplotlib] # + matplotlib
|
|
53
|
+
pip install spyre2[plotly] # + plotly
|
|
54
|
+
pip install spyre2[all] # everything
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import numpy as np
|
|
63
|
+
import matplotlib.pyplot as plt
|
|
64
|
+
import spyre2
|
|
65
|
+
|
|
66
|
+
class SineApp(spyre2.App):
|
|
67
|
+
title = "Sine Wave"
|
|
68
|
+
|
|
69
|
+
inputs = [
|
|
70
|
+
spyre2.Slider("frequency", label="Frequency", min=1, max=20, default=5),
|
|
71
|
+
spyre2.Dropdown("color", label="Color",
|
|
72
|
+
options=["steelblue", "crimson", "seagreen"]),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
outputs = [
|
|
76
|
+
spyre2.Plot("sine_plot"),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def sine_plot(self, frequency, color):
|
|
80
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
|
81
|
+
x = np.linspace(0, 2 * np.pi, 500)
|
|
82
|
+
ax.plot(x, np.sin(frequency * x), color=color)
|
|
83
|
+
return fig
|
|
84
|
+
|
|
85
|
+
SineApp().launch()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Open `http://127.0.0.1:8000`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Inputs
|
|
93
|
+
|
|
94
|
+
| Class | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `Slider(id, min, max, step, default)` | Numeric range slider |
|
|
97
|
+
| `Dropdown(id, options, default)` | Select dropdown |
|
|
98
|
+
| `RadioButtons(id, options, default)` | Radio button group |
|
|
99
|
+
| `CheckboxGroup(id, options, default)` | Multi-select checkboxes |
|
|
100
|
+
| `TextInput(id, default, placeholder)` | Free-text input |
|
|
101
|
+
| `FileUpload(id, accept)` | File picker |
|
|
102
|
+
|
|
103
|
+
All inputs accept a `label` keyword argument. If omitted, the label is inferred from the `id`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Outputs
|
|
108
|
+
|
|
109
|
+
| Class | Handler return type |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `Plot(id)` | `matplotlib.figure.Figure` or `plotly.Figure` or `altair.Chart` |
|
|
112
|
+
| `Table(id)` | `pandas.DataFrame` |
|
|
113
|
+
| `HTML(id)` | `str` (HTML) |
|
|
114
|
+
| `Download(id)` | `(filename: str, content: str \| bytes)` |
|
|
115
|
+
|
|
116
|
+
The handler method name must match the output `id`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
outputs = [spyre2.Plot("my_plot")]
|
|
120
|
+
|
|
121
|
+
def my_plot(self, **kwargs): # method name = output id
|
|
122
|
+
...
|
|
123
|
+
return fig
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Layouts
|
|
129
|
+
|
|
130
|
+
### Sidebar (default)
|
|
131
|
+
|
|
132
|
+
Controls on the left, outputs stacked on the right. No configuration needed.
|
|
133
|
+
|
|
134
|
+
### Grid
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from spyre2 import Layout
|
|
138
|
+
|
|
139
|
+
class MyApp(spyre2.App):
|
|
140
|
+
layout = Layout.grid([
|
|
141
|
+
["controls", "controls" ],
|
|
142
|
+
["plot_a", "plot_b" ],
|
|
143
|
+
["big_table", "big_table" ],
|
|
144
|
+
])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Repeat an ID across adjacent cells to span columns. `"controls"` is a reserved name for the input panel.
|
|
148
|
+
|
|
149
|
+
### Tabs
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
layout = Layout.tabs({
|
|
153
|
+
"Overview": ["trend_plot"],
|
|
154
|
+
"Detail": ["data_table", "bar_chart"],
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Multiple chart libraries
|
|
161
|
+
|
|
162
|
+
The chart library is detected automatically from the return type — no configuration needed.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# matplotlib
|
|
166
|
+
def my_plot(self, x):
|
|
167
|
+
fig, ax = plt.subplots()
|
|
168
|
+
ax.plot(...)
|
|
169
|
+
return fig # → rendered as SVG
|
|
170
|
+
|
|
171
|
+
# Plotly
|
|
172
|
+
import plotly.express as px
|
|
173
|
+
def my_plot(self, x):
|
|
174
|
+
return px.scatter(df, x="a", y="b") # → rendered via Plotly.js
|
|
175
|
+
|
|
176
|
+
# Altair
|
|
177
|
+
import altair as alt
|
|
178
|
+
def my_plot(self, x):
|
|
179
|
+
return alt.Chart(df).mark_line()... # → rendered via Vega-Embed
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Jupyter notebooks
|
|
185
|
+
|
|
186
|
+
`app.launch()` detects when it's running inside a Jupyter kernel and automatically starts the server in a background thread and displays an inline `IFrame`.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
SineApp().launch(port=8765) # displays inline in the notebook
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Migrating from spyre v1
|
|
195
|
+
|
|
196
|
+
Use the included CLI tool to mechanically convert a v1 app:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
spyre2-migrate myapp.py # preview conversion
|
|
200
|
+
spyre2-migrate myapp.py -o new.py # write to new file
|
|
201
|
+
spyre2-migrate myapp.py --in-place # overwrite (creates .bak backup)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The tool converts imports, `inputs`/`outputs` list-of-dicts, and renames `getPlot`/`getTable` etc., leaving `# TODO` comments where manual cleanup is needed.
|
|
205
|
+
|
|
206
|
+
**What changes:**
|
|
207
|
+
|
|
208
|
+
| spyre v1 | spyre2 |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `from spyre import server` | `import spyre2` |
|
|
211
|
+
| `class App(server.App)` | `class App(spyre2.App)` |
|
|
212
|
+
| `inputs = [{"type": "slider", "key": "x", ...}]` | `inputs = [spyre2.Slider("x", ...)]` |
|
|
213
|
+
| `def getPlot(self, params): x = params["freq"]` | `def my_plot(self, freq):` |
|
|
214
|
+
| CherryPy on port 9093 | uvicorn on port 8000 |
|
|
215
|
+
|
|
216
|
+
For apps that can't be fully migrated yet, `spyre2.compat.App` accepts the old dict-based syntax (deprecated, will be removed in a future release).
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
| File | Demonstrates |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `examples/sine_example.py` | Sidebar layout, matplotlib, slider + dropdown |
|
|
225
|
+
| `examples/grid_example.py` | Grid layout, matplotlib, multiple outputs + table |
|
|
226
|
+
| `examples/plotly_example.py` | Tabs layout, Plotly, scatter + histogram |
|
spyre2-0.1.0/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# spyre2
|
|
2
|
+
|
|
3
|
+
Build interactive data web apps in pure Python — no HTML, CSS, or JavaScript required.
|
|
4
|
+
|
|
5
|
+
Spyre2 is a ground-up rewrite of [spyre](https://github.com/adamhajari/spyre) using a modern stack: **FastAPI**, **Alpine.js**, and native support for **matplotlib**, **Plotly**, and **Altair**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install spyre2
|
|
13
|
+
pip install spyre2[matplotlib] # + matplotlib
|
|
14
|
+
pip install spyre2[plotly] # + plotly
|
|
15
|
+
pip install spyre2[all] # everything
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import numpy as np
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
import spyre2
|
|
26
|
+
|
|
27
|
+
class SineApp(spyre2.App):
|
|
28
|
+
title = "Sine Wave"
|
|
29
|
+
|
|
30
|
+
inputs = [
|
|
31
|
+
spyre2.Slider("frequency", label="Frequency", min=1, max=20, default=5),
|
|
32
|
+
spyre2.Dropdown("color", label="Color",
|
|
33
|
+
options=["steelblue", "crimson", "seagreen"]),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
outputs = [
|
|
37
|
+
spyre2.Plot("sine_plot"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def sine_plot(self, frequency, color):
|
|
41
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
|
42
|
+
x = np.linspace(0, 2 * np.pi, 500)
|
|
43
|
+
ax.plot(x, np.sin(frequency * x), color=color)
|
|
44
|
+
return fig
|
|
45
|
+
|
|
46
|
+
SineApp().launch()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open `http://127.0.0.1:8000`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Inputs
|
|
54
|
+
|
|
55
|
+
| Class | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `Slider(id, min, max, step, default)` | Numeric range slider |
|
|
58
|
+
| `Dropdown(id, options, default)` | Select dropdown |
|
|
59
|
+
| `RadioButtons(id, options, default)` | Radio button group |
|
|
60
|
+
| `CheckboxGroup(id, options, default)` | Multi-select checkboxes |
|
|
61
|
+
| `TextInput(id, default, placeholder)` | Free-text input |
|
|
62
|
+
| `FileUpload(id, accept)` | File picker |
|
|
63
|
+
|
|
64
|
+
All inputs accept a `label` keyword argument. If omitted, the label is inferred from the `id`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Outputs
|
|
69
|
+
|
|
70
|
+
| Class | Handler return type |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `Plot(id)` | `matplotlib.figure.Figure` or `plotly.Figure` or `altair.Chart` |
|
|
73
|
+
| `Table(id)` | `pandas.DataFrame` |
|
|
74
|
+
| `HTML(id)` | `str` (HTML) |
|
|
75
|
+
| `Download(id)` | `(filename: str, content: str \| bytes)` |
|
|
76
|
+
|
|
77
|
+
The handler method name must match the output `id`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
outputs = [spyre2.Plot("my_plot")]
|
|
81
|
+
|
|
82
|
+
def my_plot(self, **kwargs): # method name = output id
|
|
83
|
+
...
|
|
84
|
+
return fig
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Layouts
|
|
90
|
+
|
|
91
|
+
### Sidebar (default)
|
|
92
|
+
|
|
93
|
+
Controls on the left, outputs stacked on the right. No configuration needed.
|
|
94
|
+
|
|
95
|
+
### Grid
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from spyre2 import Layout
|
|
99
|
+
|
|
100
|
+
class MyApp(spyre2.App):
|
|
101
|
+
layout = Layout.grid([
|
|
102
|
+
["controls", "controls" ],
|
|
103
|
+
["plot_a", "plot_b" ],
|
|
104
|
+
["big_table", "big_table" ],
|
|
105
|
+
])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Repeat an ID across adjacent cells to span columns. `"controls"` is a reserved name for the input panel.
|
|
109
|
+
|
|
110
|
+
### Tabs
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
layout = Layout.tabs({
|
|
114
|
+
"Overview": ["trend_plot"],
|
|
115
|
+
"Detail": ["data_table", "bar_chart"],
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Multiple chart libraries
|
|
122
|
+
|
|
123
|
+
The chart library is detected automatically from the return type — no configuration needed.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# matplotlib
|
|
127
|
+
def my_plot(self, x):
|
|
128
|
+
fig, ax = plt.subplots()
|
|
129
|
+
ax.plot(...)
|
|
130
|
+
return fig # → rendered as SVG
|
|
131
|
+
|
|
132
|
+
# Plotly
|
|
133
|
+
import plotly.express as px
|
|
134
|
+
def my_plot(self, x):
|
|
135
|
+
return px.scatter(df, x="a", y="b") # → rendered via Plotly.js
|
|
136
|
+
|
|
137
|
+
# Altair
|
|
138
|
+
import altair as alt
|
|
139
|
+
def my_plot(self, x):
|
|
140
|
+
return alt.Chart(df).mark_line()... # → rendered via Vega-Embed
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Jupyter notebooks
|
|
146
|
+
|
|
147
|
+
`app.launch()` detects when it's running inside a Jupyter kernel and automatically starts the server in a background thread and displays an inline `IFrame`.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
SineApp().launch(port=8765) # displays inline in the notebook
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Migrating from spyre v1
|
|
156
|
+
|
|
157
|
+
Use the included CLI tool to mechanically convert a v1 app:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
spyre2-migrate myapp.py # preview conversion
|
|
161
|
+
spyre2-migrate myapp.py -o new.py # write to new file
|
|
162
|
+
spyre2-migrate myapp.py --in-place # overwrite (creates .bak backup)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The tool converts imports, `inputs`/`outputs` list-of-dicts, and renames `getPlot`/`getTable` etc., leaving `# TODO` comments where manual cleanup is needed.
|
|
166
|
+
|
|
167
|
+
**What changes:**
|
|
168
|
+
|
|
169
|
+
| spyre v1 | spyre2 |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `from spyre import server` | `import spyre2` |
|
|
172
|
+
| `class App(server.App)` | `class App(spyre2.App)` |
|
|
173
|
+
| `inputs = [{"type": "slider", "key": "x", ...}]` | `inputs = [spyre2.Slider("x", ...)]` |
|
|
174
|
+
| `def getPlot(self, params): x = params["freq"]` | `def my_plot(self, freq):` |
|
|
175
|
+
| CherryPy on port 9093 | uvicorn on port 8000 |
|
|
176
|
+
|
|
177
|
+
For apps that can't be fully migrated yet, `spyre2.compat.App` accepts the old dict-based syntax (deprecated, will be removed in a future release).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Examples
|
|
182
|
+
|
|
183
|
+
| File | Demonstrates |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `examples/sine_example.py` | Sidebar layout, matplotlib, slider + dropdown |
|
|
186
|
+
| `examples/grid_example.py` | Grid layout, matplotlib, multiple outputs + table |
|
|
187
|
+
| `examples/plotly_example.py` | Tabs layout, Plotly, scatter + histogram |
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grid layout example: two plots side-by-side + a data table below.
|
|
3
|
+
Demonstrates Layout.grid() and returning a pandas DataFrame from a Table output.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import sys
|
|
9
|
+
sys.path.insert(0, "/Users/adam/dev/spyre2")
|
|
10
|
+
|
|
11
|
+
import spyre2
|
|
12
|
+
from spyre2.layout import Layout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WaveApp(spyre2.App):
|
|
16
|
+
title = "Wave Comparison"
|
|
17
|
+
|
|
18
|
+
layout = Layout.grid([
|
|
19
|
+
["controls", "controls" ],
|
|
20
|
+
["sine_plot", "cos_plot" ],
|
|
21
|
+
["data_table","data_table"],
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
inputs = [
|
|
25
|
+
spyre2.Slider("frequency", label="Frequency", min=1, max=10, default=3),
|
|
26
|
+
spyre2.Slider("points", label="Sample Points", min=10, max=200, default=100),
|
|
27
|
+
spyre2.Dropdown("style", label="Line Style",
|
|
28
|
+
options=["solid", "dashed", "dotted"], default="solid"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
outputs = [
|
|
32
|
+
spyre2.Plot("sine_plot", label="Sine"),
|
|
33
|
+
spyre2.Plot("cos_plot", label="Cosine"),
|
|
34
|
+
spyre2.Table("data_table", label="Sample Data"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def _data(self, frequency, points):
|
|
38
|
+
x = np.linspace(0, 2 * np.pi, int(points))
|
|
39
|
+
return x, np.sin(float(frequency) * x), np.cos(float(frequency) * x)
|
|
40
|
+
|
|
41
|
+
def sine_plot(self, frequency, points, style):
|
|
42
|
+
x, y, _ = self._data(frequency, points)
|
|
43
|
+
fig, ax = plt.subplots(figsize=(5, 3))
|
|
44
|
+
ls = {"solid": "-", "dashed": "--", "dotted": ":"}[style]
|
|
45
|
+
ax.plot(x, y, ls, color="steelblue", linewidth=2)
|
|
46
|
+
ax.set_title(f"sin({frequency}x)")
|
|
47
|
+
ax.grid(True, alpha=0.3)
|
|
48
|
+
fig.tight_layout()
|
|
49
|
+
return fig
|
|
50
|
+
|
|
51
|
+
def cos_plot(self, frequency, points, style):
|
|
52
|
+
x, _, y = self._data(frequency, points)
|
|
53
|
+
fig, ax = plt.subplots(figsize=(5, 3))
|
|
54
|
+
ls = {"solid": "-", "dashed": "--", "dotted": ":"}[style]
|
|
55
|
+
ax.plot(x, y, ls, color="crimson", linewidth=2)
|
|
56
|
+
ax.set_title(f"cos({frequency}x)")
|
|
57
|
+
ax.grid(True, alpha=0.3)
|
|
58
|
+
fig.tight_layout()
|
|
59
|
+
return fig
|
|
60
|
+
|
|
61
|
+
def data_table(self, frequency, points, style):
|
|
62
|
+
x, s, c = self._data(frequency, points)
|
|
63
|
+
step = max(1, int(points) // 10)
|
|
64
|
+
return pd.DataFrame({
|
|
65
|
+
"x": x[::step].round(3),
|
|
66
|
+
"sin": s[::step].round(4),
|
|
67
|
+
"cos": c[::step].round(4),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
WaveApp().launch()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plotly example: interactive scatter + histogram using tabs layout.
|
|
3
|
+
Demonstrates that returning a plotly Figure is handled automatically.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import sys
|
|
8
|
+
sys.path.insert(0, "/Users/adam/dev/spyre2")
|
|
9
|
+
|
|
10
|
+
import spyre2
|
|
11
|
+
from spyre2.layout import Layout
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import plotly.express as px
|
|
15
|
+
except ImportError:
|
|
16
|
+
raise SystemExit("Install plotly first: pip install plotly")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ScatterApp(spyre2.App):
|
|
20
|
+
title = "Plotly Demo"
|
|
21
|
+
|
|
22
|
+
layout = Layout.tabs({
|
|
23
|
+
"Scatter": ["scatter_plot"],
|
|
24
|
+
"Distribution": ["hist_plot", "summary_table"],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
inputs = [
|
|
28
|
+
spyre2.Slider("n_points", label="Points", min=50, max=500, default=200),
|
|
29
|
+
spyre2.Slider("noise", label="Noise", min=0.1, max=3.0, step=0.1, default=1.0),
|
|
30
|
+
spyre2.Dropdown("color_scale", label="Color Scale",
|
|
31
|
+
options=["viridis", "plasma", "blues"], default="viridis"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
outputs = [
|
|
35
|
+
spyre2.Plot("scatter_plot", label="Scatter"),
|
|
36
|
+
spyre2.Plot("hist_plot", label="Distribution"),
|
|
37
|
+
spyre2.Table("summary_table", label="Stats"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def _make_df(self, n_points, noise):
|
|
41
|
+
rng = np.random.default_rng(42)
|
|
42
|
+
n = int(n_points)
|
|
43
|
+
x = rng.uniform(0, 10, n)
|
|
44
|
+
y = 2 * x + rng.normal(0, float(noise), n)
|
|
45
|
+
return pd.DataFrame({"x": x.round(3), "y": y.round(3)})
|
|
46
|
+
|
|
47
|
+
def scatter_plot(self, n_points, noise, color_scale):
|
|
48
|
+
df = self._make_df(n_points, noise)
|
|
49
|
+
return px.scatter(df, x="x", y="y", color="y",
|
|
50
|
+
color_continuous_scale=color_scale,
|
|
51
|
+
title=f"y = 2x + noise (σ={noise})")
|
|
52
|
+
|
|
53
|
+
def hist_plot(self, n_points, noise, color_scale):
|
|
54
|
+
df = self._make_df(n_points, noise)
|
|
55
|
+
return px.histogram(df, x="y", nbins=30, title="Distribution of y")
|
|
56
|
+
|
|
57
|
+
def summary_table(self, n_points, noise, color_scale):
|
|
58
|
+
df = self._make_df(n_points, noise)
|
|
59
|
+
return df.describe().reset_index().rename(columns={"index": "stat"})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
ScatterApp().launch()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import sys
|
|
4
|
+
sys.path.insert(0, "/Users/adam/dev/spyre2")
|
|
5
|
+
|
|
6
|
+
import spyre2
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SineApp(spyre2.App):
|
|
10
|
+
title = "Sine Wave Explorer"
|
|
11
|
+
|
|
12
|
+
inputs = [
|
|
13
|
+
spyre2.Slider("frequency", label="Frequency", min=1, max=20, default=5),
|
|
14
|
+
spyre2.Slider("amplitude", label="Amplitude", min=0.1, max=5.0, step=0.1, default=1.0),
|
|
15
|
+
spyre2.Dropdown("color", label="Line Color",
|
|
16
|
+
options=["steelblue", "crimson", "seagreen"], default="steelblue"),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
outputs = [
|
|
20
|
+
spyre2.Plot("sine_plot"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def sine_plot(self, frequency, amplitude, color):
|
|
24
|
+
fig, ax = plt.subplots(figsize=(8, 4))
|
|
25
|
+
x = np.linspace(0, 2 * np.pi, 500)
|
|
26
|
+
ax.plot(x, amplitude * np.sin(float(frequency) * x), color=color, linewidth=2)
|
|
27
|
+
ax.set_xlabel("x")
|
|
28
|
+
ax.set_ylabel("y")
|
|
29
|
+
ax.set_title(f"y = {amplitude} · sin({frequency}x)")
|
|
30
|
+
ax.grid(True, alpha=0.3)
|
|
31
|
+
fig.tight_layout()
|
|
32
|
+
return fig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
SineApp().launch()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-app site example: serves all spyre2 examples under one server.
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
sys.path.insert(0, "/Users/adam/dev/spyre2")
|
|
6
|
+
|
|
7
|
+
from sine_example import SineApp
|
|
8
|
+
from grid_example import WaveApp
|
|
9
|
+
from plotly_example import ScatterApp
|
|
10
|
+
from us_heatmap_example import USHeatmap
|
|
11
|
+
|
|
12
|
+
import spyre2
|
|
13
|
+
|
|
14
|
+
site = spyre2.Site(
|
|
15
|
+
(SineApp, "/sine", "Sine Wave"),
|
|
16
|
+
(WaveApp, "/waves", "Wave Comparison"),
|
|
17
|
+
(ScatterApp, "/scatter","Scatter / Distribution"),
|
|
18
|
+
(USHeatmap, "/map", "US Heatmap"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
site.launch()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-app site example: serves all spyre2 examples under one server.
|
|
3
|
+
"""
|
|
4
|
+
# import sys
|
|
5
|
+
# sys.path.insert(0, "/Users/adam/dev/spyre2")
|
|
6
|
+
|
|
7
|
+
from sliders_altair import SlidersApp as SlidersAppAltair
|
|
8
|
+
from sliders_migrate_from_spyre1 import SlidersApp as SlidersAppMigrated
|
|
9
|
+
from sliders_plotly import SlidersApp as SlidersAppPlotly
|
|
10
|
+
import spyre2
|
|
11
|
+
|
|
12
|
+
site = spyre2.Site(
|
|
13
|
+
(SlidersAppAltair, "/altair", "Example Using Altair"),
|
|
14
|
+
(SlidersAppPlotly, "/plotly", "Example Using Plotly"),
|
|
15
|
+
(SlidersAppMigrated, "/migrated_from_spyre_1","Example Migrated from Spyre1"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
site.launch()
|