Junoview — figure-first Jupyter notebook viewing and slide decks. Streamline presentations from Jupyter: display your plots and documentation, single file, stdlib only
Project description
Junoview
Figure-first viewing of executed Jupyter notebooks — and presentations built from the same cells.
NASA's Juno probe viewed Jupiter; Junoview views your Jupyter.
pip install junoview # then: junoview
▶ Live demo — no install · a real climate-diagnostics notebook, rendered.
Turn an executed Jupyter notebook into a figure-first, nonlinear analysis environment. Instead of rendering every cell with equal weight (the Quarto / nbconvert model), it recovers the scientific structure underneath the notebook —
Dataset → Transform → Diagnostic → Figure → Interpretation
— and renders figures as the primary objects, with code collapsed behind them, sections in a navigable rail, and a live provenance graph of how each diagnostic derives from the data.
There is no kernel and no re-execution: run your notebook once, normally, in
Jupyter; the renderer reads the outputs already stored in the .ipynb. Only
dependency is the Python standard library.
Run it — the app (recommended)
python semantic_render.py
launches a small local server and opens the semantic notebook app in your browser. The layout is IDE-style: a controls bar on top (+ Open and the global Hide/Show filters, left-aligned), the notebook tabs beneath it, and a vertical presentations rail down the left edge.
- + Open (controls bar, top left) browses your file system; or just
drag-and-drop
.ipynbfiles anywhere onto the window — or paste a URL into the open dialog (GitHubbloblinks are converted to raw automatically). URL notebooks reload with ↻ and come back on restart. - Every notebook opens as a tab. Click to switch, ↻ re-reads a notebook from disk after you re-run it in Jupyter, ✕ closes it.
- The presentations rail (left edge) stacks your presentations
vertically under a Documents button. Exactly one item is active at a
time: click a ▶ presentation to open it in the builder, click
Documents to go back — that button is always visible, so there is
always an obvious way out (
Escand the builder's ✕ Close work too). New starts a presentation; unsaved drafts carry an amber dot and stay listed while you work on others. « collapses the rail to icons, and again to hide it completely — a small » handle at the bottom-left brings it back. While the builder is docked, notebook tabs keep working — switch tabs to pull cards from different notebooks into the same deck. - Open tabs and recent files are remembered in
junoview_project.jsonnext to where you launched the app — restart later and your workspace comes back. - Presentations can mix cards from every open tab (see below) and save into the same project file.
Options: preload tabs with python semantic_render.py --app A.ipynb B.ipynb;
choose the project folder with --root, the port with --port, and skip the
auto-opened browser with --no-browser. The server binds to 127.0.0.1 only
and the URL carries a session token.
Run it — static export
For a shareable, self-contained page (no server needed to view it):
# one notebook -> my_notebook.html next to it
python semantic_render.py my_notebook.ipynb
# choose output / title
python semantic_render.py my_notebook.ipynb -o report.html --title "Run 42"
# several notebooks -> ONE page with tabs (default: semantic_view.html)
python semantic_render.py part1.ipynb part2.ipynb -o project.html
Open the example to see it:
python semantic_render.py example_climate_analysis.ipynb
open example_climate_analysis.html
To rebuild the example notebook from scratch (needs nbformat, nbclient,
xarray, matplotlib):
python make_example_notebook.py
jupyter execute example_climate_analysis.ipynb # or run it in Jupyter
python semantic_render.py example_climate_analysis.ipynb
Publish it — the hosted web version
The tool ships as a fully client-side web app: the same single Python file runs in the visitor's browser via Pyodide (Python compiled to WebAssembly). There is no backend at all, which means free static hosting, nothing to maintain, and — the important part for science — notebooks are never uploaded anywhere; files people open stay on their machine.
python semantic_render.py --build-web docs
writes docs/index.html + docs/semantic_render.py. To publish on
GitHub Pages:
- Commit the
docs/folder and push. - On GitHub: Settings → Pages → Source: Deploy from a branch →
main/docs. - Your tool is live at
https://<you>.github.io/<repo>/.
(Any static host works — Netlify, Cloudflare Pages, a plain web server.) The web version supports drag-and-drop, a file picker, and opening notebooks by URL; presentations autosave as browser drafts and can be downloaded as JSON or saved into a notebook via the file picker. The build bundles the example notebook, so first-time visitors get a Try the example notebook button, and every mode has a Help overlay covering the directives and everything the tool can do. The first visit downloads the Python runtime (a few MB, then cached).
Do not deploy the local app server (--app) to a public machine —
it is deliberately single-user: it binds to 127.0.0.1 and browses the
host's filesystem. The web build above is the safe public face; the app
server is for your own machine.
Install as a command
pip install junoview # or: pipx install junoview
junoview # launches the app from anywhere
Or from a checkout: pip install . (add pipx to isolate it). The Jupyter
widget extras come with pip install "junoview[widget]".
How to format a notebook for it
You annotate cells with #| key: value directive lines at the very top of a
code cell. They are parsed and then stripped from the displayed source.
Everything is optional — with no directives at all the renderer still infers a
sensible layout from each cell's outputs.
Directives
| Directive | What it does |
|---|---|
| `# | section:` |
| `# | subsection:` |
| `# | title:` |
| `# | display:` |
| `# | code:` |
| `# | id:` |
| `# | depends:` |
| `# | caption:` |
| `# | group:` |
| `# | order:` |
| `# | step:` |
| `# | stack:` |
A figure cell
#| display: figure
#| id: block_comp
#| depends: anom, block_freq
#| title: Composite Z500 anomaly on blocked days
#| caption: The localised positive centre is the blocking high.
comp = z_anom.sel(time=blocked).mean('time')
comp.plot(cmap='RdBu_r')
This renders as a figure card titled "Composite Z500 anomaly on blocked days",
with the caption beneath it, the code tucked behind a Show code toggle, a
derives from anom · block_freq provenance line, and a node in the rail graph
wired to the anom and block_freq nodes.
Grouping several cells under one figure
A figure is usually the last step of a small pipeline — regrid, composite,
plot. Give those cells the same #| group: name and they collapse into a
single card: the cell that draws the figure is the face, and the prep folds
behind one Show code toggle as numbered steps.
#| group: fig_zonal
#| order: 1
#| step: zonal mean + 30-day smoothing
zm = z_anom.mean('lon')
zm_mon = zm.rolling(time=30, center=True).mean().resample(time='1MS').mean()
#| group: fig_zonal
#| order: 2
#| step: plot Hovmöller
#| display: figure
#| id: zonal_hov
#| depends: anom
#| title: Zonal-mean Z500 anomaly (time–latitude)
zm_mon.plot(x='time', cmap='RdBu_r')
Both cells become the one zonal_hov card. Notes on the merge:
- Face = the cell with
display: figure(or, absent that, the last cell that produces an image / any output). Its output is shown; the others' code folds underneath, and any intermediate output (a printed shape, a repr) is tucked under its own step. - Title / caption / id come from the group — the first member that sets each wins, preferring the figure cell.
dependsis the union across all members, so the prep cell's inputs and the plot cell's inputs both feed the one node. The group is therefore a single vertex in the provenance graph — grouping declutters the graph as well as the page.- Section / subsection is taken from where the group's first cell sits (a
##/###heading above it, or asubsection:on that cell).
step: is the clean way to label a chunk; subsection: on a grouped member
is also accepted as a chunk label, to match the obvious shorthand.
Stacking shared cells under a figure (reuse)
Grouping is push — each cell tags itself into one group, so a cell can only
live under a single figure. When the same prep feeds several figures
(opening the data, regridding, a shared plotting helper), use #| stack:
instead. A figure names the upstream cells by id, and they fold in front of
its own code:
#| id: maphelper # define the shared cell once
#| step: shared map helper
def plot_anom_map(da, ax, title, vmax=None):
...
#| display: figure
#| id: block_comp
#| depends: anom, block_freq
#| stack: maphelper # ← fold the helper under this figure
comp = z_anom.sel(time=blocked).mean('time')
pc = plot_anom_map(comp, ax, 'Blocked-day composite')
#| display: figure
#| id: enso_comp
#| depends: anom, nino34
#| stack: maphelper # ← and under this one too (same cell)
pc = plot_anom_map(tele, ax, 'Warm-phase composite')
maphelper now folds in as step 1 of both composite cards. Key points:
- A cell named in any
stack:list is consumed: it gets no card of its own and no graph node — it lives only under the figures that stack it. The same id may be stacked under any number of figures. - Stacked cells render before the card's own code, in the order listed;
the figure's own code is the final step. Use
#| order:-style intent by ordering the ids in the list. - Stacking folds code only; it does not add provenance edges. Use
depends:for lineage you want drawn in the graph.
group vs stack — which to use
group: (push) |
stack: (pull) |
|
|---|---|---|
| Who references whom | each cell tags itself | the figure names cells by id |
| Cell can belong to | one card | any number of cards |
| Best for | a few adjacent cells authored as a unit | shared prep reused across figures |
| Standalone card | merged away | consumed (no card, no node) |
A useful split to remember: depends: keeps a cell as its own node in the
graph; stack: folds its code into a figure and collapses it. One is about
scientific lineage, the other about reproducibility of a single figure.
A Markdown cell whose first line is a heading opens structure:
## Blocking diagnostics ← H2 opens a section
### Regional composite ← H3 opens a subsection
Any prose under a heading (or any plain Markdown cell) becomes an interpretation note — rendered in a serif face to set human commentary apart from machine output.
You can mix styles: use ## headings for some sections and #| section: on a
code cell for others. The example notebook does both.
What happens with no directives
| Cell produces… | Inferred card |
|---|---|
| an image | figure |
| an xarray HTML repr | dataset |
| only short text / stdout | metric |
| longer text | text |
| no output | code (collapsed) |
So an un-annotated notebook still renders cleanly; directives are how you take
control — naming diagnostics, writing captions, and declaring the provenance
graph with id / depends.
What you get in the page
- Left rail — section tree plus a live analysis graph. Scrolling highlights the active section and its node; clicking a node jumps to that card.
- Figure stage — each diagnostic as a card: title, output, serif caption,
amber
derives from …provenance chips (click to jump to a source), and a collapsible code block. - Controls bar (top row, global — it applies to every tab) — three buttons whose labels follow the state: Hide/Show figures, Hide/Show markup (the markdown/equation cells) and Hide/Show code. Show code shows ALL code: it reveals the code-only cards and unfolds the code tucked under every figure and dataset card in one click. Any combination works — hide code for a figures-plus-documentation reading view, leave only markup for just the prose. A hidden card collapses to a slim dashed stub that expands in place when clicked.
- Raw notebook (controls bar) — flips the active tab to the notebook
exactly as authored: every cell in order,
#|directives visible, outputs underneath. This is the transparency view — it shows precisely where each card's title, caption and section came from. Click again (or any nav link) to return to the formatted view. - Notebook tabs (beneath the controls) — one per open notebook.
- Presentations rail (left edge, vertical) — a Documents button on top, then your presentations; the active item is highlighted.
- Responsive to mobile, keyboard-navigable, respects reduced-motion.
Presentation deck
Click a ▶ presentation in the left rail to open that deck in the
builder (below); the ▶ Present button at the top of the
panel plays the deck full-screen — arrow buttons / arrow keys to move,
Esc or ✕ Exit to drop back to the builder, Docs to
leave altogether. The rail's Documents button returns to the
documents from anywhere. With nothing saved yet you get auto: figures
— one full-screen slide per figure, in document order.
The deck has two axes. Horizontal (←/→) is your story. Vertical is
the code trail: on any slide whose frames carry code, a ↓ arrow
(and "↓ how it was made" hint) appears — press it (or ArrowDown)
to descend through every cell that produced what's on screen, one cell
per step, in execution order: open data → transforms → the plot
itself. ↑ climbs back; Esc returns to the slide; ←/→ leave the
trail and continue the story. The counter shows where you are
(3 / 8 · code 2 / 5).
The trail is the union of your declared depends: edges and
automatic variable tracing: the renderer parses each cell's code
and links a figure to whichever cells last assigned the variables it
reads, so even un-annotated notebooks get the full lineage. (Static
best-effort: it can't see mutation without assignment, e.g.
ds.load(); declare depends: where the trace misses something.)
Create mode
The builder docks full-height beside the presentations rail with the
real document view next to it — the tab row and filters shift right to
sit above the document. ▶ Present at the top plays the deck;
Documents in the rail (or Esc, or ✕ Close) exits back to the
documents. You build slides by pointing at the document:
- + Add slide, then pick a layout from the diagram buttons: full, halves (side by side), rows (stacked), quarters, a title slide, or a blank canvas (dashed icon) that you compose entirely in ✎ Edit with text, arrows, boxes and notebook cells. Title/subtitle can be typed in the panel or — in ✎ Edit — right on the slide, where they are movable text objects like everything else.
- Click a pane in the layout diagram, then click a card in the document to place it there; the next empty pane is selected automatically, and ✕ on a pane clears it. Figure panes show a faint live preview of their image.
- The filmstrip shows PowerPoint-style thumbnails of every slide with the actual content — scaled-down figures, text stripes for markup — click to select, ↑ ↓ to reorder, ✕ to delete.
- ✎ Edit slide opens the slide in the document area (the builder
stays on the left, tabs above) with drawing tools — + Text (click
to place a text box, type straight into it), + Arrow and + Box
(drag to draw), + Cell (below), Select to move things (text
moves by its ⠿ handle) and Delete /
Del. Selecting any item reveals a format bar: six colours, text size A− / A+, line thickness, Dash, Fill for boxes, Bg to strip a text box's background. Everything is stored with the slide in percent coordinates, so it scales with the screen and shows in playback. Done orEscreturns to the builder. - + Cell places a draggable, resizable frame that says "Click to add from notebook" — clicking it flips you back to the notebook view with a picker banner; click any card and you're returned to the editor with it placed in the frame. Hovering or selecting a filled frame shows ⇄ Replace right on the frame (also in the format bar) to swap in a different card, from any open notebook.
- Everything else lives in the File ▾ menu: New presentation, Rename, the two auto-builders (figures / figures + docs, in document order), Save to notebook, Download JSON and Discard changes.
Markdown cards render with bullets/bold and LaTeX equations
($...$, $$...$$, typeset by MathJax — needs internet on first view),
so "figure with its equations beside it" is a Halves slide: the figure
in one pane, the markdown card in the other.
Edits autosave as a draft in the browser (localStorage), per
presentation — refresh and nothing is lost; the status pill shows
auto / saved / unsaved draft, and Discard reverts to the saved copy.
Presentations across notebooks
Projects usually span several notebooks, so the deck works across all
open tabs: the tab strip stays visible in Create mode — switch tabs
while building and click cards from any notebook; a Halves slide can
show a figure from part1 next to a figure from part2. When more than
one notebook is open, panes and slides carry a small chip naming the
source notebook, and the auto-builders walk every tab in order.
Internally a pane in a multi-notebook deck is stored as
<notebook>::<anchor>; single-notebook decks keep plain anchors, so
everything stays compatible with the classic sidecar / embed flow below.
If a slide references a notebook that isn't open, the pane says so and
comes back when you reopen the tab.
Named presentations, saved where you work
Multiple named presentations — each one is a ▶ item in the left rail; New starts one, File → Rename (or clicking the name in the builder) renames it. Saving routes:
- App mode: autosave to project (default on) — every change is
written to
junoview_project.jsonin the app's root folder about a second after you make it, alongside your open-tab session. Toggle it with File → Autosave; with it off, File → Save to project saves manually. File → Delete presentation removes one. - Download / Load deck JSON (everywhere, including the web
version) — Download JSON saves the deck as a file on your machine;
Load deck JSON… imports it back, later or on another computer. In
the web version, edits also autosave as browser drafts, so a normal
reload never loses work. A sidecar
<notebook>.deck.jsonplaced beside a local.ipynbauto-loads; bake one in withpython semantic_render.py nb.ipynb --embed-deck nb.deck.json, or render with a project file via--deck project.deck.json. (Direct save-into-.ipynb from the browser exists in the code but is currently disabled.)
Slides reference cards by a stable anchor — the cell's #| id: if it
has one, else the notebook's built-in cell id (nbformat ≥ 4.5) — never by
position. Reordering, editing or adding cells does not break the deck;
deleting a referenced cell just skips that slide with a note. Prefer
#| id: anchors: they also survive copy-pasting cells between notebooks.
--deck other.json renders with a specific deck file (overrides the
sidecar and the embedded metadata).
Design notes / extending
- The renderer is one file (
semantic_render.py) with the directive parser, a tokenizer-based Python highlighter, output renderers, the semantic model, the graph-layout, and the inlined HTML/CSS/JS.python semantic_render.py --self-testruns a built-in sanity check. - Because the static page is precomputed, its interactivity is limited to what can be baked in (navigation, provenance highlighting, code toggles). For a live kernel — recompute and persistent arrangement — use the widget below.
Live in Jupyter (semantic_widget.py)
The static page is for sharing — one self-contained file, no Jupyter needed.
When you want to explore, SemanticNotebook renders the same figure-first
view inside a notebook, against the live kernel. It imports the same parser
and the same card HTML from semantic_render, so the directive format and the
look are identical; it just adds the two things a dead file can't do.
from semantic_widget import SemanticNotebook
view = SemanticNotebook.from_ipynb("analysis.ipynb") # an executed notebook
view
1 · View-state that persists back to Python. Hide a card (hover → ✕), switch the layout (Column / Grid / Compact), or collapse the graph, and the choices are synced to the kernel:
view.view_state # -> {'hidden': ['enso_spec'], 'layout': 'grid', ...}
view.export_html("clean.html") # static page with hidden cards dropped
export_html is the bridge back to the shareable artifact: arrange it live,
then export a clean page.
2 · Live recompute. Attach a function to a figure id; its parameters
become controls, and changing them re-runs the function on the kernel and swaps
that figure in place. Every other figure stays static.
@view.recompute("block_comp",
threshold=(0.5, 2.5, 0.1), # slider
region=["Tasman", "Ross", "Weddell"]) # dropdown
def _(threshold, region):
box = z_anom.sel(**REGIONS[region]).mean(["lat", "lon"])
comp = z_anom.sel(time=box > box.std() * threshold).mean("time")
fig, ax = plt.subplots()
plot_anom_map(comp, ax, f"{region} composite")
return fig # return a matplotlib Figure (or just draw one)
Parameter shorthands:
| you write | control |
|---|---|
(lo, hi) or (lo, hi, step) |
slider |
["a", "b", "c"] |
dropdown |
5 / 1.5 |
number box |
True |
checkbox |
"text" |
text box |
{"type": "range", "min": …, "max": …, "value": …} |
full control |
The recompute function closes over your kernel namespace, so it can use the same
variables and helpers your analysis already defined (z_anom, plot_anom_map,
…). make_widget_demo.py builds example_widget.ipynb, a complete runnable
example with one live composite figure.
Constructors: SemanticNotebook.from_ipynb(path), .from_notebook(nb) (an
nbformat object), or SemanticNotebook(document=…) if you already parsed one.
height= sets the panel height.
Requirements / notes. Needs anywidget and ipywidgets (pip install
anywidget). Runs in JupyterLab, Notebook 7, VS Code, and Colab. Fonts load from
Google Fonts, and recompute needs a live kernel — so both only show up when you
actually run it in Jupyter, not in a previewed .ipynb. The widget CSS is
scoped to a private root so it can't bleed into the rest of your notebook.
The three tiers
The durable asset is the directive spec + parser/model in semantic_render;
every frontend consumes it.
- Static HTML (
render_html) — share, publish, attach to CI. No Jupyter. - Widget (
SemanticNotebook) — explore in-kernel: recompute + persistent arrange/hide. This is Tier 1. - A full JupyterLab extension (directive autocomplete, continuous two-way model sync, editor squiggles) would be Tier 2 — worth building only if this becomes the primary way several people read notebooks day to day.
semantic-rendering
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file junoview-0.1.0.tar.gz.
File metadata
- Download URL: junoview-0.1.0.tar.gz
- Upload date:
- Size: 180.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e08b69d52397436b1b94ef573e6df08bfcb153faa20e953f187c4affd7adaac0
|
|
| MD5 |
95679422e20aca16f36b875d5a064aea
|
|
| BLAKE2b-256 |
9afd552a07181add88c4368eab35651a04b08c5e6537677c6baf3f52f995fe08
|
File details
Details for the file junoview-0.1.0-py3-none-any.whl.
File metadata
- Download URL: junoview-0.1.0-py3-none-any.whl
- Upload date:
- Size: 182.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1eb09b1c32347faeb426a8ddc834270739c7be94a329dda98411852544aa9a47
|
|
| MD5 |
0dc1b4e57e89eecb9c784989bf05ce25
|
|
| BLAKE2b-256 |
ed755ff255e2c41dec8938d365883e37b9686bdf7f233cd1023c455083d34227
|