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 11 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
View file Open in desktop
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')
91 changes: 89 additions & 2 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -1552,9 +1552,96 @@ def get_angle(self):
angle = property(get_angle, set_angle)


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

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[0], self.center[1], r, self.width, self.angle)

@docstring.dedent_interpd
def __init__(self, xy, r, width, angle=0.0, **kwargs):
"""
xy : (float, float)
xy coordinates of annulus centre
r : scalar or 1D array_like
The radius, or semi-major axes
- If float: radius of the outer circle
- If array_like of size 2: semi-major and -minor axes of outer
ellipse
width : float
Width of the annulus
angle: float, optional
Rotation angle in degrees (anti-clockwise). Ignored for circular
annuli (ie. if `r` is a scalar).

Valid kwargs are:

%(Patch_kwdoc)s
"""
Patch.__init__(self, **kwargs)

if np.shape(r) == (2,):
self.a, self.b = r
elif np.shape(r) == ():
self.a = self.b = float(r)
else:
raise ValueError(
'r parameter should be either float, or array_like of size 2')

if min(self.a, self.b) <= width:
raise ValueError(
'Width of annulus should be smaller than semi-minor axis')

self.center = xy
self.width = width
self.angle = angle
self._path = None

def _transform_verts(self, verts, a, b):
center = (self.convert_xunits(self.center[0]),
self.convert_yunits(self.center[1]))
a = self.convert_xunits(a)
b = self.convert_yunits(b)
tr = transforms.Affine2D() \
.scale(a, b) \
.rotate_deg(self.angle) \
.translate(*center)

return tr.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, arc.codes, Path.MOVETO, Path.CLOSEPOLY])
c[len(arc.codes)] = Path.MOVETO

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.
15 changes: 14 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 @@ -582,6 +583,18 @@ 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')


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