Skip to content

ENH: add a gradient map to background gradient #39930

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 45 commits into from
Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ce4cb00
add a gradient map to background gradient
attack68 Feb 20, 2021
d12d0af
whats new
attack68 Feb 20, 2021
e0cf6f0
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 22, 2021
ea7b5ee
add examples
attack68 Feb 22, 2021
d0a3b9e
add examples
attack68 Feb 22, 2021
ddaa18a
add examples
attack68 Feb 22, 2021
72c0825
shape validation
attack68 Feb 22, 2021
c6adc82
update docs
attack68 Feb 22, 2021
898bd8c
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 23, 2021
aa3a83e
add tests to new modules.
attack68 Feb 23, 2021
3bd0c51
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 23, 2021
56e80ef
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 25, 2021
af852d0
update reshaping
attack68 Feb 26, 2021
3c06bd3
update reshaping
attack68 Feb 26, 2021
a696353
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 27, 2021
233e4c3
mypy fix
attack68 Feb 27, 2021
357c1bb
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Feb 28, 2021
59d5a57
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 1, 2021
ffa19c7
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 2, 2021
1f44a5b
req changes
attack68 Mar 4, 2021
233135a
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 5, 2021
0245960
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 5, 2021
64136e9
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 7, 2021
73a5cb1
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 8, 2021
a92e42d
html tests
attack68 Mar 9, 2021
0ec339e
align DataFrame and Series gmap with underlying data
attack68 Mar 9, 2021
b6c2fda
align DataFrame and Series gmap with underlying data
attack68 Mar 9, 2021
bd025b8
mypy fixup
attack68 Mar 9, 2021
277e2d7
make elif
attack68 Mar 10, 2021
7141e4c
add tests
attack68 Mar 10, 2021
439a853
add arg shape validation function
attack68 Mar 17, 2021
88117eb
change 'right' text to 'correct' to avoid ambiguity
attack68 Mar 17, 2021
380d2d3
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 17, 2021
b7b7179
mypy updates
attack68 Mar 17, 2021
2656c75
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 24, 2021
41f0e8c
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 27, 2021
02d13f4
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 28, 2021
8db2b50
partial out axis_
attack68 Mar 28, 2021
3612431
remove axis and remove staticmethod
attack68 Mar 29, 2021
e6c3aaa
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Mar 31, 2021
9d247e9
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Apr 2, 2021
aad0fa9
Merge remote-tracking branch 'upstream/master' into background_gradie…
attack68 Apr 2, 2021
968e99f
pre-commit fix
attack68 Apr 2, 2021
130d735
merge upstream master
attack68 Apr 6, 2021
946d11f
fix typing to new standard
attack68 Apr 6, 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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Other enhancements
- :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`)
- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`)
- :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`)
- :meth:`.Styler.background_gradient` now allows the ability to supply a specific gradient map (:issue:`22727`)
- :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`)
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)
Expand Down
74 changes: 43 additions & 31 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,63 +1308,78 @@ def background_gradient(
text_color_threshold: float = 0.408,
vmin: Optional[float] = None,
vmax: Optional[float] = None,
gmap: Optional[Sequence] = None,
) -> Styler:
"""
Color the background in a gradient style.

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

Parameters
----------
cmap : str or colormap
Matplotlib colormap.
low : float
Compress the range by the low.
Compress the color range at the low end. This is a multiple of the data
range to extend below the minimum; sound values usually in [0, 0.5],
defaults to 0.
high : float
Compress the range by the high.
Compress the color range at the high end. This is a multiple of the data
range to extend above the maximum; sound values usually in [0, 0.5],
defaults to 0.
axis : {0 or 'index', 1 or 'columns', None}, 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``.
subset : IndexSlice
A valid slice for ``data`` to limit the style application to.
text_color_threshold : float or int
Luminance threshold for determining text color. Facilitates text
visibility across varying background colors. From 0 to 1.
0 = all text is dark colored, 1 = all text is light colored.
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.

.. versionadded:: 0.24.0

vmin : float, optional
Minimum data value that corresponds to colormap minimum value.
When None (default): the minimum value of the data will be used.
If not specified the minimum value of the data will be used.

.. versionadded:: 1.0.0

vmax : float, optional
Maximum data value that corresponds to colormap maximum value.
When None (default): the maximum value of the data will be used.
If not specified the maximum value of the data will be used.

.. versionadded:: 1.0.0

gmap : array-like, optional
Gradient map for determining the background colors. If not supplied
will use the input data from rows, columns or frame. Must be an
identical shape for sampling columns, rows or DataFrame based on ``axis``.

.. versionadded:: 1.3.0

Returns
-------
self : Styler

Raises
------
ValueError
If ``text_color_threshold`` is not a value from 0 to 1.

Notes
-----
Set ``text_color_threshold`` or tune ``low`` and ``high`` to keep the
text legible by not using the entire range of the color map. The range
of the data is extended by ``low * (x.max() - x.min())`` and ``high *
(x.max() - x.min())`` before normalizing.
When using ``low`` and ``high`` the range
of the data is extended at the low end effectively by
`data.min - low * data.range` and at the high end by
`data.max + high * data.range` before the colors are normalized and determined.

If combining with ``vmin`` and ``vmax`` the `data.min`, `data.max` and
`data.range` are replaced by values according to the values derived from
``vmin`` and ``vmax``.

This method will preselect numeric columns and ignore non-numeric columns
unless a ``gmap`` is supplied in which case no preselection occurs.
"""
if subset is None:
if subset is None and gmap is None:
subset = self.data.select_dtypes(include=np.number).columns

self.apply(
Expand All @@ -1377,6 +1392,7 @@ def background_gradient(
text_color_threshold=text_color_threshold,
vmin=vmin,
vmax=vmax,
gmap=gmap,
)
return self

Expand All @@ -1389,26 +1405,22 @@ def _background_gradient(
text_color_threshold: float = 0.408,
vmin: Optional[float] = None,
vmax: Optional[float] = None,
gmap: Optional[Sequence] = None,
):
"""
Color background in a range according to the data.
Color background in a range according to the data or a gradient map
"""
if (
not isinstance(text_color_threshold, (float, int))
or not 0 <= text_color_threshold <= 1
):
msg = "`text_color_threshold` must be a value from 0 to 1."
raise ValueError(msg)

if gmap is None:
gmap = s.to_numpy(dtype=float)
else:
gmap = np.asarray(gmap, dtype=float).reshape(s.shape)
with _mpl(Styler.background_gradient) as (plt, colors):
smin = np.nanmin(s.to_numpy()) if vmin is None else vmin
smax = np.nanmax(s.to_numpy()) if vmax is None else vmax
smin = np.nanmin(gmap) if vmin is None else vmin
smax = np.nanmax(gmap) if vmax is None else vmax
rng = smax - smin
# extend lower / upper bounds, compresses color range
norm = colors.Normalize(smin - (rng * low), smax + (rng * high))
# matplotlib colors.Normalize modifies inplace?
# https://github.com/matplotlib/matplotlib/issues/5427
rgbas = plt.cm.get_cmap(cmap)(norm(s.to_numpy(dtype=float)))
rgbas = plt.cm.get_cmap(cmap)(norm(gmap))

def relative_luminance(rgba) -> float:
"""
Expand Down
49 changes: 40 additions & 9 deletions pandas/tests/io/formats/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1966,15 +1966,6 @@ def test_text_color_threshold(self, c_map, expected):
result = df.style.background_gradient(cmap=c_map)._compute().ctx
assert result == expected

@pytest.mark.parametrize("text_color_threshold", [1.1, "1", -1, [2, 2]])
def test_text_color_threshold_raises(self, text_color_threshold):
df = DataFrame([[1, 2], [2, 4]], columns=["A", "B"])
msg = "`text_color_threshold` must be a value from 0 to 1."
with pytest.raises(ValueError, match=msg):
df.style.background_gradient(
text_color_threshold=text_color_threshold
)._compute()

@td.skip_if_no_mpl
def test_background_gradient_axis(self):
df = DataFrame([[1, 2], [2, 4]], columns=["A", "B"])
Expand Down Expand Up @@ -2017,6 +2008,46 @@ def test_background_gradient_int64(self):
assert ctx2[(1, 0)] == ctx1[(1, 0)]
assert ctx2[(2, 0)] == ctx1[(2, 0)]

@pytest.mark.parametrize(
"axis, gmap, expected",
[
(
0,
[1, 2],
{
(0, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
(1, 0): [("background-color", "#023858"), ("color", "#f1f1f1")],
(0, 1): [("background-color", "#fff7fb"), ("color", "#000000")],
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
},
),
(
1,
[1, 2],
{
(0, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
(1, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
(0, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
},
),
(
None,
np.array([[2, 1], [1, 2]]),
{
(0, 0): [("background-color", "#023858"), ("color", "#f1f1f1")],
(1, 0): [("background-color", "#fff7fb"), ("color", "#000000")],
(0, 1): [("background-color", "#fff7fb"), ("color", "#000000")],
(1, 1): [("background-color", "#023858"), ("color", "#f1f1f1")],
},
),
],
)
def test_background_gradient_gmap(self, axis, gmap, expected):
df = DataFrame([[1, 2], [2, 1]])
result = df.style.background_gradient(axis=axis, gmap=gmap)._compute().ctx
assert result == expected


def test_block_names():
# catch accidental removal of a block
Expand Down