Skip to content

Commit 2882ada

Browse files
authored
feat(data frame): Restore input.<ID>_selected_rows(). Rename input.<ID>_data_view_indices to input.<ID>_data_view_rows (#1377)
1 parent 811f8c7 commit 2882ada

File tree

12 files changed

+216
-56
lines changed

12 files changed

+216
-56
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10-
### Breaking Changes
10+
### `input` key changes
11+
12+
* 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, #1377)
13+
14+
* `@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. (#1377)
1115

1216
### New features
1317

@@ -21,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2125

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

28+
* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351, #1377)
29+
2430
### Other changes
2531

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

js/data-frame/index.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
320320

321321
useEffect(() => {
322322
if (!id) return;
323-
const shinyId = `${id}_cell_selection`;
324323
let shinyValue: CellSelection | null = null;
325324
if (rowSelectionModes.is_none()) {
326325
shinyValue = null;
@@ -334,28 +333,28 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
334333
} else {
335334
console.error("Unhandled row selection mode:", rowSelectionModes);
336335
}
337-
Shiny.setInputValue!(shinyId, shinyValue);
336+
Shiny.setInputValue!(`${id}_cell_selection`, shinyValue);
338337
}, [id, rowSelection, rowSelectionModes, table, table.getSortedRowModel]);
339338

340339
useEffect(() => {
341340
if (!id) return;
342-
const shinyId = `${id}_column_sort`;
343-
Shiny.setInputValue!(shinyId, sorting);
341+
Shiny.setInputValue!(`${id}_column_sort`, sorting);
344342
}, [id, sorting]);
345343
useEffect(() => {
346344
if (!id) return;
347-
const shinyId = `${id}_column_filter`;
348-
Shiny.setInputValue!(shinyId, columnFilters);
345+
Shiny.setInputValue!(`${id}_column_filter`, columnFilters);
349346
}, [id, columnFilters]);
350347
useEffect(() => {
351348
if (!id) return;
352-
const shinyId = `${id}_data_view_indices`;
353349

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

357353
const rowIndices = table.getSortedRowModel().rows.map((row) => row.index);
358-
Shiny.setInputValue!(shinyId, rowIndices);
354+
Shiny.setInputValue!(`${id}_data_view_rows`, rowIndices);
355+
356+
// Legacy value as of 2024-05-13
357+
Shiny.setInputValue!(`${id}_data_view_indices`, rowIndices);
359358
}, [
360359
id,
361360
table,
@@ -364,6 +363,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
364363
columnFilters,
365364
]);
366365

366+
// Restored for legacy purposes. Only send selected rows to Shiny when row selection is performed.
367+
useEffect(() => {
368+
if (!id) return;
369+
let shinyValue: number[] | null = null;
370+
if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
371+
const rowSelectionKeys = rowSelection.keys().toList();
372+
const rowsById = table.getSortedRowModel().rowsById;
373+
shinyValue = rowSelectionKeys.map((key) => rowsById[key].index).sort();
374+
}
375+
Shiny.setInputValue!(`${id}_selected_rows`, shinyValue);
376+
}, [id, rowSelection, rowSelectionModes, table]);
377+
367378
// ### End row selection ############################################################
368379

369380
// ### Editable cells ###############################################################

shiny/render/_data_frame.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
297297
selected cells.
298298
"""
299299

300-
_input_data_view_indices: reactive.Calc_[list[int]]
300+
_input_data_view_rows: reactive.Calc_[list[int]]
301301
"""
302302
Reactive value of the data frame's view indices.
303303
@@ -403,13 +403,13 @@ def self_input_cell_selection() -> CellSelection | None:
403403
# self._input_column_filter = self__input_column_filter
404404

405405
@reactive.calc
406-
def self__input_data_view_indices() -> list[int]:
407-
data_view_indices = self._get_session().input[
406+
def self__input_data_view_rows() -> list[int]:
407+
data_view_rows = self._get_session().input[
408408
f"{self.output_id}_data_view_indices"
409409
]()
410-
return data_view_indices
410+
return data_view_rows
411411

412-
self._input_data_view_indices = self__input_data_view_indices
412+
self._input_data_view_rows = self__input_data_view_rows
413413

414414
# @reactive.calc
415415
# def self__data_selected() -> pd.DataFrame:
@@ -485,23 +485,25 @@ def _subset_data_view(selected: bool) -> pd.DataFrame:
485485
data = self._data_patched().copy(deep=False)
486486

487487
# Turn into list for pandas compatibility
488-
data_view_indices = list(self._input_data_view_indices())
488+
data_view_rows = list(self._input_data_view_rows())
489489

490490
# Possibly subset the indices to selected rows
491491
if selected:
492492
cell_selection = self.input_cell_selection()
493493
if cell_selection is not None and cell_selection["type"] == "row":
494494
# Use a `set` for faster lookups
495-
selected_row_indices_set = set(cell_selection["rows"])
496-
497-
# Subset the data view indices to only include the selected rows
498-
data_view_indices = [
499-
index
500-
for index in data_view_indices
501-
if index in selected_row_indices_set
495+
selected_row_set = set(cell_selection["rows"])
496+
nrow = data.shape[0]
497+
498+
# Subset the data view indices to only include the selected rows that are in the data
499+
data_view_rows = [
500+
row
501+
for row in data_view_rows
502+
# Make sure the row is not larger than the number of rows
503+
if row in selected_row_set and row < nrow
502504
]
503505

504-
return data.iloc[data_view_indices]
506+
return data.iloc[data_view_rows]
505507

506508
# Helper reactives so that internal calculations can be cached for use in other calculations
507509
@reactive.calc

shiny/www/shared/py-shiny/data-frame/data-frame.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/shared/py-shiny/data-frame/data-frame.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pandas as pd
2+
3+
from shiny import App, Inputs, reactive, render, req, ui
4+
5+
app_ui = ui.page_fluid(
6+
ui.output_data_frame("df1"),
7+
ui.output_text_verbatim("selected_rows", placeholder=True),
8+
ui.output_text_verbatim("cell_selection", placeholder=True),
9+
)
10+
11+
12+
def server(input: Inputs):
13+
df = reactive.Value(pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=["A", "B"]))
14+
15+
@render.data_frame
16+
def df1():
17+
return render.DataGrid(df(), selection_mode="rows")
18+
19+
@render.text
20+
def selected_rows():
21+
return f"Input selected rows: {input.df1_selected_rows()}"
22+
23+
@render.text
24+
def cell_selection():
25+
cell_selection = df1.input_cell_selection()
26+
if cell_selection is None:
27+
req(cell_selection)
28+
raise ValueError("Cell selection is None")
29+
if cell_selection["type"] != "row":
30+
raise ValueError(
31+
f"Cell selection type is not 'row': {cell_selection['type']}"
32+
)
33+
rows = cell_selection["rows"]
34+
return f"Cell selection rows: {rows}"
35+
36+
37+
app = App(app_ui, server)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
from conftest import ShinyAppProc
4+
from controls import OutputDataFrame, OutputTextVerbatim
5+
from playwright.sync_api import Page
6+
7+
8+
def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
9+
page.goto(local_app.url)
10+
11+
df = OutputDataFrame(page, "df1")
12+
selected_rows = OutputTextVerbatim(page, "selected_rows")
13+
cell_selection = OutputTextVerbatim(page, "cell_selection")
14+
15+
df.expect_n_row(3)
16+
selected_rows.expect_value("Input selected rows: ()")
17+
cell_selection.expect_value("Cell selection rows: ()")
18+
19+
df.select_rows([0, 2])
20+
21+
selected_rows.expect_value("Input selected rows: (0, 2)")
22+
cell_selection.expect_value("Cell selection rows: (0, 2)")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pandas as pd
2+
3+
from shiny import App, Inputs, reactive, render, ui
4+
5+
app_ui = ui.page_fluid(
6+
ui.markdown(
7+
"""
8+
## Description
9+
10+
When you add a row, click on it and click the clear button you get:
11+
12+
13+
"""
14+
),
15+
ui.input_action_button("add_row", "Add row"),
16+
ui.input_action_button("clear_table", "Clear table"),
17+
ui.output_text_verbatim("number_of_selected_rows"),
18+
ui.output_data_frame("df1"),
19+
)
20+
21+
22+
def server(input: Inputs):
23+
df = reactive.Value(pd.DataFrame(columns=["A", "B"]))
24+
25+
@render.data_frame
26+
def df1():
27+
return render.DataGrid(df(), selection_mode="rows")
28+
29+
@reactive.effect
30+
@reactive.event(input.add_row)
31+
def _():
32+
old_df = df()
33+
new_df = pd.concat( # pyright: ignore[reportUnknownMemberType]
34+
[old_df, pd.DataFrame([[1, 2]], columns=["A", "B"])]
35+
)
36+
df.set(new_df)
37+
38+
@render.text
39+
def number_of_selected_rows():
40+
df_selected = df1.data_view(selected=True)
41+
return f"Selected rows: {len(df_selected)}"
42+
43+
@reactive.effect
44+
@reactive.event(input.clear_table)
45+
def _():
46+
df.set(pd.DataFrame(columns=["A", "B"]))
47+
48+
49+
app = App(app_ui, server)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from conftest import ShinyAppProc
4+
from controls import InputActionButton, OutputDataFrame, OutputTextVerbatim
5+
from playwright.sync_api import Page
6+
7+
8+
def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
9+
page.goto(local_app.url)
10+
11+
df = OutputDataFrame(page, "df1")
12+
add_row = InputActionButton(page, "add_row")
13+
clear_table = InputActionButton(page, "clear_table")
14+
selected_rows = OutputTextVerbatim(page, "number_of_selected_rows")
15+
16+
df.expect_n_row(0)
17+
selected_rows.expect_value("Selected rows: 0")
18+
19+
add_row.click()
20+
21+
df.expect_n_row(1)
22+
selected_rows.expect_value("Selected rows: 0")
23+
24+
df.cell_locator(0, 0).click()
25+
df.select_rows([0])
26+
27+
df.expect_n_row(1)
28+
selected_rows.expect_value("Selected rows: 1")
29+
30+
clear_table.click()
31+
selected_rows.expect_value("Selected rows: 0")
32+
33+
bad_error_lines = [line for line in local_app.stderr._lines if "INFO:" not in line]
34+
assert len(bad_error_lines) == 0, bad_error_lines

tests/playwright/shiny/components/data_frame/edit/app.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,22 @@ def summary_data():
8484

8585
from shiny import reactive
8686

87-
@reactive.effect
88-
def _():
89-
print(
90-
"Filters:",
91-
summary_data._input_column_filter(), # pyright: ignore[reportUnknownArgumentType,reportAttributeAccessIssue]
92-
)
93-
94-
@reactive.effect
95-
def _():
96-
print(
97-
"Sorting:",
98-
summary_data._input_column_sort(), # pyright: ignore[reportUnknownArgumentType,reportAttributeAccessIssue]
99-
)
87+
# @reactive.effect
88+
# def _():
89+
# print(
90+
# "Filters:",
91+
# summary_data._input_column_filter(), # pyright: ignore[reportUnknownArgumentType,reportAttributeAccessIssue]
92+
# )
93+
# @reactive.effect
94+
# def _():
95+
# print(
96+
# "Sorting:",
97+
# summary_data._input_column_sort(), # pyright: ignore[reportUnknownArgumentType,reportAttributeAccessIssue]
98+
# )
10099

101100
@reactive.effect
102101
def _():
103-
print("indices:", summary_data._input_data_view_indices())
102+
print("indices:", summary_data._input_data_view_rows())
104103

105104
@reactive.effect
106105
def _():

tests/playwright/shiny/components/data_frame/html_columns_df/df_organization/app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
),
2121
ui.output_data_frame("iris_df"),
2222
ui.h2("Data view indices"),
23-
ui.output_text_verbatim("data_view_indices"),
23+
ui.output_text_verbatim("data_view_rows"),
2424
ui.h2("Indices when view_selected=True"),
2525
ui.output_text_verbatim("data_view_selected_true"),
2626
ui.h2("Indices when view_selected=False"),
@@ -41,8 +41,8 @@ def iris_df():
4141
)
4242

4343
@render.code # pyright: ignore[reportArgumentType]
44-
def data_view_indices():
45-
return iris_df._input_data_view_indices()
44+
def data_view_rows():
45+
return iris_df._input_data_view_rows()
4646

4747
@render.code # pyright: ignore[reportArgumentType]
4848
def data_view_selected_false(): # pyright: ignore[reportUnknownParameterType]

0 commit comments

Comments
 (0)