Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `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)

* `@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)

* 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)

### Bug fixes

* Fixed an issue that prevented Shiny from serving the `font.css` file referenced in Shiny's Bootstrap CSS file. (#1342)
Expand Down
9 changes: 6 additions & 3 deletions examples/dataframe/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,13 @@ def handle_edit():

@render.text
def detail():
selected_rows = (grid.input_cell_selection() or {}).get("rows", ())
if len(selected_rows) > 0:
cell_selection = grid.input_cell_selection()
if cell_selection is None:
return ""

if len(cell_selection["rows"]) > 0:
# "split", "records", "index", "columns", "values", "table"
return df().iloc[list(selected_rows)]
return df().iloc[list(cell_selection["rows"])]


app = App(app_ui, server)
2 changes: 1 addition & 1 deletion js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
const rowsById = table.getSortedRowModel().rowsById;
shinyValue = {
type: "row",
rows: rowSelectionKeys.map((key) => rowsById[key].index).sort(),
rows: rowSelectionKeys.map((key) => rowsById[key].index),
};
} else {
console.error("Unhandled row selection mode:", rowSelectionModes);
Expand Down
72 changes: 52 additions & 20 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from ..types import ListOrTuple
from ._data_frame_utils import (
AbstractTabularData,
BrowserCellSelection,
CellPatch,
CellPatchProcessed,
CellSelection,
Expand Down Expand Up @@ -147,15 +148,18 @@ class data_frame(Renderer[DataFrameResult]):
-------------
When using the row selection feature, you can access the selected rows by using the
`<data_frame_renderer>.input_cell_selection()` method, where `<data_frame_renderer>`
is the render function name that corresponds with the `id=` used in
:func:`~shiny.ui.outout_data_frame`. Internally, this method retrieves the selected
cell information from session's `input.<id>_cell_selection()` value. The value
returned will be `None` if the selection mode is `"none"`, or a tuple of
integers representing the indices of the selected rows if the selection mode is
`"row"` or `"rows"`. If no rows have been selected (while in a non-`"none"` row
selection mode), an empty tuple will be returned. To filter a pandas data frame down
to the selected rows, use `<data_frame_renderer>.data_view()` or
`df.iloc[list(input.<id>_cell_selection()["rows"])]`.
is the `@render.data_frame` function name that corresponds with the `id=` used in
:func:`~shiny.ui.outout_data_frame`. Internally,
`<data_frame_renderer>.input_cell_selection()` retrieves the selected cell
information from session's `input.<data_frame_renderer>_cell_selection()` value and
upgrades it for consistent subsetting.

To filter your pandas data frame (`df`) down to the selected rows, you can use:

* `df.iloc[list(input.<data_frame_renderer>_cell_selection()["rows"])]`
* `df.iloc[list(<data_frame_renderer>.input_cell_selection()["rows"])]`
* `df.iloc[list(<data_frame_renderer>.data_view_info()["selected_rows"])]`
* `<data_frame_renderer>.data_view(selected=True)`

Editing cells
-------------
Expand Down Expand Up @@ -303,12 +307,16 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
the `id` of the data frame output. This method returns the selected rows and
will cause reactive updates as the selected rows change.

The value has been enhanced from it's vanilla form to include the missing `cols` key
(or `rows` key) as a tuple of integers representing all column (or row) numbers.
This allows for consistent usage within code when subsetting your data. These
missing keys are not sent over the wire as they are independent of the selection.

Returns
-------
:
* `None` if the selection mode is `"none"`
* :class:`~shiny.render.CellSelection` representing the indices of the
selected cells.
:class:`~shiny.render.CellSelection` representing the indices of the selected
cells. If no cells are currently selected, `None` is returned.
"""

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

@reactive.calc
def self_input_cell_selection() -> CellSelection | None:
browser_cell_selection_input = self._get_session().input[
f"{self.output_id}_cell_selection"
]()

browser_cell_selection = as_cell_selection(
browser_cell_selection_input = cast(
BrowserCellSelection,
self._get_session().input[f"{self.output_id}_cell_selection"](),
)
cell_selection = as_cell_selection(
browser_cell_selection_input,
selection_modes=self.selection_modes(),
data=self.data(),
data_view_rows=self._input_data_view_rows(),
data_view_cols=tuple(range(self.data().shape[1])),
)
if browser_cell_selection["type"] == "none":
# If it is an empty selection, return `None`
if cell_selection["type"] == "none":
return None
elif cell_selection["type"] == "row":
if len(cell_selection["rows"]) == 0:
return None
if cell_selection["type"] == "col":
if len(cell_selection["cols"]) == 0:
return None
elif cell_selection["type"] == "rect":
if (
len(cell_selection["rows"]) == 0
and len(cell_selection["cols"]) == 0
):
return None
else:
raise ValueError(
f"Unexpected cell selection type: {cell_selection['type']}"
)

return browser_cell_selection
return cell_selection

self.input_cell_selection = self_input_cell_selection

Expand Down Expand Up @@ -861,7 +889,7 @@ async def _send_message_to_browser(self, handler: str, obj: dict[str, Any]):
async def update_cell_selection(
# self, selection: SelectionLocation | CellSelection
self,
selection: CellSelection | Literal["all"] | None,
selection: CellSelection | Literal["all"] | None | BrowserCellSelection,
) -> None:
"""
Update the cell selection in the data frame.
Expand All @@ -884,6 +912,8 @@ async def update_cell_selection(
with reactive.isolate():
selection_modes = self.selection_modes()
data = self.data()
data_view_rows = self._input_data_view_rows()
data_view_cols = tuple(range(data.shape[1]))

if selection_modes._is_none():
warnings.warn(
Expand All @@ -900,6 +930,8 @@ async def update_cell_selection(
selection,
selection_modes=selection_modes,
data=data,
data_view_rows=data_view_rows,
data_view_cols=data_view_cols,
)

if cell_selection["type"] == "none":
Expand Down
2 changes: 2 additions & 0 deletions shiny/render/_data_frame_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
cell_patch_processed_to_jsonifiable,
)
from ._selection import (
BrowserCellSelection,
CellSelection,
SelectionMode,
SelectionModes,
Expand All @@ -40,6 +41,7 @@
"PatchFnSync",
"assert_patches_shape",
"cell_patch_processed_to_jsonifiable",
"BrowserCellSelection",
"CellSelection",
"SelectionMode",
"SelectionModes",
Expand Down
Loading