Repository: posit-dev/py-shiny (language: python)

Issue:
feat(data frame): Support basic cell styling
~~POC for styling cells via `great_tables` package.  ~~ POC proven. `great_tables` support pushed to later release.

Using this approach as `great_tables` has generic styling support, whereas the `styler` component of `pandas` is only for pandas and `great_tables` is already integrated into `polars`. Hopefully for the next release, both py-shiny and great-tables will be leveraging [`narwhals`](https://github.com/narwhals-dev/narwhals) ("Lightweight and extensible compatibility layer between Polars, pandas, cuDF, Modin, and more"). This way, both py-shiny gets styling support for any data frame package! (Related: #1439)

-------------------

Example excerpt:

```python
@render.data_frame
def summary_data():
    df_gt = gt.GT(df).tab_style(
        [
            gt.style.fill(color="purple"),
            gt.style.borders(color="green", style="dashed"),
        ],
        gt.loc.body("Species", [1, 2]),
    )

    # Current working code
    return render.DataGrid(
        df,
        styles=df_gt._styles,
    )
    
    # Ideal code
    return render.DataGrid(df_gt)
```

![Screenshot 2024-06-21 at 11 11 35 AM](https://github.com/posit-dev/py-shiny/assets/93231/b4c1b87e-56a1-4879-8493-bc8c7ae74036)








-----------------------

Update: June 27th:

While the `great_tables` integration was a success, it brought up problems for how `render.data_frame` was to interact with the `GT` object. We will approach this subject in a 1.1 release.

For now, the `styles=` parameter has been added to `render.DataTable` and `render.DataGrid`. This value can either be a list of style info objects or function (given data as a param) that returns a list of style objects.  This style function will be called after any cells have been updated as styling for the whole table may change due to a single cell update.

Ex:

```python
from palmerpenguins import load_penguins_raw

from shiny import App, Inputs, render, ui

df = load_penguins_raw()


df_styles: list[render.StyleInfo] = [
    {
        "location": "body",
        "rows": None,
        "cols": None,
        "style": {"background-color": "lightblue"},
    },
    {
        "location": "body",
        "rows": [1, 2],
        "cols": "Species",
        "style": {
            "background-color": "purple",
            "border-color": "green",
            "border-style": "dashed",
        },
    },
    {
        "location": "body",
        "rows": 2,
        "cols": ["Region"],
        "style": {"background-color": "yellow"},
    },
    {
        "location": "body",
        "rows": None,
        "cols": [4],  # "Island",
        "style": {"background-color": "red"},
    },
    {
        "location": "body",
        "rows": [False, True, False, False, False],
        "cols": [False, False, False, False, True, True],  # "Stage",
        "style": {"background-color": "green"},
    },
]

def server(input: Inputs):
    @render.data_frame
    def list_styles():
        return render.DataTable(
            df,
            selection_mode=("rows"),
            editable=True,
            # filters=True,
            styles=df_styles,
        )

```


Discussion / hints:

Fix patch:
diff --git a/shiny/render/__init__.py b/shiny/render/__init__.py
index 854e19833..721b3634c 100644
--- a/shiny/render/__init__.py
+++ b/shiny/render/__init__.py
@@ -12,7 +12,7 @@
     DataTable,
     data_frame,
 )
-from ._data_frame_utils import CellSelection
+from ._data_frame_utils import CellSelection, StyleInfo
 from ._deprecated import (  # noqa: F401
     RenderFunction,  # pyright: ignore[reportUnusedImport]
     RenderFunctionAsync,  # pyright: ignore[reportUnusedImport]
@@ -46,4 +46,5 @@
     "CellPatch",
     "CellValue",
     "CellSelection",
+    "StyleInfo",
 )
diff --git a/shiny/render/_data_frame_utils/__init__.py b/shiny/render/_data_frame_utils/__init__.py
index 5687a5229..a7f2edfb1 100644
--- a/shiny/render/_data_frame_utils/__init__.py
+++ b/shiny/render/_data_frame_utils/__init__.py
@@ -24,6 +24,7 @@
     SelectionModes,
     as_cell_selection,
 )
+from ._styles import StyleInfo
 
 __all__ = (
     "AbstractTabularData",
@@ -46,4 +47,5 @@
     "SelectionMode",
     "SelectionModes",
     "as_cell_selection",
+    "StyleInfo",
 )


Test patch:
diff --git a/tests/playwright/shiny/components/data_frame/styles/app.py b/tests/playwright/shiny/components/data_frame/styles/app.py
index 5518697a9..f7f5e7f26 100644
--- a/tests/playwright/shiny/components/data_frame/styles/app.py
+++ b/tests/playwright/shiny/components/data_frame/styles/app.py
@@ -5,7 +5,6 @@
 from palmerpenguins import load_penguins_raw
 
 from shiny import App, Inputs, render, ui
-from shiny.render._data_frame_utils._styles import StyleInfo
 
 # Load the dataset
 penguins = load_penguins_raw()
@@ -43,7 +42,7 @@
 # )
 # df_styles = gt_styles(df_gt)
 
-df_styles: list[StyleInfo] = [
+df_styles: list[render.StyleInfo] = [
     {
         "location": "body",
         "rows": None,
@@ -105,11 +104,11 @@ def fn_styles():
 
         counter = 0
 
-        def df_styles_fn(data: pd.DataFrame) -> list[StyleInfo]:
+        def df_styles_fn(data: pd.DataFrame) -> list[render.StyleInfo]:
             nonlocal counter
             counter = (counter + 1) % len(df_styles)
 
-            ret: list[StyleInfo] = []
+            ret: list[render.StyleInfo] = []
             for style in df_styles:
                 style_val = style.get("style", None)
                 if style_val is None: