Skip to content

Commit dfd83c2

Browse files
authored
Merge pull request #22356 from timhoffm/triplot
Cleanup tripcolor()
2 parents 2fe38b5 + 703b574 commit dfd83c2

File tree

3 files changed

+245
-90
lines changed

3 files changed

+245
-90
lines changed

lib/matplotlib/tests/test_triangulation.py

+104
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,67 @@
1212
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1313

1414

15+
class TestTriangulationParams:
16+
x = [-1, 0, 1, 0]
17+
y = [0, -1, 0, 1]
18+
triangles = [[0, 1, 2], [0, 2, 3]]
19+
mask = [False, True]
20+
21+
@pytest.mark.parametrize('args, kwargs, expected', [
22+
([x, y], {}, [x, y, None, None]),
23+
([x, y, triangles], {}, [x, y, triangles, None]),
24+
([x, y], dict(triangles=triangles), [x, y, triangles, None]),
25+
([x, y], dict(mask=mask), [x, y, None, mask]),
26+
([x, y, triangles], dict(mask=mask), [x, y, triangles, mask]),
27+
([x, y], dict(triangles=triangles, mask=mask), [x, y, triangles, mask])
28+
])
29+
def test_extract_triangulation_params(self, args, kwargs, expected):
30+
other_args = [1, 2]
31+
other_kwargs = {'a': 3, 'b': '4'}
32+
x_, y_, triangles_, mask_, args_, kwargs_ = \
33+
mtri.Triangulation._extract_triangulation_params(
34+
args + other_args, {**kwargs, **other_kwargs})
35+
x, y, triangles, mask = expected
36+
assert x_ is x
37+
assert y_ is y
38+
assert_array_equal(triangles_, triangles)
39+
assert mask_ is mask
40+
assert args_ == other_args
41+
assert kwargs_ == other_kwargs
42+
43+
44+
def test_extract_triangulation_positional_mask():
45+
# mask cannot be passed positionally
46+
mask = [True]
47+
args = [[0, 2, 1], [0, 0, 1], [[0, 1, 2]], mask]
48+
x_, y_, triangles_, mask_, args_, kwargs_ = \
49+
mtri.Triangulation._extract_triangulation_params(args, {})
50+
assert mask_ is None
51+
assert args_ == [mask]
52+
# the positional mask must be caught downstream because this must pass
53+
# unknown args through
54+
55+
56+
def test_triangulation_init():
57+
x = [-1, 0, 1, 0]
58+
y = [0, -1, 0, 1]
59+
with pytest.raises(ValueError, match="x and y must be equal-length"):
60+
mtri.Triangulation(x, [1, 2])
61+
with pytest.raises(
62+
ValueError,
63+
match=r"triangles must be a \(N, 3\) int array, but found shape "
64+
r"\(3,\)"):
65+
mtri.Triangulation(x, y, [0, 1, 2])
66+
with pytest.raises(
67+
ValueError,
68+
match=r"triangles must be a \(N, 3\) int array, not 'other'"):
69+
mtri.Triangulation(x, y, 'other')
70+
with pytest.raises(ValueError, match="found value 99"):
71+
mtri.Triangulation(x, y, [[0, 1, 99]])
72+
with pytest.raises(ValueError, match="found value -1"):
73+
mtri.Triangulation(x, y, [[0, 1, -1]])
74+
75+
1576
def test_delaunay():
1677
# No duplicate points, regular grid.
1778
nx = 5
@@ -177,6 +238,49 @@ def test_tripcolor():
177238
plt.title('facecolors')
178239

179240

241+
def test_tripcolor_color():
242+
x = [-1, 0, 1, 0]
243+
y = [0, -1, 0, 1]
244+
fig, ax = plt.subplots()
245+
with pytest.raises(ValueError, match="Missing color parameter"):
246+
ax.tripcolor(x, y)
247+
with pytest.raises(ValueError, match="The length of C must match either"):
248+
ax.tripcolor(x, y, [1, 2, 3])
249+
with pytest.raises(ValueError,
250+
match="length of facecolors must match .* triangles"):
251+
ax.tripcolor(x, y, facecolors=[1, 2, 3, 4])
252+
with pytest.raises(ValueError,
253+
match="'gouraud' .* at the points.* not at the faces"):
254+
ax.tripcolor(x, y, facecolors=[1, 2], shading='gouraud')
255+
with pytest.raises(ValueError,
256+
match="'gouraud' .* at the points.* not at the faces"):
257+
ax.tripcolor(x, y, [1, 2], shading='gouraud') # faces
258+
with pytest.raises(ValueError,
259+
match=r"pass C positionally or facecolors via keyword"):
260+
ax.tripcolor(x, y, C=[1, 2, 3, 4])
261+
262+
# smoke test for valid color specifications (via C or facecolors)
263+
ax.tripcolor(x, y, [1, 2, 3, 4]) # edges
264+
ax.tripcolor(x, y, [1, 2, 3, 4], shading='gouraud') # edges
265+
ax.tripcolor(x, y, [1, 2]) # faces
266+
ax.tripcolor(x, y, facecolors=[1, 2]) # faces
267+
268+
269+
def test_tripcolor_warnings():
270+
x = [-1, 0, 1, 0]
271+
y = [0, -1, 0, 1]
272+
C = [0.4, 0.5]
273+
fig, ax = plt.subplots()
274+
# additional parameters
275+
with pytest.warns(UserWarning, match="Additional positional parameters"):
276+
ax.tripcolor(x, y, C, 'unused_positional')
277+
# facecolors takes precednced over C
278+
with pytest.warns(UserWarning, match="Positional parameter C .*no effect"):
279+
ax.tripcolor(x, y, C, facecolors=C)
280+
with pytest.warns(UserWarning, match="Positional parameter C .*no effect"):
281+
ax.tripcolor(x, y, 'interpreted as C', facecolors=C)
282+
283+
180284
def test_no_modify():
181285
# Test that Triangulation does not modify triangles array passed to it.
182286
triangles = np.array([[3, 2, 0], [3, 1, 0]], dtype=np.int32)

lib/matplotlib/tri/triangulation.py

+54-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import numpy as np
22

3+
from matplotlib import _api
4+
35

46
class Triangulation:
57
"""
@@ -41,7 +43,9 @@ def __init__(self, x, y, triangles=None, mask=None):
4143
self.x = np.asarray(x, dtype=np.float64)
4244
self.y = np.asarray(y, dtype=np.float64)
4345
if self.x.shape != self.y.shape or self.x.ndim != 1:
44-
raise ValueError("x and y must be equal-length 1D arrays")
46+
raise ValueError("x and y must be equal-length 1D arrays, but "
47+
f"found shapes {self.x.shape!r} and "
48+
f"{self.y.shape!r}")
4549

4650
self.mask = None
4751
self._edges = None
@@ -56,13 +60,25 @@ def __init__(self, x, y, triangles=None, mask=None):
5660
else:
5761
# Triangulation specified. Copy, since we may correct triangle
5862
# orientation.
59-
self.triangles = np.array(triangles, dtype=np.int32, order='C')
63+
try:
64+
self.triangles = np.array(triangles, dtype=np.int32, order='C')
65+
except ValueError as e:
66+
raise ValueError('triangles must be a (N, 3) int array, not '
67+
f'{triangles!r}') from e
6068
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
61-
raise ValueError('triangles must be a (?, 3) array')
69+
raise ValueError(
70+
'triangles must be a (N, 3) int array, but found shape '
71+
f'{self.triangles.shape!r}')
6272
if self.triangles.max() >= len(self.x):
63-
raise ValueError('triangles max element is out of bounds')
73+
raise ValueError(
74+
'triangles are indices into the points and must be in the '
75+
f'range 0 <= i < {len(self.x)} but found value '
76+
f'{self.triangles.max()}')
6477
if self.triangles.min() < 0:
65-
raise ValueError('triangles min element is out of bounds')
78+
raise ValueError(
79+
'triangles are indices into the points and must be in the '
80+
f'range 0 <= i < {len(self.x)} but found value '
81+
f'{self.triangles.min()}')
6682

6783
if mask is not None:
6884
self.mask = np.asarray(mask, dtype=bool)
@@ -135,35 +151,43 @@ def get_from_args_and_kwargs(*args, **kwargs):
135151
"""
136152
if isinstance(args[0], Triangulation):
137153
triangulation, *args = args
154+
if 'triangles' in kwargs:
155+
_api.warn_external(
156+
"Passing the keyword 'triangles' has no effect when also "
157+
"passing a Triangulation")
158+
if 'mask' in kwargs:
159+
_api.warn_external(
160+
"Passing the keyword 'mask' has no effect when also "
161+
"passing a Triangulation")
138162
else:
139-
x, y, *args = args
140-
141-
# Check triangles in kwargs then args.
142-
triangles = kwargs.pop('triangles', None)
143-
from_args = False
144-
if triangles is None and args:
145-
triangles = args[0]
146-
from_args = True
147-
148-
if triangles is not None:
149-
try:
150-
triangles = np.asarray(triangles, dtype=np.int32)
151-
except ValueError:
152-
triangles = None
153-
154-
if triangles is not None and (triangles.ndim != 2 or
155-
triangles.shape[1] != 3):
156-
triangles = None
157-
158-
if triangles is not None and from_args:
159-
args = args[1:] # Consumed first item in args.
160-
161-
# Check for mask in kwargs.
162-
mask = kwargs.pop('mask', None)
163-
163+
x, y, triangles, mask, args, kwargs = \
164+
Triangulation._extract_triangulation_params(args, kwargs)
164165
triangulation = Triangulation(x, y, triangles, mask)
165166
return triangulation, args, kwargs
166167

168+
@staticmethod
169+
def _extract_triangulation_params(args, kwargs):
170+
x, y, *args = args
171+
# Check triangles in kwargs then args.
172+
triangles = kwargs.pop('triangles', None)
173+
from_args = False
174+
if triangles is None and args:
175+
triangles = args[0]
176+
from_args = True
177+
if triangles is not None:
178+
try:
179+
triangles = np.asarray(triangles, dtype=np.int32)
180+
except ValueError:
181+
triangles = None
182+
if triangles is not None and (triangles.ndim != 2 or
183+
triangles.shape[1] != 3):
184+
triangles = None
185+
if triangles is not None and from_args:
186+
args = args[1:] # Consumed first item in args.
187+
# Check for mask in kwargs.
188+
mask = kwargs.pop('mask', None)
189+
return x, y, triangles, mask, args, kwargs
190+
167191
def get_trifinder(self):
168192
"""
169193
Return the default `matplotlib.tri.TriFinder` of this

0 commit comments

Comments
 (0)