From 9f7e99a7ae50c0d8bdfb3c9f42a41c9d324193e3 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Wed, 20 Jun 2018 00:53:27 +0200 Subject: [PATCH 1/4] 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 --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/io/formats/style.py | 186 ++++++++------------------ pandas/tests/io/formats/test_style.py | 145 ++++++++++++-------- 3 files changed, 148 insertions(+), 184 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index d7feb6e547b22..cc7388e5c0a61 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -728,6 +728,7 @@ Other - :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`) - Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`) - :meth: `~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) +- :meth: `~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` - - - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4d68971bf0ef6..3e5cdd991edaa 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -993,132 +993,67 @@ def set_properties(self, subset=None, **kwargs): return self.applymap(f, subset=subset) @staticmethod - def _bar_left(s, color, width, base): - """ - The minimum value is aligned at the left of the cell - Parameters - ---------- - color: 2-tuple/list, of [``color_negative``, ``color_positive``] - width: float - A number between 0 or 100. The largest value will cover ``width`` - percent of the cell's width - base: str - The base css format of the cell, e.g.: - ``base = 'width: 10em; height: 80%;'`` - Returns - ------- - self : Styler - """ - normed = width * (s - s.min()) / (s.max() - s.min()) - zero_normed = width * (0 - s.min()) / (s.max() - s.min()) - attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, ' - 'transparent 0%)') - - return [base if x == 0 else attrs.format(c=color[0], w=x) - if x < zero_normed - else attrs.format(c=color[1], w=x) if x >= zero_normed - else base for x in normed] - - @staticmethod - def _bar_center_zero(s, color, width, base): - """ - Creates a bar chart where the zero is centered in the cell - Parameters - ---------- - color: 2-tuple/list, of [``color_negative``, ``color_positive``] - width: float - A number between 0 or 100. The largest value will cover ``width`` - percent of the cell's width - base: str - The base css format of the cell, e.g.: - ``base = 'width: 10em; height: 80%;'`` - Returns - ------- - self : Styler - """ - - # Either the min or the max should reach the edge - # (50%, centered on zero) - m = max(abs(s.min()), abs(s.max())) - - normed = s * 50 * width / (100.0 * m) - - attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%' - ', transparent {w:.1f}%, {c} {w:.1f}%, ' - '{c} 50%, transparent 50%)') - - attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%' - ', transparent 50%, {c} 50%, {c} {w:.1f}%, ' - 'transparent {w:.1f}%)') - - return [attrs_pos.format(c=color[1], w=(50 + x)) if x >= 0 - else attrs_neg.format(c=color[0], w=(50 + x)) - for x in normed] + def _bar(s, align, colors, width=100): + """Draw bar chart in dataframe cells""" + + # Get input value range. + smin = s.values.min() + smax = s.values.max() + if align == 'mid': + smin = min(0, smin) + smax = max(0, smax) + elif align == 'zero': + # For "zero" mode, we want the range to be symmetrical around zero. + smax = max(abs(smin), abs(smax)) + smin = -smax + # Transform to percent-range of linear-gradient + normed = width * (s.values - smin) / (smax - smin + 1e-12) + zero = -width * smin / (smax - smin + 1e-12) + + def css_bar(start, end, color): + """Generate CSS code to draw a bar from start to end.""" + css = 'width: 10em; height: 80%;' + if end > start: + css += 'background: linear-gradient(90deg,' + if start > 0: + css += ' transparent {s:.1f}%, {c} {s:.1f}%, '.format( + s=start, c=color + ) + css += '{c} {e:.1f}%, transparent {e:.1f}%)'.format( + e=end, c=color, + ) + return css - @staticmethod - def _bar_center_mid(s, color, width, base): - """ - Creates a bar chart where the midpoint is centered in the cell - Parameters - ---------- - color: 2-tuple/list, of [``color_negative``, ``color_positive``] - width: float - A number between 0 or 100. The largest value will cover ``width`` - percent of the cell's width - base: str - The base css format of the cell, e.g.: - ``base = 'width: 10em; height: 80%;'`` - Returns - ------- - self : Styler - """ + def css(x): + if align == 'left': + return css_bar(0, x, colors[x > zero]) + else: + return css_bar(min(x, zero), max(x, zero), colors[x > zero]) - if s.min() >= 0: - # In this case, we place the zero at the left, and the max() should - # be at width - zero = 0.0 - slope = width / s.max() - elif s.max() <= 0: - # In this case, we place the zero at the right, and the min() - # should be at 100-width - zero = 100.0 - slope = width / -s.min() + if s.ndim == 1: + return [css(x) for x in normed] else: - slope = width / (s.max() - s.min()) - zero = (100.0 + width) / 2.0 - slope * s.max() - - normed = zero + slope * s - - attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%' - ', transparent {w:.1f}%, {c} {w:.1f}%, ' - '{c} {zero:.1f}%, transparent {zero:.1f}%)') - - attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%' - ', transparent {zero:.1f}%, {c} {zero:.1f}%, ' - '{c} {w:.1f}%, transparent {w:.1f}%)') - - return [attrs_pos.format(c=color[1], zero=zero, w=x) if x > zero - else attrs_neg.format(c=color[0], zero=zero, w=x) - for x in normed] + return pd.DataFrame( + [[css(x) for x in row] for row in normed], + index=s.index, columns=s.columns + ) def bar(self, subset=None, axis=0, color='#d65f5f', width=100, align='left'): """ - Color the background ``color`` proportional to the values in each - column. - Excludes non-numeric data by default. + Draw bar chart in the cell backgrounds. Parameters ---------- - subset: IndexSlice, default None + subset: IndexSlice, optional a valid slice for ``data`` to limit the style application to - axis: int + axis: int, default 0, meaning column-wise color: str or 2-tuple/list If a str is passed, the color is the same for both negative and positive numbers. If 2-tuple/list is used, the first element is the color_negative and the second is the color_positive (eg: ['#d65f5f', '#5fba7d']) - width: float + width: float, default 100 A number between 0 or 100. The largest value will cover ``width`` percent of the cell's width align : {'left', 'zero',' mid'}, default 'left' @@ -1134,33 +1069,22 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100, ------- self : Styler """ - subset = _maybe_numeric_slice(self.data, subset) - subset = _non_reducing_slice(subset) - - base = 'width: 10em; height: 80%;' + if align not in ('left', 'zero', 'mid'): + raise ValueError("`align` must be one of {'left', 'zero',' mid'}") - if not(is_list_like(color)): + if not (is_list_like(color)): color = [color, color] elif len(color) == 1: color = [color[0], color[0]] elif len(color) > 2: - msg = ("Must pass `color` as string or a list-like" - " of length 2: [`color_negative`, `color_positive`]\n" - "(eg: color=['#d65f5f', '#5fba7d'])") - raise ValueError(msg) + raise ValueError("`color` must be string or a list-like" + " of length 2: [`color_neg`, `color_pos`]" + " (eg: color=['#d65f5f', '#5fba7d'])") - if align == 'left': - self.apply(self._bar_left, subset=subset, axis=axis, color=color, - width=width, base=base) - elif align == 'zero': - self.apply(self._bar_center_zero, subset=subset, axis=axis, - color=color, width=width, base=base) - elif align == 'mid': - self.apply(self._bar_center_mid, subset=subset, axis=axis, - color=color, width=width, base=base) - else: - msg = ("`align` must be one of {'left', 'zero',' mid'}") - raise ValueError(msg) + subset = _maybe_numeric_slice(self.data, subset) + subset = _non_reducing_slice(subset) + self.apply(self._bar, subset=subset, axis=axis, + align=align, colors=color, width=width) return self diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index bcfd3cbb739ff..efe07ec836aaf 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -349,10 +349,10 @@ def test_bar_align_left(self): (0, 0): ['width: 10em', ' height: 80%'], (1, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(' - '90deg,#d65f5f 50.0%, transparent 0%)'], + '90deg,#d65f5f 50.0%, transparent 50.0%)'], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(' - '90deg,#d65f5f 100.0%, transparent 0%)'] + '90deg,#d65f5f 100.0%, transparent 100.0%)'] } assert result == expected @@ -361,10 +361,10 @@ def test_bar_align_left(self): (0, 0): ['width: 10em', ' height: 80%'], (1, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(' - '90deg,red 25.0%, transparent 0%)'], + '90deg,red 25.0%, transparent 25.0%)'], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(' - '90deg,red 50.0%, transparent 0%)'] + '90deg,red 50.0%, transparent 50.0%)'] } assert result == expected @@ -383,46 +383,46 @@ def test_bar_align_left_0points(self): (0, 2): ['width: 10em', ' height: 80%'], (1, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%,' - ' transparent 0%)'], + ' transparent 50.0%)'], (1, 1): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%,' - ' transparent 0%)'], + ' transparent 50.0%)'], (1, 2): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%,' - ' transparent 0%)'], + ' transparent 50.0%)'], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)'], + ', transparent 100.0%)'], (2, 1): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)'], + ', transparent 100.0%)'], (2, 2): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)']} + ', transparent 100.0%)']} assert result == expected result = df.style.bar(axis=1)._compute().ctx expected = {(0, 0): ['width: 10em', ' height: 80%'], (0, 1): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%,' - ' transparent 0%)'], + ' transparent 50.0%)'], (0, 2): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)'], + ', transparent 100.0%)'], (1, 0): ['width: 10em', ' height: 80%'], (1, 1): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%' - ', transparent 0%)'], + ', transparent 50.0%)'], (1, 2): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)'], + ', transparent 100.0%)'], (2, 0): ['width: 10em', ' height: 80%'], (2, 1): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 50.0%' - ', transparent 0%)'], + ', transparent 50.0%)'], (2, 2): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg,#d65f5f 100.0%' - ', transparent 0%)']} + ', transparent 100.0%)']} assert result == expected def test_bar_align_mid_pos_and_neg(self): @@ -432,21 +432,16 @@ def test_bar_align_mid_pos_and_neg(self): '#d65f5f', '#5fba7d'])._compute().ctx expected = {(0, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, #d65f5f 0.0%, ' + 'background: linear-gradient(90deg,' '#d65f5f 10.0%, transparent 10.0%)'], - (1, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 10.0%, ' - '#d65f5f 10.0%, #d65f5f 10.0%, ' - 'transparent 10.0%)'], + (1, 0): ['width: 10em', ' height: 80%', ], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 10.0%, #5fba7d 10.0%' + 'transparent 10.0%, #5fba7d 10.0%' ', #5fba7d 30.0%, transparent 30.0%)'], (3, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 10.0%, ' + 'transparent 10.0%, ' '#5fba7d 10.0%, #5fba7d 100.0%, ' 'transparent 100.0%)']} @@ -459,20 +454,16 @@ def test_bar_align_mid_all_pos(self): '#d65f5f', '#5fba7d'])._compute().ctx expected = {(0, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, ' + 'background: linear-gradient(90deg,' '#5fba7d 10.0%, transparent 10.0%)'], (1, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, ' + 'background: linear-gradient(90deg,' '#5fba7d 20.0%, transparent 20.0%)'], (2, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, ' + 'background: linear-gradient(90deg,' '#5fba7d 50.0%, transparent 50.0%)'], (3, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, ' + 'background: linear-gradient(90deg,' '#5fba7d 100.0%, transparent 100.0%)']} assert result == expected @@ -484,23 +475,21 @@ def test_bar_align_mid_all_neg(self): '#d65f5f', '#5fba7d'])._compute().ctx expected = {(0, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 0.0%, ' - '#d65f5f 0.0%, #d65f5f 100.0%, ' - 'transparent 100.0%)'], + 'background: linear-gradient(90deg,' + '#d65f5f 100.0%, transparent 100.0%)'], (1, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 40.0%, ' + 'transparent 40.0%, ' '#d65f5f 40.0%, #d65f5f 100.0%, ' 'transparent 100.0%)'], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 70.0%, ' + 'transparent 70.0%, ' '#d65f5f 70.0%, #d65f5f 100.0%, ' 'transparent 100.0%)'], (3, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 80.0%, ' + 'transparent 80.0%, ' '#d65f5f 80.0%, #d65f5f 100.0%, ' 'transparent 100.0%)']} assert result == expected @@ -511,25 +500,75 @@ def test_bar_align_zero_pos_and_neg(self): result = df.style.bar(align='zero', color=[ '#d65f5f', '#5fba7d'], width=90)._compute().ctx - expected = {(0, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 45.0%, ' - '#d65f5f 45.0%, #d65f5f 50%, ' - 'transparent 50%)'], - (1, 0): ['width: 10em', ' height: 80%', - 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 50%, ' - '#5fba7d 50%, #5fba7d 50.0%, ' - 'transparent 50.0%)'], + 'transparent 40.0%, #d65f5f 40.0%, ' + '#d65f5f 45.0%, transparent 45.0%)'], + (1, 0): ['width: 10em', ' height: 80%'], (2, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 50%, #5fba7d 50%, ' - '#5fba7d 60.0%, transparent 60.0%)'], + 'transparent 45.0%, #5fba7d 45.0%, ' + '#5fba7d 55.0%, transparent 55.0%)'], (3, 0): ['width: 10em', ' height: 80%', 'background: linear-gradient(90deg, ' - 'transparent 0%, transparent 50%, #5fba7d 50%, ' - '#5fba7d 95.0%, transparent 95.0%)']} + 'transparent 45.0%, #5fba7d 45.0%, ' + '#5fba7d 90.0%, transparent 90.0%)']} + assert result == expected + + def test_bar_align_left_axis_none(self): + df = pd.DataFrame({'A': [0, 1], 'B': [2, 4]}) + result = df.style.bar(axis=None)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 25.0%, transparent 25.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 50.0%, transparent 50.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + + def test_bar_align_zero_axis_none(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='zero', axis=None)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 50.0%, #d65f5f 50.0%, ' + '#d65f5f 62.5%, transparent 62.5%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 50.0%, transparent 50.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 50.0%, #d65f5f 50.0%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + + def test_bar_align_mid_axis_none(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='mid', axis=None)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 33.3%, #d65f5f 33.3%, ' + '#d65f5f 50.0%, transparent 50.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 33.3%, transparent 33.3%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 33.3%, #d65f5f 33.3%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } assert result == expected def test_bar_bad_align_raises(self): From 6b4e2c2604b524fd3eb0b568a57034976586f232 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Mon, 25 Jun 2018 00:07:30 +0200 Subject: [PATCH 2/4] ENH: Styler.bar: add support for vmin/vmax #21526 --- doc/source/whatsnew/v0.24.0.txt | 6 +- pandas/io/formats/style.py | 28 ++++++++-- pandas/tests/io/formats/test_style.py | 80 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index cc7388e5c0a61..568cd2c73bc94 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -725,10 +725,10 @@ Build Changes Other ^^^^^ -- :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`) +- :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`) - Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`) -- :meth: `~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) -- :meth: `~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` +- :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) +- :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`) - - - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3e5cdd991edaa..77b3e964238f0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -993,12 +993,12 @@ def set_properties(self, subset=None, **kwargs): return self.applymap(f, subset=subset) @staticmethod - def _bar(s, align, colors, width=100): + def _bar(s, align, colors, width=100, vmin=None, vmax=None): """Draw bar chart in dataframe cells""" # Get input value range. - smin = s.values.min() - smax = s.values.max() + smin = s.values.min() if vmin is None else vmin + smax = s.values.max() if vmax is None else vmax if align == 'mid': smin = min(0, smin) smax = max(0, smax) @@ -1020,7 +1020,7 @@ def css_bar(start, end, color): s=start, c=color ) css += '{c} {e:.1f}%, transparent {e:.1f}%)'.format( - e=end, c=color, + e=min(end, width), c=color, ) return css @@ -1039,7 +1039,7 @@ def css(x): ) def bar(self, subset=None, axis=0, color='#d65f5f', width=100, - align='left'): + align='left', vmin=None, vmax=None): """ Draw bar chart in the cell backgrounds. @@ -1065,6 +1065,21 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100, .. versionadded:: 0.20.0 + vmin: float, optional + minimum bar value, defining the left hand limit + of the bar drawing range, lower values are clipped to ``vmin``. + When None (default): the minimum value of the data will be used. + + .. versionadded:: 0.24.0 + + vmax: float, optional + maximum bar value, defining the right hand limit + of the bar drawing range, higher values are clipped to ``vmax``. + When None (default): the maximum value of the data will be used. + + .. versionadded:: 0.24.0 + + Returns ------- self : Styler @@ -1084,7 +1099,8 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100, subset = _maybe_numeric_slice(self.data, subset) subset = _non_reducing_slice(subset) self.apply(self._bar, subset=subset, axis=axis, - align=align, colors=color, width=width) + align=align, colors=color, width=width, + vmin=vmin, vmax=vmax) return self diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index efe07ec836aaf..c5e82eb412369 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -571,6 +571,86 @@ def test_bar_align_mid_axis_none(self): } assert result == expected + def test_bar_align_mid_vmin(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='mid', axis=None, vmin=-6)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 60.0%, #d65f5f 60.0%, ' + '#d65f5f 70.0%, transparent 70.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 40.0%, #d65f5f 40.0%, ' + '#d65f5f 60.0%, transparent 60.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 60.0%, #d65f5f 60.0%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + + def test_bar_align_mid_vmax(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='mid', axis=None, vmax=8)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 20.0%, #d65f5f 20.0%, ' + '#d65f5f 30.0%, transparent 30.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 20.0%, transparent 20.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 20.0%, #d65f5f 20.0%, ' + '#d65f5f 60.0%, transparent 60.0%)'] + } + assert result == expected + + def test_bar_align_mid_vmin_vmax_wide(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='mid', axis=None, + vmin=-3, vmax=7)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 30.0%, #d65f5f 30.0%, ' + '#d65f5f 40.0%, transparent 40.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 10.0%, #d65f5f 10.0%, ' + '#d65f5f 30.0%, transparent 30.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 30.0%, #d65f5f 30.0%, ' + '#d65f5f 70.0%, transparent 70.0%)'] + } + assert result == expected + + def test_bar_align_mid_vmin_vmax_clipping(self): + df = pd.DataFrame({'A': [0, 1], 'B': [-2, 4]}) + result = df.style.bar(align='mid', axis=None, + vmin=-1, vmax=3)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%'], + (1, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 50.0%, transparent 50.0%)'], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 25.0%, transparent 25.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + def test_bar_bad_align_raises(self): df = pd.DataFrame({'A': [-100, -60, -30, -20]}) with pytest.raises(ValueError): From 1c347553739febe048e688743359ac5e29b95f0d Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Tue, 14 Aug 2018 10:47:02 +0200 Subject: [PATCH 3/4] ENH: Styler.bar: properly support NaNs --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/io/formats/style.py | 12 +++++++-- pandas/tests/io/formats/test_style.py | 39 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 568cd2c73bc94..50845ee697113 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -728,7 +728,7 @@ Other - :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`) - Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`) - :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`) -- :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`) +- :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. - - - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 77b3e964238f0..6272fe9dc7237 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -30,6 +30,8 @@ import pandas.core.common as com from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice from pandas.util._decorators import Appender +from pandas.core.dtypes.generic import ABCSeries + try: import matplotlib.pyplot as plt from matplotlib import colors @@ -997,8 +999,12 @@ def _bar(s, align, colors, width=100, vmin=None, vmax=None): """Draw bar chart in dataframe cells""" # Get input value range. - smin = s.values.min() if vmin is None else vmin - smax = s.values.max() if vmax is None else vmax + smin = s.min() if vmin is None else vmin + if isinstance(smin, ABCSeries): + smin = smin.min() + smax = s.max() if vmax is None else vmax + if isinstance(smax, ABCSeries): + smax = smax.max() if align == 'mid': smin = min(0, smin) smax = max(0, smax) @@ -1025,6 +1031,8 @@ def css_bar(start, end, color): return css def css(x): + if pd.isna(x): + return '' if align == 'left': return css_bar(0, x, colors[x > zero]) else: diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index c5e82eb412369..5254ccc742ab8 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -651,6 +651,45 @@ def test_bar_align_mid_vmin_vmax_clipping(self): } assert result == expected + def test_bar_align_mid_nans(self): + df = pd.DataFrame({'A': [1, None], 'B': [-1, 3]}) + result = df.style.bar(align='mid', axis=None)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 50.0%, transparent 50.0%)'], + (1, 0): [''], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg,' + '#d65f5f 25.0%, transparent 25.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + + def test_bar_align_zero_nans(self): + df = pd.DataFrame({'A': [1, None], 'B': [-1, 2]}) + result = df.style.bar(align='zero', axis=None)._compute().ctx + expected = { + (0, 0): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 50.0%, #d65f5f 50.0%, ' + '#d65f5f 75.0%, transparent 75.0%)'], + (1, 0): [''], + (0, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 25.0%, #d65f5f 25.0%, ' + '#d65f5f 50.0%, transparent 50.0%)'], + (1, 1): ['width: 10em', ' height: 80%', + 'background: linear-gradient(90deg, ' + 'transparent 50.0%, #d65f5f 50.0%, ' + '#d65f5f 100.0%, transparent 100.0%)'] + } + assert result == expected + def test_bar_bad_align_raises(self): df = pd.DataFrame({'A': [-100, -60, -30, -20]}) with pytest.raises(ValueError): From e32c3334bcdc7fd036cd47d9dcf15bd1f5f453a1 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Tue, 21 Aug 2018 15:58:44 +0200 Subject: [PATCH 4/4] ENH: Styler.bar: docstring improvements --- pandas/io/formats/style.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6272fe9dc7237..6501717f715cb 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1053,36 +1053,40 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100, Parameters ---------- - subset: IndexSlice, optional - a valid slice for ``data`` to limit the style application to - axis: int, default 0, meaning column-wise - color: str or 2-tuple/list + subset : IndexSlice, optional + A valid slice for `data` to limit the style application to. + axis : int, str or None, default 0 + Apply to each column (`axis=0` or `'index'`) + or to each row (`axis=1` or `'columns'`) or + to the entire DataFrame at once with `axis=None`. + color : str or 2-tuple/list If a str is passed, the color is the same for both negative and positive numbers. If 2-tuple/list is used, the first element is the color_negative and the second is the - color_positive (eg: ['#d65f5f', '#5fba7d']) - width: float, default 100 - A number between 0 or 100. The largest value will cover ``width`` - percent of the cell's width + color_positive (eg: ['#d65f5f', '#5fba7d']). + width : float, default 100 + A number between 0 or 100. The largest value will cover `width` + percent of the cell's width. align : {'left', 'zero',' mid'}, default 'left' - - 'left' : the min value starts at the left of the cell - - 'zero' : a value of zero is located at the center of the cell + How to align the bars with the cells. + - 'left' : the min value starts at the left of the cell. + - 'zero' : a value of zero is located at the center of the cell. - 'mid' : the center of the cell is at (max-min)/2, or if values are all negative (positive) the zero is aligned - at the right (left) of the cell + at the right (left) of the cell. .. versionadded:: 0.20.0 - vmin: float, optional - minimum bar value, defining the left hand limit - of the bar drawing range, lower values are clipped to ``vmin``. + vmin : float, optional + Minimum bar value, defining the left hand limit + of the bar drawing range, lower values are clipped to `vmin`. When None (default): the minimum value of the data will be used. .. versionadded:: 0.24.0 - vmax: float, optional - maximum bar value, defining the right hand limit - of the bar drawing range, higher values are clipped to ``vmax``. + vmax : float, optional + Maximum bar value, defining the right hand limit + of the bar drawing range, higher values are clipped to `vmax`. When None (default): the maximum value of the data will be used. .. versionadded:: 0.24.0