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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Bug fixes

* Fixes #646: Wrap bare value box value in `<p />` tags. (#668)
* Fixed #646: Wrap bare value box value in `<p />` tags. (#668)
* Fixed #676: The `render.data_frame` selection feature was underdocumented and buggy (sometimes returning `None` as a row identifier if the pandas data frame's index had gaps in it). With this release, the selection is consistently a tuple of the 0-based row numbers of the selected rows--or `None` if no rows are selected. (#677)

### Other changes

Expand Down
9 changes: 3 additions & 6 deletions js/dataframe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,8 @@ interface ShinyDataGridProps<TIndex> {

const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = (props) => {
const { id, data, bgcolor } = props;
const { columns, index, type_hints, data: rowData } = data;
const { columns, type_hints, data: rowData } = data;
const { width, height, filters: withFilters } = data.options;
const keyToIndex: Record<string, unknown> = {};
index.forEach((value) => {
keyToIndex[value + ""] = value;
});

const containerRef = useRef<HTMLDivElement>(null);
const theadRef = useRef<HTMLTableSectionElement>(null);
Expand Down Expand Up @@ -192,7 +188,8 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = (props) => {
rowSelection
.keys()
.toList()
.map((key) => keyToIndex[key])
.map((key) => parseInt(key))
.sort()
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion js/dataframe/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface DataGridOptions {

export interface PandasData<TIndex> {
columns: ReadonlyArray<string>;
index: ReadonlyArray<TIndex>;
// index: ReadonlyArray<TIndex>;
data: unknown[][];
type_hints?: ReadonlyArray<TypeHint>;
options: DataGridOptions;
Expand Down
4 changes: 3 additions & 1 deletion shiny/api-examples/data_frame/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ def summary_data():

@reactive.Calc
def filtered_df():
# input.summary_data_selected_rows() is a tuple, so we must convert it to list,
# as that's what Pandas requires for indexing.
selected_idx = list(req(input.summary_data_selected_rows()))
countries = summary_df["country"][selected_idx]
countries = summary_df.iloc[selected_idx]["country"]
# Filter data for selected countries
return df[df["country"].isin(countries)]

Expand Down
6 changes: 3 additions & 3 deletions shiny/experimental/ui/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def output_plot(
Parameters
----------
id
An input id.
An output id.
width
The CSS width, e.g. '400px', or '100%'.
height
Expand Down Expand Up @@ -138,7 +138,7 @@ def output_image(
Parameters
----------
id
An input id.
An output id.
width
The CSS width, e.g. '400px', or '100%'.
height
Expand Down Expand Up @@ -245,7 +245,7 @@ def output_ui(
Parameters
----------
id
An input id.
An output id.
inline
If ``True``, the result is displayed inline
container
Expand Down
35 changes: 27 additions & 8 deletions shiny/render/_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ def to_payload(self) -> object:


def serialize_pandas_df(df: "pd.DataFrame") -> dict[str, Any]:
# Currently, we don't make use of the index; drop it so we don't error trying to
# serialize it or something
df = df.reset_index(drop=True)

res = json.loads(
# {index: [index], columns: [columns], data: [values]}
df.to_json(None, orient="split") # pyright: ignore[reportUnknownMemberType]
Expand Down Expand Up @@ -255,29 +259,44 @@ def data_frame(
_fn: DataFrameTransformer.ValueFn | None = None,
) -> DataFrameTransformer.OutputRenderer | DataFrameTransformer.OutputRendererDecorator:
"""
Reactively render a Pandas data frame object (or similar) as a basic HTML table.

Reactively render a Pandas data frame object (or similar) as an interactive table or
grid. Features fast virtualized scrolling, sorting, filtering, and row selection
(single or multiple).

Returns
-------
:
A decorator for a function that returns any of the following:

1. A pandas :class:`DataFrame` object.
2. A pandas :class:`Styler` object.
1. A :class:`~shiny.render.DataGrid` or :class:`~shiny.render.DataTable` object,
which can be used to customize the appearance and behavior of the data frame
output.
2. A pandas :class:`DataFrame` object. (Equivalent to
`shiny.render.DataGrid(df)`.)
3. Any object that has a `.to_pandas()` method (e.g., a Polars data frame or
Arrow table).
Arrow table). (Equivalent to `shiny.render.DataGrid(df.to_pandas())`.)

Row selection
-------------
When using the row selection feature, you can access the selected rows by using the
`input.<id>_selected_rows()` function, where `<id>` is the `id` of the
:func:`~shiny.ui.output_data_frame`. The value returned will be `None` if no rows
are selected, or a tuple of integers representing the indices of the selected rows.
To filter a pandas data frame down to the selected rows, use
`df.iloc[list(input.<id>_selected_rows())]`.

Tip
----
This decorator should be applied **before** the ``@output`` decorator. Also, the
name of the decorated function (or ``@output(id=...)``) should match the ``id`` of
a :func:`~shiny.ui.output_table` container (see :func:`~shiny.ui.output_table` for
name of the decorated function (or ``@output(id=...)``) should match the ``id`` of a
:func:`~shiny.ui.output_table` container (see :func:`~shiny.ui.output_table` for
example usage).

See Also
--------
~shiny.ui.output_data_frame
* :func:`~shiny.ui.output_data_frame`
* :class:`~shiny.render.DataGrid` and :class:`~shiny.render.DataTable` are the
objects you can return from the rendering function to specify options.
"""
return DataFrameTransformer(_fn)

Expand Down
12 changes: 6 additions & 6 deletions shiny/ui/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def output_plot(
Parameters
----------
id
An input id.
An output id.
width
The CSS width, e.g. '400px', or '100%'.
height
Expand Down Expand Up @@ -127,7 +127,7 @@ def output_image(
Parameters
----------
id
An input id.
An output id.
width
The CSS width, e.g. '400px', or '100%'.
height
Expand Down Expand Up @@ -223,7 +223,7 @@ def output_text(
Parameters
----------
id
An input id.
An output id.
inline
If ``True``, the result is displayed inline
container
Expand Down Expand Up @@ -260,7 +260,7 @@ def output_text_verbatim(id: str, placeholder: bool = False) -> Tag:
Parameters
----------
id
An input id.
An output id.
placeholder
If the output is empty or ``None``, should an empty rectangle be displayed to
serve as a placeholder? (does not affect behavior when the output is nonempty)
Expand Down Expand Up @@ -292,7 +292,7 @@ def output_table(id: str, **kwargs: TagAttrValue) -> Tag:
Parameters
----------
id
An input id.
An output id.
**kwargs
Additional attributes to add to the container.

Expand Down Expand Up @@ -320,7 +320,7 @@ def output_ui(
Parameters
----------
id
An input id.
An output id.
inline
If ``True``, the result is displayed inline
container
Expand Down
5 changes: 3 additions & 2 deletions shiny/ui/dataframe/_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ def data_frame_deps() -> HTMLDependency:

def output_data_frame(id: str) -> Tag:
"""
Create a output container for a data frame.
Create an output container for an interactive table or grid. Features fast
virtualized scrolling, sorting, filtering, and row selection (single or multiple).

Parameters
----------
id
An input id.
An output id.

Returns
-------
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/shared/dataframe/dataframe.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions shiny/www/shared/dataframe/dataframe.js.map

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions tests/e2e/bugs/0676-row-selection/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pandas as pd

from shiny import App, Inputs, Outputs, Session, render, ui

df = pd.DataFrame(
dict(
id=["one", "two", "three"],
fname=["Alice", "Bob", "Charlie"],
lname=["Smith", "Jones", "Brown"],
)
).set_index( # type: ignore
"id",
drop=False,
)

app_ui = ui.page_fluid(
ui.p("Select rows in the grid, make sure the selected rows appear below."),
ui.output_data_frame("grid"),
ui.output_table("detail"),
ui.output_text_verbatim("debug"),
class_="p-3",
)


def server(input: Inputs, output: Outputs, session: Session):
@output
@render.data_frame
def grid():
return render.DataGrid(
df,
row_selection_mode="multiple",
height=350,
)

@output
@render.table
def detail():
if (
input.grid_selected_rows() is not None
and len(input.grid_selected_rows()) > 0
):
# "split", "records", "index", "columns", "values", "table"
return df.iloc[list(input.grid_selected_rows())]

@output
@render.text
def debug():
return input.grid_selected_rows()


app = App(app_ui, server, debug=True)
29 changes: 29 additions & 0 deletions tests/e2e/bugs/0676-row-selection/test_0676_row_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from conftest import ShinyAppProc
from playwright.sync_api import Page, expect


def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

# The purpose of this test is to make sure that the data grid can work on Pandas
# data frames that use an index that is not simply 0-based integers.

row1 = page.locator("#grid tbody tr:nth-child(1)")
row3 = page.locator("#grid tbody tr:nth-child(3)")
result_loc = page.locator("#detail tbody tr:nth-child(1) td:nth-child(1)")
debug_loc = page.locator("#debug")

expect(row3).to_be_visible()
expect(row3.locator("td:nth-child(1)")).to_have_text("three")
expect(debug_loc).to_have_text("()")

expect(result_loc).not_to_be_attached()
row3.click()
expect(result_loc).to_have_text("three")
expect(debug_loc).to_have_text("(2,)")

# Ensure that keys are in sorted order, not the order in which they were selected
row1.click()
expect(debug_loc).to_have_text("(0, 2)")
4 changes: 3 additions & 1 deletion tests/e2e/data_frame/test_data_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ def test_table_switch(
# Switch datasets to much longer one
select_dataset.set("diamonds")
select_dataset.expect.to_have_value("diamonds")
expect(summary).to_have_text(re.compile("^Viewing rows 1 through \\d+ of 53940$"))
expect(summary).to_have_text(
re.compile("^Viewing rows 1 through \\d+ of 53940$"), timeout=10000
)


@pytest.mark.flaky(reruns=RERUNS)
Expand Down