Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ad33280

Browse files
authored
Merge pull request #9888 from astromancer/annulus-patch
Add an Annulus patch class
2 parents 8dd2838 + b21d4ee commit ad33280

File tree

4 files changed

+240
-3
lines changed

4 files changed

+240
-3
lines changed

doc/users/next_whats_new/annulus.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Add ``Annulus`` patch
2+
---------------------
3+
4+
A new class for drawing elliptical annuli.
5+
6+
.. plot::
7+
8+
import matplotlib.pyplot as plt
9+
from matplotlib.patches import Annulus
10+
11+
fig, ax = plt.subplots()
12+
cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
13+
ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
14+
fc='m', ec='b', alpha=0.5, hatch='xxx')
15+
ax.add_patch(cir)
16+
ax.add_patch(ell)
17+
ax.set_aspect('equal')

lib/matplotlib/patches.py

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,9 +1569,191 @@ def get_angle(self):
15691569
angle = property(get_angle, set_angle)
15701570

15711571

1572-
class Circle(Ellipse):
1573-
"""A circle patch."""
1572+
class Annulus(Patch):
1573+
"""
1574+
An elliptical annulus.
1575+
"""
1576+
1577+
@docstring.dedent_interpd
1578+
def __init__(self, xy, r, width, angle=0.0, **kwargs):
1579+
"""
1580+
xy : (float, float)
1581+
xy coordinates of annulus centre.
1582+
r : float or (float, float)
1583+
The radius, or semi-axes.
1584+
- If float: radius of the outer circle.
1585+
- If two floats: semi-major and -minor axes of outer ellipse.
1586+
width : float
1587+
Width (thickness) of the annular ring. The width is measured inward
1588+
from the outer ellipse so that for the inner ellipse the semi-axes
1589+
are given by `r - width`. `width` must be less than or equal to the
1590+
semi-minor axis.
1591+
angle : float, default=0
1592+
Rotation angle in degrees (anti-clockwise from the positive
1593+
x-axis). Ignored for circular annuli (ie. if *r* is a scalar).
1594+
1595+
Valid kwargs are:
1596+
1597+
%(Patch_kwdoc)s
1598+
"""
1599+
super().__init__(**kwargs)
1600+
1601+
self.set_radii(r)
1602+
self.center = xy
1603+
self.width = width
1604+
self.angle = angle
1605+
self._path = None
1606+
1607+
def __str__(self):
1608+
if self.a == self.b:
1609+
r = self.a
1610+
else:
1611+
r = (self.a, self.b)
1612+
1613+
return "Annulus(xy=(%s, %s), r=%s, width=%s, angle=%s)" % \
1614+
(*self.center, r, self.width, self.angle)
1615+
1616+
def set_center(self, xy):
1617+
"""
1618+
Set the center of the annulus.
1619+
1620+
Parameters
1621+
----------
1622+
xy : (float, float)
1623+
"""
1624+
self._center = xy
1625+
self._path = None
1626+
self.stale = True
1627+
1628+
def get_center(self):
1629+
"""Return the center of the annulus."""
1630+
return self._center
1631+
1632+
center = property(get_center, set_center)
1633+
1634+
def set_width(self, width):
1635+
"""
1636+
Set the width (thickness) of the annulus ring. The width is measured
1637+
inwards from the outer ellipse.
1638+
1639+
Parameters
1640+
----------
1641+
width : float
1642+
"""
1643+
if min(self.a, self.b) <= width:
1644+
raise ValueError(
1645+
'Width of annulus must be less than or equal semi-minor axis')
1646+
1647+
self._width = width
1648+
self._path = None
1649+
self.stale = True
1650+
1651+
def get_width(self):
1652+
"""
1653+
Return the width (thickness) of the annulus ring.
1654+
"""
1655+
return self._width
1656+
1657+
width = property(get_width, set_width)
1658+
1659+
def set_angle(self, angle):
1660+
"""
1661+
Set the tilt angle of the annulus.
1662+
1663+
Parameters
1664+
----------
1665+
angle : float
1666+
"""
1667+
self._angle = angle
1668+
self._path = None
1669+
self.stale = True
1670+
1671+
def get_angle(self):
1672+
"""Return the angle of the annulus."""
1673+
return self._angle
1674+
1675+
angle = property(get_angle, set_angle)
15741676

1677+
def set_semimajor(self, a):
1678+
"""
1679+
Set the semi-major axis *a* of the annulus.
1680+
1681+
Parameters
1682+
----------
1683+
a : float
1684+
"""
1685+
self.a = float(a)
1686+
self._path = None
1687+
self.stale = True
1688+
1689+
def set_semiminor(self, b):
1690+
"""
1691+
Set the semi-minor axis *b* of the annulus.
1692+
1693+
Parameters
1694+
----------
1695+
b : float
1696+
"""
1697+
self.b = float(b)
1698+
self._path = None
1699+
self.stale = True
1700+
1701+
def set_radii(self, r):
1702+
"""
1703+
Set the both the semi-major (*a*) and -minor radii (*b*) of the
1704+
annulus.
1705+
1706+
Parameters
1707+
----------
1708+
r : (float, float)
1709+
"""
1710+
if np.shape(r) == (2,):
1711+
self.a, self.b = r
1712+
elif np.shape(r) == ():
1713+
self.a = self.b = float(r)
1714+
else:
1715+
raise ValueError("Parameter 'r' must be one or two floats.")
1716+
1717+
self._path = None
1718+
self.stale = True
1719+
1720+
def get_radii(self):
1721+
return self.a, self.b
1722+
1723+
radii = property(get_radii, set_radii)
1724+
1725+
def _transform_verts(self, verts, a, b):
1726+
return transforms.Affine2D() \
1727+
.scale(*self._convert_xy_units((a, b))) \
1728+
.rotate_deg(self.angle) \
1729+
.translate(*self._convert_xy_units(self.center)) \
1730+
.transform(verts)
1731+
1732+
def _recompute_path(self):
1733+
# circular arc
1734+
arc = Path.arc(0, 360)
1735+
1736+
# annulus needs to draw an outer ring
1737+
# followed by a reversed and scaled inner ring
1738+
a, b, w = self.a, self.b, self.width
1739+
v1 = self._transform_verts(arc.vertices, a, b)
1740+
v2 = self._transform_verts(arc.vertices[::-1], a - w, b - w)
1741+
v = np.vstack([v1, v2, v1[0, :], (0, 0)])
1742+
c = np.hstack([arc.codes, Path.MOVETO,
1743+
arc.codes[1:], Path.MOVETO,
1744+
Path.CLOSEPOLY])
1745+
self._path = Path(v, c)
1746+
1747+
def get_path(self):
1748+
if self._path is None:
1749+
self._recompute_path()
1750+
return self._path
1751+
1752+
1753+
class Circle(Ellipse):
1754+
"""
1755+
A circle patch.
1756+
"""
15751757
def __str__(self):
15761758
pars = self.center[0], self.center[1], self.radius
15771759
fmt = "Circle(xy=(%g, %g), radius=%g)"

lib/matplotlib/tests/test_patches.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from numpy.testing import assert_almost_equal, assert_array_equal
66
import pytest
77

8-
from matplotlib.patches import Patch, Polygon, Rectangle, FancyArrowPatch
8+
from matplotlib.patches import (Annulus, Patch, Polygon, Rectangle,
9+
FancyArrowPatch)
910
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1011
from matplotlib.transforms import Bbox
1112
import matplotlib.pyplot as plt
@@ -354,6 +355,10 @@ def test_patch_str():
354355
expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)'
355356
assert str(p) == expected
356357

358+
p = mpatches.Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)
359+
expected = "Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)"
360+
assert str(p) == expected
361+
357362
p = mpatches.RegularPolygon((1, 2), 20, radius=5)
358363
assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)"
359364

@@ -603,6 +608,39 @@ def test_rotated_arcs():
603608
ax.set_aspect("equal")
604609

605610

611+
@image_comparison(baseline_images=['annulus'], extensions=['png'])
612+
def test_annulus():
613+
614+
fig, ax = plt.subplots()
615+
cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
616+
ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
617+
fc='m', ec='b', alpha=0.5, hatch='xxx')
618+
ax.add_patch(cir)
619+
ax.add_patch(ell)
620+
ax.set_aspect('equal')
621+
622+
623+
@image_comparison(baseline_images=['annulus'], extensions=['png'])
624+
def test_annulus_setters():
625+
626+
fig, ax = plt.subplots()
627+
cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
628+
ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
629+
fc='m', ec='b', alpha=0.5, hatch='xxx')
630+
ax.add_patch(cir)
631+
ax.add_patch(ell)
632+
ax.set_aspect('equal')
633+
634+
cir.center = (0.5, 0.5)
635+
cir.radii = 0.2
636+
cir.width = 0.05
637+
638+
ell.center = (0.5, 0.5)
639+
ell.radii = (0.5, 0.3)
640+
ell.width = 0.1
641+
ell.angle = 45
642+
643+
606644
def test_degenerate_polygon():
607645
point = [0, 0]
608646
correct_extents = Bbox([point, point]).extents

0 commit comments

Comments
 (0)