diff --git a/doc/users/next_whats_new/get_vertices_co_vertices.rst b/doc/users/next_whats_new/get_vertices_co_vertices.rst new file mode 100644 index 000000000000..98254a82ce63 --- /dev/null +++ b/doc/users/next_whats_new/get_vertices_co_vertices.rst @@ -0,0 +1,7 @@ +``Ellipse.get_vertices()``, ``Ellipse.get_co_vertices()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These methods return the coordinates of ellipse vertices of +major and minor axis. Additionally, an example gallery demo is added which +shows how to add an arrow to an ellipse showing a clockwise or counter-clockwise +rotation of the ellipse. To place the arrow exactly on the ellipse, +the coordinates of the vertices are used. diff --git a/galleries/examples/shapes_and_collections/ellipse_arrow.py b/galleries/examples/shapes_and_collections/ellipse_arrow.py new file mode 100644 index 000000000000..4bcdc016faa6 --- /dev/null +++ b/galleries/examples/shapes_and_collections/ellipse_arrow.py @@ -0,0 +1,53 @@ +""" +=================================== +Ellipse with orientation arrow demo +=================================== + +This demo shows how to draw an ellipse with +an orientation arrow (clockwise or counterclockwise). +Compare this to the :doc:`Ellipse collection example +`. +""" + +import matplotlib.pyplot as plt + +from matplotlib.markers import MarkerStyle +from matplotlib.patches import Ellipse +from matplotlib.transforms import Affine2D + +# Create a figure and axis +fig, ax = plt.subplots(subplot_kw={"aspect": "equal"}) + +ellipse = Ellipse( + xy=(2, 4), + width=30, + height=20, + angle=35, + facecolor="none", + edgecolor="b" +) +ax.add_patch(ellipse) + +# Plot an arrow marker at the end point of minor axis +vertices = ellipse.get_co_vertices() +t = Affine2D().rotate_deg(ellipse.angle) +ax.plot( + vertices[0][0], + vertices[0][1], + color="b", + marker=MarkerStyle(">", "full", t), + markersize=10 +) +# Note: To reverse the orientation arrow, switch the marker type from > to <. + +plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.patches` +# - `matplotlib.patches.Ellipse` diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 245bb7b777c8..ecf03ca4051b 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1654,6 +1654,37 @@ def get_corners(self): return self.get_patch_transform().transform( [(-1, -1), (1, -1), (1, 1), (-1, 1)]) + def _calculate_length_between_points(self, x0, y0, x1, y1): + return np.sqrt((x1 - x0)**2 + (y1 - y0)**2) + + def get_vertices(self): + """ + Return the vertices coordinates of the ellipse. + + The definition can be found `here `_ + + .. versionadded:: 3.8 + """ + if self.width < self.height: + ret = self.get_patch_transform().transform([(0, 1), (0, -1)]) + else: + ret = self.get_patch_transform().transform([(1, 0), (-1, 0)]) + return [tuple(x) for x in ret] + + def get_co_vertices(self): + """ + Return the co-vertices coordinates of the ellipse. + + The definition can be found `here `_ + + .. versionadded:: 3.8 + """ + if self.width < self.height: + ret = self.get_patch_transform().transform([(1, 0), (-1, 0)]) + else: + ret = self.get_patch_transform().transform([(0, 1), (0, -1)]) + return [tuple(x) for x in ret] + class Annulus(Patch): """ diff --git a/lib/matplotlib/patches.pyi b/lib/matplotlib/patches.pyi index 1e70a1efc3be..e9302563083c 100644 --- a/lib/matplotlib/patches.pyi +++ b/lib/matplotlib/patches.pyi @@ -259,6 +259,10 @@ class Ellipse(Patch): def get_corners(self) -> np.ndarray: ... + def get_vertices(self) -> list[tuple[float, float]]: ... + def get_co_vertices(self) -> list[tuple[float, float]]: ... + + class Annulus(Patch): a: float b: float diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index b9e1db32d419..fd872bac98d4 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -104,6 +104,57 @@ def test_corner_center(): assert_almost_equal(ellipse.get_corners(), corners_rot) +def test_ellipse_vertices(): + # expect 0 for 0 ellipse width, height + ellipse = Ellipse(xy=(0, 0), width=0, height=0, angle=0) + assert_almost_equal( + ellipse.get_vertices(), + [(0.0, 0.0), (0.0, 0.0)], + ) + assert_almost_equal( + ellipse.get_co_vertices(), + [(0.0, 0.0), (0.0, 0.0)], + ) + + ellipse = Ellipse(xy=(0, 0), width=2, height=1, angle=30) + assert_almost_equal( + ellipse.get_vertices(), + [ + ( + ellipse.center[0] + ellipse.width / 4 * np.sqrt(3), + ellipse.center[1] + ellipse.width / 4, + ), + ( + ellipse.center[0] - ellipse.width / 4 * np.sqrt(3), + ellipse.center[1] - ellipse.width / 4, + ), + ], + ) + assert_almost_equal( + ellipse.get_co_vertices(), + [ + ( + ellipse.center[0] - ellipse.height / 4, + ellipse.center[1] + ellipse.height / 4 * np.sqrt(3), + ), + ( + ellipse.center[0] + ellipse.height / 4, + ellipse.center[1] - ellipse.height / 4 * np.sqrt(3), + ), + ], + ) + v1, v2 = np.array(ellipse.get_vertices()) + np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center) + v1, v2 = np.array(ellipse.get_co_vertices()) + np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center) + + ellipse = Ellipse(xy=(2.252, -10.859), width=2.265, height=1.98, angle=68.78) + v1, v2 = np.array(ellipse.get_vertices()) + np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center) + v1, v2 = np.array(ellipse.get_co_vertices()) + np.testing.assert_almost_equal((v1 + v2) / 2, ellipse.center) + + def test_rotate_rect(): loc = np.asarray([1.0, 2.0]) width = 2