Skip to content

feat(data frame): Add .data_view_rows(), .sort(), .filter(), .update_sort(), and .update_filter(); cell_selection() no longer returns None #1374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 41 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5892661
Only subset the selected data if the index row exists!
schloerke May 10, 2024
7ef4f84
Update CHANGELOG.md
schloerke May 10, 2024
c8ed2c3
Make sure the index being subsetted is less than the row length, not …
schloerke May 13, 2024
437793a
Restore `input.<ID>_selected_rows()`. Add tests
schloerke May 13, 2024
0c39dd1
Update _renderer.py
schloerke May 13, 2024
e023993
Merge branch 'main' into render_df_bugs
schloerke May 13, 2024
c0a4253
Expose `.data_view_info()` for `@render.data_frame` obj
schloerke May 13, 2024
d6c0d39
Update CHANGELOG.md
schloerke May 13, 2024
fab4d8c
Merge branch 'render_df_bugs' into data_view_meta
schloerke May 13, 2024
f9a322e
Update CHANGELOG.md
schloerke May 13, 2024
102079c
Fix lints; Update Changelog
schloerke May 14, 2024
6b5bd52
Update _data_frame.py
schloerke May 14, 2024
43a5168
Apply suggestions from code review
schloerke May 14, 2024
31c4571
Merge branch 'main' into data_view_meta
schloerke May 15, 2024
34029f1
Have `.data_view()` use `.data_view_info()` information for consisten…
schloerke May 15, 2024
7da5921
Lints
schloerke May 15, 2024
0b03498
`ColumnFilter` and `ColumnSort` should use `col: num` and not `id: st…
schloerke May 15, 2024
bc10e8b
Add `update_sort()` and `update_filter()` to DF
schloerke May 20, 2024
46ad96b
Add demo apps for `update_sort()` and `update_filter()`
schloerke May 20, 2024
86b5e42
Merge branch 'main' into data_view_meta
schloerke May 20, 2024
5799782
Add `.data_view_rows()`
schloerke May 21, 2024
d48bdc7
Remove `.data_view_info()` method
schloerke May 21, 2024
f71e8b6
Update controls.py
schloerke May 21, 2024
237174f
Add type support for `serialize_numpy_dtype()`; Determine default sor…
schloerke May 21, 2024
db3ebcb
`.input_column_sort()` -> `.input_sort()`; `.input_column_filter()` -…
schloerke May 21, 2024
1a07fe0
feat(df): Make `.input_cell_selection()` return a consistent type sha…
schloerke May 21, 2024
16cd1ba
Merge branch 'main' into data_view_meta
schloerke May 21, 2024
435b916
tmp
schloerke May 21, 2024
f9158c4
merge main
schloerke May 29, 2024
f0c36a1
Clean up input_cell_selection method renaming
schloerke May 30, 2024
fcfb740
Make legacy `.input_cell_selection()` return `None` if `type=="none"`
schloerke May 30, 2024
67423e4
consolidate changelog changes before merging from main
schloerke May 30, 2024
45f92f0
Add `playwright` target and use `TEST_FILE` arg that contains full re…
schloerke May 30, 2024
0ee9c9c
Merge branch 'main' into data_view_meta
schloerke May 30, 2024
d5c2406
Spelling
schloerke May 30, 2024
f945b0d
Merge branch 'main' into data_view_meta
schloerke May 31, 2024
5f44e73
Update imports
schloerke Jun 3, 2024
52cacba
Merge branch 'main' into data_view_meta
schloerke Jun 3, 2024
cb04a14
Update _unsafe.py
schloerke Jun 3, 2024
c765781
Code review
schloerke Jun 3, 2024
a05b8ff
`input_sort` -> `sort`; `input_filter` -> `filter`
schloerke Jun 3, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking Changes

* `@render.data_frame`'s input value `input.<ID>_data_view_indices` has been renamed to `input.<ID>_data_view_rows` for consistent naming. Please use `input.<ID>_data_view_rows` and consider `input.<ID>_data_view_indices` deprecated. (#1374)

* Restored `@render.data_frame`'s (prematurely removed in v0.9.0) input value `input.<ID>_selected_rows()`. Please use `<ID>.input_cell_selection()["rows"]` and consider `input.<ID>_selected_rows()` deprecated. (#1345)

### New features

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

* Added busy indicators to provide users with a visual cue when the server is busy calculating outputs or otherwise serving requests to the client. More specifically, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. Use the new `ui.busy_indicator.options()` function to customize the appearance of the busy indicators and `ui.busy_indicator.use()` to disable/enable them. (#918)

* Added support for creating modules using Shiny Express syntax, and using modules in Shiny Express apps. (#1220)
Expand All @@ -21,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351)

### Other changes

* `Session` is now an abstract base class, and `AppSession` is a concrete subclass of it. Also, `ExpressMockSession` has been renamed `ExpressStubSession` and is a concrete subclass of `Session`. (#1331)
Expand Down
28 changes: 19 additions & 9 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,6 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({

useEffect(() => {
if (!id) return;
const shinyId = `${id}_cell_selection`;
let shinyValue: CellSelection | null = null;
if (rowSelectionModes.is_none()) {
shinyValue = null;
Expand All @@ -334,28 +333,27 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
} else {
console.error("Unhandled row selection mode:", rowSelectionModes);
}
Shiny.setInputValue!(shinyId, shinyValue);
Shiny.setInputValue!(`${id}_cell_selection`, shinyValue);
}, [id, rowSelection, rowSelectionModes, table, table.getSortedRowModel]);

useEffect(() => {
if (!id) return;
const shinyId = `${id}_column_sort`;
Shiny.setInputValue!(shinyId, sorting);
Shiny.setInputValue!(`${id}_column_sort`, sorting);
}, [id, sorting]);
useEffect(() => {
if (!id) return;
const shinyId = `${id}_column_filter`;
Shiny.setInputValue!(shinyId, columnFilters);
Shiny.setInputValue!(`${id}_column_filter`, columnFilters);
}, [id, columnFilters]);
useEffect(() => {
if (!id) return;
const shinyId = `${id}_data_view_indices`;

// Already prefiltered rows!
const shinyValue: RowModel<unknown[]> = table.getSortedRowModel();

const rowIndices = table.getSortedRowModel().rows.map((row) => row.index);
Shiny.setInputValue!(shinyId, rowIndices);
Shiny.setInputValue!(`${id}_data_view_rows`, rowIndices);

// Legacy value as of 2024-05-13
Shiny.setInputValue!(`${id}_data_view_indices`, rowIndices);
}, [
id,
table,
Expand All @@ -364,6 +362,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
columnFilters,
]);

// Restored for legacy purposes. Only send selected rows to Shiny when row selection is performed.
useEffect(() => {
if (!id) return;
let shinyValue: number[] | null = null;
if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = rowSelection.keys().toList();
const rowsById = table.getSortedRowModel().rowsById;
shinyValue = rowSelectionKeys.map((key) => rowsById[key].index).sort();
}
Shiny.setInputValue!(`${id}_selected_rows`, shinyValue);
}, [id, rowSelection, rowSelectionModes, table]);

// ### End row selection ############################################################

// ### Editable cells ###############################################################
Expand Down
131 changes: 91 additions & 40 deletions shiny/render/_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import warnings

# TODO-barret; Should `.input_cell_selection()` ever return None? Is that value even helpful? Empty lists would be much more user friendly.
# TODO-barret-render.data_frame; Add method `update_sort()`
# TODO-barret-render.data_frame; Add method `update_filter()`
# TODO-barret-render.data_frame; Docs
# TODO-barret-render.data_frame; Add examples!
from typing import (
Expand Down Expand Up @@ -58,11 +61,6 @@
from ._data_frame_utils._datagridtable import DataFrameResult


class SelectedIndices(TypedDict):
rows: tuple[int] | None
columns: tuple[int] | None


class ColumnSort(TypedDict):
id: str
desc: bool
Expand All @@ -75,7 +73,19 @@ class ColumnFilterStr(TypedDict):

class ColumnFilterNumber(TypedDict):
id: str
value: tuple[float, float]
value: tuple[float, float] | tuple[float, None] | tuple[None, float]


ColumnFilter = Union[ColumnFilterStr, ColumnFilterNumber]


class DataViewInfo(TypedDict):
sort: tuple[ColumnSort, ...]
filter: tuple[ColumnFilter, ...]

rows: tuple[int, ...] # sorted and filtered row number
selected_rows: tuple[int, ...] # selected and sorted and filtered row number
# selected_columns: tuple[int, ...] # selected and sorted and filtered row number


# # TODO-future; Use `dataframe-api-compat>=0.2.6` to injest dataframes and return standardized dataframe structures
Expand Down Expand Up @@ -275,6 +285,18 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
else:
return self._data_view_all()

data_view_info: reactive.Calc_[DataViewInfo]
"""
Reactive value of the data frame's view information.

This includes:
* `sort`: An array of `{"id": str, "desc": bool }` information. This is the output of `.input_column_sort()`.
* `filter`: An array of `{"id": str, "value": str | tuple[float, float]}` information. This is the output of `.input_column_filter()`.
* `rows`: The row numbers of the data frame that are currently being viewed in the browser after sorting and filtering has been applied.
* `selected_rows`: `rows` values that have been selected by the user. This value created from the `rows` key in `.input_cell_selection()`.

"""

# TODO-barret-render.data_frame; Allow for DataTable and DataGrid to accept SelectionModes
selection_modes: reactive.Calc_[SelectionModes]
"""
Expand All @@ -285,7 +307,7 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
"""
Reactive value of selected cell information.

This method is a wrapper around `input.<id>_selected_cells()`, where `<id>` is
This method is a wrapper around `input.<id>_cell_selection()`, where `<id>` is
the `id` of the data frame output. This method returns the selected rows and
will cause reactive updates as the selected rows change.

Expand All @@ -297,7 +319,7 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
selected cells.
"""

_input_data_view_indices: reactive.Calc_[list[int]]
_input_data_view_rows: reactive.Calc_[tuple[int, ...]]
"""
Reactive value of the data frame's view indices.

Expand All @@ -311,6 +333,16 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
This is the data frame with all the user's edit patches applied to it.
"""

input_column_sort: reactive.Calc_[tuple[ColumnSort, ...]]
"""
Reactive value of the data frame's column sorting information.
"""

input_column_filter: reactive.Calc_[tuple[ColumnFilter, ...]]
"""
Reactive value of the data frame's column filters.
"""

def _reset_reactives(self) -> None:
self._value.set(None)
self._cell_patch_map.set({})
Expand Down Expand Up @@ -380,36 +412,53 @@ def self_input_cell_selection() -> CellSelection | None:

self.input_cell_selection = self_input_cell_selection

# # Array of sorted column information
# # TODO-barret-render.data_frame; Expose and update column sorting
# # Do not expose until update methods are provided
# @reactive.calc
# def self__input_column_sort() -> list[ColumnSort]:
# column_sort = self._get_session().input[f"{self.output_id}_column_sort"]()
# return column_sort
@reactive.calc
def self_input_column_sort() -> tuple[ColumnSort, ...]:
column_sort = self._get_session().input[f"{self.output_id}_column_sort"]()
return tuple(column_sort)

# self._input_column_sort = self__input_column_sort
self.input_column_sort = self_input_column_sort

# # Array of column filters applied by user
# # TODO-barret-render.data_frame; Expose and update column filters
# # Do not expose until update methods are provided
# @reactive.calc
# def self__input_column_filter() -> list[ColumnFilterStr | ColumnFilterNumber]:
# column_filter = self._get_session().input[
# f"{self.output_id}_column_filter"
# ]()
# return column_filter
@reactive.calc
def self_input_column_filter() -> tuple[ColumnFilter, ...]:
column_filter = self._get_session().input[
f"{self.output_id}_column_filter"
]()
return tuple(column_filter)

self.input_column_filter = self_input_column_filter

@reactive.calc
def self_data_view_info() -> DataViewInfo:

cell_selection = self.input_cell_selection()
selected_rows = tuple(
cell_selection["rows"]
if cell_selection is not None and "rows" in cell_selection
else ()
)

sort = self.input_column_sort()
filter = self.input_column_filter()
rows = self._input_data_view_rows()

# self._input_column_filter = self__input_column_filter
return {
"sort": sort,
"filter": filter,
"rows": rows,
"selected_rows": selected_rows,
}

self.data_view_info = self_data_view_info

@reactive.calc
def self__input_data_view_indices() -> list[int]:
data_view_indices = self._get_session().input[
f"{self.output_id}_data_view_indices"
def self__input_data_view_rows() -> tuple[int, ...]:
data_view_rows = self._get_session().input[
f"{self.output_id}_data_view_rows"
]()
return data_view_indices
return tuple(data_view_rows)

self._input_data_view_indices = self__input_data_view_indices
self._input_data_view_rows = self__input_data_view_rows

# @reactive.calc
# def self__data_selected() -> pd.DataFrame:
Expand Down Expand Up @@ -485,23 +534,25 @@ def _subset_data_view(selected: bool) -> pd.DataFrame:
data = self._data_patched().copy(deep=False)

# Turn into list for pandas compatibility
data_view_indices = list(self._input_data_view_indices())
data_view_rows = list(self._input_data_view_rows())

# Possibly subset the indices to selected rows
if selected:
cell_selection = self.input_cell_selection()
if cell_selection is not None and cell_selection["type"] == "row":
# Use a `set` for faster lookups
selected_row_indices_set = set(cell_selection["rows"])

# Subset the data view indices to only include the selected rows
data_view_indices = [
index
for index in data_view_indices
if index in selected_row_indices_set
selected_row_set = set(cell_selection["rows"])
nrow = data.shape[0]

# Subset the data view indices to only include the selected rows that are in the data
data_view_rows = [
row
for row in data_view_rows
# Make sure the row is not larger than the number of rows
if row in selected_row_set and row < nrow
]

return data.iloc[data_view_indices]
return data.iloc[data_view_rows]

# Helper reactives so that internal calculations can be cached for use in other calculations
@reactive.calc
Expand Down
15 changes: 7 additions & 8 deletions shiny/render/renderer/_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,15 @@ class Renderer(Generic[IT]):
used!)

There are two methods that must be implemented by the subclasses:
`.auto_output_ui(self, id: str)` and either `.transform(self, value: IT)` or
`.render(self)`.
`.auto_output_ui(self)` and either `.transform(self, value: IT)` or `.render(self)`.

* In Express mode, the output renderer will automatically render its UI via
`.auto_output_ui(self, id: str)`. This helper method allows App authors to skip
adding a `ui.output_*` function to their UI, making Express mode even more
concise. If more control is needed over the UI, `@ui.hold` can be used to suppress
the auto rendering of the UI. When using `@ui.hold` on a renderer, the renderer's
UI will need to be added to the app to connect the rendered output to Shiny's
reactive graph.
`.auto_output_ui(self)`. This helper method allows App authors to skip adding a
`ui.output_*` function to their UI, making Express mode even more concise. If more
control is needed over the UI, `@ui.hold` can be used to suppress the auto
rendering of the UI. When using `@ui.hold` on a renderer, the renderer's UI will
need to be added to the app to connect the rendered output to Shiny's reactive
graph.
* The `render` method is responsible for executing the value function and performing
any transformations for the output value to be JSON-serializable (`None` is a
valid value!). To avoid the boilerplate of resolving the value function and
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js

Large diffs are not rendered by default.

Loading