Skip to content

Enable 3D visualizations with CytoDataFrame#186

Merged
d33bs merged 44 commits intocytomining:mainfrom
d33bs:3d-views
Feb 13, 2026
Merged

Enable 3D visualizations with CytoDataFrame#186
d33bs merged 44 commits intocytomining:mainfrom
d33bs:3d-views

Conversation

@d33bs
Copy link
Member

@d33bs d33bs commented Feb 6, 2026

Description

This PR enables 3D visualizations with CytoDataFrame. We use trame and pyvista to assist with this to provide interactivity within Jupyter notebooks for 3D objects. Existing 2D functionality remains rendered through ipywidgets.

Note: this only addresses raw image viewing. We'll follow up with labels (masks, outlines) in separate work.

Closes #175

What kind of change(s) are included?

  • Documentation (changes docs or other related content)
  • Bug fix (fixes an issue).
  • Enhancement (adds functionality).
  • Breaking change (these changes would cause existing functionality to not work as expected).

Checklist

Please ensure that all boxes are checked before indicating that this pull request is ready for review.

  • I have read and followed the CONTRIBUTING.md guidelines.
  • I have searched for existing content to ensure this is not a duplicate.
  • I have performed a self-review of these additions (including spelling, grammar, and related).
  • These changes pass all pre-commit checks.
  • I have added comments to my code to help provide understanding
  • I have added a test which covers the code changes found within this PR
  • I have deleted all non-relevant text in this pull request template.

Summary by CodeRabbit

  • New Features

    • Interactive 3D volume detection and rendering in notebooks with per-row extraction, caching, snapshots, and backend fallbacks.
  • Documentation

    • Added 3D example/tutorial, README notes on 3D-aware rendering and display options, testing output note, and two new citation entries.
  • Tests

    • Extensive new and expanded tests for 3D volumes, rendering fallbacks, image utilities, HTML outputs, and integration fallbacks.
  • Chores

    • Project config reorganized and pre-commit tooling updated; added a reproducible 3D processing pipeline with containerized run script.

@d33bs d33bs marked this pull request as ready for review February 7, 2026 18:46
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 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 comprehensive 3D volume support: OME‑Arrow/TIFF volume extraction, per-row cropping and caching, vtk.js/PyVista/Trame rendering with interactive and static snapshot fallbacks, display options and examples, CellProfiler tutorial assets, tests, and related configuration/tooling updates.

Changes

Cohort / File(s) Summary
Configuration & Tooling
\.pre-commit-config\.yaml, pyproject\.toml
Updated pre-commit hooks/revs and args, reorganized pyproject into grouped dependency sections, added runtime deps (pyvista, trame, trame-vtk, trame-vuetify, nest-asyncio), and moved/adjusted tooling/task definitions.
Core 3D Rendering
src/cytodataframe/frame\.py, src/cytodataframe/volume\.py
Introduces 3D detection, OME‑Arrow decoding, volume extraction, vtk.js HTML builders, PyVista viewer construction, Trame/ipywidgets integration, snapshot generation, per-row caching, new constants and many public methods. High-density functional changes.
Documentation & Examples
README\.md, CITATION\.cff, docs/src/contributing\.md, docs/src/examples/cytodataframe_at_a_glance\.py
Documentation and example updates: README notes for 3D-aware display options, two new citation entries, small contributing note on pytest coverage, and a 3D example block/metadata update.
Tutorial Assets
CP tutorial
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/*
Adds a CellProfiler 3D pipeline (3d-nuclei-profiling.cppipe), Dockerfile, and run.sh to support a 3D nuclei profiling tutorial and reproducible runs (amd64-focused Dockerfile).
Tests
tests/test_frame\.py, tests/test_volume\.py, tests/test_image\.py, tests/test_project_integration\.py
Large new and expanded tests for 3D volume extraction, HTML/vtk.js output, PyVista/Trame fallbacks, image outline/scale-bar utilities, conditional imports, and extensive mocking/monkeypatching.
Misc test data
tests/data/...
Added test assets and pipeline files used by the new tutorial examples and tests (including generated CSV exclusion in pre-commit config).

Sequence Diagram(s)

sequenceDiagram
    participant NB as Notebook
    participant CDF as CytoDataFrame
    participant VOL as VolumeModule
    participant PV as PyVista
    participant HTML as HTML/VTK.js

    NB->>CDF: request _repr_html_()
    CDF->>CDF: _find_3d_columns_for_display()
    CDF->>CDF: check display_options (auto_trame_for_3d)

    alt 3D detected & interactive allowed
        CDF->>CDF: _get_3d_volume_from_cell(row,column)
        CDF->>VOL: extract_volume_from_ome_arrow() or load file
        VOL-->>CDF: (volume, dims)
        CDF->>PV: _build_pyvista_viewer(volume,dims,...)
        PV-->>CDF: viewer object / snapshot
        CDF->>HTML: show_trame() or show_widget_table()
        HTML-->>NB: interactive 3D display
    else 3D detected but interactive unsupported
        CDF->>PV: _pyvista_volume_snapshot_html()
        PV-->>CDF: PNG data URL
        CDF->>HTML: return img tag fallback
        HTML-->>NB: static 3D preview
    else No 3D
        CDF->>HTML: standard 2D HTML table
        HTML-->>NB: tabular display
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through slices, voxel by voxel bright,

Rows opened tunnels of luminous light.
PyVista and Trame spun tiny worlds to see,
From OME to HTML I bounded with glee—
A rabbit cheers: 3D per row, hip‑hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Enable 3D visualizations with CytoDataFrame' directly and accurately reflects the primary change: adding 3D visualization support to CytoDataFrame using trame and pyvista.
Linked Issues check ✅ Passed The PR successfully implements the primary objective from issue #175 to enable 3D image visualization per record/row, adding substantial 3D rendering infrastructure with PyVista/Trame backends, OME-Arrow decoding, and comprehensive HTML generation.
Out of Scope Changes check ✅ Passed All substantial changes align with the 3D visualization objective. Minor updates to pre-commit configs, citations, and documentation are supportive in scope. Test infrastructure and one unrelated image testing enhancement appear tangentially related but acceptable.

✏️ 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

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.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
src/cytodataframe/frame.py (1)

1302-1330: ⚠️ Potential issue | 🔴 Critical

Critical: Duplicate method definition for _ensure_uint8.

The static method _ensure_uint8 is defined twice - first at lines 1302-1312, then again at lines 1320-1330. The second definition shadows the first. The two implementations also have subtly different logic:

  • First (line 1310): if 0 <= min_val <= 255 and 0 <= max_val <= 255
  • Second (line 1328): if min_val >= 0 and max_val <= 255

Remove one of the duplicate definitions.

🐛 Proposed fix - remove the duplicate method
     `@staticmethod`
     def _is_3d_image_array(array: np.ndarray) -> bool:
         return array.ndim >= 3 and not (  # noqa: PLR2004
             array.ndim == 3 and array.shape[-1] in (1, 3, 4)  # noqa: PLR2004
         )
-
-    `@staticmethod`
-    def _ensure_uint8(array: np.ndarray) -> np.ndarray:
-        """Convert the provided array to uint8 without unnecessary warnings."""
-
-        arr = np.asarray(array)
-        if np.issubdtype(arr.dtype, np.integer):
-            min_val = arr.min(initial=0)
-            max_val = arr.max(initial=0)
-            if min_val >= 0 and max_val <= 255:  # noqa: PLR2004
-                return arr.astype(np.uint8, copy=False)
-        return img_as_ubyte(arr)
🤖 Fix all issues with AI agents
In `@pyproject.toml`:
- Around line 42-48: Remove the direct dependency on ipython-genutils from
pyproject.toml: delete the "ipython-genutils = \"^0.2.0\"" entry, then run your
dependency resolver (poetry/pip-tools/pip) to refresh the lockfile and ensure no
direct imports in the codebase require it; if any test/runtime failures surface,
identify the importing package and either add a maintained replacement or add a
compatible requirement with a comment, otherwise rely on the transitive
dependency resolution to supply it.

In `@src/cytodataframe/frame.py`:
- Around line 2968-2999: The nested _render_cell currently closes over the loop
variable image_col causing a late-binding bug; fix it by binding image_col per
iteration (e.g., turn the nested function into a renderer factory or give
image_col a default argument) so each _render_cell uses the correct column value
when called by data.loc[display_indices].apply; keep the existing calls to
_snapshot_cache_key, _get_3d_volume_from_cell and _pyvista_volume_snapshot_html
but ensure they use the bound image_col for that iteration.

In `@src/cytodataframe/volume.py`:
- Around line 274-279: The function extract_volume_from_ome_arrow declares an
ensure_uint8 parameter but never uses it; fix by applying ensure_uint8 to the
extracted numpy volume before returning (i.e., after you build the ndarray
inside extract_volume_from_ome_arrow call volume = ensure_uint8(volume) so the
returned ndarray is normalized to uint8), and keep the return type
Tuple[np.ndarray, Tuple[int,int,int]> intact; alternatively if normalization is
not desired remove the ensure_uint8 parameter and its type from the function
signature and all call sites.
- Line 178: The script src is unpinned
("script.src='https://unpkg.com/vtk.js';") which can break on upstream
changes—replace that string (and the other occurrence) with a pinned ESM package
URL for `@kitware/vtk.js` (e.g.
"https://unpkg.com/@kitware/vtk.js@<VERSION>/dist/vtk.js" or the appropriate ESM
entry), updating the literal assigned to script.src in the code paths that set
it (replace the "https://unpkg.com/vtk.js" literal wherever used, e.g., the
assignment to script.src at the two occurrences) and choose a concrete tested
<VERSION> before committing.

In `@tests/data/CP_tutorial_3D_noise_nuclei_segmentation/run.sh`:
- Around line 3-7: The CPDOCKER_RUNDIR variable points to the wrong directory
causing docker build to fail; update CPDOCKER_RUNDIR (used by the docker build
command alongside CPDOCKER_IMAGE_NAME) to the directory that contains the
Dockerfile (tests/data/CP_tutorial_3D_noise_nuclei_segmentation) so the docker
build -f "$CPDOCKER_RUNDIR/Dockerfile" . can locate the Dockerfile correctly;
ensure the docker build invocation remains unchanged.
🧹 Nitpick comments (11)
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/Dockerfile (1)

1-50: Consider adding a non-root user for improved container security.

The Dockerfile runs all operations as root. While acceptable for test/CI infrastructure, adding a non-root user is a security best practice that prevents potential privilege escalation if the container is compromised.

🛡️ Suggested improvement to add non-root user
 WORKDIR /usr/local/src
+
+# Create non-root user for running cellprofiler
+RUN useradd -m -s /bin/bash cpuser
+USER cpuser
+
 ENTRYPOINT ["cellprofiler"]

 CMD ["--run", "--run-headless", "--help"]
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/run.sh (1)

1-21: Add error handling and fix redundant command.

The script lacks error handling (set -e), which means failures will be silently ignored. Additionally, Line 21 specifies cellprofiler explicitly, but since the ENTRYPOINT is already cellprofiler, this results in running cellprofiler cellprofiler -c -r ....

♻️ Proposed improvements
 #!/bin/bash
+set -euo pipefail
 # build and run cellprofiler from a docker container
-CPDOCKER_RUNDIR=$PWD/src/docker/3d-nuclei-profiling
+CPDOCKER_RUNDIR=$PWD/tests/data/CP_tutorial_3D_noise_nuclei_segmentation
 CPDOCKER_IMAGE_NAME=cp-3d-nuclei-profiling

 # build image
 docker build --platform linux/amd64 -t "$CPDOCKER_IMAGE_NAME" -f "$CPDOCKER_RUNDIR/Dockerfile" .

 # show the CellProfiler version and use run as a quick test
 echo "CellProfiler version:"
 docker run --rm --platform linux/amd64 -w /app \
     -v "$CPDOCKER_RUNDIR:/app" \
     "$CPDOCKER_IMAGE_NAME" \
     --version

 # run cellprofiler with the examplehuman dataset from:
 # https://cellprofiler.org/examples
 docker run --rm --platform linux/amd64 -w /app \
     -v "$CPDOCKER_RUNDIR:/app" \
     "$CPDOCKER_IMAGE_NAME" \
-    cellprofiler -c -r -p 3d-nuclei-profiling.cppipe -o output -i input
+    -c -r -p 3d-nuclei-profiling.cppipe -o output -i input
src/cytodataframe/volume.py (2)

145-150: Inconsistent transfer function configurations between renderers.

The build_3d_vtk_js_script and build_3d_vtk_js_initializer use different color/opacity transfer functions:

  • Script (lines 145-150): 2 RGB points (0→black, 255→white), 2 opacity points (0→0.0, 255→0.2)
  • Initializer (lines 217-224): 3 RGB points (0→black, 1→white, 255→white), 3 opacity points (0→0.0, 1→0.15, 255→0.2)

This could cause different visual appearances when rendering the same volume via different code paths.

Consider extracting the transfer function configuration to a shared constant or function to ensure consistent rendering behavior.

Also applies to: 217-224


70-71: Potential in-place modification when input is already uint8.

volume.astype(np.uint8, copy=False) returns the same array if the dtype is already uint8, which means tobytes() will serialize the original array. This is fine functionally, but be aware that if the volume is modified elsewhere concurrently, it could affect the serialization.

For absolute safety with concurrent access, consider copy=True, though the current implementation is acceptable for single-threaded use.

tests/test_volume.py (1)

266-271: Consider removing unused noqa directives.

The # noqa: ANN204 directives on __init__ methods are flagged as unused by Ruff. These can be safely removed to reduce noise.

♻️ Remove unused noqa directives
     class FailingOMEArrow:
-        def __init__(self, data: str):  # noqa: ANN204
+        def __init__(self, data: str) -> None:
             raise RuntimeError("boom")

Apply similar changes to FakeOMEArrow and BadOMEArrow classes.

Also applies to: 290-295, 316-321

tests/test_frame.py (1)

1154-1154: Consider using tmp_path fixture instead of hardcoded /tmp.

The test uses hardcoded /tmp paths which triggers Ruff's S108 warning about insecure temporary file usage. While this is test code and poses minimal risk, using pytest's tmp_path fixture would be more consistent with other tests in the file.

♻️ Suggested improvement
-    cdf = CytoDataFrame(base, data_context_dir="/tmp")
+    cdf = CytoDataFrame(base, data_context_dir=str(tmp_path))

Add tmp_path: pathlib.Path to the test function signature.

Also applies to: 1169-1169

src/cytodataframe/frame.py (5)

1862-1878: Silent exception swallowing hides potential issues.

The try-except-pass block at lines 1877-1878 silently swallows all exceptions from the OME-Arrow fallback path. This can mask import failures, data corruption, or other issues that would be useful for debugging.

♻️ Proposed fix - add debug logging
             try:
                 from ome_arrow import OMEArrow  # type: ignore

                 if volume is None:
                     ome_struct = OMEArrow(data=str(data_path)).data
                     if hasattr(ome_struct, "as_py"):
                         ome_struct = ome_struct.as_py()
                     volume_data = extract_volume_from_ome_arrow(
                         ome_struct,
                         self._ensure_uint8,
                         self._is_ome_arrow_value,
                         logger,
                     )
                     if volume_data is not None:
                         volume, dims = volume_data
-            except Exception:
-                pass
+            except Exception as exc:
+                logger.debug("OME-Arrow fallback failed for %s: %s", data_path, exc)

1984-1995: Unused method arguments dims and widget_width.

The dims parameter (line 1987) and widget_width parameter (line 1990) are declared in the method signature but never used in the method body. The dimensions are recalculated from volume.shape at line 2015, and widget_width is not referenced.

Either use these parameters or remove them from the signature to avoid confusion.

♻️ Proposed fix - remove unused parameters
-    def _build_pyvista_viewer(  # noqa: C901, PLR0912, PLR0913, PLR0915
+    def _build_pyvista_viewer(  # noqa: PLR0913
         self: CytoDataFrame_type,
         volume: np.ndarray,
-        dims: Tuple[int, int, int],
         backend: str,
         widget_height: str,
-        widget_width: str = "100%",
         spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0),
         opacity: Any = "sigmoid",
         shade: bool = False,
         **kwargs: Any,
     ) -> Any:

Then update all call sites to remove the dims argument.


2945-2951: Stub method _enqueue_snapshot_tasks does nothing.

This method accepts rows and columns parameters but immediately returns None without using them. If this is a placeholder for future implementation, consider adding a TODO comment or docstring explaining the intended behavior. If it's not needed, remove it.

♻️ Proposed fix - add TODO or remove
     def _enqueue_snapshot_tasks(
         self: CytoDataFrame_type,
         rows: List[Any],
         columns: List[Any],
     ) -> None:
-        """Queue background snapshot generation for given rows/columns."""
+        """Queue background snapshot generation for given rows/columns.
+
+        TODO: Implement background snapshot generation for improved performance.
+        Currently a no-op placeholder.
+        """
+        # Future: implement async/threaded snapshot generation
         return None

2877-2877: Hardcoded spacing value instead of using variable.

Line 2877 computes base_sample = max(min((1.0, 1.0, 1.0)), 1e-6) which always evaluates to 1.0. The hardcoded tuple (1.0, 1.0, 1.0) at line 2880 is assigned to grid.spacing, but base_sample should likely use this same spacing variable rather than a separate hardcoded tuple.

♻️ Proposed fix
+        spacing = (1.0, 1.0, 1.0)
-        base_sample = max(min((1.0, 1.0, 1.0)), 1e-6)
+        base_sample = max(min(spacing), 1e-6)
         grid = pv.ImageData()
         grid.dimensions = tuple(int(v) for v in vol_xyz.shape)
-        grid.spacing = (1.0, 1.0, 1.0)
+        grid.spacing = spacing

1314-1318: Consider removing unused noqa directives.

The noqa: PLR2004 comments on lines 1316-1317 suppress magic number warnings, but static analysis indicates these directives are not needed (the rule may not be enabled). Consider removing them to keep the code clean.

     `@staticmethod`
     def _is_3d_image_array(array: np.ndarray) -> bool:
-        return array.ndim >= 3 and not (  # noqa: PLR2004
-            array.ndim == 3 and array.shape[-1] in (1, 3, 4)  # noqa: PLR2004
-        )
+        return array.ndim >= 3 and not (
+            array.ndim == 3 and array.shape[-1] in (1, 3, 4)
+        )

Copilot AI review requested due to automatic review settings February 8, 2026 18:30
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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/cytodataframe/frame.py`:
- Around line 1801-1951: The method stores unbounded per-cell volumes in
self._custom_attrs["_volume_cache"], which can exhaust memory; change caching to
respect a display option and evict old entries: read display_options =
self._custom_attrs.get("display_options", {}) and if
display_options.get("volume_disable_cache") is truthy, skip storing/reading the
cache. Otherwise, initialize or replace _volume_cache with an ordered mapping
(e.g., collections.OrderedDict) keyed by cache_key and enforce a max size from
display_options.get("volume_cache_max_entries", 32) by popping the oldest entry
when inserting beyond the limit; update accesses around cache_key, cache[...] =
(volume,dims), and early-return on cache hit accordingly (symbols:
_volume_cache, cache_key, _custom_attrs, display_options).
🧹 Nitpick comments (3)
tests/test_frame.py (1)

1403-1416: Consider removing unused monkeypatch parameter.

Static analysis correctly identifies that monkeypatch is unused in test_enable_debug_mode_adds_handler_once. The test manipulates the logger handlers directly without needing monkeypatch.

♻️ Suggested fix
-def test_enable_debug_mode_adds_handler_once(monkeypatch: pytest.MonkeyPatch) -> None:
+def test_enable_debug_mode_adds_handler_once() -> None:
src/cytodataframe/volume.py (1)

129-196: Consider extracting shared vtk.js rendering logic.

build_3d_vtk_js_script and build_3d_vtk_js_initializer contain nearly identical vtk.js setup code (ImageData creation, mapper/volume setup, renderer configuration, interactor binding). Consider extracting the shared rendering logic into a helper string constant or function to reduce duplication.

♻️ Conceptual refactor
# Extract common vtk.js rendering code as a constant
VTK_JS_RENDER_SETUP = (
    "const imageData=vtk.Common.DataModel.vtkImageData.newInstance();"
    "imageData.setDimensions(dims);"
    # ... shared setup code ...
    "renderer.addVolume(volume);"
    "renderer.resetCamera();"
    "renderWindow.render();"
)

def build_3d_vtk_js_script(element_id: str) -> str:
    return (
        "<script>"
        "(function(){"
        f"const container=document.getElementById('{element_id}');"
        # ... per-element wrapper ...
        f"{VTK_JS_RENDER_SETUP}"
        # ... loading logic ...
        "})();"
        "</script>"
    )

Also applies to: 199-270

src/cytodataframe/frame.py (1)

2834-2935: Consider silencing the unused dims parameter.

If dims is intentionally unused, renaming to _dims (or using it for validation) will keep lint clean.

♻️ Minimal rename
-    def _pyvista_volume_snapshot_html(
+    def _pyvista_volume_snapshot_html(
         self: CytoDataFrame_type,
         volume: np.ndarray,
-        dims: Tuple[int, int, int],
+        _dims: Tuple[int, int, int],
     ) -> Optional[str]:

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class 3D volume visualization support to CytoDataFrame for notebook workflows, using VTK-in-HTML for inline rendering and a PyVista/trame-backed interactive layout option, while keeping existing 2D rendering behavior.

Changes:

  • Introduces a new cytodataframe.volume module to extract 3D volumes (notably from OME-Arrow) and generate 3D HTML/JS views.
  • Extends CytoDataFrame rendering to detect 3D content, optionally switch to a trame/PyVista-backed widget table, and provide snapshot fallbacks.
  • Expands tests, docs, and bundled test data to cover 3D pipelines and improve coverage reporting.

Reviewed changes

Copilot reviewed 19 out of 27 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
uv.lock Adds uv lock metadata (currently pins Python requirement).
src/cytodataframe/volume.py New helpers for OME-Arrow volume extraction and VTK.js HTML/JS rendering.
src/cytodataframe/frame.py Adds 3D detection/rendering paths, widget table + trame view integration, snapshot support, and path-resolution options.
pyproject.toml Adds 3D visualization dependencies and pytest coverage configuration; reformats tool config.
poetry.lock Updates lockfile to include new dependencies.
tests/test_volume.py New unit tests for volume extraction + 3D HTML/JS helpers.
tests/test_frame.py Adds extensive tests for 3D detection, widget/trame rendering paths, snapshots, and notebook environment detection.
tests/test_image.py Adds/extends tests for outline drawing, bbox helper behavior, and scale bar edge cases.
tests/test_project_integration.py Makes cosmicqc integration tests conditional via importorskip.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/run.sh Adds script to regenerate 3D CellProfiler tutorial outputs (data provenance).
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/Dockerfile Adds a pinned Docker build environment for generating the tutorial outputs.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/3d-nuclei-profiling.cppipe Adds the CellProfiler pipeline used to generate the tutorial outputs.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/output/MyExpt_Experiment.csv Adds tutorial output metadata for 3D example/test workflows.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/output/MyExpt_Image.csv Adds tutorial per-image CSV output.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/output/MyExpt_RealsizeNuclei.csv Adds tutorial per-object measurements including 3D bbox fields.
tests/data/CP_tutorial_3D_noise_nuclei_segmentation/output/MyExpt_Watershed.csv Adds tutorial object index/path metadata output.
docs/src/examples/cytodataframe_at_a_glance.py Extends the example to include a 3D dataset workflow and visualization behavior.
docs/src/contributing.md Notes that pytest output includes terminal coverage summary.
README.md Documents default 3D notebook display behavior and configuration knobs.
CITATION.cff Adds citations for 3D nuclei tooling/data used in examples.
.pre-commit-config.yaml Updates hook versions and adjusts coverage hook invocation.
media/coverage-badge.svg Updates coverage badge to reflect new coverage level.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/cytodataframe/frame.py (1)

1741-1770: ⚠️ Potential issue | 🟡 Minor

process_ome_arrow_data_as_html_display() return type is str, but it can return a non-string data_value.

Even if this is fine at runtime, it’s confusing (and undermines static typing). Converting the fallback to str(data_value) would match the annotation.

Proposed diff
             if volume_data is None:
-                return data_value
+                return str(data_value)
@@
         except Exception:
-            return data_value
+            return str(data_value)
🤖 Fix all issues with AI agents
In `@pyproject.toml`:
- Around line 129-138: The TOML tables use invalid dotted-key layout; replace
the current [tool.pytest] with a proper table [tool.pytest.ini_options] and move
ini_options.addopts into the addopts key, and replace [tool.coverage] with
[tool.coverage.run] and move run.omit into the omit key (so use addopts =
"...cov..." and omit = [ "config.py", "config-3.py" ]), ensuring the sections
are named [tool.pytest.ini_options] and [tool.coverage.run] and the keys are
addopts and omit respectively.

In `@src/cytodataframe/frame.py`:
- Around line 1036-1043: find_image_columns() currently only treats strings as
potential paths (the lambda uses isinstance(value, str)), so Path objects are
missed; update the lambda in find_image_columns() to accept Path-like objects
(e.g., isinstance(value, (str, os.PathLike))) and call re.match on str(value) so
pathlib.Path or other PathLike values are handled; add the necessary import
(import os or from pathlib import Path) if missing.
- Around line 2190-2349: The backend parameter is annotated as backend: str =
"trame" but code accepts None (see backend is None checks and _repr_html_
calling with backend=None); update the type to allow None (e.g., backend: str |
None = "trame" or Optional[str] = "trame") in both show_trame and
show_widget_table signatures so the annotation matches actual usage; ensure
imports or Python version compatibility if you choose Optional[str].
- Around line 1833-1942: _get_3d_volume_from_cell is missing the CytoTable-style
path-column lookup and recursive rglob fallback that
_prepare_cropped_image_layers uses; add the same logic: when data_context_dir is
not set or the direct data_path.is_file() check fails, consult Image_PathName_*
columns (or the data_image_paths helper) for an alternative filename for the
same row, and if still not found perform a context_dir rglob search for the
filename to locate nested folders before attempting imageio or OME-Arrow
decoding; update _get_3d_volume_from_cell to normalize the candidate filename
the same way you do for 2D images, try candidates from the path-column list, and
replace the existing single-file lookup with a loop over discovered candidate
paths (including pathlib.Path(context_dir)/candidate and any rglob matches) so
volume/dims resolution mirrors _prepare_cropped_image_layers behavior.
🧹 Nitpick comments (10)
pyproject.toml (3)

8-11: Consider simplifying the multiline description to avoid accidental leading whitespace in published metadata.

The current indentation inside the triple-quoted string can end up in the rendered package metadata (depending on how Poetry serializes it). This is mostly cosmetic, but it’s easy to avoid.


44-48: 3D stack deps: consider (a) adding upper bounds and (b) making them optional extras to avoid forcing VTK/Trame on all installs.

Right now these are unconditional runtime deps with only lower bounds, which increases the chance of (1) install pain on some platforms and (2) breakage on future major versions.

Proposed diff (cap major versions; optionally mark as extras)
 pyvista = ">=0.46.4"
-trame = ">=3.12"
-trame-vtk = ">=2.10"
-trame-vuetify = ">=3.1"
+trame = "^3.12"
+trame-vtk = "^2.10"
+trame-vuetify = "^3.1"
 nest-asyncio = "^1.6.0"

If you want these optional (common for visualization stacks), Poetry-style could be:

# [tool.poetry.dependencies]
pyvista = { version = "^0.46.4", optional = true }
trame = { version = "^3.12", optional = true }
trame-vtk = { version = "^2.10", optional = true }
trame-vuetify = { version = "^3.1", optional = true }
nest-asyncio = { version = "^1.6.0", optional = true }

# [tool.poetry.extras]
viz3d = ["pyvista", "trame", "trame-vtk", "trame-vuetify", "nest-asyncio"]

151-158: tasks.poster-render.shell looks overly escape-heavy; consider switching back to a plain multiline shell block (like poster-preview).

The current mix of \\\n + explicit line continuations is hard to maintain and easy to break.

Proposed diff (clean multiline shell)
-tasks.poster-render.shell = """\
-  quarto render \\\n  docs/presentations/2025-sbi2/poster.qmd\n  mv \
-  docs/presentations/2025-sbi2/cytodataframe-2025-poster.pdf \\\n  docs/src/_static/cytodataframe-2025-poster.pdf\n\
-  """
+tasks.poster-render.shell = """
+quarto render \
+  docs/presentations/2025-sbi2/poster.qmd
+mv \
+  docs/presentations/2025-sbi2/cytodataframe-2025-poster.pdf \
+  docs/src/_static/cytodataframe-2025-poster.pdf
+"""
tests/test_frame.py (1)

823-862: Optional: clean up Ruff warnings in test helpers (unused args / unused noqa).

A handful of inner helpers and lambdas intentionally ignore parameters; using _/*_ patterns (and removing now-unused # noqa) will keep the test file warning-free.

Also applies to: 924-928

tests/test_volume.py (1)

127-141: Avoid hard-coding the vtk.js CDN URL/version in tests; import the constant from cytodataframe.volume instead.

This reduces maintenance friction when bumping vtk.js.

Proposed diff
-from cytodataframe.volume import (
+from cytodataframe.volume import (
+    VTK_JS_CDN_URL,
     build_3d_html_from_path,
     build_3d_image_html_stub,
     build_3d_image_html_view,
     build_3d_vtk_js_initializer,
     build_3d_vtk_js_script,
     extract_volume_from_ome_arrow,
 )
@@
 def test_vtk_js_helpers_include_expected_hooks():
     script = build_3d_vtk_js_script("abc")
     initializer = build_3d_vtk_js_initializer()
-    assert "https://unpkg.com/@kitware/vtk.js@34.9.1/dist/vtk.js" in script
+    assert VTK_JS_CDN_URL in script
     assert "document.getElementById('abc')" in script
     assert "querySelectorAll('.cyto-3d-image[data-volume][data-dims]')" in initializer
src/cytodataframe/volume.py (2)

74-162: Consider validating (volume, dims) consistency to prevent hard-to-debug client-side failures.

Since these helpers are public, a simple sanity check (3D volume, np.prod(dims) == volume_uint8.size) would catch misuse early.

Possible minimal validation
 def build_3d_image_html_view(
     volume: np.ndarray,
     dims: Tuple[int, int, int],
@@
 ) -> str:
@@
     volume_uint8 = np.array(volume, dtype=np.uint8, copy=True)
+    if volume_uint8.ndim != MIN_VOLUME_NDIM:
+        return build_3d_image_html_stub(
+            data_value=data_value,
+            candidate_path=candidate_path,
+            display_options=display_options,
+            message="3D image has unsupported shape for inline rendering",
+        )
+    if int(np.prod(dims)) != int(volume_uint8.size):
+        return build_3d_image_html_stub(
+            data_value=data_value,
+            candidate_path=candidate_path,
+            display_options=display_options,
+            message="3D image dims do not match volume size",
+        )

164-197: vtk.js init path: consider setting render-window size in the per-element script too (standalone HTML robustness).

Right now build_3d_vtk_js_script() uses include_container_size=False, so standalone HTML that relies on the embedded <script> may render at an incorrect/zero size depending on vtk.js behavior. Using container-based sizing in both paths tends to be more reliable.

Proposed diff (enable sizing in the per-element script)
 def build_3d_vtk_js_script(element_id: str, vtk_js_url: Optional[str] = None) -> str:
@@
-        f"{_build_vtk_js_renderer_core(include_container_size=False)}"
+        f"{_build_vtk_js_renderer_core(include_container_size=True)}"

Also applies to: 200-247, 249-284

src/cytodataframe/frame.py (3)

1431-1474: 3D-in-_prepare_cropped_image_layers: consider applying the XY bounding box crop before building the inline volume HTML.

Without cropping, inline volume payloads can be much larger than necessary and may exceed max_inline_volume_bytes even when the per-cell crop would fit.


2020-2048: Optional: _find_3d_columns_for_display() swallows all exceptions; consider a debug log when a candidate column fails probing.

A single debug log (maybe gated behind a display/debug option) can make “why didn’t auto-trame trigger?” much easier to diagnose.


3036-3115: _generate_trame_snapshot_html() has a broad except Exception: pass in the cell renderer; consider logging at debug at least once.

Even a minimal logger.debug(..., exc_info=True) would help debug “Snapshot unavailable” cases without spamming normal logs.

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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.pre-commit-config.yaml:
- Around line 56-60: Update and verify the pre-commit bump to ruff rev "v0.15.0"
by running the new formatter and checker locally: run `ruff format` across the
repo (or via the ruff-format hook) to reformat the entire codebase to the 2026
style and commit those changes, then run `ruff check` (or ruff-check hook) to
surface lint failures; also validate config resolution for pyproject.toml and
any extended configs by running ruff with `--show-settings`/`--config` as needed
to ensure the updated resolver loads expected options; reference the hooks
ruff-format and ruff-check and the rev: "v0.15.0" entry when making and testing
these changes.

Copy link
Member

@jenna-tomkinson jenna-tomkinson left a comment

Choose a reason for hiding this comment

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

LGTM! Super excited to see this!

d33bs and others added 6 commits February 12, 2026 16:28
Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
Co-Authored-By: Jenna Tomkinson <107513215+jenna-tomkinson@users.noreply.github.com>
@d33bs
Copy link
Member Author

d33bs commented Feb 13, 2026

Thanks @jenna-tomkinson for the review! Merging this in.

@d33bs d33bs merged commit 6bae600 into cytomining:main Feb 13, 2026
9 checks passed
@d33bs d33bs deleted the 3d-views branch February 13, 2026 00:04
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.

Enable 3d image visualization per record/row within CytoDataFrame

2 participants