Skip to content

Commit c33266b

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 87885e7 commit c33266b

File tree

4 files changed

+107
-53
lines changed

4 files changed

+107
-53
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=(9, 5))
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: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -511,35 +511,66 @@ def test_formatstrformatter():
511511
assert '002-01' == tmp_form(2, 1)
512512

513513

514-
percentformatter_test_cases = (
515-
# Check explicitly set decimals over different intervals and values
516-
(100, 0, '%', 120, 100, '120%'),
517-
(100, 0, '%', 100, 90, '100%'),
518-
(100, 0, '%', 90, 50, '90%'),
519-
(100, 0, '%', 1.7, 40, '2%'),
520-
(100, 1, '%', 90.0, 100, '90.0%'),
521-
(100, 1, '%', 80.1, 90, '80.1%'),
522-
(100, 1, '%', 70.23, 50, '70.2%'),
523-
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
524-
(100, 1, '%', 60.554, 40, '60.6%'),
525-
# Check auto decimals over different intervals and values
526-
(100, None, '%', 95, 1, '95.00%'),
527-
(1.0, None, '%', 3, 6, '300%'),
528-
(17.0, None, '%', 1, 8.5, '6%'),
529-
(17.0, None, '%', 1, 8.4, '5.9%'),
530-
(5, None, '%', -100, 0.000001, '-2000.00000%'),
531-
# Check percent symbol
532-
(1.0, 2, None, 1.2, 100, '120.00'),
533-
(75, 3, '', 50, 100, '66.667'),
534-
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
535-
)
536-
537-
538514
@pytest.mark.parametrize('xmax, decimals, symbol, x, display_range, expected',
539-
percentformatter_test_cases)
515+
[
516+
# Check explicitly set decimals over different intervals and values
517+
(100, 0, '%', 120, 100, '120%'),
518+
(100, 0, '%', 100, 90, '100%'),
519+
(100, 0, '%', 90, 50, '90%'),
520+
(100, 0, '%', -1.7, 40, '-2%'),
521+
(100, 1, '%', 90.0, 100, '90.0%'),
522+
(100, 1, '%', 80.1, 90, '80.1%'),
523+
(100, 1, '%', 70.23, 50, '70.2%'),
524+
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
525+
(100, 1, '%', -60.554, 40, '-60.6%'),
526+
# Check auto decimals over different intervals and values
527+
(100, None, '%', 95, 1, '95.00%'),
528+
(1.0, None, '%', 3, 6, '300%'),
529+
(17.0, None, '%', 1, 8.5, '6%'),
530+
(17.0, None, '%', 1, 8.4, '5.9%'),
531+
(5, None, '%', -100, 0.000001, '-2000.00000%'),
532+
# Check percent symbol
533+
(1.0, 2, None, 1.2, 100, '120.00'),
534+
(75, 3, '', 50, 100, '66.667'),
535+
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
536+
], ids=[
537+
# Check explicitly set decimals over different intervals and values
538+
'decimals=0, x>100%',
539+
'decimals=0, x=100%',
540+
'decimals=0, x<100%',
541+
'decimals=0, x<0%',
542+
'decimals=1, x>100%',
543+
'decimals=1, x=100%',
544+
'decimals=1, x<100%',
545+
'decimals=1, x<0%',
546+
# Check auto decimals over different intervals and values
547+
'autodecimal, x<100%, display_range=1',
548+
'autodecimal, x>100%, display_range=6 (custom xmax test)',
549+
'autodecimal, x<100%, display_range=8.5 (autodecimal test 1)',
550+
'autodecimal, x<100%, display_range=8.4 (autodecimal test 2)',
551+
'autodecimal, x<-100%, display_range=1e-6 (tiny display range)',
552+
# Check percent symbol
553+
'None as percent symbol',
554+
'Empty percent symbol',
555+
'Custom percent symbol',
556+
])
540557
def test_percentformatter(xmax, decimals, symbol, x, display_range, expected):
541558
formatter = mticker.PercentFormatter(xmax, decimals, symbol)
542-
assert formatter.format_pct(x, display_range) == expected
559+
with matplotlib.rc_context(rc={'text.usetex': False}):
560+
assert formatter.format_pct(x, display_range) == expected
561+
562+
563+
@pytest.mark.parametrize('is_latex, usetex, expected',
564+
[
565+
(False, False, r'50\{t}%'),
566+
(False, True, r'50\\\{t\}\%'),
567+
(True, False, r'50\{t}%'),
568+
(True, True, r'50\{t}%'),
569+
])
570+
def test_percentformatter_latex(is_latex, usetex, expected):
571+
formatter = mticker.PercentFormatter(symbol='\\{t}%', is_latex=is_latex)
572+
with matplotlib.rc_context(rc={'text.usetex': usetex}):
573+
assert formatter.format_pct(50, 100) == expected
543574

544575

545576
def test_EngFormatter_formatting():

lib/matplotlib/ticker.py

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

13021305
def __call__(self, x, pos=None):
13031306
"""
@@ -1353,13 +1356,35 @@ def format_pct(self, x, display_range):
13531356
decimals = self.decimals
13541357
s = '{x:0.{decimals}f}'.format(x=x, decimals=int(decimals))
13551358

1356-
if self.symbol:
1357-
return s + self.symbol
1358-
return s
1359+
return s + self.symbol
13591360

13601361
def convert_to_pct(self, x):
13611362
return 100.0 * (x / self.xmax)
13621363

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

13641389
class Locator(TickHelper):
13651390
"""

0 commit comments

Comments
 (0)