Skip to content

Commit c0a4253

Browse files
committed
Expose .data_view_info() for @render.data_frame obj
1 parent 811f8c7 commit c0a4253

File tree

6 files changed

+224
-53
lines changed

6 files changed

+224
-53
lines changed

js/data-frame/index.tsx

Lines changed: 7 additions & 9 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,27 @@ 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`;
353-
354349
// Already prefiltered rows!
355350
const shinyValue: RowModel<unknown[]> = table.getSortedRowModel();
356351

357352
const rowIndices = table.getSortedRowModel().rows.map((row) => row.index);
358-
Shiny.setInputValue!(shinyId, rowIndices);
353+
Shiny.setInputValue!(`${id}_data_view_rows`, rowIndices);
354+
355+
// Legacy value as of 2024-05-13
356+
Shiny.setInputValue!(`${id}_data_view_indices`, rowIndices);
359357
}, [
360358
id,
361359
table,

shiny/render/_data_frame.py

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import warnings
44

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

6063

61-
class SelectedIndices(TypedDict):
62-
rows: tuple[int] | None
63-
columns: tuple[int] | None
64-
65-
6664
class ColumnSort(TypedDict):
6765
id: str
6866
desc: bool
@@ -78,6 +76,18 @@ class ColumnFilterNumber(TypedDict):
7876
value: tuple[float, float]
7977

8078

79+
ColumnFilter = Union[ColumnFilterStr, ColumnFilterNumber]
80+
81+
82+
class DataViewInfo(TypedDict):
83+
sort: tuple[ColumnSort, ...]
84+
filter: tuple[ColumnFilter, ...]
85+
86+
rows: tuple[int, ...] # sorted and filtered row number
87+
selected_rows: tuple[int, ...] # selected and sorted and filtered row number
88+
# selected_columns: tuple[int, ...] # selected and sorted and filtered row number
89+
90+
8191
# # TODO-future; Use `dataframe-api-compat>=0.2.6` to injest dataframes and return standardized dataframe structures
8292
# # TODO-future: Find this type definition: https://github.com/data-apis/dataframe-api-compat/blob/273c0be45962573985b3a420869d0505a3f9f55d/dataframe_api_compat/polars_standard/dataframe_object.py#L22
8393
# # Related: https://data-apis.org/dataframe-api-compat/quick_start/
@@ -275,6 +285,18 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
275285
else:
276286
return self._data_view_all()
277287

288+
data_view_info: reactive.Calc_[DataViewInfo]
289+
"""
290+
Reactive value of the data frame's view information.
291+
292+
This includes:
293+
* `sort`: An array of `{"id": str, "desc": bool }` information. This is the output of `.input_column_sort()`.
294+
* `filter`: An array of `{"id": str, "value": str | tuple[float, float]}` information. This is the output of `.input_column_filter()`.
295+
* `rows`: The row numbers of the data frame that are currently being viewed in the browser after sorting and filtering has been applied.
296+
* `selected_rows`: `rows` values that have been selected by the user. This value created from the `rows` key in `.input_cell_selection()`.
297+
298+
"""
299+
278300
# TODO-barret-render.data_frame; Allow for DataTable and DataGrid to accept SelectionModes
279301
selection_modes: reactive.Calc_[SelectionModes]
280302
"""
@@ -285,7 +307,7 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
285307
"""
286308
Reactive value of selected cell information.
287309
288-
This method is a wrapper around `input.<id>_selected_cells()`, where `<id>` is
310+
This method is a wrapper around `input.<id>_cell_selection()`, where `<id>` is
289311
the `id` of the data frame output. This method returns the selected rows and
290312
will cause reactive updates as the selected rows change.
291313
@@ -297,7 +319,7 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
297319
selected cells.
298320
"""
299321

300-
_input_data_view_indices: reactive.Calc_[list[int]]
322+
_input_data_view_rows: reactive.Calc_[tuple[int, ...]]
301323
"""
302324
Reactive value of the data frame's view indices.
303325
@@ -311,6 +333,16 @@ def data_view(self, *, selected: bool = False) -> pd.DataFrame:
311333
This is the data frame with all the user's edit patches applied to it.
312334
"""
313335

336+
input_column_sort: reactive.Calc_[tuple[ColumnSort, ...]]
337+
"""
338+
Reactive value of the data frame's column sorting information.
339+
"""
340+
341+
input_column_filter: reactive.Calc_[tuple[ColumnFilter, ...]]
342+
"""
343+
Reactive value of the data frame's column filters.
344+
"""
345+
314346
def _reset_reactives(self) -> None:
315347
self._value.set(None)
316348
self._cell_patch_map.set({})
@@ -380,36 +412,53 @@ def self_input_cell_selection() -> CellSelection | None:
380412

381413
self.input_cell_selection = self_input_cell_selection
382414

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

391-
# self._input_column_sort = self__input_column_sort
420+
self.input_column_sort = self_input_column_sort
392421

393-
# # Array of column filters applied by user
394-
# # TODO-barret-render.data_frame; Expose and update column filters
395-
# # Do not expose until update methods are provided
396-
# @reactive.calc
397-
# def self__input_column_filter() -> list[ColumnFilterStr | ColumnFilterNumber]:
398-
# column_filter = self._get_session().input[
399-
# f"{self.output_id}_column_filter"
400-
# ]()
401-
# return column_filter
422+
@reactive.calc
423+
def self_input_column_filter() -> tuple[ColumnFilter, ...]:
424+
column_filter = self._get_session().input[
425+
f"{self.output_id}_column_filter"
426+
]()
427+
return tuple(column_filter)
428+
429+
self.input_column_filter = self_input_column_filter
430+
431+
@reactive.calc
432+
def self_data_view_info() -> DataViewInfo:
433+
434+
cell_selection = self.input_cell_selection()
435+
selected_rows = tuple(
436+
cell_selection["rows"]
437+
if cell_selection is not None and "rows" in cell_selection
438+
else tuple[int]()
439+
)
440+
441+
sort = self.input_column_sort()
442+
filter = self.input_column_filter()
443+
rows = self._input_data_view_rows()
444+
445+
return {
446+
"sort": sort,
447+
"filter": filter,
448+
"rows": rows,
449+
"selected_rows": selected_rows,
450+
}
402451

403-
# self._input_column_filter = self__input_column_filter
452+
self.data_view_info = self_data_view_info
404453

405454
@reactive.calc
406-
def self__input_data_view_indices() -> list[int]:
407-
data_view_indices = self._get_session().input[
408-
f"{self.output_id}_data_view_indices"
455+
def self__input_data_view_rows() -> tuple[int, ...]:
456+
data_view_rows = self._get_session().input[
457+
f"{self.output_id}_data_view_rows"
409458
]()
410-
return data_view_indices
459+
return tuple(data_view_rows)
411460

412-
self._input_data_view_indices = self__input_data_view_indices
461+
self._input_data_view_rows = self__input_data_view_rows
413462

414463
# @reactive.calc
415464
# def self__data_selected() -> pd.DataFrame:
@@ -485,23 +534,21 @@ def _subset_data_view(selected: bool) -> pd.DataFrame:
485534
data = self._data_patched().copy(deep=False)
486535

487536
# Turn into list for pandas compatibility
488-
data_view_indices = list(self._input_data_view_indices())
537+
data_view_rows = list(self._input_data_view_rows())
489538

490539
# Possibly subset the indices to selected rows
491540
if selected:
492541
cell_selection = self.input_cell_selection()
493542
if cell_selection is not None and cell_selection["type"] == "row":
494543
# Use a `set` for faster lookups
495-
selected_row_indices_set = set(cell_selection["rows"])
544+
selected_row_set = set(cell_selection["rows"])
496545

497546
# 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
547+
data_view_rows = [
548+
row for row in data_view_rows if row in selected_row_set
502549
]
503550

504-
return data.iloc[data_view_indices]
551+
return data.iloc[data_view_rows]
505552

506553
# Helper reactives so that internal calculations can be cached for use in other calculations
507554
@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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# pyright: reportUnknownMemberType = false
2+
# pyright: reportMissingTypeStubs = false
3+
# pyright: reportArgumentType = false
4+
# pyright: reportUnknownMemberType = false
5+
6+
from palmerpenguins import load_penguins_raw
7+
8+
from shiny import App, Inputs, Outputs, Session, module, render, ui
9+
10+
# Load the dataset
11+
penguins = load_penguins_raw().head(5)[["Sample Number", "Individual ID", "Date Egg"]]
12+
df = penguins
13+
MOD_ID = "testing"
14+
15+
16+
@module.ui
17+
def mod_ui():
18+
return ui.TagList(
19+
ui.card(
20+
ui.layout_column_wrap(
21+
ui.TagList(
22+
ui.div("Size: ", ui.output_text_verbatim("info_size")),
23+
ui.div("Sort: ", ui.output_text_verbatim("sort")),
24+
ui.div("Filter: ", ui.output_text_verbatim("filter")),
25+
ui.div("Rows: ", ui.output_text_verbatim("rows")),
26+
ui.div("Selected Rows: ", ui.output_text_verbatim("selected_rows")),
27+
),
28+
ui.output_data_frame("penguins_df"),
29+
width=1 / 2,
30+
)
31+
),
32+
)
33+
34+
35+
app_ui = ui.page_fillable(
36+
{"class": "p-3"},
37+
mod_ui(MOD_ID),
38+
)
39+
40+
41+
@module.server
42+
def mod_server(input: Inputs, output: Outputs, session: Session):
43+
@render.data_frame
44+
def penguins_df():
45+
# return df
46+
return render.DataGrid(
47+
df,
48+
selection_mode="rows",
49+
editable=False,
50+
filters=True,
51+
)
52+
# return render.DataTable(df, selection_mode="none", editable=True)
53+
# return render.DataGrid(df, selection_mode="rows", editable=True)
54+
# return render.DataTable(df, selection_mode="rows", editable=True)
55+
return render.DataGrid(df, selection_mode="rows", editable=False)
56+
# return render.DataTable(df, selection_mode="rows", editable=False)
57+
58+
@render.code
59+
def info_size():
60+
return str(len(penguins_df.data_view_info()))
61+
62+
@render.code
63+
def sort():
64+
return str(penguins_df.data_view_info()["sort"])
65+
66+
@render.code
67+
def filter():
68+
return str(penguins_df.data_view_info()["filter"])
69+
70+
@render.code
71+
def rows():
72+
return str(penguins_df.data_view_info()["rows"])
73+
74+
@render.code
75+
def selected_rows():
76+
return str(penguins_df.data_view_info()["selected_rows"])
77+
78+
79+
def server(input: Inputs, output: Outputs, session: Session):
80+
mod_server(MOD_ID)
81+
82+
83+
app = App(app_ui, server, debug=False)
84+
app.sanitize_errors = True
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from conftest import ShinyAppProc
2+
from controls import OutputDataFrame, OutputTextVerbatim
3+
from playwright.sync_api import Page
4+
5+
6+
def test_validate_html_columns(page: Page, local_app: ShinyAppProc) -> None:
7+
page.goto(local_app.url)
8+
9+
data_frame = OutputDataFrame(page, "testing-penguins_df")
10+
11+
OutputTextVerbatim(page, "testing-info_size").expect_value("4")
12+
13+
sort = OutputTextVerbatim(page, "testing-sort")
14+
filter = OutputTextVerbatim(page, "testing-filter")
15+
rows = OutputTextVerbatim(page, "testing-rows")
16+
selected_rows = OutputTextVerbatim(page, "testing-selected_rows")
17+
18+
sort.expect_value("()")
19+
filter.expect_value("()")
20+
rows.expect_value("(0, 1, 2, 3, 4)")
21+
selected_rows.expect_value("()")
22+
23+
data_frame.set_column_sort(col=2)
24+
data_frame.set_column_sort(col=2)
25+
sort.expect_value("({'id': 'Date Egg', 'desc': True},)")
26+
filter.expect_value("()")
27+
rows.expect_value("(2, 3, 4, 0, 1)")
28+
selected_rows.expect_value("()")
29+
30+
data_frame.set_column_filter(1, text="A2")
31+
sort.expect_value("({'id': 'Date Egg', 'desc': True},)")
32+
33+
data_frame.set_column_filter(0, text=["2", ""])
34+
filter.expect_value(
35+
"({'id': 'Individual ID', 'value': 'A2'}, {'id': 'Sample Number', 'value': (2, None)})"
36+
)
37+
38+
rows.expect_value("(3, 1)")
39+
selected_rows.expect_value("()")
40+
data_frame.select_rows([0])
41+
rows.expect_value("(3, 1)")
42+
selected_rows.expect_value("(3,)")

0 commit comments

Comments
 (0)