Skip to content

Commit a1b24ad

Browse files
committed
fix slicing capabilities
1 parent cf60c30 commit a1b24ad

2 files changed

Lines changed: 86 additions & 45 deletions

File tree

src/cytodataframe/frame.py

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def __init__( # noqa: PLR0913
277277
)
278278
),
279279
"_filter_range_sliders": {},
280+
"_initializing": True,
280281
}
281282

282283
if self._custom_attrs["data_context_dir"] is not None:
@@ -351,6 +352,7 @@ def __init__( # noqa: PLR0913
351352
# Wrap methods so they return CytoDataFrames
352353
# instead of Pandas DataFrames.
353354
self._wrap_methods()
355+
self._custom_attrs["_initializing"] = False
354356

355357
def __getitem__(self: CytoDataFrame_type, key: Union[int, str]) -> Any:
356358
"""
@@ -371,28 +373,50 @@ def __getitem__(self: CytoDataFrame_type, key: Union[int, str]) -> Any:
371373
return result
372374

373375
elif isinstance(result, pd.DataFrame):
374-
cdf = CytoDataFrame(
375-
super().__getitem__(key),
376-
data_context_dir=self._custom_attrs["data_context_dir"],
377-
data_image_paths=self._custom_attrs["data_image_paths"],
378-
data_bounding_box=self._custom_attrs["data_bounding_box"],
379-
compartment_center_xy=self._custom_attrs["compartment_center_xy"],
380-
data_mask_context_dir=self._custom_attrs["data_mask_context_dir"],
381-
data_outline_context_dir=self._custom_attrs["data_outline_context_dir"],
382-
segmentation_file_regex=self._custom_attrs["segmentation_file_regex"],
383-
image_adjustment=self._custom_attrs["image_adjustment"],
384-
display_options=self._custom_attrs["display_options"],
385-
)
376+
return self._build_result_cdf(result)
386377

387-
# add widget control meta
388-
cdf._custom_attrs["_widget_state"] = self._custom_attrs["_widget_state"]
389-
cdf._custom_attrs["_scale_slider"] = self._custom_attrs["_scale_slider"]
390-
cdf._custom_attrs["_filter_range_sliders"] = self._custom_attrs[
391-
"_filter_range_sliders"
392-
]
393-
cdf._custom_attrs["_output"] = self._custom_attrs["_output"]
378+
@property
379+
def _constructor(self: CytoDataFrame_type) -> Callable[..., CytoDataFrame_type]:
380+
"""Return a constructor that preserves CytoDataFrame display state."""
381+
if self._custom_attrs.get("_initializing", False):
382+
return pd.DataFrame
383+
return self._build_result_cdf
394384

395-
return cdf
385+
def _build_result_cdf(
386+
self: CytoDataFrame_type,
387+
data: Union["CytoDataFrame", pd.DataFrame, pd.Series, Any],
388+
*args: Tuple[Any, ...],
389+
**kwargs: Dict[str, Any],
390+
) -> CytoDataFrame_type:
391+
"""Construct a result frame while preserving CytoDataFrame metadata."""
392+
if data is None and "data" in kwargs:
393+
data = kwargs.pop("data")
394+
395+
cdf = CytoDataFrame(
396+
data=data,
397+
data_context_dir=self._custom_attrs["data_context_dir"],
398+
data_image_paths=self._custom_attrs["data_image_paths"],
399+
data_bounding_box=self._custom_attrs["data_bounding_box"],
400+
compartment_center_xy=self._custom_attrs["compartment_center_xy"],
401+
data_mask_context_dir=self._custom_attrs["data_mask_context_dir"],
402+
data_outline_context_dir=self._custom_attrs["data_outline_context_dir"],
403+
segmentation_file_regex=self._custom_attrs["segmentation_file_regex"],
404+
image_adjustment=self._custom_attrs["image_adjustment"],
405+
display_options=self._custom_attrs["display_options"],
406+
*args,
407+
**kwargs,
408+
)
409+
410+
# Preserve the shared interactive widget objects across derived views.
411+
cdf._custom_attrs["is_transposed"] = self._custom_attrs["is_transposed"]
412+
cdf._custom_attrs["_widget_state"] = self._custom_attrs["_widget_state"]
413+
cdf._custom_attrs["_scale_slider"] = self._custom_attrs["_scale_slider"]
414+
cdf._custom_attrs["_filter_range_sliders"] = self._custom_attrs[
415+
"_filter_range_sliders"
416+
]
417+
cdf._custom_attrs["_output"] = self._custom_attrs["_output"]
418+
419+
return cdf
396420

397421
def _return_cytodataframe(
398422
self: CytoDataFrame_type,
@@ -423,34 +447,19 @@ def _return_cytodataframe(
423447
424448
"""
425449

426-
result = method(*args, **kwargs)
450+
result = (
451+
pd.DataFrame(self).transpose(*args, **kwargs)
452+
if method_name == "transpose"
453+
else method(*args, **kwargs)
454+
)
427455

428456
if isinstance(result, pd.DataFrame):
429-
cdf = CytoDataFrame(
430-
data=result,
431-
data_context_dir=self._custom_attrs["data_context_dir"],
432-
data_image_paths=self._custom_attrs["data_image_paths"],
433-
data_bounding_box=self._custom_attrs["data_bounding_box"],
434-
compartment_center_xy=self._custom_attrs["compartment_center_xy"],
435-
data_mask_context_dir=self._custom_attrs["data_mask_context_dir"],
436-
data_outline_context_dir=self._custom_attrs["data_outline_context_dir"],
437-
segmentation_file_regex=self._custom_attrs["segmentation_file_regex"],
438-
image_adjustment=self._custom_attrs["image_adjustment"],
439-
display_options=self._custom_attrs["display_options"],
440-
)
457+
cdf = self._build_result_cdf(result)
441458
# If the method name is transpose we know that
442459
# the dataframe has been transposed.
443460
if method_name == "transpose" and not self._custom_attrs["is_transposed"]:
444461
cdf._custom_attrs["is_transposed"] = True
445462

446-
# add widget control meta
447-
cdf._custom_attrs["_widget_state"] = self._custom_attrs["_widget_state"]
448-
cdf._custom_attrs["_scale_slider"] = self._custom_attrs["_scale_slider"]
449-
cdf._custom_attrs["_filter_range_sliders"] = self._custom_attrs[
450-
"_filter_range_sliders"
451-
]
452-
cdf._custom_attrs["_output"] = self._custom_attrs["_output"]
453-
454463
return cdf
455464

456465
def _wrap_method(self: CytoDataFrame_type, method_name: str) -> Callable:
@@ -4346,7 +4355,7 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
43464355
# if the data are transposed,
43474356
# we transpose them back to keep
43484357
# logic the same here.
4349-
data = self.transpose()
4358+
data = pd.DataFrame(self).transpose()
43504359

43514360
# Re-add bounding box columns if they are no longer available
43524361
bounding_box_externally_joined = False
@@ -4362,7 +4371,12 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
43624371
)
43634372
bounding_box_externally_joined = True
43644373
else:
4365-
data = self.copy() if not bounding_box_externally_joined else data
4374+
data = (
4375+
data
4376+
if self._custom_attrs["is_transposed"]
4377+
or bounding_box_externally_joined
4378+
else self.copy()
4379+
)
43664380

43674381
# Re-add compartment center xy columns if they are no longer available
43684382
compartment_center_externally_joined = False
@@ -4381,7 +4395,8 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
43814395
else:
43824396
data = (
43834397
data
4384-
if bounding_box_externally_joined
4398+
if self._custom_attrs["is_transposed"]
4399+
or bounding_box_externally_joined
43854400
or compartment_center_externally_joined
43864401
else self.copy()
43874402
)
@@ -4411,7 +4426,9 @@ def _generate_jupyter_dataframe_html( # noqa: C901, PLR0912, PLR0915
44114426
else:
44124427
data = (
44134428
data
4414-
if image_paths_externally_joined or bounding_box_externally_joined
4429+
if self._custom_attrs["is_transposed"]
4430+
or image_paths_externally_joined
4431+
or bounding_box_externally_joined
44154432
else self.copy()
44164433
)
44174434

tests/test_frame.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,30 @@ def test_return_cytodataframe(cytotable_NF1_data_parquet_shrunken: str):
796796
assert isinstance(cdf.tail(), CytoDataFrame)
797797
assert isinstance(cdf.sort_values(by="Metadata_ImageNumber"), CytoDataFrame)
798798
assert isinstance(cdf.sample(n=5), CytoDataFrame)
799+
assert isinstance(cdf.iloc[0:2], CytoDataFrame)
800+
assert isinstance(cdf.iloc[1:1], CytoDataFrame)
801+
assert isinstance(cdf.iloc[0:5:2], CytoDataFrame)
802+
803+
804+
def test_iloc_slice_preserves_cytodataframe_html_formatting():
805+
"""Ensure ``iloc`` slices keep the CytoDataFrame notebook HTML renderer."""
806+
807+
cdf = CytoDataFrame(pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}))
808+
809+
sliced = cdf.iloc[0:3:2]
810+
empty_sliced = cdf.iloc[1:1]
811+
812+
assert isinstance(sliced, CytoDataFrame)
813+
assert isinstance(empty_sliced, CytoDataFrame)
814+
assert sliced._custom_attrs["_output"] is cdf._custom_attrs["_output"]
815+
assert sliced._custom_attrs["_widget_state"] is cdf._custom_attrs["_widget_state"]
816+
assert empty_sliced._custom_attrs["_output"] is cdf._custom_attrs["_output"]
817+
assert (
818+
empty_sliced._custom_attrs["_widget_state"]
819+
is cdf._custom_attrs["_widget_state"]
820+
)
821+
assert "background:#EBEBEB" in sliced._repr_html_(debug=True)
822+
assert "background:#EBEBEB" in empty_sliced._repr_html_(debug=True)
799823

800824

801825
def test_cytodataframe_dynamic_width_and_height(

0 commit comments

Comments
 (0)