From 0aea4447b39e17fefd6bc4457cbdacd59d108e70 Mon Sep 17 00:00:00 2001 From: Bruno Wolf Date: Wed, 4 Jun 2025 11:44:54 -0300 Subject: [PATCH 1/3] Add consistency check for PathCollection sizes and edgecolors (#28833) This patch adds a check_consistency() method to PathCollection, which emits a warning if the number of sizes or edgecolors does not match the number of paths. The check is invoked during the draw() call to ensure it happens after all data is set. Also adds a unit test that triggers the warning for mismatched sizes. --- lib/matplotlib/collections.py | 54 ++++++++++++++++++++++++ lib/matplotlib/tests/test_collections.py | 13 +++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ec6d40805da0..75a52d819ede 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1131,6 +1131,45 @@ def __init__(self, paths, sizes=None, **kwargs): def get_paths(self): return self._paths + + def check_consistency(self): + """ + Emit warnings if the lengths of certain properties do not match + the number of paths. + + This method checks whether the lengths of `sizes` and `edgecolors` + are either 1 or equal to the number of paths in the collection. + If not, a warning is issued. + + This is useful for identifying potential bugs or silent mismatches + in collection properties when plotting, especially when properties + are expected to be applied per-path. + + Notes + ----- + - This method does not raise an error, only a warning. + - It is recommended to call this at draw-time to ensure properties + have been fully set. + + Warnings + -------- + UserWarning + If the number of sizes or edgecolors does not match the number + of paths and is not a singleton (length 1). + + Examples + -------- + >>> paths = [Path(...), Path(...), Path(...)] + >>> pc = PathCollection(paths, sizes=[10, 20]) # 3 paths, 2 sizes + >>> pc.check_consistency() + UserWarning: Number of sizes does not match number of paths. + """ + n_paths = len(self.get_paths()) + if self._sizes is not None and len(self._sizes) not in (1, n_paths): + warnings.warn("Number of sizes does not match number of paths.") + if self._edgecolors is not None and len(self._edgecolors) not in (1, n_paths): + warnings.warn("Number of edgecolors does not match number of paths.") + def legend_elements(self, prop="colors", num="auto", fmt=None, func=lambda x: x, **kwargs): @@ -1271,6 +1310,21 @@ def legend_elements(self, prop="colors", num="auto", labels.append(l) return handles, labels + + def draw(self, renderer): + """ + Draw the collection using the given renderer. + + This override adds consistency checks before delegating + to the base implementation. + + Parameters + ---------- + renderer : RendererBase instance + The renderer to use for drawing. + """ + self.check_consistency() + return super().draw(renderer) class PolyCollection(_CollectionWithSizes): diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 642e5829a7b5..0654fa3418bf 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -16,10 +16,10 @@ import matplotlib.path as mpath import matplotlib.transforms as mtransforms from matplotlib.collections import (Collection, LineCollection, - EventCollection, PolyCollection) + EventCollection, PolyCollection, PathCollection) from matplotlib.collections import FillBetweenPolyCollection from matplotlib.testing.decorators import check_figures_equal, image_comparison - +from matplotlib.path import Path @pytest.fixture(params=["pcolormesh", "pcolor"]) def pcfunc(request): @@ -1533,3 +1533,12 @@ def mock_draw_path_collection(self, gc, master_transform, paths, all_transforms, ax.add_collection(coll) plt.draw() + +def test_pathcollection_size_mismatch_warning(): + + paths = [Path([(0, 0), (1, 0), (0.5, 1)]) for _ in range(5)] + fig, ax = plt.subplots() + with pytest.warns(UserWarning, match="Number of sizes does not match"): + pc = PathCollection(paths, sizes=[10, 20]) + ax.add_collection(pc) + fig.canvas.draw() From aeae6f3ce68c9f4e9b4270785bb0361dc2eea310 Mon Sep 17 00:00:00 2001 From: Bruno Wolf Date: Wed, 4 Jun 2025 11:57:23 -0300 Subject: [PATCH 2/3] Add check_consistency to PathCollection stub --- lib/matplotlib/collections.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/collections.pyi b/lib/matplotlib/collections.pyi index ecd969cfacc6..5158a9465ae2 100644 --- a/lib/matplotlib/collections.pyi +++ b/lib/matplotlib/collections.pyi @@ -86,6 +86,7 @@ class PathCollection(_CollectionWithSizes): def __init__( self, paths: Sequence[Path], sizes: ArrayLike | None = ..., **kwargs ) -> None: ... + def check_consistency(self) -> None: ... def set_paths(self, paths: Sequence[Path]) -> None: ... def get_paths(self) -> Sequence[Path]: ... def legend_elements( From d75790a93f2f6d624b7dcff0df7151bdea0a61b2 Mon Sep 17 00:00:00 2001 From: Bruno Wolf Date: Wed, 4 Jun 2025 12:01:06 -0300 Subject: [PATCH 3/3] Fix ruff linting issues --- lib/matplotlib/collections.py | 4 ++-- lib/matplotlib/tests/test_collections.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 75a52d819ede..69c16f6d5662 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -1131,7 +1131,7 @@ def __init__(self, paths, sizes=None, **kwargs): def get_paths(self): return self._paths - + def check_consistency(self): """ Emit warnings if the lengths of certain properties do not match @@ -1310,7 +1310,7 @@ def legend_elements(self, prop="colors", num="auto", labels.append(l) return handles, labels - + def draw(self, renderer): """ Draw the collection using the given renderer. diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 0654fa3418bf..1bcd3769e168 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -21,6 +21,7 @@ from matplotlib.testing.decorators import check_figures_equal, image_comparison from matplotlib.path import Path + @pytest.fixture(params=["pcolormesh", "pcolor"]) def pcfunc(request): return request.param @@ -1534,6 +1535,7 @@ def mock_draw_path_collection(self, gc, master_transform, paths, all_transforms, plt.draw() + def test_pathcollection_size_mismatch_warning(): paths = [Path([(0, 0), (1, 0), (0.5, 1)]) for _ in range(5)] @@ -1541,4 +1543,4 @@ def test_pathcollection_size_mismatch_warning(): with pytest.warns(UserWarning, match="Number of sizes does not match"): pc = PathCollection(paths, sizes=[10, 20]) ax.add_collection(pc) - fig.canvas.draw() + fig.canvas.draw()