8000 FIX: correctly handle large arcs · matplotlib/matplotlib@98ab2a0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 98ab2a0

Browse files
committed
FIX: correctly handle large arcs
The draw method of mpatches.Arc has two paths: - if the arc is "small" compared to its size in the rendered image then render the whole arc and let clipping do it's thing - if the arc is "big" compared to its size on the screen then sort out where the circle intersects the axes boundary and only draw that part of it This makes several changes to the Arc draw method: - make sure that we keep angles in [0, 360) range - only go through the angle stretching code if we need to (to avoid numerical instability of angles not round-tripping with scale=1) - compute length, not offset from origin of width / height and use the correct transform. Previously we were effectively squaring the height and width Tests: - Adjusted an existing test image to use this failing case and to exercise both code paths. - Added a test function of ensuring we can draw a big arc in each quadrant
1 parent efda565 commit 98ab2a0

File tree

4 files changed

+626
-31
lines changed

4 files changed

+626
-31
lines changed

lib/matplotlib/patches.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1659,14 +1659,36 @@ def theta_stretch(theta, scale):
16591659
theta = np.deg2rad(theta)
16601660
x = np.cos(theta)
16611661
y = np.sin(theta)
1662-
return np.rad2deg(np.arctan2(scale * y, x))
1663-
theta1 = theta_stretch(self.theta1, width / height)
1664-
theta2 = theta_stretch(self.theta2, width / height)
1665-
1666-
# Get width and height in pixels
1667-
width, height = self.get_transform().transform((width, height))
1662+
stheta = np.rad2deg(np.arctan2(scale * y, x))
1663+
# arctan2 has the range [-pi, pi], we expect [0, 2*pi]
1664+
1665+
return (stheta + 360) % 360
1666+
1667+
theta1 = self.theta1
1668+
theta2 = self.theta2
1669+
1670+
if (
1671+
# if we need to stretch the angles because we are distorted
1672+
width != height
1673+
# and we are not doing a full circle
1674+
and not (theta1 != theta2 and theta1 % 360 == theta2 % 360)
1675+
):
1676+
theta1 = theta_stretch(self.theta1, width / height)
1677+
theta2 = theta_stretch(self.theta2, width / height)
1678+
1679+
# Get width and height in pixels we need to use
1680+
# `self.get_data_transform` rather than `self.get_transform`
1681+
# because we want the transform from from dataspace to the
1682+
# screen space to estimate how big the arc will be in physical
1683+
# units when rendered (the transform that we get via
1684+
# `self.get_transform()` goes from an idealized unit-radius
1685+
# space to screen space).
1686+
data_to_screen_trans = self.get_data_transform()
1687+
pwidth, pheight = (data_to_screen_trans.transform((width, height)) -
1688+
data_to_screen_trans.transform((0, 0)))
16681689
inv_error = (1.0 / 1.89818e-6) * 0.5
1669-
if width < inv_error and height < inv_error:
1690+
1691+
if pwidth < inv_error and pheight < inv_error:
16701692
self._path = Path.arc(theta1, theta2)
16711693
return Patch.draw(self, renderer)
16721694

@@ -1700,29 +1722,32 @@ def segment_circle_intersect(x0, y0, x1, y1):
17001722
y0e, y1e = y0, y1
17011723
xys = line_circle_intersect(x0, y0, x1, y1)
17021724
xs, ys = xys.T
1703-
return xys[(x0e - epsilon < xs) & (xs < x1e + epsilon)
1704-
& (y0e - epsilon < ys) & (ys < y1e + epsilon)]
1725+
return xys[
1726+
(x0e - epsilon < xs) & (xs < x1e + epsilon)
1727+
& (y0e - epsilon < ys) & (ys < y1e + epsilon)
1728+
]
17051729

17061730
# Transforms the axes box_path so that it is relative to the unit
17071731
# circle in the same way that it is relative to the desired ellipse.
1708-
box_path = Path.unit_rectangle()
17091732
box_path_transform = (transforms.BboxTransformTo(self.axes.bbox)
17101733
+ self.get_transform().inverted())
1711-
box_path = box_path.transformed(box_path_transform)
1734+
box_path = Path.unit_rectangle().transformed(box_path_transform)
17121735

17131736
thetas = set()
17141737
# For each of the point pairs, there is a line segment
17151738
for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
17161739
xy = segment_circle_intersect(*p0, *p1)
17171740
x, y = xy.T
1718-
theta = np.rad2deg(np.arctan2(y, x))
1741+
# arctan2 return [-pi, pi), the rest of our angles are in
1742+
# [0, 360], adjust as needed.
1743+
theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360
17191744
thetas.update(theta[(theta1 < theta) & (theta < theta2)])
17201745
thetas = sorted(thetas) + [theta2]
1721-
17221746
last_theta = theta1
17231747
theta1_rad = np.deg2rad(theta1)
1724-
inside = box_path.contains_point((np.cos(theta1_rad),
1725-
np.sin(theta1_rad)))
1748+
inside = box_path.contains_point(
1749+
(np.cos(theta1_rad), np.sin(theta1_rad))
1750+
)
17261751

17271752
# save original path
17281753
path_original = self._path

0 commit comments

Comments
 (0)
0