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

feat: add fmt_icon() method #515

Merged
merged 21 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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 @@ -132,6 +132,7 @@ quartodoc:
- GT.fmt_markdown
- GT.fmt_units
- GT.fmt_image
- GT.fmt_icon
- GT.fmt_nanoplot
- GT.fmt
- GT.data_color
Expand Down
285 changes: 285 additions & 0 deletions great_tables/_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from babel.dates import format_date, format_datetime, format_time
from typing_extensions import TypeAlias

import faicons

from ._gt_data import FormatFn, FormatFns, FormatInfo, GTData
from ._helpers import px
from ._locale import _get_currencies_data, _get_default_locales_data, _get_locales_data
Expand Down Expand Up @@ -3830,6 +3832,289 @@
return f'<img src="{uri}" style="{style_string}">'


def fmt_icon(
self: GTSelf,
columns: SelectExpr = None,
rows: int | list[int] | None = None,
height: str | None = None,
sep: str = " ",
stroke_color: str | None = None,
stroke_width: str | int | None = None,
stroke_alpha: float | None = None,
fill_color: str | dict[str, str] | None = None,
fill_alpha: float | None = None,
margin_left: str | None = None,
margin_right: str | None = None,
) -> GTSelf:
"""Use icons within a table's body cells.

We can draw from a library of thousands of icons and selectively insert them into a table. The
`fmt_icon()` method makes this possible by mapping input cell labels to an icon name. We are
exclusively using Font Awesome icons here so the reference is the short icon name. Multiple
icons can be included per cell by separating icon names with commas (e.g.,
`"hard-drive,clock"`). The `sep=` argument allows for a common separator to be applied between
icons.

Parameters
----------
columns
The columns to target. Can either be a single column name or a series of column names
provided in a list.
rows
In conjunction with `columns=`, we can specify which of their rows should undergo
formatting. The default is all rows, resulting in all rows in targeted columns being
formatted. Alternatively, we can supply a list of row indices.
height
The absolute height of the icon in the table cell. By default, this is set to "1em".
sep
In the output of icons within a body cell, `sep=` provides the separator between each icon.
stroke_color
The icon stroke is essentially the outline of the icon. The color of the stroke can be
modified by applying a single color here. If not provided then the default value of
`"currentColor"` is applied so that the stroke color matches that of the parent HTML
element's color attribute.
stroke_width
The `stroke_width=` option allows for setting the color of the icon outline stroke. By
default, the stroke width is very small at "1px" so a size adjustment here can sometimes be
useful. If an integer value is provided then it is assumed to be in pixels.
stroke_alpha
The level of transparency for the icon stroke can be controlled with a decimal value between
`0` and `1`.
fill_color
The fill color of the icon can be set with `fill_color=`; providing a single color here will
change the color of the fill but not of the icon's 'stroke' or outline (use `stroke_color=`
to modify that). A dictionary comprising the icon names with corresponding fill colors can
alternatively be used here (e.g., `{"circle-check" = "green", "circle-xmark" = "red"}`. If
nothing is provided then the default value of `"currentColor"` is applied so that the fill
matches the color of the parent HTML element's color attribute.
fill_alpha
The level of transparency for the icon fill can be controlled with a decimal value between
`0` and `1`.
margin_left
The length value for the margin that's to the left of the icon. By default, `"auto"` is
used for this but if space is needed on the left-hand side then a length of `"0.2em"` is
recommended as a starting point.
margin_right
The length value for the margin right of the icon. By default, `"auto"` is used but if
space is needed on the right-hand side then a length of `"0.2em"` is recommended as a
starting point.

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
--------
For this first example of generating icons with `fmt_icon()`, let's make a simple DataFrame that
has two columns of Font Awesome icon names. We separate multiple icons per cell with commas. By
default, the icons are 1 em in height; we're going to make the icons slightly larger here (so we
can see the fine details of them) by setting height = "4em".

```{python}
import pandas as pd
from great_tables import GT

animals_foods_df = pd.DataFrame(
{
"animals": ["hippo", "fish,spider", "mosquito,locust,frog", "dog,cat", "kiwi-bird"],
"foods": ["bowl-rice", "egg,pizza-slice", "burger,lemon,cheese", "carrot,hotdog", "bacon"],
}
)

(
GT(animals_foods_df)
.fmt_icon(
columns=["animals", "foods"],
height="4em"
)
.cols_align(
align="center",
columns=["animals", "foods"]
)
)
```

Let's take a few rows from the towny dataset and make it so the `csd_type` column contains
*Font Awesome* icon names (we want only the `"city"` and `"house-chimney"` icons here). After
using `fmt_icon()` to format the `csd_type` column, we get icons that are representative of the
two categories of municipality for this subset of data.

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


# Assuming `towny` is a Polars DataFrame
towny_mini = (
pl.from_pandas(towny)
.select(["name", "csd_type", "population_2021"])
.filter(pl.col("csd_type").is_in(["city", "town"]))
.group_by("csd_type", maintain_order=True)
.agg([
pl.col("name").sort_by("population_2021", descending=True).head(5),
pl.col("population_2021").sort(descending=True).head(5)
])
.sort("csd_type")
.explode(["name", "population_2021"])
.with_columns(
csd_type = pl.when(pl.col("csd_type") == "town")
.then(pl.lit("house-chimney"))
.otherwise(pl.lit("city"))
)
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to just select a few rows of this data. E.g. towny[[0, 3, 5], :]. Say maybe the top 2 in each of your groups. It seems easier to get into an example with less setup (a doesn't seem critical to have a comprehensive table).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took your approach and the setup for the example is far simpler.


(
GT(towny_mini)
.fmt_integer(columns="population_2021")
.fmt_icon(columns="csd_type")
.cols_label(
csd_type="",
name="City/Town",
population_2021="Population"
)
)
```

A fairly common thing to do with icons in tables is to indicate whether a quantity is either
higher or lower than another. Up and down arrow symbols can serve as good visual indicators for
this purpose. We can make use of the `"up-arrow"` and `"down-arrow"` icons here. As those
strings are available in the `dir` column of the table derived from the `sp500` dataset,
`fmt_icon()` can be used. We set the `fill_color` argument with a dictionary that indicates
which color should be used for each icon.

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

sp500_mini = (
pl.from_pandas(sp500)
.head(10)
.select(["date", "open", "close"])
.sort("date", descending=False)
.with_columns(
dir = pl.when(pl.col("close") >= pl.col("open")).then(
pl.lit("arrow-up")).otherwise(pl.lit("arrow-down"))
)
)

(
GT(sp500_mini, rowname_col="date")
.fmt_icon(
columns="dir",
fill_color={"arrow-up": "green", "arrow-down": "red"}
)
.cols_label(
open="Opening Value",
close="Closing Value",
dir=""
)
.opt_stylize(style=1, color="gray")
)
```
"""

formatter = FmtIcon(
self._tbl_data,
height=height,
sep=sep,
stroke_color=stroke_color,
stroke_width=stroke_width,
stroke_alpha=stroke_alpha,
fill_color=fill_color,
fill_alpha=fill_alpha,
margin_left=margin_left,
margin_right=margin_right,
)

return fmt(
self,
fns=FormatFns(html=formatter.to_html, latex=formatter.to_latex, default=formatter.to_html),
columns=columns,
rows=rows,
)


@dataclass
class FmtIcon:
dispatch_on: DataFrameLike | Agnostic = Agnostic()
height: str | None = None
sep: str = " "
stroke_color: str | None = None
stroke_width: str | int | float | None = None
stroke_alpha: float | None = None
fill_color: str | dict[str, str] | None = None
fill_alpha: float | None = None
margin_left: str | None = None
margin_right: str | None = None

SPAN_TEMPLATE: ClassVar = '<span style="white-space:nowrap;">{}</span>'

def to_html(self, val: Any):

if is_na(self.dispatch_on, val):
return val

Check warning on line 4057 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4057

Added line #L4057 was not covered by tests

if "," in val:
icon_list = re.split(r",\s*", val)
else:
icon_list = [val]

if self.height is None:
height = "1em"
else:
height = self.height

if self.stroke_width is None:
stroke_width = "1px"
elif isinstance(self.stroke_width, (int, float)):
stroke_width = f"{str(self.stroke_width)}px"
else:
stroke_width = self.stroke_width

Check warning on line 4074 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4074

Added line #L4074 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please test this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now tested.


out: list[str] = []

for icon in icon_list:

if isinstance(self.fill_color, dict):

if icon in self.fill_color:
fill_color = self.fill_color[icon]
else:
fill_color = None

Check warning on line 4085 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4085

Added line #L4085 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please test this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests now present for this.

else:
fill_color = self.fill_color

icon_svg = faicons.icon_svg(
icon,
height=height,
stroke=self.stroke_color,
stroke_width=stroke_width,
stroke_opacity=str(self.stroke_alpha),
fill=fill_color,
fill_opacity=str(self.fill_alpha),
margin_left=self.margin_left,
margin_right=self.margin_right,
)

out.append(str(icon_svg))

img_tags = self.sep.join(out)
span = self.SPAN_TEMPLATE.format(img_tags)

return span

def to_latex(self, val: Any):

from ._gt_data import FormatterSkipElement
from warnings import warn

Check warning on line 4111 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4110-L4111

Added lines #L4110 - L4111 were not covered by tests

warn("fmt_icon() is not currently implemented in LaTeX output.")

Check warning on line 4113 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4113

Added line #L4113 was not covered by tests

return FormatterSkipElement()

Check warning on line 4115 in great_tables/_formats.py

View check run for this annotation

Codecov / codecov/patch

great_tables/_formats.py#L4115

Added line #L4115 was not covered by tests


def fmt_nanoplot(
self: GTSelf,
columns: str | None = None,
Expand Down
2 changes: 2 additions & 0 deletions great_tables/gt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
fmt_currency,
fmt_date,
fmt_datetime,
fmt_icon,
fmt_image,
fmt_integer,
fmt_markdown,
Expand Down Expand Up @@ -232,6 +233,7 @@ def __init__(
fmt_datetime = fmt_datetime
fmt_markdown = fmt_markdown
fmt_image = fmt_image
fmt_icon = fmt_icon
fmt_units = fmt_units
fmt_nanoplot = fmt_nanoplot
data_color = data_color
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ classifiers = [
]
dependencies = [
"commonmark>=0.9.1",
"faicons>=0.2.2",
"htmltools>=0.4.1",
"importlib-metadata",
"typing_extensions>=3.10.0.0",
Expand Down
Loading
Loading