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): """
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: