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

Fmt url tweak #93

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ quartodoc:
- GT.fmt_roman
- GT.fmt_date
- GT.fmt_time
- GT.fmt_url
- GT.fmt_markdown
- GT.fmt
- title: Modifying columns
Expand Down
268 changes: 268 additions & 0 deletions great_tables/_formats.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
from decimal import Decimal
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union, List, cast, Optional, Dict, Literal
from typing_extensions import TypeAlias
from ._tbl_data import n_rows
Expand Down Expand Up @@ -1771,6 +1772,273 @@ def fmt_time_fn(
return fmt(self, fns=fmt_time_fn, columns=columns, rows=rows)


# fmt_url ---------------------------------------------------------------------


def fmt_url(
self: GTSelf,
columns: Union[str, List[str], None] = None,
rows: Union[int, List[int], None] = None,
label: str | None = None,
as_button: bool = False,
color: str = "auto",
show_underline: str | bool = "auto",
button_fill: str = "auto",
button_width: str = "auto",
button_outline: str = "auto",
) -> GTSelf:
"""
Format URLs to generate links.

Should cells contain URLs, the `fmt_url()` method can be used to make them navigable links. This
should be expressly used on columns that contain *only* URL text (i.e., no URLs as part of a
larger block of text). Should you have such a column of data, there are options for how the
links should be styled. They can be of the conventional style (with underlines and text coloring
that sets it apart from other text), or, they can appear to be button-like (with a surrounding
box that can be filled with a color of your choosing).

URLs in data cells are detected in two ways. The first is using the simple Markdown notation for
URLs of the form: `[label](URL)`. The second assumes that the text is the URL. In the latter
case the URL is also used as the label but there is the option to use the `label` argument to
modify that text.

Parameters
----------
columns : Union[str, List[str], None]
The columns to target. Can either be a single column name or a series of column names
provided in a list.
rows : Union[int, List[int], None]
In conjunction with `columns`, we can specify which of their rows should undergo formatting.
The default is all rows, resulting in all rows in `columns` being formatted. Alternatively,
we can supply a list of row indices.
label : str | None
The visible 'label' to use for the link. If `None` (the default) the URL will serve as the
label. There are two non-`None` options: (1) using a piece of static text for the label
through provision of a string, and (2) a function can be provided to fashion a label from
every URL.
as_button : bool
An option to style the link as a button. By default, this is `False`. If this option is
chosen then the `button_fill` argument becomes usable.
color : str
The color used for the resulting link and its underline. This is `"auto"` by default; this
allows **Great Tables** to choose an appropriate color based on various factors (such as the
background `button_fill` when `as_button` is `True`).
show_underline : str | bool
Should the link be decorated with an underline? By default this is `"auto"` which means that
**Great Tables** will choose `True` when `as_button=False` and `False` in the other case.
The link underline will be the same color as that set in the `color` option.
button_fill : str
The color to fill the button with. This is ignored if `as_button` is `False`. The default is
`"auto"` which will use the default background color for the button.
button_width : str
The width of the button. This is ignored if `as_button` is `False`. The default is `"auto"`
which will use the default button width for link-as-button.
button_outline : str
Options for styling a link-as-button (and only applies if `as_button=True`). This option is
by default set to `"auto"`, allowing **Great Tables** to choose the appropriate outline
color value.

Returns
-------
GT
The GT object is returned.
"""

if as_button:
#
# All determinations of `color`, `show_underline`, `button_fill` and
# `button_width` for the case where `as_button=True`; each of the
# above arguments are set to "auto" by default
#

# In the button case, we opt to never show an underline unless it's
# requested by the user (i.e., `show_underline=True`)
if show_underline == "auto":
show_underline = False

if button_width == "auto":
button_width = None

button_outline_color = button_outline
button_outline_style = "solid"
button_outline_width = "2px"

# There are various combinations of "auto" or not with `button_fill` and
# `color` that need to be handled delicately so as to ensure contrast
# between foreground text and background fill is maximized
if button_fill == "auto" and color == "auto":
# Choose a fixed and standard color combination if both options are
# 'auto'; these will be 'steelblue' and 'white'
button_fill = "#4682B4"
color = "#FFFFFF"

elif button_fill == "auto" and color != "auto":
# If `button_fill` is 'auto' but `color` is not, then we need to
# determine whether the background should be light or dark
bgrnd_bw = "#FFFFFF"

if bgrnd_bw == "#FFFFFF":
# Background should be light so using 'lightblue'
button_fill = "#ADD8E6"
else:
# Background should be dark so using 'darkblue'
button_fill = "#00008B"

if button_outline == "auto":
button_outline_color = "#BEBEBE"
button_outline_style = "none"

elif button_fill != "auto" and color == "auto":
if button_outline == "auto":
button_outline_color = "#DFDFDF"

if button_fill in [
"#FFFFFF",
"#FFFFFF",
"#FAF5EF",
"#FAFAFA",
"#FFFEFC",
"#FBFCFA",
"#FBFAF2",
]:
button_outline_style = "solid"
else:
button_outline_style = "none"

else:
pass

else:
button_outline_style = "none"
button_outline_color = "invisible"
button_outline_width = "0px"

if show_underline == "auto":
show_underline = True

if color == "auto":
color = "#008B8B"
else:
pass
# TODO: Ensure that the incoming `color` is transformed to hexadecimal form
# color = html_color(colors=color, alpha=None)

# Generate a function that will operate on single `x` values in the table body
f = partial(
_fmt_url_fn,
label=label,
as_button=as_button,
button_width=button_width,
button_fill=button_fill,
button_outline_style=button_outline_style,
button_outline_color=button_outline_color,
button_outline_width=button_outline_width,
color=color,
show_underline=show_underline,
)
return fmt(self, fns=f, columns=columns, rows=rows)


class FmtUrlBtn:
FILL_DARK = "#4682B4"
FILL_LIGHT = "#FFFFFF"
COLOR_DARK = "#4682B4"
COLOR_LIGHT = "#4682B4"

@classmethod
def get_fill(cls, fill: str, color: str, outline: str):
if fill != "auto":
return fill

if color == "auto":
return cls.FILL_DARK

if cls.is_dark(color):
return cls.FILL_LIGHT
else:
return cls.FILL_DARK

@classmethod
def get_color(cls, fill: str, color: str, outline: str):
if color != "auto":
return color

if fill == "auto":
return cls.COLOR_LIGHT

if cls.is_dark(fill):
return cls.COLOR_LIGHT

else:
return cls.COLOR_DARK

# fill = FmtUrlBtn.get_fill(fill="auto", color="auto", outline="auto")
# color = FmtUrlBtn.get_color(fill=fill, color="auto", outline="auto")


def _fmt_url_fn(
x: Any,
label,
as_button,
button_width,
button_fill,
button_outline_style,
button_outline_color,
button_outline_width,
color,
show_underline,
) -> str:
# If the `x` value is a Pandas 'NA', then return the same value
if pd.isna(x):
return x

href_str = x

if label is not None:
if label is Callable:
label_str = label(x)
else:
label_str = label

else:
import re

pattern = r"^rgba\(\s*(?:[0-9]+?\s*,\s*){3}[0-9\.]+?\s*\)$"
matched = bool(re.match(pattern, x))

if matched:
# Generate label
if bool(re.match(r"\\[.*?\\]\\(.*?\\)", x)):
label_str = re.sub(r"\\[(.*?)\\]\\(.*?\\)", "\\1", x)
else:
label_str = x

# Generate href value
if bool(re.match(r"\\[.*?\\]\\(.*?\\)", x)):
href_str = re.sub(r"\\[.*?\\]\\((.*?)\\)", "\\1", x)
else:
href_str = x

else:
label_str = x

if as_button:
if button_width is not None:
button_width_str = f"width:{button_width};text-align:center;"
else:
button_width_str = ""

button_attrs = f"background-color:{button_fill};padding:8px 12px;{button_width_str};outline-style:{button_outline_style};outline-color:{button_outline_color};outline-width:{button_outline_width};"

else:
button_attrs = ""

attrs = f'color:{color};text-decoration:{"underline" if show_underline else "none"};{"text-underline-position:under;" if show_underline else ""}display:inline-block;{button_attrs}'
x_formatted = f'<a style="{attrs}" href="{href_str}">{label_str}</a>'

return x_formatted


def fmt_markdown(
self: GTSelf,
columns: Union[str, List[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 @@ -21,6 +21,7 @@
fmt_roman,
fmt_date,
fmt_time,
fmt_url,
fmt_markdown,
)
from great_tables._heading import tab_header
Expand Down Expand Up @@ -202,6 +203,7 @@ def __init__(
fmt_roman = fmt_roman
fmt_date = fmt_date
fmt_time = fmt_time
fmt_url = fmt_url
fmt_markdown = fmt_markdown

tab_options = tab_options
Expand Down
91 changes: 91 additions & 0 deletions tests/__snapshots__/test_fmt_url.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# serializer version: 1
# name: test_fmt_url_01
'''
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Addington Highlands</td>
<td class="gt_row gt_left"><a style="color:#008B8B;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://addingtonhighlands.ca">https://addingtonhighlands.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adelaide Metcalfe</td>
<td class="gt_row gt_left"><a style="color:#008B8B;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://adelaidemetcalfe.on.ca">https://adelaidemetcalfe.on.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adjala-Tosorontio</td>
<td class="gt_row gt_left"><a style="color:#008B8B;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://www.adjtos.ca">https://www.adjtos.ca</a></td>
</tr>
</tbody>
'''
# ---
# name: test_fmt_url_02
'''
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Addington Highlands</td>
<td class="gt_row gt_left"><a style="color:#FFFFFF;text-decoration:none;display:inline-block;background-color:#4682B4;padding:8px 12px;;outline-style:solid;outline-color:auto;outline-width:2px;" href="https://addingtonhighlands.ca">https://addingtonhighlands.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adelaide Metcalfe</td>
<td class="gt_row gt_left"><a style="color:#FFFFFF;text-decoration:none;display:inline-block;background-color:#4682B4;padding:8px 12px;;outline-style:solid;outline-color:auto;outline-width:2px;" href="https://adelaidemetcalfe.on.ca">https://adelaidemetcalfe.on.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adjala-Tosorontio</td>
<td class="gt_row gt_left"><a style="color:#FFFFFF;text-decoration:none;display:inline-block;background-color:#4682B4;padding:8px 12px;;outline-style:solid;outline-color:auto;outline-width:2px;" href="https://www.adjtos.ca">https://www.adjtos.ca</a></td>
</tr>
</tbody>
'''
# ---
# name: test_fmt_url_03
'''
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Addington Highlands</td>
<td class="gt_row gt_left"><a style="color:red;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://addingtonhighlands.ca">https://addingtonhighlands.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adelaide Metcalfe</td>
<td class="gt_row gt_left"><a style="color:red;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://adelaidemetcalfe.on.ca">https://adelaidemetcalfe.on.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adjala-Tosorontio</td>
<td class="gt_row gt_left"><a style="color:red;text-decoration:underline;text-underline-position:under;display:inline-block;" href="https://www.adjtos.ca">https://www.adjtos.ca</a></td>
</tr>
</tbody>
'''
# ---
# name: test_fmt_url_04
'''
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Addington Highlands</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;background-color:#ADD8E6;padding:8px 12px;;outline-style:none;outline-color:#BEBEBE;outline-width:2px;" href="https://addingtonhighlands.ca">https://addingtonhighlands.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adelaide Metcalfe</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;background-color:#ADD8E6;padding:8px 12px;;outline-style:none;outline-color:#BEBEBE;outline-width:2px;" href="https://adelaidemetcalfe.on.ca">https://adelaidemetcalfe.on.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adjala-Tosorontio</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;background-color:#ADD8E6;padding:8px 12px;;outline-style:none;outline-color:#BEBEBE;outline-width:2px;" href="https://www.adjtos.ca">https://www.adjtos.ca</a></td>
</tr>
</tbody>
'''
# ---
# name: test_fmt_url_05
'''
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_left">Addington Highlands</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;" href="https://addingtonhighlands.ca">https://addingtonhighlands.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adelaide Metcalfe</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;" href="https://adelaidemetcalfe.on.ca">https://adelaidemetcalfe.on.ca</a></td>
</tr>
<tr>
<td class="gt_row gt_left">Adjala-Tosorontio</td>
<td class="gt_row gt_left"><a style="color:green;text-decoration:none;display:inline-block;" href="https://www.adjtos.ca">https://www.adjtos.ca</a></td>
</tr>
</tbody>
'''
# ---
Loading