diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index d7feb6e547b22..50845ee697113 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -725,9 +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.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`). ``NaN`` values are also handled properly. - - - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4d68971bf0ef6..6501717f715cb 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 @@ -993,174 +995,124 @@ 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, vmin=None, vmax=None): + """Draw bar chart in dataframe cells""" + + # Get input value range. + 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) + 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=min(end, width), 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 pd.isna(x): + return '' + 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'): + align='left', vmin=None, vmax=None): """ - 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 - a valid slice for ``data`` to limit the style application to - axis: int - 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 - 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`. + 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 """ - subset = _maybe_numeric_slice(self.data, subset) - subset = _non_reducing_slice(subset) + if align not in ('left', 'zero', 'mid'): + raise ValueError("`align` must be one of {'left', 'zero',' mid'}") - base = 'width: 10em; height: 80%;' - - 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, + 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 bcfd3cbb739ff..5254ccc742ab8 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,194 @@ 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_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_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):