From 78de816de7a1c26b5cf0fbb1bf0eaad726b13024 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 2 Apr 2024 18:58:08 +0100 Subject: [PATCH 1/4] Add base class for collection with width, height and angle to be used as parent class of EllipseCollection and RectangleCollection --- lib/matplotlib/collections.py | 69 ++++++++++++++++-- lib/matplotlib/collections.pyi | 5 +- lib/matplotlib/path.py | 15 ++++ lib/matplotlib/path.pyi | 2 + .../RectangleCollection_test_image.png | Bin 0 -> 19203 bytes lib/matplotlib/tests/test_collections.py | 31 +++++++- 6 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_collections/RectangleCollection_test_image.png diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index fd6cc4339d64..e48a327eec65 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1720,8 +1720,10 @@ def __init__(self, sizes, **kwargs): self._paths = [mpath.Path.unit_circle()] -class EllipseCollection(Collection): - """A collection of ellipses, drawn using splines.""" +class _CollectionWithWidthHeightAngle(Collection): + """ + Base class for collections that have an array of widths, heights and angles + """ def __init__(self, widths, heights, angles, *, units='points', **kwargs): """ @@ -1751,7 +1753,7 @@ def __init__(self, widths, heights, angles, *, units='points', **kwargs): self._units = units self.set_transform(transforms.IdentityTransform()) self._transforms = np.empty((0, 3, 3)) - self._paths = [mpath.Path.unit_circle()] + self._paths = [self._path_generator()] def _set_transforms(self): """Calculate transforms immediately before drawing.""" @@ -1797,12 +1799,12 @@ def _set_transforms(self): def set_widths(self, widths): """Set the lengths of the first axes (e.g., major axis).""" - self._widths = 0.5 * np.asarray(widths).ravel() + self._widths = np.asarray(widths).ravel() self.stale = True def set_heights(self, heights): """Set the lengths of second axes (e.g., minor axes).""" - self._heights = 0.5 * np.asarray(heights).ravel() + self._heights = np.asarray(heights).ravel() self.stale = True def set_angles(self, angles): @@ -1812,11 +1814,11 @@ def set_angles(self, angles): def get_widths(self): """Get the lengths of the first axes (e.g., major axis).""" - return self._widths * 2 + return self._widths def get_heights(self): """Set the lengths of second axes (e.g., minor axes).""" - return self._heights * 2 + return self._heights def get_angles(self): """Get the angles of the first axes, degrees CCW from the x-axis.""" @@ -1828,6 +1830,59 @@ def draw(self, renderer): super().draw(renderer) +class EllipseCollection(_CollectionWithWidthHeightAngle): + """ + A collection of ellipses, drawn using splines. + + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + heights : array-like + The lengths of second axes. + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + """ + _path_generator = mpath.Path.half_unit_circle + + +class RectangleCollection(_CollectionWithWidthHeightAngle): + """ + A collection of rectangles, drawn using splines. + + Parameters + ---------- + widths : array-like + The lengths of the first axes (e.g., major axis lengths). + heights : array-like + The lengths of second axes. + angles : array-like + The angles of the first axes, degrees CCW from the x-axis. + units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'} + The units in which majors and minors are given; 'width' and + 'height' refer to the dimensions of the axes, while 'x' and 'y' + refer to the *offsets* data units. 'xy' differs from all others in + that the angle as plotted varies with the aspect ratio, and equals + the specified angle only when the aspect ratio is unity. Hence + it behaves the same as the `~.patches.Ellipse` with + ``axes.transData`` as its transform. + **kwargs + Forwarded to `Collection`. + + """ + _path_generator = mpath.Path.unit_rectangle + + class PatchCollection(Collection): """ A generic collection of patches. diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index e4c46229517f..d4c0164a3b3a 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -168,7 +168,7 @@ class EventCollection(LineCollection): class CircleCollection(_CollectionWithSizes): def __init__(self, sizes: float | ArrayLike, **kwargs) -> None: ... -class EllipseCollection(Collection): +class _CollectionWithWidthHeightAngle(Collection): def __init__( self, widths: ArrayLike, @@ -187,6 +187,9 @@ class EllipseCollection(Collection): def get_heights(self) -> ArrayLike: ... def get_angles(self) -> ArrayLike: ... +class EllipseCollection(_CollectionWithWidthHeightAngle): ... +class RectangleCollection(_CollectionWithWidthHeightAngle): ... + class PatchCollection(Collection): def __init__( self, patches: Iterable[Patch], *, match_original: bool = ..., **kwargs diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index e72eb1a9ca73..083f653bc580 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -799,6 +799,21 @@ def unit_regular_asterisk(cls, numVertices): """ return cls.unit_regular_star(numVertices, 0.0) + _half_unit_circle = None + + @classmethod + def half_unit_circle(cls): + """ + Return the readonly :class:`Path` of the half unit circle. + + For most cases, :func:`Path.circle` will be what you want. + """ + if cls._half_unit_circle is None: + cls._half_unit_circle = cls.circle( + center=(0, 0), radius=0.5, readonly=True + ) + return cls._half_unit_circle + _unit_circle = None @classmethod diff --git a/lib/matplotlib/path.pyi b/lib/matplotlib/path.pyi index 464fc6d9a912..3f9932591b7d 100644 --- a/lib/matplotlib/path.pyi +++ b/lib/matplotlib/path.pyi @@ -107,6 +107,8 @@ class Path: @classmethod def unit_regular_asterisk(cls, numVertices: int) -> Path: ... @classmethod + def half_unit_circle(cls) -> Path: ... + @classmethod def unit_circle(cls) -> Path: ... @classmethod def circle( diff --git a/lib/matplotlib/tests/baseline_images/test_collections/RectangleCollection_test_image.png b/lib/matplotlib/tests/baseline_images/test_collections/RectangleCollection_test_image.png new file mode 100644 index 0000000000000000000000000000000000000000..23c67ea29f9a9e56b0f69461369ec9e19d672f67 GIT binary patch literal 19203 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%0A7i?t>&B0HdkDdz=O0{EtT1 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 G{J#Lip9s4E literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 23e951b17a2f..e57ba4f83a2e 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -410,7 +410,30 @@ def test_EllipseCollection(): ax.autoscale_view() -def test_EllipseCollection_setter_getter(): +@image_comparison(['RectangleCollection_test_image.png'], remove_text=True) +def test_RectangleCollection(): + # Test basic functionality + fig, ax = plt.subplots() + x = np.arange(4) + y = np.arange(3) + X, Y = np.meshgrid(x, y) + XY = np.vstack((X.ravel(), Y.ravel())).T + + ww = X / x[-1] + hh = Y / y[-1] + aa = np.ones_like(ww) * 20 # first axis is 20 degrees CCW from x axis + + ec = mcollections.RectangleCollection( + ww, hh, aa, units='x', offsets=XY, offset_transform=ax.transData, + facecolors='none') + ax.add_collection(ec) + ax.autoscale_view() + + +@pytest.mark.parametrize( + 'Class', [mcollections.EllipseCollection, mcollections.RectangleCollection] + ) +def test_WidthHeightAngleCollection_setter_getter(Class): # Test widths, heights and angle setter rng = np.random.default_rng(0) @@ -421,7 +444,7 @@ def test_EllipseCollection_setter_getter(): fig, ax = plt.subplots() - ec = mcollections.EllipseCollection( + ec = Class( widths=widths, heights=heights, angles=angles, @@ -430,8 +453,8 @@ def test_EllipseCollection_setter_getter(): offset_transform=ax.transData, ) - assert_array_almost_equal(ec._widths, np.array(widths).ravel() * 0.5) - assert_array_almost_equal(ec._heights, np.array(heights).ravel() * 0.5) + assert_array_almost_equal(ec._widths, np.array(widths).ravel()) + assert_array_almost_equal(ec._heights, np.array(heights).ravel()) assert_array_almost_equal(ec._angles, np.deg2rad(angles).ravel()) assert_array_almost_equal(ec.get_widths(), widths) From 14a09aaa92b32238117de0e97389671478921790 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 2 Apr 2024 18:18:04 +0100 Subject: [PATCH 2/4] Add parameter to use the center or a corner of the rectangle for the position --- lib/matplotlib/collections.py | 10 +++++++++- lib/matplotlib/path.py | 14 ++++++++++++++ lib/matplotlib/path.pyi | 2 ++ lib/matplotlib/tests/test_collections.py | 11 +++++++++-- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index e48a327eec65..4afcc5da2065 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1874,14 +1874,22 @@ class RectangleCollection(_CollectionWithWidthHeightAngle): refer to the *offsets* data units. 'xy' differs from all others in that the angle as plotted varies with the aspect ratio, and equals the specified angle only when the aspect ratio is unity. Hence - it behaves the same as the `~.patches.Ellipse` with + it behaves the same as the `~.patches.Rectangle` with ``axes.transData`` as its transform. + centered : bool + Whether to use the center or the corner of the rectangle to + define the position of the rectangles. Default is False. **kwargs Forwarded to `Collection`. """ _path_generator = mpath.Path.unit_rectangle + def __init__(self, *args, **kwargs): + if kwargs.pop("centered", False): + self._path_generator = mpath.Path.unit_rectangle_centered + super().__init__(*args, **kwargs) + class PatchCollection(Collection): """ diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 083f653bc580..371e4f3e8e07 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -741,6 +741,20 @@ def unit_rectangle(cls): closed=True, readonly=True) return cls._unit_rectangle + _unit_rectangle_centered = None + + @classmethod + def unit_rectangle_centered(cls): + """ + Return a `Path` instance of the unit rectangle from (-0.5, -0.5) to (0.5, 0.5). + """ + if cls._unit_rectangle_centered is None: + cls._unit_rectangle_centered = cls( + [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5], [-0.5, -0.5]], + closed=True, readonly=True + ) + return cls._unit_rectangle_centered + _unit_regular_polygons = WeakValueDictionary() @classmethod diff --git a/lib/matplotlib/path.pyi b/lib/matplotlib/path.pyi index 3f9932591b7d..35977b4eab5f 100644 --- a/lib/matplotlib/path.pyi +++ b/lib/matplotlib/path.pyi @@ -101,6 +101,8 @@ class Path: @classmethod def unit_rectangle(cls) -> Path: ... @classmethod + def unit_rectangle_centered(cls) -> Path: ... + @classmethod def unit_regular_polygon(cls, numVertices: int) -> Path: ... @classmethod def unit_regular_star(cls, numVertices: int, innerCircle: float = ...) -> Path: ... diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index e57ba4f83a2e..0439a63030d7 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -431,9 +431,12 @@ def test_RectangleCollection(): @pytest.mark.parametrize( - 'Class', [mcollections.EllipseCollection, mcollections.RectangleCollection] + 'Class, centered', + [(mcollections.EllipseCollection, None), + (mcollections.RectangleCollection, False), + (mcollections.RectangleCollection, True)] ) -def test_WidthHeightAngleCollection_setter_getter(Class): +def test_WidthHeightAngleCollection_setter_getter(Class, centered): # Test widths, heights and angle setter rng = np.random.default_rng(0) @@ -444,6 +447,9 @@ def test_WidthHeightAngleCollection_setter_getter(Class): fig, ax = plt.subplots() + kwargs = {} + if centered is not None: + kwargs["centered"] = centered ec = Class( widths=widths, heights=heights, @@ -451,6 +457,7 @@ def test_WidthHeightAngleCollection_setter_getter(Class): offsets=offsets, units='x', offset_transform=ax.transData, + **kwargs, ) assert_array_almost_equal(ec._widths, np.array(widths).ravel()) From e62539cf6064b65f35585ccfd4a7748b6ab8087e Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 2 Aug 2023 20:31:03 +0100 Subject: [PATCH 3/4] Add whats_new entry --- .../add_RectangleCollection.rst | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 doc/users/next_whats_new/add_RectangleCollection.rst diff --git a/doc/users/next_whats_new/add_RectangleCollection.rst b/doc/users/next_whats_new/add_RectangleCollection.rst new file mode 100644 index 000000000000..ef0f0775ecfc --- /dev/null +++ b/doc/users/next_whats_new/add_RectangleCollection.rst @@ -0,0 +1,33 @@ +Add ``RectangleCollection`` +--------------------------- + +The `~matplotlib.collections.RectangleCollection` is added to create collection of `~matplotlib.patches.Rectangle` + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib.collections import RectangleCollection + import numpy as np + + rng = np.random.default_rng(0) + + widths = (2, ) + heights = (3, ) + angles = (45, ) + offsets = rng.random((10, 2)) * 10 + + fig, ax = plt.subplots() + + ec = RectangleCollection( + widths=widths, + heights=heights, + angles=angles, + offsets=offsets, + units='x', + offset_transform=ax.transData, + ) + + ax.add_collection(ec) + ax.set_xlim(-2, 12) + ax.set_ylim(-2, 12) From 6cd30bde34744a7d1eaf63822449e3f6938a1bb9 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 2 Apr 2024 19:20:38 +0100 Subject: [PATCH 4/4] Fix missing reference doc --- doc/missing-references.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/missing-references.json b/doc/missing-references.json index 1b0a6f9ef226..9b65f4bc4ab9 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -158,6 +158,12 @@ "lib/matplotlib/collections.py:docstring of matplotlib.collections.PolyCollection:1", "lib/matplotlib/collections.py:docstring of matplotlib.collections.RegularPolyCollection:1" ], + "matplotlib.collections._CollectionWithWidthHeightAngle": [ + "doc/api/artist_api.rst:202", + "doc/api/collections_api.rst:13", + "lib/matplotlib/collections.py:docstring of matplotlib.collections.EllipseCollection:1", + "lib/matplotlib/collections.py:docstring of matplotlib.collections.RectangleCollection:1" + ], "matplotlib.collections._MeshData": [ "doc/api/artist_api.rst:202", "doc/api/collections_api.rst:13", @@ -326,6 +332,18 @@ "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" ], + "matplotlib.collections._CollectionWithWidthHeightAngle.set_angles": [ + "lib/matplotlib/collections.py:docstring of matplotlib.artist.EllipseCollection.set:15", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.RectangleCollection.set:15" + ], + "matplotlib.collections._CollectionWithWidthHeightAngle.set_heights": [ + "lib/matplotlib/collections.py:docstring of matplotlib.artist.EllipseCollection.set:31", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.RectangleCollection.set:31" + ], + "matplotlib.collections._CollectionWithWidthHeightAngle.set_widths": [ + "lib/matplotlib/collections.py:docstring of matplotlib.artist.EllipseCollection.set:52", + "lib/matplotlib/collections.py:docstring of matplotlib.artist.RectangleCollection.set:52" + ], "matplotlib.collections._MeshData.set_array": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:160", "lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:17",