|
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