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",
" Align | \n",
" All Negative | \n",
- " All Positive | \n",
" Both Neg and Pos | \n",
+ " All Positive | \n",
+ " Large Positive | \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