diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py
index 42affae91949..abf4b97a6c7d 100644
--- a/lib/matplotlib/patches.py
+++ b/lib/matplotlib/patches.py
@@ -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
@@ -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()
@@ -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
@@ -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
diff --git a/lib/matplotlib/tests/baseline_images/test_axes/arc_angles.png b/lib/matplotlib/tests/baseline_images/test_axes/arc_angles.png
index 8b81cde703ef..caa050aed900 100644
Binary files a/lib/matplotlib/tests/baseline_images/test_axes/arc_angles.png and b/lib/matplotlib/tests/baseline_images/test_axes/arc_angles.png differ
diff --git a/lib/matplotlib/tests/baseline_images/test_patches/all_quadrants_arcs.svg b/lib/matplotlib/tests/baseline_images/test_patches/all_quadrants_arcs.svg
new file mode 100644
index 000000000000..a45712972165
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/test_patches/all_quadrants_arcs.svg
@@ -0,0 +1,481 @@
+
+
+
+
diff --git a/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg b/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg
new file mode 100644
index 000000000000..d902870aa7b7
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/test_patches/large_arc.svg
@@ -0,0 +1,79 @@
+
+
+
+
diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py
index 32ce5db1cebe..c0eda5542927 100644
--- a/lib/matplotlib/tests/test_patches.py
+++ b/lib/matplotlib/tests/test_patches.py
@@ -490,3 +490,60 @@ def test_fancyarrow_units():
fig, ax = plt.subplots()
arrow = FancyArrowPatch((0, dtime), (0.01, dtime))
ax.add_patch(arrow)
+
+
+@image_comparison(["large_arc.svg"], style="mpl20")
+def test_large_arc():
+ fig, (ax1, ax2) = plt.subplots(1, 2)
+ x = 210
+ y = -2115
+ diameter = 4261
+ for ax in [ax1, ax2]:
+ a = mpatches.Arc((x, y), diameter, diameter, lw=2, color='k')
+ ax.add_patch(a)
+ ax.set_axis_off()
+ ax.set_aspect('equal')
+ # force the high accuracy case
+ ax1.set_xlim(7, 8)
+ ax1.set_ylim(5, 6)
+
+ # force the low accuracy case
+ ax2.set_xlim(-25000, 18000)
+ ax2.set_ylim(-20000, 6600)
+
+
+@image_comparison(["all_quadrants_arcs.svg"], style="mpl20")
+def test_rotated_arcs():
+ fig, ax_arr = plt.subplots(2, 2, squeeze=False, figsize=(10, 10))
+
+ scale = 10_000_000
+ diag_centers = ((-1, -1), (-1, 1), (1, 1), (1, -1))
+ on_axis_centers = ((0, 1), (1, 0), (0, -1), (-1, 0))
+ skews = ((2, 2), (2, 1/10), (2, 1/100), (2, 1/1000))
+
+ for ax, (sx, sy) in zip(ax_arr.ravel(), skews):
+ k = 0
+ for prescale, centers in zip((1 - .0001, (1 - .0001) / np.sqrt(2)),
+ (on_axis_centers, diag_centers)):
+ for j, (x_sign, y_sign) in enumerate(centers, start=k):
+ a = mpatches.Arc(
+ (x_sign * scale * prescale,
+ y_sign * scale * prescale),
+ scale * sx,
+ scale * sy,
+ lw=4,
+ color=f"C{j}",
+ zorder=1 + j,
+ angle=np.rad2deg(np.arctan2(y_sign, x_sign)) % 360,
+ label=f'big {j}',
+ gid=f'big {j}'
+ )
+ ax.add_patch(a)
+
+ k = j+1
+ ax.set_xlim(-scale / 4000, scale / 4000)
+ ax.set_ylim(-scale / 4000, scale / 4000)
+ ax.axhline(0, color="k")
+ ax.axvline(0, color="k")
+ ax.set_axis_off()
+ ax.set_aspect("equal")