Skip to content

Commit

Permalink
ENH: Added percent formatter and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
madphysicist committed Mar 31, 2016
1 parent 5d0ca1d commit 94a8859
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 11 deletions.
6 changes: 6 additions & 0 deletions doc/users/whats_new/percent_formatter.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added `matplotlib.ticker.PercentFormatter`
------------------------------------------

The new formatter has some nice features like being able to convert from
arbitrary data scales to percents, a customizable percent symbol and
either automatic or manual control over the decimal points.
33 changes: 33 additions & 0 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,39 @@ def test_formatstrformatter():
tmp_form = mticker.StrMethodFormatter('{x:05d}')
nose.tools.assert_equal('00002', tmp_form(2))


def _percent_format_helper(mx, decimals, symbol, x, d, expected):
form = mticker.PercentFormatter(mx, decimals, symbol)
nose.tools.assert_equal(form.format_pct(x, d), expected)


def test_percentformatter():
test_cases = (
# Check explicitly set decimals over different intervals and values
(100, 0, '%', 120, 100, '120%'),
(100, 0, '%', 100, 90, '100%'),
(100, 0, '%', 90, 50, '90%'),
(100, 0, '%', 1.7, 40, '2%'),
(100, 1, '%', 90.0, 100, '90.0%'),
(100, 1, '%', 80.1, 90, '80.1%'),
(100, 1, '%', 70.23, 50, '70.2%'),
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
(100, 1, '%', 60.554, 40, '60.6%'),
# Check auto decimals over different intervals and values
(100, None, '%', 95, 1, '95.00%'),
(1.0, None, '%', 3, 6, '300%'),
(17.0, None, '%', 1, 8.5, '6%'),
(17.0, None, '%', 1, 8.4, '5.9%'),
(5, None, '%', -100, 0.000001, '-2000.00000%'),
# Check percent symbol
(1.0, 2, None, 1.2, 100, '120.00'),
(75, 3, '', 50, 100, '66.667'),
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
)
for mx, decimals, symbol, x, d, expected in test_cases:
yield _percent_format_helper, mx, decimals, symbol, x, d, expected


if __name__ == '__main__':
import nose
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
110 changes: 99 additions & 11 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@
:class:`LogFormatter`
formatter for log axes
:class:`PercentFormatter`
Format labels as a percentage
You can derive your own formatter from the Formatter base class by
simply overriding the ``__call__`` method. The formatter class has access
Expand Down Expand Up @@ -165,6 +167,18 @@

import warnings


__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
'LogFormatterExponent', 'LogFormatterMathtext',
'LogitFormatter', 'EngFormatter', 'PercentFormatter',
'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator',
'LinearLocator', 'LogLocator', 'AutoLocator',
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
'SymmetricalLogLocator')


if six.PY3:
long = int

Expand Down Expand Up @@ -922,7 +936,8 @@ def __call__(self, x, pos=None):
return self.fix_minus(s)

def format_eng(self, num):
""" Formats a number in engineering notation, appending a letter
"""
Formats a number in engineering notation, appending a letter
representing the power of 1000 of the original number. Some examples:
>>> format_eng(0) # for self.places = 0
Expand Down Expand Up @@ -973,6 +988,89 @@ def format_eng(self, num):
return formatted.strip()


class PercentFormatter(Formatter):
"""
Format numbers as a percentage.
How the number is converted into a percentage is determined by the
`max` parameter. `max` is the data value that corresponds to 100%.
Percentages are computed as ``x / max * 100``. So if the data is
already scaled to be percentages, `max` will be 100. Another common
situation is where `max` is 1.0.
"""
def __init__(self, max=100, decimals=None, symbol='%'):
"""
Initializes the formatter.
`max` is the data value that corresponds to 100%. `symbol` is
a string which will be appended to the label. It may be `None`
or empty to indicate that no symbol should be used. `decimals`
is the number of decimal places to place after the point. If
it is set to `None` (the default), the number will be computed
automatically.
"""
self.max = max + 0.0
self.decimals = decimals
self.symbol = symbol

def __call__(self, x, pos=None):
"""
Formats the tick as a percentage with the appropriate scaling.
"""
xmin, xmax = self.axis.get_view_interval()
d = abs(xmax - xmin)

return self.fix_minus(format_pct(x, d))

def format_pct(self, x, d):
"""
Formats the number as a percentage number with the correct
number of decimals and adds the percent symbol, if any.
If `self.decimals` is `None`, the number of digits after the
decimal point is set based on the width of the domain `d` as
follows:
+-------+----------+------------------------+
| d | decimals | sample |
+-------+----------+------------------------+
+ >50 | 0 | ``x = 34.5`` => 34% |
+-------+----------+------------------------+
| >5 | 1 | ``x = 34.5`` => 34.5% |
+-------+----------+------------------------+
| >0.5 | 2 | ``x = 34.5`` => 34.50% |
+-------+----------+------------------------+
| ... | ... | ... |
+-------+----------+------------------------+
This method will not be very good for tiny ranges or extremely
large ranges. It assumes that the values on the chart are
percentages displayed on a reasonable scale.
"""
x = self.convert_to_pct(x)
if self.decimals is None:
# Luckily Python's built-in `ceil` rounds to +inf, not away
# from zero. This is very important since the equation for
# `decimals` starts out as `d > 0.5 * 10**(2 - decimals)`
# and ends up with `decimals > 2 - log10(2 * d)`.
d = self.convert_to_pct(d) # d is a difference, so this works
decimals = math.ceil(2.0 - math.log10(2.0 * d))
if decimals > 5:
decimals = 5
elif decimals < 0:
decimals = 0
else:
decimals = self.decimals
s = '{x:0.{decimals}f}'.format(x=x, decimals=int(decimals))

if self.symbol:
return s + self.symbol
return s

def convert_to_pct(self, x):
return 100.0 * (x / self.max)


class Locator(TickHelper):
"""
Determine the tick locations;
Expand Down Expand Up @@ -2055,13 +2153,3 @@ def get_locator(self, d):
locator = MultipleLocator(ticksize)

return locator


__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
'LogFormatterExponent', 'LogFormatterMathtext', 'Locator',
'IndexLocator', 'FixedLocator', 'NullLocator',
'LinearLocator', 'LogLocator', 'AutoLocator',
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
'SymmetricalLogLocator')

0 comments on commit 94a8859

Please sign in to comment.