Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f651b73

Browse filesBrowse files
committed
ticker.EngFormatter: base upon ScalarFormatter
Allows us to use many order of magnitude and offset related routines from ScalarFormatter, and removes a bit usetex related duplicated code. Solves #28463.
1 parent 739402c commit f651b73
Copy full SHA for f651b73

File tree

4 files changed

+108
-58
lines changed
Filter options

4 files changed

+108
-58
lines changed
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ticker.EngFormatter now computes offset by default
2+
--------------------------------------------------
3+
4+
``ticker.EngFormatter`` was modified to act very similar to
5+
``ticker.ScalarFormatter``, such that it computes the best offset of the axis
6+
data, and shows the offset with the known SI quantity prefixes. To disable this
7+
new behavior, simply pass ``useOffset=False`` when you instantiate it. If offsets
8+
are disabled, or if there is no particular offset that fits your axis data, the
9+
formatter will revert to the old behavior.

‎lib/matplotlib/tests/test_ticker.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_ticker.py
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,18 @@ def test_engformatter_usetex_useMathText():
15941594
assert x_tick_label_text == ['$0$', '$500$', '$1$ k']
15951595

15961596

1597+
def test_engformatter_useOffset():
1598+
fig, ax = plt.subplots()
1599+
offset = int(1e7)
1600+
ydata = range(offset, offset+5)
1601+
ax.plot(ydata)
1602+
ax.set_yticks(ydata)
1603+
ax.yaxis.set_major_formatter(mticker.EngFormatter(useOffset=True, unit="Hz"))
1604+
fig.canvas.draw()
1605+
y_tick_label_text = [labl.get_text() for labl in ax.get_yticklabels()]
1606+
assert y_tick_label_text == (np.array(ydata)-offset).astype(str).tolist()
1607+
1608+
15971609
class TestPercentFormatter:
15981610
percent_data = [
15991611
# Check explicitly set decimals over different intervals and values

‎lib/matplotlib/ticker.py

Copy file name to clipboardExpand all lines: lib/matplotlib/ticker.py
+84-44Lines changed: 84 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,7 +1338,7 @@ def format_data_short(self, value):
13381338
return f"1-{1 - value:e}"
13391339

13401340

1341-
class EngFormatter(Formatter):
1341+
class EngFormatter(ScalarFormatter):
13421342
"""
13431343
Format axis values using engineering prefixes to represent powers
13441344
of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7.
@@ -1370,7 +1370,7 @@ class EngFormatter(Formatter):
13701370
}
13711371

13721372
def __init__(self, unit="", places=None, sep=" ", *, usetex=None,
1373-
useMathText=None):
1373+
useMathText=None, useOffset=None):
13741374
r"""
13751375
Parameters
13761376
----------
@@ -1404,76 +1404,114 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None,
14041404
useMathText : bool, default: :rc:`axes.formatter.use_mathtext`
14051405
To enable/disable the use mathtext for rendering the numbers in
14061406
the formatter.
1407+
useOffset : bool or float, default: :rc:`axes.formatter.useoffset`
1408+
Whether to use offset notation. See `.set_useOffset`.
14071409
"""
14081410
self.unit = unit
14091411
self.places = places
14101412
self.sep = sep
1411-
self.set_usetex(usetex)
1412-
self.set_useMathText(useMathText)
1413-
1414-
def get_usetex(self):
1415-
return self._usetex
1416-
1417-
def set_usetex(self, val):
1418-
if val is None:
1419-
self._usetex = mpl.rcParams['text.usetex']
1420-
else:
1421-
self._usetex = val
1422-
1423-
usetex = property(fget=get_usetex, fset=set_usetex)
1424-
1425-
def get_useMathText(self):
1426-
return self._useMathText
1413+
super().__init__(
1414+
useOffset=useOffset,
1415+
useMathText=useMathText,
1416+
useLocale=False,
1417+
usetex=usetex,
1418+
)
14271419

1428-
def set_useMathText(self, val):
1429-
if val is None:
1430-
self._useMathText = mpl.rcParams['axes.formatter.use_mathtext']
1420+
def __call__(self, x, pos=None):
1421+
"""
1422+
Return the format for tick value *x* at position *pos*. If there is no
1423+
currently offset in the data, it returns the best engineering formatting
1424+
that fits the given argument, independently.
1425+
"""
1426+
if len(self.locs) == 0 or self.offset == 0:
1427+
return self.fix_minus(self.format_data(x))
14311428
else:
1432-
self._useMathText = val
1429+
xp = (x - self.offset) / (10. ** self.orderOfMagnitude)
1430+
if abs(xp) < 1e-8:
1431+
xp = 0
1432+
return self._format_maybe_minus_and_locale(self.format, xp)
14331433

1434-
useMathText = property(fget=get_useMathText, fset=set_useMathText)
1434+
def set_locs(self, locs):
1435+
# docstring inherited
1436+
self.locs = locs
1437+
if len(self.locs) > 0:
1438+
if self._useOffset:
1439+
self._compute_offset()
1440+
self._set_order_of_magnitude()
1441+
# This is what's different from ScalarFormatter: We search among
1442+
# the engineers' standard orders of magnitudes (0, -3, 3, -6, 6,
1443+
# -9, 9 etc) the oom closest to our self.orderOfMagnitude. Then we
1444+
# set our self.orderOfMagnitude to it.
1445+
c = abs(self.orderOfMagnitude)
1446+
for sciOom in itertools.count(0, 3):
1447+
if c <= sciOom:
1448+
self.orderOfMagnitude = math.copysign(sciOom, self.orderOfMagnitude)
1449+
break
1450+
self._set_format()
14351451

1436-
def __call__(self, x, pos=None):
1437-
s = f"{self.format_eng(x)}{self.unit}"
1438-
# Remove the trailing separator when there is neither prefix nor unit
1439-
if self.sep and s.endswith(self.sep):
1440-
s = s[:-len(self.sep)]
1441-
return self.fix_minus(s)
1452+
# Simplify a bit ScalarFormatter.get_offset: We always want to use
1453+
# self.format_data. We insert here the surrounding $...$ here, if tex /
1454+
# mathtext is set.
1455+
def get_offset(self):
1456+
# docstring inherited
1457+
if len(self.locs) == 0:
1458+
return ''
1459+
if self.orderOfMagnitude or self.offset:
1460+
offsetStr = ''
1461+
sciNotStr = ''
1462+
if self.offset:
1463+
offsetStr = self.format_data(self.offset)
1464+
if self.offset > 0:
1465+
offsetStr = '+' + offsetStr
1466+
if self.orderOfMagnitude:
1467+
sciNotStr = self.format_data(10 ** self.orderOfMagnitude)
1468+
if self._useMathText or self._usetex:
1469+
if sciNotStr != '':
1470+
sciNotStr = r'\times%s' % sciNotStr
1471+
s = fr'${sciNotStr}{offsetStr}$'
1472+
else:
1473+
s = ''.join((sciNotStr, offsetStr))
1474+
return self.fix_minus(s)
1475+
return ''
14421476

14431477
def format_eng(self, num):
1478+
"""Alias to EngFormatter.format_data"""
1479+
return self.format_data(num)
1480+
1481+
def format_data(self, value):
14441482
"""
14451483
Format a number in engineering notation, appending a letter
14461484
representing the power of 1000 of the original number.
14471485
Some examples:
14481486
1449-
>>> format_eng(0) # for self.places = 0
1487+
>>> format_data(0) # for self.places = 0
14501488
'0'
14511489
1452-
>>> format_eng(1000000) # for self.places = 1
1490+
>>> format_data(1000000) # for self.places = 1
14531491
'1.0 M'
14541492
1455-
>>> format_eng(-1e-6) # for self.places = 2
1493+
>>> format_data(-1e-6) # for self.places = 2
14561494
'-1.00 \N{MICRO SIGN}'
14571495
"""
14581496
sign = 1
14591497
fmt = "g" if self.places is None else f".{self.places:d}f"
14601498

1461-
if num < 0:
1499+
if value < 0:
14621500
sign = -1
1463-
num = -num
1501+
value = -value
14641502

1465-
if num != 0:
1466-
pow10 = int(math.floor(math.log10(num) / 3) * 3)
1503+
if value != 0:
1504+
pow10 = int(math.floor(math.log10(value) / 3) * 3)
14671505
else:
14681506
pow10 = 0
1469-
# Force num to zero, to avoid inconsistencies like
1507+
# Force value to zero, to avoid inconsistencies like
14701508
# format_eng(-0) = "0" and format_eng(0.0) = "0"
14711509
# but format_eng(-0.0) = "-0.0"
1472-
num = 0.0
1510+
value = 0.0
14731511

14741512
pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES))
14751513

1476-
mant = sign * num / (10.0 ** pow10)
1514+
mant = sign * value / (10.0 ** pow10)
14771515
# Taking care of the cases like 999.9..., which may be rounded to 1000
14781516
# instead of 1 k. Beware of the corner case of values that are beyond
14791517
# the range of SI prefixes (i.e. > 'Y').
@@ -1482,13 +1520,15 @@ def format_eng(self, num):
14821520
mant /= 1000
14831521
pow10 += 3
14841522

1485-
prefix = self.ENG_PREFIXES[int(pow10)]
1523+
unitPrefix = self.ENG_PREFIXES[int(pow10)]
1524+
if self.unit or unitPrefix:
1525+
suffix = f"{self.sep}{unitPrefix}{self.unit}"
1526+
else:
1527+
suffix = ""
14861528
if self._usetex or self._useMathText:
1487-
formatted = f"${mant:{fmt}}${self.sep}{prefix}"
1529+
return rf"${mant:{fmt}}${suffix}"
14881530
else:
1489-
formatted = f"{mant:{fmt}}{self.sep}{prefix}"
1490-
1491-
return formatted
1531+
return rf"{mant:{fmt}}{suffix}"
14921532

14931533

14941534
class PercentFormatter(Formatter):

‎lib/matplotlib/ticker.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/ticker.pyi
+3-14Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class LogitFormatter(Formatter):
130130
def set_minor_number(self, minor_number: int) -> None: ...
131131
def format_data_short(self, value: float) -> str: ...
132132

133-
class EngFormatter(Formatter):
133+
class EngFormatter(ScalarFormatter):
134134
ENG_PREFIXES: dict[int, str]
135135
unit: str
136136
places: int | None
@@ -142,20 +142,9 @@ class EngFormatter(Formatter):
142142
sep: str = ...,
143143
*,
144144
usetex: bool | None = ...,
145-
useMathText: bool | None = ...
145+
useMathText: bool | None = ...,
146+
useOffset: bool | float | None = ...,
146147
) -> None: ...
147-
def get_usetex(self) -> bool: ...
148-
def set_usetex(self, val: bool | None) -> None: ...
149-
@property
150-
def usetex(self) -> bool: ...
151-
@usetex.setter
152-
def usetex(self, val: bool | None) -> None: ...
153-
def get_useMathText(self) -> bool: ...
154-
def set_useMathText(self, val: bool | None) -> None: ...
155-
@property
156-
def useMathText(self) -> bool: ...
157-
@useMathText.setter
158-
def useMathText(self, val: bool | None) -> None: ...
159148
def format_eng(self, num: float) -> str: ...
160149

161150
class PercentFormatter(Formatter):

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.