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: ...