Skip to content

Commit 2f43894

Browse 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 2f43894

File tree

4 files changed

+170
-58
lines changed

4 files changed

+170
-58
lines changed
Lines 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 reside to the old behavior.

lib/matplotlib/tests/test_ticker.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,78 @@ def test_engformatter_usetex_useMathText():
15941594
assert x_tick_label_text == ['$0$', '$500$', '$1$ k']
15951595

15961596

1597+
@pytest.mark.parametrize(
1598+
'oom_center, oom_noise, oom_center_desired, oom_noise_desired', [
1599+
(11, 1, 9, 0),
1600+
(13, 7, 12, 6),
1601+
(1, -2, 0, -3),
1602+
(3, -2, 3, -3),
1603+
(5, -3, 3, -3),
1604+
(2, -3, 0, -3),
1605+
# The following sets of parameters demonstrates that when oom_center-1
1606+
# and oom_noise-2 equal a standard 3*N oom, we get that
1607+
# oom_noise_desired < oom_noise
1608+
(10, 2, 9, 3),
1609+
(1, -7, 0, -6),
1610+
(2, -4, 0, -3),
1611+
(1, -4, 0, -3),
1612+
# Tests where oom_center <= oom_noise
1613+
(4, 4, 0, 3),
1614+
(1, 4, 0, 3),
1615+
(1, 3, 0, 3),
1616+
(1, 2, 0, 0),
1617+
(1, 1, 0, 0),
1618+
]
1619+
)
1620+
def test_engformatter_offset_oom(
1621+
oom_center,
1622+
oom_noise,
1623+
oom_center_desired,
1624+
oom_noise_desired
1625+
):
1626+
UNIT = "eV"
1627+
# Doesn't really matter here, but should be of order of magnitude ~= 1
1628+
r = range(-5, 7)
1629+
fig, ax = plt.subplots()
1630+
# Use some random ugly number
1631+
data_offset = 2.7149*10**oom_center
1632+
ydata = data_offset + np.array(r, dtype=float)*10**oom_noise
1633+
ax.plot(ydata)
1634+
formatter = mticker.EngFormatter(useOffset=True, unit=UNIT)
1635+
# So that offset strings will always have the same size
1636+
formatter.ENG_PREFIXES[0] = "_"
1637+
ax.yaxis.set_major_formatter(formatter)
1638+
fig.canvas.draw()
1639+
offsetGot = formatter.get_offset()
1640+
ticksGot = [labl.get_text() for labl in ax.get_yticklabels()]
1641+
# Predicting whether offset should be 0 or not is essentially testing
1642+
# ScalarFormatter._compute_offset . This function is pretty complex and it
1643+
# would be nice to test it, but this is out of scope for this test which
1644+
# only makes sure that offset text and the ticks gets the correct unit
1645+
# prefixes and the ticks.
1646+
if formatter.offset:
1647+
prefix_noise_got = offsetGot[2]
1648+
prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired]
1649+
prefix_center_got = offsetGot[-1-len(UNIT)]
1650+
prefix_center_desired = formatter.ENG_PREFIXES[oom_center_desired]
1651+
assert prefix_noise_desired == prefix_noise_got
1652+
assert prefix_center_desired == prefix_center_got
1653+
# Make sure the ticks didn't get the UNIT
1654+
for tick in ticksGot:
1655+
assert UNIT not in tick
1656+
else:
1657+
assert oom_center_desired == 0
1658+
assert offsetGot == ""
1659+
# Make sure the ticks contain now the prefixes
1660+
for tick in ticksGot:
1661+
# 0 is zero on all orders of magnitudes, no
1662+
if tick[0] == "0":
1663+
prefixIdx = 0
1664+
else:
1665+
prefixIdx = oom_noise_desired
1666+
assert tick.endswith(formatter.ENG_PREFIXES[prefixIdx] + UNIT)
1667+
1668+
15971669
class TestPercentFormatter:
15981670
percent_data = [
15991671
# Check explicitly set decimals over different intervals and values

lib/matplotlib/ticker.py

Lines changed: 86 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,116 @@ 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+
vmin, vmax = sorted(self.axis.get_view_interval())
1439+
if self._useOffset:
1440+
self._compute_offset()
1441+
if self.offset != 0:
1442+
# We don't want to use the offset computed by
1443+
# self._compute_offset because it rounds the offset unaware
1444+
# of our engineering prefixes preference, and this can
1445+
# cause ticks with 4+ digits to appear. These ticks are
1446+
# slightly less readable, so if offset is justified
1447+
# (decided by self._compute_offset) we set it to better
1448+
# value:
1449+
self.offset = round((vmin + vmax)/2, 3)
1450+
# Use log1000 to use engineers' oom standards
1451+
self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000))*3
1452+
self._set_format()
14351453

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)
1454+
# Simplify a bit ScalarFormatter.get_offset: We always want to use
1455+
# self.format_data. Also we want to return a non-empty string only if there
1456+
# is an offset, no matter what is self.orderOfMagnitude. if there is an
1457+
# offset OTH, self.orderOfMagnitude is consulted. This behavior is verified
1458+
# in `test_ticker.py`.
1459+
def get_offset(self):
1460+
# docstring inherited
1461+
if len(self.locs) == 0:
1462+
return ''
1463+
if self.offset:
1464+
offsetStr = ''
1465+
if self.offset:
1466+
offsetStr = self.format_data(self.offset)
1467+
if self.offset > 0:
1468+
offsetStr = '+' + offsetStr
1469+
sciNotStr = self.format_data(10 ** self.orderOfMagnitude)
1470+
if self._useMathText or self._usetex:
1471+
if sciNotStr != '':
1472+
sciNotStr = r'\times%s' % sciNotStr
1473+
s = fr'${sciNotStr}{offsetStr}$'
1474+
else:
1475+
s = ''.join((sciNotStr, offsetStr))
1476+
return self.fix_minus(s)
1477+
return ''
14421478

14431479
def format_eng(self, num):
1480+
"""Alias to EngFormatter.format_data"""
1481+
return self.format_data(num)
1482+
1483+
def format_data(self, value):
14441484
"""
14451485
Format a number in engineering notation, appending a letter
14461486
representing the power of 1000 of the original number.
14471487
Some examples:
14481488
1449-
>>> format_eng(0) # for self.places = 0
1489+
>>> format_data(0) # for self.places = 0
14501490
'0'
14511491
1452-
>>> format_eng(1000000) # for self.places = 1
1492+
>>> format_data(1000000) # for self.places = 1
14531493
'1.0 M'
14541494
1455-
>>> format_eng(-1e-6) # for self.places = 2
1495+
>>> format_data(-1e-6) # for self.places = 2
14561496
'-1.00 \N{MICRO SIGN}'
14571497
"""
14581498
sign = 1
14591499
fmt = "g" if self.places is None else f".{self.places:d}f"
14601500

1461-
if num < 0:
1501+
if value < 0:
14621502
sign = -1
1463-
num = -num
1503+
value = -value
14641504

1465-
if num != 0:
1466-
pow10 = int(math.floor(math.log10(num) / 3) * 3)
1505+
if value != 0:
1506+
pow10 = int(math.floor(math.log10(value) / 3) * 3)
14671507
else:
14681508
pow10 = 0
1469-
# Force num to zero, to avoid inconsistencies like
1509+
# Force value to zero, to avoid inconsistencies like
14701510
# format_eng(-0) = "0" and format_eng(0.0) = "0"
14711511
# but format_eng(-0.0) = "-0.0"
1472-
num = 0.0
1512+
value = 0.0
14731513

14741514
pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES))
14751515

1476-
mant = sign * num / (10.0 ** pow10)
1516+
mant = sign * value / (10.0 ** pow10)
14771517
# Taking care of the cases like 999.9..., which may be rounded to 1000
14781518
# instead of 1 k. Beware of the corner case of values that are beyond
14791519
# the range of SI prefixes (i.e. > 'Y').
@@ -1482,13 +1522,15 @@ def format_eng(self, num):
14821522
mant /= 1000
14831523
pow10 += 3
14841524

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

14931535

14941536
class PercentFormatter(Formatter):

lib/matplotlib/ticker.pyi

Lines 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)
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