From 8aa56242b81220c67f2dd9b8f93f59663375b3fc Mon Sep 17 00:00:00 2001 From: Sreekanth-M8 Date: Wed, 11 Feb 2026 21:53:23 +0530 Subject: [PATCH 1/2] added ChoiceLocator --- lib/matplotlib/projections/polar.py | 79 +++++++++++++++++++++++++++- lib/matplotlib/projections/polar.pyi | 6 +++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 8b0a01f556e3..d3e925ec639e 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -283,6 +283,48 @@ def view_limits(self, vmin, vmax): return np.deg2rad(self.base.view_limits(vmin, vmax)) +class ChoiceLocator(mticker.Locator): + def __init__(self, base=None, choices=None): + if choices is None: + choices = [ + np.arange(-360, 360, 30.0), + np.arange(-360, 360, 45.0), + np.arange(-360, 360, 60.0), + np.arange(-360, 360, 90.0), + ] + if base is None: + base = mticker.AutoLocator() + self.choices = choices + self.base = base + self.axis = self.base.axis = _AxisWrapper(self.base.axis) + + def set_axis(self, axis): + self.axis = _AxisWrapper(axis) + self.base.set_axis(self.axis) + + def __call__(self): + lim = self.axis.get_view_interval() + vmin = min(lim[0], lim[1]) + vmax = max(lim[0], lim[1]) + max_ticks = self.axis.get_tick_space() + tol = 1e-12 + if (vmax - vmin > 60): + for ticks in self.choices: + in_range = (ticks >= vmin - tol) & (ticks <= vmax + tol) + ticks = ticks[in_range] + if len(ticks) <= max_ticks: + return np.deg2rad(ticks) + else: + return np.deg2rad(self.base()) + ticks = self.choices[-1] + ticks = ticks[(ticks >= vmin - tol) & (ticks <= vmax + tol)] + return ticks + + def view_limits(self, vmin, vmax): + vmin, vmax = np.rad2deg((vmin, vmax)) + return np.deg2rad(self.base.view_limits(vmin, vmax)) + + class ThetaTick(maxis.XTick): """ A theta-axis tick. @@ -387,7 +429,7 @@ class ThetaAxis(maxis.XAxis): _tick_class = ThetaTick def _wrap_locator_formatter(self): - self.set_major_locator(ThetaLocator(self.get_major_locator())) + self.set_major_locator(ChoiceLocator(self.get_major_locator())) self.set_major_formatter(ThetaFormatter()) self.isDefault_majloc = True self.isDefault_majfmt = True @@ -406,7 +448,6 @@ def _set_scale(self, value, **kwargs): # LinearScale.set_default_locators_and_formatters just set the major # locator to be an AutoLocator, so we customize it here to have ticks # at sensible degree multiples. - self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10]) self._wrap_locator_formatter() def _copy_tick_props(self, src, dest): @@ -421,6 +462,23 @@ def _copy_tick_props(self, src, dest): trans = dest._get_text2_transform()[0] dest.label2.set_transform(trans + dest._text2_translate) + def get_tick_space(self): + ends = mtransforms.Bbox.unit().transformed( + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) + + thetamin, thetamax = self.axes._realViewLim.intervalx + radius = min(ends.height, ends.width) * 72 + if abs(thetamax - thetamin) > np.pi / 2: + radius /=2 + angle = abs(thetamax - thetamin) + arc_length = radius * angle + size = self._get_tick_label_size('x') * 3 + if size > 0: + return int(np.floor(arc_length / size)) + else: + return 2**31 - 1 + + class RadialLocator(mticker.Locator): """ @@ -690,6 +748,23 @@ def clear(self): super().clear() self.set_ticks_position('none') + def get_tick_space(self): + ends = mtransforms.Bbox.unit().transformed( + self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) + thetamin, thetamax = self.axes._realViewLim.intervalx + rmin, rmax = self.axes._realViewLim.intervaly + rorigin = self.axes.get_rorigin() + radius = min(ends.height, ends.width) * 72 + actual_ratio = rmax / (rmax - rorigin) + if abs(thetamax - thetamin) > np.pi / 2: + radius /=2 + # Having a spacing of at least 3 just looks good + size = self._get_tick_label_size('y') * 3 + if size > 0: + return int(np.floor(radius * actual_ratio / size)) + else: + return 2**31 - 1 + def _is_full_circle_deg(thetamin, thetamax): """ diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index fc1d508579b5..69d3b24ecc34 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -53,6 +53,12 @@ class ThetaLocator(mticker.Locator): axis: _AxisWrapper | None def __init__(self, base: mticker.Locator) -> None: ... +class ChoiceLocator(mticker.Locator): + choices: list[np.ndarray] + base: mticker.Locator | None + axis: _AxisWrapper | None + def __init__(self, base: mticker.Locator | None = ..., choices: list[np.ndarray] | None = ...) -> None: ... + class ThetaTick(maxis.XTick): def __init__(self, axes: PolarAxes, *args, **kwargs) -> None: ... From f64510df6c8157d2a7365e4d3aa309c9685f4df1 Mon Sep 17 00:00:00 2001 From: Sreekanth-M8 Date: Sun, 1 Mar 2026 18:03:29 +0530 Subject: [PATCH 2/2] changes --- lib/matplotlib/projections/polar.py | 76 ++++++++++------------------ lib/matplotlib/projections/polar.pyi | 7 +-- 2 files changed, 28 insertions(+), 55 deletions(-) diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index d3e925ec639e..6d20618048a2 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -254,35 +254,6 @@ def get_tick_space(self): return self._axis.get_tick_space() -class ThetaLocator(mticker.Locator): - """ - Used to locate theta ticks. - - This will work the same as the base locator except in the case that the - view spans the entire circle. In such cases, the previously used default - locations of every 45 degrees are returned. - """ - - def __init__(self, base): - self.base = base - self.axis = self.base.axis = _AxisWrapper(self.base.axis) - - def set_axis(self, axis): - self.axis = _AxisWrapper(axis) - self.base.set_axis(self.axis) - - def __call__(self): - lim = self.axis.get_view_interval() - if _is_full_circle_deg(lim[0], lim[1]): - return np.deg2rad(min(lim)) + np.arange(8) * 2 * np.pi / 8 - else: - return np.deg2rad(self.base()) - - def view_limits(self, vmin, vmax): - vmin, vmax = np.rad2deg((vmin, vmax)) - return np.deg2rad(self.base.view_limits(vmin, vmax)) - - class ChoiceLocator(mticker.Locator): def __init__(self, base=None, choices=None): if choices is None: @@ -306,14 +277,38 @@ def __call__(self): lim = self.axis.get_view_interval() vmin = min(lim[0], lim[1]) vmax = max(lim[0], lim[1]) - max_ticks = self.axis.get_tick_space() + max_ticks = max(self.axis.get_tick_space() + 1, 2) + tick_interval = (vmax -vmin) / (max_ticks - 1) tol = 1e-12 + if _is_full_circle_deg(lim[0], lim[1]): + return np.deg2rad(min(lim)) + np.arange(8) * 2 * np.pi / 8 if (vmax - vmin > 60): for ticks in self.choices: in_range = (ticks >= vmin - tol) & (ticks <= vmax + tol) ticks = ticks[in_range] - if len(ticks) <= max_ticks: + if len(ticks) > max_ticks: + continue + if len(ticks) == max_ticks: + if vmin != ticks[0]: + ticks[0] = vmin + if vmax != ticks[-1]: + ticks[-1] = vmax return np.deg2rad(ticks) + if len(ticks) < max_ticks: + if vmin != ticks[0]: + if abs(ticks[0] - vmin) >= tick_interval: + ticks = np.concatenate(([vmin], ticks)) + else: + ticks[0] = vmin + if vmax != ticks[-1]: + if len(ticks) < max_ticks: + if abs(vmax - ticks[-1]) >= tick_interval: + ticks = np.concatenate((ticks, [vmax])) + else: + ticks[-1] = vmax + else: + ticks[-1] = vmax + return np.deg2rad(ticks) else: return np.deg2rad(self.base()) ticks = self.choices[-1] @@ -748,23 +743,6 @@ def clear(self): super().clear() self.set_ticks_position('none') - def get_tick_space(self): - ends = mtransforms.Bbox.unit().transformed( - self.axes.transAxes - self.get_figure(root=False).dpi_scale_trans) - thetamin, thetamax = self.axes._realViewLim.intervalx - rmin, rmax = self.axes._realViewLim.intervaly - rorigin = self.axes.get_rorigin() - radius = min(ends.height, ends.width) * 72 - actual_ratio = rmax / (rmax - rorigin) - if abs(thetamax - thetamin) > np.pi / 2: - radius /=2 - # Having a spacing of at least 3 just looks good - size = self._get_tick_label_size('y') * 3 - if size > 0: - return int(np.floor(radius * actual_ratio / size)) - else: - return 2**31 - 1 - def _is_full_circle_deg(thetamin, thetamax): """ @@ -1585,4 +1563,4 @@ def drag_pan(self, button, key, x, y): PolarAxes.InvertedPolarTransform = InvertedPolarTransform PolarAxes.ThetaFormatter = ThetaFormatter PolarAxes.RadialLocator = RadialLocator -PolarAxes.ThetaLocator = ThetaLocator +PolarAxes.ChoiceLocator = ChoiceLocator diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index 69d3b24ecc34..50d56653764f 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -48,11 +48,6 @@ class _AxisWrapper: def set_data_interval(self, vmin: float, vmax: float) -> None: ... def get_tick_space(self) -> int: ... -class ThetaLocator(mticker.Locator): - base: mticker.Locator - axis: _AxisWrapper | None - def __init__(self, base: mticker.Locator) -> None: ... - class ChoiceLocator(mticker.Locator): choices: list[np.ndarray] base: mticker.Locator | None @@ -90,7 +85,7 @@ class PolarAxes(Axes): InvertedPolarTransform: ClassVar[type] = InvertedPolarTransform ThetaFormatter: ClassVar[type] = ThetaFormatter RadialLocator: ClassVar[type] = RadialLocator - ThetaLocator: ClassVar[type] = ThetaLocator + ChoiceLocator: ClassVar[type] = ChoiceLocator name: str use_sticky_edges: bool