Skip to content

Commit ffdd0cf

Browse files
committed
FIX: update arc to do better rounding and allow wrap
1 parent 492a478 commit ffdd0cf

File tree

4 files changed

+93
-31
lines changed

4 files changed

+93
-31
lines changed

lib/matplotlib/path.py

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -949,36 +949,70 @@ def unit_circle_righthalf(cls):
949949
return cls._unit_circle_righthalf
950950

951951
@classmethod
952-
def arc(cls, theta1, theta2, n=None, is_wedge=False):
952+
def arc(cls, theta1, theta2, n=None, is_wedge=False, wrap=True):
953953
"""
954954
Return a `Path` for the unit circle arc from angles *theta1* to
955955
*theta2* (in degrees).
956956
957-
*theta2* is unwrapped to produce the shortest arc within 360 degrees.
958-
That is, if *theta2* > *theta1* + 360, the arc will be from *theta1* to
959-
*theta2* - 360 and not a full circle plus some extra overlap.
960-
961-
If *n* is provided, it is the number of spline segments to make.
962-
If *n* is not provided, the number of spline segments is
963-
determined based on the delta between *theta1* and *theta2*.
957+
Parameters
958+
----------
959+
theta1, theta2 : float
960+
The angles (in degrees) of the start and end of the arc. If the
961+
arc is greater than 360 degrees, then the arc will be wrapped to
962+
fit between theta1 and theta1 + 360, if *wrap* is True.
963+
964+
n : int, optional
965+
The number of spline segments to make. If not provided, the number
966+
of spline segments is determined based on the delta between
967+
*theta1* and *theta2*.
968+
969+
is_wedge : bool, default: False
970+
If True, the arc is a wedge. The first vertex is the center of the
971+
wedge, the second vertex is the start of the arc, and the last
972+
vertex is the end of the arc. The wedge is closed with a line
973+
segment to the center of the wedge. If False, the arc is a
974+
polyline. The first vertex is the start of the arc, and the last
975+
vertex is the end of the arc. The arc is closed with a line
976+
segment to the start of the arc. The wedge is not closed with a
977+
line segment to the start of the arc.
978+
979+
wrap : bool, default: True
980+
If True, the arc is wrapped to fit between *theta1* and *theta1* +
981+
360 degrees. If False, the arc is not wrapped. The arc will be
982+
drawn from *theta1* to *theta2*.
964983
965-
Masionobe, L. 2003. `Drawing an elliptical arc using
966-
polylines, quadratic or cubic Bezier curves
984+
Notes
985+
-----
986+
The arc is approximated using cubic Bézier curves, as described in
987+
Masionobe, L. 2003. `Drawing an elliptical arc using polylines,
988+
quadratic or cubic Bezier curves
967989
<https://web.archive.org/web/20190318044212/http://www.spaceroots.org/documents/ellipse/index.html>`_.
968990
"""
969-
halfpi = np.pi * 0.5
970991

971992
eta1 = theta1
972-
eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
973-
# Ensure 2pi range is not flattened to 0 due to floating-point errors,
974-
# but don't try to expand existing 0 range.
975-
if theta2 != theta1 and eta2 <= eta1:
976-
eta2 += 360
993+
if wrap:
994+
# Wrap theta2 to 0-360 degrees from theta1.
995+
eta2 = np.mod(theta2 - theta1, 360.0) + theta1
996+
print('Eta1, Eta20', eta1, eta2)
997+
# Ensure 360-deg range is not flattened to 0 due to floating-point
998+
# errors, but don't try to expand existing 0 range.
999+
if theta2 != theta1 and eta2 <= eta1:
1000+
eta2 += 360
1001+
print('Eta1, Eta2', eta1, eta2)
1002+
else:
1003+
eta2 = theta2
9771004
eta1, eta2 = np.deg2rad([eta1, eta2])
9781005

9791006
# number of curve segments to make
9801007
if n is None:
981-
n = int(2 ** np.ceil((eta2 - eta1) / halfpi))
1008+
if np.abs(eta2 - eta1) <= 2.2 * np.pi:
1009+
# this doesn't need to grow exponentially, but we have left
1010+
# this way for back compatibility
1011+
n = int(2 ** np.ceil(2 * np.abs(eta2 - eta1) / np.pi))
1012+
print('Here')
1013+
else:
1014+
# this will not grow exponentially if we allow wrapping arcs:
1015+
n = int(2 * np.ceil(2 * np.abs(eta2 - eta1) / np.pi))
9821016
if n < 1:
9831017
raise ValueError("n must be >= 1 or None")
9841018

@@ -1028,22 +1062,29 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False):
10281062
return cls(vertices, codes, readonly=True)
10291063

10301064
@classmethod
1031-
def wedge(cls, theta1, theta2, n=None):
1065+
def wedge(cls, theta1, theta2, n=None, wrap=True):
10321066
"""
10331067
Return a `Path` for the unit circle wedge from angles *theta1* to
10341068
*theta2* (in degrees).
10351069
1036-
*theta2* is unwrapped to produce the shortest wedge within 360 degrees.
1037-
That is, if *theta2* > *theta1* + 360, the wedge will be from *theta1*
1038-
to *theta2* - 360 and not a full circle plus some extra overlap.
1039-
1040-
If *n* is provided, it is the number of spline segments to make.
1041-
If *n* is not provided, the number of spline segments is
1042-
determined based on the delta between *theta1* and *theta2*.
1043-
1044-
See `Path.arc` for the reference on the approximation used.
1045-
"""
1046-
return cls.arc(theta1, theta2, n, True)
1070+
Parameters
1071+
----------
1072+
theta1, theta2 : float
1073+
The angles (in degrees) of the start and end of the arc. If the
1074+
arc is greater than 360 degrees, then the arc will be wrapped to
1075+
fit between theta1 and theta1 + 360, if *wrap* is True.
1076+
1077+
n : int, optional
1078+
The number of spline segments to make. If not provided, the number
1079+
of spline segments is determined based on the delta between
1080+
*theta1* and *theta2*.
1081+
1082+
wrap : bool, default: True
1083+
If True, the arc is wrapped to fit between *theta1* and *theta1* +
1084+
360 degrees. If False, the arc is not wrapped. The arc will be
1085+
drawn from *theta1* to *theta2*.
1086+
"""
1087+
return cls.arc(theta1, theta2, n, wedge=True, wrap=wrap)
10471088

10481089
@staticmethod
10491090
@lru_cache(8)

lib/matplotlib/path.pyi

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ class Path:
119119
def unit_circle_righthalf(cls) -> Path: ...
120120
@classmethod
121121
def arc(
122-
cls, theta1: float, theta2: float, n: int | None = ..., is_wedge: bool = ...
122+
cls, theta1: float, theta2: float, n: int | None = ...,
123+
is_wedge: bool = ..., wrap: bool = ...
123124
) -> Path: ...
124125
@classmethod
125-
def wedge(cls, theta1: float, theta2: float, n: int | None = ...) -> Path: ...
126+
def wedge(
127+
cls, theta1: float, theta2: float, n: int | None = ...,
128+
wrap: bool = ...
129+
) -> Path: ...
126130
@overload
127131
@staticmethod
128132
def hatch(hatchpattern: str, density: float = ...) -> Path: ...

lib/matplotlib/tests/test_path.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,12 +471,29 @@ def test_full_arc(offset):
471471
high = 360 + offset
472472

473473
path = Path.arc(low, high)
474+
print(path.vertices)
474475
mins = np.min(path.vertices, axis=0)
475476
maxs = np.max(path.vertices, axis=0)
476477
np.testing.assert_allclose(mins, -1)
477478
np.testing.assert_allclose(maxs, 1)
478479

479480

481+
@image_comparison(['arc_close360'], style='default', remove_text=True,
482+
extensions=['png'])
483+
def test_arc_close360():
484+
fig, ax = plt.subplots(ncols=3)
485+
ax[0].add_patch(patches.PathPatch(Path.arc(theta1=-90 - 1e-14, theta2=270)))
486+
#ax[0].set_title("arc(-90-1e-14, 270), should be a circle")
487+
ax[1].add_patch(patches.PathPatch(Path.arc(theta1=-90, theta2=270)))
488+
#ax[1].set_title("arc(-90, 270), is a circle")
489+
ax[2].add_patch(patches.PathPatch(Path.arc(theta1=-90 - 1e-14, theta2=-90)))
490+
#ax[2].set_title("arc(-90, -90-1e-14), should not be a circle")
491+
for a in ax:
492+
a.set_xlim(-1, 1)
493+
a.set_ylim(-1, 1)
494+
a.set_aspect("equal")
495+
496+
480497
def test_disjoint_zero_length_segment():
481498
this_path = Path(
482499
np.array([

0 commit comments

Comments
 (0)