Skip to content

Add generic plot widgets and demos#341

Merged
janosh merged 8 commits intomainfrom
add-plot-widgets-and-demos
Mar 1, 2026
Merged

Add generic plot widgets and demos#341
janosh merged 8 commits intomainfrom
add-plot-widgets-and-demos

Conversation

@janosh
Copy link
Owner

@janosh janosh commented Feb 28, 2026

Summary

  • add ScatterPlotWidget, BarPlotWidget, and HistogramWidget with shared plot-series normalization in pymatviz.widgets._normalize
  • wire new widgets through Python exports and anywidget frontend rendering, plus richer notebook/marimo/vscode widget demos
  • harden TrajectoryWidget schema handling for empty species lists and expand trajectory/widget regression tests

Test plan

  • pytest -q tests/widgets/test_new_widgets.py tests/widgets/test_trajectory_widget.py
  • pre-commit hooks on commit (ruff, ty, deno, eslint, etc.)

Summary by CodeRabbit

  • New Features

    • Added three new plot widgets (Scatter, Bar, Histogram) and a large interactive gallery: many demo widgets, MIME auto-rendering for atoms/phonons, local & remote trajectory examples, and fixture-based demo data.
  • Bug Fixes

    • Improved validation and clearer errors for plot series inputs and trajectory species.
  • Tests

    • Added coverage for new widgets, normalization, trajectory validation, asset-loading, and runtime behaviors.
  • Chores

    • Updated demos, pre-commit hook, and package exports.

- add ScatterPlotWidget, BarPlotWidget, and HistogramWidget with shared series normalization
- wire new widgets into exports and anywidget frontend mapping, plus notebook/marimo/vscode demos
- harden trajectory dict validation for empty species lists and extend widget tests
@janosh janosh added widgets feature Entirely new features, not improvements to existing ones examples New or improved usage examples testing Test all the things labels Feb 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three new plotting widgets (ScatterPlot, BarPlot, Histogram), JSON-safe plotting normalization helpers, TypeScript renderers, Marimo-aware MatterViz asset handling and caching, expanded example demos/notebooks and MIME examples, trajectory species-list validation, tests for new behavior, and a local pre-commit marimo check.

Changes

Cohort / File(s) Summary
Plot Widgets
pymatviz/widgets/scatter_plot.py, pymatviz/widgets/bar_plot.py, pymatviz/widgets/histogram.py
Introduce ScatterPlotWidget, BarPlotWidget, HistogramWidget as MatterVizWidget subclasses with traitlets, constructors, and input normalization.
Normalization Helpers
pymatviz/widgets/_normalize.py
Add _normalize_numeric_sequence, normalize_plot_json, and normalize_plot_series to validate and JSON-serialize plotting payloads with contextual errors.
Frontend Rendering (TypeScript)
pymatviz/widgets/web/anywidget.ts
Register new renderers (scatter_plot, bar_plot, histogram), add prop-key presets, adjust pick_props signature and mount plumbing, and add render helpers.
MatterViz assets & Marimo
pymatviz/widgets/matterviz.py
Add Marimo runtime detection and ESM URL resolution, class-level asset cache, Marimo-specific asset initialization, and instance _esm/_css handling plus asset-loading flows.
Trajectory validation
pymatviz/widgets/trajectory.py
Add TrajectoryWidget._validate_species_list and enforce non-empty species-list validation in structure completion and trajectory checks.
Examples & Demos
examples/widgets/jupyter_demo.ipynb, examples/widgets/marimo_demo.py, examples/widgets/vscode_interactive_demo.py
Expand demos with many new widget examples (plots, Bands/DOS fixtures, local/remote trajectories), MIME renderings (ASE/Phonopy), and explicit display/fixture usage.
Exports & Packaging
pymatviz/__init__.py, pymatviz/widgets/__init__.py
Export new plotting widgets at package and widgets-module level.
Tests
tests/widgets/test_new_widgets.py, tests/widgets/test_matterviz.py, tests/widgets/test_trajectory_widget.py
Add tests for normalize_plot_series, new plot widgets, MatterViz asset/Marimo behaviors, asset-cache scenarios, and trajectory species-list validation.
Pre-commit
.pre-commit-config.yaml
Add local marimo-check pre-commit hook targeting examples/widgets/marimo_demo.py.
Examples (notebook metadata)
examples/widgets/jupyter_demo.ipynb
Update kernel metadata (display name/version) and substantially expand notebook content and examples.

Sequence Diagram

sequenceDiagram
    participant User
    participant PyWidget as ScatterPlotWidget\n(Python)
    participant Normalize as normalize_plot_series\n& normalize_plot_json
    participant FrontEnd as anywidget.ts\n(TypeScript Renderer)
    participant UI as ScatterPlot Component\n(matterviz)

    User->>PyWidget: Instantiate widget with series & options
    PyWidget->>Normalize: Validate & normalize series, axes, styles
    Normalize-->>PyWidget: Normalized JSON-safe payload
    PyWidget->>FrontEnd: Sync widget_type & props over comm
    FrontEnd->>FrontEnd: Select scatter_plot renderer
    FrontEnd->>UI: Mount ScatterPlot with normalized props
    UI-->>User: Render interactive plot
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

notebook

Poem

🐰 I hopped through code with curious paws,
Bars and scatters, histograms and laws.
Widgets bloom, demos gleam so bright,
MIME renders dance in morning light.
Hop, render, munch—cheers to new sight! 🎨📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add generic plot widgets and demos' accurately and concisely describes the main changes: introducing three new plot widgets (ScatterPlot, Bar, Histogram) and expanding associated demo notebooks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-plot-widgets-and-demos

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/widgets/jupyter_demo.ipynb (1)

488-492: ⚠️ Potential issue | 🟡 Minor

Use explicit display(...) for MIME demo objects in this linted notebook.

These expression statements are flagged by Ruff (B018). Wrapping them in display(...) preserves notebook behavior and avoids lint noise.

🔧 Suggested fix
+from IPython.display import display
 ...
-ase_atoms
+display(ase_atoms)
 ...
-ase_molecule
+display(ase_molecule)

Also applies to: 510-514

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/widgets/jupyter_demo.ipynb` around lines 488 - 492, Replace bare
expression statements that rely on Jupyter MIME rendering (the variable
ase_atoms) with explicit display() calls: locate the expression statements that
simply reference ase_atoms (and the similar occurrences at lines noted in the
comment) and wrap them in display(ase_atoms). Update any imports or ensure from
IPython.display import display is present in the notebook if not already; adjust
the statements in the cells where ase_atoms is created/printed (references:
ase_atoms) to use display(ase_atoms) to satisfy the linter and preserve notebook
output.
🧹 Nitpick comments (3)
examples/widgets/vscode_interactive_demo.py (1)

29-30: Prefer instance .display() over MatterVizWidget.display(widget) and drop the internal import.

The current class-style invocation works, but instance calls are clearer and avoid coupling this example to pymatviz.widgets.matterviz.

♻️ Suggested refactor
-from pymatviz.widgets.matterviz import MatterVizWidget
...
-MatterVizWidget.display(scatter_plot_widget)
+scatter_plot_widget.display()
...
-MatterVizWidget.display(bar_plot_widget)
+bar_plot_widget.display()
...
-MatterVizWidget.display(histogram_widget)
+histogram_widget.display()

Run this read-only check; expected result after refactor: no matches.

#!/bin/bash
rg -n --type=py 'from pymatviz\.widgets\.matterviz import MatterVizWidget|MatterVizWidget\.display\(' examples/widgets

Also applies to: 198-220, 248-248

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/widgets/vscode_interactive_demo.py` around lines 29 - 30, Replace
the class-style invocation and internal import for MatterVizWidget with an
instance call: remove the line importing MatterVizWidget from
pymatviz.widgets.matterviz and any use of MatterVizWidget.display(widget) and
instead construct the widget instance (e.g., widget = MatterVizWidget(...))
where used and call widget.display(); update all occurrences referenced (the
current import at top and usages around lines 29 and the other ranges 198-220
and 248) so the example uses the instance method and no longer imports the
internal module.
pymatviz/widgets/bar_plot.py (1)

13-82: Consider extending state_fields for to_dict() completeness.

The base MatterVizWidget.to_dict() method returns only fields listed in state_fields (currently "widget_type", "style", "show_controls"). If to_dict() should serialize widget-specific fields like series, orientation, mode, etc., you'll need to override state_fields in this class.

If to_dict() isn't intended for serializing widget-specific state, this is fine as-is.

💡 Optional: extend state_fields if needed
 class BarPlotWidget(MatterVizWidget):
     """MatterViz widget wrapper for grouped/stacked/overlay bar plots."""

+    state_fields: tuple[str, ...] = (
+        "widget_type", "style", "show_controls",
+        "series", "orientation", "mode", "x_axis", "y_axis", "y2_axis",
+        "display", "legend", "bar", "line", "ref_lines", "controls",
+    )
+
     series = tl.List(allow_none=True).tag(sync=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pymatviz/widgets/bar_plot.py` around lines 13 - 82, The BarPlotWidget
currently doesn't include its widget-specific traits in serialization because
MatterVizWidget.to_dict() only exports names in state_fields; override the class
attribute state_fields in BarPlotWidget (e.g., set state_fields =
super().state_fields + ("series", "orientation", "mode", "x_axis", "y_axis",
"y2_axis", "display", "legend", "bar", "line", "ref_lines", "controls")) so
to_dict() will include those trait names; ensure the names you add exactly match
the trait names (series, orientation, mode, x_axis, y_axis, y2_axis, display,
legend, bar, line, ref_lines, controls) and keep it a sequence type the base
class expects.
examples/widgets/marimo_demo.py (1)

202-224: Minor: Avoid redundant np.linspace calls.

np.linspace(0, 6.0, 60) is computed twice per series (once for x, once in np.sin/np.cos). Consider storing it in a variable for clarity and slight efficiency.

♻️ Optional cleanup
 `@app.cell`
 def _(np, pmv):
+    x_vals = np.linspace(0, 6.0, 60).tolist()
     scatter_series = [
         {
             "label": "sin(x)",
-            "x": np.linspace(0, 6.0, 60).tolist(),
-            "y": np.sin(np.linspace(0, 6.0, 60)).tolist(),
+            "x": x_vals,
+            "y": np.sin(np.linspace(0, 6.0, 60)).tolist(),
         },
         {
             "label": "cos(x)",
-            "x": np.linspace(0, 6.0, 60).tolist(),
-            "y": np.cos(np.linspace(0, 6.0, 60)).tolist(),
+            "x": x_vals,
+            "y": np.cos(np.linspace(0, 6.0, 60)).tolist(),
             "y_axis": "y2",
         },
     ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/widgets/marimo_demo.py` around lines 202 - 224, Compute
np.linspace(0, 6.0, 60) once and reuse it instead of calling it multiple times:
create a variable (e.g., x_vals) before building scatter_series, use x_vals for
each series' "x" and pass np.sin(x_vals) / np.cos(x_vals) for the "y" values;
update the code around scatter_series and the subsequent pmv.ScatterPlotWidget
call to reference that variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@examples/widgets/jupyter_demo.ipynb`:
- Around line 488-492: Replace bare expression statements that rely on Jupyter
MIME rendering (the variable ase_atoms) with explicit display() calls: locate
the expression statements that simply reference ase_atoms (and the similar
occurrences at lines noted in the comment) and wrap them in display(ase_atoms).
Update any imports or ensure from IPython.display import display is present in
the notebook if not already; adjust the statements in the cells where ase_atoms
is created/printed (references: ase_atoms) to use display(ase_atoms) to satisfy
the linter and preserve notebook output.

---

Nitpick comments:
In `@examples/widgets/marimo_demo.py`:
- Around line 202-224: Compute np.linspace(0, 6.0, 60) once and reuse it instead
of calling it multiple times: create a variable (e.g., x_vals) before building
scatter_series, use x_vals for each series' "x" and pass np.sin(x_vals) /
np.cos(x_vals) for the "y" values; update the code around scatter_series and the
subsequent pmv.ScatterPlotWidget call to reference that variable.

In `@examples/widgets/vscode_interactive_demo.py`:
- Around line 29-30: Replace the class-style invocation and internal import for
MatterVizWidget with an instance call: remove the line importing MatterVizWidget
from pymatviz.widgets.matterviz and any use of MatterVizWidget.display(widget)
and instead construct the widget instance (e.g., widget = MatterVizWidget(...))
where used and call widget.display(); update all occurrences referenced (the
current import at top and usages around lines 29 and the other ranges 198-220
and 248) so the example uses the instance method and no longer imports the
internal module.

In `@pymatviz/widgets/bar_plot.py`:
- Around line 13-82: The BarPlotWidget currently doesn't include its
widget-specific traits in serialization because MatterVizWidget.to_dict() only
exports names in state_fields; override the class attribute state_fields in
BarPlotWidget (e.g., set state_fields = super().state_fields + ("series",
"orientation", "mode", "x_axis", "y_axis", "y2_axis", "display", "legend",
"bar", "line", "ref_lines", "controls")) so to_dict() will include those trait
names; ensure the names you add exactly match the trait names (series,
orientation, mode, x_axis, y_axis, y2_axis, display, legend, bar, line,
ref_lines, controls) and keep it a sequence type the base class expects.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 485ed72 and 55c2ec2.

📒 Files selected for processing (13)
  • examples/widgets/jupyter_demo.ipynb
  • examples/widgets/marimo_demo.py
  • examples/widgets/vscode_interactive_demo.py
  • pymatviz/__init__.py
  • pymatviz/widgets/__init__.py
  • pymatviz/widgets/_normalize.py
  • pymatviz/widgets/bar_plot.py
  • pymatviz/widgets/histogram.py
  • pymatviz/widgets/scatter_plot.py
  • pymatviz/widgets/trajectory.py
  • pymatviz/widgets/web/anywidget.ts
  • tests/widgets/test_new_widgets.py
  • tests/widgets/test_trajectory_widget.py

Marimo serializes cell outputs independently with a ~10 MB limit,
causing widgets embedding ~10 MB of inline JS to fail. anywidget
also only treats _esm values starting with http(s):// as URLs.

- Serve ESM via marimo's virtual-file system with absolute URL
  resolution so anywidget imports it as a URL (not inline source)
- Keep CSS inline (~166 KB) to avoid stylesheet loading edge cases
- Add _in_marimo_runtime, _marimo_esm_url, _init_marimo_assets with
  27 tests including mutation-verified coverage
- Fix marimo demo cell dependency wiring (_struct, _phonon_doc,
  _band_data, _dos_data), broken markdown cells, and ruff noqa
- Add marimo check --strict pre-commit hook
- Fix RUF005 in bar_plot.py, TRY300 in matterviz.py
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
examples/widgets/vscode_interactive_demo.py (1)

198-248: Minor inconsistency in display patterns (optional).

Some widgets use display(widget) from IPython while others use widget.display(). Both work, but using widget.display() consistently would align with the rest of the file and enable method chaining.

♻️ Optional: Consistent display() usage
-display(scatter_plot_widget)
+scatter_plot_widget.display()
-display(bar_plot_widget)
+bar_plot_widget.display()
-display(histogram_widget)
+histogram_widget.display()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/widgets/vscode_interactive_demo.py` around lines 198 - 248, Change
the inconsistent IPython display calls to use the widget instance display method
for consistency and chaining: replace display(scatter_plot_widget) with
scatter_plot_widget.display(), replace display(bar_plot_widget) with
bar_plot_widget.display(), and replace display(histogram_widget) with
histogram_widget.display(); keep all widget construction (e.g.,
scatter_plot_widget, bar_plot_widget, histogram_widget and their argument names
like series, mode, style) unchanged and ensure you import or rely on the
widget.display() method already provided by the pmv widget classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/widgets/vscode_interactive_demo.py`:
- Around line 198-248: Change the inconsistent IPython display calls to use the
widget instance display method for consistency and chaining: replace
display(scatter_plot_widget) with scatter_plot_widget.display(), replace
display(bar_plot_widget) with bar_plot_widget.display(), and replace
display(histogram_widget) with histogram_widget.display(); keep all widget
construction (e.g., scatter_plot_widget, bar_plot_widget, histogram_widget and
their argument names like series, mode, style) unchanged and ensure you import
or rely on the widget.display() method already provided by the pmv widget
classes.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55c2ec2 and 35f2982.

📒 Files selected for processing (7)
  • .pre-commit-config.yaml
  • examples/widgets/jupyter_demo.ipynb
  • examples/widgets/marimo_demo.py
  • examples/widgets/vscode_interactive_demo.py
  • pymatviz/widgets/bar_plot.py
  • pymatviz/widgets/matterviz.py
  • tests/widgets/test_matterviz.py

Replace synthetic band/DOS demo payloads with phonon test fixtures and add concise explanatory headings across notebook, marimo, and VSCode interactive demos.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
examples/widgets/jupyter_demo.ipynb (1)

58-74: Avoid committing transient widget outputs in this notebook.

This output block includes runtime-only values (model_id, object memory repr) that will churn on every re-run and make diffs noisy. Please clear widget outputs before committing notebook changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/widgets/jupyter_demo.ipynb` around lines 58 - 74, The notebook
contains runtime-only widget outputs (e.g., the
application/vnd.jupyter.widget-view+json block with keys like "model_id" and the
printed "<pymatviz.widgets.convex_hull.ConvexHullWidget...>"), which churn
diffs; remove/clear these transient outputs before committing by clearing widget
outputs for this cell (or run a notebook-cleaning step such as
nbstripout/nbconvert to strip widget state), ensuring the ConvexHullWidget
output and its model_id/version fields are removed from the saved .ipynb.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/widgets/jupyter_demo.ipynb`:
- Around line 58-74: The notebook contains runtime-only widget outputs (e.g.,
the application/vnd.jupyter.widget-view+json block with keys like "model_id" and
the printed "<pymatviz.widgets.convex_hull.ConvexHullWidget...>"), which churn
diffs; remove/clear these transient outputs before committing by clearing widget
outputs for this cell (or run a notebook-cleaning step such as
nbstripout/nbconvert to strip widget state), ensuring the ConvexHullWidget
output and its model_id/version fields are removed from the saved .ipynb.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35f2982 and db6012a.

📒 Files selected for processing (3)
  • examples/widgets/jupyter_demo.ipynb
  • examples/widgets/marimo_demo.py
  • examples/widgets/vscode_interactive_demo.py

- Skip js() virtual file call when virtual_files_supported=False to
  prevent ~14 MB base64 data URL allocations that crash the kernel
- Fall back to relative ./@file/ URL when request context is unavailable
- Set resolved ESM URL on class (not per-instance) to avoid N copies
- Use display() from IPython for plot widgets that shadow the method
- Document VS Code extension limitation in MatterVizWidget docstring
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
tests/widgets/test_matterviz.py (2)

342-374: Consider adding test for virtual_files_supported=False.

The tests cover the case where marimo is not installed and various fallback scenarios, but there's no explicit test for when get_context().virtual_files_supported returns False (the VS Code extension case mentioned in the implementation). This is an important edge case that triggers the early return None at line 47 of matterviz.py.

🧪 Suggested test case
def test_marimo_esm_url_vfiles_not_supported() -> None:
    """Returns None when virtual files aren't supported (VS Code extension)."""
    mock_ctx = MagicMock()
    mock_ctx.virtual_files_supported = False
    mock_ctx_mod = MagicMock()
    mock_ctx_mod.get_context.return_value = mock_ctx
    
    mods = {
        "marimo._output.data.data": MagicMock(),
        "marimo._runtime.context": mock_ctx_mod,
    }
    with patch.dict("sys.modules", mods):
        assert _marimo_esm_url("code") is None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/widgets/test_matterviz.py` around lines 342 - 374, Add a unit test that
covers the case where marimo reports virtual files are not supported: mock the
marimo._runtime.context module so get_context() returns an object with
virtual_files_supported = False, include a marimo._output.data.data MagicMock as
well, patch sys.modules with these mods and assert that calling
_marimo_esm_url("code") returns None; this exercise targets the early return in
the _marimo_esm_url function when get_context().virtual_files_supported is
False.

59-71: Potential incomplete cleanup if attributes are newly created during test.

If _esm or _css were not set before the test (saved_esm is None) but get set during the test, lines 68-71 won't clean them up. This could leak state to subsequent tests.

Consider unconditionally deleting class-level _esm/_css attributes at teardown and only restoring if they existed:

🔧 Proposed fix for complete cleanup
 `@pytest.fixture`
 def _clean_asset_cache() -> Generator[None]:
     """Save and restore MatterVizWidget class-level asset state around a test."""
     saved_esm = getattr(matterviz.MatterVizWidget, "_esm", None)
     saved_css = getattr(matterviz.MatterVizWidget, "_css", None)
+    had_esm = hasattr(matterviz.MatterVizWidget, "_esm")
+    had_css = hasattr(matterviz.MatterVizWidget, "_css")
     saved_cache = matterviz.MatterVizWidget._asset_cache.copy()
     matterviz.MatterVizWidget._asset_cache.clear()
     yield
     matterviz.MatterVizWidget._asset_cache = saved_cache
-    if saved_esm is not None:
+    # Clean up any newly created attributes first
+    if hasattr(matterviz.MatterVizWidget, "_esm"):
+        delattr(matterviz.MatterVizWidget, "_esm")
+    if hasattr(matterviz.MatterVizWidget, "_css"):
+        delattr(matterviz.MatterVizWidget, "_css")
+    # Restore original attributes if they existed
+    if had_esm:
         matterviz.MatterVizWidget._esm = saved_esm
-    if saved_css is not None:
+    if had_css:
         matterviz.MatterVizWidget._css = saved_css
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/widgets/test_matterviz.py` around lines 59 - 71, The fixture
_clean_asset_cache leaves class-level attributes _esm and _css if they are
created during a test because it only restores them when saved_esm/saved_css are
not None; update the teardown to restore previous values when
saved_esm/saved_css exist and otherwise remove any attributes added by the test
(use delattr on matterviz.MatterVizWidget for "_esm" and "_css" when saved_* is
None), and ensure _asset_cache is restored by assigning saved_cache back;
implement this logic in the _clean_asset_cache fixture so newly created
_esm/_css are cleaned up while existing ones are restored.
pymatviz/widgets/matterviz.py (1)

204-223: Redundant CSS assignment and class-level state mutation concern.

Line 223 assigns css_text to self.__class__._css, but this is already set by _set_class_assets() on line 216. The assignment is a no-op.

Additionally, mutating _esm at the class level (line 221) means subsequent non-marimo initializations (or subclasses) would inherit the URL string instead of the ESM source. If MatterVizWidget is subclassed, the subclass would share the same _asset_cache and could inherit a URL string where ESM content is expected.

Consider whether these semantics are intentional for shared widget instances.

🔧 Proposed cleanup for redundant assignment
         esm_url = _marimo_esm_url(esm_text)
         if esm_url:
             self.__class__._esm = esm_url
-            # CSS inline on class — shared across instances
-            self.__class__._css = css_text
+            # CSS already set by _set_class_assets() — no action needed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pymatviz/widgets/matterviz.py` around lines 204 - 223, The
_init_marimo_assets method redundantly reassigns class _css (no-op after
_set_class_assets()) and dangerously mutates class _esm with a URL string;
instead remove the redundant self.__class__._css = css_text line and avoid
writing back to the class _esm. Keep class attribute _esm as the original source
set by _set_class_assets(), and store the marimo-serving URL on the instance
(e.g. self._esm_url or another instance attribute) so only this widget instance
uses the URL; update any code that consumes the ESM to prefer the instance
attribute (falling back to self.__class__._esm) and leave _set_class_assets,
_esm and _css unchanged at class scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pymatviz/widgets/matterviz.py`:
- Around line 204-223: The _init_marimo_assets method redundantly reassigns
class _css (no-op after _set_class_assets()) and dangerously mutates class _esm
with a URL string; instead remove the redundant self.__class__._css = css_text
line and avoid writing back to the class _esm. Keep class attribute _esm as the
original source set by _set_class_assets(), and store the marimo-serving URL on
the instance (e.g. self._esm_url or another instance attribute) so only this
widget instance uses the URL; update any code that consumes the ESM to prefer
the instance attribute (falling back to self.__class__._esm) and leave
_set_class_assets, _esm and _css unchanged at class scope.

In `@tests/widgets/test_matterviz.py`:
- Around line 342-374: Add a unit test that covers the case where marimo reports
virtual files are not supported: mock the marimo._runtime.context module so
get_context() returns an object with virtual_files_supported = False, include a
marimo._output.data.data MagicMock as well, patch sys.modules with these mods
and assert that calling _marimo_esm_url("code") returns None; this exercise
targets the early return in the _marimo_esm_url function when
get_context().virtual_files_supported is False.
- Around line 59-71: The fixture _clean_asset_cache leaves class-level
attributes _esm and _css if they are created during a test because it only
restores them when saved_esm/saved_css are not None; update the teardown to
restore previous values when saved_esm/saved_css exist and otherwise remove any
attributes added by the test (use delattr on matterviz.MatterVizWidget for
"_esm" and "_css" when saved_* is None), and ensure _asset_cache is restored by
assigning saved_cache back; implement this logic in the _clean_asset_cache
fixture so newly created _esm/_css are cleaned up while existing ones are
restored.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db6012a and 95d0acf.

📒 Files selected for processing (2)
  • pymatviz/widgets/matterviz.py
  • tests/widgets/test_matterviz.py

Use dedicated class-level marimo URL cache fields instead of overloading the generic asset tuple cache, and keep test cache cleanup aligned with the new cache state.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/widgets/test_matterviz.py (1)

28-32: Consider using a custom exception class for cleaner import blocking.

The static analysis tool flags TRY003 for the long message in the exception. While this is a minor issue in test code, you could simplify:

♻️ Optional refactor
 def _block_marimo(name: str, *args: Any, **kwargs: Any) -> types.ModuleType:
     """Import hook that raises ImportError for any marimo submodule."""
     if name.startswith("marimo"):
-        raise ImportError(f"No module named '{name}'")
+        raise ImportError(name)
     return _real_import(name, *args, **kwargs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/widgets/test_matterviz.py` around lines 28 - 32, The test import hook
_block_marimo currently raises ImportError with a long formatted message which
triggers TRY003; create a small custom exception (e.g., MarimoImportBlocked) and
raise that from _block_marimo when name.startswith("marimo") (or replace the
long ImportError message with a minimal message) so static analysis stops
flagging the long string—update any test code that expects ImportError to catch
the new custom exception or adjust assertions accordingly.
pymatviz/widgets/matterviz.py (1)

164-168: Class-level Marimo URL cache is never reset across different ESM content.

The _marimo_esm_cache_ready flag prevents re-computing the URL even if _esm content changes (e.g., after a version override in a prior widget). If a first widget is created without version_override and caches the URL, then subsequent widgets with version_override will skip the Marimo path entirely (due to the if version_override branch), which is fine. However, if the class-level _esm content were ever to change between calls to _init_marimo_assets, the stale cached URL would be used.

This is likely acceptable given the current initialization flow (version_override bypasses Marimo path), but consider adding a comment clarifying this assumption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pymatviz/widgets/matterviz.py` around lines 164 - 168, The class-level Marimo
URL cache (_marimo_esm_cached_url and _marimo_esm_cache_ready) can become stale
if the class-level _esm content changes between calls to _init_marimo_assets;
either reset the cache when _esm changes or at minimum document the assumption
that _esm never changes after first initialization. Update _init_marimo_assets
(and any place that may modify _esm or call it) to compare the current _esm to
the value used for caching and clear
_marimo_esm_cached_url/_marimo_esm_cache_ready when they differ, or add a
concise comment near the class variables and in _init_marimo_assets referencing
version_override and the single-initialization assumption to make the behavior
explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pymatviz/widgets/matterviz.py`:
- Around line 164-168: The class-level Marimo URL cache (_marimo_esm_cached_url
and _marimo_esm_cache_ready) can become stale if the class-level _esm content
changes between calls to _init_marimo_assets; either reset the cache when _esm
changes or at minimum document the assumption that _esm never changes after
first initialization. Update _init_marimo_assets (and any place that may modify
_esm or call it) to compare the current _esm to the value used for caching and
clear _marimo_esm_cached_url/_marimo_esm_cache_ready when they differ, or add a
concise comment near the class variables and in _init_marimo_assets referencing
version_override and the single-initialization assumption to make the behavior
explicit.

In `@tests/widgets/test_matterviz.py`:
- Around line 28-32: The test import hook _block_marimo currently raises
ImportError with a long formatted message which triggers TRY003; create a small
custom exception (e.g., MarimoImportBlocked) and raise that from _block_marimo
when name.startswith("marimo") (or replace the long ImportError message with a
minimal message) so static analysis stops flagging the long string—update any
test code that expects ImportError to catch the new custom exception or adjust
assertions accordingly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95d0acf and 4f0e0b3.

📒 Files selected for processing (2)
  • pymatviz/widgets/matterviz.py
  • tests/widgets/test_matterviz.py

Replace manual state_fields tuples with automatic discovery via
self.traits(sync=True) in MatterVizWidget.to_dict(). This fixes
serialization for ScatterPlotWidget and HistogramWidget which were
missing their state_fields definitions, and eliminates the need for
every subclass to maintain a redundant field list.

Also simplify production code (DRY numpy conversion in normalize_plot_json,
loop-ify lattice params, remove narrating comments in trajectory.py) and
split test_new_widgets.py into test_normalize.py, test_widget_construction.py,
and test_mime.py organized by domain rather than chronology.
- Rename MatterVizWidget.display() to show() to avoid shadowing the
  'display' traitlet on ScatterPlot/BarPlot/HistogramWidget which made
  widget.display() raise TypeError: 'dict' object is not callable
- Add configure_assets(version=, esm_src=, css_src=) for global asset
  override via version tags, HTTP URLs, or local file paths
- Fix marimo demo: cells now properly return and declare dependencies
  for _struct, _band_data, _phonon_doc, _dos_data
- Simplify matterviz.py: flatten _marimo_esm_url resolution, trim docstrings,
  merge isinstance checks in normalize_plot_json
Runs all three widget demos (vscode, marimo, jupyter) in a single
consolidated step to catch runtime errors in demo scripts.
@janosh janosh merged commit a3cab5e into main Mar 1, 2026
47 of 48 checks passed
@janosh janosh deleted the add-plot-widgets-and-demos branch March 1, 2026 15:33
janosh added a commit that referenced this pull request Mar 1, 2026
* Add generic plot widgets and demos

- add ScatterPlotWidget, BarPlotWidget, and HistogramWidget with shared series normalization
- wire new widgets into exports and anywidget frontend mapping, plus notebook/marimo/vscode demos
- harden trajectory dict validation for empty species lists and extend widget tests

* Fix marimo widget rendering and improve demo notebooks

Marimo serializes cell outputs independently with a ~10 MB limit,
causing widgets embedding ~10 MB of inline JS to fail. anywidget
also only treats _esm values starting with http(s):// as URLs.

- Serve ESM via marimo's virtual-file system with absolute URL
  resolution so anywidget imports it as a URL (not inline source)
- Keep CSS inline (~166 KB) to avoid stylesheet loading edge cases
- Add _in_marimo_runtime, _marimo_esm_url, _init_marimo_assets with
  27 tests including mutation-verified coverage
- Fix marimo demo cell dependency wiring (_struct, _phonon_doc,
  _band_data, _dos_data), broken markdown cells, and ruff noqa
- Add marimo check --strict pre-commit hook
- Fix RUF005 in bar_plot.py, TRY300 in matterviz.py

* Update widget demos with realistic fixture data and clearer narratives

Replace synthetic band/DOS demo payloads with phonon test fixtures and add concise explanatory headings across notebook, marimo, and VSCode interactive demos.

* Guard against marimo VS Code extension kernel crash

- Skip js() virtual file call when virtual_files_supported=False to
  prevent ~14 MB base64 data URL allocations that crash the kernel
- Fall back to relative ./@file/ URL when request context is unavailable
- Set resolved ESM URL on class (not per-instance) to avoid N copies
- Use display() from IPython for plot widgets that shadow the method
- Document VS Code extension limitation in MatterVizWidget docstring

* Simplify marimo ESM caching in MatterVizWidget

Use dedicated class-level marimo URL cache fields instead of overloading the generic asset tuple cache, and keep test cache cleanup aligned with the new cache state.

* Auto-discover to_dict fields from synced traitlets and reorganize tests

Replace manual state_fields tuples with automatic discovery via
self.traits(sync=True) in MatterVizWidget.to_dict(). This fixes
serialization for ScatterPlotWidget and HistogramWidget which were
missing their state_fields definitions, and eliminates the need for
every subclass to maintain a redundant field list.

Also simplify production code (DRY numpy conversion in normalize_plot_json,
loop-ify lattice params, remove narrating comments in trajectory.py) and
split test_new_widgets.py into test_normalize.py, test_widget_construction.py,
and test_mime.py organized by domain rather than chronology.

* Rename display() to show(), add configure_assets(), fix marimo cell deps

- Rename MatterVizWidget.display() to show() to avoid shadowing the
  'display' traitlet on ScatterPlot/BarPlot/HistogramWidget which made
  widget.display() raise TypeError: 'dict' object is not callable
- Add configure_assets(version=, esm_src=, css_src=) for global asset
  override via version tags, HTTP URLs, or local file paths
- Fix marimo demo: cells now properly return and declare dependencies
  for _struct, _band_data, _phonon_doc, _dos_data
- Simplify matterviz.py: flatten _marimo_esm_url resolution, trim docstrings,
  merge isinstance checks in normalize_plot_json

* Add CI job to execute widget demos

Runs all three widget demos (vscode, marimo, jupyter) in a single
consolidated step to catch runtime errors in demo scripts.
janosh added a commit that referenced this pull request Mar 2, 2026
* Add generic plot widgets and demos

- add ScatterPlotWidget, BarPlotWidget, and HistogramWidget with shared series normalization
- wire new widgets into exports and anywidget frontend mapping, plus notebook/marimo/vscode demos
- harden trajectory dict validation for empty species lists and extend widget tests

* Fix marimo widget rendering and improve demo notebooks

Marimo serializes cell outputs independently with a ~10 MB limit,
causing widgets embedding ~10 MB of inline JS to fail. anywidget
also only treats _esm values starting with http(s):// as URLs.

- Serve ESM via marimo's virtual-file system with absolute URL
  resolution so anywidget imports it as a URL (not inline source)
- Keep CSS inline (~166 KB) to avoid stylesheet loading edge cases
- Add _in_marimo_runtime, _marimo_esm_url, _init_marimo_assets with
  27 tests including mutation-verified coverage
- Fix marimo demo cell dependency wiring (_struct, _phonon_doc,
  _band_data, _dos_data), broken markdown cells, and ruff noqa
- Add marimo check --strict pre-commit hook
- Fix RUF005 in bar_plot.py, TRY300 in matterviz.py

* Update widget demos with realistic fixture data and clearer narratives

Replace synthetic band/DOS demo payloads with phonon test fixtures and add concise explanatory headings across notebook, marimo, and VSCode interactive demos.

* Guard against marimo VS Code extension kernel crash

- Skip js() virtual file call when virtual_files_supported=False to
  prevent ~14 MB base64 data URL allocations that crash the kernel
- Fall back to relative ./@file/ URL when request context is unavailable
- Set resolved ESM URL on class (not per-instance) to avoid N copies
- Use display() from IPython for plot widgets that shadow the method
- Document VS Code extension limitation in MatterVizWidget docstring

* Simplify marimo ESM caching in MatterVizWidget

Use dedicated class-level marimo URL cache fields instead of overloading the generic asset tuple cache, and keep test cache cleanup aligned with the new cache state.

* Auto-discover to_dict fields from synced traitlets and reorganize tests

Replace manual state_fields tuples with automatic discovery via
self.traits(sync=True) in MatterVizWidget.to_dict(). This fixes
serialization for ScatterPlotWidget and HistogramWidget which were
missing their state_fields definitions, and eliminates the need for
every subclass to maintain a redundant field list.

Also simplify production code (DRY numpy conversion in normalize_plot_json,
loop-ify lattice params, remove narrating comments in trajectory.py) and
split test_new_widgets.py into test_normalize.py, test_widget_construction.py,
and test_mime.py organized by domain rather than chronology.

* Rename display() to show(), add configure_assets(), fix marimo cell deps

- Rename MatterVizWidget.display() to show() to avoid shadowing the
  'display' traitlet on ScatterPlot/BarPlot/HistogramWidget which made
  widget.display() raise TypeError: 'dict' object is not callable
- Add configure_assets(version=, esm_src=, css_src=) for global asset
  override via version tags, HTTP URLs, or local file paths
- Fix marimo demo: cells now properly return and declare dependencies
  for _struct, _band_data, _phonon_doc, _dos_data
- Simplify matterviz.py: flatten _marimo_esm_url resolution, trim docstrings,
  merge isinstance checks in normalize_plot_json

* Add CI job to execute widget demos

Runs all three widget demos (vscode, marimo, jupyter) in a single
consolidated step to catch runtime errors in demo scripts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

examples New or improved usage examples feature Entirely new features, not improvements to existing ones testing Test all the things widgets

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant