-
Notifications
You must be signed in to change notification settings - Fork 74
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
Changes from 14 commits
60886fd
d51c9a1
e96b907
9d8e82c
fe4a232
c35399a
ab1f33a
9dc57d5
5fea3f9
2ce033f
da3d8b2
55170a4
fb53d49
0e9e9e6
0e94fc2
ddd9575
a72d97f
81eec3a
e21ac16
0d633d0
02e138a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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")) | ||
) | ||
) | ||
|
||
( | ||
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 | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please test this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please test this There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
warn("fmt_icon() is not currently implemented in LaTeX output.") | ||
|
||
return FormatterSkipElement() | ||
|
||
|
||
def fmt_nanoplot( | ||
self: GTSelf, | ||
columns: str | None = None, | ||
|
There was a problem hiding this comment.
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).There was a problem hiding this comment.
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.