Skip to content

Fix interactive viewer display in non-blocking matplotlib environments - #95

Merged
bjmorgan merged 10 commits into
mainfrom
fix-interactive-nonblocking
Sep 1, 2026
Merged

Fix interactive viewer display in non-blocking matplotlib environments#95
bjmorgan merged 10 commits into
mainfrom
fix-interactive-nonblocking

Conversation

@bjmorgan

@bjmorgan bjmorgan commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Fixes #81.

render_mpl_interactive assumed plt.show() blocks until the viewer window closes. With matplotlib in interactive mode (%matplotlib widget in Jupyter, %matplotlib qt in IPython, plt.ion()), show() returns immediately, so the unconditional plt.close() destroyed the figure before it was ever displayed.

  • Drop the close after show(). GUI backends deregister the figure themselves when its window is closed, so it only ever fired on non-GUI backends, and any prediction of whether show() blocked is a proxy for matplotlib's own rule. Instead, close the figure whenever the initial draw or show() raises, which previously leaked an undisplayed figure.
  • Draw with a per-draw copy of the style carrying the low-fidelity interactive segment counts rather than mutating and restoring the shared style. Previously, live redraws in non-blocking environments silently ran at full static fidelity.
  • Work on a copy of the caller's style, so display toggles no longer modify the object passed in; changes are reflected only in the returned style.
  • Document the return semantics per environment: blocking sessions return a final snapshot when the window closes; interactive-mode sessions return immediately with live-updating view and style objects.

With matplotlib in interactive mode (Jupyter %matplotlib widget,
%matplotlib qt in IPython, plt.ion()), plt.show() returns immediately
and the unconditional plt.close() destroyed the figure before it was
displayed. Close the figure only when matplotlib is not in interactive
mode, i.e. only when show() actually blocked.
The interactive viewer previously mutated the style's segment counts
for the whole session and restored them after show(). In non-blocking
environments the restore ran immediately, so live redraws silently used
publication-quality counts. Swap in the low-fidelity counts only around
each draw, so the returned style always carries its static counts.
Copilot AI lite review requested due to automatic review settings September 1, 2026 18:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes directly address the non-blocking interactive backend lifecycle issue, include targeted regression tests, and align code/doc semantics without introducing apparent behavioral regressions.

Pull request overview

Fixes StructureScene.render_mpl_interactive behavior in non-blocking matplotlib environments (e.g., Jupyter %matplotlib widget, IPython %matplotlib qt, or plt.ion()), ensuring the interactive viewer figure isn’t immediately destroyed and that interactive redraws use reduced-fidelity segment counts without degrading the returned style.

Changes:

  • Close the interactive viewer figure only when matplotlib is not in interactive mode, preventing premature figure destruction in non-blocking sessions.
  • Apply low-fidelity interactive segment counts only during draws via a context manager, ensuring the returned RenderStyle retains publication-quality segment settings.
  • Add/extend tests and documentation clarifying blocking vs interactive return semantics and backend requirements (including ipympl).
File summaries
File Description
tests/test_rendering/test_interactive.py Adds figure-lifecycle and draw-fidelity tests for interactive vs blocking behavior and style restoration.
src/hofmann/rendering/interactive.py Implements conditional figure closing and per-draw interactive fidelity switching via a context manager; updates docstring semantics.
src/hofmann/model/structure_scene.py Updates wrapper docstring to reflect blocking vs interactive return semantics.
docs/interactive.rst Documents non-blocking interactive-mode behavior and notes ipympl support for %matplotlib widget.
docs/changelog.rst Adds unreleased changelog entries describing the interactive viewer fix and fidelity behavior change.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@bjmorgan bjmorgan added the bug Something isn't working label Sep 1, 2026
GUI backends deregister a figure themselves when its window is closed,
so the close after show() was only ever reached on non-GUI backends
and, gated on matplotlib's interactive flag, mis-predicted blocking
under %matplotlib qt with plt.ioff(). Drop it, and instead close the
figure whenever the initial draw or show() raises, which previously
leaked an undisplayed figure.

Draw with a per-draw copy of the style carrying the low-fidelity
segment counts rather than mutating and restoring the shared style,
and work on a copy of the caller's style so display toggles no longer
modify the object they passed in.
Move figure creation to immediately above the try block, so facecolor,
subplots_adjust, the event connects, and the default key handler
disconnect all sit inside the close-on-failure guard along with the
initial draw and show().
Copilot AI review requested due to automatic review settings September 1, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change directly addresses the documented root cause, adds targeted regression tests for lifecycle and fidelity, and keeps API behavior coherent with updated documentation.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@bjmorgan
bjmorgan merged commit 8f1ea6e into main Sep 1, 2026
6 checks passed
@bjmorgan
bjmorgan deleted the fix-interactive-nonblocking branch September 1, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Problems with widget support on remote machines in new package versions

2 participants