Skip to content

ENH: Styler.text_gradient: easy extension alternative to .background_gradient #41098

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

Merged
merged 30 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4bce0d2
new method
attack68 Feb 18, 2021
0816d6b
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 9, 2021
0120bfa
add docs
attack68 Apr 9, 2021
59f2b3b
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 15, 2021
dc26879
add docs
attack68 Apr 15, 2021
386c001
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 18, 2021
433da4e
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 20, 2021
e78b1ed
add tests
attack68 Apr 20, 2021
d6bab5d
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 21, 2021
2e02ce6
Merge branch 'master' into text_gradient
attack68 Apr 22, 2021
9d90012
whats new
attack68 Apr 22, 2021
1bcf943
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 26, 2021
7023720
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 27, 2021
463116f
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 Apr 29, 2021
82b62e3
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 1, 2021
db97e39
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 3, 2021
b250d07
shared doc string
attack68 May 4, 2021
57be4c0
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 5, 2021
e12e4d4
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 6, 2021
7813681
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 7, 2021
29c48ca
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 9, 2021
78ff74c
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 10, 2021
6c3cae6
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 11, 2021
d4e2321
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 12, 2021
e65c902
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 13, 2021
9540cbc
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 14, 2021
cb6f1ef
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 17, 2021
30b500f
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 18, 2021
11aa8e1
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 22, 2021
4d98b73
Merge remote-tracking branch 'upstream/master' into text_gradient
attack68 May 24, 2021
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
Binary file added doc/source/_static/style/tg_ax0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/tg_axNone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/tg_axNone_gmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/tg_axNone_lowhigh.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/tg_axNone_vminvmax.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/style/tg_gmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/source/reference/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Builtin styles
Styler.highlight_min
Styler.highlight_between
Styler.background_gradient
Styler.text_gradient
Styler.bar

Style export and import
Expand Down
4 changes: 2 additions & 2 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ to allow custom CSS highlighting instead of default background coloring (:issue:
Enhancements to other built-in methods include extending the :meth:`.Styler.background_gradient`
method to shade elements based on a given gradient map and not be restricted only to
values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`). Additional
built-in methods such as :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile`
have been added (:issue:`39821` and :issue:`40926`).
built-in methods such as :meth:`.Styler.highlight_between`, :meth:`.Styler.highlight_quantile`
and :math:`.Styler.text_gradient` have been added (:issue:`39821`, :issue:`40926`, :issue:`41098`).

The :meth:`.Styler.apply` now consistently allows functions with ``ndarray`` output to
allow more flexible development of UDFs when ``axis`` is ``None`` ``0`` or ``1`` (:issue:`39393`).
Expand Down
105 changes: 77 additions & 28 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,13 @@ def hide_columns(self, subset: Subset) -> Styler:
# A collection of "builtin" styles
# -----------------------------------------------------------------------

@doc(
name="background",
alt="text",
image_prefix="bg",
axis="{0 or 'index', 1 or 'columns', None}",
text_threshold="",
)
def background_gradient(
self,
cmap="PuBu",
Expand All @@ -1483,9 +1490,9 @@ def background_gradient(
gmap: Sequence | None = None,
) -> Styler:
"""
Color the background in a gradient style.
Color the {name} in a gradient style.

The background color is determined according
The {name} color is determined according
to the data in each column, row or frame, or by a given
gradient map. Requires matplotlib.

Expand All @@ -1501,7 +1508,7 @@ def background_gradient(
Compress the color range at the high end. This is a multiple of the data
range to extend above the maximum; good values usually in [0, 1],
defaults to 0.
axis : {0 or 'index', 1 or 'columns', None}, default 0
axis : {axis}, default 0
Apply to each column (``axis=0`` or ``'index'``), to each row
(``axis=1`` or ``'columns'``), or to the entire DataFrame at once
with ``axis=None``.
Expand All @@ -1510,6 +1517,7 @@ def background_gradient(
or single key, to `DataFrame.loc[:, <subset>]` where the columns are
prioritised, to limit ``data`` to *before* applying the function.
text_color_threshold : float or int
{text_threshold}
Luminance threshold for determining text color in [0, 1]. Facilitates text
visibility across varying background colors. All text is dark if 0, and
light if 1, defaults to 0.408.
Expand All @@ -1529,7 +1537,7 @@ def background_gradient(
.. versionadded:: 1.0.0

gmap : array-like, optional
Gradient map for determining the background colors. If not supplied
Gradient map for determining the {name} colors. If not supplied
will use the underlying data from rows, columns or frame. If given as an
ndarray or list-like must be an identical shape to the underlying data
considering ``axis`` and ``subset``. If given as DataFrame or Series must
Expand All @@ -1543,6 +1551,10 @@ def background_gradient(
-------
self : Styler

See Also
--------
Styler.{alt}_gradient: Color the {alt} in a gradient style.

Notes
-----
When using ``low`` and ``high`` the range
Expand All @@ -1560,52 +1572,50 @@ def background_gradient(

Examples
--------
>>> df = pd.DataFrame({
... 'City': ['Stockholm', 'Oslo', 'Copenhagen'],
... 'Temp (c)': [21.6, 22.4, 24.5],
... 'Rain (mm)': [5.0, 13.3, 0.0],
... 'Wind (m/s)': [3.2, 3.1, 6.7]
... })
>>> df = pd.DataFrame(columns=["City", "Temp (c)", "Rain (mm)", "Wind (m/s)"],
... data=[["Stockholm", 21.6, 5.0, 3.2],
... ["Oslo", 22.4, 13.3, 3.1],
... ["Copenhagen", 24.5, 0.0, 6.7]])

Shading the values column-wise, with ``axis=0``, preselecting numeric columns

>>> df.style.background_gradient(axis=0)
>>> df.style.{name}_gradient(axis=0)

.. figure:: ../../_static/style/bg_ax0.png
.. figure:: ../../_static/style/{image_prefix}_ax0.png

Shading all values collectively using ``axis=None``

>>> df.style.background_gradient(axis=None)
>>> df.style.{name}_gradient(axis=None)

.. figure:: ../../_static/style/bg_axNone.png
.. figure:: ../../_static/style/{image_prefix}_axNone.png

Compress the color map from the both ``low`` and ``high`` ends

>>> df.style.background_gradient(axis=None, low=0.75, high=1.0)
>>> df.style.{name}_gradient(axis=None, low=0.75, high=1.0)

.. figure:: ../../_static/style/bg_axNone_lowhigh.png
.. figure:: ../../_static/style/{image_prefix}_axNone_lowhigh.png

Manually setting ``vmin`` and ``vmax`` gradient thresholds

>>> df.style.background_gradient(axis=None, vmin=6.7, vmax=21.6)
>>> df.style.{name}_gradient(axis=None, vmin=6.7, vmax=21.6)

.. figure:: ../../_static/style/bg_axNone_vminvmax.png
.. figure:: ../../_static/style/{image_prefix}_axNone_vminvmax.png

Setting a ``gmap`` and applying to all columns with another ``cmap``

>>> df.style.background_gradient(axis=0, gmap=df['Temp (c)'], cmap='YlOrRd')
>>> df.style.{name}_gradient(axis=0, gmap=df['Temp (c)'], cmap='YlOrRd')

.. figure:: ../../_static/style/bg_gmap.png
.. figure:: ../../_static/style/{image_prefix}_gmap.png

Setting the gradient map for a dataframe (i.e. ``axis=None``), we need to
explicitly state ``subset`` to match the ``gmap`` shape

>>> gmap = np.array([[1,2,3], [2,3,4], [3,4,5]])
>>> df.style.background_gradient(axis=None, gmap=gmap,
>>> df.style.{name}_gradient(axis=None, gmap=gmap,
... cmap='YlOrRd', subset=['Temp (c)', 'Rain (mm)', 'Wind (m/s)']
... )

.. figure:: ../../_static/style/bg_axNone_gmap.png
.. figure:: ../../_static/style/{image_prefix}_axNone_gmap.png
"""
if subset is None and gmap is None:
subset = self.data.select_dtypes(include=np.number).columns
Expand All @@ -1624,6 +1634,41 @@ def background_gradient(
)
return self

@doc(
background_gradient,
name="text",
alt="background",
image_prefix="tg",
axis="{0 or 'index', 1 or 'columns', None}",
text_threshold="This argument is ignored (only used in `background_gradient`).",
)
def text_gradient(
self,
cmap="PuBu",
low: float = 0,
high: float = 0,
axis: Axis | None = 0,
subset: Subset | None = None,
vmin: float | None = None,
vmax: float | None = None,
gmap: Sequence | None = None,
) -> Styler:
if subset is None and gmap is None:
subset = self.data.select_dtypes(include=np.number).columns

return self.apply(
_background_gradient,
cmap=cmap,
subset=subset,
axis=axis,
low=low,
high=high,
vmin=vmin,
vmax=vmax,
gmap=gmap,
text_only=True,
)

def set_properties(self, subset: Subset | None = None, **kwargs) -> Styler:
"""
Set defined CSS-properties to each ``<td>`` HTML element within the given
Expand Down Expand Up @@ -2332,6 +2377,7 @@ def _background_gradient(
vmin: float | None = None,
vmax: float | None = None,
gmap: Sequence | np.ndarray | FrameOrSeries | None = None,
text_only: bool = False,
):
"""
Color background in a range according to the data or a gradient map
Expand Down Expand Up @@ -2371,16 +2417,19 @@ def relative_luminance(rgba) -> float:
)
return 0.2126 * r + 0.7152 * g + 0.0722 * b

def css(rgba) -> str:
dark = relative_luminance(rgba) < text_color_threshold
text_color = "#f1f1f1" if dark else "#000000"
return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
def css(rgba, text_only) -> str:
if not text_only:
dark = relative_luminance(rgba) < text_color_threshold
text_color = "#f1f1f1" if dark else "#000000"
return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
else:
return f"color: {colors.rgb2hex(rgba)};"

if data.ndim == 1:
return [css(rgba) for rgba in rgbas]
return [css(rgba, text_only) for rgba in rgbas]
else:
return DataFrame(
[[css(rgba) for rgba in row] for row in rgbas],
[[css(rgba, text_only) for rgba in row] for row in rgbas],
index=data.index,
columns=data.columns,
)
Expand Down
39 changes: 26 additions & 13 deletions pandas/tests/io/formats/style/test_matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,22 @@ def styler_blank(df_blank):
return Styler(df_blank, uuid_len=0)


def test_background_gradient(styler):
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
def test_function_gradient(styler, f):
for c_map in [None, "YlOrRd"]:
result = styler.background_gradient(cmap=c_map)._compute().ctx
result = getattr(styler, f)(cmap=c_map)._compute().ctx
assert all("#" in x[0][1] for x in result.values())
assert result[(0, 0)] == result[(0, 1)]
assert result[(1, 0)] == result[(1, 1)]


def test_background_gradient_color(styler):
result = styler.background_gradient(subset=IndexSlice[1, "A"])._compute().ctx
assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")]
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
def test_background_gradient_color(styler, f):
result = getattr(styler, f)(subset=IndexSlice[1, "A"])._compute().ctx
if f == "background_gradient":
assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")]
elif f == "text_gradient":
assert result[(1, 0)] == [("color", "#fff7fb")]


@pytest.mark.parametrize(
Expand All @@ -54,15 +59,23 @@ def test_background_gradient_color(styler):
(None, ["low", "mid", "mid", "high"]),
],
)
def test_background_gradient_axis(styler, axis, expected):
bg_colors = {
"low": [("background-color", "#f7fbff"), ("color", "#000000")],
"mid": [("background-color", "#abd0e6"), ("color", "#000000")],
"high": [("background-color", "#08306b"), ("color", "#f1f1f1")],
}
result = styler.background_gradient(cmap="Blues", axis=axis)._compute().ctx
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
def test_background_gradient_axis(styler, axis, expected, f):
if f == "background_gradient":
colors = {
"low": [("background-color", "#f7fbff"), ("color", "#000000")],
"mid": [("background-color", "#abd0e6"), ("color", "#000000")],
"high": [("background-color", "#08306b"), ("color", "#f1f1f1")],
}
elif f == "text_gradient":
colors = {
"low": [("color", "#f7fbff")],
"mid": [("color", "#abd0e6")],
"high": [("color", "#08306b")],
}
result = getattr(styler, f)(cmap="Blues", axis=axis)._compute().ctx
for i, cell in enumerate([(0, 0), (0, 1), (1, 0), (1, 1)]):
assert result[cell] == bg_colors[expected[i]]
assert result[cell] == colors[expected[i]]


@pytest.mark.parametrize(
Expand Down