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.
@@ -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
+ ![Junoview demo](docs/demo.gif) -->
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 **&#9654; Present** button at the top of the
382
+ panel plays the deck full-screen — arrow buttons / arrow keys to move,
383
+ `Esc` or **&#10005; 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. **&#9654; 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ junoview = semantic_render:main