Skip to content

style.bar: add support for axis=None (tablewise application instead of rowwise or columnwise) #21548

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

Merged
merged 4 commits into from
Aug 30, 2018
Merged
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
5 changes: 3 additions & 2 deletions doc/source/whatsnew/v0.24.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -725,9 +725,10 @@ Build Changes
Other
^^^^^

- :meth: `~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`)
- :meth:`~pandas.io.formats.style.Styler.background_gradient` now takes a ``text_color_threshold`` parameter to automatically lighten the text color based on the luminance of the background color. This improves readability with dark background colors without the need to limit the background colormap range. (:issue:`21258`)
- Require at least 0.28.2 version of ``cython`` to support read-only memoryviews (:issue:`21688`)
- :meth: `~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`)
- :meth:`~pandas.io.formats.style.Styler.background_gradient` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` (:issue:`15204`)
- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly.
-
-
-
232 changes: 92 additions & 140 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import pandas.core.common as com
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
from pandas.util._decorators import Appender
from pandas.core.dtypes.generic import ABCSeries

try:
import matplotlib.pyplot as plt
from matplotlib import colors
Expand Down Expand Up @@ -993,174 +995,124 @@ def set_properties(self, subset=None, **kwargs):
return self.applymap(f, subset=subset)

@staticmethod
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())
zero_normed = width * (0 - s.min()) / (s.max() - s.min())
attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, '
'transparent 0%)')

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]
def _bar(s, align, colors, width=100, vmin=None, vmax=None):
"""Draw bar chart in dataframe cells"""

# Get input value range.
smin = s.min() if vmin is None else vmin
if isinstance(smin, ABCSeries):
smin = smin.min()
smax = s.max() if vmax is None else vmax
if isinstance(smax, ABCSeries):
smax = smax.max()
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.values - smin) / (smax - smin + 1e-12)
zero = -width * smin / (smax - smin + 1e-12)

def css_bar(start, end, color):
"""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 += ' transparent {s:.1f}%, {c} {s:.1f}%, '.format(
s=start, c=color
)
css += '{c} {e:.1f}%, transparent {e:.1f}%)'.format(
e=min(end, width), c=color,
)
return css

@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
"""
def css(x):
if pd.isna(x):
return ''
if align == 'left':
return css_bar(0, x, colors[x > zero])
else:
return css_bar(min(x, zero), max(x, zero), colors[x > zero])

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()
if s.ndim == 1:
return [css(x) for x in normed]
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]
return pd.DataFrame(
[[css(x) for x in row] for row in normed],
index=s.index, columns=s.columns
)

def bar(self, subset=None, axis=0, color='#d65f5f', width=100,
align='left'):
align='left', vmin=None, vmax=None):
"""
Color the background ``color`` proportional to the values in each
column.
Excludes non-numeric data by default.
Draw bar chart in the cell backgrounds.

Parameters
----------
subset: IndexSlice, default None
a valid slice for ``data`` to limit the style application to
axis: int
color: str or 2-tuple/list
subset : IndexSlice, optional
A valid slice for `data` to limit the style application to.
axis : int, str or None, default 0
Apply to each column (`axis=0` or `'index'`)
or to each row (`axis=1` or `'columns'`) or
to the entire DataFrame at once with `axis=None`.
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
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'
- 'left' : the min value starts at the left of the cell
- 'zero' : a value of zero is located at the center of the cell
How to align the bars with the cells.
- '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
at the right (left) of the cell.

.. versionadded:: 0.20.0

vmin : float, optional
Minimum bar value, defining the left hand limit
of the bar drawing range, lower values are clipped to `vmin`.
When None (default): the minimum value of the data will be used.

.. versionadded:: 0.24.0

vmax : float, optional
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.

.. versionadded:: 0.24.0


Returns
-------
self : Styler
"""
subset = _maybe_numeric_slice(self.data, subset)
subset = _non_reducing_slice(subset)
if align not in ('left', 'zero', 'mid'):
raise ValueError("`align` must be one of {'left', 'zero',' mid'}")

base = 'width: 10em; height: 80%;'

if not(is_list_like(color)):
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)
raise ValueError("`color` must be string or a list-like"
" of length 2: [`color_neg`, `color_pos`]"
" (eg: color=['#d65f5f', '#5fba7d'])")

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)
subset = _maybe_numeric_slice(self.data, subset)
subset = _non_reducing_slice(subset)
self.apply(self._bar, subset=subset, axis=axis,
align=align, colors=color, width=width,
vmin=vmin, vmax=vmax)

return self

Expand Down
Loading