Skip to content

Commit 982efe1

Browse files
authored
Merge pull request #17655 from tacaswell/auto-backport-of-pr-17564-on-v3.2.x
Auto backport of pr 17564 on v3.2.x
2 parents aead584 + d67550c commit 982efe1

File tree

5 files changed

+682
-53
lines changed

5 files changed

+682
-53
lines changed

lib/matplotlib/patches.py

+65-53
Original file line numberDiff line numberDiff line change
@@ -1592,14 +1592,12 @@ def draw(self, renderer):
15921592
calculation much easier than doing rotated ellipse
15931593
intersection directly).
15941594
1595-
This uses the "line intersecting a circle" algorithm
1596-
from:
1595+
This uses the "line intersecting a circle" algorithm from:
15971596
15981597
Vince, John. *Geometry for Computer Graphics: Formulae,
15991598
Examples & Proofs.* London: Springer-Verlag, 2005.
16001599
1601-
2. The angles of each of the intersection points are
1602-
calculated.
1600+
2. The angles of each of the intersection points are calculated.
16031601
16041602
3. Proceeding counterclockwise starting in the positive
16051603
x-direction, each of the visible arc-segments between the
@@ -1609,6 +1607,8 @@ def draw(self, renderer):
16091607
"""
16101608
if not hasattr(self, 'axes'):
16111609
raise RuntimeError('Arcs can only be used in Axes instances')
1610+
if not self.get_visible():
1611+
return
16121612

16131613
self._recompute_transform()
16141614

@@ -1621,44 +1621,62 @@ def theta_stretch(theta, scale):
16211621
theta = np.deg2rad(theta)
16221622
x = np.cos(theta)
16231623
y = np.sin(theta)
1624-
return np.rad2deg(np.arctan2(scale * y, x))
1625-
theta1 = theta_stretch(self.theta1, width / height)
1626-
theta2 = theta_stretch(self.theta2, width / height)
1627-
1628-
# Get width and height in pixels
1629-
width, height = self.get_transform().transform((width, height))
1624+
stheta = np.rad2deg(np.arctan2(scale * y, x))
1625+
# arctan2 has the range [-pi, pi], we expect [0, 2*pi]
1626+
return (stheta + 360) % 360
1627+
1628+
theta1 = self.theta1
1629+
theta2 = self.theta2
1630+
1631+
if (
1632+
# if we need to stretch the angles because we are distorted
1633+
width != height
1634+
# and we are not doing a full circle.
1635+
#
1636+
# 0 and 360 do not exactly round-trip through the angle
1637+
# stretching (due to both float precision limitations and
1638+
# the difference between the range of arctan2 [-pi, pi] and
1639+
# this method [0, 360]) so avoid doing it if we don't have to.
1640+
and not (theta1 != theta2 and theta1 % 360 == theta2 % 360)
1641+
):
1642+
theta1 = theta_stretch(self.theta1, width / height)
1643+
theta2 = theta_stretch(self.theta2, width / height)
1644+
1645+
# Get width and height in pixels we need to use
1646+
# `self.get_data_transform` rather than `self.get_transform`
1647+
# because we want the transform from dataspace to the
1648+
# screen space to estimate how big the arc will be in physical
1649+
# units when rendered (the transform that we get via
1650+
# `self.get_transform()` goes from an idealized unit-radius
1651+
# space to screen space).
1652+
data_to_screen_trans = self.get_data_transform()
1653+
pwidth, pheight = (data_to_screen_trans.transform((width, height)) -
1654+
data_to_screen_trans.transform((0, 0)))
16301655
inv_error = (1.0 / 1.89818e-6) * 0.5
1631-
if width < inv_error and height < inv_error:
1656+
1657+
if pwidth < inv_error and pheight < inv_error:
16321658
self._path = Path.arc(theta1, theta2)
16331659
return Patch.draw(self, renderer)
16341660

1635-
def iter_circle_intersect_on_line(x0, y0, x1, y1):
1661+
def line_circle_intersect(x0, y0, x1, y1):
16361662
dx = x1 - x0
16371663
dy = y1 - y0
16381664
dr2 = dx * dx + dy * dy
16391665
D = x0 * y1 - x1 * y0
16401666
D2 = D * D
16411667
discrim = dr2 - D2
1642-
1643-
# Single (tangential) intersection
1644-
if discrim == 0.0:
1645-
x = (D * dy) / dr2
1646-
y = (-D * dx) / dr2
1647-
yield x, y
1648-
elif discrim > 0.0:
1649-
# The definition of "sign" here is different from
1650-
# np.sign: we never want to get 0.0
1651-
if dy < 0.0:
1652-
sign_dy = -1.0
1653-
else:
1654-
sign_dy = 1.0
1668+
if discrim >= 0.0:
1669+
sign_dy = np.copysign(1, dy) # +/-1, never 0.
16551670
sqrt_discrim = np.sqrt(discrim)
1656-
for sign in (1., -1.):
1657-
x = (D * dy + sign * sign_dy * dx * sqrt_discrim) / dr2
1658-
y = (-D * dx + sign * np.abs(dy) * sqrt_discrim) / dr2
1659-
yield x, y
1671+
return np.array(
1672+
[[(D * dy + sign_dy * dx * sqrt_discrim) / dr2,
1673+
(-D * dx + abs(dy) * sqrt_discrim) / dr2],
1674+
[(D * dy - sign_dy * dx * sqrt_discrim) / dr2,
1675+
(-D * dx - abs(dy) * sqrt_discrim) / dr2]])
1676+
else:
1677+
return np.empty((0, 2))
16601678

1661-
def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
1679+
def segment_circle_intersect(x0, y0, x1, y1):
16621680
epsilon = 1e-9
16631681
if x1 < x0:
16641682
x0e, x1e = x1, x0
@@ -1668,40 +1686,34 @@ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
16681686
y0e, y1e = y1, y0
16691687
else:
16701688
y0e, y1e = y0, y1
1671-
x0e -= epsilon
1672-
y0e -= epsilon
1673-
x1e += epsilon
1674-
y1e += epsilon
1675-
for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1):
1676-
if x0e <= x <= x1e and y0e <= y <= y1e:
1677-
yield x, y
1689+
xys = line_circle_intersect(x0, y0, x1, y1)
1690+
xs, ys = xys.T
1691+
return xys[
1692+
(x0e - epsilon < xs) & (xs < x1e + epsilon)
1693+
& (y0e - epsilon < ys) & (ys < y1e + epsilon)
1694+
]
16781695

16791696
# Transforms the axes box_path so that it is relative to the unit
16801697
# circle in the same way that it is relative to the desired ellipse.
1681-
box_path = Path.unit_rectangle()
16821698
box_path_transform = (transforms.BboxTransformTo(self.axes.bbox)
1683-
- self.get_transform())
1684-
box_path = box_path.transformed(box_path_transform)
1699+
+ self.get_transform().inverted())
1700+
box_path = Path.unit_rectangle().transformed(box_path_transform)
16851701

16861702
thetas = set()
16871703
# For each of the point pairs, there is a line segment
16881704
for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
1689-
x0, y0 = p0
1690-
x1, y1 = p1
1691-
for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
1692-
theta = np.arccos(x)
1693-
if y < 0:
1694-
theta = 2 * np.pi - theta
1695-
# Convert radians to angles
1696-
theta = np.rad2deg(theta)
1697-
if theta1 < theta < theta2:
1698-
thetas.add(theta)
1705+
xy = segment_circle_intersect(*p0, *p1)
1706+
x, y = xy.T
1707+
# arctan2 return [-pi, pi), the rest of our angles are in
1708+
# [0, 360], adjust as needed.
1709+
theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360
1710+
thetas.update(theta[(theta1 < theta) & (theta < theta2)])
16991711
thetas = sorted(thetas) + [theta2]
1700-
17011712
last_theta = theta1
17021713
theta1_rad = np.deg2rad(theta1)
1703-
inside = box_path.contains_point((np.cos(theta1_rad),
1704-
np.sin(theta1_rad)))
1714+
inside = box_path.contains_point(
1715+
(np.cos(theta1_rad), np.sin(theta1_rad))
1716+
)
17051717

17061718
# save original path
17071719
path_original = self._path
Loading

0 commit comments

Comments
 (0)