Skip to content

Commit 4779ae3

Browse files
attack68JulianWgs
authored andcommitted
ENH: Styler.text_gradient: easy extension alternative to .background_gradient (pandas-dev#41098)
1 parent c7dc75a commit 4779ae3

File tree

10 files changed

+106
-43
lines changed

10 files changed

+106
-43
lines changed

doc/source/_static/style/tg_ax0.png

12.7 KB
Loading
13.2 KB
Loading
13.4 KB
Loading
13.3 KB
Loading
12.5 KB
Loading

doc/source/_static/style/tg_gmap.png

12.3 KB
Loading

doc/source/reference/style.rst

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Builtin styles
5656
Styler.highlight_min
5757
Styler.highlight_between
5858
Styler.background_gradient
59+
Styler.text_gradient
5960
Styler.bar
6061

6162
Style export and import

doc/source/whatsnew/v1.3.0.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ to allow custom CSS highlighting instead of default background coloring (:issue:
120120
Enhancements to other built-in methods include extending the :meth:`.Styler.background_gradient`
121121
method to shade elements based on a given gradient map and not be restricted only to
122122
values in the DataFrame (:issue:`39930` :issue:`22727` :issue:`28901`). Additional
123-
built-in methods such as :meth:`.Styler.highlight_between` and :meth:`.Styler.highlight_quantile`
124-
have been added (:issue:`39821` and :issue:`40926`).
123+
built-in methods such as :meth:`.Styler.highlight_between`, :meth:`.Styler.highlight_quantile`
124+
and :math:`.Styler.text_gradient` have been added (:issue:`39821`, :issue:`40926`, :issue:`41098`).
125125

126126
The :meth:`.Styler.apply` now consistently allows functions with ``ndarray`` output to
127127
allow more flexible development of UDFs when ``axis`` is ``None`` ``0`` or ``1`` (:issue:`39393`).

pandas/io/formats/style.py

+77-28
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,13 @@ def hide_columns(self, subset: Subset) -> Styler:
14701470
# A collection of "builtin" styles
14711471
# -----------------------------------------------------------------------
14721472

1473+
@doc(
1474+
name="background",
1475+
alt="text",
1476+
image_prefix="bg",
1477+
axis="{0 or 'index', 1 or 'columns', None}",
1478+
text_threshold="",
1479+
)
14731480
def background_gradient(
14741481
self,
14751482
cmap="PuBu",
@@ -1483,9 +1490,9 @@ def background_gradient(
14831490
gmap: Sequence | None = None,
14841491
) -> Styler:
14851492
"""
1486-
Color the background in a gradient style.
1493+
Color the {name} in a gradient style.
14871494
1488-
The background color is determined according
1495+
The {name} color is determined according
14891496
to the data in each column, row or frame, or by a given
14901497
gradient map. Requires matplotlib.
14911498
@@ -1501,7 +1508,7 @@ def background_gradient(
15011508
Compress the color range at the high end. This is a multiple of the data
15021509
range to extend above the maximum; good values usually in [0, 1],
15031510
defaults to 0.
1504-
axis : {0 or 'index', 1 or 'columns', None}, default 0
1511+
axis : {axis}, default 0
15051512
Apply to each column (``axis=0`` or ``'index'``), to each row
15061513
(``axis=1`` or ``'columns'``), or to the entire DataFrame at once
15071514
with ``axis=None``.
@@ -1510,6 +1517,7 @@ def background_gradient(
15101517
or single key, to `DataFrame.loc[:, <subset>]` where the columns are
15111518
prioritised, to limit ``data`` to *before* applying the function.
15121519
text_color_threshold : float or int
1520+
{text_threshold}
15131521
Luminance threshold for determining text color in [0, 1]. Facilitates text
15141522
visibility across varying background colors. All text is dark if 0, and
15151523
light if 1, defaults to 0.408.
@@ -1529,7 +1537,7 @@ def background_gradient(
15291537
.. versionadded:: 1.0.0
15301538
15311539
gmap : array-like, optional
1532-
Gradient map for determining the background colors. If not supplied
1540+
Gradient map for determining the {name} colors. If not supplied
15331541
will use the underlying data from rows, columns or frame. If given as an
15341542
ndarray or list-like must be an identical shape to the underlying data
15351543
considering ``axis`` and ``subset``. If given as DataFrame or Series must
@@ -1543,6 +1551,10 @@ def background_gradient(
15431551
-------
15441552
self : Styler
15451553
1554+
See Also
1555+
--------
1556+
Styler.{alt}_gradient: Color the {alt} in a gradient style.
1557+
15461558
Notes
15471559
-----
15481560
When using ``low`` and ``high`` the range
@@ -1560,52 +1572,50 @@ def background_gradient(
15601572
15611573
Examples
15621574
--------
1563-
>>> df = pd.DataFrame({
1564-
... 'City': ['Stockholm', 'Oslo', 'Copenhagen'],
1565-
... 'Temp (c)': [21.6, 22.4, 24.5],
1566-
... 'Rain (mm)': [5.0, 13.3, 0.0],
1567-
... 'Wind (m/s)': [3.2, 3.1, 6.7]
1568-
... })
1575+
>>> df = pd.DataFrame(columns=["City", "Temp (c)", "Rain (mm)", "Wind (m/s)"],
1576+
... data=[["Stockholm", 21.6, 5.0, 3.2],
1577+
... ["Oslo", 22.4, 13.3, 3.1],
1578+
... ["Copenhagen", 24.5, 0.0, 6.7]])
15691579
15701580
Shading the values column-wise, with ``axis=0``, preselecting numeric columns
15711581
1572-
>>> df.style.background_gradient(axis=0)
1582+
>>> df.style.{name}_gradient(axis=0)
15731583
1574-
.. figure:: ../../_static/style/bg_ax0.png
1584+
.. figure:: ../../_static/style/{image_prefix}_ax0.png
15751585
15761586
Shading all values collectively using ``axis=None``
15771587
1578-
>>> df.style.background_gradient(axis=None)
1588+
>>> df.style.{name}_gradient(axis=None)
15791589
1580-
.. figure:: ../../_static/style/bg_axNone.png
1590+
.. figure:: ../../_static/style/{image_prefix}_axNone.png
15811591
15821592
Compress the color map from the both ``low`` and ``high`` ends
15831593
1584-
>>> df.style.background_gradient(axis=None, low=0.75, high=1.0)
1594+
>>> df.style.{name}_gradient(axis=None, low=0.75, high=1.0)
15851595
1586-
.. figure:: ../../_static/style/bg_axNone_lowhigh.png
1596+
.. figure:: ../../_static/style/{image_prefix}_axNone_lowhigh.png
15871597
15881598
Manually setting ``vmin`` and ``vmax`` gradient thresholds
15891599
1590-
>>> df.style.background_gradient(axis=None, vmin=6.7, vmax=21.6)
1600+
>>> df.style.{name}_gradient(axis=None, vmin=6.7, vmax=21.6)
15911601
1592-
.. figure:: ../../_static/style/bg_axNone_vminvmax.png
1602+
.. figure:: ../../_static/style/{image_prefix}_axNone_vminvmax.png
15931603
15941604
Setting a ``gmap`` and applying to all columns with another ``cmap``
15951605
1596-
>>> df.style.background_gradient(axis=0, gmap=df['Temp (c)'], cmap='YlOrRd')
1606+
>>> df.style.{name}_gradient(axis=0, gmap=df['Temp (c)'], cmap='YlOrRd')
15971607
1598-
.. figure:: ../../_static/style/bg_gmap.png
1608+
.. figure:: ../../_static/style/{image_prefix}_gmap.png
15991609
16001610
Setting the gradient map for a dataframe (i.e. ``axis=None``), we need to
16011611
explicitly state ``subset`` to match the ``gmap`` shape
16021612
16031613
>>> gmap = np.array([[1,2,3], [2,3,4], [3,4,5]])
1604-
>>> df.style.background_gradient(axis=None, gmap=gmap,
1614+
>>> df.style.{name}_gradient(axis=None, gmap=gmap,
16051615
... cmap='YlOrRd', subset=['Temp (c)', 'Rain (mm)', 'Wind (m/s)']
16061616
... )
16071617
1608-
.. figure:: ../../_static/style/bg_axNone_gmap.png
1618+
.. figure:: ../../_static/style/{image_prefix}_axNone_gmap.png
16091619
"""
16101620
if subset is None and gmap is None:
16111621
subset = self.data.select_dtypes(include=np.number).columns
@@ -1624,6 +1634,41 @@ def background_gradient(
16241634
)
16251635
return self
16261636

1637+
@doc(
1638+
background_gradient,
1639+
name="text",
1640+
alt="background",
1641+
image_prefix="tg",
1642+
axis="{0 or 'index', 1 or 'columns', None}",
1643+
text_threshold="This argument is ignored (only used in `background_gradient`).",
1644+
)
1645+
def text_gradient(
1646+
self,
1647+
cmap="PuBu",
1648+
low: float = 0,
1649+
high: float = 0,
1650+
axis: Axis | None = 0,
1651+
subset: Subset | None = None,
1652+
vmin: float | None = None,
1653+
vmax: float | None = None,
1654+
gmap: Sequence | None = None,
1655+
) -> Styler:
1656+
if subset is None and gmap is None:
1657+
subset = self.data.select_dtypes(include=np.number).columns
1658+
1659+
return self.apply(
1660+
_background_gradient,
1661+
cmap=cmap,
1662+
subset=subset,
1663+
axis=axis,
1664+
low=low,
1665+
high=high,
1666+
vmin=vmin,
1667+
vmax=vmax,
1668+
gmap=gmap,
1669+
text_only=True,
1670+
)
1671+
16271672
def set_properties(self, subset: Subset | None = None, **kwargs) -> Styler:
16281673
"""
16291674
Set defined CSS-properties to each ``<td>`` HTML element within the given
@@ -2332,6 +2377,7 @@ def _background_gradient(
23322377
vmin: float | None = None,
23332378
vmax: float | None = None,
23342379
gmap: Sequence | np.ndarray | FrameOrSeries | None = None,
2380+
text_only: bool = False,
23352381
):
23362382
"""
23372383
Color background in a range according to the data or a gradient map
@@ -2371,16 +2417,19 @@ def relative_luminance(rgba) -> float:
23712417
)
23722418
return 0.2126 * r + 0.7152 * g + 0.0722 * b
23732419

2374-
def css(rgba) -> str:
2375-
dark = relative_luminance(rgba) < text_color_threshold
2376-
text_color = "#f1f1f1" if dark else "#000000"
2377-
return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
2420+
def css(rgba, text_only) -> str:
2421+
if not text_only:
2422+
dark = relative_luminance(rgba) < text_color_threshold
2423+
text_color = "#f1f1f1" if dark else "#000000"
2424+
return f"background-color: {colors.rgb2hex(rgba)};color: {text_color};"
2425+
else:
2426+
return f"color: {colors.rgb2hex(rgba)};"
23782427

23792428
if data.ndim == 1:
2380-
return [css(rgba) for rgba in rgbas]
2429+
return [css(rgba, text_only) for rgba in rgbas]
23812430
else:
23822431
return DataFrame(
2383-
[[css(rgba) for rgba in row] for row in rgbas],
2432+
[[css(rgba, text_only) for rgba in row] for row in rgbas],
23842433
index=data.index,
23852434
columns=data.columns,
23862435
)

pandas/tests/io/formats/style/test_matplotlib.py

+26-13
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,22 @@ def styler_blank(df_blank):
3333
return Styler(df_blank, uuid_len=0)
3434

3535

36-
def test_background_gradient(styler):
36+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
37+
def test_function_gradient(styler, f):
3738
for c_map in [None, "YlOrRd"]:
38-
result = styler.background_gradient(cmap=c_map)._compute().ctx
39+
result = getattr(styler, f)(cmap=c_map)._compute().ctx
3940
assert all("#" in x[0][1] for x in result.values())
4041
assert result[(0, 0)] == result[(0, 1)]
4142
assert result[(1, 0)] == result[(1, 1)]
4243

4344

44-
def test_background_gradient_color(styler):
45-
result = styler.background_gradient(subset=IndexSlice[1, "A"])._compute().ctx
46-
assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")]
45+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
46+
def test_background_gradient_color(styler, f):
47+
result = getattr(styler, f)(subset=IndexSlice[1, "A"])._compute().ctx
48+
if f == "background_gradient":
49+
assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")]
50+
elif f == "text_gradient":
51+
assert result[(1, 0)] == [("color", "#fff7fb")]
4752

4853

4954
@pytest.mark.parametrize(
@@ -54,15 +59,23 @@ def test_background_gradient_color(styler):
5459
(None, ["low", "mid", "mid", "high"]),
5560
],
5661
)
57-
def test_background_gradient_axis(styler, axis, expected):
58-
bg_colors = {
59-
"low": [("background-color", "#f7fbff"), ("color", "#000000")],
60-
"mid": [("background-color", "#abd0e6"), ("color", "#000000")],
61-
"high": [("background-color", "#08306b"), ("color", "#f1f1f1")],
62-
}
63-
result = styler.background_gradient(cmap="Blues", axis=axis)._compute().ctx
62+
@pytest.mark.parametrize("f", ["background_gradient", "text_gradient"])
63+
def test_background_gradient_axis(styler, axis, expected, f):
64+
if f == "background_gradient":
65+
colors = {
66+
"low": [("background-color", "#f7fbff"), ("color", "#000000")],
67+
"mid": [("background-color", "#abd0e6"), ("color", "#000000")],
68+
"high": [("background-color", "#08306b"), ("color", "#f1f1f1")],
69+
}
70+
elif f == "text_gradient":
71+
colors = {
72+
"low": [("color", "#f7fbff")],
73+
"mid": [("color", "#abd0e6")],
74+
"high": [("color", "#08306b")],
75+
}
76+
result = getattr(styler, f)(cmap="Blues", axis=axis)._compute().ctx
6477
for i, cell in enumerate([(0, 0), (0, 1), (1, 0), (1, 1)]):
65-
assert result[cell] == bg_colors[expected[i]]
78+
assert result[cell] == colors[expected[i]]
6679

6780

6881
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)