Skip to content

ENH: Fixed PercentFormatter usage with latex #7965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 8 additions & 19 deletions examples/pylab_examples/histogram_percent_demo.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import matplotlib
from numpy.random import randn
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
from matplotlib.ticker import PercentFormatter


def to_percent(y, position):
# Ignore the passed in position. This has the effect of scaling the default
# tick locations.
s = str(100 * y)

# The percent symbol needs escaping in latex
if matplotlib.rcParams['text.usetex'] is True:
return s + r'$\%$'
else:
return s + '%'

x = randn(5000)

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

# Create the formatter using the function to_percent. This multiplies all the
# default labels by 100, making them all percentages
formatter = FuncFormatter(to_percent)
# Make a normed histogram. It'll be multiplied by 100 later.
ax.hist(x, bins=50, normed=True)

# Set the formatter
plt.gca().yaxis.set_major_formatter(formatter)
# Set the formatter. `xmax` sets the value that maps to 100%.
ax.yaxis.set_major_formatter(PercentFormatter(xmax=1))

plt.show()
13 changes: 11 additions & 2 deletions examples/ticks_and_spines/tick-formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def setup(ax):
ax.patch.set_alpha(0.0)


plt.figure(figsize=(8, 5))
n = 6
plt.figure(figsize=(8, 6))
n = 7

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

# Percent formatter
ax = plt.subplot(n, 1, 7)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5))
ax.text(0.0, 0.1, "PercentFormatter(xmax=5)",
fontsize=15, transform=ax.transAxes)

# Push the top of the top axes outside the figure because we only show the
# bottom spine.
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)
Expand Down
86 changes: 52 additions & 34 deletions lib/matplotlib/tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,40 @@ def test_basic(self):
tmp_form = mticker.FormatStrFormatter('%05d')
assert '00002' == tmp_form(2)

# test str.format() style formatter
tmp_form = mticker.StrMethodFormatter('{x:05d}')
assert '00002' == tmp_form(2)

# test str.format() style formatter with `pos`
tmp_form = mticker.StrMethodFormatter('{x:03d}-{pos:02d}')
assert '002-01' == tmp_form(2, 1)
class TestStrMethodFormatter(object):
test_data = [
('{x:05d}', (2,), '00002'),
('{x:03d}-{pos:02d}', (2, 1), '002-01'),
]

@pytest.mark.parametrize('format, input, expected', test_data)
def test_basic(self, format, input, expected):
fmt = mticker.StrMethodFormatter(format)
assert fmt(*input) == expected


class TestEngFormatter(object):
format_data = [
('', 0.1, u'100 m'),
('', 1, u'1'),
('', 999.9, u'999.9'),
('', 1001, u'1.001 k'),
(u's', 0.1, u'100 ms'),
(u's', 1, u'1 s'),
(u's', 999.9, u'999.9 s'),
(u's', 1001, u'1.001 ks'),
]

@pytest.mark.parametrize('unit, input, expected', format_data)
def test_formatting(self, unit, input, expected):
"""
Test the formatting of EngFormatter with some inputs, against
instances with and without units. Cases focus on when no SI
prefix is present, for values in [1, 1000).
"""
fmt = mticker.EngFormatter(unit)
assert fmt(input) == expected


class TestPercentFormatter(object):
Expand All @@ -525,12 +552,12 @@ class TestPercentFormatter(object):
(100, 0, '%', 120, 100, '120%'),
(100, 0, '%', 100, 90, '100%'),
(100, 0, '%', 90, 50, '90%'),
(100, 0, '%', 1.7, 40, '2%'),
(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%'),
(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%'),
Expand Down Expand Up @@ -565,33 +592,24 @@ class TestPercentFormatter(object):
'Custom percent symbol',
]

latex_data = [
(False, False, r'50\{t}%'),
(False, True, r'50\\\{t\}\%'),
(True, False, r'50\{t}%'),
(True, True, r'50\{t}%'),
]

@pytest.mark.parametrize(
'xmax, decimals, symbol, x, display_range, expected',
percent_data, ids=percent_ids)
def test_percentformatter(self, xmax, decimals, symbol,
x, display_range, expected):
def test_basic(self, xmax, decimals, symbol,
x, display_range, expected):
formatter = mticker.PercentFormatter(xmax, decimals, symbol)
assert formatter.format_pct(x, display_range) == expected


class TestEngFormatter(object):
format_data = [
('', 0.1, u'100 m'),
('', 1, u'1'),
('', 999.9, u'999.9'),
('', 1001, u'1.001 k'),
(u's', 0.1, u'100 ms'),
(u's', 1, u'1 s'),
(u's', 999.9, u'999.9 s'),
(u's', 1001, u'1.001 ks'),
]

@pytest.mark.parametrize('unit, input, expected', format_data)
def test_formatting(self, unit, input, expected):
"""
Test the formatting of EngFormatter with some inputs, against
instances with and without units. Cases focus on when no SI
prefix is present, for values in [1, 1000).
"""
fmt = mticker.EngFormatter(unit)
assert fmt(input) == expected
with matplotlib.rc_context(rc={'text.usetex': False}):
assert formatter.format_pct(x, display_range) == expected

@pytest.mark.parametrize('is_latex, usetex, expected', latex_data)
def test_latex(self, is_latex, usetex, expected):
fmt = mticker.PercentFormatter(symbol='\\{t}%', is_latex=is_latex)
with matplotlib.rc_context(rc={'text.usetex': usetex}):
assert fmt.format_pct(50, 100) == expected
37 changes: 31 additions & 6 deletions lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1285,16 +1285,19 @@ class PercentFormatter(Formatter):
situation is where `xmax` is 1.0.

`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.
`None` or empty to indicate that no symbol should be used. LaTeX
special characters are escaped in `symbol` whenever latex mode is
enabled, unless `is_latex` is `True`.

`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.
"""
def __init__(self, xmax=100, decimals=None, symbol='%'):
def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False):
self.xmax = xmax + 0.0
self.decimals = decimals
self.symbol = symbol
self._symbol = symbol
self._is_latex = is_latex

def __call__(self, x, pos=None):
"""
Expand Down Expand Up @@ -1350,13 +1353,35 @@ def format_pct(self, x, display_range):
decimals = self.decimals
s = '{x:0.{decimals}f}'.format(x=x, decimals=int(decimals))

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

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

@property
def symbol(self):
"""
The configured percent symbol as a string.

If LaTeX is enabled via ``rcParams['text.usetex']``, the special
characters `{'#', '$', '%', '&', '~', '_', '^', '\\', '{', '}'}`
are automatically escaped in the string.
"""
symbol = self._symbol
if not symbol:
symbol = ''
elif rcParams['text.usetex'] and not self._is_latex:
# Source: http://www.personal.ceu.hu/tex/specchar.htm
# Backslash must be first for this to work correctly since
# it keeps getting added in
for spec in r'\#$%&~_^{}':
symbol = symbol.replace(spec, '\\' + spec)
return symbol

@symbol.setter
def symbol(self):
self._symbol = symbol


class Locator(TickHelper):
"""
Expand Down