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

Prototype class-based options API

Open machow opened this issue 6 months ago • 2 comments

Currently, GT.tab_options() exposes about 150 options that can be set. This is great for customization, but can make it a bit cumbersome to find things. Moreover, because .tab_options() is a method, it only allows setting options via an action. But for themes you often want a configuration object.

Background

tab_options categorizes all the options using three dimensions: <table_part>_<options_type>_<option_attr>. For example, table_body_border_bottom has the following:

  • table part: table_body
  • options type: e.g. border
  • option attr: bottom

There are two key aspects to how tab_options is documented:

  • The function signature is ordered by table_part (e.g. all table_body parameters are together)
  • The parameter documentation is sometimes grouped by option type and attr
    • e.g. all *_font_size parameters are documented together.

Some engineers might balk at them not following a single rule (e.g. ordering signature and parameter docs by table_part), but it makes certain questions quick to answer. For example, in the documentation, it's very quick to identify all the table parts where font size can be set (at the expense of identifying every option for a given table part).

Experimental solution

Let's try to implement an opts module, that exposes each <table_part> option set as a dataclass. This will allow people to set options like this...

from great_tables import GT, exibble, opts

# add a single set of container options
GT(exibble).tab_options(opts.container(width = "1000px", height="500px"))

# add multiple sets of options
GT(exibble).tab_options(
    opts.container(width = "1000px", height="500px"),
    opts.table(font_size="20pt")
)

Because opts is only data, you can store sets of options easily...

from great_tables import GT, exibble, opts

my_theme = [
    opts.container(width = "1000px", height="500px"),
    opts.table(font_size="20pt")
]

GT(exibble).tab_options(*my_theme)

Finally, exploring sets of options should be quick, since you can tab complete table parts...

from great_tables import GT, exibble, opts

# which table part am I setting options on?
opts.<tab>

# *sees all table parts*. Oh yeah, column_labels
opts.column_labels(...)

# *sees argument documentation specific to column labels*

Implementation

Each option set could have a resolve method, for double dispatching the setting of options on a GT object:

from ._gt_data import GTData
import dataclasses as _dc
from typing import ClassVar

class Option:
    _name: ClassVar

    # NOTE: this is double dispatch, because GT.tab_options would call something like
    # option_object.resolve(self)
    def resolve(self, gt: GTData) -> GTData:
        """Set options onto a GT object."""

        field_vals = {
            self._name + "_" + field.name: getattr(self, field.name) for field in _dc.fields(self)
        }
        new_options = _dc.replace(gt._options, **field_vals)

        return self._replace(_options=new_options)


@_dc.dataclass
class container(Option):
    """DOCSTRING HERE"""

    width: str
    height: str
    overflow_x: str
    overflow_y: str

    _name: ClassVar = "container"

Potential downsides

One downside of the above approach is that surfacing options would be done hierarchically. This lets you target the information you need, but at the cost of making grouping by things like *_font_size impossible.

Option sets need not be mutually exclusive, so we could always add a opts.font_size. However, it feels like it might be a lot to expose people to for now.

Relatedly, methods like GT.opt_horizontal_padding() do the job of setting horizontal_padding across many table parts. So, in a sense, there's maybe a future opportunity to expose special option sets in the future.

Other considerations

In the GT R package, it seems like many of the same styles can be configured both through tab_options() or tab_style(). For example, here are two ways to set footnote bg color:

gt_obj = gt(mtcars) %>% tab_header(title = "abc")

gt_obj %>%
  tab_options(heading.background.color="grey")

gt_obj %>%
  tab_style(cell_fill("grey"), cells_title())

machow avatar Feb 02 '24 16:02 machow