8000 Add an Annulus patch class by astromancer · Pull Request #9888 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Add an Annulus patch class #9888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/users/next_whats_new/annulus.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Add ``Annulus`` patch
---------------------

A new class for drawing elliptical annuli.

.. plot::

import matplotlib.pyplot as plt
from matplotlib.patches import Annulus

fig, ax = plt.subplots()
cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
fc='m', ec='b', alpha=0.5, hatch='xxx')
ax.add_patch(cir)
ax.add_patch(ell)
ax.set_aspect('equal')
186 changes: 184 additions & 2 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,9 +1552,191 @@ def get_angle(self):
angle = property(get_angle, set_angle)


class Circle(Ellipse):
"""A circle patch."""
class Annulus(Patch):
"""
An elliptical annulus.
"""

@docstring.dedent_interpd
def __init__(self, xy, r, width, angle=0.0, **kwargs):
"""
xy : (float, float)
xy coordinates of annulus centre.
r : float or (float, float)
The radius, or semi-axes.
- If float: radius of the outer circle.
- If two floats: semi-major and -minor axes of outer ellipse.
width : float
Width (thickness) of the annular ring. The width is measured inward
from the outer ellipse so that for the inner ellipse the semi-axes
are given by `r - width`. `width` must be less than or equal to the
semi-minor axis.
angle : float, default=0
Rotation angle in degrees (anti-clockwise from the positive
x-axis). Ignored for circular annuli (ie. if *r* is a scalar).

Valid kwargs are:

%(Patch_kwdoc)s
"""
super().__init__(**kwargs)

self.set_radii(r)
self.center = xy
self.width = width
self.angle = angle
self._path = None

def __str__(self):
if self.a == self.b:
r = self.a
else:
r = (self.a, self.b)

return "Annulus(xy=(%s, %s), r=%s, width=%s, angle=%s)" % \
(*self.center, r, self.width, self.angle)

def set_center(self, xy):
"""
Set the center of the annulus.

Parameters
----------
xy : (float, float)
"""
self._center = xy
self._path = None
self.stale = True

def get_center(self):
"""Return the center of the annulus."""
return self._center

center = property(get_center, set_center)

def set_width(self, width):
"""
Set the width (thickness) of the annulus ring. The width is measured
inwards from the outer ellipse.

Parameters
----------
width : float
"""
if min(self.a, self.b) <= width:
raise ValueError(
'Width of annulus must be less than or equal semi-minor axis')

self._width = width
self._path = None
self.stale = True

def get_width(self):
"""
Return the width (thickness) of the annulus ring.
"""
return self._width

width = property(get_width, set_width)

def set_angle(self, angle):
"""
Set the tilt angle of the annulus.

Parameters
----------
angle : float
"""
self._angle = angle
self._path = None
self.stale = True

def get_angle(self):
"""Return the angle of the annulus."""
return self._angle

angle = property(get_angle, set_angle)

def set_semimajor(self, a):
"""
Set the semi-major axis *a* of the annulus.

Parameters
----------
a : float
"""
self.a = float(a)
self._path = None
self.stale = True

def set_semiminor(self, b):
"""
Set the semi-minor axis *b* of the annulus.

Parameters
----------
b : float
"""
self.b = float(b)
self._path = None
self.stale = True

def set_radii(self, r):
"""
Set the both the semi-major (*a*) and -minor radii (*b*) of the
annulus.

Parameters
----------
r : (float, float)
"""
if np.shape(r) == (2,):
self.a, self.b = r
elif np.shape(r) == ():
self.a = self.b = float(r)
else:
raise ValueError("Parameter 'r' must be one or two floats.")

self._path = None
self.stale = True

def get_radii(self):
return self.a, self.b

radii = property(get_radii, set_radii)

def _transform_verts(self, verts, a, b):
return transforms.Affine2D() \
.scale(*self._convert_xy_units((a, b))) \
.rotate_deg(self.angle) \
.translate(*self._convert_xy_units(self.center)) \
.transform(verts)

def _recompute_path(self):
# circular arc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. However, do we understand why codecov is confused by this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have given up trying to understand codecov. Above it warns on a line that's part of a docstring.

arc = Path.arc(0, 360)

# annulus needs to draw an outer ring
# followed by a reversed and scaled inner ring
a, b, w = self.a, self.b, self.width
v1 = self._transform_verts(arc.vertices, a, b)
v2 = self._transform_verts(arc.vertices[::-1], a - w, b - w)
v = np.vstack([v1, v2, v1[0, :], (0, 0)])
c = np.hstack([arc.codes, Path.MOVETO,
arc.codes[1:], Path.MOVETO,
Path.CLOSEPOLY])
self._path = Path(v, c)

def get_path(self):
if self._path is None:
self._recompute_path()
return self._path


class Circle(Ellipse):
"""
A circle patch.
"""
def __str__(self):
pars = self.center[0], self.center[1], self.radius
fmt = "Circle(xy=(%g, %g), radius=%g)"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 39 additions & 1 deletion lib/matplotlib/tests/test_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from numpy.testing import assert_almost_equal, assert_array_equal
import pytest

from matplotlib.patches import Patch, Polygon, Rectangle, FancyArrowPatch
from matplotlib.patches import (Annulus, Patch, Polygon, Rectangle,
FancyArrowPatch)
from matplotlib.testing.decorators import image_comparison, check_figures_equal
from matplotlib.transforms import Bbox
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -333,6 +334,10 @@ def test_patch_str():
expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)'
assert str(p) == expected

p = mpatches.Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)
expected = "Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)"
assert str(p) == expected

p = mpatches.RegularPolygon((1, 2), 20, radius=5)
assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)"

Expand Down Expand Up @@ -582,6 +587,39 @@ def test_rotated_arcs():
ax.set_aspect("equal")


@image_comparison(baseline_images=['annulus'], extensions=['png'])
def test_annulus():

fig, ax = plt.subplots()
cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus
ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical
fc='m', ec='b', alpha=0.5, hatch='xxx')
ax.add_patch(cir)
ax.add_patch(ell)
ax.set_aspect('equal')


@image_comparison(baseline_images=['annulus'], extensions=['png'])
def test_annulus_setters():

fig, ax = plt.subplots()
cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus
ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical
fc='m', ec='b', alpha=0.5, hatch='xxx')
ax.add_patch(cir)
ax.add_patch(ell)
ax.set_aspect('equal')

cir.center = (0.5, 0.5)
cir.radii = 0.2
cir.width = 0.05

ell.center = (0.5, 0.5)
ell.radii = (0.5, 0.3)
ell.width = 0.1
ell.angle = 45


def test_degenerate_polygon():
point = [0, 0]
correct_extents = Bbox([point, point]).extents
Expand Down
0