express.render.data_frame

express.render.data_frame(self, fn)

Decorator for a function that returns a pandas, polars, or eager narwhals compatible DataFrame object to render 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 DataGrid or DataTable object, which can be used to customize the appearance and behavior of the data frame output. 2. A pandas, polars, or eager narwhals compatible DataFrame object. This object will be internally upgraded to a default shiny.express.render.DataGrid(df).

Row selection

When using the row selection feature, you can access the selected rows by using the <data_frame_renderer>.cell_selection() method, where <data_frame_renderer> is the @render.data_frame function name that corresponds with the id= used in outout_data_frame. Internally, <data_frame_renderer>.cell_selection() retrieves the selected cell information from session’s input.<data_frame_renderer>_cell_selection() value and upgrades it for consistent subsetting.

For example, to filter your pandas data frame (df) down to the selected rows you can use:

  • df.iloc[list(input.<data_frame_renderer>_cell_selection()["rows"])]
  • df.iloc[list(<data_frame_renderer>.cell_selection()["rows"])]
  • <data_frame_renderer>.data_view(selected=True)

The last method (.data_view(selected=True)) will also apply any sorting, filtering, or edits that has been applied by the user.

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 the corresponding value as a string.

Data methods

There are several methods available to inspect and update data frame renderer. It is important to know the side effects of each method to know how they interact with each other.

  • Data frame render method:
    • When this method is reactively executed, the .data() data frame is set to the underlying data frame and all .cell_patches() are removed.
    • When this method is reactively executed, all user state is reset. This includes the user’s edits, sorting, filtering.
  • .data():
    • Reactive calculation that returns the render method’s underlying data frame or the data frame supplied to .update_data(data), whichever has been most recently set.
  • .cell_patches():
    • Reactive calculation that returns a list of user edits (or updated cell values) that will be applied to .data() to create the .data_patched() data frame.
  • .data_patched():
    • Reactive calculation that returns the .data() data frame with all .cell_patches() patches applied.
  • .data_view(*, selected: bool):
    • Reactive function that returns the .data_patched() data frame with the user’s sorting and filtering applied. It represents the data frame as viewed by the user within the browser.
    • If selected=True, only the selected rows are returned.
  • .update_cell_value(value, row, col):
    • Sets a new entry in .cell_patches().
    • Calling this method will not reset the user’s sorting or filtering.
  • .update_data(data):
    • Updates the .data() data frame with new data.
    • Calling this method will remove all .cell_patches().
    • Calling this method will not reset the user’s sorting or filtering.

Note: All data methods are shallow copies of each other. If they are mutated in place, it will modify the underlying data object and possibly alter other data objects.

Narwhals

Shiny uses narwhals to manage data frame interactions. From their website: “Extremely lightweight and extensible compatibility layer between dataframe libraries!”. This allows for seamless integration between pandas, polars, and any other eagerly defined data frame type.

There are some reasonable limitations to the narwhals compatibility layer. As they are found, they will be added to this list: * When converting the column type who does not have a 1:1 mapping between libraries (such as pandas’ columns containing str and dict items both share the same object data type), narwhals will only inspect the first row to disambiguate the cell type. This could lead to false negatives in the data type conversion. Shiny could inspect each column in an attempt to disambiguate the cell type, but this would be a costly operation. The best way to avoid this is to use consistent typing. For example, if your first row of the pandas column contains a string and the second row of the same column contains a ui.TagList, the column will incorrectly be interpreted as a string. To get around this, you can wrap all cells (or at the very lest the first cell) in the same column within a ui.TagList as it will not insert any tags, but it will cause the column to be interpreted as html where possible. (tl/dr: Use consistent typing in your columns!)

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

  • output_data_frame
  • DataGrid and DataTable are the objects you can return from the rendering function to specify options.

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 render_widget

from shiny import reactive, req
from shiny.express import render, 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)

# Set up the UI

ui.page_opts(fillable=True)

ui.markdown(
    "**Instructions**: Select one or more countries in the table below to see more information."
)

with ui.layout_columns(col_widths=[12, 6, 6]):
    with ui.card(height="400px"):

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

    with ui.card(height="400px"):

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

    with ui.card(height="400px"):

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


@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)]

Methods

Name Description
cell_patches Reactive calculation of the data frame’s edits.
cell_selection Reactive calculation of selected cell information.
data Reactive calculation of the data frame’s data.
data_patched Reactive calculation of the data frame’s patched data.
data_view Reactive function that retrieves the data how it is viewed within the browser.
data_view_rows Reactive calculation of the data frame’s user view row numbers.
filter Reactive calculation of the data frame’s column filters.
input_cell_selection [Deprecated] Reactive calculation of selected cell information.
selection_modes Reactive calculation of the data frame’s possible selection modes.
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.
sort Reactive calculation of the data frame’s column sorting information.
update_cell_selection Update the cell selection in the data frame.
update_cell_value Update the value of a cell in the data frame.
update_data Update the data frame with new data.
update_filter Update the column filtering in the data frame.
update_sort Update the column sorting in the data frame.

cell_patches

express.render.data_frame.cell_patches()

Reactive calculation of the data frame's edits.

This reactive calculation that returns a list of user edits (or updated cell values) that will be applied to .data() to create the .data_patched() data frame.

Returns

: list[CellPatch]

A list of cell patches to apply to the data frame.

cell_selection

express.render.data_frame.cell_selection()

Reactive calculation of selected cell information.

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.

The value has been enhanced from it's vanilla form to include the missing cols key (or rows key) as a tuple of integers representing all column (or row) numbers. This allows for consistent usage within code when subsetting your data. These missing keys are not sent over the wire as they are independent of the selection.

Returns

: CellSelection

CellSelection representing the indices of the selected cells. If no cells are currently selected, None is returned.

data

express.render.data_frame.data()

Reactive calculation of the data frame's data.

This reactive calculation returns the render method's underlying data frame or the data frame supplied to .update_data(data), whichever has been most recently set.

The returned value is a shallow copy of the original data frame. It is possible that alterations to the .data() data frame could alter other associated data frame values. Please be cautious when using this value directly.

Returns

: IntoDataFrameT

This reactive calculation returns the render method’s underlying data frame or the data frame supplied to .update_data(data), whichever has been most recently set.

data_patched

express.render.data_frame.data_patched()

Reactive calculation of the data frame's patched data.

This method returns the .data() data frame with all .cell_patches() patches applied.

The returned value is a shallow copy of the original data frame. It is possible that alterations to the .data_patched() data frame could alter other associated data frame values. Please be cautious when using this value directly.

Returns

: IntoDataFrameT

The patched data frame.

data_view

express.render.data_frame.data_view(selected=False)

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

This function will return the .data_patched() data frame with the user's sorting and filtering applied. It represents the data frame as viewed by the user within the browser.

The returned value is a shallow copy of the original data frame. It is possible that alterations to the .data_view() data frame could alter other associated date frame values. 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 (all rows).

Returns

: IntoDataFrameT

A view of the (possibly selected) data frame as seen in the browser.

See Also

Examples

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

## file: app.py
from shared import mtcars

from shiny import reactive
from shiny.express import render, ui

df = reactive.value(mtcars.iloc[:, range(4)])


with ui.layout_columns(col_widths=[4, 4, 4]):
    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Editable data frame
                * Edit the cells!
                * Sort the columns!
                """
            )

            @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"])

    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Updated data from the first data frame
                * Select the rows!
                * Filter and sort the columns!
                """
            )

            @render.data_frame
            def df_edited():
                return render.DataGrid(
                    df_original.data_view(),
                    selection_mode="rows",
                    filters=True,
                )

    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Selected data from the second data frame
                * Sort the columns!
                """
            )

            @render.data_frame
            def df_selected():
                return render.DataGrid(
                    df_edited.data_view(selected=True),
                    selection_mode="none",
                )


## 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")

data_view_rows

express.render.data_frame.data_view_rows()

Reactive calculation of the data frame's user view row numbers.

This value is a wrapper around input.<id>_data_view_rows(), where <id> is the id of the data frame output.

Returns

: tuple[int, …]

The row numbers of the data frame that are currently being viewed in the browser after sorting and filtering has been applied.

filter

express.render.data_frame.filter()

Reactive calculation of the data frame's column filters.

Returns

: tuple[ColumnFilter, …]

An array of column number and value information. If the column type is a number, a tuple of (min, max) is used for value. If no min (or max) value is set, None is used in its place. If the column type is a string, the string value is used for value.

input_cell_selection

express.render.data_frame.input_cell_selection()

[Deprecated] Reactive calculation of selected cell information.

Please use ~shiny.express.render.data_frame's .cell_selection() method instead.

selection_modes

express.render.data_frame.selection_modes()

Reactive calculation of the data frame's possible selection modes.

Returns

: SelectionModes

The possible selection modes for the data frame.

set_patch_fn

express.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.

Examples

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

## file: app.py
from shared import mtcars

from shiny import reactive
from shiny.express import render, ui

df = reactive.value(mtcars.iloc[:, range(4)])


with ui.layout_columns(col_widths=[4, 4, 4]):
    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Editable data frame
                * Edit the cells!
                * Sort the columns!
                """
            )

            @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"])

    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Updated data from the first data frame
                * Select the rows!
                * Filter and sort the columns!
                """
            )

            @render.data_frame
            def df_edited():
                return render.DataGrid(
                    df_original.data_view(),
                    selection_mode="rows",
                    filters=True,
                )

    with ui.card():
        with ui.card_header():
            ui.markdown(
                """
                ##### Selected data from the second data frame
                * Sort the columns!
                """
            )

            @render.data_frame
            def df_selected():
                return render.DataGrid(
                    df_edited.data_view(selected=True),
                    selection_mode="none",
                )


## 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")

set_patches_fn

express.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.

Parameters

fn : PatchesFn | PatchesFnSync

A function that accepts a kwarg patches and returns a list of (possibly updated) patches to apply to the data frame.

Examples

#| 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 reactive
from shiny.express import render, ui

here = Path(__file__).parent
mtcars_df = reactive.value(pd.read_csv(here / "mtcars.csv").iloc[:, range(4)])

# A copy of the data frame that will store all the edits
edited_df = reactive.value(None)


# Copy mtcars_df to edited_df when mtcars_df changes and on initial load
@reactive.effect
def _sync_mtcars_to_edited_df():
    edited_df.set(mtcars_df())


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).
    """
)

with ui.card():

    @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
        df = edited_df().copy()
        for patch in patches:
            df.iloc[patch["row_index"], patch["column_index"]] = patch["value"]
        edited_df.set(df)
        df.to_csv(here / "mtcars.csv", index=False)
        print("Saved the edited data to './mtcars.csv'")

        return patches


## 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

sort

express.render.data_frame.sort()

Reactive calculation of the data frame's column sorting information.

Returns

: tuple[ColumnSort, …]

An array of column number and is descending information.

update_cell_selection

express.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 | BrowserCellSelection

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.

update_cell_value

express.render.data_frame.update_cell_value(value, *, row, col)

Update the value of a cell in the data frame.

Calling this method will set a new entry in .cell_patches(). It will not reset the user's sorting or filtering of their rendered data frame.

Parameters

value : CellValue

The new value to set the cell to.

row : int

The row index of the cell to update.

column :

The column index of the cell to update.

update_data

express.render.data_frame.update_data(data)

Update the data frame with new data.

Calling this method will update the .data() data frame with new data and will remove all .cell_patches(). It will not reset the user's sorting or filtering of their rendered data frame. Any incompatible sorting or filtering settings will be silently dropped.

Parameters

data : IntoDataFrameT

The new data to render.

update_filter

express.render.data_frame.update_filter(filter)

Update the column filtering in the data frame.

Parameters

filter : ListOrTuple[ColumnFilter] | None

A list of column filtering information. If None, filtering will be removed.

Examples

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

## file: app.py
from shared import mtcars

from shiny import reactive
from shiny.express import input, render, ui

data = reactive.value(mtcars.iloc[:, range(4)])
with ui.card():
    with ui.layout_column_wrap(fill=False):
        ui.input_action_button("btn", "Filter on columns 0, 1, and 3")
        ui.input_action_button("reset", "Reset column filters")

    @render.data_frame
    def df():
        return render.DataGrid(data(), filters=True)


@reactive.effect
@reactive.event(input.reset)
async def _():
    await df.update_filter(None)


@reactive.effect
@reactive.event(input.btn)
async def _():
    await df.update_filter(
        [
            {"col": 0, "value": [19, 25]},
            {"col": 1, "value": [None, 6]},
            {"col": 3, "value": [100, None]},
        ]
    )


## 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")

update_sort

express.render.data_frame.update_sort(sort)

Update the column sorting in the data frame.

The sort will be applied in reverse order so that the first value has the highest precedence. This mean ties will go to the second sort column (and so on).

Parameters

sort : ListOrTuple[ColumnSort | int] | int | ColumnSort | None

A list of column sorting information. If None, sorting will be removed. int values will be upgraded to {"col": int, "desc": <DESC>} where <DESC> is True if the column is number like and False otherwise.

Examples

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

## file: app.py
from shared import mtcars

from shiny import reactive
from shiny.express import input, render, ui

data = reactive.value(mtcars.iloc[:, range(4)])

with ui.card():
    ui.input_action_button("btn", "Sort on columns 1↑ and 3↓")

    @render.data_frame
    def df():
        return render.DataGrid(data())


@reactive.effect
@reactive.event(input.btn)
async def _():
    await df.update_sort([{"col": 1, "desc": False}, {"col": 3, "desc": True}])


## 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")