Skip to content

Commit 5c036fd

Browse files
soxofaanvictor
authored and
victor
committed
ENH? Styler.bar support for axis=None
* ENH: Styler.bar: add support for tablewise application with axis=None pandas-dev#21548 - eliminate code duplication related to style.bar with different align modes - add support for axis=None - fix minor bug with align 'zero' and width < 100 - make generated CSS gradients more compact * ENH: Styler.bar: add support for vmin/vmax pandas-dev#21526 * ENH: Styler.bar: properly support NaNs * ENH: Styler.bar: docstring improvements
1 parent 5d1af2f commit 5c036fd

File tree

3 files changed

+306
-195
lines changed

3 files changed

+306
-195
lines changed

doc/source/whatsnew/v0.24.0.txt

+3-2
Original file line numberDiff line numberDiff line change
@@ -736,9 +736,10 @@ Build Changes
736736
Other
737737
^^^^^
738738

739-
- :meth: `~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`)
739+
- :meth:`~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`)
740740
- Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`)
741-
- :meth: `~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`)
741+
- :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`)
742+
- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly.
742743
-
743744
-
744745
-

pandas/io/formats/style.py

+92-140
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import pandas.core.common as com
3131
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
3232
from pandas.util._decorators import Appender
33+
from pandas.core.dtypes.generic import ABCSeries
34+
3335
try:
3436
import matplotlib.pyplot as plt
3537
from matplotlib import colors
@@ -993,174 +995,124 @@ def set_properties(self, subset=None, **kwargs):
993995
return self.applymap(f, subset=subset)
994996

995997
@staticmethod
996-
def _bar_left(s, color, width, base):
997-
"""
998-
The minimum value is aligned at the left of the cell
999-
Parameters
1000-
----------
1001-
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
1002-
width: float
1003-
A number between 0 or 100. The largest value will cover ``width``
1004-
percent of the cell's width
1005-
base: str
1006-
The base css format of the cell, e.g.:
1007-
``base = 'width: 10em; height: 80%;'``
1008-
Returns
1009-
-------
1010-
self : Styler
1011-
"""
1012-
normed = width * (s - s.min()) / (s.max() - s.min())
1013-
zero_normed = width * (0 - s.min()) / (s.max() - s.min())
1014-
attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, '
1015-
'transparent 0%)')
1016-
1017-
return [base if x == 0 else attrs.format(c=color[0], w=x)
1018-
if x < zero_normed
1019-
else attrs.format(c=color[1], w=x) if x >= zero_normed
1020-
else base for x in normed]
1021-
1022-
@staticmethod
1023-
def _bar_center_zero(s, color, width, base):
1024-
"""
1025-
Creates a bar chart where the zero is centered in the cell
1026-
Parameters
1027-
----------
1028-
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
1029-
width: float
1030-
A number between 0 or 100. The largest value will cover ``width``
1031-
percent of the cell's width
1032-
base: str
1033-
The base css format of the cell, e.g.:
1034-
``base = 'width: 10em; height: 80%;'``
1035-
Returns
1036-
-------
1037-
self : Styler
1038-
"""
1039-
1040-
# Either the min or the max should reach the edge
1041-
# (50%, centered on zero)
1042-
m = max(abs(s.min()), abs(s.max()))
1043-
1044-
normed = s * 50 * width / (100.0 * m)
1045-
1046-
attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
1047-
', transparent {w:.1f}%, {c} {w:.1f}%, '
1048-
'{c} 50%, transparent 50%)')
1049-
1050-
attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
1051-
', transparent 50%, {c} 50%, {c} {w:.1f}%, '
1052-
'transparent {w:.1f}%)')
1053-
1054-
return [attrs_pos.format(c=color[1], w=(50 + x)) if x >= 0
1055-
else attrs_neg.format(c=color[0], w=(50 + x))
1056-
for x in normed]
998+
def _bar(s, align, colors, width=100, vmin=None, vmax=None):
999+
"""Draw bar chart in dataframe cells"""
1000+
1001+
# Get input value range.
1002+
smin = s.min() if vmin is None else vmin
1003+
if isinstance(smin, ABCSeries):
1004+
smin = smin.min()
1005+
smax = s.max() if vmax is None else vmax
1006+
if isinstance(smax, ABCSeries):
1007+
smax = smax.max()
1008+
if align == 'mid':
1009+
smin = min(0, smin)
1010+
smax = max(0, smax)
1011+
elif align == 'zero':
1012+
# For "zero" mode, we want the range to be symmetrical around zero.
1013+
smax = max(abs(smin), abs(smax))
1014+
smin = -smax
1015+
# Transform to percent-range of linear-gradient
1016+
normed = width * (s.values - smin) / (smax - smin + 1e-12)
1017+
zero = -width * smin / (smax - smin + 1e-12)
1018+
1019+
def css_bar(start, end, color):
1020+
"""Generate CSS code to draw a bar from start to end."""
1021+
css = 'width: 10em; height: 80%;'
1022+
if end > start:
1023+
css += 'background: linear-gradient(90deg,'
1024+
if start > 0:
1025+
css += ' transparent {s:.1f}%, {c} {s:.1f}%, '.format(
1026+
s=start, c=color
1027+
)
1028+
css += '{c} {e:.1f}%, transparent {e:.1f}%)'.format(
1029+
e=min(end, width), c=color,
1030+
)
1031+
return css
10571032

1058-
@staticmethod
1059-
def _bar_center_mid(s, color, width, base):
1060-
"""
1061-
Creates a bar chart where the midpoint is centered in the cell
1062-
Parameters
1063-
----------
1064-
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
1065-
width: float
1066-
A number between 0 or 100. The largest value will cover ``width``
1067-
percent of the cell's width
1068-
base: str
1069-
The base css format of the cell, e.g.:
1070-
``base = 'width: 10em; height: 80%;'``
1071-
Returns
1072-
-------
1073-
self : Styler
1074-
"""
1033+
def css(x):
1034+
if pd.isna(x):
1035+
return ''
1036+
if align == 'left':
1037+
return css_bar(0, x, colors[x > zero])
1038+
else:
1039+
return css_bar(min(x, zero), max(x, zero), colors[x > zero])
10751040

1076-
if s.min() >= 0:
1077-
# In this case, we place the zero at the left, and the max() should
1078-
# be at width
1079-
zero = 0.0
1080-
slope = width / s.max()
1081-
elif s.max() <= 0:
1082-
# In this case, we place the zero at the right, and the min()
1083-
# should be at 100-width
1084-
zero = 100.0
1085-
slope = width / -s.min()
1041+
if s.ndim == 1:
1042+
return [css(x) for x in normed]
10861043
else:
1087-
slope = width / (s.max() - s.min())
1088-
zero = (100.0 + width) / 2.0 - slope * s.max()
1089-
1090-
normed = zero + slope * s
1091-
1092-
attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
1093-
', transparent {w:.1f}%, {c} {w:.1f}%, '
1094-
'{c} {zero:.1f}%, transparent {zero:.1f}%)')
1095-
1096-
attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
1097-
', transparent {zero:.1f}%, {c} {zero:.1f}%, '
1098-
'{c} {w:.1f}%, transparent {w:.1f}%)')
1099-
1100-
return [attrs_pos.format(c=color[1], zero=zero, w=x) if x > zero
1101-
else attrs_neg.format(c=color[0], zero=zero, w=x)
1102-
for x in normed]
1044+
return pd.DataFrame(
1045+
[[css(x) for x in row] for row in normed],
1046+
index=s.index, columns=s.columns
1047+
)
11031048

11041049
def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
1105-
align='left'):
1050+
align='left', vmin=None, vmax=None):
11061051
"""
1107-
Color the background ``color`` proportional to the values in each
1108-
column.
1109-
Excludes non-numeric data by default.
1052+
Draw bar chart in the cell backgrounds.
11101053
11111054
Parameters
11121055
----------
1113-
subset: IndexSlice, default None
1114-
a valid slice for ``data`` to limit the style application to
1115-
axis: int
1116-
color: str or 2-tuple/list
1056+
subset : IndexSlice, optional
1057+
A valid slice for `data` to limit the style application to.
1058+
axis : int, str or None, default 0
1059+
Apply to each column (`axis=0` or `'index'`)
1060+
or to each row (`axis=1` or `'columns'`) or
1061+
to the entire DataFrame at once with `axis=None`.
1062+
color : str or 2-tuple/list
11171063
If a str is passed, the color is the same for both
11181064
negative and positive numbers. If 2-tuple/list is used, the
11191065
first element is the color_negative and the second is the
1120-
color_positive (eg: ['#d65f5f', '#5fba7d'])
1121-
width: float
1122-
A number between 0 or 100. The largest value will cover ``width``
1123-
percent of the cell's width
1066+
color_positive (eg: ['#d65f5f', '#5fba7d']).
1067+
width : float, default 100
1068+
A number between 0 or 100. The largest value will cover `width`
1069+
percent of the cell's width.
11241070
align : {'left', 'zero',' mid'}, default 'left'
1125-
- 'left' : the min value starts at the left of the cell
1126-
- 'zero' : a value of zero is located at the center of the cell
1071+
How to align the bars with the cells.
1072+
- 'left' : the min value starts at the left of the cell.
1073+
- 'zero' : a value of zero is located at the center of the cell.
11271074
- 'mid' : the center of the cell is at (max-min)/2, or
11281075
if values are all negative (positive) the zero is aligned
1129-
at the right (left) of the cell
1076+
at the right (left) of the cell.
11301077
11311078
.. versionadded:: 0.20.0
11321079
1080+
vmin : float, optional
1081+
Minimum bar value, defining the left hand limit
1082+
of the bar drawing range, lower values are clipped to `vmin`.
1083+
When None (default): the minimum value of the data will be used.
1084+
1085+
.. versionadded:: 0.24.0
1086+
1087+
vmax : float, optional
1088+
Maximum bar value, defining the right hand limit
1089+
of the bar drawing range, higher values are clipped to `vmax`.
1090+
When None (default): the maximum value of the data will be used.
1091+
1092+
.. versionadded:: 0.24.0
1093+
1094+
11331095
Returns
11341096
-------
11351097
self : Styler
11361098
"""
1137-
subset = _maybe_numeric_slice(self.data, subset)
1138-
subset = _non_reducing_slice(subset)
1099+
if align not in ('left', 'zero', 'mid'):
1100+
raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
11391101

1140-
base = 'width: 10em; height: 80%;'
1141-
1142-
if not(is_list_like(color)):
1102+
if not (is_list_like(color)):
11431103
color = [color, color]
11441104
elif len(color) == 1:
11451105
color = [color[0], color[0]]
11461106
elif len(color) > 2:
1147-
msg = ("Must pass `color` as string or a list-like"
1148-
" of length 2: [`color_negative`, `color_positive`]\n"
1149-
"(eg: color=['#d65f5f', '#5fba7d'])")
1150-
raise ValueError(msg)
1107+
raise ValueError("`color` must be string or a list-like"
1108+
" of length 2: [`color_neg`, `color_pos`]"
1109+
" (eg: color=['#d65f5f', '#5fba7d'])")
11511110

1152-
if align == 'left':
1153-
self.apply(self._bar_left, subset=subset, axis=axis, color=color,
1154-
width=width, base=base)
1155-
elif align == 'zero':
1156-
self.apply(self._bar_center_zero, subset=subset, axis=axis,
1157-
color=color, width=width, base=base)
1158-
elif align == 'mid':
1159-
self.apply(self._bar_center_mid, subset=subset, axis=axis,
1160-
color=color, width=width, base=base)
1161-
else:
1162-
msg = ("`align` must be one of {'left', 'zero',' mid'}")
1163-
raise ValueError(msg)
1111+
subset = _maybe_numeric_slice(self.data, subset)
1112+
subset = _non_reducing_slice(subset)
1113+
self.apply(self._bar, subset=subset, axis=axis,
1114+
align=align, colors=color, width=width,
1115+
vmin=vmin, vmax=vmax)
11641116

11651117
return self
11661118

0 commit comments

Comments
 (0)