8000 Auto backport of pr 17564 on v3.2.x by tacaswell · Pull Request #17655 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Auto backport of pr 17564 on v3.2.x #17655

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

8000 Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 65 additions & 53 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -1592,14 +1592,12 @@ def draw(self, renderer):
calculation much easier than doing rotated ellipse
intersection directly).

This uses the "line intersecting a circle" algorithm
from:
This uses the "line intersecting a circle" algorithm from:

Vince, John. *Geometry for Computer Graphics: Formulae,
Examples & Proofs.* London: Springer-Verlag, 2005.

2. The angles of each of the intersection points are
calculated.
2. The angles of each of the intersection points are calculated.

3. Proceeding counterclockwise starting in the positive
x-direction, each of the visible arc-segments between the
Expand All @@ -1609,6 +1607,8 @@ def draw(self, renderer):
"""
if not hasattr(self, 'axes'):
raise RuntimeError('Arcs can only be used in Axes instances')
if not self.get_visible():
return

self._recompute_transform()

Expand All @@ -1621,44 +1621,62 @@ def theta_stretch(theta, scale):
theta = np.deg2rad(theta)
x = np.cos(theta)
y = np.sin(theta)
return np.rad2deg(np.arctan2(scale * y, x))
theta1 = theta_stretch(self.theta1, width / height)
theta2 = theta_stretch(self.theta2, width / height)

# Get width and height in pixels
width, height = self.get_transform().transform((width, height))
stheta = np.rad2deg(np.arctan2(scale * y, x))
# arctan2 has the range [-pi, pi], we expect [0, 2*pi]
return (stheta + 360) % 360

theta1 = self.theta1
theta2 = self.theta2

if (
# if we need to stretch the angles because we are distorted
width != height
# and we are not doing a full circle.
#
# 0 and 360 do not exactly round-trip through the angle
# stretching (due to both float precision limitations and
# the difference between the range of arctan2 [-pi, pi] and
# this method [0, 360]) so avoid doing it if we don't have to.
and not (theta1 != theta2 and theta1 % 360 == theta2 % 360)
):
theta1 = theta_stretch(self.theta1, width / height)
theta2 = theta_stretch(self.theta2, width / height)

# Get width and height in pixels we need to use
# `self.get_data_transform` rather than `self.get_transform`
# because we want the transform from dataspace to the
# screen space to estimate how big the arc will be in physical
# units when rendered (the transform that we get via
# `self.get_transform()` goes from an idealized unit-radius
# space to screen space).
data_to_screen_trans = self.get_data_transform()
pwidth, pheight = (data_to_screen_trans.transform((width, height)) -
data_to_screen_trans.transform((0, 0)))
inv_error = (1.0 / 1.89818e-6) * 0.5
if width < inv_error and height < inv_error:

if pwidth < inv_error and pheight < inv_error:
self._path = Path.arc(theta1, theta2)
return Patch.draw(self, renderer)

def iter_circle_intersect_on_line(x0, y0, x1, y1):
def line_circle_intersect(x0, y0, x1, y1):
dx = x1 - x0
dy = y1 - y0
dr2 = dx * dx + dy * dy
D = x0 * y1 - x1 * y0
D2 = D * D
discrim = dr2 - D2

# Single (tangential) intersection
if discrim == 0.0:
x = (D * dy) / dr2
y = (-D * dx) / dr2
yield x, y
elif discrim > 0.0:
# The definition of "sign" here is different from
# np.sign: we never want to get 0.0
if dy < 0.0:
sign_dy = -1.0
else:
sign_dy = 1.0
if discrim >= 0.0:
sign_dy = np.copysign(1, dy) # +/-1, never 0.
sqrt_discrim = np.sqrt(discrim)
for sign in (1., -1.):
x = (D * dy + sign * sign_dy * dx * sqrt_discrim) / dr2
y = (-D * dx + sign * np.abs(dy) * sqrt_discrim) / dr2
yield x, y
return np.array(
[[(D * dy + sign_dy * dx * sqrt_discrim) / dr2,
(-D * dx + abs(dy) * sqrt_discrim) / dr2],
[(D * dy - sign_dy * dx * sqrt_discrim) / dr2,
(-D * dx - abs(dy) * sqrt_discrim) / dr2]])
else:
return np.empty((0, 2))

def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
def segment_circle_intersect(x0, y0, x1, y1):
epsilon = 1e-9
if x1 < x0:
x0e, x1e = x1, x0
Expand All @@ -1668,40 +1686,34 @@ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
y0e, y1e = y1, y0
else:
y0e, y1e = y0, y1
x0e -= epsilon
y0e -= epsilon
x1e += epsilon
y1e += epsilon
for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1):
if x0e <= x <= x1e and y0e <= y <= y1e:
yield x, y
xys = line_circle_intersect(x0, y0, x1, y1)
xs, ys = xys.T
return xys[
(x0e - epsilon < xs) & (xs < x1e + epsilon)
& (y0e - epsilon < ys) & (ys < y1e + epsilon)
]

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

thetas = set()
# For each of the point pairs, there is a line segment
for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
x0, y0 = p0
x1, y1 = p1
for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
theta = np.arccos(x)
if y < 0:
theta = 2 * np.pi - theta
# Convert radians to angles
theta = np.rad2deg(theta)
if theta1 < theta < theta2:
thetas.add(theta)
xy = segment_circle_intersect(*p0, *p1)
x, y = xy.T
# arctan2 return [-pi, pi), the rest of our angles are in
# [0, 360], adjust as needed.
theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360
thetas.update(theta[(theta1 < theta) & (theta < theta2)])
thetas = sorted(thetas) + [theta2]

last_theta = theta1
theta1_rad = np.deg2rad(theta1)
inside = box_path.contains_point((np.cos(theta1_rad),
np.sin(theta1_rad)))
inside = box_path.contains_point(
(np.cos(theta1_rad), np.sin(theta1_rad))
)

# save original path
path_original = self._path
Expand Down
Binary file modified lib/matplotlib/tests/baseline_images/test_axes/arc_angles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
0