diff --git a/doc/api/api_changes/2017-08-24-deprecation-in-engformatter.rst b/doc/api/api_changes/2017-08-24-deprecation-in-engformatter.rst new file mode 100644 index 000000000000..630bab605668 --- /dev/null +++ b/doc/api/api_changes/2017-08-24-deprecation-in-engformatter.rst @@ -0,0 +1,5 @@ +Deprecation in EngFormatter +``````````````````````````` + +Passing a string as *num* argument when calling an instance of +`matplotlib.ticker.EngFormatter` is deprecated and will be removed in 2.3. diff --git a/doc/users/whats_new/EngFormatter_new_kwarg_sep.rst b/doc/users/whats_new/EngFormatter_new_kwarg_sep.rst new file mode 100644 index 000000000000..9e4bb2840e64 --- /dev/null +++ b/doc/users/whats_new/EngFormatter_new_kwarg_sep.rst @@ -0,0 +1,10 @@ +New keyword argument 'sep' for EngFormatter +------------------------------------------- + +A new "sep" keyword argument has been added to +:class:`~matplotlib.ticker.EngFormatter` and provides a means to define +the string that will be used between the value and its unit. The default +string is " ", which preserves the former behavior. Besides, the separator is +now present between the value and its unit even in the absence of SI prefix. +There was formerly a bug that was causing strings like "3.14V" to be returned +instead of the expected "3.14 V" (with the default behavior). diff --git a/examples/api/engineering_formatter.py b/examples/api/engineering_formatter.py index 743ee819cae9..4d9f2dfdec90 100644 --- a/examples/api/engineering_formatter.py +++ b/examples/api/engineering_formatter.py @@ -14,13 +14,31 @@ # Fixing random state for reproducibility prng = np.random.RandomState(19680801) -fig, ax = plt.subplots() -ax.set_xscale('log') -formatter = EngFormatter(unit='Hz') -ax.xaxis.set_major_formatter(formatter) - +# Create artificial data to plot. +# The x data span over several decades to demonstrate several SI prefixes. xs = np.logspace(1, 9, 100) ys = (0.8 + 0.4 * prng.uniform(size=100)) * np.log10(xs)**2 -ax.plot(xs, ys) +# Figure width is doubled (2*6.4) to display nicely 2 subplots side by side. +fig, (ax0, ax1) = plt.subplots(nrows=2, figsize=(7, 9.6)) +for ax in (ax0, ax1): + ax.set_xscale('log') + +# Demo of the default settings, with a user-defined unit label. +ax0.set_title('Full unit ticklabels, w/ default precision & space separator') +formatter0 = EngFormatter(unit='Hz') +ax0.xaxis.set_major_formatter(formatter0) +ax0.plot(xs, ys) +ax0.set_xlabel('Frequency') + +# Demo of the options `places` (number of digit after decimal point) and +# `sep` (separator between the number and the prefix/unit). +ax1.set_title('SI-prefix only ticklabels, 1-digit precision & ' + 'thin space separator') +formatter1 = EngFormatter(places=1, sep=u"\N{THIN SPACE}") # U+2009 +ax1.xaxis.set_major_formatter(formatter1) +ax1.plot(xs, ys) +ax1.set_xlabel('Frequency [Hz]') + +plt.tight_layout() plt.show() diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 865cc085c376..921d40cae3f7 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -549,26 +549,97 @@ def test_basic(self, format, 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'), + # (input, expected) where ''expected'' corresponds to the outputs + # respectively returned when (places=None, places=0, places=2) + raw_format_data = [ + (-1234.56789, ('-1.23457 k', '-1 k', '-1.23 k')), + (-1.23456789, ('-1.23457', '-1', '-1.23')), + (-0.123456789, ('-123.457 m', '-123 m', '-123.46 m')), + (-0.00123456789, ('-1.23457 m', '-1 m', '-1.23 m')), + (-0.0, ('0', '0', '0.00')), + (-0, ('0', '0', '0.00')), + (0, ('0', '0', '0.00')), + (1.23456789e-6, ('1.23457 \u03bc', '1 \u03bc', '1.23 \u03bc')), + (0.123456789, ('123.457 m', '123 m', '123.46 m')), + (0.1, ('100 m', '100 m', '100.00 m')), + (1, ('1', '1', '1.00')), + (1.23456789, ('1.23457', '1', '1.23')), + (999.9, ('999.9', '1 k', '999.90')), # places=0: corner-case rounding + (999.9999, ('1 k', '1 k', '1.00 k')), # corner-case roudning for all + (1000, ('1 k', '1 k', '1.00 k')), + (1001, ('1.001 k', '1 k', '1.00 k')), + (100001, ('100.001 k', '100 k', '100.00 k')), + (987654.321, ('987.654 k', '988 k', '987.65 k')), + (1.23e27, ('1230 Y', '1230 Y', '1230.00 Y')) # OoR value (> 1000 Y) ] - @pytest.mark.parametrize('unit, input, expected', format_data) - def test_formatting(self, unit, input, expected): + @pytest.mark.parametrize('input, expected', raw_format_data) + def test_params(self, 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). + Test the formatting of EngFormatter for various values of the 'places' + argument, in several cases: + 0. without a unit symbol but with a (default) space separator; + 1. with both a unit symbol and a (default) space separator; + 2. with both a unit symbol and some non default separators; + 3. without a unit symbol but with some non default separators. + Note that cases 2. and 3. are looped over several separator strings. """ - fmt = mticker.EngFormatter(unit) - assert fmt(input) == expected + + UNIT = 's' # seconds + DIGITS = '0123456789' # %timeit showed 10-20% faster search than set + + # Case 0: unit='' (default) and sep=' ' (default). + # 'expected' already corresponds to this reference case. + exp_outputs = expected + formatters = ( + mticker.EngFormatter(), # places=None (default) + mticker.EngFormatter(places=0), + mticker.EngFormatter(places=2) + ) + for _formatter, _exp_output in zip(formatters, exp_outputs): + assert _formatter(input) == _exp_output + + # Case 1: unit=UNIT and sep=' ' (default). + # Append a unit symbol to the reference case. + # Beware of the values in [1, 1000), where there is no prefix! + exp_outputs = (_s + " " + UNIT if _s[-1] in DIGITS # case w/o prefix + else _s + UNIT for _s in expected) + formatters = ( + mticker.EngFormatter(unit=UNIT), # places=None (default) + mticker.EngFormatter(unit=UNIT, places=0), + mticker.EngFormatter(unit=UNIT, places=2) + ) + for _formatter, _exp_output in zip(formatters, exp_outputs): + assert _formatter(input) == _exp_output + + # Test several non default separators: no separator, a narrow + # no-break space (unicode character) and an extravagant string. + for _sep in ("", "\N{NARROW NO-BREAK SPACE}", "@_@"): + # Case 2: unit=UNIT and sep=_sep. + # Replace the default space separator from the reference case + # with the tested one `_sep` and append a unit symbol to it. + exp_outputs = (_s + _sep + UNIT if _s[-1] in DIGITS # no prefix + else _s.replace(" ", _sep) + UNIT + for _s in expected) + formatters = ( + mticker.EngFormatter(unit=UNIT, sep=_sep), # places=None + mticker.EngFormatter(unit=UNIT, places=0, sep=_sep), + mticker.EngFormatter(unit=UNIT, places=2, sep=_sep) + ) + for _formatter, _exp_output in zip(formatters, exp_outputs): + assert _formatter(input) == _exp_output + + # Case 3: unit='' (default) and sep=_sep. + # Replace the default space separator from the reference case + # with the tested one `_sep`. Reference case is already unitless. + exp_outputs = (_s.replace(" ", _sep) for _s in expected) + formatters = ( + mticker.EngFormatter(sep=_sep), # places=None (default) + mticker.EngFormatter(places=0, sep=_sep), + mticker.EngFormatter(places=2, sep=_sep) + ) + for _formatter, _exp_output in zip(formatters, exp_outputs): + assert _formatter(input) == _exp_output class TestPercentFormatter(object): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 65a23537b58a..84933aeb0e35 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -173,7 +173,6 @@ import six -import decimal import itertools import locale import math @@ -1184,15 +1183,8 @@ class EngFormatter(Formatter): """ Formats axis values using engineering prefixes to represent powers of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. - - `unit` is a string containing the abbreviated name of the unit, - suitable for use with single-letter representations of powers of - 1000. For example, 'Hz' or 'm'. - - `places` is the precision with which to display the number, - specified in digits after the decimal point (there will be between - one and three digits before the decimal point). """ + # The SI engineering prefixes ENG_PREFIXES = { -24: "y", @@ -1214,12 +1206,42 @@ class EngFormatter(Formatter): 24: "Y" } - def __init__(self, unit="", places=None): + def __init__(self, unit="", places=None, sep=" "): + """ + Parameters + ---------- + unit : str (default: "") + Unit symbol to use, suitable for use with single-letter + representations of powers of 1000. For example, 'Hz' or 'm'. + + places : int (default: None) + Precision with which to display the number, specified in + digits after the decimal point (there will be between one + and three digits before the decimal point). If it is None, + the formatting falls back to the floating point format '%g', + which displays up to 6 *significant* digits, i.e. the equivalent + value for *places* varies between 0 and 5 (inclusive). + + sep : str (default: " ") + Separator used between the value and the prefix/unit. For + example, one get '3.14 mV' if ``sep`` is " " (default) and + '3.14mV' if ``sep`` is "". Besides the default behavior, some + other useful options may be: + + * ``sep=""`` to append directly the prefix/unit to the value; + * ``sep="\\N{THIN SPACE}"`` (``U+2009``); + * ``sep="\\N{NARROW NO-BREAK SPACE}"`` (``U+202F``); + * ``sep="\\N{NO-BREAK SPACE}"`` (``U+00A0``). + """ self.unit = unit self.places = places + self.sep = sep def __call__(self, x, pos=None): s = "%s%s" % (self.format_eng(x), self.unit) + # Remove the trailing separator when there is neither prefix nor unit + if len(self.sep) > 0 and s.endswith(self.sep): + s = s[:-len(self.sep)] return self.fix_minus(s) def format_eng(self, num): @@ -1238,40 +1260,47 @@ def format_eng(self, num): u'-1.00 \N{GREEK SMALL LETTER MU}' `num` may be a numeric value or a string that can be converted - to a numeric value with the `decimal.Decimal` constructor. + to a numeric value with ``float(num)``. """ - dnum = decimal.Decimal(str(num)) + if isinstance(num, six.string_types): + warnings.warn( + "Passing a string as *num* argument is deprecated since" + "Matplotlib 2.1, and is expected to be removed in 2.3.", + mplDeprecation) + dnum = float(num) sign = 1 + fmt = "g" if self.places is None else ".{:d}f".format(self.places) if dnum < 0: sign = -1 dnum = -dnum if dnum != 0: - pow10 = decimal.Decimal(int(math.floor(dnum.log10() / 3) * 3)) + pow10 = int(math.floor(math.log10(dnum) / 3) * 3) else: - pow10 = decimal.Decimal(0) - - pow10 = pow10.min(max(self.ENG_PREFIXES)) - pow10 = pow10.max(min(self.ENG_PREFIXES)) + pow10 = 0 + # Force dnum to zero, to avoid inconsistencies like + # format_eng(-0) = "0" and format_eng(0.0) = "0" + # but format_eng(-0.0) = "-0.0" + dnum = 0.0 + + pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) + + mant = sign * dnum / (10.0 ** pow10) + # Taking care of the cases like 999.9..., which + # may be rounded to 1000 instead of 1 k. Beware + # of the corner case of values that are beyond + # the range of SI prefixes (i.e. > 'Y'). + _fmant = float("{mant:{fmt}}".format(mant=mant, fmt=fmt)) + if _fmant >= 1000 and pow10 != max(self.ENG_PREFIXES): + mant /= 1000 + pow10 += 3 prefix = self.ENG_PREFIXES[int(pow10)] - mant = sign * dnum / (10 ** pow10) - - if self.places is None: - format_str = "%g %s" - elif self.places == 0: - format_str = "%i %s" - elif self.places > 0: - format_str = ("%%.%if %%s" % self.places) - - formatted = format_str % (mant, prefix) - - formatted = formatted.strip() - if (self.unit != "") and (prefix == self.ENG_PREFIXES[0]): - formatted = formatted + " " + formatted = "{mant:{fmt}}{sep}{prefix}".format( + mant=mant, sep=self.sep, prefix=prefix, fmt=fmt) return formatted pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy