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

Lines changed: 65 additions & 53 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy