Skip to content

Fix bbox of polar Axes #29051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
200 changes: 127 additions & 73 deletions lib/matplotlib/projections/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,30 @@
axis_name = 'theta' #: Read-only name identifying the axis.
_tick_class = ThetaTick

def _update_label_position(self, renderer):
"""
Update the label position based on the bounding box enclosing
all the ticklabels and axis spine
"""
if not self._autolabelpos:
return

Check warning on line 435 in lib/matplotlib/projections/polar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/projections/polar.py#L435

Added line #L435 was not covered by tests

# get bounding boxes for this axis and any siblings
# that have been set by `fig.align_xlabels()`
xbboxes, xbboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
ybboxes, ybboxes2 = self.axes.yaxis._get_tick_boxes_siblings(renderer=renderer)
# Union with extents of the bottom spine if present, of the axes otherwise.
bbox = mtransforms.Bbox.union([
*xbboxes, *xbboxes2, *ybboxes, *ybboxes2,
self.axes.spines.get(self.label_position, self.axes).get_window_extent()])

x, y = self.label.get_position()
if self.label_position == 'bottom':
y = bbox.y0 - self.labelpad * self.get_figure(root=True).dpi / 72
else:
y = bbox.y1 + self.labelpad * self.get_figure(root=True).dpi / 72

Check warning on line 450 in lib/matplotlib/projections/polar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/projections/polar.py#L450

Added line #L450 was not covered by tests
self.label.set_position((x, y))

def _wrap_locator_formatter(self):
self.set_major_locator(ThetaLocator(self.get_major_locator()))
self.set_major_formatter(ThetaFormatter())
Expand Down Expand Up @@ -719,6 +743,30 @@
super().__init__(*args, **kwargs)
self.sticky_edges.y.append(0)

def _update_label_position(self, renderer):
"""
Update the label position based on the bounding box enclosing
all the ticklabels and axis spine
"""
if not self._autolabelpos:
return

Check warning on line 752 in lib/matplotlib/projections/polar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/projections/polar.py#L752

Added line #L752 was not covered by tests

# get bounding boxes for this axis and any siblings
# that have been set by `fig.align_xlabels()`
xbboxes, xbboxes2 = self._get_tick_boxes_siblings(renderer=renderer)
ybboxes, ybboxes2 = self.axes.xaxis._get_tick_boxes_siblings(renderer=renderer)
# Union with extents of the linked spine if present, of the axes otherwise.
bbox = mtransforms.Bbox.union([
*xbboxes, *xbboxes2, *ybboxes, *ybboxes2,
self.axes.spines.get(self.label_position, self.axes).get_window_extent()])

x, y = self.label.get_position()
if self.label_position == 'left':
x = bbox.x0 - self.labelpad * self.get_figure(root=True).dpi / 72
else:
x = bbox.x1 + self.labelpad * self.get_figure(root=True).dpi / 72

Check warning on line 767 in lib/matplotlib/projections/polar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/projections/polar.py#L767

Added line #L767 was not covered by tests
self.label.set_position((x, y))

def _wrap_locator_formatter(self):
self.set_major_locator(RadialLocator(self.get_major_locator(),
self.axes))
Expand Down Expand Up @@ -757,6 +805,8 @@
"""
Transform (theta, r) wedge Bbox into Axes bounding box.

Additionally, this class will update the Axes patch, if set by `_set_wedge`.

Parameters
----------
center : (float, float)
Expand All @@ -771,6 +821,7 @@
self._center = center
self._viewLim = viewLim
self._originLim = originLim
self._wedge = None
self.set_children(viewLim, originLim)

__str__ = mtransforms._make_str_method("_center", "_viewLim", "_originLim")
Expand All @@ -793,21 +844,32 @@
width = min(points[1, 1] - points[0, 1], 0.5)

# Generate bounding box for wedge.
wedge = mpatches.Wedge(self._center, points[1, 1],
points[0, 0], points[1, 0],
width=width)
self.update_from_path(wedge.get_path())

# Ensure equal aspect ratio.
w, h = self._points[1] - self._points[0]
deltah = max(w - h, 0) / 2
deltaw = max(h - w, 0) / 2
self._points += np.array([[-deltaw, -deltah], [deltaw, deltah]])
if self._wedge is None:
# A PolarAxes subclass may not generate a Wedge as Axes patch and call
# _WedgeBbox._set_wedge, so use a temporary instance to calculate the
# bounds instead.
wedge = mpatches.Wedge(self._center, points[1, 1],

Check warning on line 851 in lib/matplotlib/projections/polar.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/projections/polar.py#L851

Added line #L851 was not covered by tests
points[0, 0], points[1, 0],
width=width)
else:
# Update the owning Axes' patch.
wedge = self._wedge
wedge.set_center(self._center)
wedge.set_theta1(points[0, 0])
wedge.set_theta2(points[1, 0])
wedge.set_radius(points[1, 1])
wedge.set_width(width)
self.update_from_path(wedge.get_path(), ignore=True)

self._invalid = 0

return self._points

def _set_wedge(self, wedge):
"""Set the wedge patch to update when the transform changes."""
_api.check_isinstance(mpatches.Wedge, wedge=wedge)
self._wedge = wedge


class PolarAxes(Axes):
"""
Expand Down Expand Up @@ -859,8 +921,11 @@
self.spines['polar'].register_axis(self.yaxis)

def _set_lim_and_transforms(self):
# A view limit where the minimum radius can be locked if the user
# specifies an alternate origin.
# self.viewLim is set by the superclass and contains (θ, r) as its (x, y)
# components.

# This is a view limit (still in (θ, r) space) where the minimum radius can be
# locked if the user specifies an alternate origin.
self._originViewLim = mtransforms.LockableBbox(self.viewLim)

# Handle angular offset and direction.
Expand All @@ -869,30 +934,26 @@
self._theta_offset = mtransforms.Affine2D() \
.translate(self._default_theta_offset, 0.0)
self.transShift = self._direction + self._theta_offset
# A view limit shifted to the correct location after accounting for
# orientation and offset.
self._realViewLim = mtransforms.TransformedBbox(self.viewLim,
self.transShift)

# Transforms the x and y axis separately by a scale factor
# It is assumed that this part will have non-linear components
self.transScale = mtransforms.TransformWrapper(
mtransforms.IdentityTransform())

# Scale view limit into a bbox around the selected wedge. This may be
# smaller than the usual unit axes rectangle if not plotting the full
# circle.
self.axesLim = _WedgeBbox((0.5, 0.5),
self._realViewLim, self._originViewLim)

# Scale the wedge to fill the axes.
# This is a view limit in (θ, r) shifted to the correct location after
# accounting for θ orientation and offset.
self._realViewLim = mtransforms.TransformedBbox(self.viewLim, self.transShift)

# Transforms the θ and r axis separately by a scale factor. It is assumed that
# this part will have the non-linear components.
self.transScale = mtransforms.TransformWrapper(mtransforms.IdentityTransform())

# Scale view limit into a bbox around the selected wedge. This may be smaller
# than the usual unit axes rectangle if not plotting the full circle.
self.axesLim = _WedgeBbox((0.5, 0.5), self._realViewLim, self._originViewLim)

# Scale the wedge to fill the Axes unit space.
self.transWedge = mtransforms.BboxTransformFrom(self.axesLim)

# Scale the axes to fill the figure.
# Scale the Axes unit space to fill the Axes actual position.
self.transAxes = mtransforms.BboxTransformTo(self.bbox)

# A (possibly non-linear) projection on the (already scaled)
# data. This one is aware of rmin
# A (possibly non-linear) projection on the (already scaled) data. This one is
# aware of rmin.
self.transProjection = self.PolarTransform(
self,
apply_theta_transforms=False,
Expand All @@ -901,52 +962,48 @@
# Add dependency on rorigin.
self.transProjection.set_children(self._originViewLim)

# An affine transformation on the data, generally to limit the
# range of the axes
# An affine transformation on the data, generally to limit the range of the axes
self.transProjectionAffine = self.PolarAffine(self.transScale,
self._originViewLim)

# The complete data transformation stack -- from data all the
# way to display coordinates
#
# 1. Remove any radial axis scaling (e.g. log scaling)
# 2. Shift data in the theta direction
# 3. Project the data from polar to cartesian values
# (with the origin in the same place)
# 4. Scale and translate the cartesian values to Axes coordinates
# (here the origin is moved to the lower left of the Axes)
# 5. Move and scale to fill the Axes
# 6. Convert from Axes coordinates to Figure coordinates
# The complete data transformation stack -- from data all the way to display
# coordinates.
self.transData = (
# 1. Remove any radial axis scaling (e.g. log scaling).
self.transScale +
# 2. Shift data in the θ direction.
self.transShift +
# 3. Project the data from polar to cartesian values (with the origin in the
# same place).
self.transProjection +
(
# 4. Scale and translate the cartesian values to Axes coordinates (here
# the origin is moved to the lower left of the Axes).
self.transProjectionAffine +
# 5. Move and scale to fill the Axes.
self.transWedge +
# 6. Convert from Axes coordinates to Figure coordinates.
self.transAxes
)
)

# This is the transform for theta-axis ticks. It is
# equivalent to transData, except it always puts r == 0.0 and r == 1.0
# at the edge of the axis circles.
# This is the transform for θ-axis ticks. It is equivalent to transData, except
# it always puts r == 0.0 and r == 1.0 at the edge of the axis circles.
self._xaxis_transform = (
mtransforms.blended_transform_factory(
mtransforms.IdentityTransform(),
mtransforms.BboxTransformTo(self.viewLim)) +
self.transData)
# The theta labels are flipped along the radius, so that text 1 is on
# the outside by default. This should work the same as before.
# The θ labels are flipped along the radius, so that text 1 is on the outside by
# default. This should work the same as before.
flipr_transform = mtransforms.Affine2D() \
.translate(0.0, -0.5) \
.scale(1.0, -1.0) \
.translate(0.0, 0.5)
self._xaxis_text_transform = flipr_transform + self._xaxis_transform

# This is the transform for r-axis ticks. It scales the theta
# axis so the gridlines from 0.0 to 1.0, now go from thetamin to
# thetamax.
# This is the transform for r-axis ticks. It scales the θ-axis so the gridlines
# from 0.0 to 1.0, now go from thetamin to thetamax.
self._yaxis_transform = (
mtransforms.blended_transform_factory(
mtransforms.BboxTransformTo(self.viewLim),
Expand Down Expand Up @@ -999,31 +1056,16 @@

def draw(self, renderer):
self._unstale_viewLim()
thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx)
if thetamin > thetamax:
thetamin, thetamax = thetamax, thetamin
rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) *
self.get_rsign())
self.axesLim.get_points() # Unstale bbox and Axes patch.
self.set_aspect(self.axesLim.height / self.axesLim.width)
if isinstance(self.patch, mpatches.Wedge):
# Backwards-compatibility: Any subclassed Axes might override the
# patch to not be the Wedge that PolarAxes uses.
center = self.transWedge.transform((0.5, 0.5))
self.patch.set_center(center)
self.patch.set_theta1(thetamin)
self.patch.set_theta2(thetamax)

edge, _ = self.transWedge.transform((1, 0))
radius = edge - center[0]
width = min(radius * (rmax - rmin) / rmax, radius)
self.patch.set_radius(radius)
self.patch.set_width(width)

inner_width = radius - width
inner = self.spines.get('inner', None)
if inner:
inner.set_visible(inner_width != 0.0)
inner.set_visible(self.patch.r != self.patch.width)

visible = not _is_full_circle_deg(thetamin, thetamax)
visible = not _is_full_circle_rad(*self._realViewLim.intervalx)
# For backwards compatibility, any subclassed Axes might override the
# spines to not include start/end that PolarAxes uses.
start = self.spines.get('start', None)
Expand All @@ -1043,8 +1085,20 @@

super().draw(renderer)

def _wedge_get_patch_transform(self):
# See _gen_axes_patch for the use of this function. It's not a lambda or nested
# function to not break pickling.
return self.transWedge

def _gen_axes_patch(self):
return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0)
wedge = mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0)
self.axesLim._set_wedge(wedge)
# The caller of this function will set the wedge's transform directly to
# `self.transAxes`, but `self.axesLim` will update to a pre-`self.transWedge`
# coordinate space, so override the patch transform (which is otherwise always
# an identity transform) to get the wedge in the right coordinate space.
wedge.get_patch_transform = self._wedge_get_patch_transform
return wedge

def _gen_axes_spines(self):
spines = {
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading