From 3694152533988eca9343cb00030f45b4cb37f57d Mon Sep 17 00:00:00 2001 From: Christian Aurich Date: Wed, 9 Sep 2026 21:07:20 -0300 Subject: [PATCH] FIX: make EngFormatter respect axes.formatter.use_locale rcParam EngFormatter subclasses ScalarFormatter, but the inherited locale machinery is reachable only in the offset path. The normal engineering formatting path both forces the flag off at construction, by passing useLocale=False to super().__init__, and bypasses locale-aware formatting, by interpolating the mantissa directly. The rcParam therefore had no effect, and set_useLocale flipped a flag that format_data never consulted. Pass useLocale through to ScalarFormatter and add the matching keyword argument, so that the rcParam applies and an explicit value overrides it. When locale formatting is on, the mantissa goes through locale.format_string, and the separators it introduces are escaped for mathtext the same way ScalarFormatter already escapes them. --- .../next_whats_new/engformatter_locale.rst | 17 ++++++++ lib/matplotlib/tests/test_ticker.py | 39 +++++++++++++++++++ lib/matplotlib/ticker.py | 22 +++++++++-- lib/matplotlib/ticker.pyi | 1 + 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 doc/release/next_whats_new/engformatter_locale.rst diff --git a/doc/release/next_whats_new/engformatter_locale.rst b/doc/release/next_whats_new/engformatter_locale.rst new file mode 100644 index 000000000000..69252c74bb2f --- /dev/null +++ b/doc/release/next_whats_new/engformatter_locale.rst @@ -0,0 +1,17 @@ +``EngFormatter`` now respects ``axes.formatter.use_locale`` +----------------------------------------------------------- + +`~matplotlib.ticker.EngFormatter` now formats the decimal separator according to +the current locale, like `~matplotlib.ticker.ScalarFormatter` already did. This +is controlled by the :rc:`axes.formatter.use_locale` rcParam, or by the new +*useLocale* keyword argument: + +.. code-block:: python + + import locale + from matplotlib.ticker import EngFormatter + + locale.setlocale(locale.LC_ALL, "de_DE.UTF-8") + + ax.yaxis.set_major_formatter(EngFormatter(places=2, useLocale=True)) + # tick labels now read "555,10 m" instead of "555.10 m" diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 189949638100..2a1684761aa0 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1810,6 +1810,45 @@ def test_locale_comma(): pytest.skip(skip_msg) +def _impl_engformatter_locale(): + try: + locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8') + except locale.Error: + print('SKIP: Locale de_DE.UTF-8 is not supported on this machine') + return + # EngFormatter is a ScalarFormatter subclass, so it must honour the + # axes.formatter.use_locale rcParam like its parent does. + with mpl.rc_context(rc={'axes.formatter.use_locale': True}): + fmt = mticker.EngFormatter(places=2) + assert fmt.get_useLocale() + assert fmt.format_data(0.5551) == '555,10 m' + # An explicit keyword argument overrides the rcParam. + fmt = mticker.EngFormatter(places=2, useLocale=False) + assert not fmt.get_useLocale() + assert fmt.format_data(1234.5) == '1.23 k' + fmt = mticker.EngFormatter(places=2, useLocale=True) + assert fmt.get_useLocale() + assert fmt.format_data(1234.5) == '1,23 k' + # The inherited setter also reaches the engineering formatting path. + fmt = mticker.EngFormatter(places=2, useLocale=False) + fmt.set_useLocale(True) + assert fmt.format_data(1234.5) == '1,23 k' + # Separators are escaped for mathtext, as ScalarFormatter does. + fmt = mticker.EngFormatter(places=2, useLocale=True, useMathText=True) + assert fmt.format_data(0.5551) == '$555{,}10$ m' + + +def test_engformatter_locale(): + proc = mpl.testing.subprocess_run_helper( + _impl_engformatter_locale, timeout=60, extra_env={'MPLBACKEND': 'Agg'}) + skip_msg = next((line[len('SKIP:'):].strip() + for line in proc.stdout.splitlines() + if line.startswith('SKIP:')), + '') + if skip_msg: + pytest.skip(skip_msg) + + def test_majformatter_type(): fig, ax = plt.subplots() with pytest.raises(TypeError): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 391e24dd2a06..2116ce8c38a5 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1433,7 +1433,7 @@ class EngFormatter(ScalarFormatter): } def __init__(self, unit="", places=None, sep=" ", *, usetex=None, - useMathText=None, useOffset=False): + useMathText=None, useOffset=False, useLocale=None): r""" Parameters ---------- @@ -1476,6 +1476,12 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, 3 digits. See also `.set_useOffset`. .. versionadded:: 3.10 + + useLocale : bool, default: :rc:`axes.formatter.use_locale`. + Whether to use locale settings for decimal sign and positive sign. + See `.set_useLocale`. + + .. versionadded:: 3.12 """ self.unit = unit self.places = places @@ -1483,7 +1489,7 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, super().__init__( useOffset=useOffset, useMathText=useMathText, - useLocale=False, + useLocale=useLocale, usetex=usetex, ) @@ -1598,10 +1604,18 @@ def format_data(self, value): suffix = f"{self.sep}{unit_prefix}{self.unit}" else: suffix = "" + if self._useLocale: + mant_str = locale.format_string(f"%{fmt}", (mant,), True) + if self._useMathText: + # Escape the separators introduced by locale.format_string, so + # that mathtext does not apply punctuation spacing to them. + mant_str = mant_str.replace(",", "{,}") + else: + mant_str = f"{mant:{fmt}}" if self._usetex or self._useMathText: - return f"${mant:{fmt}}${suffix}" + return f"${mant_str}${suffix}" else: - return f"{mant:{fmt}}{suffix}" + return f"{mant_str}{suffix}" class PercentFormatter(Formatter): diff --git a/lib/matplotlib/ticker.pyi b/lib/matplotlib/ticker.pyi index 8625c3ebd3fd..097938a28152 100644 --- a/lib/matplotlib/ticker.pyi +++ b/lib/matplotlib/ticker.pyi @@ -150,6 +150,7 @@ class EngFormatter(ScalarFormatter): usetex: bool | None = ..., useMathText: bool | None = ..., useOffset: bool | float | None = ..., + useLocale: bool | None = ..., ) -> None: ... def format_eng(self, num: float) -> str: ...