Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit df6acf9

Browse files
authored
Merge pull request #6542 from afvincent/enh_engformatter_space_sep_new_option
ENH: EngFormatter new kwarg 'sep'
2 parents f3101f6 + ac42b94 commit df6acf9

File tree

5 files changed

+186
-53
lines changed

5 files changed

+186
-53
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Deprecation in EngFormatter
2+
```````````````````````````
3+
4+
Passing a string as *num* argument when calling an instance of
5+
`matplotlib.ticker.EngFormatter` is deprecated and will be removed in 2.3.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
New keyword argument 'sep' for EngFormatter
2+
-------------------------------------------
3+
4+
A new "sep" keyword argument has been added to
5+
:class:`~matplotlib.ticker.EngFormatter` and provides a means to define
6+
the string that will be used between the value and its unit. The default
7+
string is " ", which preserves the former behavior. Besides, the separator is
8+
now present between the value and its unit even in the absence of SI prefix.
9+
There was formerly a bug that was causing strings like "3.14V" to be returned
10+
instead of the expected "3.14 V" (with the default behavior).

examples/api/engineering_formatter.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,31 @@
1414
# Fixing random state for reproducibility
1515
prng = np.random.RandomState(19680801)
1616

17-
fig, ax = plt.subplots()
18-
ax.set_xscale('log')
19-
formatter = EngFormatter(unit='Hz')
20-
ax.xaxis.set_major_formatter(formatter)
21-
17+
# Create artificial data to plot.
18+
# The x data span over several decades to demonstrate several SI prefixes.
2219
xs = np.logspace(1, 9, 100)
2320
ys = (0.8 + 0.4 * prng.uniform(size=100)) * np.log10(xs)**2
24-
ax.plot(xs, ys)
2521

22+
# Figure width is doubled (2*6.4) to display nicely 2 subplots side by side.
23+
fig, (ax0, ax1) = plt.subplots(nrows=2, figsize=(7, 9.6))
24+
for ax in (ax0, ax1):
25+
ax.set_xscale('log')
26+
27+
# Demo of the default settings, with a user-defined unit label.
28+
ax0.set_title('Full unit ticklabels, w/ default precision & space separator')
29+
formatter0 = EngFormatter(unit='Hz')
30+
ax0.xaxis.set_major_formatter(formatter0)
31+
ax0.plot(xs, ys)
32+
ax0.set_xlabel('Frequency')
33+
34+
# Demo of the options `places` (number of digit after decimal point) and
35+
# `sep` (separator between the number and the prefix/unit).
36+
ax1.set_title('SI-prefix only ticklabels, 1-digit precision & '
37+
'thin space separator')
38+
formatter1 = EngFormatter(places=1, sep=u"\N{THIN SPACE}") # U+2009
39+
ax1.xaxis.set_major_formatter(formatter1)
40+
ax1.plot(xs, ys)
41+
ax1.set_xlabel('Frequency [Hz]')
42+
43+
plt.tight_layout()
2644
plt.show()

lib/matplotlib/tests/test_ticker.py

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -549,26 +549,97 @@ def test_basic(self, format, input, expected):
549549

550550

551551
class TestEngFormatter(object):
552-
format_data = [
553-
('', 0.1, u'100 m'),
554-
('', 1, u'1'),
555-
('', 999.9, u'999.9'),
556-
('', 1001, u'1.001 k'),
557-
(u's', 0.1, u'100 ms'),
558-
(u's', 1, u'1 s'),
559-
(u's', 999.9, u'999.9 s'),
560-
(u's', 1001, u'1.001 ks'),
552+
# (input, expected) where ''expected'' corresponds to the outputs
553+
# respectively returned when (places=None, places=0, places=2)
554+
raw_format_data = [
555+
(-1234.56789, ('-1.23457 k', '-1 k', '-1.23 k')),
556+
(-1.23456789, ('-1.23457', '-1', '-1.23')),
557+
(-0.123456789, ('-123.457 m', '-123 m', '-123.46 m')),
558+
(-0.00123456789, ('-1.23457 m', '-1 m', '-1.23 m')),
559+
(-0.0, ('0', '0', '0.00')),
560+
(-0, ('0', '0', '0.00')),
561+
(0, ('0', '0', '0.00')),
562+
(1.23456789e-6, ('1.23457 \u03bc', '1 \u03bc', '1.23 \u03bc')),
563+
(0.123456789, ('123.457 m', '123 m', '123.46 m')),
564+
(0.1, ('100 m', '100 m', '100.00 m')),
565+
(1, ('1', '1', '1.00')),
566+
(1.23456789, ('1.23457', '1', '1.23')),
567+
(999.9, ('999.9', '1 k', '999.90')), # places=0: corner-case rounding
568+
(999.9999, ('1 k', '1 k', '1.00 k')), # corner-case roudning for all
569+
(1000, ('1 k', '1 k', '1.00 k')),
570+
(1001, ('1.001 k', '1 k', '1.00 k')),
571+
(100001, ('100.001 k', '100 k', '100.00 k')),
572+
(987654.321, ('987.654 k', '988 k', '987.65 k')),
573+
(1.23e27, ('1230 Y', '1230 Y', '1230.00 Y')) # OoR value (> 1000 Y)
561574
]
562575

563-
@pytest.mark.parametrize('unit, input, expected', format_data)
564-
def test_formatting(self, unit, input, expected):
576+
@pytest.mark.parametrize('input, expected', raw_format_data)
577+
def test_params(self, input, expected):
565578
"""
566-
Test the formatting of EngFormatter with some inputs, against
567-
instances with and without units. Cases focus on when no SI
568-
prefix is present, for values in [1, 1000).
579+
Test the formatting of EngFormatter for various values of the 'places'
580+
argument, in several cases:
581+
0. without a unit symbol but with a (default) space separator;
582+
1. with both a unit symbol and a (default) space separator;
583+
2. with both a unit symbol and some non default separators;
584+
3. without a unit symbol but with some non default separators.
585+
Note that cases 2. and 3. are looped over several separator strings.
569586
"""
570-
fmt = mticker.EngFormatter(unit)
571-
assert fmt(input) == expected
587+
588+
UNIT = 's' # seconds
589+
DIGITS = '0123456789' # %timeit showed 10-20% faster search than set
590+
591+
# Case 0: unit='' (default) and sep=' ' (default).
592+
# 'expected' already corresponds to this reference case.
593+
exp_outputs = expected
594+
formatters = (
595+
mticker.EngFormatter(), # places=None (default)
596+
mticker.EngFormatter(places=0),
597+
mticker.EngFormatter(places=2)
598+
)
599+
for _formatter, _exp_output in zip(formatters, exp_outputs):
600+
assert _formatter(input) == _exp_output
601+
602+
# Case 1: unit=UNIT and sep=' ' (default).
603+
# Append a unit symbol to the reference case.
604+
# Beware of the values in [1, 1000), where there is no prefix!
605+
exp_outputs = (_s + " " + UNIT if _s[-1] in DIGITS # case w/o prefix
606+
else _s + UNIT for _s in expected)
607+
formatters = (
608+
mticker.EngFormatter(unit=UNIT), # places=None (default)
609+
mticker.EngFormatter(unit=UNIT, places=0),
610+
mticker.EngFormatter(unit=UNIT, places=2)
611+
)
612+
for _formatter, _exp_output in zip(formatters, exp_outputs):
613+
assert _formatter(input) == _exp_output
614+
615+
# Test several non default separators: no separator, a narrow
616+
# no-break space (unicode character) and an extravagant string.
617+
for _sep in ("", "\N{NARROW NO-BREAK SPACE}", "@_@"):
618+
# Case 2: unit=UNIT and sep=_sep.
619+
# Replace the default space separator from the reference case
620+
# with the tested one `_sep` and append a unit symbol to it.
621+
exp_outputs = (_s + _sep + UNIT if _s[-1] in DIGITS # no prefix
622+
else _s.replace(" ", _sep) + UNIT
623+
for _s in expected)
624+
formatters = (
625+
mticker.EngFormatter(unit=UNIT, sep=_sep), # places=None
626+
mticker.EngFormatter(unit=UNIT, places=0, sep=_sep),
627+
mticker.EngFormatter(unit=UNIT, places=2, sep=_sep)
628+
)
629+
for _formatter, _exp_output in zip(formatters, exp_outputs):
630+
assert _formatter(input) == _exp_output
631+
632+
# Case 3: unit='' (default) and sep=_sep.
633+
# Replace the default space separator from the reference case
634+
# with the tested one `_sep`. Reference case is already unitless.
635+
exp_outputs = (_s.replace(" ", _sep) for _s in expected)
636+
formatters = (
637+
mticker.EngFormatter(sep=_sep), # places=None (default)
638+
mticker.EngFormatter(places=0, sep=_sep),
639+
mticker.EngFormatter(places=2, sep=_sep)
640+
)
641+
for _formatter, _exp_output in zip(formatters, exp_outputs):
642+
assert _formatter(input) == _exp_output
572643

573644

574645
class TestPercentFormatter(object):

lib/matplotlib/ticker.py

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@
173173

174174
import six
175175

176-
import decimal
177176
import itertools
178177
import locale
179178
import math
@@ -1184,15 +1183,8 @@ class EngFormatter(Formatter):
11841183
"""
11851184
Formats axis values using engineering prefixes to represent powers
11861185
of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7.
1187-
1188-
`unit` is a string containing the abbreviated name of the unit,
1189-
suitable for use with single-letter representations of powers of
1190-
1000. For example, 'Hz' or 'm'.
1191-
1192-
`places` is the precision with which to display the number,
1193-
specified in digits after the decimal point (there will be between
1194-
one and three digits before the decimal point).
11951186
"""
1187+
11961188
# The SI engineering prefixes
11971189
ENG_PREFIXES = {
11981190
-24: "y",
@@ -1214,12 +1206,42 @@ class EngFormatter(Formatter):
12141206
24: "Y"
12151207
}
12161208

1217-
def __init__(self, unit="", places=None):
1209+
def __init__(self, unit="", places=None, sep=" "):
1210+
"""
1211+
Parameters
1212+
----------
1213+
unit : str (default: "")
1214+
Unit symbol to use, suitable for use with single-letter
1215+
representations of powers of 1000. For example, 'Hz' or 'm'.
1216+
1217+
places : int (default: None)
1218+
Precision with which to display the number, specified in
1219+
digits after the decimal point (there will be between one
1220+
and three digits before the decimal point). If it is None,
1221+
the formatting falls back to the floating point format '%g',
1222+
which displays up to 6 *significant* digits, i.e. the equivalent
1223+
value for *places* varies between 0 and 5 (inclusive).
1224+
1225+
sep : str (default: " ")
1226+
Separator used between the value and the prefix/unit. For
1227+
example, one get '3.14 mV' if ``sep`` is " " (default) and
1228+
'3.14mV' if ``sep`` is "". Besides the default behavior, some
1229+
other useful options may be:
1230+
1231+
* ``sep=""`` to append directly the prefix/unit to the value;
1232+
* ``sep="\\N{THIN SPACE}"`` (``U+2009``);
1233+
* ``sep="\\N{NARROW NO-BREAK SPACE}"`` (``U+202F``);
1234+
* ``sep="\\N{NO-BREAK SPACE}"`` (``U+00A0``).
1235+
"""
12181236
self.unit = unit
12191237
self.places = places
1238+
self.sep = sep
12201239

12211240
def __call__(self, x, pos=None):
12221241
s = "%s%s" % (self.format_eng(x), self.unit)
1242+
# Remove the trailing separator when there is neither prefix nor unit
1243+
if len(self.sep) > 0 and s.endswith(self.sep):
1244+
s = s[:-len(self.sep)]
12231245
return self.fix_minus(s)
12241246

12251247
def format_eng(self, num):
@@ -1238,40 +1260,47 @@ def format_eng(self, num):
12381260
u'-1.00 \N{GREEK SMALL LETTER MU}'
12391261
12401262
`num` may be a numeric value or a string that can be converted
1241-
to a numeric value with the `decimal.Decimal` constructor.
1263+
to a numeric value with ``float(num)``.
12421264
"""
1243-
dnum = decimal.Decimal(str(num))
1265+
if isinstance(num, six.string_types):
1266+
warnings.warn(
1267+
"Passing a string as *num* argument is deprecated since"
1268+
"Matplotlib 2.1, and is expected to be removed in 2.3.",
1269+
mplDeprecation)
12441270

1271+
dnum = float(num)
12451272
sign = 1
1273+
fmt = "g" if self.places is None else ".{:d}f".format(self.places)
12461274

12471275
if dnum < 0:
12481276
sign = -1
12491277
dnum = -dnum
12501278

12511279
if dnum != 0:
1252-
pow10 = decimal.Decimal(int(math.floor(dnum.log10() / 3) * 3))
1280+
pow10 = int(math.floor(math.log10(dnum) / 3) * 3)
12531281
else:
1254-
pow10 = decimal.Decimal(0)
1255-
1256-
pow10 = pow10.min(max(self.ENG_PREFIXES))
1257-
pow10 = pow10.max(min(self.ENG_PREFIXES))
1282+
pow10 = 0
1283+
# Force dnum to zero, to avoid inconsistencies like
1284+
# format_eng(-0) = "0" and format_eng(0.0) = "0"
1285+
# but format_eng(-0.0) = "-0.0"
1286+
dnum = 0.0
1287+
1288+
pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES))
1289+
1290+
mant = sign * dnum / (10.0 ** pow10)
1291+
# Taking care of the cases like 999.9..., which
1292+
# may be rounded to 1000 instead of 1 k. Beware
1293+
# of the corner case of values that are beyond
1294+
# the range of SI prefixes (i.e. > 'Y').
1295+
_fmant = float("{mant:{fmt}}".format(mant=mant, fmt=fmt))
1296+
if _fmant >= 1000 and pow10 != max(self.ENG_PREFIXES):
1297+
mant /= 1000
1298+
pow10 += 3
12581299

12591300
prefix = self.ENG_PREFIXES[int(pow10)]
12601301

1261-
mant = sign * dnum / (10 ** pow10)
1262-
1263-
if self.places is None:
1264-
format_str = "%g %s"
1265-
elif self.places == 0:
1266-
format_str = "%i %s"
1267-
elif self.places > 0:
1268-
format_str = ("%%.%if %%s" % self.places)
1269-
1270-
formatted = format_str % (mant, prefix)
1271-
1272-
formatted = formatted.strip()
1273-
if (self.unit != "") and (prefix == self.ENG_PREFIXES[0]):
1274-
formatted = formatted + " "
1302+
formatted = "{mant:{fmt}}{sep}{prefix}".format(
1303+
mant=mant, sep=self.sep, prefix=prefix, fmt=fmt)
12751304

12761305
return formatted
12771306

0 commit comments

Comments
 (0)