Repository navigation
feat: add render_k3d() for k3d 3D plots - #281
Merged
Merged
Conversation
Renders k3d.plot() widgets, filling their container by default (k3d's pixel height only applies when the user sets a non-default height), and following an explicit output_widget() height instead of overflowing it. Part of #278
Every re-render of a k3d plot leaked the old plot: k3d's Plot.close() only clears the ipywidgets Outputs it's shown in and never calls Widget.close(), so its comm stayed registered in Python and its model stayed alive in the browser (8 re-renders left 8 of each), along with the plot's data. For k3d widgets, init_shiny_widget() now wraps close() so it also runs Widget.close() (a no-op once the comm is closed). New unit test checks the comm is closed on re-render; new Playwright test checks the browser's model count settles back after re-renders.
A Layout(height="300px") on a k3d plot opts out of filling, but it only sized the widget's .lm-Widget wrapper: k3d draws at its own pixel `height` (512 by default), so the bottom 212px of the scene was cut off. Make the wrapper a flex column and let k3d's inner container flex to it. Its pixel height stays the flex basis, so plots without a definite Layout height keep it. New Playwright case covers Layout(height="300px").
Returning a drawable such as `k3d.points(...)` from @render_k3d rendered an empty output with no error: drawables are model-only widgets that draw nothing by themselves. In Jupyter, displaying one puts it on a new `k3d.plot()`, so render_k3d now does the same (Groups included). Also name the plot class by its runtime path (`k3d.Plot`; `k3d.plot.Plot` doesn't resolve because `k3d.plot` is the factory function). Unit tests moved into tests/unit/test_k3d.py.
# Conflicts: # CHANGELOG.md # pyproject.toml # shinywidgets/__init__.py # shinywidgets/_render_widget.py # shinywidgets/_render_widget_base.py # tests/unit/test_render_widget.py # tests/unit/test_render_widget_base_layout_defaults.py # uv.lock
…d-9ceabf5-67c7-s0-t0 # Conflicts: # CHANGELOG.md # pyproject.toml # tests/unit/test_render_widget_base_layout_defaults.py
Merges origin/main (per-renderer extras #330, AGENTS.md consolidation #331, @render_ipydatagrid, @render_jscatter) into render-k3d: - Accept CLAUDE.md deletion; AGENTS.md is now the real file - Add k3d = ["k3d"] extra and keep k3d in test/dev deps - Point @render_k3d docstring and ImportError at pip install 'shinywidgets[k3d]' - Add @render_k3d row to the README extras table - examples/k3d/requirements.txt now uses shinywidgets[k3d]
# Conflicts: # shinywidgets/_render_widget.py # tests/unit/test_render_widget.py
# Conflicts: # CHANGELOG.md # README.md # pyproject.toml # shinywidgets/__init__.py # shinywidgets/_render_widget.py # shinywidgets/_render_widget_base.py # tests/unit/test_render_widget.py
The sandbox/ folder is local scratch space (probe scripts, screenshots, port files) already covered by .gitignore. It was committed by accident while developing render_k3d(); drop it from the tree so the PR only contains the renderer, tests, and docs.
- Apply the k3d comm-close workaround through the class MRO (matching the perspective/itables hooks and the layout-defaults dispatch), so user-defined Plot subclasses don't leak comms and browser models on re-render. - Exclude intrinsic heights (auto/fit-content/...) from the k3d pixel-height CSS overrides, as the bqplot rule already does, so output_widget(height= "auto") keeps the plot's intrinsic height instead of collapsing to 100% of an indefinite-height parent. Adds a unit test (Plot subclass comm cleanup) and a Playwright assertion (intrinsic output height keeps k3d's 512px).
3 tasks
cpsievert
marked this pull request as ready for review
October 27, 2026 21:05
cpsievert
added a commit
that referenced
this pull request
Oct 27, 2026
Only conflict: the runtime alias chain in _render_widget.py's non-TYPE_CHECKING block -- keep both EmbeddingAtlasWidget and K3dPlot.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You can now return a k3d plot from a Shiny app and get a live WebGL scene that fills its card/sidebar layout, with typed
.widgetaccess (ak3d.plot.Plot) and the camera (and other plot state) readable reactively viareactive_read(). Requirespip install 'shinywidgets[k3d]'.Part of #278.
Notes
k3d.plot()already returns a widget, so no coercion is needed.k3d.plot(height=N)with N != 512, or an explicitLayout(height=...)on the widget, keeps the fixed height. Caveat: since k3d always setsheight, exactly 512 is indistinguishable from "unset" and fills.output_widget(height="250px")is respected via a small scoped CSS rule (without it the 512px plot was clipped: canvas 512 in a 250px output).Verification
format, pyright, unit tests (187 passed); Playwright render_k3d (5 tests, stable across repeated runs) plus render_maplibre and render_matplotlib_fill; example app run and screenshot.
🤖 Generated with Claude Code