8000 ticker.EngFormatter: base upon ScalarFormatter · matplotlib/matplotlib@dc1fde0 · GitHub
[go: up one dir, main page]

Skip to content

Commit dc1fde0

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 dc1fde0

File tree

4 files changed

+108
-58
lines changed

4 files changed

+108
-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: 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

Lines 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-
d 10000 ef __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

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