diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index cc499204318c1..24965a38a4ce1 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1190,9 +1190,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In version 0.20.0 the ability to customize the bar chart further was given. You can now have the `df.style.bar` be centered on zero or midpoint value (in addition to the already existing way of having the min value at the left side of the cell), and you can pass a list of `[color_negative, color_positive]`.\n", + "Additional keyword arguments give more control on centering and positioning, and you can pass a list of `[color_negative, color_positive]` to highlight lower and higher values.\n", "\n", - "Here's how you can change the above with the new `align='mid'` option:" + "Here's how you can change the above with the new `align` option, combined with setting `vmin` and `vmax` limits, the `width` of the figure, and underlying css `props` of cells, leaving space to display the text and the bars:" ] }, { @@ -1201,7 +1201,8 @@ "metadata": {}, "outputs": [], "source": [ - "df2.style.bar(subset=['A', 'B'], align='mid', color=['#d65f5f', '#5fba7d'])" + "df2.style.bar(align=0, vmin=-2.5, vmax=2.5, color=['#d65f5f', '#5fba7d'],\n", + " width=60, props=\"width: 120px; border-right: 1px solid black;\").format('{:.3f}', na_rep=\"\")" ] }, { @@ -1225,28 +1226,31 @@ "\n", "# Test series\n", "test1 = pd.Series([-100,-60,-30,-20], name='All Negative')\n", - "test2 = pd.Series([10,20,50,100], name='All Positive')\n", - "test3 = pd.Series([-10,-5,0,90], name='Both Pos and Neg')\n", + "test2 = pd.Series([-10,-5,0,90], name='Both Pos and Neg')\n", + "test3 = pd.Series([10,20,50,100], name='All Positive')\n", + "test4 = pd.Series([100, 103, 101, 102], name='Large Positive')\n", + "\n", "\n", "head = \"\"\"\n", "\n", " \n", " \n", " \n", - " \n", " \n", + " \n", + " \n", " \n", " \n", "\n", "\"\"\"\n", "\n", - "aligns = ['left','zero','mid']\n", + "aligns = ['left', 'right', 'zero', 'mid', 'mean', 99]\n", "for align in aligns:\n", " row = \"\".format(align)\n", - " for series in [test1,test2,test3]:\n", + " for series in [test1,test2,test3, test4]:\n", " s = series.copy()\n", " s.name=''\n", - " row += \"\".format(s.to_frame().style.bar(align=align, \n", + " row += \"\".format(s.to_frame().style.hide_index().bar(align=align, \n", " color=['#d65f5f', '#5fba7d'], \n", " width=100).render()) #testn['width']\n", " row += ''\n", diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 92cf2bed9ca47..d114f26788f00 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -30,6 +30,7 @@ enhancement2 Other enhancements ^^^^^^^^^^^^^^^^^^ - :meth:`Series.sample`, :meth:`DataFrame.sample`, and :meth:`.GroupBy.sample` now accept a ``np.random.Generator`` as input to ``random_state``. A generator will be more performant, especially with ``replace=False`` (:issue:`38100`) +- Additional options added to :meth:`.Styler.bar` to control alignment and display (:issue:`26070`) - :meth:`Series.ewm`, :meth:`DataFrame.ewm`, now support a ``method`` argument with a ``'table'`` option that performs the windowing operation over an entire :class:`DataFrame`. See :ref:`Window Overview ` for performance and functional benefits (:issue:`42273`) - diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index abc0be6419667..d1e61e12a5dd3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2042,75 +2042,16 @@ def set_properties(self, subset: Subset | None = None, **kwargs) -> Styler: values = "".join(f"{p}: {v};" for p, v in kwargs.items()) return self.applymap(lambda x: values, subset=subset) - @staticmethod - def _bar( - s, - align: str, - colors: list[str], - width: float = 100, - vmin: float | None = None, - vmax: float | None = None, - ): - """ - Draw bar chart in dataframe cells. - """ - # Get input value range. - smin = np.nanmin(s.to_numpy()) if vmin is None else vmin - smax = np.nanmax(s.to_numpy()) if vmax is None else vmax - 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.to_numpy(dtype=float) - smin) / (smax - smin + 1e-12) - zero = -width * smin / (smax - smin + 1e-12) - - def css_bar(start: float, end: float, color: str) -> str: - """ - 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 += f" transparent {start:.1f}%, {color} {start:.1f}%, " - e = min(end, width) - css += f"{color} {e:.1f}%, transparent {e:.1f}%)" - return css - - def css(x): - if pd.isna(x): - return "" - - # avoid deprecated indexing `colors[x > zero]` - color = colors[1] if x > zero else colors[0] - - if align == "left": - return css_bar(0, x, color) - else: - return css_bar(min(x, zero), max(x, zero), color) - - if s.ndim == 1: - return [css(x) for x in normed] - else: - return DataFrame( - [[css(x) for x in row] for row in normed], - index=s.index, - columns=s.columns, - ) - def bar( self, subset: Subset | None = None, axis: Axis | None = 0, color="#d65f5f", width: float = 100, - align: str = "left", + align: str | float | int | Callable = "mid", vmin: float | None = None, vmax: float | None = None, + props: str = "width: 10em;", ) -> Styler: """ Draw bar chart in the cell backgrounds. @@ -2131,16 +2072,26 @@ def bar( 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. - align : {'left', 'zero',' mid'}, default 'left' - How to align the bars with the cells. - - - 'left' : the min value starts at the left of the cell. + The percentage of the cell, measured from the left, in which to draw the + bars, in [0, 100]. + align : str, int, float, callable, default 'mid' + How to align the bars within the cells relative to a width adjusted center. + If string must be one of: + + - 'left' : bars are drawn rightwards from the minimum data value. + - 'right' : bars are drawn leftwards from the maximum data value. - '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. + - 'mid' : a value of (max-min)/2 is located at the center of the cell, + or if all values are negative (positive) the zero is + aligned at the right (left) of the cell. + - 'mean' : the mean value of the data is located at the center of the cell. + + If a float or integer is given this will indicate the center of the cell. + + If a callable should take a 1d or 2d array and return a scalar. + + .. versionchanged:: 1.4.0 + vmin : float, optional Minimum bar value, defining the left hand limit of the bar drawing range, lower values are clipped to `vmin`. @@ -2149,14 +2100,16 @@ def bar( 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. + props : str, optional + The base CSS of the cell that is extended to add the bar chart. Defaults to + `"width: 10em;"` + + .. versionadded:: 1.4.0 Returns ------- self : Styler """ - if align not in ("left", "zero", "mid"): - raise ValueError("`align` must be one of {'left', 'zero',' mid'}") - if not (is_list_like(color)): color = [color, color] elif len(color) == 1: @@ -2172,14 +2125,15 @@ def bar( subset = self.data.select_dtypes(include=np.number).columns self.apply( - self._bar, + _bar, subset=subset, axis=axis, align=align, colors=color, - width=width, + width=width / 100, vmin=vmin, vmax=vmax, + base_css=props, ) return self @@ -2830,3 +2784,166 @@ def _highlight_between( else np.full(data.shape, True, dtype=bool) ) return np.where(g_left & l_right, props, "") + + +def _bar( + data: FrameOrSeries, + align: str | float | int | Callable, + colors: list[str], + width: float, + vmin: float | None, + vmax: float | None, + base_css: str, +): + """ + Draw bar chart in data cells using HTML CSS linear gradient. + + Parameters + ---------- + data : Series or DataFrame + Underling subset of Styler data on which operations are performed. + align : str in {"left", "right", "mid", "zero", "mean"}, int, float, callable + Method for how bars are structured or scalar value of centre point. + colors : list-like of str + Two listed colors as string in valid CSS. + width : float in [0,1] + The percentage of the cell, measured from left, where drawn bars will reside. + vmin : float, optional + Overwrite the minimum value of the window. + vmax : float, optional + Overwrite the maximum value of the window. + base_css : str + Additional CSS that is included in the cell before bars are drawn. + """ + + def css_bar(start: float, end: float, color: str) -> str: + """ + Generate CSS code to draw a bar from start to end in a table cell. + + Uses linear-gradient. + + Parameters + ---------- + start : float + Relative positional start of bar coloring in [0,1] + end : float + Relative positional end of the bar coloring in [0,1] + color : str + CSS valid color to apply. + + Returns + ------- + str : The CSS applicable to the cell. + + Notes + ----- + Uses ``base_css`` from outer scope. + """ + cell_css = base_css + if end > start: + cell_css += "background: linear-gradient(90deg," + if start > 0: + cell_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%," + cell_css += f" {color} {end*100:.1f}%, transparent {end*100:.1f}%)" + return cell_css + + def css_calc(x, left: float, right: float, align: str): + """ + Return the correct CSS for bar placement based on calculated values. + + Parameters + ---------- + x : float + Value which determines the bar placement. + left : float + Value marking the left side of calculation, usually minimum of data. + right : float + Value marking the right side of the calculation, usually maximum of data + (left < right). + align : {"left", "right", "zero", "mid"} + How the bars will be positioned. + "left", "right", "zero" can be used with any values for ``left``, ``right``. + "mid" can only be used where ``left <= 0`` and ``right >= 0``. + "zero" is used to specify a center when all values ``x``, ``left``, + ``right`` are translated, e.g. by say a mean or median. + + Returns + ------- + str : Resultant CSS with linear gradient. + + Notes + ----- + Uses ``colors`` and ``width`` from outer scope. + """ + if pd.isna(x): + return base_css + + color = colors[0] if x < 0 else colors[1] + x = left if x < left else x + x = right if x > right else x # trim data if outside of the window + + start: float = 0 + end: float = 1 + + if align == "left": + # all proportions are measured from the left side between left and right + end = (x - left) / (right - left) + + elif align == "right": + # all proportions are measured from the right side between left and right + start = (x - left) / (right - left) + + else: + z_frac: float = 0.5 # location of zero based on the left-right range + if align == "zero": + # all proportions are measured from the center at zero + limit: float = max(abs(left), abs(right)) + left, right = -limit, limit + elif align == "mid": + # bars drawn from zero either leftwards or rightwards with center at mid + mid: float = (left + right) / 2 + z_frac = ( + -mid / (right - left) + 0.5 if mid < 0 else -left / (right - left) + ) + + if x < 0: + start, end = (x - left) / (right - left), z_frac + else: + start, end = z_frac, (x - left) / (right - left) + + return css_bar(start * width, end * width, color) + + values = data.to_numpy() + left = np.nanmin(values) if vmin is None else vmin + right = np.nanmax(values) if vmax is None else vmax + z: float = 0 # adjustment to translate data + + if align == "mid": + if left >= 0: # "mid" is documented to act as "left" if all values positive + align, left = "left", 0 if vmin is None else vmin + elif right <= 0: # "mid" is documented to act as "right" if all values negative + align, right = "right", 0 if vmax is None else vmax + elif align == "mean": + z, align = np.nanmean(values), "zero" + elif callable(align): + z, align = align(values), "zero" + elif isinstance(align, (float, int)): + z, align = float(align), "zero" + elif not (align == "left" or align == "right" or align == "zero"): + raise ValueError( + "`align` should be in {'left', 'right', 'mid', 'mean', 'zero'} or be a " + "value defining the center line or a callable that returns a float" + ) + + assert isinstance(align, str) # mypy: should now be in [left, right, mid, zero] + if data.ndim == 1: + return [css_calc(x - z, left - z, right - z, align) for x in values] + else: + return DataFrame( + [ + [css_calc(x - z, left - z, right - z, align) for x in row] + for row in values + ], + index=data.index, + columns=data.columns, + ) diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index f81c1fbd6d85e..be7d2fc3be518 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from pandas import DataFrame @@ -7,7 +8,7 @@ def bar_grad(a=None, b=None, c=None, d=None): """Used in multiple tests to simplify formatting of expected result""" - ret = [("width", "10em"), ("height", "80%")] + ret = [("width", "10em")] if all(x is None for x in [a, b, c, d]): return ret return ret + [ @@ -18,59 +19,76 @@ def bar_grad(a=None, b=None, c=None, d=None): ] +def no_bar(): + return bar_grad() + + +def bar_to(x): + return bar_grad(f" #d65f5f {x:.1f}%", f" transparent {x:.1f}%") + + +def bar_from_to(x, y): + return bar_grad( + f" transparent {x:.1f}%", + f" #d65f5f {x:.1f}%", + f" #d65f5f {y:.1f}%", + f" transparent {y:.1f}%", + ) + + class TestStylerBarAlign: def test_bar_align_left(self): df = DataFrame({"A": [0, 1, 2]}) - result = df.style.bar()._compute().ctx + result = df.style.bar(align="left")._compute().ctx expected = { (0, 0): bar_grad(), - (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 0): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), } assert result == expected - result = df.style.bar(color="red", width=50)._compute().ctx + result = df.style.bar(color="red", width=50, align="left")._compute().ctx expected = { (0, 0): bar_grad(), - (1, 0): bar_grad("red 25.0%", " transparent 25.0%"), - (2, 0): bar_grad("red 50.0%", " transparent 50.0%"), + (1, 0): bar_grad(" red 25.0%", " transparent 25.0%"), + (2, 0): bar_grad(" red 50.0%", " transparent 50.0%"), } assert result == expected df["C"] = ["a"] * len(df) - result = df.style.bar(color="red", width=50)._compute().ctx + result = df.style.bar(color="red", width=50, align="left")._compute().ctx assert result == expected df["C"] = df["C"].astype("category") - result = df.style.bar(color="red", width=50)._compute().ctx + result = df.style.bar(color="red", width=50, align="left")._compute().ctx assert result == expected def test_bar_align_left_0points(self): df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - result = df.style.bar()._compute().ctx + result = df.style.bar(align="left")._compute().ctx expected = { (0, 0): bar_grad(), (0, 1): bar_grad(), (0, 2): bar_grad(), - (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 2): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (2, 1): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 0): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (1, 1): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), + (2, 1): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), + (2, 2): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), } assert result == expected - result = df.style.bar(axis=1)._compute().ctx + result = df.style.bar(axis=1, align="left")._compute().ctx expected = { (0, 0): bar_grad(), - (0, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (0, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (0, 1): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (0, 2): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), (1, 0): bar_grad(), - (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 1): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), (2, 0): bar_grad(), - (2, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 1): bar_grad(" #d65f5f 50.0%", " transparent 50.0%"), + (2, 2): bar_grad(" #d65f5f 100.0%", " transparent 100.0%"), } assert result == expected @@ -79,7 +97,7 @@ def test_bar_align_mid_pos_and_neg(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { (0, 0): bar_grad( - "#d65f5f 10.0%", + " #d65f5f 10.0%", " transparent 10.0%", ), (1, 0): bar_grad(), @@ -105,19 +123,19 @@ def test_bar_align_mid_all_pos(self): expected = { (0, 0): bar_grad( - "#5fba7d 10.0%", + " #5fba7d 10.0%", " transparent 10.0%", ), (1, 0): bar_grad( - "#5fba7d 20.0%", + " #5fba7d 20.0%", " transparent 20.0%", ), (2, 0): bar_grad( - "#5fba7d 50.0%", + " #5fba7d 50.0%", " transparent 50.0%", ), (3, 0): bar_grad( - "#5fba7d 100.0%", + " #5fba7d 100.0%", " transparent 100.0%", ), } @@ -131,7 +149,7 @@ def test_bar_align_mid_all_neg(self): expected = { (0, 0): bar_grad( - "#d65f5f 100.0%", + " #d65f5f 100.0%", " transparent 100.0%", ), (1, 0): bar_grad( @@ -189,19 +207,19 @@ def test_bar_align_zero_pos_and_neg(self): def test_bar_align_left_axis_none(self): df = DataFrame({"A": [0, 1], "B": [2, 4]}) - result = df.style.bar(axis=None)._compute().ctx + result = df.style.bar(axis=None, align="left")._compute().ctx expected = { (0, 0): bar_grad(), (1, 0): bar_grad( - "#d65f5f 25.0%", + " #d65f5f 25.0%", " transparent 25.0%", ), (0, 1): bar_grad( - "#d65f5f 50.0%", + " #d65f5f 50.0%", " transparent 50.0%", ), (1, 1): bar_grad( - "#d65f5f 100.0%", + " #d65f5f 100.0%", " transparent 100.0%", ), } @@ -245,7 +263,7 @@ def test_bar_align_mid_axis_none(self): " transparent 50.0%", ), (0, 1): bar_grad( - "#d65f5f 33.3%", + " #d65f5f 33.3%", " transparent 33.3%", ), (1, 1): bar_grad( @@ -295,7 +313,7 @@ def test_bar_align_mid_vmax(self): " transparent 30.0%", ), (0, 1): bar_grad( - "#d65f5f 20.0%", + " #d65f5f 20.0%", " transparent 20.0%", ), (1, 1): bar_grad( @@ -344,7 +362,7 @@ def test_bar_align_mid_vmin_vmax_clipping(self): " #d65f5f 50.0%", " transparent 50.0%", ), - (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (0, 1): bar_grad(" #d65f5f 25.0%", " transparent 25.0%"), (1, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", @@ -364,7 +382,8 @@ def test_bar_align_mid_nans(self): " #d65f5f 50.0%", " transparent 50.0%", ), - (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (0, 1): bar_grad(" #d65f5f 25.0%", " transparent 25.0%"), + (1, 0): bar_grad(), (1, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", @@ -390,6 +409,7 @@ def test_bar_align_zero_nans(self): " #d65f5f 50.0%", " transparent 50.0%", ), + (1, 0): bar_grad(), (1, 1): bar_grad( " transparent 50.0%", " #d65f5f 50.0%", @@ -401,6 +421,66 @@ def test_bar_align_zero_nans(self): def test_bar_bad_align_raises(self): df = DataFrame({"A": [-100, -60, -30, -20]}) - msg = "`align` must be one of {'left', 'zero',' mid'}" + msg = "`align` should be in {'left', 'right', 'mid', 'mean', 'zero'} or" with pytest.raises(ValueError, match=msg): - df.style.bar(align="poorly", color=["#d65f5f", "#5fba7d"]) + df.style.bar(align="poorly", color=["#d65f5f", "#5fba7d"]).render() + + +@pytest.mark.parametrize( + "align, exp", + [ + ("left", [no_bar(), bar_to(50), bar_to(100)]), + ("right", [bar_to(100), bar_from_to(50, 100), no_bar()]), + ("mid", [bar_to(33.33), bar_to(66.66), bar_to(100)]), + ("zero", [bar_from_to(50, 66.7), bar_from_to(50, 83.3), bar_from_to(50, 100)]), + ("mean", [bar_to(50), no_bar(), bar_from_to(50, 100)]), + (2.0, [bar_to(50), no_bar(), bar_from_to(50, 100)]), + (np.median, [bar_to(50), no_bar(), bar_from_to(50, 100)]), + ], +) +def test_bar_align_positive_cases(align, exp): + # test different align cases for all positive values + data = DataFrame([[1], [2], [3]]) + result = data.style.bar(align=align)._compute().ctx + expected = {(0, 0): exp[0], (1, 0): exp[1], (2, 0): exp[2]} + assert result == expected + + +@pytest.mark.parametrize( + "align, exp", + [ + ("left", [bar_to(100), bar_to(50), no_bar()]), + ("right", [no_bar(), bar_from_to(50, 100), bar_to(100)]), + ("mid", [bar_from_to(66.66, 100), bar_from_to(33.33, 100), bar_to(100)]), + ("zero", [bar_from_to(33.33, 50), bar_from_to(16.66, 50), bar_to(50)]), + ("mean", [bar_from_to(50, 100), no_bar(), bar_to(50)]), + (-2.0, [bar_from_to(50, 100), no_bar(), bar_to(50)]), + (np.median, [bar_from_to(50, 100), no_bar(), bar_to(50)]), + ], +) +def test_bar_align_negative_cases(align, exp): + # test different align cases for all negative values + data = DataFrame([[-1], [-2], [-3]]) + result = data.style.bar(align=align)._compute().ctx + expected = {(0, 0): exp[0], (1, 0): exp[1], (2, 0): exp[2]} + assert result == expected + + +@pytest.mark.parametrize( + "align, exp", + [ + ("left", [no_bar(), bar_to(80), bar_to(100)]), + ("right", [bar_to(100), bar_from_to(80, 100), no_bar()]), + ("mid", [bar_to(60), bar_from_to(60, 80), bar_from_to(60, 100)]), + ("zero", [bar_to(50), bar_from_to(50, 66.66), bar_from_to(50, 83.33)]), + ("mean", [bar_to(50), bar_from_to(50, 66.66), bar_from_to(50, 83.33)]), + (-0.0, [bar_to(50), bar_from_to(50, 66.66), bar_from_to(50, 83.33)]), + (np.median, [bar_to(50), no_bar(), bar_from_to(50, 62.5)]), + ], +) +def test_bar_align_mixed_cases(align, exp): + # test different align cases for mixed positive and negative values + data = DataFrame([[-3], [1], [2]]) + result = data.style.bar(align=align)._compute().ctx + expected = {(0, 0): exp[0], (1, 0): exp[1], (2, 0): exp[2]} + assert result == expected
AlignAll NegativeAll PositiveBoth Neg and PosAll PositiveLarge Positive
{}{}{}