Skip to content

Commit d144f7c

Browse files
committed
ENH: Styler.bar: add support for tablewise application with axis=None #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
1 parent ffae158 commit d144f7c

File tree

3 files changed

+148
-184
lines changed

3 files changed

+148
-184
lines changed

doc/source/whatsnew/v0.24.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@ Other
686686
- :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`)
687687
- Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`)
688688
- :meth: `~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`)
689+
- :meth: `~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None``
689690
-
690691
-
691692
-

pandas/io/formats/style.py

+55-131
Original file line numberDiff line numberDiff line change
@@ -993,132 +993,67 @@ def set_properties(self, subset=None, **kwargs):
993993
return self.applymap(f, subset=subset)
994994

995995
@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]
996+
def _bar(s, align, colors, width=100):
997+
"""Draw bar chart in dataframe cells"""
998+
999+
# Get input value range.
1000+
smin = s.values.min()
1001+
smax = s.values.max()
1002+
if align == 'mid':
1003+
smin = min(0, smin)
1004+
smax = max(0, smax)
1005+
elif align == 'zero':
1006+
# For "zero" mode, we want the range to be symmetrical around zero.
1007+
smax = max(abs(smin), abs(smax))
1008+
smin = -smax
1009+
# Transform to percent-range of linear-gradient
1010+
normed = width * (s.values - smin) / (smax - smin + 1e-12)
1011+
zero = -width * smin / (smax - smin + 1e-12)
1012+
1013+
def css_bar(start, end, color):
1014+
"""Generate CSS code to draw a bar from start to end."""
1015+
css = 'width: 10em; height: 80%;'
1016+
if end > start:
1017+
css += 'background: linear-gradient(90deg,'
1018+
if start > 0:
1019+
css += ' transparent {s:.1f}%, {c} {s:.1f}%, '.format(
1020+
s=start, c=color
1021+
)
1022+
css += '{c} {e:.1f}%, transparent {e:.1f}%)'.format(
1023+
e=end, c=color,
1024+
)
1025+
return css
10571026

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-
"""
1027+
def css(x):
1028+
if align == 'left':
1029+
return css_bar(0, x, colors[x > zero])
1030+
else:
1031+
return css_bar(min(x, zero), max(x, zero), colors[x > zero])
10751032

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()
1033+
if s.ndim == 1:
1034+
return [css(x) for x in normed]
10861035
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]
1036+
return pd.DataFrame(
1037+
[[css(x) for x in row] for row in normed],
1038+
index=s.index, columns=s.columns
1039+
)
11031040

11041041
def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
11051042
align='left'):
11061043
"""
1107-
Color the background ``color`` proportional to the values in each
1108-
column.
1109-
Excludes non-numeric data by default.
1044+
Draw bar chart in the cell backgrounds.
11101045
11111046
Parameters
11121047
----------
1113-
subset: IndexSlice, default None
1048+
subset: IndexSlice, optional
11141049
a valid slice for ``data`` to limit the style application to
1115-
axis: int
1050+
axis: int, default 0, meaning column-wise
11161051
color: str or 2-tuple/list
11171052
If a str is passed, the color is the same for both
11181053
negative and positive numbers. If 2-tuple/list is used, the
11191054
first element is the color_negative and the second is the
11201055
color_positive (eg: ['#d65f5f', '#5fba7d'])
1121-
width: float
1056+
width: float, default 100
11221057
A number between 0 or 100. The largest value will cover ``width``
11231058
percent of the cell's width
11241059
align : {'left', 'zero',' mid'}, default 'left'
@@ -1134,33 +1069,22 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
11341069
-------
11351070
self : Styler
11361071
"""
1137-
subset = _maybe_numeric_slice(self.data, subset)
1138-
subset = _non_reducing_slice(subset)
1139-
1140-
base = 'width: 10em; height: 80%;'
1072+
if align not in ('left', 'zero', 'mid'):
1073+
raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
11411074

1142-
if not(is_list_like(color)):
1075+
if not (is_list_like(color)):
11431076
color = [color, color]
11441077
elif len(color) == 1:
11451078
color = [color[0], color[0]]
11461079
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)
1080+
raise ValueError("`color` must be string or a list-like"
1081+
" of length 2: [`color_neg`, `color_pos`]"
1082+
" (eg: color=['#d65f5f', '#5fba7d'])")
11511083

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)
1084+
subset = _maybe_numeric_slice(self.data, subset)
1085+
subset = _non_reducing_slice(subset)
1086+
self.apply(self._bar, subset=subset, axis=axis,
1087+
align=align, colors=color, width=width)
11641088

11651089
return self
11661090

0 commit comments

Comments
 (0)