From b26858558b3cf64d76d8d45ae8b6fa3993bbd622 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 29 Jul 2023 09:29:30 +0100 Subject: [PATCH 1/7] Add `set_U`, `set_V` and `set_C` method to `matplotlib.quiver.Quiver` --- lib/matplotlib/quiver.py | 56 +++++++++++++++++++++++- lib/matplotlib/quiver.pyi | 5 ++- lib/matplotlib/tests/test_collections.py | 51 +++++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 8fa1962d6321..a53778074abd 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -444,8 +444,8 @@ class Quiver(mcollections.PolyCollection): """ Specialized PolyCollection for arrows. - The only API method is set_UVC(), which can be used - to change the size, orientation, and color of the + The API methods are set_UVC(), set_U(), set_V() and set_C(), which + can be used to change the size, orientation, and color of the arrows; their locations are fixed when the class is instantiated. Possibly this method will be useful in animations. @@ -540,7 +540,59 @@ def draw(self, renderer): super().draw(renderer) self.stale = False + def set_U(self, U): + """ + Set x direction components of the arrow vectors. + + Parameters + ---------- + U : array-like or None + The size must the same as the existing U, V or be one. + """ + self.set_UVC(U, None, None) + + def set_V(self, V): + """ + Set y direction components of the arrow vectors. + + Parameters + ---------- + V : array-like or None + The size must the same as the existing U, V or be one. + """ + self.set_UVC(None, V, None) + + def set_C(self, C): + """ + Set the arrow colors. + + Parameters + ---------- + C : array-like or None + The size must the same as the existing U, V or be one. + """ + self.set_UVC(None, None, C) + def set_UVC(self, U, V, C=None): + """ + Set the U, V (x and y direction components of the arrow vectors) and + C (arrow colors) values of the arrows. + + Parameters + ---------- + U : array-like or None + The x direction components of the arrows. If None it is unchanged. + The size must the same as the existing U, V or be one. + V : array-like or None + The y direction components of the arrows. If None it is unchanged. + The size must the same as the existing U, V or be one. + C : array-like or None, optional + The arrow colors. The default is None. + """ + if U is None: + U = self.U + if V is None: + V = self.V # We need to ensure we have a copy, not a reference # to an array that might change before draw(). U = ma.masked_invalid(U, copy=True).ravel() diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index 2a043a92b4b5..c86d519fee89 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -122,8 +122,11 @@ class Quiver(mcollections.PolyCollection): **kwargs ) -> None: ... def get_datalim(self, transData: Transform) -> Bbox: ... + def set_U(self, U: ArrayLike) -> None: ... + def set_V(self, V: ArrayLike) -> None: ... + def set_C(self, C: ArrayLike) -> None: ... def set_UVC( - self, U: ArrayLike, V: ArrayLike, C: ArrayLike | None = ... + self, U: ArrayLike | None, V: ArrayLike | None, C: ArrayLike | None = ... ) -> None: ... class Barbs(mcollections.PolyCollection): diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 5baaeaa5d388..c80ab7264907 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -13,6 +13,7 @@ import matplotlib.collections as mcollections import matplotlib.colors as mcolors import matplotlib.path as mpath +import matplotlib.quiver as mquiver import matplotlib.transforms as mtransforms from matplotlib.collections import (Collection, LineCollection, EventCollection, PolyCollection) @@ -357,6 +358,56 @@ def test_collection_log_datalim(fig_test, fig_ref): ax_ref.plot(x, y, marker="o", ls="") +def test_quiver_offsets(): + fig, ax = plt.subplots() + x = np.arange(-10, 10, 1) + y = np.arange(-10, 10, 1) + U, V = np.meshgrid(x, y) + X = U.ravel() + Y = V.ravel() + qc = mquiver.Quiver(ax, X, Y, U, V) + ax.add_collection(qc) + ax.autoscale_view() + + expected_offsets = np.column_stack([X, Y]) + np.testing.assert_allclose(expected_offsets, qc.get_offsets()) + + new_offsets = np.column_stack([(X + 10).ravel(), Y.ravel()]) + qc.set_offsets(new_offsets) + + np.testing.assert_allclose(qc.get_offsets(), new_offsets) + + +def test_quiver_UVC(): + fig, ax = plt.subplots() + X = np.arange(-10, 10, 1) + Y = np.arange(-10, 10, 1) + U, V = np.meshgrid(X, Y) + M = np.hypot(U, V) + qc = mquiver.Quiver( + ax, X, Y, U, V, M + ) + ax.add_collection(qc) + ax.autoscale_view() + + np.testing.assert_allclose(qc.U, U.ravel()) + np.testing.assert_allclose(qc.V, V.ravel()) + np.testing.assert_allclose(qc.get_array(), M.ravel()) + + qc.set_UVC(U/2, V/3) + np.testing.assert_allclose(qc.U, U.ravel() / 2) + np.testing.assert_allclose(qc.V, V.ravel() / 3) + + qc.set_U(U/4) + np.testing.assert_allclose(qc.U, U.ravel() / 4) + + qc.set_V(V/6) + np.testing.assert_allclose(qc.V, V.ravel() / 6) + + qc.set_C(M/10) + np.testing.assert_allclose(qc.get_array(), M.ravel() / 10) + + def test_quiver_limits(): ax = plt.axes() x, y = np.arange(8), np.arange(10) From 10adf0b6b906f6989fc93a77951679bfb63bde33 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 5 Sep 2023 20:04:03 +0100 Subject: [PATCH 2/7] Add `set_offsets` to `quiver` and make the attribute (`N` and `XY`) properties to avoid inconsistent state of `quiver` --- lib/matplotlib/quiver.py | 26 +++++++++++++++++++++--- lib/matplotlib/quiver.pyi | 7 +++++-- lib/matplotlib/tests/test_collections.py | 3 +++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index a53778074abd..54a8e096f219 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -447,7 +447,7 @@ class Quiver(mcollections.PolyCollection): The API methods are set_UVC(), set_U(), set_V() and set_C(), which can be used to change the size, orientation, and color of the arrows; their locations are fixed when the class is - instantiated. Possibly this method will be useful + instantiated. Possibly these methods will be useful in animations. Much of the work in this class is done in the draw() @@ -475,8 +475,6 @@ def __init__(self, ax, *args, X, Y, U, V, C = _parse_args(*args, caller_name='quiver') self.X = X self.Y = Y - self.XY = np.column_stack((X, Y)) - self.N = len(X) self.scale = scale self.headwidth = headwidth self.headlength = float(headlength) @@ -523,6 +521,14 @@ def _init(self): self._dpi_at_last_init = self.axes.figure.dpi + @property + def N(self): + return len(self.X) + + @property + def XY(self): + return np.column_stack((self.X, self.Y)) + def get_datalim(self, transData): trans = self.get_transform() offset_trf = self.get_offset_transform() @@ -588,6 +594,7 @@ def set_UVC(self, U, V, C=None): The size must the same as the existing U, V or be one. C : array-like or None, optional The arrow colors. The default is None. + The size must the same as the existing U, V or be one. """ if U is None: U = self.U @@ -619,6 +626,19 @@ def set_UVC(self, U, V, C=None): self.set_array(C) self.stale = True + def set_offsets(self, xy): + """ + Set the offsets for the arrows. This saves the offsets passed + in and masks them as appropriate for the existing X/Y data. + + Parameters + ---------- + xy : sequence of pairs of floats + """ + self.X, self.Y = xy[:, 0], xy[:, 1] + super().set_offsets(xy) + self.stale = True + def _dots_per_unit(self, units): """Return a scale factor for converting from units to pixels.""" bb = self.axes.bbox diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index c86d519fee89..f210dca15b30 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -54,11 +54,9 @@ class QuiverKey(martist.Artist): class Quiver(mcollections.PolyCollection): X: ArrayLike Y: ArrayLike - XY: ArrayLike U: ArrayLike V: ArrayLike Umask: ArrayLike - N: int scale: float | None headwidth: float headlength: float @@ -121,6 +119,10 @@ class Quiver(mcollections.PolyCollection): pivot: Literal["tail", "mid", "middle", "tip"] = ..., **kwargs ) -> None: ... + @property + def N(self) -> int: ... + @property + def XY(self) -> ArrayLike: ... def get_datalim(self, transData: Transform) -> Bbox: ... def set_U(self, U: ArrayLike) -> None: ... def set_V(self, V: ArrayLike) -> None: ... @@ -128,6 +130,7 @@ class Quiver(mcollections.PolyCollection): def set_UVC( self, U: ArrayLike | None, V: ArrayLike | None, C: ArrayLike | None = ... ) -> None: ... + def set_offsets(self, xy: ArrayLike) -> None: ... class Barbs(mcollections.PolyCollection): sizes: dict[str, float] diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index c80ab7264907..145805e6e816 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -376,6 +376,9 @@ def test_quiver_offsets(): qc.set_offsets(new_offsets) np.testing.assert_allclose(qc.get_offsets(), new_offsets) + np.testing.assert_allclose(qc.X, new_offsets[::, 0]) + np.testing.assert_allclose(qc.Y, new_offsets[::, 1]) + np.testing.assert_allclose(qc.XY, new_offsets) def test_quiver_UVC(): From 92848b2162148f351b8ba6b7a484ef8a198dd86d Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Wed, 6 Sep 2023 11:36:17 +0100 Subject: [PATCH 3/7] Add release note --- .../next_whats_new/add_Quiver_setters.rst | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 doc/users/next_whats_new/add_Quiver_setters.rst diff --git a/doc/users/next_whats_new/add_Quiver_setters.rst b/doc/users/next_whats_new/add_Quiver_setters.rst new file mode 100644 index 000000000000..6d9c4e1b76c0 --- /dev/null +++ b/doc/users/next_whats_new/add_Quiver_setters.rst @@ -0,0 +1,50 @@ +Add ``U``, ``V`` and ``C`` setter to ``Quiver`` +----------------------------------------------- + +The ``U``, ``V`` and ``C`` values of the `~matplotlib.quiver.Quiver` +can now be changed after the collection has been created. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib.quiver import Quiver + import numpy as np + + fig, ax = plt.subplots() + X = np.arange(-10, 10, 1) + Y = np.arange(-10, 10, 1) + U, V = np.meshgrid(X, Y) + C = np.hypot(U, V) + qc = ax.quiver(X, Y, U, V, C) + + qc.set_U(U/5) + + +The number of arrows can also be changed. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + from matplotlib.quiver import Quiver + import numpy as np + + fig, ax = plt.subplots() + X = np.arange(-10, 10, 1) + Y = np.arange(-10, 10, 1) + U, V = np.meshgrid(X, Y) + C = np.hypot(U, V) + qc = ax.quiver(X, Y, U, V, C) + + # Get new X, Y, U, V, C + X = np.arange(-10, 10, 2) + Y = np.arange(-10, 10, 2) + U, V = np.meshgrid(X, Y) + C = np.hypot(U, V) + X, Y = np.meshgrid(X, Y) + XY = np.column_stack((X.ravel(), Y.ravel())) + + # Set new values + qc.set_offsets(XY) + qc.set_UVC(U, V, C) From e5b42b18eb46a4e81524a1fc12c10d057f40a95b Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 23 Mar 2024 13:19:10 +0000 Subject: [PATCH 4/7] Add `set_XYUVC` method to be used for all quiver setter --- .../next_whats_new/add_Quiver_setters.rst | 24 +- lib/matplotlib/quiver.py | 291 +++++++++++++----- lib/matplotlib/tests/test_collections.py | 64 +++- lib/matplotlib/tests/test_quiver.py | 6 +- 4 files changed, 275 insertions(+), 110 deletions(-) diff --git a/doc/users/next_whats_new/add_Quiver_setters.rst b/doc/users/next_whats_new/add_Quiver_setters.rst index 6d9c4e1b76c0..5265d3a332fc 100644 --- a/doc/users/next_whats_new/add_Quiver_setters.rst +++ b/doc/users/next_whats_new/add_Quiver_setters.rst @@ -16,35 +16,21 @@ can now be changed after the collection has been created. Y = np.arange(-10, 10, 1) U, V = np.meshgrid(X, Y) C = np.hypot(U, V) + # When X and Y are 1D and U, V are 2D, X, Y are expanded to 2D + # using X, Y = np.meshgrid(X, Y) qc = ax.quiver(X, Y, U, V, C) qc.set_U(U/5) - -The number of arrows can also be changed. - -.. plot:: - :include-source: true - - import matplotlib.pyplot as plt - from matplotlib.quiver import Quiver - import numpy as np - - fig, ax = plt.subplots() - X = np.arange(-10, 10, 1) - Y = np.arange(-10, 10, 1) - U, V = np.meshgrid(X, Y) - C = np.hypot(U, V) - qc = ax.quiver(X, Y, U, V, C) + # The number of arrows can also be changed. # Get new X, Y, U, V, C X = np.arange(-10, 10, 2) Y = np.arange(-10, 10, 2) U, V = np.meshgrid(X, Y) C = np.hypot(U, V) + # Use 2D X, Y coordinate (X, Y will not be expanded to 2D) X, Y = np.meshgrid(X, Y) - XY = np.column_stack((X.ravel(), Y.ravel())) # Set new values - qc.set_offsets(XY) - qc.set_UVC(U, V, C) + qc.set_XYUVC(X, Y, U, V, C) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 54a8e096f219..aa84f0132841 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -15,6 +15,7 @@ """ import math +from numbers import Number import numpy as np from numpy import ma @@ -417,21 +418,30 @@ def _parse_args(*args, caller_name='function'): else: raise _api.nargs_error(caller_name, takes="from 2 to 5", given=nargs) - nr, nc = (1, U.shape[0]) if U.ndim == 1 else U.shape + nr, nc = _extract_nr_nc(U) - if X is not None: - X = X.ravel() - Y = Y.ravel() - if len(X) == nc and len(Y) == nr: - X, Y = [a.ravel() for a in np.meshgrid(X, Y)] - elif len(X) != len(Y): - raise ValueError('X and Y must be the same size, but ' - f'X.size is {X.size} and Y.size is {Y.size}.') - else: + if X is None: indexgrid = np.meshgrid(np.arange(nc), np.arange(nr)) X, Y = [np.ravel(a) for a in indexgrid] - # Size validation for U, V, C is left to the set_UVC method. - return X, Y, U, V, C + # Size validation for U, V, C is left to the set_XYUVC method. + return X, Y, U, V, C, nr, nc + + +def _process_XY(X, Y, nc, nr): + X = X.ravel() + Y = Y.ravel() + if len(X) == nc and len(Y) == nr: + X, Y = [a.ravel() for a in np.meshgrid(X, Y)] + elif len(X) != len(Y): + raise ValueError( + 'X and Y must be the same size, but ' + f'X.size is {X.size} and Y.size is {Y.size}.' + ) + return X, Y + + +def _extract_nr_nc(U): + return (1, U.shape[0]) if U.ndim == 1 else U.shape def _check_consistent_shapes(*arrays): @@ -472,9 +482,7 @@ def __init__(self, ax, *args, %s """ self._axes = ax # The attr actually set by the Artist.axes property. - X, Y, U, V, C = _parse_args(*args, caller_name='quiver') - self.X = X - self.Y = Y + self.scale = scale self.headwidth = headwidth self.headlength = float(headlength) @@ -494,10 +502,16 @@ def __init__(self, ax, *args, self.transform = kwargs.pop('transform', ax.transData) kwargs.setdefault('facecolors', color) kwargs.setdefault('linewidths', (0,)) - super().__init__([], offsets=self.XY, offset_transform=self.transform, - closed=False, **kwargs) + super().__init__( + [], offset_transform=self.transform, closed=False, **kwargs + ) self.polykw = kwargs - self.set_UVC(U, V, C) + + self._U = self._V = self._C = None + X, Y, U, V, C, self._nr, self._nc = _parse_args( + *args, caller_name='quiver()' + ) + self.set_XYUVC(X=X, Y=Y, U=U, V=V, C=C) self._dpi_at_last_init = None def _init(self): @@ -511,29 +525,89 @@ def _init(self): trans = self._set_transform() self.span = trans.inverted().transform_bbox(self.axes.bbox).width if self.width is None: - sn = np.clip(math.sqrt(self.N), 8, 25) + sn = np.clip(math.sqrt(len(self.get_offsets())), 8, 25) self.width = 0.06 * self.span / sn # _make_verts sets self.scale if not already specified if (self._dpi_at_last_init != self.axes.figure.dpi and self.scale is None): - self._make_verts(self.XY, self.U, self.V, self.angles) + self._make_verts(self.get_offsets(), self._U, self._V, self.angles) self._dpi_at_last_init = self.axes.figure.dpi @property def N(self): - return len(self.X) + _api.warn_deprecated("3.9", alternative="get_X().size") + return len(self.get_X()) + + @property + def X(self): + _api.warn_deprecated("3.9", alternative="get_X") + return self.get_X() + + @X.setter + def X(self): + _api.warn_deprecated("3.9", alternative="set_X") + return self.set_X() + + @property + def Y(self): + _api.warn_deprecated("3.9", alternative="get_Y") + return self.get_Y() + + @Y.setter + def Y(self): + _api.warn_deprecated("3.9", alternative="set_Y") + return self.set_Y() + + @property + def U(self): + _api.warn_deprecated("3.9", alternative="get_U") + return self.get_U() + + @U.setter + def U(self): + _api.warn_deprecated("3.9", alternative="set_U") + return self.set_U() + + @property + def V(self): + _api.warn_deprecated("3.9", alternative="get_V") + return self.get_V() + + @V.setter + def V(self): + _api.warn_deprecated("3.9", alternative="set_V") + return self.set_V() + + @property + def C(self): + _api.warn_deprecated("3.9", alternative="get_C") + return self.get_C() + + @C.setter + def C(self): + _api.warn_deprecated("3.9", alternative="set_C") + return self.set_C() @property def XY(self): - return np.column_stack((self.X, self.Y)) + _api.warn_deprecated("3.9", alternative="get_XY") + return self.get_offsets() + + @XY.setter + def XY(self, XY): + _api.warn_deprecated("3.9", alternative="set_XY") + self.set_offsets(offsets=XY) + + def set_offsets(self, offsets): + self.set_XYUVC(X=offsets[:, 0], Y=offsets[:, 1]) def get_datalim(self, transData): trans = self.get_transform() offset_trf = self.get_offset_transform() full_transform = (trans - transData) + (offset_trf - transData) - XY = full_transform.transform(self.XY) + XY = full_transform.transform(self.get_offsets()) bbox = transforms.Bbox.null() bbox.update_from_data_xy(XY, ignore=True) return bbox @@ -541,32 +615,87 @@ def get_datalim(self, transData): @martist.allow_rasterization def draw(self, renderer): self._init() - verts = self._make_verts(self.XY, self.U, self.V, self.angles) + verts = self._make_verts(self.get_offsets(), self._U, self._V, self.angles) self.set_verts(verts, closed=False) super().draw(renderer) self.stale = False + def get_XY(self): + """Returns the positions. Alias for ``get_offsets``.""" + return self.get_offsets() + + def set_XY(self, XY): + """ + Set positions. Alias for ``set_offsets``. If the size + changes and it is not compatible with ``U``, ``V`` or + ``C``, use ``set_XYUVC`` instead. + + Parameters + ---------- + X : array-like + The size must be compatible with ``U``, ``V`` and ``C``. + """ + self.set_offsets(offsets=XY) + + def set_X(self, X): + """ + Set positions in the horizontal direction. + + Parameters + ---------- + X : array-like + The size must the same as the existing Y. + """ + self.set_XYUVC(X=X) + + def get_X(self): + """Returns the positions in the horizontal direction.""" + return self.get_offsets()[..., 0] + + def set_Y(self, Y): + """ + Set positions in the vertical direction. + + Parameters + ---------- + Y : array-like + The size must the same as the existing X. + """ + self.set_XYUVC(Y=Y) + + def get_Y(self): + """Returns the positions in the vertical direction.""" + return self.get_offsets()[..., 1] + def set_U(self, U): """ - Set x direction components of the arrow vectors. + Set horizontal direction components. Parameters ---------- - U : array-like or None - The size must the same as the existing U, V or be one. + U : array-like + The size must the same as the existing X, Y, V or be one. """ - self.set_UVC(U, None, None) + self.set_XYUVC(U=U) + + def get_U(self): + """Returns the horizontal direction components.""" + return self._U def set_V(self, V): """ - Set y direction components of the arrow vectors. + Set vertical direction components. Parameters ---------- - V : array-like or None - The size must the same as the existing U, V or be one. + V : array-like + The size must the same as the existing X, Y, U or be one. """ - self.set_UVC(None, V, None) + self.set_XYUVC(V=V) + + def get_V(self): + """Returns the vertical direction components.""" + return self._V def set_C(self, C): """ @@ -574,44 +703,63 @@ def set_C(self, C): Parameters ---------- - C : array-like or None - The size must the same as the existing U, V or be one. + C : array-like + The size must the same as the existing X, Y, U, V or be one. """ - self.set_UVC(None, None, C) + self.set_XYUVC(C=C) - def set_UVC(self, U, V, C=None): + def get_C(self): + """Returns the arrow colors.""" + return self._C + + def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None): """ - Set the U, V (x and y direction components of the arrow vectors) and - C (arrow colors) values of the arrows. + Set the positions (X, Y) and components (U, V) of the arrow vectors + and arrow colors (C) values of the arrows. + The size of the array must match with existing values. To change + the size, all arguments must be changed at once and their size + compatible. Parameters ---------- - U : array-like or None - The x direction components of the arrows. If None it is unchanged. - The size must the same as the existing U, V or be one. - V : array-like or None - The y direction components of the arrows. If None it is unchanged. + X, Y : array-like of float or None, optional + The arrow locations in the horizontal and vertical directions. + Any shape is valid so long as X and Y have the same size. + U, V : array-like or None, optional + The horizontal and vertical direction components of the arrows. + If None it is unchanged. The size must the same as the existing U, V or be one. C : array-like or None, optional The arrow colors. The default is None. The size must the same as the existing U, V or be one. """ - if U is None: - U = self.U - if V is None: - V = self.V + + X = self.get_X() if X is None else X + Y = self.get_Y() if Y is None else Y + if U is None or isinstance(U, Number): + nr, nc = (self._nr, self._nc) + else: + nr, nc = _extract_nr_nc(U) + X, Y = _process_XY(X, Y, nc, nr) + N = len(X) + # We need to ensure we have a copy, not a reference # to an array that might change before draw(). - U = ma.masked_invalid(U, copy=True).ravel() - V = ma.masked_invalid(V, copy=True).ravel() - if C is not None: - C = ma.masked_invalid(C, copy=True).ravel() + U = ma.masked_invalid(self._U if U is None else U, copy=True).ravel() + V = ma.masked_invalid(self._V if V is None else V, copy=True).ravel() + if C is not None or self._C is not None: + C = ma.masked_invalid( + self._C if C is None else C, copy=True + ).ravel() for name, var in zip(('U', 'V', 'C'), (U, V, C)): - if not (var is None or var.size == self.N or var.size == 1): - raise ValueError(f'Argument {name} has a size {var.size}' - f' which does not match {self.N},' - ' the number of arrow positions') - + if not (var is None or var.size == N or var.size == 1): + raise ValueError( + f'Argument {name} has a size {var.size}' + f' which does not match {N},' + ' the number of arrow positions' + ) + + # now shapes are validated and we can start assigning things mask = ma.mask_or(U.mask, V.mask, copy=False, shrink=True) if C is not None: mask = ma.mask_or(mask, C.mask, copy=False, shrink=True) @@ -619,24 +767,14 @@ def set_UVC(self, U, V, C=None): C = C.filled() else: C = ma.array(C, mask=mask, copy=False) - self.U = U.filled(1) - self.V = V.filled(1) + self._U = U.filled(1) + self._V = V.filled(1) self.Umask = mask if C is not None: self.set_array(C) - self.stale = True - - def set_offsets(self, xy): - """ - Set the offsets for the arrows. This saves the offsets passed - in and masks them as appropriate for the existing X/Y data. - - Parameters - ---------- - xy : sequence of pairs of floats - """ - self.X, self.Y = xy[:, 0], xy[:, 1] - super().set_offsets(xy) + self._N = N + self._new_UV = True + super().set_offsets(np.column_stack([X, Y])) self.stale = True def _dots_per_unit(self, units): @@ -697,7 +835,7 @@ def _make_verts(self, XY, U, V, angles): a = np.abs(uv) if self.scale is None: - sn = max(10, math.sqrt(self.N)) + sn = max(10, math.sqrt(len(self.get_offsets()))) if self.Umask is not ma.nomask: amean = a[~self.Umask].mean() else: @@ -1000,10 +1138,11 @@ def __init__(self, ax, *args, kwargs['linewidth'] = 1 # Parse out the data arrays from the various configurations supported - x, y, u, v, c = _parse_args(*args, caller_name='barbs') - self.x = x - self.y = y - xy = np.column_stack((x, y)) + x, y, u, v, c, self._nr, self._nc = _parse_args( + *args, caller_name='barbs()' + ) + self.x, self.y = _process_XY(x, y, self._nr, self._nc) + xy = np.column_stack([self.x, self.y]) # Make a collection barb_size = self._length ** 2 / 4 # Empirically determined diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 145805e6e816..694e73299995 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -376,12 +376,39 @@ def test_quiver_offsets(): qc.set_offsets(new_offsets) np.testing.assert_allclose(qc.get_offsets(), new_offsets) - np.testing.assert_allclose(qc.X, new_offsets[::, 0]) - np.testing.assert_allclose(qc.Y, new_offsets[::, 1]) - np.testing.assert_allclose(qc.XY, new_offsets) + np.testing.assert_allclose(qc.get_X(), new_offsets[..., 0]) + np.testing.assert_allclose(qc.get_Y(), new_offsets[..., 1]) + new_X = qc.get_X() + 5 + qc.set_X(new_X) + np.testing.assert_allclose(qc.get_X(), new_X) -def test_quiver_UVC(): + new_Y = qc.get_Y() + 5 + qc.set_Y(new_Y) + np.testing.assert_allclose(qc.get_Y(), new_Y) + + # new length + L = 2 + with pytest.raises(ValueError): + qc.set_X(qc.get_X()[:L]) + + with pytest.raises(ValueError): + qc.set_Y(qc.get_Y()[:L]) + + with pytest.raises(ValueError): + qc.set_offsets(qc.get_offsets()[:L]) + + with pytest.raises(ValueError): + qc.set_XYUVC(X=new_X[:L], Y=new_Y[:L]) + + qc.set_XYUVC(X=X[:L], Y=Y[:L], U=qc.get_U()[:L], V=qc.get_V()[:L]) + np.testing.assert_allclose(qc.get_X(), X[:L]) + np.testing.assert_allclose(qc.get_Y(), Y[:L]) + np.testing.assert_allclose(qc.get_U(), U.ravel()[:L]) + np.testing.assert_allclose(qc.get_V(), V.ravel()[:L]) + + +def test_quiver_change_UVC(): fig, ax = plt.subplots() X = np.arange(-10, 10, 1) Y = np.arange(-10, 10, 1) @@ -393,23 +420,36 @@ def test_quiver_UVC(): ax.add_collection(qc) ax.autoscale_view() - np.testing.assert_allclose(qc.U, U.ravel()) - np.testing.assert_allclose(qc.V, V.ravel()) + np.testing.assert_allclose(qc.get_U(), U.ravel()) + np.testing.assert_allclose(qc.get_V(), V.ravel()) np.testing.assert_allclose(qc.get_array(), M.ravel()) - qc.set_UVC(U/2, V/3) - np.testing.assert_allclose(qc.U, U.ravel() / 2) - np.testing.assert_allclose(qc.V, V.ravel() / 3) + qc.set_XYUVC(U=U/2, V=V/3) + np.testing.assert_allclose(qc.get_U(), U.ravel() / 2) + np.testing.assert_allclose(qc.get_V(), V.ravel() / 3) qc.set_U(U/4) - np.testing.assert_allclose(qc.U, U.ravel() / 4) + np.testing.assert_allclose(qc.get_U(), U.ravel() / 4) qc.set_V(V/6) - np.testing.assert_allclose(qc.V, V.ravel() / 6) + np.testing.assert_allclose(qc.get_V(), V.ravel() / 6) - qc.set_C(M/10) + qc.set_C(C=M/10) np.testing.assert_allclose(qc.get_array(), M.ravel() / 10) + with pytest.raises(ValueError): + qc.set_X(X[:2]) + with pytest.raises(ValueError): + qc.set_Y(Y[:2]) + with pytest.raises(ValueError): + qc.set_U(U[:2]) + with pytest.raises(ValueError): + qc.set_V(V[:2]) + + qc.set_XYUVC() + np.testing.assert_allclose(qc.get_U(), U.ravel() / 4) + np.testing.assert_allclose(qc.get_V(), V.ravel() / 6) + def test_quiver_limits(): ax = plt.axes() diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index 7c5a9d343530..5b7262e3e920 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -24,7 +24,7 @@ def test_quiver_memory_leak(): fig, ax = plt.subplots() Q = draw_quiver(ax) - ttX = Q.X + ttX = Q.get_X() Q.remove() del Q @@ -133,7 +133,7 @@ def test_quiver_copy(): uv = dict(u=np.array([1.1]), v=np.array([2.0])) q0 = ax.quiver([1], [1], uv['u'], uv['v']) uv['v'][0] = 0 - assert q0.V[0] == 2.0 + assert q0.get_V()[0] == 2.0 @image_comparison(['quiver_key_pivot.png'], remove_text=True) @@ -332,4 +332,4 @@ def test_quiver_setuvc_numbers(): U = V = np.ones_like(X) q = ax.quiver(X, Y, U, V) - q.set_UVC(0, 1) + q.set_XYUVC(U=0, V=1) From c8d4924bc4226c85eaa81ac9e087b1369d401d58 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 23 Mar 2024 14:08:00 +0000 Subject: [PATCH 5/7] Privatise `quiver.Quiver.Umask` --- lib/matplotlib/quiver.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index aa84f0132841..08610e172741 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -325,7 +325,7 @@ def _init(self): self._set_transform() with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], # Hack: save and restore the Umask - Umask=ma.nomask): + _Umask=ma.nomask): u = self.U * np.cos(np.radians(self.angle)) v = self.U * np.sin(np.radians(self.angle)) self.verts = self.Q._make_verts([[0., 0.]], @@ -469,6 +469,7 @@ class Quiver(mcollections.PolyCollection): """ _PIVOT_VALS = ('tail', 'middle', 'tip') + Umask = _api.deprecate_privatize_attribute("3.9") @_docstring.Substitution(_quiver_doc) def __init__(self, ax, *args, @@ -769,7 +770,7 @@ def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None): C = ma.array(C, mask=mask, copy=False) self._U = U.filled(1) self._V = V.filled(1) - self.Umask = mask + self._Umask = mask if C is not None: self.set_array(C) self._N = N @@ -836,8 +837,8 @@ def _make_verts(self, XY, U, V, angles): if self.scale is None: sn = max(10, math.sqrt(len(self.get_offsets()))) - if self.Umask is not ma.nomask: - amean = a[~self.Umask].mean() + if self._Umask is not ma.nomask: + amean = a[~self._Umask].mean() else: amean = a.mean() # crude auto-scaling @@ -867,9 +868,9 @@ def _make_verts(self, XY, U, V, angles): theta = theta.reshape((-1, 1)) # for broadcasting xy = (X + Y * 1j) * np.exp(1j * theta) * self.width XY = np.stack((xy.real, xy.imag), axis=2) - if self.Umask is not ma.nomask: + if self._Umask is not ma.nomask: XY = ma.array(XY) - XY[self.Umask] = ma.masked + XY[self._Umask] = ma.masked # This might be handled more efficiently with nans, given # that nans will end up in the paths anyway. From aba8d9be333b2c33695903fe910eaa56242212a5 Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Sat, 23 Mar 2024 13:09:20 +0000 Subject: [PATCH 6/7] Fix typing --- lib/matplotlib/quiver.pyi | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index f210dca15b30..4b80f8940324 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -56,6 +56,8 @@ class Quiver(mcollections.PolyCollection): Y: ArrayLike U: ArrayLike V: ArrayLike + C: ArrayLike + XY: ArrayLike Umask: ArrayLike scale: float | None headwidth: float @@ -121,16 +123,28 @@ class Quiver(mcollections.PolyCollection): ) -> None: ... @property def N(self) -> int: ... - @property - def XY(self) -> ArrayLike: ... def get_datalim(self, transData: Transform) -> Bbox: ... + def set_offsets(self, offsets: ArrayLike) -> None: ... + def set_XY(self, XY: ArrayLike) -> None: ... + def get_XY(self) -> ArrayLike: ... + def set_X(self, X: ArrayLike) -> None: ... + def get_X(self) -> ArrayLike: ... + def set_Y(self, Y: ArrayLike) -> None: ... + def get_Y(self) -> ArrayLike: ... def set_U(self, U: ArrayLike) -> None: ... + def get_U(self) -> ArrayLike: ... def set_V(self, V: ArrayLike) -> None: ... + def get_V(self) -> ArrayLike: ... def set_C(self, C: ArrayLike) -> None: ... - def set_UVC( - self, U: ArrayLike | None, V: ArrayLike | None, C: ArrayLike | None = ... + def get_C(self) -> ArrayLike: ... + def set_XYUVC( + self, + X: ArrayLike | None = ..., + Y: ArrayLike | None = ..., + U: ArrayLike | None = ..., + V: ArrayLike | None = ..., + C: ArrayLike | None = ... ) -> None: ... - def set_offsets(self, xy: ArrayLike) -> None: ... class Barbs(mcollections.PolyCollection): sizes: dict[str, float] From 9b5bc95614dc182af4420aa2ff773ba0efc17c2a Mon Sep 17 00:00:00 2001 From: Eric Prestat Date: Tue, 26 Mar 2024 21:27:48 +0000 Subject: [PATCH 7/7] Don't check length of arguments in quiver collection and increase coverage --- lib/matplotlib/quiver.py | 84 ++++++++-------------- lib/matplotlib/quiver.pyi | 5 +- lib/matplotlib/tests/test_collections.py | 88 ++++++++++++++++-------- 3 files changed, 92 insertions(+), 85 deletions(-) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 08610e172741..20a61fe532dc 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -454,11 +454,10 @@ class Quiver(mcollections.PolyCollection): """ Specialized PolyCollection for arrows. - The API methods are set_UVC(), set_U(), set_V() and set_C(), which - can be used to change the size, orientation, and color of the - arrows; their locations are fixed when the class is - instantiated. Possibly these methods will be useful - in animations. + The API methods are set_XYUVC(), set_X(), set_Y(), set_U() and set_V(), + which can be used to change the size, orientation, and color of the + arrows; their locations are fixed when the class is instantiated. + Possibly these methods will be useful in animations. Much of the work in this class is done in the draw() method so that as much information as possible is available @@ -512,7 +511,7 @@ def __init__(self, ax, *args, X, Y, U, V, C, self._nr, self._nc = _parse_args( *args, caller_name='quiver()' ) - self.set_XYUVC(X=X, Y=Y, U=U, V=V, C=C) + self.set_XYUVC(X=X, Y=Y, U=U, V=V, C=C, check_shape=True) self._dpi_at_last_init = None def _init(self): @@ -547,9 +546,9 @@ def X(self): return self.get_X() @X.setter - def X(self): + def X(self, value): _api.warn_deprecated("3.9", alternative="set_X") - return self.set_X() + return self.set_X(value) @property def Y(self): @@ -557,9 +556,9 @@ def Y(self): return self.get_Y() @Y.setter - def Y(self): + def Y(self, value): _api.warn_deprecated("3.9", alternative="set_Y") - return self.set_Y() + return self.set_Y(value) @property def U(self): @@ -567,9 +566,9 @@ def U(self): return self.get_U() @U.setter - def U(self): + def U(self, value): _api.warn_deprecated("3.9", alternative="set_U") - return self.set_U() + return self.set_U(value) @property def V(self): @@ -577,29 +576,19 @@ def V(self): return self.get_V() @V.setter - def V(self): + def V(self, value): _api.warn_deprecated("3.9", alternative="set_V") - return self.set_V() - - @property - def C(self): - _api.warn_deprecated("3.9", alternative="get_C") - return self.get_C() - - @C.setter - def C(self): - _api.warn_deprecated("3.9", alternative="set_C") - return self.set_C() + return self.set_V(value) @property def XY(self): - _api.warn_deprecated("3.9", alternative="get_XY") + _api.warn_deprecated("3.9", alternative="get_offsets") return self.get_offsets() @XY.setter - def XY(self, XY): - _api.warn_deprecated("3.9", alternative="set_XY") - self.set_offsets(offsets=XY) + def XY(self, value): + _api.warn_deprecated("3.9", alternative="set_offsets") + self.set_offsets(offsets=value) def set_offsets(self, offsets): self.set_XYUVC(X=offsets[:, 0], Y=offsets[:, 1]) @@ -621,23 +610,6 @@ def draw(self, renderer): super().draw(renderer) self.stale = False - def get_XY(self): - """Returns the positions. Alias for ``get_offsets``.""" - return self.get_offsets() - - def set_XY(self, XY): - """ - Set positions. Alias for ``set_offsets``. If the size - changes and it is not compatible with ``U``, ``V`` or - ``C``, use ``set_XYUVC`` instead. - - Parameters - ---------- - X : array-like - The size must be compatible with ``U``, ``V`` and ``C``. - """ - self.set_offsets(offsets=XY) - def set_X(self, X): """ Set positions in the horizontal direction. @@ -711,9 +683,9 @@ def set_C(self, C): def get_C(self): """Returns the arrow colors.""" - return self._C + return self.get_array() - def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None): + def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None, check_shape=False): """ Set the positions (X, Y) and components (U, V) of the arrow vectors and arrow colors (C) values of the arrows. @@ -733,6 +705,9 @@ def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None): C : array-like or None, optional The arrow colors. The default is None. The size must the same as the existing U, V or be one. + check_shape : bool + Whether to check if the shape of the parameters are + consistent. Default is False. """ X = self.get_X() if X is None else X @@ -752,13 +727,14 @@ def set_XYUVC(self, X=None, Y=None, U=None, V=None, C=None): C = ma.masked_invalid( self._C if C is None else C, copy=True ).ravel() - for name, var in zip(('U', 'V', 'C'), (U, V, C)): - if not (var is None or var.size == N or var.size == 1): - raise ValueError( - f'Argument {name} has a size {var.size}' - f' which does not match {N},' - ' the number of arrow positions' - ) + if check_shape: + for name, var in zip(('U', 'V', 'C'), (U, V, C)): + if not (var is None or var.size == N or var.size == 1): + raise ValueError( + f'Argument {name} has a size {var.size}' + f' which does not match {N},' + ' the number of arrow positions' + ) # now shapes are validated and we can start assigning things mask = ma.mask_or(U.mask, V.mask, copy=False, shrink=True) diff --git a/lib/matplotlib/quiver.pyi b/lib/matplotlib/quiver.pyi index 4b80f8940324..33335446047f 100644 --- a/lib/matplotlib/quiver.pyi +++ b/lib/matplotlib/quiver.pyi @@ -125,8 +125,6 @@ class Quiver(mcollections.PolyCollection): def N(self) -> int: ... def get_datalim(self, transData: Transform) -> Bbox: ... def set_offsets(self, offsets: ArrayLike) -> None: ... - def set_XY(self, XY: ArrayLike) -> None: ... - def get_XY(self) -> ArrayLike: ... def set_X(self, X: ArrayLike) -> None: ... def get_X(self) -> ArrayLike: ... def set_Y(self, Y: ArrayLike) -> None: ... @@ -143,7 +141,8 @@ class Quiver(mcollections.PolyCollection): Y: ArrayLike | None = ..., U: ArrayLike | None = ..., V: ArrayLike | None = ..., - C: ArrayLike | None = ... + C: ArrayLike | None = ..., + check_shape: bool = ..., ) -> None: ... class Barbs(mcollections.PolyCollection): diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 694e73299995..1b63413927de 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -389,17 +389,9 @@ def test_quiver_offsets(): # new length L = 2 - with pytest.raises(ValueError): - qc.set_X(qc.get_X()[:L]) - - with pytest.raises(ValueError): - qc.set_Y(qc.get_Y()[:L]) - - with pytest.raises(ValueError): - qc.set_offsets(qc.get_offsets()[:L]) - - with pytest.raises(ValueError): - qc.set_XYUVC(X=new_X[:L], Y=new_Y[:L]) + qc.set_XYUVC(X=new_X[:L], Y=new_Y[:L]) + np.testing.assert_allclose(qc.get_X(), new_X[:L]) + np.testing.assert_allclose(qc.get_Y(), new_Y[:L]) qc.set_XYUVC(X=X[:L], Y=Y[:L], U=qc.get_U()[:L], V=qc.get_V()[:L]) np.testing.assert_allclose(qc.get_X(), X[:L]) @@ -408,23 +400,21 @@ def test_quiver_offsets(): np.testing.assert_allclose(qc.get_V(), V.ravel()[:L]) -def test_quiver_change_UVC(): +def test_quiver_change_XYUVC(): fig, ax = plt.subplots() X = np.arange(-10, 10, 1) Y = np.arange(-10, 10, 1) U, V = np.meshgrid(X, Y) - M = np.hypot(U, V) - qc = mquiver.Quiver( - ax, X, Y, U, V, M - ) + C = np.hypot(U, V) + qc = mquiver.Quiver(ax, X, Y, U, V, C) ax.add_collection(qc) ax.autoscale_view() np.testing.assert_allclose(qc.get_U(), U.ravel()) np.testing.assert_allclose(qc.get_V(), V.ravel()) - np.testing.assert_allclose(qc.get_array(), M.ravel()) + np.testing.assert_allclose(qc.get_C(), C.ravel()) - qc.set_XYUVC(U=U/2, V=V/3) + qc.set(U=U/2, V=V/3) np.testing.assert_allclose(qc.get_U(), U.ravel() / 2) np.testing.assert_allclose(qc.get_V(), V.ravel() / 3) @@ -434,23 +424,65 @@ def test_quiver_change_UVC(): qc.set_V(V/6) np.testing.assert_allclose(qc.get_V(), V.ravel() / 6) - qc.set_C(C=M/10) - np.testing.assert_allclose(qc.get_array(), M.ravel() / 10) + qc.set_C(C/3) + np.testing.assert_allclose(qc.get_C(), C.ravel() / 3) + # check consistency not enable + qc.set_XYUVC(X=X[:2], Y=Y[:2]) with pytest.raises(ValueError): - qc.set_X(X[:2]) - with pytest.raises(ValueError): - qc.set_Y(Y[:2]) - with pytest.raises(ValueError): - qc.set_U(U[:2]) - with pytest.raises(ValueError): - qc.set_V(V[:2]) + # setting only one of the two X, Y fails because X and Y needs + # to be stacked when passed to `offsets` + qc.set(Y=Y[:3]) - qc.set_XYUVC() + qc.set() np.testing.assert_allclose(qc.get_U(), U.ravel() / 4) np.testing.assert_allclose(qc.get_V(), V.ravel() / 6) +def test_quiver_deprecated_attribute(): + fig, ax = plt.subplots() + X = np.arange(-10, 10, 1) + Y = np.arange(-10, 10, 1) + U, V = np.meshgrid(X, Y) + C = np.hypot(U, V) + qc = mquiver.Quiver(ax, X, Y, U, V, C) + ax.add_collection(qc) + ax.autoscale_view() + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + x = qc.X + with pytest.warns(mpl.MatplotlibDeprecationWarning): + qc.X = x * 2 + np.testing.assert_allclose(qc.get_X(), x * 2) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + y = qc.Y + with pytest.warns(mpl.MatplotlibDeprecationWarning): + qc.Y = y * 2 + np.testing.assert_allclose(qc.get_Y(), y * 2) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + np.testing.assert_allclose(qc.N, len(qc.get_offsets())) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + u = qc.U + with pytest.warns(mpl.MatplotlibDeprecationWarning): + qc.U = u * 2 + np.testing.assert_allclose(qc.get_U(), u * 2) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + v = qc.V + with pytest.warns(mpl.MatplotlibDeprecationWarning): + qc.V = v * 2 + np.testing.assert_allclose(qc.get_V(), v * 2) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + xy = qc.XY + with pytest.warns(mpl.MatplotlibDeprecationWarning): + qc.XY = xy * 2 + np.testing.assert_allclose(qc.get_offsets(), xy * 2) + + def test_quiver_limits(): ax = plt.axes() x, y = np.arange(8), np.arange(10) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy