Skip to content

Commit ad81919

Browse files
Fix .T breaking whole-FOV image rendering (#227)
* transpose fix * [pre-commit.ci lite] apply automatic fixes * coderabbit review fix --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 894f6d2 commit ad81919

3 files changed

Lines changed: 136 additions & 15 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ repos:
1515
- id: check-yaml
1616
- id: detect-private-key
1717
- repo: https://github.com/tox-dev/pyproject-fmt
18-
rev: "v2.27.0"
18+
rev: "v2.28.1"
1919
hooks:
2020
- id: pyproject-fmt
2121
- repo: https://github.com/codespell-project/codespell
@@ -50,12 +50,12 @@ repos:
5050
hooks:
5151
- id: actionlint
5252
- repo: https://github.com/astral-sh/ruff-pre-commit
53-
rev: "v0.16.1"
53+
rev: "v0.16.5"
5454
hooks:
5555
- id: ruff-format
5656
- id: ruff-check
5757
- repo: https://github.com/software-gardening/almanack
58-
rev: v0.1.16
58+
rev: v0.1.17
5959
hooks:
6060
- id: almanack-check
6161
- repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update

‎src/cytodataframe/frame.py‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5029,7 +5029,10 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
50295029
# Re-add bounding box columns if they are no longer available
50305030
bounding_box_externally_joined = False
50315031
if self._custom_attrs["data_bounding_box"] is not None and not all(
5032-
col in self.columns.tolist()
5032+
col
5033+
in (
5034+
data if self._custom_attrs["is_transposed"] else self
5035+
).columns.tolist()
50335036
for col in self._custom_attrs["data_bounding_box"].columns.tolist()
50345037
):
50355038
logger.debug("Re-adding bounding box columns.")
@@ -5049,15 +5052,18 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
50495052

50505053
# Re-add compartment center xy columns if they are no longer available
50515054
compartment_center_externally_joined = False
5055+
use_data_for_compartment_center = (
5056+
self._custom_attrs["is_transposed"] or bounding_box_externally_joined
5057+
)
50525058
if self._custom_attrs["compartment_center_xy"] is not None and not all(
50535059
col
5054-
in (data if bounding_box_externally_joined else self).columns.tolist()
5060+
in (data if use_data_for_compartment_center else self).columns.tolist()
50555061
for col in self._custom_attrs["compartment_center_xy"].columns.tolist()
50565062
):
50575063
logger.debug("Re-adding compartment center xy columns.")
50585064
data = (
50595065
data.join(other=self._custom_attrs["compartment_center_xy"])
5060-
if bounding_box_externally_joined
5066+
if use_data_for_compartment_center
50615067
else self.join(other=self._custom_attrs["compartment_center_xy"])
50625068
)
50635069
compartment_center_externally_joined = True
@@ -5072,11 +5078,13 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
50725078

50735079
# Re-add image path columns if they are no longer available
50745080
image_paths_externally_joined = False
5081+
use_data_for_image_paths = (
5082+
self._custom_attrs["is_transposed"]
5083+
or compartment_center_externally_joined
5084+
or bounding_box_externally_joined
5085+
)
50755086
if self._custom_attrs["data_image_paths"] is not None and not all(
5076-
col
5077-
in (
5078-
data if compartment_center_externally_joined else self
5079-
).columns.tolist()
5087+
col in (data if use_data_for_image_paths else self).columns.tolist()
50805088
for col in self._custom_attrs["data_image_paths"].columns.tolist()
50815089
):
50825090
logger.debug("Re-adding image path columns.")
@@ -5087,17 +5095,14 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
50875095
)
50885096
data = (
50895097
data.join(other=self._custom_attrs["data_image_paths"])
5090-
if compartment_center_externally_joined
5091-
or bounding_box_externally_joined
5098+
if use_data_for_image_paths
50925099
else self.join(other=self._custom_attrs["data_image_paths"])
50935100
)
50945101
image_paths_externally_joined = True
50955102
else:
50965103
data = (
50975104
data
5098-
if self._custom_attrs["is_transposed"]
5099-
or image_paths_externally_joined
5100-
or bounding_box_externally_joined
5105+
if use_data_for_image_paths or image_paths_externally_joined
51015106
else self.copy()
51025107
)
51035108

‎tests/test_frame.py‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,55 @@ def displayed_image_sizes(frame: CytoDataFrame) -> list:
15001500
assert all(size == (expected_width, expected_height) for size in sizes)
15011501

15021502

1503+
def test_repr_html_transposed_render_whole_image_without_bounding_box_or_center(
1504+
cytotable_NF1_data_parquet_shrunken: str,
1505+
):
1506+
"""
1507+
Tests that ``.T`` on a whole-FOV CytoDataFrame (no bounding box or
1508+
compartment center columns) still renders a transposed table with the
1509+
embedded image intact, rather than falling back to an untransposed table
1510+
of raw filenames.
1511+
1512+
See https://github.com/cytomining/CytoDataFrame/issues/226.
1513+
"""
1514+
1515+
image_dir = (
1516+
f"{pathlib.Path(cytotable_NF1_data_parquet_shrunken).parent}/Plate_2_images"
1517+
)
1518+
image_filename = next(pathlib.Path(image_dir).glob("*.tif")).name
1519+
1520+
# a minimal whole-FOV frame with no bounding box or compartment
1521+
# center columns at all (mirrors the issue's reproduction).
1522+
df = pd.DataFrame(
1523+
{
1524+
"FileName_OrigDNA": [image_filename, image_filename],
1525+
"PathName_OrigDNA": [image_dir, image_dir],
1526+
"SomeMetric": [-2.5, -2.7],
1527+
}
1528+
)
1529+
1530+
whole_frame = CytoDataFrame(df, display_options={"render_whole_image": True})
1531+
assert whole_frame._custom_attrs["data_bounding_box"] is None
1532+
assert whole_frame._custom_attrs["compartment_center_xy"] is None
1533+
1534+
non_transposed_html = whole_frame.head(2)._repr_html_(debug=True)
1535+
transposed_html = whole_frame.head(2).T._repr_html_(debug=True)
1536+
1537+
non_transposed_images = re.findall(
1538+
r"data:image/png;base64,([^\"]+)", non_transposed_html
1539+
)
1540+
transposed_images = re.findall(r"data:image/png;base64,([^\"]+)", transposed_html)
1541+
1542+
# the transposed rendering must still embed the same images as the
1543+
# non-transposed rendering (previously it silently rendered none).
1544+
assert len(transposed_images) == len(non_transposed_images) > 0
1545+
1546+
# the transposed rendering must actually be transposed: the
1547+
# FileName_OrigDNA column becomes a row label instead of appearing
1548+
# in the header row.
1549+
assert ">FileName_OrigDNA<" in transposed_html
1550+
1551+
15031552
def test_repr_html_offset_bounding_box_warns_when_centers_missing_with_bbox(
15041553
cytotable_NF1_data_parquet_shrunken: str,
15051554
caplog: pytest.LogCaptureFixture,
@@ -3155,6 +3204,73 @@ def test_generate_jupyter_dataframe_html_with_joined_components(
31553204
assert "<div>OME</div>" in html
31563205

31573206

3207+
def test_generate_jupyter_dataframe_html_preserves_center_join_with_existing_paths(
3208+
monkeypatch: pytest.MonkeyPatch,
3209+
tmp_path: pathlib.Path,
3210+
):
3211+
"""
3212+
Regression test: an externally joined compartment center must survive
3213+
rendering even when image-path columns are already present on the
3214+
frame (so the image-path re-add block takes its "already present"
3215+
branch instead of re-joining). Before the fix, that branch discarded
3216+
the joined compartment center columns, and the later ``.drop(...)``
3217+
of those columns raised a ``KeyError``.
3218+
"""
3219+
base = pd.DataFrame(
3220+
{
3221+
"Image_FileName_DNA": ["dna.tiff"],
3222+
"Image_PathName_DNA": [str(tmp_path)],
3223+
},
3224+
index=[0],
3225+
)
3226+
cdf = CytoDataFrame(
3227+
base,
3228+
data_context_dir=str(tmp_path),
3229+
display_options={"render_whole_image": True},
3230+
)
3231+
assert cdf._custom_attrs["data_bounding_box"] is None
3232+
cdf._custom_attrs["compartment_center_xy"] = pd.DataFrame(
3233+
{"Cells_Location_Center_X": [5], "Cells_Location_Center_Y": [6]},
3234+
index=[0],
3235+
)
3236+
cdf._custom_attrs["data_image_paths"] = pd.DataFrame(
3237+
{"Image_PathName_DNA": [str(tmp_path)]},
3238+
index=[0],
3239+
)
3240+
3241+
options = {
3242+
"display.notebook_repr_html": True,
3243+
"display.max_rows": 10,
3244+
"display.min_rows": 10,
3245+
"display.max_columns": 10,
3246+
"display.show_dimensions": False,
3247+
}
3248+
3249+
monkeypatch.setattr("cytodataframe.frame.get_option", lambda name: options[name])
3250+
monkeypatch.setattr(
3251+
"cytodataframe.frame.CytoDataFrame.find_image_columns",
3252+
lambda self: ["Image_FileName_DNA"],
3253+
)
3254+
monkeypatch.setattr(
3255+
"cytodataframe.frame.CytoDataFrame.find_image_path_columns",
3256+
lambda self, image_cols, all_cols: {"Image_FileName_DNA": "Image_PathName_DNA"},
3257+
)
3258+
monkeypatch.setattr(
3259+
"cytodataframe.frame.CytoDataFrame.get_displayed_rows",
3260+
lambda self: [0],
3261+
)
3262+
monkeypatch.setattr(
3263+
cdf,
3264+
"process_image_data_as_html_display",
3265+
lambda **_kwargs: "<img src='x'/>",
3266+
)
3267+
3268+
# previously raised KeyError from dropping compartment center columns
3269+
# that had been silently discarded earlier in the method.
3270+
html = cdf._generate_jupyter_dataframe_html()
3271+
assert "<img src='x'/>" in html
3272+
3273+
31583274
def test_render_output_displays_js_and_print_html(monkeypatch: pytest.MonkeyPatch):
31593275
cdf = CytoDataFrame(pd.DataFrame({"A": [1]}))
31603276
cdf._custom_attrs["_output"] = nullcontext()

0 commit comments

Comments
 (0)