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

Skip to content

Add rectangle collection with rotation #26457

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions doc/missing-references.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions doc/users/next_whats_new/add_RectangleCollection.rst
Original file line number Diff line number Diff line change
@@ -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)
77 changes: 70 additions & 7 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand All @@ -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."""
Expand All @@ -1828,6 +1830,67 @@ 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.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):
"""
A generic collection of patches.
Expand Down
5 changes: 4 additions & 1 deletion lib/matplotlib/collections.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions lib/matplotlib/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -799,6 +813,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
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/path.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,16 @@ 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: ...
@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(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 34 additions & 4 deletions lib/matplotlib/tests/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,33 @@ 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, centered',
[(mcollections.EllipseCollection, None),
(mcollections.RectangleCollection, False),
(mcollections.RectangleCollection, True)]
)
def test_WidthHeightAngleCollection_setter_getter(Class, centered):
# Test widths, heights and angle setter
rng = np.random.default_rng(0)

Expand All @@ -421,17 +447,21 @@ def test_EllipseCollection_setter_getter():

fig, ax = plt.subplots()

ec = mcollections.EllipseCollection(
kwargs = {}
if centered is not None:
kwargs["centered"] = centered
ec = Class(
widths=widths,
heights=heights,
angles=angles,
offsets=offsets,
units='x',
offset_transform=ax.transData,
**kwargs,
)

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)
Expand Down