From d420964949afb7a0d443752193775297c9aae0af Mon Sep 17 00:00:00 2001 From: Sreekant Baheti <60787859+Sreekant13@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:51:26 -0700 Subject: [PATCH] Don't require an axis in PercentFormatter with explicit decimals PercentFormatter.__call__ always read self.axis.get_view_interval(), but that display range is only used to choose the number of decimals automatically, i.e. when decimals is None. With decimals set explicitly, calling the formatter while it was not attached to an axis raised AttributeError even though the axis value was never needed. Only read the axis in the auto-decimals case, and add a regression test. --- lib/matplotlib/tests/test_ticker.py | 8 ++++++++ lib/matplotlib/ticker.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index d2bc7bc7a281..189949638100 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1764,6 +1764,14 @@ def test_latex(self, is_latex, usetex, expected): with mpl.rc_context(rc={'text.usetex': usetex}): assert fmt.format_pct(50, 100) == expected + def test_call_without_axis(self): + # With explicit decimals the axis view interval is not needed, so the + # formatter should format a value even when it is not attached to an + # axis, instead of raising an AttributeError. + with mpl.rc_context(rc={'text.usetex': False}): + assert mticker.PercentFormatter(xmax=1.0, decimals=1)(0.5) == '50.0%' + assert mticker.PercentFormatter(xmax=100, decimals=0)(50) == '50%' + def _impl_locale_comma(): try: diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c82cd3bd0419..391e24dd2a06 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1638,8 +1638,15 @@ def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False): def __call__(self, x, pos=None): """Format the tick as a percentage with the appropriate scaling.""" - ax_min, ax_max = self.axis.get_view_interval() - display_range = abs(ax_max - ax_min) + if self.decimals is None: + # The display range of the axis is only needed to pick the number + # of decimals automatically; when ``decimals`` is set explicitly we + # avoid touching ``self.axis`` so the formatter also works when it + # is not attached to an axis. + ax_min, ax_max = self.axis.get_view_interval() + display_range = abs(ax_max - ax_min) + else: + display_range = None return self.fix_minus(self.format_pct(x, display_range)) def format_pct(self, x, display_range):