Skip to content

Commit

Permalink
ENH? Styler.bar support for axis=None
Browse files Browse the repository at this point in the history
* ENH: Styler.bar: add support for tablewise application with axis=None pandas-dev#21548

- eliminate code duplication related to style.bar with different align modes
- add support for axis=None
- fix minor bug with align 'zero' and width < 100
- make generated CSS gradients more compact

* ENH: Styler.bar: add support for vmin/vmax pandas-dev#21526

* ENH: Styler.bar: properly support NaNs

* ENH: Styler.bar: docstring improvements
  • Loading branch information
soxofaan authored and victor committed Sep 30, 2018
1 parent 5d1af2f commit 5c036fd
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 195 deletions.
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 @@ -736,9 +736,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

0 comments on commit 5c036fd

Please sign in to comment.