render.data_frame

render.data_frame(self, fn)

Decorator for a function that returns a pandas DataFrame object (or similar) to render as an interactive table or grid. Features fast virtualized scrolling, sorting, filtering, and row selection (single or multiple).

Returns

Type Description
A decorator for a function that returns any of the following: 1. A DataGrid or DataTable object, which can be used to customize the appearance and behavior of the data frame output. 2. A pandas 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). (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 <data_frame_renderer>.input_cell_selection() method, where <data_frame_renderer> is the render function name that corresponds with the id= used in 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"])].

Editing Cells

When a returned DataTable or DataGrid object has editable=True, app users will be able to edit the cells in the table. After a cell has been edited, the edited value will be sent to the server for processing. The handling methods are set via @<data_frame_renderer>.set_patch_fn or @<data_frame_renderer>.set_patches_fn decorators. By default, both decorators will return a string value.

To access the data viewed by the user, use <data_frame_renderer>.data_view(). This method will sort, filter, and apply any patches to the data frame as viewed by the user within the browser. This is a shallow copy of the original data frame. It is possible that alterations to data_view could alter the original data data frame.

To access the original data, use <data_frame_renderer>.data(). This is a quick reference to the original data frame (converted to a pandas.DataFrame) that was returned from the app's render function. If it is mutated in place, it will modify the original data.

Note… if the data frame renderer is re-rendered due to reactivity, then (currently) the user's edits, sorting, and filtering will be lost. We hope to improve upon this in the future.

Tip

This decorator should be applied before the @output decorator (if that decorator is used). Also, the name of the decorated function (or @output(id=...)) should match the id of a output_data_frame container (see output_data_frame for example usage).

See Also

Examples

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400

## file: app.py
import pandas  # noqa: F401 (this line needed for Shinylive to load plotly.express)
import plotly.express as px
from shinywidgets import output_widget, render_widget

from shiny import App, reactive, render, req, ui

# Load the Gapminder dataset
df = px.data.gapminder()

# Prepare a summary DataFrame
summary_df = (
    df.groupby("country")
    .agg(
        {
            "pop": ["min", "max", "mean"],
            "lifeExp": ["min", "max", "mean"],
            "gdpPercap": ["min", "max", "mean"],
        }
    )
    .reset_index()
)

summary_df.columns = ["_".join(col).strip() for col in summary_df.columns.values]
summary_df.rename(columns={"country_": "country"}, inplace=True)

app_ui = ui.page_fillable(
    {"class": "p-3"},
    ui.markdown(
        "**Instructions**: Select one or more countries in the table below to see more information."
    ),
    ui.layout_columns(
        ui.card(ui.output_data_frame("summary_data"), height="400px"),
        ui.card(output_widget("country_detail_pop"), height="400px"),
        ui.card(output_widget("country_detail_percap"), height="400px"),
        col_widths=[12, 6, 6],
    ),
)


def server(input, output, session):
    @render.data_frame
    def summary_data():
        return render.DataGrid(summary_df.round(2), selection_mode="rows")

    @reactive.calc
    def filtered_df():
        data_selected = summary_data.data_view(selected=True)
        req(not data_selected.empty)
        countries = data_selected["country"]
        # Filter data for selected countries
        return df[df["country"].isin(countries)]

    @render_widget
    def country_detail_pop():
        return px.line(
            filtered_df(),
            x="year",
            y="pop",
            color="country",
            title="Population Over Time",
        )

    @render_widget
    def country_detail_percap():
        return px.line(
            filtered_df(),
            x="year",
            y="gdpPercap",
            color="country",
            title="GDP per Capita Over Time",
        )


app = App(app_ui, server)

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400

## file: app.py
from shared import mtcars

from shiny import App, reactive, render, ui

app_ui = ui.page_fillable(
    ui.layout_columns(
        ui.card(
            ui.card_header(
                ui.markdown(
                    """
                    ##### Editable data frame
                    * Edit the cells!
                    * Sort the columns!
                    """
                )
            ),
            ui.output_data_frame("df_original"),
        ),
        ui.card(
            ui.card_header(
                ui.markdown(
                    """
                    ##### Updated data from the first data frame
                    * Select the rows!
                    * Filter and sort the columns!
                    """
                )
            ),
            ui.output_data_frame("df_edited"),
        ),
        ui.card(
            ui.card_header(
                ui.markdown(
                    """
                    ##### Selected data from the second data frame
                    * Sort the columns!
                    """
                )
            ),
            ui.output_data_frame("df_selected"),
        ),
        col_widths=[4, 4, 4],
    ),
)


def server(input, output, session):
    df = reactive.value(mtcars.iloc[:, range(4)])

    @render.data_frame
    def df_original():
        return render.DataGrid(
            df(),
            editable=True,
        )

    # Convert edited values to the correct data type
    @df_original.set_patch_fn
    def _(*, patch: render.CellPatch) -> render.CellValue:
        if patch["column_index"] in [0, 2]:
            return float(patch["value"])
        return int(patch["value"])

    @render.data_frame
    def df_edited():
        return render.DataGrid(
            # Reactive value is updated when the user edits the data within `df_original` output
            df_original.data_view(),
            selection_mode="rows",
            filters=True,
        )

    @render.data_frame
    def df_selected():
        return render.DataGrid(
            # Reactive value is updated when the user selects rows the data within `df_edited` output
            df_edited.data_view(selected=True),
            selection_mode="rows",
        )


app = App(app_ui, server)


## file: mtcars.csv
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb
21,6,160,110,3.9,2.62,16.46,0,1,4,4
21,6,160,110,3.9,2.875,17.02,0,1,4,4
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
10.4,8,460,215,3,5.424,17.82,0,0,3,4
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
15,8,301,335,3.54,3.57,14.6,0,1,5,8
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2


## file: shared.py
from pathlib import Path

import pandas as pd

app_dir = Path(__file__).parent
mtcars = pd.read_csv(app_dir / "mtcars.csv")

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400

## file: app.py
from __future__ import annotations

from pathlib import Path

import pandas as pd

from shiny import App, reactive, render, ui

app_ui = ui.page_fillable(
    {"class": "p-3"},
    ui.markdown(
        """
        #### Instructions:
        * Run the app locally so that the edits to the underlying CSV file will persist.
        * Edit the cells in the table.

        #### Note:
        The data frame will not be re-rendered as the result of `df()` has not updated.

        Once the `df()` is invalidated, all local edits are forgotten, and the data frame will be re-rendered. However, since the edits were saved to the CSV file, the edits will persist between refreshes (when run locally).
        """
    ),
    ui.card(
        ui.output_data_frame("my_data_frame"),
    ),
)

here = Path(__file__).parent


def server(input, output, session):
    mtcars_df = reactive.value(pd.read_csv(here / "mtcars.csv").iloc[:, range(4)])

    @render.data_frame
    def my_data_frame():
        return render.DataGrid(
            mtcars_df(),
            editable=True,
        )

    # Save the edited values to the data source (ex: the CSV file)
    @my_data_frame.set_patches_fn
    def _(*, patches: list[render.CellPatch]) -> list[render.CellPatch]:
        for patch in patches:
            if patch["column_index"] in [0, 2]:
                patch["value"] = float(patch["value"])
            else:
                patch["value"] = int(patch["value"])

        # "Save to the database" by writing the edited data to a CSV file
        for patch in patches:
            df = mtcars_df().copy()
            df.iloc[patch["row_index"], patch["column_index"]] = patch["value"]
        df.to_csv(here / "mtcars.csv", index=False)
        print("Saved the edited data to './mtcars.csv'")

        return patches


app = App(app_ui, server)


## file: mtcars.csv
mpg,cyl,disp,hp
21.0,6,160.0,110
21.0,6,160.0,110
22.8,4,108.0,93
21.4,6,258.0,110
18.7,8,360.0,175
18.1,6,225.0,105
14.3,8,360.0,245
24.4,4,146.7,62
22.8,4,140.8,95
19.2,6,167.6,123
17.8,6,167.6,123
16.4,8,275.8,180
17.3,8,275.8,180
15.2,8,275.8,180
10.4,8,472.0,205
10.4,8,460.0,215
14.7,8,440.0,230
32.4,4,78.7,66
30.4,4,75.7,52
33.9,4,71.1,65
21.5,4,120.1,97
15.5,8,318.0,150
15.2,8,304.0,150
13.3,8,350.0,245
19.2,8,400.0,175
27.3,4,79.0,66
26.0,4,120.3,91
30.4,4,95.1,113
15.8,8,351.0,264
19.7,6,145.0,175
15.0,8,301.0,335
21.4,4,121.0,109

Attributes

Name Description
cell_patches Reactive value of the data frame’s edits provided by the user.
data Reactive value of the data frame’s output data.
input_cell_selection Reactive value of selected cell information.
selection_modes Reactive value of the data frame’s possible selection modes.

Methods

Name Description
data_view Reactive function that retrieves the data how it is viewed within the browser.
set_patch_fn Decorator to set the function that updates a single cell in the data frame.
set_patches_fn Decorator to set the function that updates a batch of cells in the data frame.
update_cell_selection Update the cell selection in the data frame.

data_view

render.data_frame.data_view(selected=False)

Reactive function that retrieves the data how it is viewed within the browser.

This function will sort, filter, and apply any patches to the data frame as viewed by the user within the browser.

This is a shallow copy of the original data frame. It is possible that alterations to data_view could alter the original data data frame. Please be cautious when using this value directly.

Parameters

selected: bool = False

If True, subset the viewed data to the selected area. Defaults to False.

Returns

Type Description
pd.DataFrame A view of the data frame as seen in the browser. Even if the rendered data value was not pd.DataFrame, this method currently returns the converted pd.DataFrame.

See Also

  • [pandas.DataFrame.copy API documentation]h(ttps://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.copy.html)

set_patch_fn

render.data_frame.set_patch_fn(fn)

Decorator to set the function that updates a single cell in the data frame.

The default patch function returns the value as is.

Parameters

fn: PatchFn | PatchFnSync

A function that accepts a kwarg patch and returns the processed patch.value for the cell.

set_patches_fn

render.data_frame.set_patches_fn(fn)

Decorator to set the function that updates a batch of cells in the data frame.

The default patches function calls the async ._patch_fn() on each input patch and returns the updated patch values.

There are no checks made on the quantity of patches returned. The user can return more, less, or the same number of patches as the input patches. This allows for the app author to own more control over which columns are updated and how they are updated.

update_cell_selection

render.data_frame.update_cell_selection(selection)

Update the cell selection in the data frame.

Currently only single ("type": "row") or multiple ("type": "rows") row selection is supported.

If the current data frame selection mode is "none" and a non-none selection is provided, a warning will be raised and no rows will be selected. If cells are supposes to be selected, the selection mode returned from the render function must (currently) be set to "row" or "rows".

Parameters

selection: CellSelection | Literal[‘all’] | None

The cell selection to apply to the data frame. This can be a CellSelection object, "all" to select all cells (if possible), or None to clear the selection.