Skip to content

Commit 1a07fe0

Browse files
authored
feat(df): Make .input_cell_selection() return a consistent type shape; Change row order to data view row order (#1376)
1 parent db3ebcb commit 1a07fe0

File tree

13 files changed

+215
-99
lines changed

13 files changed

+215
-99
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131

3232
* `ui.page_*()` functions gain a `theme` argument that allows you to replace the Bootstrap CSS file with a new CSS file. `theme` can be a local CSS file, a URL, or a [shinyswatch](https://posit-dev.github.io/py-shinyswatch) theme. In Shiny Express apps, `theme` can be set via `express.ui.page_opts()`. (#1334)
3333

34+
* `@render.data_frame`'s `<ID>.input_cell_selection()` no longer returns a `None` value and now always returns a dictionary containing both the `rows` and `cols` keys. This is done to achieve more consistent author code when working with cell selection. When the value's `type="none"`, both `rows` and `cols` are empty tuples. When `type="row"`, `cols` represents all column numbers of the data. In the future, when `type="col"`, `rows` will represent all row numbers of the data. These extra values are not available in `input.<ID>_cell_selection()` as they are independent of cells being selected and are removed to reduce information being sent to and from the browser. (#1376)
35+
36+
* Added `@render.data_frame`'s `.data_view_info()` which is a reactive value that contains `sort` (a list of sorted column information), `filter` (a list of filtered column information), `rows` (a list of row numbers for the sorted and filtered data frame), and `selected_rows` (`rows` that have been selected by the user). (#1374)
37+
3438
### Bug fixes
3539

3640
* Fixed an issue that prevented Shiny from serving the `font.css` file referenced in Shiny's Bootstrap CSS file. (#1342)

examples/dataframe/app.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,13 @@ def handle_edit():
9999

100100
@render.text
101101
def detail():
102-
selected_rows = (grid.input_cell_selection() or {}).get("rows", ())
103-
if len(selected_rows) > 0:
102+
cell_selection = grid.input_cell_selection()
103+
if cell_selection is None:
104+
return ""
105+
106+
if len(cell_selection["rows"]) > 0:
104107
# "split", "records", "index", "columns", "values", "table"
105-
return df().iloc[list(selected_rows)]
108+
return df().iloc[list(cell_selection["rows"])]
106109

107110

108111
app = App(app_ui, server)

js/data-frame/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
399399
const rowsById = table.getSortedRowModel().rowsById;
400400
shinyValue = {
401401
type: "row",
402-
rows: rowSelectionKeys.map((key) => rowsById[key].index).sort(),
402+
rows: rowSelectionKeys.map((key) => rowsById[key].index),
403403
};
404404
} else {
405405
console.error("Unhandled row selection mode:", rowSelectionModes);

shiny/render/_data_frame.py

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from ..types import ListOrTuple
2828
from ._data_frame_utils import (
2929
AbstractTabularData,
30+
BrowserCellSelection,
3031
CellPatch,
3132
CellPatchProcessed,
3233
CellSelection,
@@ -147,15 +148,18 @@ class data_frame(Renderer[DataFrameResult]):
147148
-------------
148149
When using the row selection feature, you can access the selected rows by using the
149150
`<data_frame_renderer>.input_cell_selection()` method, where `<data_frame_renderer>`
150-
is the render function name that corresponds with the `id=` used in
151-
:func:`~shiny.ui.outout_data_frame`. Internally, this method retrieves the selected
152-
cell information from session's `input.<id>_cell_selection()` value. The value
153-
returned will be `None` if the selection mode is `"none"`, or a tuple of
154-
integers representing the indices of the selected rows if the selection mode is
155-
`"row"` or `"rows"`. If no rows have been selected (while in a non-`"none"` row
156-
selection mode), an empty tuple will be returned. To filter a pandas data frame down
157-
to the selected rows, use `<data_frame_renderer>.data_view()` or
158-
`df.iloc[list(input.<id>_cell_selection()["rows"])]`.
151+
is the `@render.data_frame` function name that corresponds with the `id=` used in
152+
:func:`~shiny.ui.outout_data_frame`. Internally,
153+
`<data_frame_renderer>.input_cell_selection()` retrieves the selected cell
154+
information from session's `input.<data_frame_renderer>_cell_selection()` value and
155+
upgrades it for consistent subsetting.
156+
157+
To filter your pandas data frame (`df`) down to the selected rows, you can use:
158+
159+
* `df.iloc[list(input.<data_frame_renderer>_cell_selection()["rows"])]`
160+
* `df.iloc[list(<data_frame_renderer>.input_cell_selection()["rows"])]`
161+
* `df.iloc[list(<data_frame_renderer>.data_view_info()["selected_rows"])]`
162+
* `<data_frame_renderer>.data_view(selected=True)`
159163
160164
Editing cells
161165
-------------
@@ -303,12 +307,16 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
303307
the `id` of the data frame output. This method returns the selected rows and
304308
will cause reactive updates as the selected rows change.
305309
310+
The value has been enhanced from it's vanilla form to include the missing `cols` key
311+
(or `rows` key) as a tuple of integers representing all column (or row) numbers.
312+
This allows for consistent usage within code when subsetting your data. These
313+
missing keys are not sent over the wire as they are independent of the selection.
314+
306315
Returns
307316
-------
308317
:
309-
* `None` if the selection mode is `"none"`
310-
* :class:`~shiny.render.CellSelection` representing the indices of the
311-
selected cells.
318+
:class:`~shiny.render.CellSelection` representing the indices of the selected
319+
cells. If no cells are currently selected, `None` is returned.
312320
"""
313321

314322
data_view_rows: reactive.Calc_[tuple[int, ...]]
@@ -408,18 +416,38 @@ def self_selection_modes() -> SelectionModes:
408416

409417
@reactive.calc
410418
def self_input_cell_selection() -> CellSelection | None:
411-
browser_cell_selection_input = self._get_session().input[
412-
f"{self.output_id}_cell_selection"
413-
]()
414-
415-
browser_cell_selection = as_cell_selection(
419+
browser_cell_selection_input = cast(
420+
BrowserCellSelection,
421+
self._get_session().input[f"{self.output_id}_cell_selection"](),
422+
)
423+
cell_selection = as_cell_selection(
416424
browser_cell_selection_input,
417425
selection_modes=self.selection_modes(),
426+
data=self.data(),
427+
data_view_rows=self._input_data_view_rows(),
428+
data_view_cols=tuple(range(self.data().shape[1])),
418429
)
419-
if browser_cell_selection["type"] == "none":
430+
# If it is an empty selection, return `None`
431+
if cell_selection["type"] == "none":
420432
return None
433+
elif cell_selection["type"] == "row":
434+
if len(cell_selection["rows"]) == 0:
435+
return None
436+
if cell_selection["type"] == "col":
437+
if len(cell_selection["cols"]) == 0:
438+
return None
439+
elif cell_selection["type"] == "rect":
440+
if (
441+
len(cell_selection["rows"]) == 0
442+
and len(cell_selection["cols"]) == 0
443+
):
444+
return None
445+
else:
446+
raise ValueError(
447+
f"Unexpected cell selection type: {cell_selection['type']}"
448+
)
421449

422-
return browser_cell_selection
450+
return cell_selection
423451

424452
self.input_cell_selection = self_input_cell_selection
425453

@@ -861,7 +889,7 @@ async def _send_message_to_browser(self, handler: str, obj: dict[str, Any]):
861889
async def update_cell_selection(
862890
# self, selection: SelectionLocation | CellSelection
863891
self,
864-
selection: CellSelection | Literal["all"] | None,
892+
selection: CellSelection | Literal["all"] | None | BrowserCellSelection,
865893
) -> None:
866894
"""
867895
Update the cell selection in the data frame.
@@ -884,6 +912,8 @@ async def update_cell_selection(
884912
with reactive.isolate():
885913
selection_modes = self.selection_modes()
886914
data = self.data()
915+
data_view_rows = self._input_data_view_rows()
916+
data_view_cols = tuple(range(data.shape[1]))
887917

888918
if selection_modes._is_none():
889919
warnings.warn(
@@ -900,6 +930,8 @@ async def update_cell_selection(
900930
selection,
901931
selection_modes=selection_modes,
902932
data=data,
933+
data_view_rows=data_view_rows,
934+
data_view_cols=data_view_cols,
903935
)
904936

905937
if cell_selection["type"] == "none":

shiny/render/_data_frame_utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
cell_patch_processed_to_jsonifiable,
1919
)
2020
from ._selection import (
21+
BrowserCellSelection,
2122
CellSelection,
2223
SelectionMode,
2324
SelectionModes,
@@ -40,6 +41,7 @@
4041
"PatchFnSync",
4142
"assert_patches_shape",
4243
"cell_patch_processed_to_jsonifiable",
44+
"BrowserCellSelection",
4345
"CellSelection",
4446
"SelectionMode",
4547
"SelectionModes",

0 commit comments

Comments
 (0)