-
-
Notifications
You must be signed in to change notification settings - Fork 18.5k
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
Changes from 11 commits
ce4cb00
d12d0af
e0cf6f0
ea7b5ee
d0a3b9e
ddaa18a
72c0825
c6adc82
898bd8c
aa3a83e
3bd0c51
56e80ef
af852d0
3c06bd3
a696353
233e4c3
357c1bb
59d5a57
ffa19c7
1f44a5b
233135a
0245960
64136e9
73a5cb1
a92e42d
0ec339e
b6c2fda
bd025b8
277e2d7
7141e4c
439a853
88117eb
380d2d3
b7b7179
2656c75
41f0e8c
02d13f4
8db2b50
3612431
e6c3aaa
9d247e9
aad0fa9
968e99f
130d735
946d11f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1308,63 +1308,130 @@ 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; good values usually in [0, 1], | ||
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; 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. | ||
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 (or gmap) 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 (or gmap) 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``. | ||
If supplied ``vmin`` and ``vmax`` should be given relative to this | ||
gradient map. | ||
|
||
.. 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 | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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`` | ||
|
||
>>> df.style.background_gradient(axis=0) | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.. figure:: ../../_static/style/bg_ax0.png | ||
|
||
Shading all values collectively using ``axis=None`` | ||
|
||
>>> df.style.background_gradient(axis=None) | ||
|
||
.. figure:: ../../_static/style/bg_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/bg_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/bg_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/bg_gmap.png | ||
|
||
Setting the gradient map for a dataframe (i.e. ``axis=None``), we need to | ||
explicitly state the numeric columns here 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/bg_axNone_gmap.png | ||
""" | ||
if subset is None: | ||
if subset is None and gmap is None: | ||
subset = self.data.select_dtypes(include=np.number).columns | ||
|
||
self.apply( | ||
|
@@ -1377,6 +1444,7 @@ def background_gradient( | |
text_color_threshold=text_color_threshold, | ||
vmin=vmin, | ||
vmax=vmax, | ||
gmap=gmap, | ||
) | ||
return self | ||
|
||
|
@@ -1389,26 +1457,29 @@ 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) | ||
jreback marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if gmap is None: | ||
gmap = s.to_numpy(dtype=float) | ||
else: | ||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, shouldn't this just be a DataFrame that is aligned? why go to all of this complication (and its fragile)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In all cases the in the case
in the case
In the case
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you need to for sure align if a dataframe is passed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok how's this: I've split the 1d and 2d cases still compare shape at end with error if they don't match. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think you are missing my point. what does a 2D gradient map mean / do? I don't see any tests that actually use this nor what its supposed to do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are tests now for 2D arrays and DataFrames, for minimalist correct functionality, but not to demonstrate purpose. the purpose of a 2D map is generally improved visualisation of 2D tabular data. If you just shade the background of a dataframe according to data that you can already read in the cell it just looks nice but doesn't convey information. But if you can, optionally, shade a background based on an alternate 2D map ( You could technically do heat maps currently but you would have to hide the cell data with CSS. This PR offers an easier solution for something like Look at 9. Heat Map (no text in cell). And you can only shade string cells with a provided quantitative map: something like doing this |
||
gmap = np.asarray(gmap, dtype=float).reshape(s.shape) | ||
except ValueError: | ||
raise ValueError( | ||
"supplied 'gmap' is not right shape for data over " | ||
f"selected 'axis': got {np.asarray(gmap).shape}, " | ||
f"expected {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 | ||
attack68 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: | ||
""" | ||
|
Uh oh!
There was an error while loading. Please reload this page.