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 16 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.
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
161 changes: 155 additions & 6 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,147 @@ def hide_columns(self, subset) -> Styler:
# A collection of "builtin" styles
# -----------------------------------------------------------------------

def text_gradient(
self,
cmap="PuBu",
low: float = 0,
high: float = 0,
axis: Axis | None = 0,
subset=None,
vmin: float | None = None,
vmax: float | None = None,
gmap: Sequence | None = None,
) -> Styler:
"""
Color the text in a gradient style.

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

.. versionadded:: 1.3.0

Parameters
----------
cmap : str or colormap
Matplotlib colormap.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these match the kwargs for background_gradient? if not why not

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you use a shared doc-string?

Copy link
Contributor Author

@attack68 attack68 May 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes they all match its the same signature. Can you use a shared doc string even if the short summary/ extended summary part, and the examples are different? I've never used a shared doc string.. any tips where one is used that I can lookup and copy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind.. i figured it out and made the changes.

low : float
Compress the color range at the low end. This is a multiple of the data
range to extend below the minimum; good values usually in [0, 1],
defaults to 0.
high : float
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
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.
vmin : float, optional
Minimum data value that corresponds to colormap minimum value.
If not specified the minimum value of the data (or gmap) will be used.
vmax : float, optional
Maximum data value that corresponds to colormap maximum value.
If not specified the maximum value of the data (or gmap) will be used.
gmap : array-like, optional
Gradient map for determining the background 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
have same index and column labels considering ``axis`` and ``subset``.
If supplied, ``vmin`` and ``vmax`` should be given relative to this
gradient map.

Returns
-------
self : Styler

See Also
--------
Styler.background_gradient: Color the background in a gradient style.

Notes
-----
When using ``low`` and ``high`` the range
of the gradient, given by the data if ``gmap`` is not given or by ``gmap``,
is extended at the low end effectively by
`map.min - low * map.range` and at the high end by
`map.max + high * map.range` before the colors are normalized and determined.

If combining with ``vmin`` and ``vmax`` the `map.min`, `map.max` and
`map.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.

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]
... })

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

>>> df.style.background_gradient(axis=0)

.. figure:: ../../_static/style/tg_ax0.png

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

>>> df.style.background_gradient(axis=None)

.. figure:: ../../_static/style/tg_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)

.. figure:: ../../_static/style/tg_axNone_lowhigh.png

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

>>> df.style.background_gradient(axis=None, vmin=6.7, vmax=21.6)

.. figure:: ../../_static/style/tg_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')

.. figure:: ../../_static/style/tg_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,
... cmap='YlOrRd', subset=['Temp (c)', 'Rain (mm)', 'Wind (m/s)']
... )

.. figure:: ../../_static/style/tg_axNone_gmap.png
"""
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 background_gradient(
self,
cmap="PuBu",
Expand Down Expand Up @@ -1143,6 +1284,10 @@ def background_gradient(
-------
self : Styler

See Also
--------
Styler.text_gradient: Color the text in a gradient style.

Notes
-----
When using ``low`` and ``high`` the range
Expand Down Expand Up @@ -1918,6 +2063,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 @@ -1957,16 +2103,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