great-tables icon indicating copy to clipboard operation
great-tables copied to clipboard

Add a lazy form of GT

Open machow opened this issue 1 year ago • 2 comments

(From pairing with @schloerke)

Currently, when applying pieces like formatting (e.g. with GT.fmt_number()), the formatting is not applied right away, but just before rendering. Delaying applying things like formatting is important because it opens the possibility of separating two activities:

  • declaring the table output you want (e.g. formatting, styling)
  • feeding in data

For example, plotnine doesn't apply anything until it goes to render the plot, so its more like a recipe:

from plotnine import *
from plotnine.data import mtcars

# specify the plot (w/o data)
p = ggplot(aes("cyl", "mpg")) + geom_point()

# feed in data for plot
mtcars >> p

# feed in slightly different data
mtcars.head() >> p

Potential solutions

  1. Change existing GT: make current methods all behave lazily (still can do some eager validation if user has passed data)
  2. Wrap existing GT: Use the data decorator pattern to create class that simply records method calls.

Benefits to tools like Shiny

shiny is a dashboarding tool.

Before laziness

from shiny.express import input, render, ui
from great_tables import GT, exibble, style, loc

def create_table(data) -> GT:
    return (
        GT(data)
        .tab_style(style.fill("yellow"), loc.body("num", lambda df: df.num > 10))
    )

@render.data_frame
def frame():
    data = exibble.head()

    # NOTE: this DataGrid(<GT object>) currently unsupported, just imagine it is
    return render.DataGrid(create_table(data), editable=True)

Note that shiny DataGrid only sees the eagerly computed GT object (incl styles). However, if an edit is made, shiny wants to recompute that GT "pattern", as if create_table() were called over the edited object. For example, if you updated a row in num to be greater than 10, it should become yellow.

After laziness

In order for shiny to be able to re-apply the changes, it would need a method like GT.reapply(some_data) that would run the lazy GT pattern on some_data.

from shiny.express import input, render, ui
from great_tables import GT, exibble, style, loc

@render.data_frame
def frame():
    data = exibble.head()

    gt = (
        GT(data)
        .tab_style(style.fill("yellow"), loc.body("num", lambda df: df.num > 10))
    )

    # gt now has .reapply method, to rerender on passed in data
    return render.DataGrid(gt, editable=True)

Example

The same code should display styles, formatting as if they were computed on the edited data. For example...

  • I use .fmt_number("num", decimals = 1)
  • A row of num starts as 1.234, but gets rendered as 1.2
  • I edit that row from 1.2 to 5.678
  • Shiny re-runs the GT pattern, and updates that row value to 5.7
    • note that currently shiny prohibits editing the same cell until you see the updated table
    • however, editing a cell could produce changes in any part of the table

machow avatar Jul 11 '24 16:07 machow