diff --git a/doc/source/style.ipynb b/doc/source/style.ipynb
index 2cacbb19d81bb..427b18b988aef 100644
--- a/doc/source/style.ipynb
+++ b/doc/source/style.ipynb
@@ -2,7 +2,9 @@
"cells": [
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "collapsed": true
+ },
"source": [
"# Styling\n",
"\n",
@@ -87,9 +89,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style"
@@ -107,9 +107,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_null().render().split('\\n')[:10]"
@@ -160,9 +158,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"s = df.style.applymap(color_negative_red)\n",
@@ -208,9 +204,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max)"
@@ -234,9 +228,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.\\\n",
@@ -290,9 +282,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max, color='darkorange', axis=None)"
@@ -340,9 +330,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max, subset=['B', 'C', 'D'])"
@@ -358,9 +346,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.applymap(color_negative_red,\n",
@@ -393,9 +379,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format(\"{:.2%}\")"
@@ -411,9 +395,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format({'B': \"{:0<4.0f}\", 'D': '{:+.2f}'})"
@@ -429,9 +411,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.format({\"B\": lambda x: \"±{:.2f}\".format(abs(x))})"
@@ -454,9 +434,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.highlight_null(null_color='red')"
@@ -472,9 +450,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"import seaborn as sns\n",
@@ -495,9 +471,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"# Uses the full color range\n",
@@ -507,9 +481,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"# Compress the color range\n",
@@ -523,67 +495,128 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "You can include \"bar charts\" in your DataFrame."
+ "There's also `.highlight_min` and `.highlight_max`."
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
- "df.style.bar(subset=['A', 'B'], color='#d65f5f')"
+ "df.style.highlight_max(axis=0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "There's also `.highlight_min` and `.highlight_max`."
+ "Use `Styler.set_properties` when the style doesn't actually depend on the values."
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
- "df.style.highlight_max(axis=0)"
+ "df.style.set_properties(**{'background-color': 'black',\n",
+ " 'color': 'lawngreen',\n",
+ " 'border-color': 'white'})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Bar charts"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can include \"bar charts\" in your DataFrame."
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
- "df.style.highlight_min(axis=0)"
+ "df.style.bar(subset=['A', 'B'], color='#d65f5f')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Use `Styler.set_properties` when the style doesn't actually depend on the values."
+ "New in version 0.20.0 is the ability to customize further the bar chart: 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",
+ "\n",
+ "Here's how you can change the above with the new `align='mid'` option:"
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
- "df.style.set_properties(**{'background-color': 'black',\n",
- " 'color': 'lawngreen',\n",
- " 'border-color': 'white'})"
+ "df.style.bar(subset=['A', 'B'], align='mid', color=['#d65f5f', '#5fba7d'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The following example aims to give a highlight of the behavior of the new align options:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "from IPython.display import HTML\n",
+ "\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",
+ "\n",
+ "head = \"\"\"\n",
+ "
\n",
+ " \n",
+ " Align | \n",
+ " All Negative | \n",
+ " All Positive | \n",
+ " Both Neg and Pos | \n",
+ " \n",
+ " \n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "aligns = ['left','zero','mid']\n",
+ "for align in aligns:\n",
+ " row = \"{} | \".format(align)\n",
+ " for serie in [test1,test2,test3]:\n",
+ " s = serie.copy()\n",
+ " s.name=''\n",
+ " row += \"{} | \".format(s.to_frame().style.bar(align=align, \n",
+ " color=['#d65f5f', '#5fba7d'], \n",
+ " width=100).render()) #testn['width']\n",
+ " row += '
'\n",
+ " head += row\n",
+ " \n",
+ "head+= \"\"\"\n",
+ "\n",
+ "
\"\"\"\n",
+ " \n",
+ "\n",
+ "HTML(head)"
]
},
{
@@ -603,9 +636,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df2 = -df\n",
@@ -616,9 +647,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"style2 = df2.style\n",
@@ -671,9 +700,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"with pd.option_context('display.precision', 2):\n",
@@ -693,9 +720,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style\\\n",
@@ -728,9 +753,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"df.style.set_caption('Colormaps, with a caption.')\\\n",
@@ -756,9 +779,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"from IPython.display import HTML\n",
@@ -854,9 +875,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"from IPython.html import widgets\n",
@@ -892,9 +911,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"np.random.seed(25)\n",
@@ -993,9 +1010,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"%mkdir templates"
@@ -1012,9 +1027,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"%%file templates/myhtml.tpl\n",
@@ -1065,9 +1078,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"MyStyler(df)"
@@ -1083,9 +1094,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"HTML(MyStyler(df).render(table_title=\"Extending Example\"))"
@@ -1101,9 +1110,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"EasyStyler = Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n",
@@ -1120,9 +1127,7 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"outputs": [],
"source": [
"with open(\"template_structure.html\") as f:\n",
diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt
index 6e4756c3c5245..4a95e580af9fd 100644
--- a/doc/source/whatsnew/v0.20.0.txt
+++ b/doc/source/whatsnew/v0.20.0.txt
@@ -524,6 +524,8 @@ Other Enhancements
- ``parallel_coordinates()`` has gained a ``sort_labels`` keyword arg that sorts class labels and the colours assigned to them (:issue:`15908`)
- Options added to allow one to turn on/off using ``bottleneck`` and ``numexpr``, see :ref:`here ` (:issue:`16157`)
+- ``DataFrame.style.bar()`` now accepts two more options to further customize the bar chart. Bar alignment is set with ``align='left'|'mid'|'zero'``, the default is "left", which is backward compatible; You can now pass a list of ``color=[color_negative, color_positive]``. (:issue:`14757`)
+
.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index 83062e7d764cd..f1ff2966dca48 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -23,6 +23,7 @@
import numpy as np
import pandas as pd
+from pandas.api.types import is_list_like
from pandas.compat import range
from pandas.core.config import get_option
from pandas.core.generic import _shared_docs
@@ -868,30 +869,141 @@ def set_properties(self, subset=None, **kwargs):
return self.applymap(f, subset=subset)
@staticmethod
- def _bar(s, color, width):
+ def _bar_left(s, color, width, base):
+ """
+ The minimum value is aligned at the left of the cell
+ Parameters
+ ----------
+ color: 2-tuple/list, of [``color_negative``, ``color_positive``]
+ width: float
+ A number between 0 or 100. The largest value will cover ``width``
+ percent of the cell's width
+ base: str
+ The base css format of the cell, e.g.:
+ ``base = 'width: 10em; height: 80%;'``
+ Returns
+ -------
+ self : Styler
+ """
normed = width * (s - s.min()) / (s.max() - s.min())
-
- base = 'width: 10em; height: 80%;'
- attrs = (base + 'background: linear-gradient(90deg,{c} {w}%, '
+ zero_normed = width * (0 - s.min()) / (s.max() - s.min())
+ attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, '
'transparent 0%)')
- return [attrs.format(c=color, w=x) if x != 0 else base for x in normed]
- def bar(self, subset=None, axis=0, color='#d65f5f', width=100):
+ return [base if x == 0 else attrs.format(c=color[0], w=x)
+ if x < zero_normed
+ else attrs.format(c=color[1], w=x) if x >= zero_normed
+ else base for x in normed]
+
+ @staticmethod
+ def _bar_center_zero(s, color, width, base):
+ """
+ Creates a bar chart where the zero is centered in the cell
+ Parameters
+ ----------
+ color: 2-tuple/list, of [``color_negative``, ``color_positive``]
+ width: float
+ A number between 0 or 100. The largest value will cover ``width``
+ percent of the cell's width
+ base: str
+ The base css format of the cell, e.g.:
+ ``base = 'width: 10em; height: 80%;'``
+ Returns
+ -------
+ self : Styler
+ """
+
+ # Either the min or the max should reach the edge
+ # (50%, centered on zero)
+ m = max(abs(s.min()), abs(s.max()))
+
+ normed = s * 50 * width / (100.0 * m)
+
+ attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
+ ', transparent {w:.1f}%, {c} {w:.1f}%, '
+ '{c} 50%, transparent 50%)')
+
+ attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
+ ', transparent 50%, {c} 50%, {c} {w:.1f}%, '
+ 'transparent {w:.1f}%)')
+
+ return [attrs_pos.format(c=color[1], w=(50 + x)) if x >= 0
+ else attrs_neg.format(c=color[0], w=(50 + x))
+ for x in normed]
+
+ @staticmethod
+ def _bar_center_mid(s, color, width, base):
+ """
+ Creates a bar chart where the midpoint is centered in the cell
+ Parameters
+ ----------
+ color: 2-tuple/list, of [``color_negative``, ``color_positive``]
+ width: float
+ A number between 0 or 100. The largest value will cover ``width``
+ percent of the cell's width
+ base: str
+ The base css format of the cell, e.g.:
+ ``base = 'width: 10em; height: 80%;'``
+ Returns
+ -------
+ self : Styler
+ """
+
+ if s.min() >= 0:
+ # In this case, we place the zero at the left, and the max() should
+ # be at width
+ zero = 0.0
+ slope = width / s.max()
+ elif s.max() <= 0:
+ # In this case, we place the zero at the right, and the min()
+ # should be at 100-width
+ zero = 100.0
+ slope = width / -s.min()
+ else:
+ slope = width / (s.max() - s.min())
+ zero = (100.0 + width) / 2.0 - slope * s.max()
+
+ normed = zero + slope * s
+
+ attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
+ ', transparent {w:.1f}%, {c} {w:.1f}%, '
+ '{c} {zero:.1f}%, transparent {zero:.1f}%)')
+
+ attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
+ ', transparent {zero:.1f}%, {c} {zero:.1f}%, '
+ '{c} {w:.1f}%, transparent {w:.1f}%)')
+
+ return [attrs_pos.format(c=color[1], zero=zero, w=x) if x > zero
+ else attrs_neg.format(c=color[0], zero=zero, w=x)
+ for x in normed]
+
+ def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
+ align='left'):
"""
Color the background ``color`` proptional to the values in each column.
Excludes non-numeric data by default.
-
.. versionadded:: 0.17.1
-
Parameters
----------
subset: IndexSlice, default None
a valid slice for ``data`` to limit the style application to
axis: int
- color: str
+ color: str or 2-tuple/list
+ If a str is passed, the color is the same for both
+ negative and positive numbers. If 2-tuple/list is used, the
+ first element is the color_negative and the second is the
+ color_positive (eg: ['#d65f5f', '#5fba7d'])
width: float
A number between 0 or 100. The largest value will cover ``width``
percent of the cell's width
+ align : {'left', 'zero',' mid'}, default 'left'
+ - 'left' : the min value starts at the left of the cell
+ - 'zero' : a value of zero is located at the center of the cell
+ - '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
+
+ .. versionadded:: 0.20.0
Returns
-------
@@ -899,8 +1011,32 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100):
"""
subset = _maybe_numeric_slice(self.data, subset)
subset = _non_reducing_slice(subset)
- self.apply(self._bar, subset=subset, axis=axis, color=color,
- width=width)
+
+ base = 'width: 10em; height: 80%;'
+
+ if not(is_list_like(color)):
+ color = [color, color]
+ elif len(color) == 1:
+ color = [color[0], color[0]]
+ elif len(color) > 2:
+ msg = ("Must pass `color` as string or a list-like"
+ " of length 2: [`color_negative`, `color_positive`]\n"
+ "(eg: color=['#d65f5f', '#5fba7d'])")
+ raise ValueError(msg)
+
+ if align == 'left':
+ self.apply(self._bar_left, subset=subset, axis=axis, color=color,
+ width=width, base=base)
+ elif align == 'zero':
+ self.apply(self._bar_center_zero, subset=subset, axis=axis,
+ color=color, width=width, base=base)
+ elif align == 'mid':
+ self.apply(self._bar_center_mid, subset=subset, axis=axis,
+ color=color, width=width, base=base)
+ else:
+ msg = ("`align` must be one of {'left', 'zero',' mid'}")
+ raise ValueError(msg)
+
return self
def highlight_max(self, subset=None, color='yellow', axis=0):
diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py
index f421c0f8e6d69..9219ac1c9c26b 100644
--- a/pandas/tests/io/formats/test_style.py
+++ b/pandas/tests/io/formats/test_style.py
@@ -266,7 +266,7 @@ def test_empty(self):
{'props': [['', '']], 'selector': 'row1_col0'}]
assert result == expected
- def test_bar(self):
+ def test_bar_align_left(self):
df = pd.DataFrame({'A': [0, 1, 2]})
result = df.style.bar()._compute().ctx
expected = {
@@ -299,7 +299,7 @@ def test_bar(self):
result = df.style.bar(color='red', width=50)._compute().ctx
assert result == expected
- def test_bar_0points(self):
+ def test_bar_align_left_0points(self):
df = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
result = df.style.bar()._compute().ctx
expected = {(0, 0): ['width: 10em', ' height: 80%'],
@@ -349,6 +349,118 @@ def test_bar_0points(self):
', transparent 0%)']}
assert result == expected
+ def test_bar_align_mid_pos_and_neg(self):
+ df = pd.DataFrame({'A': [-10, 0, 20, 90]})
+
+ result = df.style.bar(align='mid', color=[
+ '#d65f5f', '#5fba7d'])._compute().ctx
+
+ expected = {(0, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, #d65f5f 0.0%, '
+ '#d65f5f 10.0%, transparent 10.0%)'],
+ (1, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 10.0%, '
+ '#d65f5f 10.0%, #d65f5f 10.0%, '
+ 'transparent 10.0%)'],
+ (2, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 10.0%, #5fba7d 10.0%'
+ ', #5fba7d 30.0%, transparent 30.0%)'],
+ (3, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 10.0%, '
+ '#5fba7d 10.0%, #5fba7d 100.0%, '
+ 'transparent 100.0%)']}
+
+ self.assertEqual(result, expected)
+
+ def test_bar_align_mid_all_pos(self):
+ df = pd.DataFrame({'A': [10, 20, 50, 100]})
+
+ result = df.style.bar(align='mid', color=[
+ '#d65f5f', '#5fba7d'])._compute().ctx
+
+ expected = {(0, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, '
+ '#5fba7d 10.0%, transparent 10.0%)'],
+ (1, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, '
+ '#5fba7d 20.0%, transparent 20.0%)'],
+ (2, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, '
+ '#5fba7d 50.0%, transparent 50.0%)'],
+ (3, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, #5fba7d 0.0%, '
+ '#5fba7d 100.0%, transparent 100.0%)']}
+
+ self.assertEqual(result, expected)
+
+ def test_bar_align_mid_all_neg(self):
+ df = pd.DataFrame({'A': [-100, -60, -30, -20]})
+
+ result = df.style.bar(align='mid', color=[
+ '#d65f5f', '#5fba7d'])._compute().ctx
+
+ expected = {(0, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 0.0%, '
+ '#d65f5f 0.0%, #d65f5f 100.0%, '
+ 'transparent 100.0%)'],
+ (1, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 40.0%, '
+ '#d65f5f 40.0%, #d65f5f 100.0%, '
+ 'transparent 100.0%)'],
+ (2, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 70.0%, '
+ '#d65f5f 70.0%, #d65f5f 100.0%, '
+ 'transparent 100.0%)'],
+ (3, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 80.0%, '
+ '#d65f5f 80.0%, #d65f5f 100.0%, '
+ 'transparent 100.0%)']}
+ assert result == expected
+
+ def test_bar_align_zero_pos_and_neg(self):
+ # See https://github.com/pandas-dev/pandas/pull/14757
+ df = pd.DataFrame({'A': [-10, 0, 20, 90]})
+
+ result = df.style.bar(align='zero', color=[
+ '#d65f5f', '#5fba7d'], width=90)._compute().ctx
+
+ expected = {(0, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 45.0%, '
+ '#d65f5f 45.0%, #d65f5f 50%, '
+ 'transparent 50%)'],
+ (1, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 50%, '
+ '#5fba7d 50%, #5fba7d 50.0%, '
+ 'transparent 50.0%)'],
+ (2, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 50%, #5fba7d 50%, '
+ '#5fba7d 60.0%, transparent 60.0%)'],
+ (3, 0): ['width: 10em', ' height: 80%',
+ 'background: linear-gradient(90deg, '
+ 'transparent 0%, transparent 50%, #5fba7d 50%, '
+ '#5fba7d 95.0%, transparent 95.0%)']}
+ assert result == expected
+
+ def test_bar_bad_align_raises(self):
+ df = pd.DataFrame({'A': [-100, -60, -30, -20]})
+ with pytest.raises(ValueError):
+ df.style.bar(align='poorly', color=['#d65f5f', '#5fba7d'])
+
def test_highlight_null(self, null_color='red'):
df = pd.DataFrame({'A': [0, np.nan]})
result = df.style.highlight_null()._compute().ctx