Skip to content

fix(notebooks): render dashboards & panels correctly in browser JupyterLab#30

Merged
daharoni merged 8 commits into
mainfrom
fix/jupyterlab-ipympl-rendering
Jun 24, 2026
Merged

fix(notebooks): render dashboards & panels correctly in browser JupyterLab#30
daharoni merged 8 commits into
mainfrom
fix/jupyterlab-ipympl-rendering

Conversation

@daharoni

@daharoni daharoni commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #29. Once the play() MIME fix made the videos play in browser JupyterLab, three more rendering issues became visible. All were masked in VS Code (whose inline backend is Agg); the notebooks run under %matplotlib widget (ipympl) for the interactive sandboxes, and ipympl behaves differently in a browser.

Issues fixed

1. Dashboard videos showed only 1 of 5 footprint/trace rows.
build_*_frames rasterize a matplotlib figure to RGB via canvas.buffer_rgba(), assuming it equals figsize × dpi. Once an ipympl canvas has been displayed, a retina frontend stamps device_pixel_ratio = 2 onto later figures, so the buffer comes back at 2× with the figure drawn into a single quadrant — the height-only rescue in _cursor_panel can't recover it, so 4 of 5 rows fall outside the panel. Fix: render the offline-composited figures on an explicit Agg canvas (_agg_figure), so the buffer is always exactly figsize × dpi on every frontend.

2. Interactive sandbox panels came up blank until a slider was nudged.
interactive_panel drew before display(canvas); ipympl never pushes a frame rendered before its view is attached. Fix: draw after display.

3. Static-figure cells (optics reveal, vasculature, bleaching, motion) were blank for the same reason via display(fig.canvas). Fix: a small show() helper that displays then draws, with those cells routed through it.

Also

Restore play()'s height-based scaling. #29 capped the height to the source (min(height, src_h), "never upscale"), which shrank the dashboards — built at the downsampled movie resolution and relying on play() to scale them up — into an illegible strip. Now the requested height is honoured (mediapy scales up or down) while keeping the explicit even (h, w) that fixed #28.

Verification

Reproduced and verified each fix end-to-end in headless Chromium driven against a live JupyterLab server (Playwright): the Stage 4 dashboard now shows all 5 footprints + traces, and the Stage 1 / optics-reveal panels render on first run. ruff clean; full test suite passes (382).

🤖 Generated with Claude Code


📚 Documentation preview 📚: https://minisim--30.org.readthedocs.build/en/30/

daharoni and others added 4 commits June 23, 2026 10:21
…erLab

Three ipympl-vs-Agg issues surfaced once the play() MIME fix (#29) made the
videos play in browser JupyterLab. All were masked in VS Code, whose inline
backend is Agg; the notebooks run under %matplotlib widget (ipympl) for the
interactive sandboxes.

- Dashboard videos showed only the first of five footprint/trace rows. The
  build_*_frames helpers rasterize a figure to RGB via canvas.buffer_rgba(),
  assuming it equals figsize x dpi. Once an ipympl canvas has been displayed, a
  retina frontend stamps device_pixel_ratio=2 onto later figures, so the buffer
  comes back 2x with the figure drawn into one quadrant - dropping 4 of 5 rows.
  Render these offline-composited figures on an explicit Agg canvas (_agg_figure)
  so the buffer is always exactly figsize x dpi, on every frontend.

- Interactive sandbox panels (Stage 1, 2, 3, 5b, 6, 8, 9) came up blank until a
  slider was nudged. interactive_panel drew before display(canvas); ipympl never
  pushes a frame rendered before its view is attached. Draw after display.

- Static figure cells (optics reveal, vasculature, bleaching, motion) had the
  same blank-until-interaction problem via display(fig.canvas). Add a show()
  helper that displays then draws, and route those cells through it.

Also restore play()'s height-based scaling: #29 capped height to the source
("never upscale"), which shrank the small dashboards (built at the downsampled
movie resolution) to an illegible strip. Honour the requested height (mediapy
scales up or down) while keeping the explicit even (h, w) that fixed #28.

Verified end-to-end in headless Chromium against a live JupyterLab server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more browser-JupyterLab issues found reviewing the PR branch, both in
Stage 5b (vasculature):

- show() rendered blank. Routing the static builder panels through the live
  ipympl canvas (display + post-attach draw) is unreliable deep in a long
  notebook - the first frame is never pushed, and windowing re-blanks it on
  scroll. These panels are static (drawn once, never interacted with), so
  rasterize to an inline PNG and display that, sidestepping the live canvas
  entirely. Backend-independent; always paints.

- The video looked dim, reading as if the neuropil haze were missing (it is
  not - until="vasculature" runs through the earlier neuropil step). play()
  normalized by the absolute max, so a few bright cell peaks crushed the haze
  pedestal to near-black. Give play() an optional vmax (clip to a percentile,
  the same scaling the dashboard colourizers use) and pass the 99.9th percentile
  the panel already computes, so the raw movie reads as bright as its dashboards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The slider panels are fixed-size teaching figures, but ipympl wraps each
canvas in a toolbar, a status footer, and a drag-resize handle. With the
toolbar already hidden the footer + resize affordance still reserve a band
of empty canvas below the plot in browser JupyterLab. Turn header/footer/
resizable off centrally in interactive_panel so the panel ends at the plot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A component's share of the haze scales with its spatial field's mean, but
smooth_spatial_field peak-normalizes each field, which lets that mean float
~1.2-3.1x across RNG draws (measured over seeds 0-42) - so one component
could dominate the background 3:1 and, in the R/G/B components video, render
one channel far brighter than the others.

Rescale each component's field to the shared average mean in
neuropil_components, so every component carries an equal share. The aggregate
level (the mean over components) is unchanged, so amplitude still sets the
absolute background and the rendered haze is identical in total - only its
split across components is equalized. Ground-truth neuropil_spatial is now
mean-balanced rather than individually peak-normalized; the step test asserts
the equal-mean invariant instead of peak == 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

daharoni and others added 4 commits June 23, 2026 15:41
…mpl canvas

The slider sandboxes (Stages 1-3, 5b, 6, 8, 9) drive an ipympl canvas under
%matplotlib widget. ipympl does not reliably push that canvas to a browser-
JupyterLab view: near the top of the notebook a post-display draw paints it,
but deeper down (Stage 6+) the frame is dropped and the panel comes up blank -
sliders visible, plot missing - and windowing re-blanks it on scroll.

Render the figure to a PNG and (re)display it in an Output widget on every
slider change instead of showing the live canvas. Reliable on every frontend,
at any depth. continuous_update is already off so it re-renders on release, and
clear_output(wait=True) swaps without flicker. This supersedes the earlier
draw-after-display and chrome-trim workarounds (no canvas is shown now, so the
toolbar/footer/resize reserve and the first-frame race are both gone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w out the scale

A few fully-dark outer cells (heavy illumination/vignette falloff -> ~0 gain)
land at ~1e-15 SNR. On the log axis matplotlib autoscaled down to them, leaving
the detectability transition crammed into the top sliver across a dozen empty
decades. Clamp the y-limits to the meaningful range: bottom a few-fold below the
1st-percentile cell (outlier-robust) and the threshold, top just past the
brightest - always bracketing the floor line. The dark cells clip off the bottom
but stay counted in the legend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No-op commit to re-run the Read the Docs build, which failed on a transient
intersphinx inventory fetch (not a content error - the docs build clean
locally with warnings-as-errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unreachable

docs.scipy.org intermittently times out from the build network. Under sphinx
`-W` that ConnectTimeout became a hard failure ("failed to reach any of the
inventories"), breaking every docs build even though nothing in the docs is
wrong. Add a logging filter on the intersphinx logger that drops only that
fetch-failure record; cross-refs into a missing inventory fall back to plain
text (nitpicky is off), and every real warning (broken refs, autodoc import
failures) still fails the build as before. Verified locally: with scipy
unreachable the build now succeeds instead of erroring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 minisim | 🛠️ Build #33281261 | 📁 Comparing 451233c against latest (71341a4)

  🔍 Preview build  

1 file changed
± reference/metrics.html

@daharoni daharoni merged commit a76a5d4 into main Jun 24, 2026
12 checks passed
@daharoni daharoni deleted the fix/jupyterlab-ipympl-rendering branch June 24, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

play() renders unplayable video in JupyterLab — "No video with supported format and MIME type found"

1 participant