posit-dev / py-shiny

Shiny for Python
https://shiny.posit.co/py/
MIT License
1.24k stars 70 forks source link

Conditional coloring for dataframe outputs #1472

Open corey-dawson opened 3 months ago

corey-dawson commented 3 months ago

Right now, there doesn't seem to be any options for conditionally coloring rows and cells in dataframe outputs (datagrid and datatable). This is especially important for analytical dashboards and data. Requesting a new feature or any recommendations on how to achieve conditional coloring in Python Shiny's current state

schloerke commented 2 months ago

Good news! You can do this manually as of #1475 . That PR will be released in the v1.0.0 release shortly.

Type information for each StyleInfo item in styles= for render.DataGrid and render.DataTable objects being returned in a @render.data_frame output function:

StyleInfoBody = TypedDict(
    "StyleInfoBody",
    {
        "location": Required[Literal["body"]],
        "rows": NotRequired[Union[int, ListOrTuple[int], ListOrTuple[bool], None]],
        "cols": NotRequired[
            Union[str, int, ListOrTuple[str], ListOrTuple[int], ListOrTuple[bool], None]
        ],
        "style": NotRequired[Union[Dict[str, Jsonifiable], None]],
        "class": NotRequired[Union[str, None]],
    },
)
StyleInfo = StyleInfoBody

Example

import pandas as pd

from shiny.express import render

df_styles = [
    {
        "location": "body",
        "rows": [2, 4],
        "cols": [2, 4],
        "style": {
            "background-color": "coral",
        },
    }
]

n = 6
df = pd.DataFrame(
    {
        "a": range(n),
        "b": range(n, n * 2),
        "c": range(n * 2, n * 3),
        "d": range(n * 3, n * 4),
        "e": range(n * 4, n * 5),
    }
)

@render.data_frame
def my_df():
    return render.DataGrid(
        df,
        styles=df_styles,
    )

Screenshot 2024-07-10 at 9 37 44 AM


We are continuing to explore using great_tables as a way to set the styles and formatting of the cells. But for the v1 release, we ran into more problems than we were willing to let slide. However, integrating great_tables is still my end goal as its API to define lazy expressions that can create conditional formatting is very convenient!

For now, you'll need to define the style information yourself. 🫤 But at least it can be defined! 🥳


(Leaving issue open as conditional styling has not be implemented. Only cell styling.)

maxmoro commented 2 months ago

This is great and a very good solution for the 1.0. Thank you. Can styles manage also columns width? Or will it be addressed in a different way?

schloerke commented 2 months ago

Yes! ... a pleasant surprise!

import pandas as pd

from shiny.express import render

df_styles = [
    {
        "location": "body",
        "rows": [2, 4],
        "cols": [2, 4],
        "style": {
            "background-color": "coral",
            "width": "300px",
            "height": "100px",
        },
    }
]

n = 6
df = pd.DataFrame(
    {
        "a": range(n),
        "b": range(n, n * 2),
        "c": range(n * 2, n * 3),
        "d": range(n * 3, n * 4),
        "e": range(n * 4, n * 5),
    }
)

@render.data_frame
def my_df():
    return render.DataGrid(
        df,
        styles=df_styles,
    )

Screenshot 2024-07-10 at 11 48 13 AM

roivant-matts commented 2 months ago

Maybe I miss something, but if we wanted to style 14 and 28 how is it done (ie both rows and both columns but not combinatorial)? Two dictionaries in the styles array?

schloerke commented 2 months ago

if we wanted to style 14 and 28 how is it done (ie both rows and both columns but not combinatorial)? Two dictionaries in the styles array?

If rows is missing, or None, then the corresponding style or class values will be applied to all rows. Same goes for cols. If cols is missing or None, then the style or class values will be applied to all columns.

If both rows and cols are supplied, then the combination of all rows and cols values will be made and their corresponding style or class values will be applied. If no rows or cols are supplied, then it will be applied to every cell.

Obviously it is nice to have multiple style definitions for different locations, so multiple StyleInfo objects can be supplied to the styles= parameter.

Updating the example above...

import pandas as pd

from shiny.express import render

df_styles = [
    {
        "location": "body",
        "style": {
            "background-color": "lightblue",
            "border": "transparent",
            "color": "transparent",
        },
    },
    {
        "location": "body",
        "rows": [2],
        "cols": [2],
        "style": {
            "background-color": "yellow",
            "width": "100px",
            "height": "75px",
        },
    },
    {
        "location": "body",
        "cols": [1, 3, 5, 7],
        "style": {
            "background-color": "yellow",
        },
    },
    {
        "location": "body",
        "rows": [3],
        "cols": [7],
        "style": {
            "background-color": "lightblue",
        },
    },
]

n = 5
df = pd.DataFrame(
    {
        "a": range(n),
        "b": range(n, n * 2),
        "c": range(n * 2, n * 3),
        "d": range(n * 3, n * 4),
        "e": range(n * 4, n * 5),
        "f": range(n * 5, n * 6),
        "g": range(n * 6, n * 7),
        "h": range(n * 7, n * 8),
        "i": range(n * 8, n * 9),
    }
)

@render.data_frame
def my_df():
    return render.DataGrid(
        df,
        styles=df_styles,
    )

Screenshot 2024-07-10 at 4 47 17 PM

roivant-matts commented 2 months ago

Thanks. Really looking forward to this!

maxmoro commented 2 months ago

Amazing! Thank you.

slackmodel commented 4 days ago

How do I apply styles to table column headers?

schloerke commented 4 days ago

@slackmodel Currently there is no support for conditional styling for table headers.

However you can do regular css styling that applies to them.