Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ quartodoc:
- GT.opt_table_outline
- GT.opt_table_font
- GT.opt_stylize
- GT.opt_css
- title: Export
desc: >
There may come a day when you need to export a table to some specific format. A great method
Expand Down
97 changes: 96 additions & 1 deletion great_tables/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def tab_options(
table_margin_left: str | None = None,
table_margin_right: str | None = None,
table_background_color: str | None = None,
table_additional_css: list[str] | None = None,
table_additional_css: str | list[str] | None = None,
table_font_names: str | list[str] | None = None,
table_font_size: str | None = None,
table_font_weight: str | int | float | None = None,
Expand Down Expand Up @@ -553,6 +553,11 @@ def tab_options(
if isinstance(modified_args["table_font_names"], str):
modified_args["table_font_names"] = [modified_args["table_font_names"]]

# - `table_additional_css` should be a list but if given as a string, ensure it is list
if "table_additional_css" in modified_args:
if isinstance(modified_args["table_additional_css"], str):
modified_args["table_additional_css"] = [modified_args["table_additional_css"]]

new_options_info = {
k: replace(getattr(self._options, k), value=v) for k, v in modified_args.items()
}
Expand Down Expand Up @@ -1427,6 +1432,96 @@ def dict_omit_keys(dict: dict[str, str], omit_keys: set[str]) -> dict[str, str]:
return res


def opt_css(
self: GTSelf,
css: str,
add: bool = True,
allow_duplicates: bool = False,
) -> GTSelf:
"""
Option to add custom CSS for the table.

`opt_css()` makes it possible to add extra CSS rules to a table. This CSS will be added after
the compiled CSS that Great Tables generates automatically when the object is transformed to an
HTML output table.

Parameters
----------
css
The CSS to include as part of the rendered table's `<style>` element.
add
If `True`, the default, the CSS is added to any already-defined CSS (typically from
previous calls of `opt_css()` or `tab_options(table_additional_css=...)`). If this is set to
`False`, the CSS provided here will replace any previously-stored CSS.
allow_duplicates
When this is `False` (the default), the CSS provided here won't be added (provided that
`add=True`) if it is seen in the already-defined CSS.

Returns
-------
GT
The GT object is returned. This is the same object that the method is called on so that
we can facilitate method chaining.

Examples
--------
Let's use the `exibble` dataset to create a simple, two-column table (keeping only the `num` and
`currency` columns). Through use of the `opt_css()` method, we can insert CSS rulesets as a
string. We need to ensure that the table ID is set explicitly (we've done so here with the ID
value of `"one"`, setting it up with `GT(id=)`).

```{python}
from great_tables import GT, exibble
import polars as pl

exibble_mini = pl.from_pandas(exibble).select(["num", "currency"])

(
GT(exibble_mini, id="one")
.fmt_currency(columns="currency", currency="HKD")
.fmt_scientific(columns="num")
.opt_css(
css='''
#one .gt_table {
background-color: skyblue;
}
#one .gt_row {
padding: 20px 30px;
}
#one .gt_col_heading {
text-align: center !important;
}
'''
)
)
```
"""

# Get the current additional CSS from `_options.table_additional_css`
existing_additional_css: list[str] = self._options.table_additional_css.value or []

# Convert CSS to a consistent format by stripping any leading or trailing whitespace
css = css.strip()

if not add:
# If `add=False`, or if `css` is empty, replace the existing CSS
additional_css = [css] if css else []
elif not css:
# If `css` is empty and we are adding CSS, keep the existing CSS
additional_css = existing_additional_css
elif not allow_duplicates and css in existing_additional_css:
# if CSS already exists and we don't allow duplicates, return self
return self
else:
# Add the new CSS to the existing CSS
additional_css = existing_additional_css + [css]

# Use tab_options() to set the additional CSS
res = tab_options(self, table_additional_css=additional_css)

return res


@dataclass
class StyleMapper:
table_hlines_color: str
Expand Down
10 changes: 6 additions & 4 deletions great_tables/_scss.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def compile_scss(
data._google_font_imports.to_css() + "\n" if data._google_font_imports.to_css() else ""
)

# Prepend any additional CSS ----
# Process any additional CSS that will be appended at the end ----
additional_css = data._options.table_additional_css.value

# Determine if there are any additional CSS statements
Expand All @@ -159,11 +159,11 @@ def compile_scss(
# separating with `\n`; use an empty string if list is empty or value is None
if has_additional_css:
additional_css_unique = OrderedSet(additional_css).as_list()
table_additional_css = "\n".join(additional_css_unique) + "\n"
table_additional_css = "\n".join(additional_css_unique)
else:
table_additional_css = ""

gt_table_class_str = f"""{table_additional_css}{gt_table_open_str} {{
gt_table_class_str = f"""{gt_table_open_str} {{
{font_family_attr}
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
Expand All @@ -187,6 +187,8 @@ def compile_scss(
if all_important:
compiled_css = re.sub(r";", " !important;", compiled_css, count=0, flags=re.MULTILINE)

finalized_css = f"{google_font_css}{gt_table_class_str}\n\n{compiled_css}"
# Assemble blocks of CSS ----
additional_css_block = f"\n{table_additional_css}\n" if has_additional_css else ""
finalized_css = f"{google_font_css}{gt_table_class_str}\n\n{compiled_css}{additional_css_block}"

return finalized_css
2 changes: 2 additions & 0 deletions great_tables/gt.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ._options import (
opt_align_table_header,
opt_all_caps,
opt_css,
opt_footnote_marks,
opt_horizontal_padding,
opt_row_striping,
Expand Down Expand Up @@ -248,6 +249,7 @@ def __init__(
opt_stylize = opt_stylize
opt_align_table_header = opt_align_table_header
opt_all_caps = opt_all_caps
opt_css = opt_css
opt_footnote_marks = opt_footnote_marks
opt_row_striping = opt_row_striping
opt_vertical_padding = opt_vertical_padding
Expand Down
109 changes: 109 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,112 @@ def test_opt_horizontal_padding_raises(gt_tbl: GT, scale: float):
gt_tbl.opt_horizontal_padding(scale=scale)

assert "`scale` must be a value between `0` and `3`." in exc_info.value.args[0]


def test_opt_css_basic():
# Test adding CSS
res = GT(exibble).opt_css(css=".gt_table { background-color: red; }")
assert res._options.table_additional_css.value == [".gt_table { background-color: red; }"]

# Test chaining CSS additions
res_2 = res.opt_css(css=".gt_row { color: blue; }")
assert len(res_2._options.table_additional_css.value) == 2
assert ".gt_table { background-color: red; }" in res_2._options.table_additional_css.value
assert ".gt_row { color: blue; }" in res_2._options.table_additional_css.value


def test_opt_css_duplicate_handling():
gt_tbl = GT(exibble)
css_rule = ".gt_table { color: green; }"

# Add same CSS twice (should only appear once)
res = gt_tbl.opt_css(css=css_rule).opt_css(css=css_rule)

assert res._options.table_additional_css.value == [css_rule]

# Test for allowing duplicates
res_2 = gt_tbl.opt_css(css=css_rule).opt_css(css=css_rule, allow_duplicates=True)

assert res_2._options.table_additional_css.value == [css_rule, css_rule]


def test_opt_css_replace_mode():
# Add initial CSS
res = (
GT(exibble).opt_css(css=".gt_table { color: red; }").opt_css(css=".gt_row { color: blue; }")
)

assert len(res._options.table_additional_css.value) == 2

# Replace all CSS with `add=False`
res_2 = res.opt_css(css=".gt_table { color: green; }", add=False)
assert res_2._options.table_additional_css.value == [".gt_table { color: green; }"]


def test_opt_css_whitespace_handling():
gt_tbl = GT(exibble)

# Test using empty-string CSS
res = gt_tbl.opt_css(css="")
assert res._options.table_additional_css.value == []

# Test whitespace-only CSS
res_2 = gt_tbl.opt_css(css=" \n \t ")
assert res_2._options.table_additional_css.value == []

# Test CSS with leading and trailing whitespace (it should get stripped)
res_3 = gt_tbl.opt_css(css=" .gt_table { color: red; } ")
assert res_3._options.table_additional_css.value == [".gt_table { color: red; }"]


def test_opt_css_multiline():
multiline_css = """
#test_table .gt_table {
background-color: skyblue;
}
#test_table .gt_row {
padding: 20px;
}"""

result = GT(exibble, id="test_table").opt_css(css=multiline_css)

expected_css = multiline_css.strip()

assert result._options.table_additional_css.value == [expected_css]


def test_opt_css_html_output():
css_rule = "#test_table .gt_table { background-color: lightblue; }"
res = GT(exibble, id="test_table").opt_css(css=css_rule)

html = res.as_raw_html()

# Check that the CSS appears in the HTML string
assert css_rule in html

# For the CSS ordering, the added CSS should come after the default CSS
default_css_pos = html.find("#test_table .gt_table { display: table;")
custom_css_pos = html.find(css_rule)

assert default_css_pos > 0
assert custom_css_pos > 0
assert custom_css_pos > default_css_pos


def test_opt_css_with_tab_options():
res = (
GT(exibble)
.tab_options(table_additional_css=".initial { color: red; }")
.opt_css(css=".added { color: blue; }")
)

css_list = res._options.table_additional_css.value

assert len(css_list) == 2
assert ".initial { color: red; }" in css_list
assert ".added { color: blue; }" in css_list

# Test that `opt_css()` rule comes after the `tab_options()` rule in the rendered HTML
html = res.as_raw_html()

assert html.index(".added { color: blue; }") > html.index(".initial { color: red; }")
Loading