From facb403e944ccdc3464ea39b236651c5754cbfae Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Mon, 1 Jul 2024 16:02:33 +0300 Subject: [PATCH 01/10] ticker.ScalarFormatter: allow changing usetex like in EngFormatter --- lib/matplotlib/ticker.py | 21 +++++++++++++++++++-- lib/matplotlib/ticker.pyi | 6 ++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 0053031ece3e..ab219b20ac5c 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -407,6 +407,11 @@ class ScalarFormatter(Formatter): useLocale : bool, default: :rc:`axes.formatter.use_locale`. Whether to use locale settings for decimal sign and positive sign. See `.set_useLocale`. + usetex : bool, default: :rc:`text.usetex` + To enable/disable the use of TeX's math mode for rendering the + numbers in the formatter. + + .. versionadded:: 3.10 Notes ----- @@ -444,13 +449,14 @@ class ScalarFormatter(Formatter): """ - def __init__(self, useOffset=None, useMathText=None, useLocale=None): + def __init__(self, useOffset=None, useMathText=None, useLocale=None, *, + usetex=None): if useOffset is None: useOffset = mpl.rcParams['axes.formatter.useoffset'] self._offset_threshold = \ mpl.rcParams['axes.formatter.offset_threshold'] self.set_useOffset(useOffset) - self._usetex = mpl.rcParams['text.usetex'] + self.set_usetex(usetex) self.set_useMathText(useMathText) self.orderOfMagnitude = 0 self.format = '' @@ -458,6 +464,17 @@ def __init__(self, useOffset=None, useMathText=None, useLocale=None): self._powerlimits = mpl.rcParams['axes.formatter.limits'] self.set_useLocale(useLocale) + def get_usetex(self): + return self._usetex + + def set_usetex(self, val): + if val is None: + self._usetex = mpl.rcParams['text.usetex'] + else: + self._usetex = val + + usetex = property(fget=get_usetex, fset=set_usetex) + def get_useOffset(self): """ Return whether automatic mode for offset notation is active. diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index fd8e41848671..c1ad5f09ab0b 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -64,8 +64,14 @@ class ScalarFormatter(Formatter): useOffset: bool | float | None = ..., useMathText: bool | None = ..., useLocale: bool | None = ..., + *, + usetex: bool | None = ..., ) -> None: ... offset: float + def get_usetex(self) -> bool: ... + def set_usetex(self, val: bool | float) -> None: ... + @property + def usetex(self) -> bool: ... def get_useOffset(self) -> bool: ... def set_useOffset(self, val: bool | float) -> None: ... @property From 7cb3f3307aecc0c18edacf451eacba2457209abf Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Mon, 1 Jul 2024 16:21:49 +0300 Subject: [PATCH 02/10] 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. --- .../next_whats_new/engformatter_offset.rst | 14 ++ .../examples/ticks/engformatter_offset.py | 32 ++++ lib/matplotlib/tests/test_ticker.py | 73 ++++++++++ lib/matplotlib/ticker.py | 137 ++++++++++++------ lib/matplotlib/ticker.pyi | 17 +-- 5 files changed, 215 insertions(+), 58 deletions(-) create mode 100644 doc/users/next_whats_new/engformatter_offset.rst create mode 100644 galleries/examples/ticks/engformatter_offset.py diff --git a/doc/users/next_whats_new/engformatter_offset.rst b/doc/users/next_whats_new/engformatter_offset.rst new file mode 100644 index 000000000000..37f92bfa71cb --- /dev/null +++ b/doc/users/next_whats_new/engformatter_offset.rst @@ -0,0 +1,14 @@ +:class:`matplotlib.ticker.EngFormatter` now computes offset by default +---------------------------------------------------------------------- + +:class:`matplotlib.ticker.EngFormatter` has gained the ability to show an +offset text near the axis. With shared logic with +:class:`matplotlib.ticker.ScalarFormatter`, it is capable of deciding whether +the data qualifies having an offset and show it with an appropriate SI quantity +prefix, and with the supplied ``unit``. + +To enable this new behavior, simply pass ``useOffset=True`` when you +instantiate :class:`matplotlib.ticker.EngFormatter`. See example +:doc:`/gallery/ticks/engformatter_offset`. + +.. plot:: gallery/ticks/engformatter_offset.py diff --git a/galleries/examples/ticks/engformatter_offset.py b/galleries/examples/ticks/engformatter_offset.py new file mode 100644 index 000000000000..2f0de27b73eb --- /dev/null +++ b/galleries/examples/ticks/engformatter_offset.py @@ -0,0 +1,32 @@ +""" +=================================================== +SI prefixed offsets and natural order of magnitudes +=================================================== + +:class:`matplotlib.ticker.EngFormatter` is capable of computing a natural +offset for your axis data, and presenting it with a standard SI prefix +automatically calculated. + +Below is an examples of such a plot: + +""" + +import matplotlib.pyplot as plt +import numpy as np + +import matplotlib.ticker as mticker + +np.random.seed(19680801) + +UNIT = "Hz" + +fig, ax = plt.subplots() +ax.yaxis.set_major_formatter(mticker.EngFormatter( + useOffset=True, + unit=UNIT +)) +size = 100 +measurement = np.full(size, 1)*1e9 +noise = np.random.uniform(low=-2e3, high=2e3, size=(size)) +ax.plot(measurement + noise) +plt.show() diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 222a0d7e11b0..752a9c58a152 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1591,6 +1591,79 @@ def test_engformatter_usetex_useMathText(): assert x_tick_label_text == ['$0$', '$500$', '$1$ k'] +@pytest.mark.parametrize( + 'oom_center, oom_noise, oom_center_desired, oom_noise_desired', [ + (11, 1, 9, 0), + (13, 7, 12, 6), + (1, -2, 0, -3), + (3, -2, 3, -3), + (5, -3, 3, -3), + (2, -3, 0, -3), + # The following sets of parameters demonstrates that when oom_center-1 + # and oom_noise-2 equal a standard 3*N oom, we get that + # oom_noise_desired < oom_noise + (10, 2, 9, 3), + (1, -7, 0, -6), + (2, -4, 0, -3), + (1, -4, 0, -3), + # Tests where oom_center <= oom_noise, those are probably covered by the + # part where formatter.offset != 0 + (4, 4, 0, 3), + (1, 4, 0, 3), + (1, 3, 0, 3), + (1, 2, 0, 0), + (1, 1, 0, 0), + ] +) +def test_engformatter_offset_oom( + oom_center, + oom_noise, + oom_center_desired, + oom_noise_desired +): + UNIT = "eV" + # Doesn't really matter here, but should be of order of magnitude ~= 1 + r = range(-5, 7) + fig, ax = plt.subplots() + # Use some random ugly number + data_offset = 2.7149*10**oom_center + ydata = data_offset + np.array(r, dtype=float)*10**oom_noise + ax.plot(ydata) + formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) + # So that offset strings will always have the same size + formatter.ENG_PREFIXES[0] = "_" + ax.yaxis.set_major_formatter(formatter) + fig.canvas.draw() + offset_got = formatter.get_offset() + ticks_got = [labl.get_text() for labl in ax.get_yticklabels()] + # Predicting whether offset should be 0 or not is essentially testing + # ScalarFormatter._compute_offset . This function is pretty complex and it + # would be nice to test it, but this is out of scope for this test which + # only makes sure that offset text and the ticks gets the correct unit + # prefixes and the ticks. + if formatter.offset: + prefix_noise_got = offset_got[2] + prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] + prefix_center_got = offset_got[-1-len(UNIT)] + prefix_center_desired = formatter.ENG_PREFIXES[oom_center_desired] + assert prefix_noise_desired == prefix_noise_got + assert prefix_center_desired == prefix_center_got + # Make sure the ticks didn't get the UNIT + for tick in ticks_got: + assert UNIT not in tick + else: + assert oom_center_desired == 0 + assert offset_got == "" + # Make sure the ticks contain now the prefixes + for tick in ticks_got: + # 0 is zero on all orders of magnitudes, no + if tick[0] == "0": + prefix_idx = 0 + else: + prefix_idx = oom_noise_desired + assert tick.endswith(formatter.ENG_PREFIXES[prefix_idx] + UNIT) + + class TestPercentFormatter: percent_data = [ # Check explicitly set decimals over different intervals and values diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index ab219b20ac5c..713485ceb16b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1341,7 +1341,7 @@ def format_data_short(self, value): return f"1-{1 - value:e}" -class EngFormatter(Formatter): +class EngFormatter(ScalarFormatter): """ Format axis values using engineering prefixes to represent powers of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. @@ -1373,7 +1373,7 @@ class EngFormatter(Formatter): } def __init__(self, unit="", places=None, sep=" ", *, usetex=None, - useMathText=None): + useMathText=None, useOffset=False): r""" Parameters ---------- @@ -1407,76 +1407,123 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, useMathText : bool, default: :rc:`axes.formatter.use_mathtext` To enable/disable the use mathtext for rendering the numbers in the formatter. + useOffset : bool or float, default: False + Whether to use offset notation with :math:`10^{3*N}` based prefixes. + This features allows showing an offset with standard SI order of + magnitude prefix near the axis. Offset is computed similarly to + how `ScalarFormatter` computes it internally, but here you are + guaranteed to get an offset which will make the tick labels exceed + 3 digits. See also `.set_useOffset`. + + .. versionadded:: 3.10 """ self.unit = unit self.places = places self.sep = sep - self.set_usetex(usetex) - self.set_useMathText(useMathText) - - def get_usetex(self): - return self._usetex - - def set_usetex(self, val): - if val is None: - self._usetex = mpl.rcParams['text.usetex'] - else: - self._usetex = val - - usetex = property(fget=get_usetex, fset=set_usetex) - - def get_useMathText(self): - return self._useMathText + super().__init__( + useOffset=useOffset, + useMathText=useMathText, + useLocale=False, + usetex=usetex, + ) - def set_useMathText(self, val): - if val is None: - self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] + def __call__(self, x, pos=None): + """ + Return the format for tick value *x* at position *pos*. If there is no + currently offset in the data, it returns the best engineering formatting + that fits the given argument, independently. + """ + if len(self.locs) == 0 or self.offset == 0: + return self.fix_minus(self.format_data(x)) else: - self._useMathText = val + xp = (x - self.offset) / (10. ** self.orderOfMagnitude) + if abs(xp) < 1e-8: + xp = 0 + return self._format_maybe_minus_and_locale(self.format, xp) - useMathText = property(fget=get_useMathText, fset=set_useMathText) + def set_locs(self, locs): + # docstring inherited + self.locs = locs + if len(self.locs) > 0: + vmin, vmax = sorted(self.axis.get_view_interval()) + if self._useOffset: + self._compute_offset() + if self.offset != 0: + # We don't want to use the offset computed by + # self._compute_offset because it rounds the offset unaware + # of our engineering prefixes preference, and this can + # cause ticks with 4+ digits to appear. These ticks are + # slightly less readable, so if offset is justified + # (decided by self._compute_offset) we set it to better + # value: + self.offset = round((vmin + vmax)/2, 3) + # Use log1000 to use engineers' oom standards + self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000))*3 + self._set_format() - def __call__(self, x, pos=None): - s = f"{self.format_eng(x)}{self.unit}" - # Remove the trailing separator when there is neither prefix nor unit - if self.sep and s.endswith(self.sep): - s = s[:-len(self.sep)] - return self.fix_minus(s) + # Simplify a bit ScalarFormatter.get_offset: We always want to use + # self.format_data. Also we want to return a non-empty string only if there + # is an offset, no matter what is self.orderOfMagnitude. if there is an + # offset OTH, self.orderOfMagnitude is consulted. This behavior is verified + # in `test_ticker.py`. + def get_offset(self): + # docstring inherited + if len(self.locs) == 0: + return '' + if self.offset: + offsetStr = '' + if self.offset: + offsetStr = self.format_data(self.offset) + if self.offset > 0: + offsetStr = '+' + offsetStr + sciNotStr = self.format_data(10 ** self.orderOfMagnitude) + if self._useMathText or self._usetex: + if sciNotStr != '': + sciNotStr = r'\times%s' % sciNotStr + s = fr'${sciNotStr}{offsetStr}$' + else: + s = ''.join((sciNotStr, offsetStr)) + return self.fix_minus(s) + return '' def format_eng(self, num): + """Alias to EngFormatter.format_data""" + return self.format_data(num) + + def format_data(self, value): """ Format a number in engineering notation, appending a letter representing the power of 1000 of the original number. Some examples: - >>> format_eng(0) # for self.places = 0 + >>> format_data(0) # for self.places = 0 '0' - >>> format_eng(1000000) # for self.places = 1 + >>> format_data(1000000) # for self.places = 1 '1.0 M' - >>> format_eng(-1e-6) # for self.places = 2 + >>> format_data(-1e-6) # for self.places = 2 '-1.00 \N{MICRO SIGN}' """ sign = 1 fmt = "g" if self.places is None else f".{self.places:d}f" - if num < 0: + if value < 0: sign = -1 - num = -num + value = -value - if num != 0: - pow10 = int(math.floor(math.log10(num) / 3) * 3) + if value != 0: + pow10 = int(math.floor(math.log10(value) / 3) * 3) else: pow10 = 0 - # Force num to zero, to avoid inconsistencies like + # Force value to zero, to avoid inconsistencies like # format_eng(-0) = "0" and format_eng(0.0) = "0" # but format_eng(-0.0) = "-0.0" - num = 0.0 + value = 0.0 pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) - mant = sign * num / (10.0 ** pow10) + mant = sign * value / (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'). @@ -1485,13 +1532,15 @@ def format_eng(self, num): mant /= 1000 pow10 += 3 - prefix = self.ENG_PREFIXES[int(pow10)] + unit_prefix = self.ENG_PREFIXES[int(pow10)] + if self.unit or unit_prefix: + suffix = f"{self.sep}{unit_prefix}{self.unit}" + else: + suffix = "" if self._usetex or self._useMathText: - formatted = f"${mant:{fmt}}${self.sep}{prefix}" + return f"${mant:{fmt}}${suffix}" else: - formatted = f"{mant:{fmt}}{self.sep}{prefix}" - - return formatted + return f"{mant:{fmt}}{suffix}" class PercentFormatter(Formatter): diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index c1ad5f09ab0b..cf08a729fa6a 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -131,7 +131,7 @@ class LogitFormatter(Formatter): def set_minor_number(self, minor_number: int) -> None: ... def format_data_short(self, value: float) -> str: ... -class EngFormatter(Formatter): +class EngFormatter(ScalarFormatter): ENG_PREFIXES: dict[int, str] unit: str places: int | None @@ -143,20 +143,9 @@ class EngFormatter(Formatter): sep: str = ..., *, usetex: bool | None = ..., - useMathText: bool | None = ... + useMathText: bool | None = ..., + useOffset: bool | float | None = ..., ) -> None: ... - def get_usetex(self) -> bool: ... - def set_usetex(self, val: bool | None) -> None: ... - @property - def usetex(self) -> bool: ... - @usetex.setter - def usetex(self, val: bool | None) -> None: ... - def get_useMathText(self) -> bool: ... - def set_useMathText(self, val: bool | None) -> None: ... - @property - def useMathText(self) -> bool: ... - @useMathText.setter - def useMathText(self, val: bool | None) -> None: ... def format_eng(self, num: float) -> str: ... class PercentFormatter(Formatter): From bc8573180581bfe3a5db114825b904eb68a063c6 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sat, 12 Oct 2024 22:49:38 +0300 Subject: [PATCH 03/10] Fix small documentation issues from QuLogic's review --- doc/users/next_whats_new/engformatter_offset.rst | 15 +++++++-------- galleries/examples/ticks/engformatter_offset.py | 2 +- lib/matplotlib/ticker.py | 9 +++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/users/next_whats_new/engformatter_offset.rst b/doc/users/next_whats_new/engformatter_offset.rst index 37f92bfa71cb..63c270723f23 100644 --- a/doc/users/next_whats_new/engformatter_offset.rst +++ b/doc/users/next_whats_new/engformatter_offset.rst @@ -1,14 +1,13 @@ -:class:`matplotlib.ticker.EngFormatter` now computes offset by default ----------------------------------------------------------------------- +``matplotlib.ticker.EngFormatter`` now computes offset by default +----------------------------------------------------------------- -:class:`matplotlib.ticker.EngFormatter` has gained the ability to show an -offset text near the axis. With shared logic with -:class:`matplotlib.ticker.ScalarFormatter`, it is capable of deciding whether -the data qualifies having an offset and show it with an appropriate SI quantity -prefix, and with the supplied ``unit``. +`matplotlib.ticker.EngFormatter` has gained the ability to show an offset text near the +axis. Using logic shared with `matplotlib.ticker.ScalarFormatter`, it is capable of +deciding whether the data qualifies having an offset and show it with an appropriate SI +quantity prefix, and with the supplied ``unit``. To enable this new behavior, simply pass ``useOffset=True`` when you -instantiate :class:`matplotlib.ticker.EngFormatter`. See example +instantiate `matplotlib.ticker.EngFormatter`. See example :doc:`/gallery/ticks/engformatter_offset`. .. plot:: gallery/ticks/engformatter_offset.py diff --git a/galleries/examples/ticks/engformatter_offset.py b/galleries/examples/ticks/engformatter_offset.py index 2f0de27b73eb..62340788217a 100644 --- a/galleries/examples/ticks/engformatter_offset.py +++ b/galleries/examples/ticks/engformatter_offset.py @@ -3,7 +3,7 @@ SI prefixed offsets and natural order of magnitudes =================================================== -:class:`matplotlib.ticker.EngFormatter` is capable of computing a natural +`matplotlib.ticker.EngFormatter` is capable of computing a natural offset for your axis data, and presenting it with a standard SI prefix automatically calculated. diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 713485ceb16b..db96735a8efc 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1415,7 +1415,7 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, guaranteed to get an offset which will make the tick labels exceed 3 digits. See also `.set_useOffset`. - .. versionadded:: 3.10 + .. versionadded:: 3.10 """ self.unit = unit self.places = places @@ -1429,9 +1429,10 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, def __call__(self, x, pos=None): """ - Return the format for tick value *x* at position *pos*. If there is no - currently offset in the data, it returns the best engineering formatting - that fits the given argument, independently. + Return the format for tick value *x* at position *pos*. + + If there is no currently offset in the data, it returns the best + engineering formatting that fits the given argument, independently. """ if len(self.locs) == 0 or self.offset == 0: return self.fix_minus(self.format_data(x)) From 744b5440c9b255bf8fdaf244b0a3821f695702ab Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sat, 12 Oct 2024 23:50:23 +0300 Subject: [PATCH 04/10] Small comment fixups from review --- galleries/examples/ticks/engformatter_offset.py | 1 + lib/matplotlib/tests/test_ticker.py | 6 +++++- lib/matplotlib/ticker.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/galleries/examples/ticks/engformatter_offset.py b/galleries/examples/ticks/engformatter_offset.py index 62340788217a..8cbc553e70e8 100644 --- a/galleries/examples/ticks/engformatter_offset.py +++ b/galleries/examples/ticks/engformatter_offset.py @@ -16,6 +16,7 @@ import matplotlib.ticker as mticker +# Fixing random state for reproducibility np.random.seed(19680801) UNIT = "Hz" diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 752a9c58a152..09552d17702b 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1642,6 +1642,9 @@ def test_engformatter_offset_oom( # only makes sure that offset text and the ticks gets the correct unit # prefixes and the ticks. if formatter.offset: + # These prefix_ variables are used only once, so we could have inlined + # them all, but it is more comfortable in case of tests breakages to + # view their values with pytest --showlocals. prefix_noise_got = offset_got[2] prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] prefix_center_got = offset_got[-1-len(UNIT)] @@ -1656,7 +1659,8 @@ def test_engformatter_offset_oom( assert offset_got == "" # Make sure the ticks contain now the prefixes for tick in ticks_got: - # 0 is zero on all orders of magnitudes, no + # 0 is zero on all orders of magnitudes, no matter what is + # oom_noise_desired if tick[0] == "0": prefix_idx = 0 else: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index db96735a8efc..f551c5af7153 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1464,8 +1464,8 @@ def set_locs(self, locs): # Simplify a bit ScalarFormatter.get_offset: We always want to use # self.format_data. Also we want to return a non-empty string only if there - # is an offset, no matter what is self.orderOfMagnitude. if there is an - # offset OTH, self.orderOfMagnitude is consulted. This behavior is verified + # is an offset, no matter what is self.orderOfMagnitude. If there _is_ an + # offset, self.orderOfMagnitude is consulted. This behavior is verified # in `test_ticker.py`. def get_offset(self): # docstring inherited From 5eec9e137e9e2e316662c0e7fd1c404aa8ae87f6 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sun, 13 Oct 2024 12:28:28 +0300 Subject: [PATCH 05/10] test_ticker.py: small cleanups after review --- lib/matplotlib/tests/test_ticker.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 09552d17702b..e10b2d827869 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1623,11 +1623,10 @@ def test_engformatter_offset_oom( ): UNIT = "eV" # Doesn't really matter here, but should be of order of magnitude ~= 1 - r = range(-5, 7) fig, ax = plt.subplots() # Use some random ugly number data_offset = 2.7149*10**oom_center - ydata = data_offset + np.array(r, dtype=float)*10**oom_noise + ydata = data_offset + np.arange(-5, 7, dtype=float)*10**oom_noise ax.plot(ydata) formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) # So that offset strings will always have the same size @@ -1661,10 +1660,7 @@ def test_engformatter_offset_oom( for tick in ticks_got: # 0 is zero on all orders of magnitudes, no matter what is # oom_noise_desired - if tick[0] == "0": - prefix_idx = 0 - else: - prefix_idx = oom_noise_desired + prefix_idx = 0 if tick[0] == "0" else oom_noise_desired assert tick.endswith(formatter.ENG_PREFIXES[prefix_idx] + UNIT) From b0da1aabcc49736f8a8c3d7220e4e293d51892e0 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sun, 13 Oct 2024 12:29:41 +0300 Subject: [PATCH 06/10] ticker.py: small cleanups after review --- lib/matplotlib/ticker.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f551c5af7153..d98b48fafd0f 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -468,10 +468,7 @@ def get_usetex(self): return self._usetex def set_usetex(self, val): - if val is None: - self._usetex = mpl.rcParams['text.usetex'] - else: - self._usetex = val + self._usetex = mpl._val_or_rc(val, 'text.usetex') usetex = property(fget=get_usetex, fset=set_usetex) @@ -1481,9 +1478,9 @@ def get_offset(self): if self._useMathText or self._usetex: if sciNotStr != '': sciNotStr = r'\times%s' % sciNotStr - s = fr'${sciNotStr}{offsetStr}$' + s = f'${sciNotStr}{offsetStr}$' else: - s = ''.join((sciNotStr, offsetStr)) + s = sciNotStr + offsetStr return self.fix_minus(s) return '' From 8e34c06317d920651e9ee4c7bdb78d09a9fdc210 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sun, 13 Oct 2024 12:51:49 +0300 Subject: [PATCH 07/10] ticker.ScalarFormatter: Fix type hints & document new attributes --- doc/users/next_whats_new/update_features.rst | 4 ++++ lib/matplotlib/ticker.pyi | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 doc/users/next_whats_new/update_features.rst diff --git a/doc/users/next_whats_new/update_features.rst b/doc/users/next_whats_new/update_features.rst new file mode 100644 index 000000000000..a655a06b9e23 --- /dev/null +++ b/doc/users/next_whats_new/update_features.rst @@ -0,0 +1,4 @@ +Miscellaneous Changes +--------------------- + +- The `matplotlib.ticker.ScalarFormatter` class has gained a new instantiating parameter ``usetex``. diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index cf08a729fa6a..f990bf53ca42 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -69,9 +69,11 @@ class ScalarFormatter(Formatter): ) -> None: ... offset: float def get_usetex(self) -> bool: ... - def set_usetex(self, val: bool | float) -> None: ... + def set_usetex(self, val: bool) -> None: ... @property def usetex(self) -> bool: ... + @usetex.setter + def usetex(self, val: bool) -> None: ... def get_useOffset(self) -> bool: ... def set_useOffset(self, val: bool | float) -> None: ... @property From a02eb9a92aace32ee560f6baac7e703d68da859c Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sat, 19 Oct 2024 21:58:11 +0300 Subject: [PATCH 08/10] engformatter_offset.rst: fix title --- doc/users/next_whats_new/engformatter_offset.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/users/next_whats_new/engformatter_offset.rst b/doc/users/next_whats_new/engformatter_offset.rst index 63c270723f23..c805e45444c5 100644 --- a/doc/users/next_whats_new/engformatter_offset.rst +++ b/doc/users/next_whats_new/engformatter_offset.rst @@ -1,5 +1,5 @@ -``matplotlib.ticker.EngFormatter`` now computes offset by default ------------------------------------------------------------------ +``matplotlib.ticker.EngFormatter`` can computes offsets now +----------------------------------------------------------- `matplotlib.ticker.EngFormatter` has gained the ability to show an offset text near the axis. Using logic shared with `matplotlib.ticker.ScalarFormatter`, it is capable of From b7c6f903150c7ea05af3d736ebac1195b049f21c Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Wed, 23 Oct 2024 09:14:03 +0300 Subject: [PATCH 09/10] engformatter related small fixes --- galleries/examples/ticks/engformatter_offset.py | 4 ++-- lib/matplotlib/tests/test_ticker.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/galleries/examples/ticks/engformatter_offset.py b/galleries/examples/ticks/engformatter_offset.py index 8cbc553e70e8..7da2d45a7942 100644 --- a/galleries/examples/ticks/engformatter_offset.py +++ b/galleries/examples/ticks/engformatter_offset.py @@ -27,7 +27,7 @@ unit=UNIT )) size = 100 -measurement = np.full(size, 1)*1e9 -noise = np.random.uniform(low=-2e3, high=2e3, size=(size)) +measurement = np.full(size, 1e9) +noise = np.random.uniform(low=-2e3, high=2e3, size=size) ax.plot(measurement + noise) plt.show() diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index e10b2d827869..871469365a0e 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1622,7 +1622,6 @@ def test_engformatter_offset_oom( oom_noise_desired ): UNIT = "eV" - # Doesn't really matter here, but should be of order of magnitude ~= 1 fig, ax = plt.subplots() # Use some random ugly number data_offset = 2.7149*10**oom_center @@ -1641,9 +1640,6 @@ def test_engformatter_offset_oom( # only makes sure that offset text and the ticks gets the correct unit # prefixes and the ticks. if formatter.offset: - # These prefix_ variables are used only once, so we could have inlined - # them all, but it is more comfortable in case of tests breakages to - # view their values with pytest --showlocals. prefix_noise_got = offset_got[2] prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] prefix_center_got = offset_got[-1-len(UNIT)] From b2c35faa6f2a8f946369d70829870d530d2c07de Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Wed, 23 Oct 2024 09:39:20 +0300 Subject: [PATCH 10/10] test_engformatter_offset_oom: parametrize center & noise directly --- lib/matplotlib/tests/test_ticker.py | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 871469365a0e..77c0e917df8a 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1592,40 +1592,38 @@ def test_engformatter_usetex_useMathText(): @pytest.mark.parametrize( - 'oom_center, oom_noise, oom_center_desired, oom_noise_desired', [ - (11, 1, 9, 0), - (13, 7, 12, 6), - (1, -2, 0, -3), - (3, -2, 3, -3), - (5, -3, 3, -3), - (2, -3, 0, -3), - # The following sets of parameters demonstrates that when oom_center-1 - # and oom_noise-2 equal a standard 3*N oom, we get that - # oom_noise_desired < oom_noise - (10, 2, 9, 3), - (1, -7, 0, -6), - (2, -4, 0, -3), - (1, -4, 0, -3), - # Tests where oom_center <= oom_noise, those are probably covered by the - # part where formatter.offset != 0 - (4, 4, 0, 3), - (1, 4, 0, 3), - (1, 3, 0, 3), - (1, 2, 0, 0), - (1, 1, 0, 0), + 'data_offset, noise, oom_center_desired, oom_noise_desired', [ + (271_490_000_000.0, 10, 9, 0), + (27_149_000_000_000.0, 10_000_000, 12, 6), + (27.149, 0.01, 0, -3), + (2_714.9, 0.01, 3, -3), + (271_490.0, 0.001, 3, -3), + (271.49, 0.001, 0, -3), + # The following sets of parameters demonstrates that when + # oom(data_offset)-1 and oom(noise)-2 equal a standard 3*N oom, we get + # that oom_noise_desired < oom(noise) + (27_149_000_000.0, 100, 9, +3), + (27.149, 1e-07, 0, -6), + (271.49, 0.0001, 0, -3), + (27.149, 0.0001, 0, -3), + # Tests where oom(data_offset) <= oom(noise), those are probably + # covered by the part where formatter.offset != 0 + (27_149.0, 10_000, 0, 3), + (27.149, 10_000, 0, 3), + (27.149, 1_000, 0, 3), + (27.149, 100, 0, 0), + (27.149, 10, 0, 0), ] ) def test_engformatter_offset_oom( - oom_center, - oom_noise, + data_offset, + noise, oom_center_desired, oom_noise_desired ): UNIT = "eV" fig, ax = plt.subplots() - # Use some random ugly number - data_offset = 2.7149*10**oom_center - ydata = data_offset + np.arange(-5, 7, dtype=float)*10**oom_noise + ydata = data_offset + np.arange(-5, 7, dtype=float)*noise ax.plot(ydata) formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) # So that offset strings will always have the same size