Skip to content

Commit 8fab56a

Browse files
jmarrecjreback
authored andcommitted
ENH: Added more options for formats.style.bar
Author: Julien Marrec <[email protected]> Closes pandas-dev#14757 from jmarrec/style-bar and squashes the following commits: dc3cbe8 [Julien Marrec] Added a whatsnew note af6c9bd [Julien Marrec] Added a simple example before the parametric one 80a3ce0 [Julien Marrec] Check for bad align value and raise. Wrote test for it too 5875eb9 [Julien Marrec] Change docstrings for color and align 5a22ee1 [Julien Marrec] Merge commit '673fb8f828952a4907e5659c1fcf83b771db7280' into style-bar 0e74b4d [Julien Marrec] Fix versionadded 1b7ffa2 [Julien Marrec] Added documentation on the new df.style.bar options for align and Colors in the documentation. 46bee6d [Julien Marrec] Change the tests to match new float formats. 01c200c [Julien Marrec] Format flots to avoid issue with py2.7 / py3.5 compta. 7ac2443 [Julien Marrec] Changes according to @sinhrks: Raise ValueError instead of warnings when color isn’t a str or 2-tuple/list. Passing “base” from bar(). e3f714c [Julien Marrec] Added a check on color argument that will issue a warning. Not sure if need to raise TypeError or issue a UserWarning if a list with more than two elements is passed. f12faab [Julien Marrec] Fixed line too long `git diff upstream/master | flake8 --diff now passes` 7c89137 [Julien Marrec] ENH: Added more options for formats.style.bar 673fb8f [Julien Marrec] Fix versionadded 506f3d2 [Julien Marrec] Added documentation on the new df.style.bar options for align and Colors in the documentation. e0563d5 [Julien Marrec] Change the tests to match new float formats. d210938 [Julien Marrec] Format flots to avoid issue with py2.7 / py3.5 compta. b22f639 [Julien Marrec] Changes according to @sinhrks: Raise ValueError instead of warnings when color isn’t a str or 2-tuple/list. Passing “base” from bar(). 3046626 [Julien Marrec] Added a check on color argument that will issue a warning. Not sure if need to raise TypeError or issue a UserWarning if a list with more than two elements is passed. 524a9ab [Julien Marrec] Fixed line too long `git diff upstream/master | flake8 --diff now passes` d1eafbb [Julien Marrec] ENH: Added more options for formats.style.bar
1 parent 0a37067 commit 8fab56a

File tree

4 files changed

+362
-24
lines changed

4 files changed

+362
-24
lines changed

doc/source/html-styling.ipynb

+95-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"cells": [
33
{
44
"cell_type": "markdown",
5-
"metadata": {},
5+
"metadata": {
6+
"collapsed": true
7+
},
68
"source": [
79
"*New in version 0.17.1*\n",
810
"\n",
@@ -518,7 +520,7 @@
518520
"cell_type": "markdown",
519521
"metadata": {},
520522
"source": [
521-
"You can include \"bar charts\" in your DataFrame."
523+
"There's also `.highlight_min` and `.highlight_max`."
522524
]
523525
},
524526
{
@@ -529,14 +531,25 @@
529531
},
530532
"outputs": [],
531533
"source": [
532-
"df.style.bar(subset=['A', 'B'], color='#d65f5f')"
534+
"df.style.highlight_max(axis=0)"
535+
]
536+
},
537+
{
538+
"cell_type": "code",
539+
"execution_count": null,
540+
"metadata": {
541+
"collapsed": false
542+
},
543+
"outputs": [],
544+
"source": [
545+
"df.style.highlight_min(axis=0)"
533546
]
534547
},
535548
{
536549
"cell_type": "markdown",
537550
"metadata": {},
538551
"source": [
539-
"There's also `.highlight_min` and `.highlight_max`."
552+
"Use `Styler.set_properties` when the style doesn't actually depend on the values."
540553
]
541554
},
542555
{
@@ -547,7 +560,23 @@
547560
},
548561
"outputs": [],
549562
"source": [
550-
"df.style.highlight_max(axis=0)"
563+
"df.style.set_properties(**{'background-color': 'black',\n",
564+
" 'color': 'lawngreen',\n",
565+
" 'border-color': 'white'})"
566+
]
567+
},
568+
{
569+
"cell_type": "markdown",
570+
"metadata": {},
571+
"source": [
572+
"### Bar charts"
573+
]
574+
},
575+
{
576+
"cell_type": "markdown",
577+
"metadata": {},
578+
"source": [
579+
"You can include \"bar charts\" in your DataFrame."
551580
]
552581
},
553582
{
@@ -558,14 +587,16 @@
558587
},
559588
"outputs": [],
560589
"source": [
561-
"df.style.highlight_min(axis=0)"
590+
"df.style.bar(subset=['A', 'B'], color='#d65f5f')"
562591
]
563592
},
564593
{
565594
"cell_type": "markdown",
566595
"metadata": {},
567596
"source": [
568-
"Use `Styler.set_properties` when the style doesn't actually depend on the values."
597+
"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",
598+
"\n",
599+
"Here's how you can change the above with the new `align='mid'` option:"
569600
]
570601
},
571602
{
@@ -576,9 +607,62 @@
576607
},
577608
"outputs": [],
578609
"source": [
579-
"df.style.set_properties(**{'background-color': 'black',\n",
580-
" 'color': 'lawngreen',\n",
581-
" 'border-color': 'white'})"
610+
"df.style.bar(subset=['A', 'B'], align='mid', color=['#d65f5f', '#5fba7d'])"
611+
]
612+
},
613+
{
614+
"cell_type": "markdown",
615+
"metadata": {},
616+
"source": [
617+
"The following example aims to give a highlight of the behavior of the new align options:"
618+
]
619+
},
620+
{
621+
"cell_type": "code",
622+
"execution_count": null,
623+
"metadata": {
624+
"collapsed": false
625+
},
626+
"outputs": [],
627+
"source": [
628+
"import pandas as pd\n",
629+
"from IPython.display import HTML\n",
630+
"\n",
631+
"# Test series\n",
632+
"test1 = pd.Series([-100,-60,-30,-20], name='All Negative')\n",
633+
"test2 = pd.Series([10,20,50,100], name='All Positive')\n",
634+
"test3 = pd.Series([-10,-5,0,90], name='Both Pos and Neg')\n",
635+
"\n",
636+
"head = \"\"\"\n",
637+
"<table>\n",
638+
" <thead>\n",
639+
" <th>Align</th>\n",
640+
" <th>All Negative</th>\n",
641+
" <th>All Positive</th>\n",
642+
" <th>Both Neg and Pos</th>\n",
643+
" </thead>\n",
644+
" </tbody>\n",
645+
"\n",
646+
"\"\"\"\n",
647+
"\n",
648+
"aligns = ['left','zero','mid']\n",
649+
"for align in aligns:\n",
650+
" row = \"<tr><th>{}</th>\".format(align)\n",
651+
" for serie in [test1,test2,test3]:\n",
652+
" s = serie.copy()\n",
653+
" s.name=''\n",
654+
" row += \"<td>{}</td>\".format(s.to_frame().style.bar(align=align, \n",
655+
" color=['#d65f5f', '#5fba7d'], \n",
656+
" width=100).render()) #testn['width']\n",
657+
" row += '</tr>'\n",
658+
" head += row\n",
659+
" \n",
660+
"head+= \"\"\"\n",
661+
"</tbody>\n",
662+
"</table>\"\"\"\n",
663+
" \n",
664+
"\n",
665+
"HTML(head)"
582666
]
583667
},
584668
{
@@ -961,7 +1045,7 @@
9611045
"name": "python",
9621046
"nbconvert_exporter": "python",
9631047
"pygments_lexer": "ipython3",
964-
"version": "3.5.1"
1048+
"version": "3.5.2"
9651049
}
9661050
},
9671051
"nbformat": 4,

doc/source/whatsnew/v0.20.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ Other Enhancements
366366
- ``pandas.io.json.json_normalize()`` with an empty ``list`` will return an empty ``DataFrame`` (:issue:`15534`)
367367
- ``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`)
368368

369+
- ``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`)
370+
369371

370372
.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
371373

pandas/formats/style.py

+154-11
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"or `pip install Jinja2`"
1818
raise ImportError(msg)
1919

20-
from pandas.types.common import is_float, is_string_like
20+
from pandas.types.common import is_float, is_string_like, is_list_like
2121

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

859859
@staticmethod
860-
def _bar(s, color, width):
861-
normed = width * (s - s.min()) / (s.max() - s.min())
860+
def _bar_left(s, color, width, base):
861+
"""
862+
The minimum value is aligned at the left of the cell
862863
863-
base = 'width: 10em; height: 80%;'
864-
attrs = (base + 'background: linear-gradient(90deg,{c} {w}%, '
864+
Parameters
865+
----------
866+
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
867+
width: float
868+
A number between 0 or 100. The largest value will cover ``width``
869+
percent of the cell's width
870+
base: str
871+
The base css format of the cell, e.g.:
872+
``base = 'width: 10em; height: 80%;'``
873+
874+
Returns
875+
-------
876+
self : Styler
877+
"""
878+
normed = width * (s - s.min()) / (s.max() - s.min())
879+
zero_normed = width * (0 - s.min()) / (s.max() - s.min())
880+
attrs = (base + 'background: linear-gradient(90deg,{c} {w:.1f}%, '
865881
'transparent 0%)')
866-
return [attrs.format(c=color, w=x) if x != 0 else base for x in normed]
867882

868-
def bar(self, subset=None, axis=0, color='#d65f5f', width=100):
883+
return [base if x == 0 else attrs.format(c=color[0], w=x)
884+
if x < zero_normed
885+
else attrs.format(c=color[1], w=x) if x >= zero_normed
886+
else base for x in normed]
887+
888+
@staticmethod
889+
def _bar_center_zero(s, color, width, base):
890+
"""
891+
Creates a bar chart where the zero is centered in the cell
892+
893+
Parameters
894+
----------
895+
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
896+
width: float
897+
A number between 0 or 100. The largest value will cover ``width``
898+
percent of the cell's width
899+
base: str
900+
The base css format of the cell, e.g.:
901+
``base = 'width: 10em; height: 80%;'``
902+
903+
Returns
904+
-------
905+
self : Styler
906+
"""
907+
908+
# Either the min or the max should reach the edge
909+
# (50%, centered on zero)
910+
m = max(abs(s.min()), abs(s.max()))
911+
912+
normed = s * 50 * width / (100.0 * m)
913+
914+
attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
915+
', transparent {w:.1f}%, {c} {w:.1f}%, '
916+
'{c} 50%, transparent 50%)')
917+
918+
attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
919+
', transparent 50%, {c} 50%, {c} {w:.1f}%, '
920+
'transparent {w:.1f}%)')
921+
922+
return [attrs_pos.format(c=color[1], w=(50 + x)) if x >= 0
923+
else attrs_neg.format(c=color[0], w=(50 + x))
924+
for x in normed]
925+
926+
@staticmethod
927+
def _bar_center_mid(s, color, width, base):
928+
"""
929+
Creates a bar chart where the midpoint is centered in the cell
930+
931+
Parameters
932+
----------
933+
color: 2-tuple/list, of [``color_negative``, ``color_positive``]
934+
width: float
935+
A number between 0 or 100. The largest value will cover ``width``
936+
percent of the cell's width
937+
base: str
938+
The base css format of the cell, e.g.:
939+
``base = 'width: 10em; height: 80%;'``
940+
941+
Returns
942+
-------
943+
self : Styler
944+
"""
945+
946+
if s.min() >= 0:
947+
# In this case, we place the zero at the left, and the max() should
948+
# be at width
949+
zero = 0.0
950+
slope = width / s.max()
951+
elif s.max() <= 0:
952+
# In this case, we place the zero at the right, and the min()
953+
# should be at 100-width
954+
zero = 100.0
955+
slope = width / -s.min()
956+
else:
957+
slope = width / (s.max() - s.min())
958+
zero = (100.0 + width) / 2.0 - slope * s.max()
959+
960+
normed = zero + slope * s
961+
962+
attrs_neg = (base + 'background: linear-gradient(90deg, transparent 0%'
963+
', transparent {w:.1f}%, {c} {w:.1f}%, '
964+
'{c} {zero:.1f}%, transparent {zero:.1f}%)')
965+
966+
attrs_pos = (base + 'background: linear-gradient(90deg, transparent 0%'
967+
', transparent {zero:.1f}%, {c} {zero:.1f}%, '
968+
'{c} {w:.1f}%, transparent {w:.1f}%)')
969+
970+
return [attrs_pos.format(c=color[1], zero=zero, w=x) if x > zero
971+
else attrs_neg.format(c=color[0], zero=zero, w=x)
972+
for x in normed]
973+
974+
def bar(self, subset=None, align='left', axis=0,
975+
color='#d65f5f', width=100):
869976
"""
870977
Color the background ``color`` proptional to the values in each column.
871978
Excludes non-numeric data by default.
872-
873979
.. versionadded:: 0.17.1
874980
875981
Parameters
876982
----------
877983
subset: IndexSlice, default None
878984
a valid slice for ``data`` to limit the style application to
879985
axis: int
880-
color: str
986+
color: str or 2-tuple/list
987+
If a str is passed, the color is the same for both
988+
negative and positive numbers. If 2-tuple/list is used, the
989+
first element is the color_negative and the second is the
990+
color_positive (eg: ['#d65f5f', '#5fba7d'])
881991
width: float
882992
A number between 0 or 100. The largest value will cover ``width``
883993
percent of the cell's width
994+
align : {'left', 'zero',' mid'}
995+
996+
.. versionadded:: 0.20.0
997+
998+
- 'left' : the min value starts at the left of the cell
999+
- 'zero' : a value of zero is located at the center of the cell
1000+
- 'mid' : the center of the cell is at (max-min)/2, or
1001+
if values are all negative (positive) the zero is aligned
1002+
at the right (left) of the cell
8841003
8851004
Returns
8861005
-------
8871006
self : Styler
8881007
"""
8891008
subset = _maybe_numeric_slice(self.data, subset)
8901009
subset = _non_reducing_slice(subset)
891-
self.apply(self._bar, subset=subset, axis=axis, color=color,
892-
width=width)
1010+
1011+
base = 'width: 10em; height: 80%;'
1012+
1013+
if not(is_list_like(color)):
1014+
color = [color, color]
1015+
elif len(color) == 1:
1016+
color = [color[0], color[0]]
1017+
elif len(color) > 2:
1018+
msg = ("Must pass `color` as string or a list-like"
1019+
" of length 2: [`color_negative`, `color_positive`]\n"
1020+
"(eg: color=['#d65f5f', '#5fba7d'])")
1021+
raise ValueError(msg)
1022+
1023+
if align == 'left':
1024+
self.apply(self._bar_left, subset=subset, axis=axis, color=color,
1025+
width=width, base=base)
1026+
elif align == 'zero':
1027+
self.apply(self._bar_center_zero, subset=subset, axis=axis,
1028+
color=color, width=width, base=base)
1029+
elif align == 'mid':
1030+
self.apply(self._bar_center_mid, subset=subset, axis=axis,
1031+
color=color, width=width, base=base)
1032+
else:
1033+
msg = ("`align` must be one of {'left', 'zero',' mid'}")
1034+
raise ValueError(msg)
1035+
8931036
return self
8941037

8951038
def highlight_max(self, subset=None, color='yellow', axis=0):

0 commit comments

Comments
 (0)