Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the opt_table_font() method #272

Merged
merged 21 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e389b53
Add initial implementation of opt_table_font()
rich-iannone Apr 10, 2024
d6e5b3c
Add several tests for opt_table_font()
rich-iannone Apr 10, 2024
842c8a3
Add to _quarto.yml
rich-iannone Apr 10, 2024
913bb3a
Add example for opt_table_font()
rich-iannone Apr 10, 2024
0f1f9be
Merge branch 'main' into add-opt-table-font
rich-iannone Jun 4, 2024
767eb70
Update typing to match current style
rich-iannone Jun 6, 2024
435cfc0
Ensure that use of `stack` makes `add` False
rich-iannone Jun 6, 2024
b9b68f6
Import `default_fonts_list` into test file
rich-iannone Jun 6, 2024
4c4fc5b
Use `default_fonts_list` in test
rich-iannone Jun 6, 2024
54465db
Reduce verbosity of tests
rich-iannone Jun 6, 2024
7fda762
Add test for common ValueError on missing inputs
rich-iannone Jun 6, 2024
7279176
Remove commented code
rich-iannone Jun 6, 2024
44ee626
Add comment about forcing `add` to be `False`
rich-iannone Jun 6, 2024
e192a18
Merge branch 'main' into add-opt-table-font
rich-iannone Jun 6, 2024
5a07ea1
Add documentation example
rich-iannone Jun 18, 2024
1d73451
Merge branch 'main' into add-opt-table-font
rich-iannone Jun 18, 2024
6177385
Merge branch 'main' into add-opt-table-font
rich-iannone Jun 27, 2024
9083a56
Better handle weight as str, int, or float
rich-iannone Jun 28, 2024
41b2a0f
Simplify setting of `table_font_names=`
rich-iannone Jun 28, 2024
8c9d91b
Remove redundant statement
rich-iannone Jun 28, 2024
11ddbb6
Remove validation of numerical font weight val
rich-iannone Jun 28, 2024
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 @@ -175,6 +175,7 @@ quartodoc:
- GT.opt_vertical_padding
- GT.opt_horizontal_padding
- GT.opt_table_outline
- GT.opt_table_font
- GT.opt_stylize
- title: Export
desc: >
Expand Down
211 changes: 204 additions & 7 deletions great_tables/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, ClassVar, cast

from great_tables import _utils
from great_tables._helpers import FontStackName


if TYPE_CHECKING:
Expand All @@ -25,7 +26,7 @@
# table_additional_css: str | None = None,
table_font_names: str | list[str] | None = None,
table_font_size: str | None = None,
table_font_weight: str | None = None,
table_font_weight: str | int | float | None = None,
table_font_style: str | None = None,
table_font_color: str | None = None,
table_font_color_light: str | None = None,
Expand All @@ -44,9 +45,9 @@
heading_background_color: str | None = None,
heading_align: str | None = None,
heading_title_font_size: str | None = None,
heading_title_font_weight: str | None = None,
heading_title_font_weight: str | int | float | None = None,
heading_subtitle_font_size: str | None = None,
heading_subtitle_font_weight: str | None = None,
heading_subtitle_font_weight: str | int | float | None = None,
heading_padding: str | None = None,
heading_padding_horizontal: str | None = None,
heading_border_bottom_style: str | None = None,
Expand All @@ -57,7 +58,7 @@
heading_border_lr_color: str | None = None,
column_labels_background_color: str | None = None,
column_labels_font_size: str | None = None,
column_labels_font_weight: str | None = None,
column_labels_font_weight: str | int | float | None = None,
column_labels_text_transform: str | None = None,
column_labels_padding: str | None = None,
column_labels_padding_horizontal: str | None = None,
Expand All @@ -76,7 +77,7 @@
column_labels_hidden: bool | None = None,
row_group_background_color: str | None = None,
row_group_font_size: str | None = None,
row_group_font_weight: str | None = None,
row_group_font_weight: str | int | float | None = None,
row_group_text_transform: str | None = None,
row_group_padding: str | None = None,
row_group_padding_horizontal: str | None = None,
Expand Down Expand Up @@ -108,13 +109,13 @@
table_body_border_bottom_color: str | None = None,
stub_background_color: str | None = None,
stub_font_size: str | None = None,
stub_font_weight: str | None = None,
stub_font_weight: str | int | float | None = None,
stub_text_transform: str | None = None,
stub_border_style: str | None = None,
stub_border_width: str | None = None,
stub_border_color: str | None = None,
stub_row_group_font_size: str | None = None,
stub_row_group_font_weight: str | None = None,
stub_row_group_font_weight: str | int | float | None = None,
stub_row_group_text_transform: str | None = None,
stub_row_group_border_style: str | None = None,
stub_row_group_border_width: str | None = None,
Expand Down Expand Up @@ -1054,6 +1055,202 @@
return res


def opt_table_font(
self: GTSelf,
font: str | list[str] | None = None,
stack: FontStackName | None = None,
weight: str | int | float | None = None,
style: str | None = None,
add: bool = True,
) -> GTSelf:
"""Options to define font choices for the entire table.

The `opt_table_font()` method makes it possible to define fonts used for an entire table. Any
font names supplied in `font=` will (by default, with `add=True`) be placed before the names
present in the existing font stack (i.e., they will take precedence). You can choose to base the
font stack on those provided by the [`system_fonts()`](`system_fonts.md`) helper function by
providing a valid keyword for a themed set of fonts. Take note that you could still have
entirely different fonts in specific locations of the table. To make that possible you would
need to use [`tab_style()`](`great_tables.GT.tab_style`) in conjunction with
[`style.text()`](`great_tables.style.text`).

Parameters
----------
font
One or more font names available on the user system. This can be a string or a list of
strings. The default value is `None` since you could instead opt to use `stack` to define
a list of fonts.
stack
A name that is representative of a font stack (obtained via internally via the
`system_fonts()` helper function. If provided, this new stack will replace any defined fonts
and any `font=` values will be prepended.
style
An option to modify the text style. Can be one of either `"normal"`, `"italic"`, or
`"oblique"`.
weight
Option to set the weight of the font. Can be a text-based keyword such as `"normal"`,
`"bold"`, `"lighter"`, `"bolder"`, or, a numeric value between `1` and `1000`. Please note
that typefaces have varying support for the numeric mapping of weight.
add
Should fonts be added to the beginning of any already-defined fonts for the table? By
default, this is `True` and is recommended since those fonts already present can serve as
fallbacks when everything specified in `font` is not available. If a `stack=` value is
provided, then `add` will automatically set to `False`.

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.

Possibilities for the `stack` argument
--------------------------------------

There are several themed font stacks available via the [`system_fonts()`](`system_fonts.md`)
helper function. That function can be used to generate all or a segment of a list supplied to
the `font=` argument. However, using the `stack=` argument with one of the 15 keywords for the
font stacks available in [`system_fonts()`](`system_fonts.md`), we could be sure that the
typeface class will work across multiple computer systems. Any of the following keywords can be
used with `stack=`:

- `"system-ui"`
- `"transitional"`
- `"old-style"`
- `"humanist"`
- `"geometric-humanist"`
- `"classical-humanist"`
- `"neo-grotesque"`
- `"monospace-slab-serif"`
- `"monospace-code"`
- `"industrial"`
- `"rounded-sans"`
- `"slab-serif"`
- `"antique"`
- `"didone"`
- `"handwritten"`

Examples
--------
Let's use a subset of the `sp500` dataset to create a small table. With `opt_table_font()` we
can add some preferred font choices for modifying the text of the entire table. Here we'll use
the `"Superclarendon"` and `"Georgia"` fonts (the second font serves as a fallback).

```{python}
import polars as pl
from great_tables import GT
from great_tables.data import sp500

sp500_mini = pl.from_pandas(sp500).slice(0, 10).drop(["volume", "adj_close"])

(
GT(sp500_mini, rowname_col="date")
.fmt_currency(use_seps=False)
.opt_table_font(font=["Superclarendon", "Georgia"])
)
```

In practice, both of these fonts are not likely to be available on all systems. The
`opt_table_font()` method safeguards against this by prepending the fonts in the `font=` list to
the existing font stack. This way, if both fonts are not available, the table will fall back to
using the list of default table fonts. This behavior is controlled by the `add=` argument, which
is `True` by default.

With the `sza` dataset we'll create a two-column, eleven-row table. Within `opt_table_font()`,
the `stack=` argument will be supplied with the "rounded-sans" font stack. This sets up a family
of fonts with rounded, curved letterforms that should be locally available in different
computing environments.

```{python}
from great_tables.data import sza

sza_mini = (
pl.from_pandas(sza)
.filter((pl.col("latitude") == "20") & (pl.col("month") == "jan"))
.drop_nulls()
.drop(["latitude", "month"])
)

(
GT(sza_mini)
.opt_table_font(stack="rounded-sans")
.opt_all_caps()
)
```
"""

if font is None and stack is None:
raise ValueError("Either `font=` or `stack=` must be provided.")

# Get the existing fonts for the table from the options; we may either prepend to this
# list or replace it entirely
existing_fonts = self._options.table_font_names.value

# If `existing_fonts` is not a list, throw an error
if not isinstance(existing_fonts, list):
raise ValueError("The value from `_options.table_font_names` must be a list.")

Check warning on line 1190 in great_tables/_options.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_options.py#L1190

Added line #L1190 was not covered by tests

res = self

# If `stack` is provided, we ignore the `add` argument and always replace the existing fonts;
# This is because users are likely to want to use the font stack as a whole the set is safe
# enough to use across different systems (so keeping fallbacks usually won't be seen as
# necessary)
if stack is not None:
add = False
rich-iannone marked this conversation as resolved.
Show resolved Hide resolved

if font is not None:

# If `font` is a string, convert it to a list
if isinstance(font, str):
font = [font]

else:
font = []

if stack is not None:

# Case where value is given to `stack=` and this is a keyword that returns a
# list of fonts (i.e., the font stack); in this case we combine with `font=` values
# (if provided) and we *always* replace the existing fonts (`add=` is ignored)
from great_tables._helpers import system_fonts

font_stack = system_fonts(name=stack)
combined_fonts = font + font_stack
elif add:
# Case where `font=` is prepended to existing fonts
combined_fonts = font + existing_fonts
else:
# Case where `font=` replacing existing fonts
combined_fonts = font

res = tab_options(res, table_font_names=combined_fonts)

if weight is not None:

if isinstance(weight, int) or isinstance(weight, float):
rich-iannone marked this conversation as resolved.
Show resolved Hide resolved

if weight < 1 or weight > 1000:
rich-iannone marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(

Check warning on line 1233 in great_tables/_options.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_options.py#L1232-L1233

Added lines #L1232 - L1233 were not covered by tests
"If `weight=` provided as a numeric value, it must be between 1 and 1000."
)

weight = str(round(weight))

Check warning on line 1237 in great_tables/_options.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_options.py#L1237

Added line #L1237 was not covered by tests

elif not isinstance(weight, str):
raise TypeError(

Check warning on line 1240 in great_tables/_options.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_options.py#L1240

Added line #L1240 was not covered by tests
"`weight=` must be a numeric value between 1 and 1000 or a text-based keyword."
)

res = tab_options(res, table_font_weight=weight)
res = tab_options(res, column_labels_font_weight=weight)

if style is not None:

res = tab_options(res, table_font_style=style)

return res


def opt_stylize(self: GTSelf, style: int = 1, color: str = "blue") -> GTSelf:
"""
Stylize your table with a colorful look.
Expand Down
2 changes: 2 additions & 0 deletions great_tables/gt.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
opt_row_striping,
opt_stylize,
opt_table_outline,
opt_table_font,
opt_vertical_padding,
tab_options,
)
Expand Down Expand Up @@ -244,6 +245,7 @@ def __init__(
opt_vertical_padding = opt_vertical_padding
opt_horizontal_padding = opt_horizontal_padding
opt_table_outline = opt_table_outline
opt_table_font = opt_table_font

cols_align = cols_align
cols_width = cols_width
Expand Down
45 changes: 45 additions & 0 deletions tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytest
from great_tables import GT, exibble, md
from great_tables._scss import compile_scss
from great_tables._gt_data import default_fonts_list


def test_options_overwrite():
Expand Down Expand Up @@ -324,3 +325,47 @@ def test_scss_from_opt_table_outline(gt_tbl: GT, snapshot):
assert getattr(gt_tbl_outline._options, f"table_border_{part}_color").value == "blue"

assert snapshot == compile_scss(gt_tbl_outline, id="abc", compress=False)


def test_opt_table_font_add_font():

gt_tbl = GT(exibble).opt_table_font(font="Arial", weight="bold", style="italic")

assert gt_tbl._options.table_font_names.value == ["Arial"] + default_fonts_list
assert gt_tbl._options.table_font_weight.value == "bold"
assert gt_tbl._options.table_font_style.value == "italic"


def test_opt_table_font_replace_font():

gt_tbl = GT(exibble).opt_table_font(font="Arial", weight="bold", style="bold", add=False)

assert gt_tbl._options.table_font_names.value == ["Arial"]
assert gt_tbl._options.table_font_weight.value == "bold"
assert gt_tbl._options.table_font_style.value == "bold"


def test_opt_table_font_use_stack():

gt_tbl = GT(exibble).opt_table_font(stack="humanist")

assert gt_tbl._options.table_font_names.value[0] == "Seravek"
assert gt_tbl._options.table_font_names.value[-1] == "Noto Color Emoji"


def test_opt_table_font_use_stack_and_system_font():

gt_tbl = GT(exibble).opt_table_font(font="Comic Sans MS", stack="humanist")

assert gt_tbl._options.table_font_names.value[0] == "Comic Sans MS"
assert gt_tbl._options.table_font_names.value[1] == "Seravek"
assert gt_tbl._options.table_font_names.value[-1] == "Noto Color Emoji"


def test_opt_table_font_raises():

# Both `font` and `stack` cannot be `None`
with pytest.raises(ValueError) as exc_info:
GT(exibble).opt_table_font(font=None, stack=None)

assert "Either `font=` or `stack=` must be provided." in exc_info.value.args[0]
Loading