From 0de9cd10e7c743be9f52669d80df4cc6b31bb8b0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 28 Jun 2021 22:11:43 +0200 Subject: [PATCH 01/11] change styler bar code --- pandas/io/formats/style.py | 199 +++++++++++++------- pandas/tests/io/formats/style/test_align.py | 6 +- 2 files changed, 137 insertions(+), 68 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 00c18ac261bab..b5e4921254179 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 = "left", vmin: float | None = None, vmax: float | None = None, + base_css: str = "width: 10em;", ) -> Styler: """ Draw bar chart in the cell backgrounds. @@ -2154,8 +2095,8 @@ def bar( ------- self : Styler """ - if align not in ("left", "zero", "mid"): - raise ValueError("`align` must be one of {'left', 'zero',' mid'}") + # 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] @@ -2172,14 +2113,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=base_css, ) return self @@ -2815,3 +2757,130 @@ 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, + vmax: float, + base_css: str, +): + """ + Draw bar chart in dataframe cells. + """ + + def css_bar(base_css: str, 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 + ---------- + base_css : str + Additional CSS applied to cell as well as linear gradient. + 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. + """ + if end > start: + base_css += "background: linear-gradient(90deg," + if start > 0: + base_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%, " + base_css += f"{color} {end*100:.1f}%, transparent {end*100:.1f}%)" + return base_css + + def css_calc(x, left: float, right: float, align: str, width: float): + """ + 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. + right : float + Value marking the right side of the calculation (left < right). + align : {"left", "right", "zero"} + How the bars will be positioned. Note if using "zero" with an adjustment + such as a mean, ensure ``left`` and ``right`` are also adjusted on input. + width : float + The proportionate width of the cell to take up in [0,1]. Measured according + to ``align``. + + Returns + ------- + str : Resultant CSS with linear gradient. + """ + if pd.isna(x): + return "" + + color = colors[0] if x < 0 else colors[1] + x = left if x < left else x + x = right if x > right else x + + if align == "left": + end = (x - left) / (right - left) + return css_bar(base_css, 0, end * width, color) + elif align == "right": + start = (x - left) / (right - left) + return css_bar(base_css, (1 - width) + start * width, 1, color) + elif align == "zero": + if right < 0: + right = -left # since abs(right) < abs(left) + elif left > 0: + left = -right # since abs(left) < abs(right) + elif abs(right) < abs(left): + right = -left # apparent + else: + left = -right + + if x < 0: + start = (x - left) / (right - left) + return css_bar(base_css, (1 - width) / 2 + start * width, 0.5, color) + else: + end = (x - 0) / (right - left) + return css_bar(base_css, 0.5, end * width + 0.5, 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 + + if align == "mid": + z, align = (left + right) / 2, "zero" + 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 align == "left" or align == "right" or align == "zero": + z = 0 + else: + 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" + ) + + if data.ndim == 1: + return [css_calc(x - z, left - z, right - z, align, width) for x in values] + else: + return DataFrame( + [ + [css_calc(x - z, left - z, right - z, align, width) 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..00ea05ea67506 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -7,7 +7,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 + [ @@ -401,6 +401,6 @@ 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() From 9addb866f80fea45948a1bae55b9a54c7051223a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 28 Jun 2021 23:56:30 +0200 Subject: [PATCH 02/11] width centralised --- pandas/io/formats/style.py | 51 +++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b5e4921254179..f4598861ac4d5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2812,7 +2812,7 @@ def css_calc(x, left: float, right: float, align: str, width: float): Value marking the left side of calculation. right : float Value marking the right side of the calculation (left < right). - align : {"left", "right", "zero"} + align : {"left", "right", "zero", "mid"} How the bars will be positioned. Note if using "zero" with an adjustment such as a mean, ensure ``left`` and ``right`` are also adjusted on input. width : float @@ -2828,37 +2828,60 @@ def css_calc(x, left: float, right: float, align: str, width: float): color = colors[0] if x < 0 else colors[1] x = left if x < left else x - x = right if x > right else x + x = right if x > right else x # trim data if outside of the window if align == "left": - end = (x - left) / (right - left) - return css_bar(base_css, 0, end * width, color) + # all proportions are measured from the left side between left and right + start, end = 0, (x - left) / (right - left) * width + elif align == "right": - start = (x - left) / (right - left) - return css_bar(base_css, (1 - width) + start * width, 1, color) + # all proportions are measured from the right side between left and right + start, end = (1 - width) + (x - left) / (right - left) * width, 1 + elif align == "zero": + # all proportions are measured from the center which is set to zero + # input data will have been translated if centering about a mean for example if right < 0: - right = -left # since abs(right) < abs(left) - elif left > 0: - left = -right # since abs(left) < abs(right) + right = -left # since abs(right) < abs(left): all data below zero + elif left >= 0: + left = -right # since abs(left) < abs(right): all data above zero elif abs(right) < abs(left): - right = -left # apparent + right = -left # standardise left and right as same distance from zero else: left = -right + if x < 0: + start, end = (1 - width) / 2 + width * (x - left) / (right - left), 0.5 + else: + start, end = 0.5, x / (right - left) * width + 0.5 + + elif align == "mid": + mid = (left + right) / 2 + if mid < 0: + zero_frac = (0 - mid) / (right - mid) + 0.5 + else: + zero_frac = (0 - left) / (right - left) + if x < 0: start = (x - left) / (right - left) - return css_bar(base_css, (1 - width) / 2 + start * width, 0.5, color) + end = zero_frac else: - end = (x - 0) / (right - left) - return css_bar(base_css, 0.5, end * width + 0.5, color) + start = zero_frac + end = (x - left) / (right - left) + + return css_bar(base_css, start, end, 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 if align == "mid": - z, align = (left + right) / 2, "zero" + if all(values >= 0): # "mid" is documented to act as "left" if all positive + z, align, left = 0, "left", 0 if vmin is None else vmin + elif all(values <= 0): # "mid" is documented to act as "right" if all negative + z, align, right = 0, "right", 0 if vmax is None else vmax + else: + z = 0 elif align == "mean": z, align = np.nanmean(values), "zero" elif callable(align): From 54a7813875cbe3deaa95e261371b4606c9a7f016 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 00:11:01 +0200 Subject: [PATCH 03/11] align with existing tests --- pandas/io/formats/style.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f4598861ac4d5..8f1928ca4b5f5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2832,11 +2832,11 @@ def css_calc(x, left: float, right: float, align: str, width: float): if align == "left": # all proportions are measured from the left side between left and right - start, end = 0, (x - left) / (right - left) * width + start, end = 0, (x - left) / (right - left) elif align == "right": # all proportions are measured from the right side between left and right - start, end = (1 - width) + (x - left) / (right - left) * width, 1 + start, end = (x - left) / (right - left), 1 elif align == "zero": # all proportions are measured from the center which is set to zero @@ -2851,14 +2851,14 @@ def css_calc(x, left: float, right: float, align: str, width: float): left = -right if x < 0: - start, end = (1 - width) / 2 + width * (x - left) / (right - left), 0.5 + start, end = (x - left) / (right - left), 0.5 else: - start, end = 0.5, x / (right - left) * width + 0.5 + start, end = 0.5, x / (right - left) + 0.5 elif align == "mid": mid = (left + right) / 2 if mid < 0: - zero_frac = (0 - mid) / (right - mid) + 0.5 + zero_frac = (0 - mid) / (right - left) + 0.5 else: zero_frac = (0 - left) / (right - left) @@ -2869,16 +2869,16 @@ def css_calc(x, left: float, right: float, align: str, width: float): start = zero_frac end = (x - left) / (right - left) - return css_bar(base_css, start, end, color) + return css_bar(base_css, 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 if align == "mid": - if all(values >= 0): # "mid" is documented to act as "left" if all positive + if np.all(values >= 0): # "mid" is documented to act as "left" if all positive z, align, left = 0, "left", 0 if vmin is None else vmin - elif all(values <= 0): # "mid" is documented to act as "right" if all negative + elif np.all(values <= 0): # "mid" is documented to act as "right" if all neg z, align, right = 0, "right", 0 if vmax is None else vmax else: z = 0 From 50eff0fa61b9c613e90fa04e3afafae66254047b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 00:18:18 +0200 Subject: [PATCH 04/11] fix formattimg and tests --- pandas/io/formats/style.py | 4 +- pandas/tests/io/formats/style/test_align.py | 58 ++++++++++----------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 8f1928ca4b5f5..cc2453a2c801f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2796,8 +2796,8 @@ def css_bar(base_css: str, start: float, end: float, color: str) -> str: if end > start: base_css += "background: linear-gradient(90deg," if start > 0: - base_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%, " - base_css += f"{color} {end*100:.1f}%, transparent {end*100:.1f}%)" + base_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%," + base_css += f" {color} {end*100:.1f}%, transparent {end*100:.1f}%)" return base_css def css_calc(x, left: float, right: float, align: str, width: float): diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index 00ea05ea67506..209370825444d 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -24,16 +24,16 @@ def test_bar_align_left(self): result = df.style.bar()._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 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 @@ -51,26 +51,26 @@ def test_bar_align_left_0points(self): (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 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 +79,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 +105,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 +131,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( @@ -193,15 +193,15 @@ def test_bar_align_left_axis_none(self): 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 +245,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 +295,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 +344,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 +364,7 @@ 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, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", From bcd4914dc8164f80471b8a2f0a1272db13578f9d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 01:07:31 +0200 Subject: [PATCH 05/11] documentation and amended tests --- doc/source/user_guide/style.ipynb | 22 ++++++----- pandas/io/formats/style.py | 42 +++++++++++++-------- pandas/tests/io/formats/style/test_align.py | 2 + 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 6e10e6ec74b48..4da473e762e92 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/pandas/io/formats/style.py b/pandas/io/formats/style.py index cc2453a2c801f..fc814a97e1900 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2051,7 +2051,7 @@ def bar( align: str | float | int | callable = "left", vmin: float | None = None, vmax: float | None = None, - base_css: str = "width: 10em;", + props: str = "width: 10em;", ) -> Styler: """ Draw bar chart in the cell backgrounds. @@ -2072,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 'left' + 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`. @@ -2090,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: 100px;"` + + .. 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: @@ -2121,7 +2133,7 @@ def bar( width=width / 100, vmin=vmin, vmax=vmax, - base_css=base_css, + base_css=props, ) return self @@ -2824,7 +2836,7 @@ def css_calc(x, left: float, right: float, align: str, width: float): str : Resultant CSS with linear gradient. """ if pd.isna(x): - return "" + return base_css color = colors[0] if x < 0 else colors[1] x = left if x < left else x diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index 209370825444d..7e74f0d0f304f 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -365,6 +365,7 @@ def test_bar_align_mid_nans(self): " transparent 50.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 +391,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%", From e711c7d223180faeeddd45645ac90080807b3189 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 08:39:20 +0200 Subject: [PATCH 06/11] documentation, refactor, and amended tests --- pandas/io/formats/style.py | 73 +++++++++++++-------- pandas/tests/io/formats/style/test_align.py | 4 +- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index fc814a97e1900..187231942b866 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2048,7 +2048,7 @@ def bar( axis: Axis | None = 0, color="#d65f5f", width: float = 100, - align: str | float | int | callable = "left", + align: str | float | int | callable = "mid", vmin: float | None = None, vmax: float | None = None, props: str = "width: 10em;", @@ -2074,7 +2074,7 @@ def bar( width : float, default 100 The percentage of the cell, measured from the left, in which to draw the bars, in [0, 100]. - align : str, int, float, callable, default 'left' + 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: @@ -2776,15 +2776,32 @@ def _bar( align: str | float | int | callable, colors: list[str], width: float, - vmin: float, - vmax: float, + vmin: float | None, + vmax: float | None, base_css: str, ): """ - Draw bar chart in dataframe cells. + 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(base_css: str, start: float, end: float, color: str) -> str: + 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. @@ -2792,8 +2809,6 @@ def css_bar(base_css: str, start: float, end: float, color: str) -> str: Parameters ---------- - base_css : str - Additional CSS applied to cell as well as linear gradient. start : float Relative positional start of bar coloring in [0,1] end : float @@ -2804,15 +2819,20 @@ def css_bar(base_css: str, start: float, end: float, color: str) -> str: Returns ------- str : The CSS applicable to the cell. + + Notes + ----- + Uses ``base_css`` from outer scope. """ + cell_css = base_css if end > start: - base_css += "background: linear-gradient(90deg," + cell_css += "background: linear-gradient(90deg," if start > 0: - base_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%," - base_css += f" {color} {end*100:.1f}%, transparent {end*100:.1f}%)" - return base_css + 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, width: float): + def css_calc(x, left: float, right: float, align: str): """ Return the correct CSS for bar placement based on calculated values. @@ -2827,13 +2847,14 @@ def css_calc(x, left: float, right: float, align: str, width: float): align : {"left", "right", "zero", "mid"} How the bars will be positioned. Note if using "zero" with an adjustment such as a mean, ensure ``left`` and ``right`` are also adjusted on input. - width : float - The proportionate width of the cell to take up in [0,1]. Measured according - to ``align``. Returns ------- str : Resultant CSS with linear gradient. + + Notes + ----- + Uses ``colors`` and ``width`` from outer scope. """ if pd.isna(x): return base_css @@ -2868,20 +2889,20 @@ def css_calc(x, left: float, right: float, align: str, width: float): start, end = 0.5, x / (right - left) + 0.5 elif align == "mid": + # bars are drawn from zero either leftwards or rightwards with center of + # cell placed at mid mid = (left + right) / 2 - if mid < 0: - zero_frac = (0 - mid) / (right - left) + 0.5 + if mid < 0: # get the proportionate location of zero where bars start/end + zero_frac = -mid / (right - left) + 0.5 else: - zero_frac = (0 - left) / (right - left) + zero_frac = -left / (right - left) if x < 0: - start = (x - left) / (right - left) - end = zero_frac + start, end = (x - left) / (right - left), zero_frac else: - start = zero_frac - end = (x - left) / (right - left) + start, end = zero_frac, (x - left) / (right - left) - return css_bar(base_css, start * width, end * width, color) + return css_bar(start * width, end * width, color) values = data.to_numpy() left = np.nanmin(values) if vmin is None else vmin @@ -2909,11 +2930,11 @@ def css_calc(x, left: float, right: float, align: str, width: float): ) if data.ndim == 1: - return [css_calc(x - z, left - z, right - z, align, width) for x in values] + 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, width) for x in row] + [css_calc(x - z, left - z, right - z, align) for x in row] for row in values ], index=data.index, diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index 7e74f0d0f304f..78fc8051af25c 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -46,7 +46,7 @@ def test_bar_align_left(self): 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(), @@ -60,7 +60,7 @@ def test_bar_align_left_0points(self): } 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%"), From debec3e192bae2a38064469e48efe8aee4fb5a1c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 08:40:52 +0200 Subject: [PATCH 07/11] tests explicit --- pandas/tests/io/formats/style/test_align.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index 78fc8051af25c..111e884c279c4 100644 --- a/pandas/tests/io/formats/style/test_align.py +++ b/pandas/tests/io/formats/style/test_align.py @@ -21,7 +21,7 @@ def bar_grad(a=None, b=None, c=None, d=None): 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%"), @@ -29,7 +29,7 @@ def test_bar_align_left(self): } 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%"), @@ -38,10 +38,10 @@ def test_bar_align_left(self): 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): @@ -189,7 +189,7 @@ 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( From 444b0e9469277097a9182df35d1518f63f213750 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 09:11:37 +0200 Subject: [PATCH 08/11] mypy fix and black --- pandas/io/formats/style.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 187231942b866..c6eb5119328d7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2048,7 +2048,7 @@ def bar( axis: Axis | None = 0, color="#d65f5f", width: float = 100, - align: str | float | int | callable = "mid", + align: str | float | int | Callable = "mid", vmin: float | None = None, vmax: float | None = None, props: str = "width: 10em;", @@ -2773,7 +2773,7 @@ def _highlight_between( def _bar( data: FrameOrSeries, - align: str | float | int | callable, + align: str | float | int | Callable, colors: list[str], width: float, vmin: float | None, @@ -2863,6 +2863,9 @@ def css_calc(x, left: float, right: float, align: str): 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 start, end = 0, (x - left) / (right - left) @@ -2891,44 +2894,41 @@ def css_calc(x, left: float, right: float, align: str): elif align == "mid": # bars are drawn from zero either leftwards or rightwards with center of # cell placed at mid - mid = (left + right) / 2 - if mid < 0: # get the proportionate location of zero where bars start/end - zero_frac = -mid / (right - left) + 0.5 - else: - zero_frac = -left / (right - left) + mid: float = (left + right) / 2 + z_frac: float = ( + -mid / (right - left) + 0.5 if mid < 0 else -left / (right - left) + ) if x < 0: - start, end = (x - left) / (right - left), zero_frac + start, end = (x - left) / (right - left), z_frac else: - start, end = zero_frac, (x - left) / (right - left) + 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 np.all(values >= 0): # "mid" is documented to act as "left" if all positive - z, align, left = 0, "left", 0 if vmin is None else vmin + align, left = "left", 0 if vmin is None else vmin elif np.all(values <= 0): # "mid" is documented to act as "right" if all neg - z, align, right = 0, "right", 0 if vmax is None else vmax - else: - z = 0 + 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 align == "left" or align == "right" or align == "zero": - z = 0 - else: + 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) if data.ndim == 1: return [css_calc(x - z, left - z, right - z, align) for x in values] else: From e677f067c338f9dbe2e46a8f75d6b342529cd49c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 14:24:40 +0200 Subject: [PATCH 09/11] add tests for new cases --- pandas/io/formats/style.py | 2 +- pandas/tests/io/formats/style/test_align.py | 78 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index c6eb5119328d7..aabafddd23d72 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2102,7 +2102,7 @@ def bar( 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: 100px;"` + `"width: 10em;"` .. versionadded:: 1.4.0 diff --git a/pandas/tests/io/formats/style/test_align.py b/pandas/tests/io/formats/style/test_align.py index 111e884c279c4..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 @@ -18,6 +19,23 @@ 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]}) @@ -406,3 +424,63 @@ def test_bar_bad_align_raises(self): 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"]).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 From 6970a038de525bc99311dccdd1f6f688e04e9250 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 29 Jun 2021 15:19:19 +0200 Subject: [PATCH 10/11] whats new --- doc/source/whatsnew/v1.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 81545ada63ce5..0968ee742185d 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -29,7 +29,7 @@ enhancement2 Other enhancements ^^^^^^^^^^^^^^^^^^ -- +- Additional options added to :meth:`.Styler.bar` to control alignment and display (:issue:`26070`) - .. --------------------------------------------------------------------------- From db18100b8148b5bd3f0b4543ac2df2f8b925d47c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 30 Jun 2021 11:50:28 +0200 Subject: [PATCH 11/11] simplify --- pandas/io/formats/style.py | 58 ++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index aabafddd23d72..445e46dbeb767 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2841,12 +2841,16 @@ def css_calc(x, left: float, right: float, align: str): x : float Value which determines the bar placement. left : float - Value marking the left side of calculation. + Value marking the left side of calculation, usually minimum of data. right : float - Value marking the right side of the calculation (left < right). + 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. Note if using "zero" with an adjustment - such as a mean, ensure ``left`` and ``right`` are also adjusted on input. + 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 ------- @@ -2868,36 +2872,24 @@ def css_calc(x, left: float, right: float, align: str): if align == "left": # all proportions are measured from the left side between left and right - start, end = 0, (x - left) / (right - left) + end = (x - left) / (right - left) elif align == "right": # all proportions are measured from the right side between left and right - start, end = (x - left) / (right - left), 1 - - elif align == "zero": - # all proportions are measured from the center which is set to zero - # input data will have been translated if centering about a mean for example - if right < 0: - right = -left # since abs(right) < abs(left): all data below zero - elif left >= 0: - left = -right # since abs(left) < abs(right): all data above zero - elif abs(right) < abs(left): - right = -left # standardise left and right as same distance from zero - else: - left = -right + start = (x - left) / (right - left) - if x < 0: - start, end = (x - left) / (right - left), 0.5 - else: - start, end = 0.5, x / (right - left) + 0.5 - - elif align == "mid": - # bars are drawn from zero either leftwards or rightwards with center of - # cell placed at mid - mid: float = (left + right) / 2 - z_frac: float = ( - -mid / (right - left) + 0.5 if mid < 0 else -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 @@ -2912,9 +2904,9 @@ def css_calc(x, left: float, right: float, align: str): z: float = 0 # adjustment to translate data if align == "mid": - if np.all(values >= 0): # "mid" is documented to act as "left" if all positive + 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 np.all(values <= 0): # "mid" is documented to act as "right" if all neg + 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" @@ -2928,7 +2920,7 @@ def css_calc(x, left: float, right: float, align: str): "value defining the center line or a callable that returns a float" ) - assert isinstance(align, str) + 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:
AlignAll NegativeAll PositiveBoth Neg and PosAll PositiveLarge Positive
{}{}{}