Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions lib/matplotlib/projections/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,16 +254,18 @@ 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):
class ChoiceLocator(mticker.Locator):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add docstrings.

def __init__(self, base=None, choices=None):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should base be optional here, considering for ThetaLocator it was not.
  2. I think we don't need to take choices as a parameter. Or is there a usecase to be able to control this?

if choices is None:
choices = [
np.arange(-360, 360, 30.0),
Comment thread
r3kste marked this conversation as resolved.
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)

Expand All @@ -273,10 +275,45 @@ def 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 = 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:
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())
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this check up

ticks = self.choices[-1]
ticks = ticks[(ticks >= vmin - tol) & (ticks <= vmax + tol)]
return ticks
Comment on lines 283 to +316
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here is pretty convoluted, but seems to be correct. Can you add comments for each case?


def view_limits(self, vmin, vmax):
vmin, vmax = np.rad2deg((vmin, vmax))
Expand Down Expand Up @@ -387,7 +424,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
Expand All @@ -406,7 +443,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):
Expand All @@ -421,6 +457,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):
"""
Expand Down Expand Up @@ -1510,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
9 changes: 5 additions & 4 deletions lib/matplotlib/projections/polar.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ 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
class ChoiceLocator(mticker.Locator):
choices: list[np.ndarray]
base: mticker.Locator | None
axis: _AxisWrapper | None
def __init__(self, base: mticker.Locator) -> 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: ...
Expand Down Expand Up @@ -84,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
Expand Down