Skip to content

Commit 14d5441

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

File tree

4 files changed

+103
-35
lines changed

4 files changed

+103
-35
lines changed

lib/matplotlib/path.py

+80-33
Original file line numberDiff line numberDiff line change
@@ -949,36 +949,73 @@ 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
"""
954-
Return a `Path` for the unit circle arc from angles *theta1* to
955-
*theta2* (in degrees).
954+
Return a `Path` for a counter-clockwise unit circle arc from angles
955+
*theta1* to *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) defining the start (*theta1*) and end
961+
(*theta2*) of the arc. If the arc spans more than 360 degrees, it
962+
will be wrapped to fit within the range from *theta1* to *theta1* +
963+
360, provided *wrap* is True. The arc is drawn counter-clockwise
964+
from *theta1* to *theta2*. For instance, if *theta1* =90 and
965+
*theta2* = 70, the resulting arc will span 320 degrees.
966+
967+
n : int, optional
968+
The number of spline segments to make. If not provided, the number
969+
of spline segments is determined based on the delta between
970+
*theta1* and *theta2*.
971+
972+
is_wedge : bool, default: False
973+
If True, the arc is a wedge. The first vertex is the center of the
974+
wedge, the second vertex is the start of the arc, and the last
975+
vertex is the end of the arc. The wedge is closed with a line
976+
segment to the center of the wedge. If False, the arc is a
977+
polyline. The first vertex is the start of the arc, and the last
978+
vertex is the end of the arc. The arc is closed with a line
979+
segment to the start of the arc. The wedge is not closed with a
980+
line segment to the start of the arc.
981+
982+
wrap : bool, default: True
983+
If True, the arc is wrapped to fit between *theta1* and *theta1* +
984+
360 degrees. If False, the arc is not wrapped. The arc will be
985+
drawn from *theta1* to *theta2*.
964986
965-
Masionobe, L. 2003. `Drawing an elliptical arc using
966-
polylines, quadratic or cubic Bezier curves
987+
Notes
988+
-----
989+
The arc is approximated using cubic Bézier curves, as described in
990+
Masionobe, L. 2003. `Drawing an elliptical arc using polylines,
991+
quadratic or cubic Bezier curves
967992
<https://web.archive.org/web/20190318044212/http://www.spaceroots.org/documents/ellipse/index.html>`_.
968993
"""
969-
halfpi = np.pi * 0.5
970994

971995
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
996+
if wrap:
997+
# Wrap theta2 to 0-360 degrees from theta1.
998+
eta2 = np.mod(theta2 - theta1, 360.0) + theta1
999+
print('Eta1, Eta20', eta1, eta2)
1000+
# Ensure 360-deg range is not flattened to 0 due to floating-point
1001+
# errors, but don't try to expand existing 0 range.
1002+
if theta2 != theta1 and eta2 <= eta1:
1003+
eta2 += 360
1004+
print('Eta1, Eta2', eta1, eta2)
1005+
else:
1006+
eta2 = theta2
9771007
eta1, eta2 = np.deg2rad([eta1, eta2])
9781008

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

@@ -1028,22 +1065,32 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False):
10281065
return cls(vertices, codes, readonly=True)
10291066

10301067
@classmethod
1031-
def wedge(cls, theta1, theta2, n=None):
1068+
def wedge(cls, theta1, theta2, n=None, wrap=True):
10321069
"""
1033-
Return a `Path` for the unit circle wedge from angles *theta1* to
1034-
*theta2* (in degrees).
1035-
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*.
1070+
Return a `Path` for a counter-clockwise unit circle wedge from angles
1071+
*theta1* to *theta2* (in degrees).
10431072
1044-
See `Path.arc` for the reference on the approximation used.
1045-
"""
1046-
return cls.arc(theta1, theta2, n, True)
1073+
Parameters
1074+
----------
1075+
theta1, theta2 : float
1076+
The angles (in degrees) defining the start (*theta1*) and end
1077+
(*theta2*) of the arc. If the arc spans more than 360 degrees, it
1078+
will be wrapped to fit within the range from *theta1* to *theta1* +
1079+
360, provided *wrap* is True. The arc is drawn counter-clockwise
1080+
from *theta1* to *theta2*. For instance, if *theta1* =90 and
1081+
*theta2* = 70, the resulting arc will span 320 degrees.
1082+
1083+
n : int, optional
1084+
The number of spline segments to make. If not provided, the number
1085+
of spline segments is determined based on the delta between
1086+
*theta1* and *theta2*.
1087+
1088+
wrap : bool, default: True
1089+
If True, the arc is wrapped to fit between *theta1* and *theta1* +
1090+
360 degrees. If False, the arc is not wrapped. The arc will be
1091+
drawn from *theta1* to *theta2*.
1092+
"""
1093+
return cls.arc(theta1, theta2, n, wedge=True, wrap=wrap)
10471094

10481095
@staticmethod
10491096
@lru_cache(8)

lib/matplotlib/path.pyi

+6-2
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

+17
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)