Skip to content

(rebased) ENH: Added more options for formats.style.bar #15900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 95 additions & 11 deletions doc/source/html-styling.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"metadata": {
"collapsed": true
},
"source": [
"*New in version 0.17.1*\n",
"\n",
Expand Down Expand Up @@ -518,7 +520,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"You can include \"bar charts\" in your DataFrame."
"There's also `.highlight_min` and `.highlight_max`."
]
},
{
Expand All @@ -529,14 +531,25 @@
},
"outputs": [],
"source": [
"df.style.bar(subset=['A', 'B'], color='#d65f5f')"
"df.style.highlight_max(axis=0)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"df.style.highlight_min(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."
]
},
{
Expand All @@ -547,7 +560,23 @@
},
"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."
]
},
{
Expand All @@ -558,14 +587,16 @@
},
"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:"
]
},
{
Expand All @@ -576,9 +607,62 @@
},
"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": {
"collapsed": false
},
"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",
"<table>\n",
" <thead>\n",
" <th>Align</th>\n",
" <th>All Negative</th>\n",
" <th>All Positive</th>\n",
" <th>Both Neg and Pos</th>\n",
" </thead>\n",
" </tbody>\n",
"\n",
"\"\"\"\n",
"\n",
"aligns = ['left','zero','mid']\n",
"for align in aligns:\n",
" row = \"<tr><th>{}</th>\".format(align)\n",
" for serie in [test1,test2,test3]:\n",
" s = serie.copy()\n",
" s.name=''\n",
" row += \"<td>{}</td>\".format(s.to_frame().style.bar(align=align, \n",
" color=['#d65f5f', '#5fba7d'], \n",
" width=100).render()) #testn['width']\n",
" row += '</tr>'\n",
" head += row\n",
" \n",
"head+= \"\"\"\n",
"</tbody>\n",
"</table>\"\"\"\n",
" \n",
"\n",
"HTML(head)"
]
},
{
Expand Down Expand Up @@ -961,7 +1045,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
"version": "3.5.2"
}
},
"nbformat": 4,
Expand Down
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.20.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ Other Enhancements
- ``pandas.io.json.json_normalize()`` with an empty ``list`` will return an empty ``DataFrame`` (:issue:`15534`)
- ``pandas.io.json.json_normalize()`` has gained a ``sep`` option that accepts ``str`` to separate joined fields; the default is ".", which is backward compatible. (:issue:`14883`)

- ``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

Expand Down
165 changes: 154 additions & 11 deletions pandas/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"or `pip install Jinja2`"
raise ImportError(msg)

from pandas.types.common import is_float, is_string_like
from pandas.types.common import is_float, is_string_like, is_list_like

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -857,39 +857,182 @@ def set_properties(self, subset=None, **kwargs):
return self.applymap(f, subset=subset)

@staticmethod
def _bar(s, color, width):
normed = width * (s - s.min()) / (s.max() - s.min())
def _bar_left(s, color, width, base):
"""
The minimum value is aligned at the left of the cell

base = 'width: 10em; height: 80%;'
attrs = (base + 'background: linear-gradient(90deg,{c} {w}%, '
Parameters
----------
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
width: float
A number between 0 or 100. The largest value will cover ``width``
percent of the cell's width
base: str
The base css format of the cell, e.g.:
``base = 'width: 10em; height: 80%;'``

Returns
-------
self : Styler
"""
normed = width * (s - s.min()) / (s.max() - s.min())
zero_normed = width * (0 - s.min()) / (s.max() - s.min())
attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, '
'transparent 0%)')
return [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, align='left', axis=0,
color='#d65f5f', width=100):
"""
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'}

.. versionadded:: 0.20.0

- '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

Returns
-------
self : Styler
"""
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):
Expand Down
Loading