From 6add6ce677fea5c72e4921d972ab1dbf44f0a71e Mon Sep 17 00:00:00 2001 From: Joseph Fox-Rabinovitz Date: Fri, 27 Jan 2017 17:02:25 -0500 Subject: [PATCH] ENH: Fixed PercentFormatter usage with latex Added an example to the big formatter example Added test for latex correctness Modified existing pylab example --- .../pylab_examples/histogram_percent_demo.py | 27 ++---- examples/ticks_and_spines/tick-formatters.py | 13 ++- lib/matplotlib/tests/test_ticker.py | 86 +++++++++++-------- lib/matplotlib/ticker.py | 37 ++++++-- 4 files changed, 102 insertions(+), 61 deletions(-) diff --git a/examples/pylab_examples/histogram_percent_demo.py b/examples/pylab_examples/histogram_percent_demo.py index 9d7b5d34423f..ce62b0a16017 100644 --- a/examples/pylab_examples/histogram_percent_demo.py +++ b/examples/pylab_examples/histogram_percent_demo.py @@ -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() diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks_and_spines/tick-formatters.py index 19b486de69e9..13f17ab1ac02 100644 --- a/examples/ticks_and_spines/tick-formatters.py +++ b/examples/ticks_and_spines/tick-formatters.py @@ -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) @@ -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) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 443fa05d8dcf..d6eee0d6d7bd 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -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): @@ -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%'), @@ -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 diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 5b5f0de93974..ddfae88619a5 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -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): """ @@ -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): """