Skip to content

Commit 9f1aaed

Browse files
committed
ENH: Fixed PercentFormatter usage with latex
Added an example to the big formatter example Added test for latex correctness Modified existing pylab example
1 parent 1f999f4 commit 9f1aaed

File tree

4 files changed

+102
-62
lines changed

4 files changed

+102
-62
lines changed
Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
import matplotlib
22
from numpy.random import randn
33
import matplotlib.pyplot as plt
4-
from matplotlib.ticker import FuncFormatter
4+
from matplotlib.ticker import PercentFormatter
55

66

7-
def to_percent(y, position):
8-
# Ignore the passed in position. This has the effect of scaling the default
9-
# tick locations.
10-
s = str(100 * y)
11-
12-
# The percent symbol needs escaping in latex
13-
if matplotlib.rcParams['text.usetex'] is True:
14-
return s + r'$\%$'
15-
else:
16-
return s + '%'
17-
187
x = randn(5000)
198

20-
# Make a normed histogram. It'll be multiplied by 100 later.
21-
plt.hist(x, bins=50, normed=True)
9+
# Create a figure with some axes. This makes it easier to set the
10+
# formatter later, since that is only available through the OO API.
11+
fig, ax = plt.subplots()
2212

23-
# Create the formatter using the function to_percent. This multiplies all the
24-
# default labels by 100, making them all percentages
25-
formatter = FuncFormatter(to_percent)
13+
# Make a normed histogram. It'll be multiplied by 100 later.
14+
plt.hist(x, bins=50, normed=True, axes=ax)
2615

27-
# Set the formatter
28-
plt.gca().yaxis.set_major_formatter(formatter)
16+
# Set the formatter. `xmax` sets the value that maps to 100%.
17+
ax.yaxis.set_major_formatter(PercentFormatter(xmax=1))
2918

3019
plt.show()

examples/ticks_and_spines/tick-formatters.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ def setup(ax):
2121
ax.patch.set_alpha(0.0)
2222

2323

24-
plt.figure(figsize=(8, 5))
25-
n = 6
24+
plt.figure(figsize=(8, 6))
25+
n = 7
2626

2727
# Null formatter
2828
ax = plt.subplot(n, 1, 1)
@@ -87,6 +87,15 @@ def major_formatter(x, pos):
8787
ax.text(0.0, 0.1, "StrMethodFormatter('{x}')",
8888
fontsize=15, transform=ax.transAxes)
8989

90+
# Percent formatter
91+
ax = plt.subplot(n, 1, 7)
92+
setup(ax)
93+
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
94+
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
95+
ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5))
96+
ax.text(0.0, 0.1, "PercentFormatter(xmax=5)",
97+
fontsize=15, transform=ax.transAxes)
98+
9099
# Push the top of the top axes outside the figure because we only show the
91100
# bottom spine.
92101
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)

lib/matplotlib/tests/test_ticker.py

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -510,27 +510,53 @@ def test_basic(self):
510510
tmp_form = mticker.FormatStrFormatter('%05d')
511511
assert '00002' == tmp_form(2)
512512

513-
# test str.format() style formatter
514-
tmp_form = mticker.StrMethodFormatter('{x:05d}')
515-
assert '00002' == tmp_form(2)
513+
class TestStrMethodFormatter(object):
514+
test_data = [
515+
('{x:05d}', (2,), '00002'),
516+
('{x:03d}-{pos:02d}', (2, 1), '002-01'),
517+
]
518+
519+
@pytest.mark.parametrize('format, input, expected', test_data)
520+
def test_basic(self, format, input, expected):
521+
fmt = mticker.StrMethodFormatter(format)
522+
assert fmt(*input) == expected
523+
524+
525+
class TestEngFormatter(object):
526+
format_data = [
527+
('', 0.1, u'100 m'),
528+
('', 1, u'1'),
529+
('', 999.9, u'999.9'),
530+
('', 1001, u'1.001 k'),
531+
(u's', 0.1, u'100 ms'),
532+
(u's', 1, u'1 s'),
533+
(u's', 999.9, u'999.9 s'),
534+
(u's', 1001, u'1.001 ks'),
535+
]
516536

517-
# test str.format() style formatter with `pos`
518-
tmp_form = mticker.StrMethodFormatter('{x:03d}-{pos:02d}')
519-
assert '002-01' == tmp_form(2, 1)
537+
@pytest.mark.parametrize('unit, input, expected', format_data)
538+
def test_formatting(self, unit, input, expected):
539+
"""
540+
Test the formatting of EngFormatter with some inputs, against
541+
instances with and without units. Cases focus on when no SI
542+
prefix is present, for values in [1, 1000).
543+
"""
544+
fmt = mticker.EngFormatter(unit)
545+
assert fmt(input) == expected
520546

521547

522548
class TestPercentFormatter(object):
523-
percent_data = [
549+
basic_data = [
524550
# Check explicitly set decimals over different intervals and values
525551
(100, 0, '%', 120, 100, '120%'),
526552
(100, 0, '%', 100, 90, '100%'),
527553
(100, 0, '%', 90, 50, '90%'),
528-
(100, 0, '%', 1.7, 40, '2%'),
554+
(100, 0, '%', -1.7, 40, '-2%'),
529555
(100, 1, '%', 90.0, 100, '90.0%'),
530556
(100, 1, '%', 80.1, 90, '80.1%'),
531557
(100, 1, '%', 70.23, 50, '70.2%'),
532558
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
533-
(100, 1, '%', 60.554, 40, '60.6%'),
559+
(100, 1, '%', -60.554, 40, '-60.6%'),
534560
# Check auto decimals over different intervals and values
535561
(100, None, '%', 95, 1, '95.00%'),
536562
(1.0, None, '%', 3, 6, '300%'),
@@ -543,7 +569,7 @@ class TestPercentFormatter(object):
543569
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
544570
]
545571

546-
percent_ids = [
572+
basic_ids = [
547573
# Check explicitly set decimals over different intervals and values
548574
'decimals=0, x>100%',
549575
'decimals=0, x=100%',
@@ -565,33 +591,24 @@ class TestPercentFormatter(object):
565591
'Custom percent symbol',
566592
]
567593

594+
latex_data = [
595+
(False, False, r'50\{t}%'),
596+
(False, True, r'50\\\{t\}\%'),
597+
(True, False, r'50\{t}%'),
598+
(True, True, r'50\{t}%'),
599+
]
600+
568601
@pytest.mark.parametrize(
569602
'xmax, decimals, symbol, x, display_range, expected',
570603
percent_data, ids=percent_ids)
571-
def test_percentformatter(self, xmax, decimals, symbol,
604+
def test_basic(self, xmax, decimals, symbol,
572605
x, display_range, expected):
573606
formatter = mticker.PercentFormatter(xmax, decimals, symbol)
574-
assert formatter.format_pct(x, display_range) == expected
575-
576-
577-
class TestEngFormatter(object):
578-
format_data = [
579-
('', 0.1, u'100 m'),
580-
('', 1, u'1'),
581-
('', 999.9, u'999.9'),
582-
('', 1001, u'1.001 k'),
583-
(u's', 0.1, u'100 ms'),
584-
(u's', 1, u'1 s'),
585-
(u's', 999.9, u'999.9 s'),
586-
(u's', 1001, u'1.001 ks'),
587-
]
588-
589-
@pytest.mark.parametrize('unit, input, expected', format_data)
590-
def test_formatting(self, unit, input, expected):
591-
"""
592-
Test the formatting of EngFormatter with some inputs, against
593-
instances with and without units. Cases focus on when no SI
594-
prefix is present, for values in [1, 1000).
595-
"""
596-
fmt = mticker.EngFormatter(unit)
597-
assert fmt(input) == expected
607+
with matplotlib.rc_context(rc={'text.usetex': False}):
608+
assert formatter.format_pct(x, display_range) == expected
609+
610+
@pytest.mark.parametrize('is_latex, usetex, expected', latex_data)
611+
def test_latex(self, is_latex, usetex, expected):
612+
fmt = mticker.PercentFormatter(symbol='\\{t}%', is_latex=is_latex)
613+
with matplotlib.rc_context(rc={'text.usetex': usetex}):
614+
assert formatter.format_pct(50, 100) == expected

lib/matplotlib/ticker.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,16 +1285,19 @@ class PercentFormatter(Formatter):
12851285
situation is where `xmax` is 1.0.
12861286
12871287
`symbol` is a string which will be appended to the label. It may be
1288-
`None` or empty to indicate that no symbol should be used.
1288+
`None` or empty to indicate that no symbol should be used. LaTeX
1289+
special characters are escaped in `symbol` whenever latex mode is
1290+
enabled, unless `is_latex` is `True`.
12891291
12901292
`decimals` is the number of decimal places to place after the point.
12911293
If it is set to `None` (the default), the number will be computed
12921294
automatically.
12931295
"""
1294-
def __init__(self, xmax=100, decimals=None, symbol='%'):
1296+
def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False):
12951297
self.xmax = xmax + 0.0
12961298
self.decimals = decimals
1297-
self.symbol = symbol
1299+
self._symbol = symbol
1300+
self._is_latex = is_latex
12981301

12991302
def __call__(self, x, pos=None):
13001303
"""
@@ -1350,13 +1353,35 @@ def format_pct(self, x, display_range):
13501353
decimals = self.decimals
13511354
s = '{x:0.{decimals}f}'.format(x=x, decimals=int(decimals))
13521355

1353-
if self.symbol:
1354-
return s + self.symbol
1355-
return s
1356+
return s + self.symbol
13561357

13571358
def convert_to_pct(self, x):
13581359
return 100.0 * (x / self.xmax)
13591360

1361+
@property
1362+
def symbol(self):
1363+
"""
1364+
The configured percent symbol as a string.
1365+
1366+
If LaTeX is enabled via ``rcParams['text.usetex']``, the special
1367+
characters `{'#', '$', '%', '&', '~', '_', '^', '\\', '{', '}'}`
1368+
are automatically escaped in the string.
1369+
"""
1370+
symbol = self._symbol
1371+
if not symbol:
1372+
symbol = ''
1373+
elif rcParams['text.usetex'] and not self._is_latex:
1374+
# Source: http://www.personal.ceu.hu/tex/specchar.htm
1375+
# Backslash must be first for this to work correctly since
1376+
# it keeps getting added in
1377+
for spec in r'\#$%&~_^{}':
1378+
symbol = symbol.replace(spec, '\\' + spec)
1379+
return symbol
1380+
1381+
@symbol.setter
1382+
def symbol(self):
1383+
self._symbol = symbol
1384+
13601385

13611386
class Locator(TickHelper):
13621387
"""

0 commit comments

Comments
 (0)