Fix interactive viewer display in non-blocking matplotlib environments - #95
Conversation
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.
There was a problem hiding this comment.
🟢 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
RenderStyleretains 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.
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().
There was a problem hiding this comment.
🟢 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
Fixes #81.
render_mpl_interactiveassumedplt.show()blocks until the viewer window closes. With matplotlib in interactive mode (%matplotlib widgetin Jupyter,%matplotlib qtin IPython,plt.ion()),show()returns immediately, so the unconditionalplt.close()destroyed the figure before it was ever displayed.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 whethershow()blocked is a proxy for matplotlib's own rule. Instead, close the figure whenever the initial draw orshow()raises, which previously leaked an undisplayed figure.