junoview 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- junoview-0.1.0.dist-info/METADATA +597 -0
- junoview-0.1.0.dist-info/RECORD +9 -0
- junoview-0.1.0.dist-info/WHEEL +5 -0
- junoview-0.1.0.dist-info/entry_points.txt +2 -0
- junoview-0.1.0.dist-info/licenses/LICENSE +202 -0
- junoview-0.1.0.dist-info/licenses/NOTICE +4 -0
- junoview-0.1.0.dist-info/top_level.txt +2 -0
- semantic_render.py +12650 -0
- semantic_widget.py +615 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: junoview
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Junoview — figure-first Jupyter notebook viewing and slide decks. Streamline presentations from Jupyter: display your plots and documentation, single file, stdlib only
|
|
5
|
+
Author: Alex Borowiak
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexborowiak/semantic-rendering
|
|
8
|
+
Project-URL: Repository, https://github.com/alexborowiak/semantic-rendering
|
|
9
|
+
Keywords: jupyter,notebook,visualization,presentation,report
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Framework :: Jupyter
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: NOTICE
|
|
18
|
+
Provides-Extra: widget
|
|
19
|
+
Requires-Dist: anywidget; extra == "widget"
|
|
20
|
+
Requires-Dist: ipywidgets; extra == "widget"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Junoview
|
|
24
|
+
|
|
25
|
+
**Figure-first viewing of executed Jupyter notebooks — and presentations built from the same cells.**
|
|
26
|
+
|
|
27
|
+
> NASA's **Juno** probe viewed Jupiter; **Junoview** views your **Jupyter**.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install junoview # then: junoview
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**▶ [Live demo — no install](https://plotline-nb.dev/example_climate_analysis.html)** · a real climate-diagnostics notebook, rendered.
|
|
34
|
+
<!-- TODO: update the link to the junoview domain once it's live. -->
|
|
35
|
+
|
|
36
|
+
<!-- TODO: drop a 30-second screen recording here — it drives more adoption
|
|
37
|
+
than any prose. Record with the browser, save as docs/demo.gif, then:
|
|
38
|
+
 -->
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
Turn an **executed** Jupyter notebook into a figure-first, nonlinear analysis
|
|
43
|
+
environment. Instead of rendering every cell with equal weight (the Quarto /
|
|
44
|
+
nbconvert model), it recovers the scientific structure underneath the notebook —
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Dataset → Transform → Diagnostic → Figure → Interpretation
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
— and renders figures as the primary objects, with code collapsed behind them,
|
|
51
|
+
sections in a navigable rail, and a live **provenance graph** of how each
|
|
52
|
+
diagnostic derives from the data.
|
|
53
|
+
|
|
54
|
+
There is no kernel and no re-execution: run your notebook once, normally, in
|
|
55
|
+
Jupyter; the renderer reads the outputs already stored in the `.ipynb`. Only
|
|
56
|
+
dependency is the Python standard library.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Run it — the app (recommended)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python semantic_render.py
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
launches a small local server and opens the **semantic notebook app** in your
|
|
67
|
+
browser. The layout is IDE-style: a **controls bar** on top (**+ Open**
|
|
68
|
+
and the global Hide/Show filters, left-aligned), the **notebook tabs**
|
|
69
|
+
beneath it, and a vertical **presentations rail** down the left edge.
|
|
70
|
+
|
|
71
|
+
- **+ Open** (controls bar, top left) browses your file system; or just
|
|
72
|
+
**drag-and-drop `.ipynb` files** anywhere onto the window — or paste a
|
|
73
|
+
**URL** into the open dialog (GitHub `blob` links are converted to raw
|
|
74
|
+
automatically). URL notebooks reload with ↻ and come back on restart.
|
|
75
|
+
- Every notebook opens as a **tab**. Click to switch, **↻** re-reads a
|
|
76
|
+
notebook from disk after you re-run it in Jupyter, **✕** closes it.
|
|
77
|
+
- The **presentations rail** (left edge) stacks your presentations
|
|
78
|
+
vertically under a **Documents** button. Exactly one item is active at a
|
|
79
|
+
time: click a ▶ presentation to open it in the builder, click
|
|
80
|
+
**Documents** to go back — that button is always visible, so there is
|
|
81
|
+
always an obvious way out (`Esc` and the builder's **✕ Close** work
|
|
82
|
+
too). **New** starts a presentation; unsaved drafts carry an amber dot
|
|
83
|
+
and stay listed while you work on others. **«** collapses the rail to
|
|
84
|
+
icons, and again to hide it completely — a small **»** handle at the
|
|
85
|
+
bottom-left brings it back. While the builder is docked, notebook tabs
|
|
86
|
+
keep working — switch tabs to pull cards from different notebooks into
|
|
87
|
+
the same deck.
|
|
88
|
+
- Open tabs and recent files are remembered in `junoview_project.json` next to
|
|
89
|
+
where you launched the app — restart later and your workspace comes back.
|
|
90
|
+
- Presentations can **mix cards from every open tab** (see below) and save
|
|
91
|
+
into the same project file.
|
|
92
|
+
|
|
93
|
+
Options: preload tabs with `python semantic_render.py --app A.ipynb B.ipynb`;
|
|
94
|
+
choose the project folder with `--root`, the port with `--port`, and skip the
|
|
95
|
+
auto-opened browser with `--no-browser`. The server binds to `127.0.0.1` only
|
|
96
|
+
and the URL carries a session token.
|
|
97
|
+
|
|
98
|
+
## Run it — static export
|
|
99
|
+
|
|
100
|
+
For a shareable, self-contained page (no server needed to view it):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# one notebook -> my_notebook.html next to it
|
|
104
|
+
python semantic_render.py my_notebook.ipynb
|
|
105
|
+
|
|
106
|
+
# choose output / title
|
|
107
|
+
python semantic_render.py my_notebook.ipynb -o report.html --title "Run 42"
|
|
108
|
+
|
|
109
|
+
# several notebooks -> ONE page with tabs (default: semantic_view.html)
|
|
110
|
+
python semantic_render.py part1.ipynb part2.ipynb -o project.html
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Open the example to see it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python semantic_render.py example_climate_analysis.ipynb
|
|
117
|
+
open example_climate_analysis.html
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
To rebuild the example notebook from scratch (needs `nbformat`, `nbclient`,
|
|
121
|
+
`xarray`, `matplotlib`):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python make_example_notebook.py
|
|
125
|
+
jupyter execute example_climate_analysis.ipynb # or run it in Jupyter
|
|
126
|
+
python semantic_render.py example_climate_analysis.ipynb
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Publish it — the hosted web version
|
|
132
|
+
|
|
133
|
+
The tool ships as a **fully client-side web app**: the same single Python
|
|
134
|
+
file runs *in the visitor's browser* via [Pyodide](https://pyodide.org)
|
|
135
|
+
(Python compiled to WebAssembly). There is no backend at all, which means
|
|
136
|
+
free static hosting, nothing to maintain, and — the important part for
|
|
137
|
+
science — **notebooks are never uploaded anywhere**; files people open
|
|
138
|
+
stay on their machine.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python semantic_render.py --build-web docs
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
writes `docs/index.html` + `docs/semantic_render.py`. To publish on
|
|
145
|
+
GitHub Pages:
|
|
146
|
+
|
|
147
|
+
1. Commit the `docs/` folder and push.
|
|
148
|
+
2. On GitHub: *Settings → Pages → Source: Deploy from a branch →*
|
|
149
|
+
`main` */docs*.
|
|
150
|
+
3. Your tool is live at `https://<you>.github.io/<repo>/`.
|
|
151
|
+
|
|
152
|
+
(Any static host works — Netlify, Cloudflare Pages, a plain web server.)
|
|
153
|
+
The web version supports drag-and-drop, a file picker, and opening
|
|
154
|
+
notebooks by URL; presentations autosave as browser drafts and can be
|
|
155
|
+
downloaded as JSON or saved into a notebook via the file picker. The
|
|
156
|
+
build bundles the example notebook, so first-time visitors get a
|
|
157
|
+
**Try the example notebook** button, and every mode has a **Help**
|
|
158
|
+
overlay covering the directives and everything the tool can do. The
|
|
159
|
+
first visit downloads the Python runtime (a few MB, then cached).
|
|
160
|
+
|
|
161
|
+
**Do not deploy the local app server (`--app`) to a public machine** —
|
|
162
|
+
it is deliberately single-user: it binds to `127.0.0.1` and browses the
|
|
163
|
+
host's filesystem. The web build above is the safe public face; the app
|
|
164
|
+
server is for your own machine.
|
|
165
|
+
|
|
166
|
+
### Install as a command
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install junoview # or: pipx install junoview
|
|
170
|
+
junoview # launches the app from anywhere
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or from a checkout: `pip install .` (add `pipx` to isolate it). The Jupyter
|
|
174
|
+
widget extras come with `pip install "junoview[widget]"`.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## How to format a notebook for it
|
|
179
|
+
|
|
180
|
+
You annotate cells with `#| key: value` **directive lines at the very top of a
|
|
181
|
+
code cell**. They are parsed and then stripped from the displayed source.
|
|
182
|
+
Everything is optional — with no directives at all the renderer still infers a
|
|
183
|
+
sensible layout from each cell's outputs.
|
|
184
|
+
|
|
185
|
+
### Directives
|
|
186
|
+
|
|
187
|
+
| Directive | What it does |
|
|
188
|
+
|-----------------|----------------------------------------------------------------------|
|
|
189
|
+
| `#| section:` | Start a top-level section (also doable with a Markdown `##` heading). |
|
|
190
|
+
| `#| subsection:`| Nested group inside the current section. |
|
|
191
|
+
| `#| title:` | Human title for the card (otherwise inferred from the code). |
|
|
192
|
+
| `#| display:` | Card type: `figure` `dataset` `transform` `diagnostic` `metric` `text` `code` `hidden`. |
|
|
193
|
+
| `#| code:` | Default code visibility: `hidden` (default) or `show`. |
|
|
194
|
+
| `#| id:` | Stable slug for this cell — makes it a node in the provenance graph. |
|
|
195
|
+
| `#| depends:` | Comma-separated `id`s this cell derives from — draws the graph edges.|
|
|
196
|
+
| `#| caption:` | Interpretation text / what to look for, shown under the output. |
|
|
197
|
+
| `#| group:` | Merge several cells into **one** card (alias: `tag:`). |
|
|
198
|
+
| `#| order:` | Sort this cell within its group (integer; defaults to appearance). |
|
|
199
|
+
| `#| step:` | Label this cell's chunk in the folded code. |
|
|
200
|
+
| `#| stack:` | Fold the code of cells with these `id`s under this card; reusable. |
|
|
201
|
+
|
|
202
|
+
### A figure cell
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
#| display: figure
|
|
206
|
+
#| id: block_comp
|
|
207
|
+
#| depends: anom, block_freq
|
|
208
|
+
#| title: Composite Z500 anomaly on blocked days
|
|
209
|
+
#| caption: The localised positive centre is the blocking high.
|
|
210
|
+
comp = z_anom.sel(time=blocked).mean('time')
|
|
211
|
+
comp.plot(cmap='RdBu_r')
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This renders as a figure card titled "Composite Z500 anomaly on blocked days",
|
|
215
|
+
with the caption beneath it, the code tucked behind a **Show code** toggle, a
|
|
216
|
+
`derives from anom · block_freq` provenance line, and a node in the rail graph
|
|
217
|
+
wired to the `anom` and `block_freq` nodes.
|
|
218
|
+
|
|
219
|
+
### Grouping several cells under one figure
|
|
220
|
+
|
|
221
|
+
A figure is usually the last step of a small pipeline — regrid, composite,
|
|
222
|
+
plot. Give those cells the same `#| group:` name and they collapse into a
|
|
223
|
+
single card: the cell that draws the figure is the face, and the prep folds
|
|
224
|
+
behind one **Show code** toggle as numbered steps.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
#| group: fig_zonal
|
|
228
|
+
#| order: 1
|
|
229
|
+
#| step: zonal mean + 30-day smoothing
|
|
230
|
+
zm = z_anom.mean('lon')
|
|
231
|
+
zm_mon = zm.rolling(time=30, center=True).mean().resample(time='1MS').mean()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
#| group: fig_zonal
|
|
236
|
+
#| order: 2
|
|
237
|
+
#| step: plot Hovmöller
|
|
238
|
+
#| display: figure
|
|
239
|
+
#| id: zonal_hov
|
|
240
|
+
#| depends: anom
|
|
241
|
+
#| title: Zonal-mean Z500 anomaly (time–latitude)
|
|
242
|
+
zm_mon.plot(x='time', cmap='RdBu_r')
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Both cells become the one **zonal_hov** card. Notes on the merge:
|
|
246
|
+
|
|
247
|
+
- **Face** = the cell with `display: figure` (or, absent that, the last cell
|
|
248
|
+
that produces an image / any output). Its output is shown; the others' code
|
|
249
|
+
folds underneath, and any *intermediate* output (a printed shape, a repr)
|
|
250
|
+
is tucked under its own step.
|
|
251
|
+
- **Title / caption / id** come from the group — the first member that sets
|
|
252
|
+
each wins, preferring the figure cell.
|
|
253
|
+
- **`depends`** is the union across all members, so the prep cell's inputs and
|
|
254
|
+
the plot cell's inputs both feed the one node. The group is therefore a
|
|
255
|
+
**single** vertex in the provenance graph — grouping declutters the graph as
|
|
256
|
+
well as the page.
|
|
257
|
+
- **Section / subsection** is taken from where the group's first cell sits (a
|
|
258
|
+
`##` / `###` heading above it, or a `subsection:` on that cell).
|
|
259
|
+
|
|
260
|
+
`step:` is the clean way to label a chunk; `subsection:` on a grouped member
|
|
261
|
+
is also accepted as a chunk label, to match the obvious shorthand.
|
|
262
|
+
|
|
263
|
+
### Stacking shared cells under a figure (reuse)
|
|
264
|
+
|
|
265
|
+
Grouping is *push* — each cell tags itself into one group, so a cell can only
|
|
266
|
+
live under a single figure. When the same prep feeds **several** figures
|
|
267
|
+
(opening the data, regridding, a shared plotting helper), use `#| stack:`
|
|
268
|
+
instead. A figure names the upstream cells by `id`, and they fold in front of
|
|
269
|
+
its own code:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
#| id: maphelper # define the shared cell once
|
|
273
|
+
#| step: shared map helper
|
|
274
|
+
def plot_anom_map(da, ax, title, vmax=None):
|
|
275
|
+
...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
#| display: figure
|
|
280
|
+
#| id: block_comp
|
|
281
|
+
#| depends: anom, block_freq
|
|
282
|
+
#| stack: maphelper # ← fold the helper under this figure
|
|
283
|
+
comp = z_anom.sel(time=blocked).mean('time')
|
|
284
|
+
pc = plot_anom_map(comp, ax, 'Blocked-day composite')
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
#| display: figure
|
|
289
|
+
#| id: enso_comp
|
|
290
|
+
#| depends: anom, nino34
|
|
291
|
+
#| stack: maphelper # ← and under this one too (same cell)
|
|
292
|
+
pc = plot_anom_map(tele, ax, 'Warm-phase composite')
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`maphelper` now folds in as step 1 of **both** composite cards. Key points:
|
|
296
|
+
|
|
297
|
+
- A cell named in any `stack:` list is **consumed**: it gets no card of its
|
|
298
|
+
own and no graph node — it lives only under the figures that stack it. The
|
|
299
|
+
same id may be stacked under any number of figures.
|
|
300
|
+
- Stacked cells render **before** the card's own code, in the order listed;
|
|
301
|
+
the figure's own code is the final step. Use `#| order:`-style intent by
|
|
302
|
+
ordering the ids in the list.
|
|
303
|
+
- Stacking folds **code only**; it does *not* add provenance edges. Use
|
|
304
|
+
`depends:` for lineage you want drawn in the graph.
|
|
305
|
+
|
|
306
|
+
#### group vs stack — which to use
|
|
307
|
+
|
|
308
|
+
| | `group:` (push) | `stack:` (pull) |
|
|
309
|
+
|---|---|---|
|
|
310
|
+
| Who references whom | each cell tags itself | the figure names cells by `id` |
|
|
311
|
+
| Cell can belong to | one card | any number of cards |
|
|
312
|
+
| Best for | a few adjacent cells authored as a unit | shared prep reused across figures |
|
|
313
|
+
| Standalone card | merged away | consumed (no card, no node) |
|
|
314
|
+
|
|
315
|
+
A useful split to remember: **`depends:` keeps a cell as its own node in the
|
|
316
|
+
graph; `stack:` folds its code into a figure and collapses it.** One is about
|
|
317
|
+
scientific lineage, the other about reproducibility of a single figure.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
A Markdown cell whose first line is a heading opens structure:
|
|
322
|
+
|
|
323
|
+
```markdown
|
|
324
|
+
## Blocking diagnostics ← H2 opens a section
|
|
325
|
+
### Regional composite ← H3 opens a subsection
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Any prose under a heading (or any plain Markdown cell) becomes an
|
|
329
|
+
**interpretation note** — rendered in a serif face to set human commentary
|
|
330
|
+
apart from machine output.
|
|
331
|
+
|
|
332
|
+
You can mix styles: use `##` headings for some sections and `#| section:` on a
|
|
333
|
+
code cell for others. The example notebook does both.
|
|
334
|
+
|
|
335
|
+
### What happens with no directives
|
|
336
|
+
|
|
337
|
+
| Cell produces… | Inferred card |
|
|
338
|
+
|---------------------------|---------------|
|
|
339
|
+
| an image | `figure` |
|
|
340
|
+
| an xarray HTML repr | `dataset` |
|
|
341
|
+
| only short text / stdout | `metric` |
|
|
342
|
+
| longer text | `text` |
|
|
343
|
+
| no output | `code` (collapsed) |
|
|
344
|
+
|
|
345
|
+
So an un-annotated notebook still renders cleanly; directives are how you take
|
|
346
|
+
control — naming diagnostics, writing captions, and declaring the provenance
|
|
347
|
+
graph with `id` / `depends`.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## What you get in the page
|
|
352
|
+
|
|
353
|
+
- **Left rail** — section tree plus a live analysis graph. Scrolling highlights
|
|
354
|
+
the active section and its node; clicking a node jumps to that card.
|
|
355
|
+
- **Figure stage** — each diagnostic as a card: title, output, serif caption,
|
|
356
|
+
amber `derives from …` provenance chips (click to jump to a source), and a
|
|
357
|
+
collapsible code block.
|
|
358
|
+
- **Controls bar** (top row, global — it applies to every tab) — three
|
|
359
|
+
buttons whose labels follow the state: *Hide/Show figures*, *Hide/Show
|
|
360
|
+
markup* (the markdown/equation cells) and *Hide/Show code*. **Show
|
|
361
|
+
code shows ALL code**: it reveals the code-only cards *and* unfolds the
|
|
362
|
+
code tucked under every figure and dataset card in one click. Any
|
|
363
|
+
combination works — hide code for a figures-plus-documentation reading
|
|
364
|
+
view, leave only markup for just the prose. A hidden card collapses to
|
|
365
|
+
a slim dashed stub that expands in place when clicked.
|
|
366
|
+
- **Raw notebook** (controls bar) — flips the active tab to the notebook
|
|
367
|
+
exactly as authored: every cell in order, `#|` directives visible,
|
|
368
|
+
outputs underneath. This is the transparency view — it shows precisely
|
|
369
|
+
where each card's title, caption and section came from. Click again
|
|
370
|
+
(or any nav link) to return to the formatted view.
|
|
371
|
+
- **Notebook tabs** (beneath the controls) — one per open notebook.
|
|
372
|
+
- **Presentations rail** (left edge, vertical) — a **Documents** button on
|
|
373
|
+
top, then your presentations; the active item is highlighted.
|
|
374
|
+
- Responsive to mobile, keyboard-navigable, respects reduced-motion.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Presentation deck
|
|
379
|
+
|
|
380
|
+
Click a **▶ presentation** in the left rail to open that deck in the
|
|
381
|
+
**builder** (below); the **▶ Present** button at the top of the
|
|
382
|
+
panel plays the deck full-screen — arrow buttons / arrow keys to move,
|
|
383
|
+
`Esc` or **✕ Exit** to drop back to the builder, **Docs** to
|
|
384
|
+
leave altogether. The rail's **Documents** button returns to the
|
|
385
|
+
documents from anywhere. With nothing saved yet you get *auto: figures*
|
|
386
|
+
— one full-screen slide per figure, in document order.
|
|
387
|
+
|
|
388
|
+
**The deck has two axes.** Horizontal (←/→) is your story. Vertical is
|
|
389
|
+
the **code trail**: on any slide whose frames carry code, a ↓ arrow
|
|
390
|
+
(and *"↓ how it was made"* hint) appears — press it (or `ArrowDown`)
|
|
391
|
+
to descend through every cell that produced what's on screen, one cell
|
|
392
|
+
per step, in execution order: *open data → transforms → the plot
|
|
393
|
+
itself*. ↑ climbs back; `Esc` returns to the slide; ←/→ leave the
|
|
394
|
+
trail and continue the story. The counter shows where you are
|
|
395
|
+
(`3 / 8 · code 2 / 5`).
|
|
396
|
+
|
|
397
|
+
The trail is the union of your declared `depends:` edges and
|
|
398
|
+
**automatic variable tracing**: the renderer parses each cell's code
|
|
399
|
+
and links a figure to whichever cells last assigned the variables it
|
|
400
|
+
reads, so even un-annotated notebooks get the full lineage. (Static
|
|
401
|
+
best-effort: it can't see mutation without assignment, e.g.
|
|
402
|
+
`ds.load()`; declare `depends:` where the trace misses something.)
|
|
403
|
+
|
|
404
|
+
### Create mode
|
|
405
|
+
|
|
406
|
+
The builder docks full-height beside the presentations rail with the
|
|
407
|
+
real document view next to it — the tab row and filters shift right to
|
|
408
|
+
sit above the document. **▶ Present** at the top plays the deck;
|
|
409
|
+
**Documents** in the rail (or `Esc`, or **✕ Close**) exits back to the
|
|
410
|
+
documents. You build slides by pointing at the document:
|
|
411
|
+
|
|
412
|
+
- **+ Add slide**, then pick a layout from the diagram buttons:
|
|
413
|
+
**full**, **halves** (side by side), **rows** (stacked), **quarters**,
|
|
414
|
+
a **title slide**, or a **blank canvas** (dashed icon) that you compose
|
|
415
|
+
entirely in ✎ Edit with text, arrows, boxes and notebook cells.
|
|
416
|
+
Title/subtitle can be typed in the panel or — in ✎ Edit — right on the
|
|
417
|
+
slide, where they are movable text objects like everything else.
|
|
418
|
+
- Click a pane in the layout diagram, then **click a card in the
|
|
419
|
+
document** to place it there; the next empty pane is selected
|
|
420
|
+
automatically, and ✕ on a pane clears it. Figure panes show a faint
|
|
421
|
+
live preview of their image.
|
|
422
|
+
- The filmstrip shows PowerPoint-style thumbnails of every slide with the
|
|
423
|
+
actual content — scaled-down figures, text stripes for markup — click
|
|
424
|
+
to select, ↑ ↓ to reorder, ✕ to delete.
|
|
425
|
+
- **✎ Edit slide** opens the slide in the document area (the builder
|
|
426
|
+
stays on the left, tabs above) with drawing tools — **+ Text** (click
|
|
427
|
+
to place a text box, type straight into it), **+ Arrow** and **+ Box**
|
|
428
|
+
(drag to draw), **+ Cell** (below), **Select** to move things (text
|
|
429
|
+
moves by its ⠿ handle) and **Delete** / `Del`. Selecting any item
|
|
430
|
+
reveals a **format bar**: six colours, text size **A− / A+**, line
|
|
431
|
+
thickness, **Dash**, **Fill** for boxes, **Bg** to strip a text box's
|
|
432
|
+
background. Everything is stored with the slide in percent
|
|
433
|
+
coordinates, so it scales with the screen and shows in playback.
|
|
434
|
+
**Done** or `Esc` returns to the builder.
|
|
435
|
+
- **+ Cell** places a draggable, **resizable** frame that says *"Click
|
|
436
|
+
to add from notebook"* — clicking it flips you back to the notebook
|
|
437
|
+
view with a picker banner; click any card and you're returned to the
|
|
438
|
+
editor with it placed in the frame. Hovering or selecting a filled
|
|
439
|
+
frame shows **⇄ Replace** right on the frame (also in the format bar)
|
|
440
|
+
to swap in a different card, from any open notebook.
|
|
441
|
+
- Everything else lives in the **File ▾** menu: *New presentation*,
|
|
442
|
+
*Rename*, the two auto-builders (*figures* / *figures + docs*, in
|
|
443
|
+
document order), *Save to notebook*, *Download JSON* and *Discard
|
|
444
|
+
changes*.
|
|
445
|
+
|
|
446
|
+
Markdown cards render with bullets/bold and **LaTeX equations**
|
|
447
|
+
(`$...$`, `$$...$$`, typeset by MathJax — needs internet on first view),
|
|
448
|
+
so "figure with its equations beside it" is a *Halves* slide: the figure
|
|
449
|
+
in one pane, the markdown card in the other.
|
|
450
|
+
|
|
451
|
+
Edits autosave as a **draft** in the browser (`localStorage`), per
|
|
452
|
+
presentation — refresh and nothing is lost; the status pill shows
|
|
453
|
+
*auto / saved / unsaved draft*, and *Discard* reverts to the saved copy.
|
|
454
|
+
|
|
455
|
+
### Presentations across notebooks
|
|
456
|
+
|
|
457
|
+
Projects usually span several notebooks, so the deck works across **all
|
|
458
|
+
open tabs**: the tab strip stays visible in Create mode — switch tabs
|
|
459
|
+
while building and click cards from any notebook; a *Halves* slide can
|
|
460
|
+
show a figure from `part1` next to a figure from `part2`. When more than
|
|
461
|
+
one notebook is open, panes and slides carry a small chip naming the
|
|
462
|
+
source notebook, and the auto-builders walk every tab in order.
|
|
463
|
+
|
|
464
|
+
Internally a pane in a multi-notebook deck is stored as
|
|
465
|
+
`<notebook>::<anchor>`; single-notebook decks keep plain anchors, so
|
|
466
|
+
everything stays compatible with the classic sidecar / embed flow below.
|
|
467
|
+
If a slide references a notebook that isn't open, the pane says so and
|
|
468
|
+
comes back when you reopen the tab.
|
|
469
|
+
|
|
470
|
+
### Named presentations, saved where you work
|
|
471
|
+
|
|
472
|
+
Multiple **named presentations** — each one is a ▶ item in the left
|
|
473
|
+
rail; **New** starts one, *File → Rename* (or clicking the name in the
|
|
474
|
+
builder) renames it. Saving routes:
|
|
475
|
+
|
|
476
|
+
1. **App mode: autosave to project** (default on) — every change is
|
|
477
|
+
written to `junoview_project.json` in the app's root folder about a
|
|
478
|
+
second after you make it, alongside your open-tab session. Toggle it
|
|
479
|
+
with *File → Autosave*; with it off, *File → Save to project* saves
|
|
480
|
+
manually. *File → Delete presentation* removes one.
|
|
481
|
+
2. **Download / Load deck JSON** (everywhere, including the web
|
|
482
|
+
version) — *Download JSON* saves the deck as a file on your machine;
|
|
483
|
+
*Load deck JSON…* imports it back, later or on another computer. In
|
|
484
|
+
the web version, edits also autosave as browser drafts, so a normal
|
|
485
|
+
reload never loses work. A sidecar `<notebook>.deck.json` placed
|
|
486
|
+
beside a local `.ipynb` auto-loads; bake one in with
|
|
487
|
+
`python semantic_render.py nb.ipynb --embed-deck nb.deck.json`, or
|
|
488
|
+
render with a project file via `--deck project.deck.json`.
|
|
489
|
+
(Direct save-into-.ipynb from the browser exists in the code but is
|
|
490
|
+
currently disabled.)
|
|
491
|
+
|
|
492
|
+
Slides reference cards by a **stable anchor** — the cell's `#| id:` if it
|
|
493
|
+
has one, else the notebook's built-in cell id (nbformat ≥ 4.5) — never by
|
|
494
|
+
position. Reordering, editing or adding cells does not break the deck;
|
|
495
|
+
deleting a referenced cell just skips that slide with a note. Prefer
|
|
496
|
+
`#| id:` anchors: they also survive copy-pasting cells between notebooks.
|
|
497
|
+
|
|
498
|
+
`--deck other.json` renders with a specific deck file (overrides the
|
|
499
|
+
sidecar and the embedded metadata).
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## Design notes / extending
|
|
504
|
+
|
|
505
|
+
- The renderer is one file (`semantic_render.py`) with the directive parser,
|
|
506
|
+
a tokenizer-based Python highlighter, output renderers, the semantic model,
|
|
507
|
+
the graph-layout, and the inlined HTML/CSS/JS. `python semantic_render.py
|
|
508
|
+
--self-test` runs a built-in sanity check.
|
|
509
|
+
- Because the static page is precomputed, its interactivity is limited to what
|
|
510
|
+
can be baked in (navigation, provenance highlighting, code toggles). For a
|
|
511
|
+
*live* kernel — recompute and persistent arrangement — use the widget below.
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Live in Jupyter (`semantic_widget.py`)
|
|
516
|
+
|
|
517
|
+
The static page is for sharing — one self-contained file, no Jupyter needed.
|
|
518
|
+
When you want to *explore*, `SemanticNotebook` renders the same figure-first
|
|
519
|
+
view **inside a notebook**, against the live kernel. It imports the same parser
|
|
520
|
+
and the same card HTML from `semantic_render`, so the directive format and the
|
|
521
|
+
look are identical; it just adds the two things a dead file can't do.
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from semantic_widget import SemanticNotebook
|
|
525
|
+
|
|
526
|
+
view = SemanticNotebook.from_ipynb("analysis.ipynb") # an executed notebook
|
|
527
|
+
view
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**1 · View-state that persists back to Python.** Hide a card (hover → ✕), switch
|
|
531
|
+
the layout (Column / Grid / Compact), or collapse the graph, and the choices are
|
|
532
|
+
synced to the kernel:
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
view.view_state # -> {'hidden': ['enso_spec'], 'layout': 'grid', ...}
|
|
536
|
+
view.export_html("clean.html") # static page with hidden cards dropped
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`export_html` is the bridge back to the shareable artifact: arrange it live,
|
|
540
|
+
then export a clean page.
|
|
541
|
+
|
|
542
|
+
**2 · Live recompute.** Attach a function to a figure `id`; its parameters
|
|
543
|
+
become controls, and changing them re-runs the function on the kernel and swaps
|
|
544
|
+
that figure in place. Every other figure stays static.
|
|
545
|
+
|
|
546
|
+
```python
|
|
547
|
+
@view.recompute("block_comp",
|
|
548
|
+
threshold=(0.5, 2.5, 0.1), # slider
|
|
549
|
+
region=["Tasman", "Ross", "Weddell"]) # dropdown
|
|
550
|
+
def _(threshold, region):
|
|
551
|
+
box = z_anom.sel(**REGIONS[region]).mean(["lat", "lon"])
|
|
552
|
+
comp = z_anom.sel(time=box > box.std() * threshold).mean("time")
|
|
553
|
+
fig, ax = plt.subplots()
|
|
554
|
+
plot_anom_map(comp, ax, f"{region} composite")
|
|
555
|
+
return fig # return a matplotlib Figure (or just draw one)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Parameter shorthands:
|
|
559
|
+
|
|
560
|
+
| you write | control |
|
|
561
|
+
|-----------|---------|
|
|
562
|
+
| `(lo, hi)` or `(lo, hi, step)` | slider |
|
|
563
|
+
| `["a", "b", "c"]` | dropdown |
|
|
564
|
+
| `5` / `1.5` | number box |
|
|
565
|
+
| `True` | checkbox |
|
|
566
|
+
| `"text"` | text box |
|
|
567
|
+
| `{"type": "range", "min": …, "max": …, "value": …}` | full control |
|
|
568
|
+
|
|
569
|
+
The recompute function closes over your kernel namespace, so it can use the same
|
|
570
|
+
variables and helpers your analysis already defined (`z_anom`, `plot_anom_map`,
|
|
571
|
+
…). `make_widget_demo.py` builds `example_widget.ipynb`, a complete runnable
|
|
572
|
+
example with one live composite figure.
|
|
573
|
+
|
|
574
|
+
Constructors: `SemanticNotebook.from_ipynb(path)`, `.from_notebook(nb)` (an
|
|
575
|
+
`nbformat` object), or `SemanticNotebook(document=…)` if you already parsed one.
|
|
576
|
+
`height=` sets the panel height.
|
|
577
|
+
|
|
578
|
+
**Requirements / notes.** Needs `anywidget` and `ipywidgets` (`pip install
|
|
579
|
+
anywidget`). Runs in JupyterLab, Notebook 7, VS Code, and Colab. Fonts load from
|
|
580
|
+
Google Fonts, and recompute needs a live kernel — so both only show up when you
|
|
581
|
+
actually run it in Jupyter, not in a previewed `.ipynb`. The widget CSS is
|
|
582
|
+
scoped to a private root so it can't bleed into the rest of your notebook.
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## The three tiers
|
|
587
|
+
|
|
588
|
+
The durable asset is the **directive spec + parser/model** in `semantic_render`;
|
|
589
|
+
every frontend consumes it.
|
|
590
|
+
|
|
591
|
+
- **Static HTML** (`render_html`) — share, publish, attach to CI. No Jupyter.
|
|
592
|
+
- **Widget** (`SemanticNotebook`) — explore in-kernel: recompute + persistent
|
|
593
|
+
arrange/hide. This is Tier 1.
|
|
594
|
+
- A full JupyterLab extension (directive autocomplete, continuous two-way model
|
|
595
|
+
sync, editor squiggles) would be Tier 2 — worth building only if this becomes
|
|
596
|
+
the primary way several people read notebooks day to day.
|
|
597
|
+
# semantic-rendering
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
semantic_render.py,sha256=KXdMNYO_jqOh7iEvUc_HMHS3bmdnlWdsEzG5ifzorCg,575525
|
|
2
|
+
semantic_widget.py,sha256=TN-acNwTgxKcYdVj97G-lqrCgpDFsALWw8hDPZzcwF8,26535
|
|
3
|
+
junoview-0.1.0.dist-info/licenses/LICENSE,sha256=QsORRpSP0yjJGnMTVlVMvG0_GtjoQoZUIL4ABrjo1DE,11346
|
|
4
|
+
junoview-0.1.0.dist-info/licenses/NOTICE,sha256=vgUjEFG6hbbUqcsUcirpi3zCwEnTXmPnIBEu-Mwl5oA,98
|
|
5
|
+
junoview-0.1.0.dist-info/METADATA,sha256=22_1CGpD3JP70xpAC0tx5Zq-onNlFaozy0LnpYcLsL4,27248
|
|
6
|
+
junoview-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
junoview-0.1.0.dist-info/entry_points.txt,sha256=hf6eKszySUH6zcxIsxFZWiOXzBcLzmnmmmj8tZyYo_M,50
|
|
8
|
+
junoview-0.1.0.dist-info/top_level.txt,sha256=GCi5sAjKNuvcKvO_QYFotGyJc5KCyj5oO3MGKiBnXAs,32
|
|
9
|
+
junoview-0.1.0.dist-info/RECORD,,
|