Skip to content

Commit 5f2dfc2

Browse files
committed
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
1 parent 5d0daa0 commit 5f2dfc2

File tree

3 files changed

+148
-182
lines changed

3 files changed

+148
-182
lines changed

doc/source/whatsnew/v0.24.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ Other
425425

426426
- :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`)
427427
- Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`)
428+
- :meth: `~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None``
428429
-
429430
-
430431
-

pandas/io/formats/style.py

+55-129
Original file line numberDiff line numberDiff line change
@@ -984,126 +984,63 @@ def set_properties(self, subset=None, **kwargs):
984984
return self.applymap(f, subset=subset)
985985

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

1067-
if s.min() >= 0:
1068-
# In this case, we place the zero at the left, and the max() should
1069-
# be at width
1070-
zero = 0.0
1071-
slope = width / s.max()
1072-
elif s.max() <= 0:
1073-
# In this case, we place the zero at the right, and the min()
1074-
# should be at 100-width
1075-
zero = 100.0
1076-
slope = width / -s.min()
1024+
if s.ndim == 1:
1025+
return [css(x) for x in normed]
10771026
else:
1078-
slope = width / (s.max() - s.min())
1079-
zero = (100.0 + width) / 2.0 - slope * s.max()
1080-
1081-
normed = zero + slope * s
1082-
1083-
attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
1084-
', transparent {w:.1f}%, {c} {w:.1f}%, '
1085-
'{c} {zero:.1f}%, transparent {zero:.1f}%)')
1086-
1087-
attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
1088-
', transparent {zero:.1f}%, {c} {zero:.1f}%, '
1089-
'{c} {w:.1f}%, transparent {w:.1f}%)')
1090-
1091-
return [attrs_pos.format(c=color[1], zero=zero, w=x) if x > zero
1092-
else attrs_neg.format(c=color[0], zero=zero, w=x)
1093-
for x in normed]
1027+
return pd.DataFrame(
1028+
[[css(x) for x in row] for row in normed],
1029+
index=s.index, columns=s.columns
1030+
)
10941031

10951032
def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
10961033
align='left'):
10971034
"""
1098-
Color the background ``color`` proportional to the values in each
1099-
column.
1035+
Draw bars in the cell backgrounds, with a width proportional to
1036+
the values
11001037
Excludes non-numeric data by default.
11011038
11021039
Parameters
11031040
----------
11041041
subset: IndexSlice, default None
11051042
a valid slice for ``data`` to limit the style application to
1106-
axis: int
1043+
axis: int or None
11071044
color: str or 2-tuple/list
11081045
If a str is passed, the color is the same for both
11091046
negative and positive numbers. If 2-tuple/list is used, the
@@ -1125,33 +1062,22 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
11251062
-------
11261063
self : Styler
11271064
"""
1128-
subset = _maybe_numeric_slice(self.data, subset)
1129-
subset = _non_reducing_slice(subset)
1130-
1131-
base = 'width: 10em; height: 80%;'
1065+
if align not in ('left', 'zero', 'mid'):
1066+
raise ValueError("`align` must be one of {'left', 'zero',' mid'}")
11321067

1133-
if not(is_list_like(color)):
1068+
if not (is_list_like(color)):
11341069
color = [color, color]
11351070
elif len(color) == 1:
11361071
color = [color[0], color[0]]
11371072
elif len(color) > 2:
1138-
msg = ("Must pass `color` as string or a list-like"
1139-
" of length 2: [`color_negative`, `color_positive`]\n"
1140-
"(eg: color=['#d65f5f', '#5fba7d'])")
1141-
raise ValueError(msg)
1073+
raise ValueError("`color` must be string or a list-like"
1074+
" of length 2: [`color_neg`, `color_pos`]"
1075+
" (eg: color=['#d65f5f', '#5fba7d'])")
11421076

1143-
if align == 'left':
1144-
self.apply(self._bar_left, subset=subset, axis=axis, color=color,
1145-
width=width, base=base)
1146-
elif align == 'zero':
1147-
self.apply(self._bar_center_zero, subset=subset, axis=axis,
1148-
color=color, width=width, base=base)
1149-
elif align == 'mid':
1150-
self.apply(self._bar_center_mid, subset=subset, axis=axis,
1151-
color=color, width=width, base=base)
1152-
else:
1153-
msg = ("`align` must be one of {'left', 'zero',' mid'}")
1154-
raise ValueError(msg)
1077+
subset = _maybe_numeric_slice(self.data, subset)
1078+
subset = _non_reducing_slice(subset)
1079+
self.apply(self._bar, subset=subset, axis=axis,
1080+
align=align, color=color, width=width)
11551081

11561082
return self
11571083

0 commit comments

Comments
 (0)