|
30 | 30 | import pandas.core.common as com
|
31 | 31 | from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
|
32 | 32 | from pandas.util._decorators import Appender
|
| 33 | +from pandas.core.dtypes.generic import ABCSeries |
| 34 | + |
33 | 35 | try:
|
34 | 36 | import matplotlib.pyplot as plt
|
35 | 37 | from matplotlib import colors
|
@@ -993,174 +995,124 @@ def set_properties(self, subset=None, **kwargs):
|
993 | 995 | return self.applymap(f, subset=subset)
|
994 | 996 |
|
995 | 997 | @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 |
1057 | 1032 |
|
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]) |
1075 | 1040 |
|
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] |
1086 | 1043 | 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 | + ) |
1103 | 1048 |
|
1104 | 1049 | def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
|
1105 |
| - align='left'): |
| 1050 | + align='left', vmin=None, vmax=None): |
1106 | 1051 | """
|
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. |
1110 | 1053 |
|
1111 | 1054 | Parameters
|
1112 | 1055 | ----------
|
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 |
1117 | 1063 | If a str is passed, the color is the same for both
|
1118 | 1064 | negative and positive numbers. If 2-tuple/list is used, the
|
1119 | 1065 | 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. |
1124 | 1070 | 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. |
1127 | 1074 | - 'mid' : the center of the cell is at (max-min)/2, or
|
1128 | 1075 | 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. |
1130 | 1077 |
|
1131 | 1078 | .. versionadded:: 0.20.0
|
1132 | 1079 |
|
| 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 | +
|
1133 | 1095 | Returns
|
1134 | 1096 | -------
|
1135 | 1097 | self : Styler
|
1136 | 1098 | """
|
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'}") |
1139 | 1101 |
|
1140 |
| - base = 'width: 10em; height: 80%;' |
1141 |
| - |
1142 |
| - if not(is_list_like(color)): |
| 1102 | + if not (is_list_like(color)): |
1143 | 1103 | color = [color, color]
|
1144 | 1104 | elif len(color) == 1:
|
1145 | 1105 | color = [color[0], color[0]]
|
1146 | 1106 | 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'])") |
1151 | 1110 |
|
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) |
1164 | 1116 |
|
1165 | 1117 | return self
|
1166 | 1118 |
|
|
0 commit comments