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

Skip to content

Commit fbb8eb4

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

File tree

3 files changed

+142
-18
lines changed

3 files changed

+142
-18
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, 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: 103 additions & 18 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
@@ -165,6 +167,18 @@
165167

166168
import warnings
167169

170+
171+
__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
172+
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
173+
'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
174+
'LogFormatterExponent', 'LogFormatterMathtext',
175+
'LogitFormatter', 'EngFormatter', 'PercentFormatter',
176+
'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator',
177+
'LinearLocator', 'LogLocator', 'AutoLocator',
178+
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
179+
'SymmetricalLogLocator')
180+
181+
168182
if six.PY3:
169183
long = int
170184

@@ -922,8 +936,10 @@ def __call__(self, x, pos=None):
922936
return self.fix_minus(s)
923937

924938
def format_eng(self, num):
925-
""" Formats a number in engineering notation, appending a letter
926-
representing the power of 1000 of the original number. Some examples:
939+
"""
940+
Formats a number in engineering notation, appending a letter
941+
representing the power of 1000 of the original number.
942+
Some examples:
927943
928944
>>> format_eng(0) # for self.places = 0
929945
'0'
@@ -934,13 +950,9 @@ def format_eng(self, num):
934950
>>> format_eng("-1e-6") # for self.places = 2
935951
u'-1.00 \u03bc'
936952
937-
@param num: the value to represent
938-
@type num: either a numeric value or a string that can be converted to
939-
a numeric value (as per decimal.Decimal constructor)
940-
941-
@return: engineering formatted string
953+
`num` may be a numeric value or a string that can be converted
954+
to a numeric value with the `decimal.Decimal` constructor.
942955
"""
943-
9449 F438 56
dnum = decimal.Decimal(str(num))
945957

946958
sign = 1
@@ -973,6 +985,89 @@ def format_eng(self, num):
973985
return formatted.strip()
974986

975987

988+
class PercentFormatter(Formatter):
989+
"""
990+
Format numbers as a percentage.
991+
992+
How the number is converted into a percentage is determined by the
993+
`max` parameter. `max` is the data value that corresponds to 100%.
994+
Percentages are computed as ``x / max * 100``. So if the data is
995+
already scaled to be percentages, `max` will be 100. Another common
996+
situation is where `max` is 1.0.
997+
"""
998+
def __init__(self, max=100, decimals=None, symbol='%'):
999+
"""
1000+
Initializes the formatter.
1001+
1002+
`max` is the data value that corresponds to 100%. `symbol` is
1003+
a string which will be appended to the label. It may be `None`
1004+
or empty to indicate that no symbol should be used. `decimals`
1005+
is the number of decimal places to place after the point. If
1006+
it is set to `None` (the default), the number will be computed
1007+
automatically.
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
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;
@@ -2055,13 +2150,3 @@ def get_locator(self, d):
20552150
locator = MultipleLocator(ticksize)
20562151

20572152
return locator
2058-
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