8000 ENH: Added percent formatter and tests · matplotlib/matplotlib@06d6f41 · GitHub
[go: up one dir, main page]

Skip to content

Commit 06d6f41

Browse files
committed
ENH: Added percent formatter and tests
1 parent 5d0ca1d commit 06d6f41

File tree

3 files changed

+134
-9
lines changed

3 files changed

+134
-9
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Added `matplotlib.ticker.PercentFormatter`
2+
------------------------------------------
3+
4+
The new formatter has some nice features like being able to convert from
5+
arbitrary data scales to percents, a customizable percent symbol and
6+
either automatic or manual control over the decimal points.

lib/matplotlib/tests/test_ticker.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,39 @@ def test_formatstrformatter():
366366
tmp_form = mticker.StrMethodFormatter('{x:05d}')
367367
nose.tools.assert_equal('00002', tmp_form(2))
368368

369+
370+
def _percent_format_helper(mx, decimals, symbol, x, d, expected):
371+
form = mticker.PercentFormatter(mx, decimals, symbol)
372+
nose.tools.assert_equal(form.format_pct(x, d), expected)
373+
374+
375+
def test_percentformatter():
376+
test_cases = (
377+
# Check explicitly set decimals over different intervals and values
378+
(100 10000 , 0, '%', 120, 100, '120%'),
379+
(100, 0, '%', 100, 90, '100%'),
380+
(100, 0, '%', 90, 50, '90%'),
381+
(100, 0, '%', 1.7, 40, '2%'),
382+
(100, 1, '%', 90.0, 100, '90.0%'),
383+
(100, 1, '%', 80.1, 90, '80.1%'),
384+
(100, 1, '%', 70.23, 50, '70.2%'),
385+
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
386+
(100, 1, '%', 60.554, 40, '60.6%'),
387+
# Check auto decimals over different intervals and values
388+
(100, None, '%', 95, 1, '95.00%'),
389+
(1.0, None, '%', 3, 6, '300%'),
390+
(17.0, None, '%', 1, 8.5, '6%'),
391+
(17.0, None, '%', 1, 8.4, '5.9%'),
392+
(5, None, '%', -100, 0.000001, '-2000.00000%'),
393+
# Check percent symbol
394+
(1.0, 2, None, 1.2, 100, '120.00'),
395+
(75, 3, '', 50, 100, '66.667'),
396+
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
397+
)
398+
for mx, decimals, symbol, x, d, expected in test_cases:
399+
yield _percent_format_helper, mx, decimals, symbol, x, d, expected
400+
401+
369402
if __name__ == '__main__':
370403
import nose
371404
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)

lib/matplotlib/ticker.py

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@
131131
:class:`LogFormatter`
132132
formatter for log axes
133133
134+
:class:`PercentFormatter`
135+
Format labels as a percentage
134136
135137
You can derive your own formatter from the Formatter base class by
136138
simply overriding the ``__call__`` method. The formatter class has access
@@ -152,6 +154,18 @@
152154
from __future__ import (absolute_import, division, print_function,
153155
unicode_literals)
154156

157+
158+
__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
159+
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
160+
'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
161+
'LogFormatterExponent', 'LogFormatterMathtext',
162+
'LogitFormatter', 'EngFormatter', 'PercentFormatter',
163+
'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator',
164+
'LinearLocator', 'LogLocator', 'AutoLocator',
165+
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
166+
'SymmetricalLogLocator')
167+
168+
155169
from matplotlib.externals import six
156170

157171
import decimal
@@ -973,6 +987,87 @@ def format_eng(self, num):
973987
return formatted.strip()
974988

975989

990+
class PercentFormatter(Formatter):
991+
"""
992+
Format numbers as a percentage.
993+
994+
How the number is converted into a percentage is determined by the
995+
`max` parameter. `max` is the data value that corresponds to 100%.
996+
Percentages are computed as ``x / max * 100``. So if the data is
997+
already scaled to be percentages, `max` will be 100. Another common
998+
situation is where `max` is 1.0.
999+
"""
1000+
def __init__(self, max=100, decimals=None, symbol='%'):
1001+
"""
1002+
Initializes the formatter.
1003+
1004+
`max` is the data value that corresponds to 100%. `symbol` is
1005+
a string which will be appended to the label. It may be `None`
1006+
or empty to indicate that no symbol should be used. `decimals`
1007+
is the number of decimal places to place after the
1008+
"""
1009+
self.max = max + 0.0
1010+
self.decimals = decimals
1011+
self.symbol = symbol
1012+
1013+
def __call__(self, x, pos=None):
1014+
"""
1015+
Formats the tick as a percentage with the appropriate scaling.
1016+
"""
1017+
xmin, xmax = self.axis.get_view_interval()
1018+
d = abs(xmax - xmin)
1019+
1020+
return self.fix_minus(format_pct(x, d))
1021+
1022+
def format_pct(self, x, d):
1023+
"""
1024+
Formats the number as a percentage number with the correct
1025+
number of decimals and adds the percent symbol, if any.
1026+
1027+
If `self.decimals` is `None`, the number of digits after the
1028+
decimal point is set based on the width of the domain `d` as
1029+
follows:
1030+
1031+
+-------+----------+------------------------+
1032+
| d | decimals | sample |
1033+
+-------+----------+------------------------+
1034+
+ >50 | 0 | ``x = 34.5`` => 34% |
1035+
+-------+----------+------------------------+
1036+
| >5 | 1 | ``x = 34.5`` => 34.5% |
1037+
+-------+----------+------------------------+
1038+
| >0.5 | 2 | ``x = 34.5`` => 34.50% |
1039+
+-------+----------+------------------------+
1040+
| ... | ... | ... |
1041+
+-------+----------+------------------------+
1042+
1043+
This method will not be very good for tiny ranges or extremely
1044+
large ranges. It assumes that the values on the chart are
1045+
percentages displayed on a reasonable scale.
1046+
"""
1047+
x = self.convert_to_pct(x)
1048+
if self.decimals is None:
1049+
# Luckily Python's built-in `ceil` rounds to +inf, not away
1050+
# from zero. This is very important since the equation for
1051+
# `decimals` starts out as `d > 0.5 * 10**(2 - decimals)`
1052+
# and ends up with `decimals > 2 - log10(2 * d)`.
1053+
d = self.convert_to_pct(d) # d is a difference, so this works fine
1054+
decimals = math.ceil(2.0 - math.log10(2.0 * d))
1055+
if decimals > 5:
1056+
decimals = 5
1057+
elif decimals < 0:
1058+
decimals = 0
1059+
else:
1060+
decimals = self.decimals
1061+
s = '{x:0.{decimals}f}'.format(x=x, decimals=int(decimals))
1062+
1063+
if self.symbol:
1064+
return s + self.symbol
1065+
return s
1066+
1067+
def convert_to_pct(self, x):
1068+
return 100.0 * (x / self.max)
1069+
1070+
9761071
class Locator(TickHelper):
9771072
"""
9781073
Determine the tick locations;
@@ -2056,12 +2151,3 @@ def get_locator(self, d):
20562151

20572152
return locator
20582153

2059-
2060-
__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
2061-
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
2062-
'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
2063-
'LogFormatterExponent', 'LogFormatterMathtext', 'Locator',
2064-
'IndexLocator', 'FixedLocator', 'NullLocator',
2065-
'LinearLocator', 'LogLocator', 'AutoLocator',
2066-
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
2067-
'SymmetricalLogLocator')

0 commit comments

Comments
 (0)
0