great-tables
great-tables copied to clipboard
Prototype class-based options API
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.
- e.g. all
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())