Skip to content

Commit fde20d2

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 f858cc5 commit fde20d2

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=(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: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -509,35 +509,66 @@ def test_formatstrformatter():
509509
assert '002-01' == tmp_form(2, 1)
510510

511511

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

542573

543574
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)