@@ -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
0 commit comments