Skip to content

feat: add render_mosaic_widget() for Mosaic dashboards - #285

Merged
cpsievert merged 13 commits into
mainfrom
render-mosaic-widget
Oct 23, 2026
Merged

cpsievert merged 13 commits into
mainfrom
render-mosaic-widget

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Merge note (#285 and #294): both PRs add a [tool.uv] table to pyproject.toml (this one with constraint-dependencies, #294 with override-dependencies). Git merges them without a conflict into an invalid file (tomllib: Cannot declare ('tool', 'uv') twice), so uv, pip and hatch would all fail. Whoever merges second must fold the keys into a single [tool.uv] table and re-run uv lock.

Mosaic (mosaic_widget.MosaicWidget) builds dashboards of linked, cross-filtering views backed by DuckDB. With this PR you can drop one into a Shiny app with a dedicated, typed @render_mosaic_widget decorator, and use the user's brush selection from Python: reactive_read(widget, "params") re-runs your code whenever the user brushes, and widget.data() hands back the selected rows as a DuckDB relation to drive other outputs. An example app (examples/mosaic_widget) shows a scatterplot and histogram that cross-filter each other, with a Shiny summary table of the selection.

Part of #278.

Notes

  • Sizing behavior: the output fills its container (or takes an explicit height), but the plots inside keep the spec's width/height, because Mosaic specs have no responsive sizing. With an explicit height smaller than the plots, they overflow visibly rather than being clipped. Measured in a 500px fillable card, the output, its container and the widget element have identical boxes (480x466, and 480x316 after shrinking the card to 350px); output_widget(height="260px") gives exactly 260px. No sizing code was needed: the existing .mosaic-widget CSS already handles it.
  • MosaicWidget is already an ipywidget, so there is no coercion; the decorator types the return value (and .widget/.value) as MosaicWidget, and type checkers reject other return values.
  • Dependencies: mosaic-widget 0.33.0 declares narwhals>=2.8 but imports is_into_lazyframe, which only exists in narwhals 2.22+. A [tool.uv] constraint-dependencies entry (with a comment to remove it once upstream fixes its metadata) keeps the lock on a working narwhals, so uv.lock bumps narwhals 2.19.0 -> 2.27.1 for everything. The example's requirements.txt carries the same pin. Dependencies resolve for Python 3.10-3.14 (duckdb 1.5.6 / 2.0.0 and pyarrow wheels cover the range).

Verification

  • make py-check-format, make pyright, make test-unit (186 passed)
  • Playwright render_mosaic_widget: 5 passed (renders without console errors, fill + resize, explicit height, brush round trip to Python, re-render lifecycle with one view per output)
  • Mosaic unit tests also pass on a clean Python 3.10 environment
  • Example app smoke-run: brushing updates the Python-side selection count

🤖 Generated with Claude Code

Adds a typed @render_mosaic_widget renderer for mosaic_widget.MosaicWidget
(linked, DuckDB-backed views), with an example app, unit tests and
Playwright coverage for fill, explicit height, a brush round trip through
reactive_read(), and re-render cleanup.
…er_mosaic_widget

Mosaic answers browser queries in the server's DuckDB connection, so the
connection a MosaicWidget holds decides which data each Shiny session sees.
Passing a shared con= or a duckdb relation (which mosaic-widget registers on
DuckDB's process-wide default connection) lets one session's table replace
another's, and Mosaic's non-temporary mosaic.preagg_* cubes are reused across
sessions with matching queries. Nothing in the docstring hinted at this.

Also note that re-rendering keeps the old widget and its connection alive
(anywidget holds a strong reference to every file-based instance) and that
in-place `.widget.spec` updates avoid that.
…ion in DuckDB

Brushing the scatter plot rescaled the histogram's x-axis (170-230 became
186-200) underneath its own brush, so the example taught a crossfilter whose
brushes drift away from their data. Fixed domains keep both axes stable.

The selection is now a reactive.calc returning the lazy DuckDB relation, so
one brush move runs one query per output instead of pulling every selected
row into pandas twice; that is the pattern that scales to the large data
Mosaic is for.
# Conflicts:
#	shinywidgets/_render_widget.py
#	uv.lock
Resolve conflicts in CHANGELOG.md (keep both sides' renderer entries,
dropping install-command mentions to match main's style) and
shinywidgets/_render_widget.py (union of TYPE_CHECKING stubs).

Add the mosaic_widget optional-dependency extra, point the
@render_mosaic_widget docstring and README extras table at
pip install 'shinywidgets[mosaic_widget]', and use the extra in
examples/mosaic_widget/requirements.txt.
# Conflicts:
#	shinywidgets/_render_widget.py
# Conflicts:
#	shinywidgets/_render_widget.py
The sandbox/ scripts (probe.py, smoke.py, typecheck.py, app.py) and their
screenshots were local debugging helpers unrelated to the shipped renderer.
Ignore sandbox/ going forward so local scratch work can't be committed.

The files remain on disk locally.
@cpsievert
cpsievert marked this pull request as ready for review October 23, 2026 21:59
@cpsievert
cpsievert merged commit b617819 into main Oct 23, 2026
5 checks passed
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.

1 participant