diff --git a/docs/_quarto.yml b/docs/_quarto.yml
index a652ceaf2..24dc86446 100644
--- a/docs/_quarto.yml
+++ b/docs/_quarto.yml
@@ -135,6 +135,7 @@ quartodoc:
- GT.fmt_markdown
- GT.fmt_units
- GT.fmt_image
+ - GT.fmt_image_circle
- GT.fmt_icon
- GT.fmt_flag
- GT.fmt_nanoplot
@@ -246,6 +247,7 @@ quartodoc:
- vals.fmt_time
- vals.fmt_markdown
- vals.fmt_image
+ - vals.fmt_image_circle
- title: Built in datasets
desc: >
The **Great Tables** package is equipped with sixteen datasets that come in all shapes and
diff --git a/great_tables/_formats.py b/great_tables/_formats.py
index 66833e4b1..a6d775d27 100644
--- a/great_tables/_formats.py
+++ b/great_tables/_formats.py
@@ -3657,7 +3657,7 @@ def fmt_image(
columns: SelectExpr = None,
rows: int | list[int] | None = None,
height: str | int | None = None,
- width: str | int | None = None,
+ width: str | None = None,
sep: str = " ",
path: str | Path | None = None,
file_pattern: str = "{}",
@@ -3740,7 +3740,165 @@ def fmt_image(
)
```
"""
+ return _fmt_image(
+ self,
+ columns=columns,
+ rows=rows,
+ height=height,
+ width=width,
+ sep=sep,
+ path=path,
+ file_pattern=file_pattern,
+ encode=encode,
+ )
+
+
+def fmt_image_circle(
+ self: GTSelf,
+ columns: SelectExpr = None,
+ rows: int | list[int] | None = None,
+ height: str | int | None = None,
+ width: str | None = None,
+ border_radius: str | None = "50%",
+ border_width: str | int | None = None,
+ border_color: str | None = None,
+ border_style: str | None = None,
+ sep: str = " ",
+ path: str | Path | None = None,
+ file_pattern: str = "{}",
+ encode: bool = True,
+) -> GTSelf:
+ """Format image paths to generate circular images within table cells.
+ `fmt_image_circle()` is a utility function similar to [`fmt_image()`](`great_tables.fmt_image`),
+ but it also accepts additional parameters for customizing the image border:
+ `border_radius=`, `border_width=`, `border_color=`, and `border_style=`.
+
+ When calling `fmt_image_circle()`, **Great Tables** automatically sets `border_radius="50%"` to
+ create a full circle. However, we can't assume whether you want the border to be visible.
+ Therefore, you should supply at least one of the following: `border_width=`, `border_color=`,
+ or `border_style=`. Based on your input, sensible defaults will be applied for any unset border
+ properties.
+
+ 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 height of the rendered images.
+ width
+ The width of the rendered images.
+ border_radius
+ The radius of the image border. Accepts values in pixels (`px`) or percentages (`%`).
+ Defaults to `50%` to create a circular image.
+ border_width
+ The width of the image border.
+ border_color
+ The color of the image border.
+ border_style
+ The style of the image border (e.g., solid, dashed, dotted).
+ sep
+ In the output of images within a body cell, `sep=` provides the separator between each
+ image.
+ path
+ An optional path to local image files or an HTTP/HTTPS URL.
+ This is combined with the filenames to form the complete image paths.
+ file_pattern
+ The pattern to use for mapping input values in the body cells to the names of the graphics
+ files. The string supplied should use `"{}"` in the pattern to map filename fragments to
+ input strings.
+ encode
+ The option to always use Base64 encoding for image paths that are determined to be local. By
+ default, this is `True`.
+
+ 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
+ --------
+ This example demonstrates how to use `fmt_image_circle()` to create circular images in table cells,
+ along with its counterpart [`vals.fmt_image_circle`](`great_tables.vals.fmt_image_circle`) to render
+ a circular image in the header.
+ ```{python}
+ import polars as pl
+ from great_tables import GT, vals, html
+
+ posit_avatar = "https://avatars.githubusercontent.com/u/107264312?s=200&v=4"
+ rich_avatar = "https://avatars.githubusercontent.com/u/5612024?v=4"
+ michael_avatar = "https://avatars.githubusercontent.com/u/2574498?v=4"
+
+ title_img = vals.fmt_image_circle(posit_avatar, height=100, border_color="#D3D3D3")[0]
+ df = pl.DataFrame({"@rich-iannone": [rich_avatar], "@machow": [michael_avatar]})
+
+ (
+ GT(df)
+ .fmt_image_circle(height=150, border_width=5)
+ .tab_header(html(title_img))
+ .cols_align("center")
+ .opt_stylize(color="green", style=6)
+ )
+ ```
+ """
+ default_border_props = {
+ "border-width": "3px",
+ "border-color": "#0A0A0A",
+ "border-style": "solid",
+ }
+
+ border_props = {
+ "border-width": border_width,
+ "border-color": border_color,
+ "border-style": border_style,
+ }
+
+ # This block assigns default values to `border-width`, `border-color`, and `border-style`
+ # if the user specifies at least one of them but leaves others unset.
+ if any(border_props.values()):
+ for k, v in default_border_props.items():
+ if border_props[k] is None:
+ border_props[k] = v
+
+ border_width, border_color, border_style = border_props.values()
+
+ return _fmt_image(
+ self,
+ columns=columns,
+ rows=rows,
+ height=height,
+ width=width,
+ border_radius=border_radius,
+ border_width=border_width,
+ border_color=border_color,
+ border_style=border_style,
+ sep=sep,
+ path=path,
+ file_pattern=file_pattern,
+ encode=encode,
+ )
+
+def _fmt_image(
+ self: GTSelf,
+ columns: SelectExpr = None,
+ rows: int | list[int] | None = None,
+ height: str | int | None = None,
+ width: str | None = None,
+ border_radius: str | None = None,
+ border_width: str | int | None = None,
+ border_color: str | None = None,
+ border_style: str | None = None,
+ sep: str = " ",
+ path: str | Path | None = None,
+ file_pattern: str = "{}",
+ encode: bool = True,
+) -> GTSelf:
# TODO: most parameter options should allow a polars expression (or from_column) ----
# can other fmt functions do this kind of thing?
expr_cols = [height, width, sep, path, file_pattern, encode]
@@ -3754,7 +3912,19 @@ def fmt_image(
if height is None and width is None:
height = "2em"
- formatter = FmtImage(self._tbl_data, height, width, sep, path, file_pattern, encode)
+ formatter = FmtImage(
+ self._tbl_data,
+ height=height,
+ width=width,
+ border_radius=border_radius,
+ border_width=border_width,
+ border_color=border_color,
+ border_style=border_style,
+ sep=sep,
+ path=path,
+ file_pattern=file_pattern,
+ encode=encode,
+ )
return fmt(
self,
fns=FormatFns(html=formatter.to_html, latex=formatter.to_latex, default=formatter.to_html),
@@ -3767,12 +3937,15 @@ def fmt_image(
class FmtImage:
dispatch_on: DataFrameLike | Agnostic = Agnostic()
height: str | int | None = None
- width: str | int | None = None
+ width: str | None = None
+ border_radius: str | None = None
+ border_width: str | int | None = None
+ border_color: str | None = None
+ border_style: str | None = None
sep: str = " "
path: str | Path | None = None
file_pattern: str = "{}"
encode: bool = True
-
SPAN_TEMPLATE: ClassVar = '{}'
def to_html(self, val: Any):
@@ -3799,7 +3972,29 @@ def to_html(self, val: Any):
# TODO: note that only height can be numeric in the R program. Is this on purpose?
# In any event, raising explicitly for numeric width below.
if isinstance(self.width, (int, float)):
- raise NotImplementedError("The width argument must be specified as a string.")
+ raise NotImplementedError("The `width=` argument must be specified as a string.")
+ else:
+ width = self.width
+
+ if self.border_radius is not None:
+ if not isinstance(self.border_radius, str):
+ raise NotImplementedError(
+ "The `border_radius=` argument must be specified as a string."
+ )
+ if not any(self.border_radius.endswith(suffix) for suffix in {"px", "%"}):
+ raise NotImplementedError(
+ 'The `border_radius=` argument must end with either "px" or "%"'
+ )
+
+ border_radius = self.border_radius
+
+ if isinstance(self.border_width, (int, float)):
+ border_width = px(self.border_width)
+ else:
+ border_width = self.border_width
+
+ border_color = self.border_color
+ border_style = self.border_style
full_files = self._apply_pattern(self.file_pattern, files)
@@ -3822,7 +4017,17 @@ def to_html(self, val: Any):
uri = filename
# TODO: do we have a way to create tags, that is good at escaping, etc..?
- out.append(self._build_img_tag(uri, height, self.width))
+ out.append(
+ self._build_img_tag(
+ uri=uri,
+ height=height,
+ width=width,
+ border_radius=border_radius,
+ border_width=border_width,
+ border_color=border_color,
+ border_style=border_style,
+ )
+ )
img_tags = self.sep.join(out)
span = self.SPAN_TEMPLATE.format(img_tags)
@@ -3866,15 +4071,26 @@ def _get_mime_type(filename: str) -> str:
return f"image/{suffix}"
@staticmethod
- def _build_img_tag(uri: str, height: str | None = None, width: str | None = None) -> str:
+ def _build_img_tag(
+ uri: str,
+ height: str | None = None,
+ width: str | None = None,
+ border_radius: str | None = None,
+ border_width: str | None = None,
+ border_color: str | None = None,
+ border_style: str | None = None,
+ ) -> str:
style_string = "".join(
[
f"height: {height};" if height is not None else "",
f"width: {width};" if width is not None else "",
+ f"border-radius: {border_radius};" if border_radius is not None else "",
+ f"border-width: {border_width};" if border_width is not None else "",
+ f"border-color: {border_color};" if border_color is not None else "",
+ f"border-style: {border_style};" if border_style is not None else "",
"vertical-align: middle;",
]
)
-
return f'
'
diff --git a/great_tables/_formats_vals.py b/great_tables/_formats_vals.py
index 4d16f6b29..2150da209 100644
--- a/great_tables/_formats_vals.py
+++ b/great_tables/_formats_vals.py
@@ -1051,7 +1051,7 @@ def val_fmt_markdown(
def val_fmt_image(
x: X,
height: str | int | None = None,
- width: str | int | None = None,
+ width: str | None = None,
sep: str = " ",
path: str | Path | None = None,
file_pattern: str = "{}",
@@ -1116,3 +1116,89 @@ def val_fmt_image(
vals_fmt = _get_column_of_values(gt=gt_obj_fmt, column_name="x", context="html")
return vals_fmt
+
+
+def val_fmt_image_circle(
+ x: X,
+ height: str | int | None = None,
+ width: str | None = None,
+ border_radius: str | None = "50%",
+ border_width: str | int | None = None,
+ border_color: str | None = None,
+ border_style: str | None = None,
+ sep: str = " ",
+ path: str | Path | None = None,
+ file_pattern: str = "{}",
+ encode: bool = True,
+) -> list[str]:
+ """Format image paths to generate circular images within table cells.
+ `fmt_image_circle()` is a utility function similar to [`fmt_image()`](`great_tables.fmt_image`),
+ but it also accepts additional parameters for customizing the image border:
+ `border_radius=`, `border_width=`, `border_color=`, and `border_style=`.
+
+ When calling `fmt_image_circle()`, **Great Tables** automatically sets `border_radius="50%"` to
+ create a full circle. However, we can't assume whether you want the border to be visible.
+ Therefore, you should supply at least one of the following: `border_width=`, `border_color=`,
+ or `border_style=`. Based on your input, sensible defaults will be applied for any unset border
+ properties.
+
+ Parameters
+ ----------
+ x
+ A list of values to be formatted.
+ height
+ The height of the rendered images.
+ width
+ The width of the rendered images.
+ border_radius
+ The radius of the image border. Accepts values in pixels (`px`) or percentages (`%`).
+ Defaults to `50%` to create a circular image.
+ border_width
+ The width of the image border.
+ border_color
+ The color of the image border.
+ border_style
+ The style of the image border (e.g., solid, dashed, dotted).
+ sep
+ In the output of images within a body cell, `sep=` provides the separator between each
+ image.
+ path
+ An optional path to local image files or an HTTP/HTTPS URL.
+ This is combined with the filenames to form the complete image paths.
+ file_pattern
+ The pattern to use for mapping input values in the body cells to the names of the graphics
+ files. The string supplied should use `"{}"` in the pattern to map filename fragments to
+ input strings.
+ encode
+ The option to always use Base64 encoding for image paths that are determined to be local. By
+ default, this is `True`.
+
+ Returns
+ -------
+ list[str]
+ A list of formatted values is returned.
+
+ See Also
+ --------
+ Check out our blog post, [Rendering images anywhere in Great Tables](https://posit-dev.github.io/great-tables/blog/rendering-images/),
+ which walks through how to use `vals.fmt_image()`.
+ """
+ gt_obj: GTData = _make_one_col_table(vals=x)
+
+ gt_obj_fmt = gt_obj.fmt_image_circle(
+ columns="x",
+ height=height,
+ width=width,
+ border_radius=border_radius,
+ border_width=border_width,
+ border_color=border_color,
+ border_style=border_style,
+ sep=sep,
+ path=path,
+ file_pattern=file_pattern,
+ encode=encode,
+ )
+
+ vals_fmt = _get_column_of_values(gt=gt_obj_fmt, column_name="x", context="html")
+
+ return vals_fmt
diff --git a/great_tables/gt.py b/great_tables/gt.py
index 82b43fd99..fb75df089 100644
--- a/great_tables/gt.py
+++ b/great_tables/gt.py
@@ -18,6 +18,7 @@
fmt_flag,
fmt_icon,
fmt_image,
+ fmt_image_circle,
fmt_integer,
fmt_markdown,
fmt_nanoplot,
@@ -233,6 +234,7 @@ def __init__(
fmt_datetime = fmt_datetime
fmt_markdown = fmt_markdown
fmt_image = fmt_image
+ fmt_image_circle = fmt_image_circle
fmt_icon = fmt_icon
fmt_flag = fmt_flag
fmt_units = fmt_units
diff --git a/great_tables/vals.py b/great_tables/vals.py
index 81f79329b..b81ca3dbe 100644
--- a/great_tables/vals.py
+++ b/great_tables/vals.py
@@ -14,4 +14,5 @@
val_fmt_time as fmt_time,
val_fmt_markdown as fmt_markdown,
val_fmt_image as fmt_image,
+ val_fmt_image_circle as fmt_image_circle,
)
diff --git a/tests/test_formats.py b/tests/test_formats.py
index 12e5fcf7b..7967cff0a 100644
--- a/tests/test_formats.py
+++ b/tests/test_formats.py
@@ -1554,12 +1554,14 @@ def test_fmt_image_height_int():
assert strip_windows_drive(res) == dst
-def test_fmt_image_width_int():
+def test_fmt_image_width_int_raises():
formatter = FmtImage(encode=False, width=20)
- with pytest.raises(NotImplementedError):
+ with pytest.raises(NotImplementedError) as exc_info:
formatter.to_html("/a")
+ assert "The `width=` argument must be specified as a string." in exc_info.value.args[0]
+
@pytest.mark.skipif(sys.platform == "win32", reason="uses linux specific paths")
def test_fmt_image_path():
@@ -1582,6 +1584,46 @@ def test_fmt_image_path_http(url: str):
assert strip_windows_drive(res) == dst
+def test_fmt_image_circle_single():
+ formatter = FmtImage(border_radius="50%", sep=" ", file_pattern="{}.svg", encode=False)
+ res = formatter.to_html("/a")
+ dst = formatter.SPAN_TEMPLATE.format(
+ '
'
+ )
+
+ assert strip_windows_drive(res) == dst
+
+
+def test_fmt_image_circle_multiple():
+ formatter = FmtImage(border_radius="50%", sep="---", file_pattern="{}.svg", encode=False)
+ res = formatter.to_html("/a,/b")
+ dst = formatter.SPAN_TEMPLATE.format(
+ '
'
+ "---"
+ '
'
+ )
+
+ assert strip_windows_drive(res) == dst
+
+
+def test_fmt_image_circle_border_radius_raises1():
+ formatter = FmtImage(encode=False, border_radius=20)
+
+ with pytest.raises(NotImplementedError) as exc_info:
+ formatter.to_html("/a")
+ assert "The `border_radius=` argument must be specified as a string." in exc_info.value.args[0]
+
+
+def test_fmt_image_circle_border_radius_raises2():
+ formatter = FmtImage(encode=False, border_radius="20")
+
+ with pytest.raises(NotImplementedError) as exc_info:
+ formatter.to_html("/a")
+ assert (
+ 'The `border_radius=` argument must end with either "px" or "%"' in exc_info.value.args[0]
+ )
+
+
def test_fmt_icon_one_per_cell():
df = pd.DataFrame({"x": ["hippo", "burger", "pizza-slice"]})
diff --git a/tests/test_formats_vals.py b/tests/test_formats_vals.py
index 660426a56..80a87a81b 100644
--- a/tests/test_formats_vals.py
+++ b/tests/test_formats_vals.py
@@ -29,3 +29,29 @@ def test_val_fmt_image_multiple(img_paths: Path):
assert 'img src="data:image/svg+xml;base64' in img1
assert 'img src="data:image/svg+xml;base64' in img2
+
+
+def test_locate_val_fmt_image_circle(img_paths: Path):
+ imgs = vals.fmt_image_circle(
+ "1", border_style="solid", path=img_paths, file_pattern="metro_{}.svg"
+ )
+ with open(img_paths / "metro_1.svg", "rb") as f:
+ encoded = base64.b64encode(f.read()).decode()
+
+ assert encoded in imgs[0]
+
+
+def test_val_fmt_image_circle_single(img_paths: Path):
+ imgs = vals.fmt_image_circle(
+ "1", border_style="solid", path=img_paths, file_pattern="metro_{}.svg"
+ )
+ assert 'img src="data:image/svg+xml;base64' in imgs[0]
+
+
+def test_val_fmt_image_circle_multiple(img_paths: Path):
+ img1, img2 = vals.fmt_image_circle(
+ ["1", "2"], border_style="solid", path=img_paths, file_pattern="metro_{}.svg"
+ )
+
+ assert 'img src="data:image/svg+xml;base64' in img1
+ assert 'img src="data:image/svg+xml;base64' in img2