Skip to content

Commit ef81739

Browse files
timhoffmQuLogic
andcommitted
Cleanup logic and documentation of tripcolor
This issues more and more precise warnings on usage errors but does not change behavior. Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
1 parent 39cfe2b commit ef81739

File tree

3 files changed

+183
-91
lines changed

3 files changed

+183
-91
lines changed

lib/matplotlib/tests/test_triangulation.py

+92-34
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,65 @@
1212
from matplotlib.testing.decorators import image_comparison, check_figures_equal
1313

1414

15-
x = [-1, 0, 1, 0]
16-
y = [0, -1, 0, 1]
17-
triangles = [[0, 1, 2], [0, 2, 3]]
18-
mask = [False, True]
19-
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(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
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
4242

4343

4444
def test_extract_triangulation_positional_mask():
45-
global x, y, triangles, mask
4645
# mask cannot be passed positionally
46+
mask = [True]
47+
args = [[0, 2, 1], [0, 0, 1], [[0, 1, 2]], mask]
4748
x_, y_, triangles_, mask_, args_, kwargs_ = \
48-
mtri.Triangulation._extract_triangulation_params(x, y, triangles, mask)
49+
mtri.Triangulation._extract_triangulation_params(args, {})
4950
assert mask_ is None
5051
assert args_ == [mask]
51-
# the positional mask has to be catched downstream because this must pass
52+
# the positional mask must be caught downstream because this must pass
5253
# unknown args through
5354

5455

55-
del x
56-
del y
57-
del triangles
58-
del mask
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]])
5974

6075

6176
def test_delaunay():
@@ -223,6 +238,49 @@ def test_tripcolor():
223238
plt.title('facecolors')
224239

225240

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+
226284
def test_no_modify():
227285
# Test that Triangulation does not modify triangles array passed to it.
228286
triangles = np.array([[3, 2, 0], [3, 1, 0]], dtype=np.int32)

lib/matplotlib/tri/triangulation.py

+19-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def __init__(self, x, y, triangles=None, mask=None):
4343
self.x = np.asarray(x, dtype=np.float64)
4444
self.y = np.asarray(y, dtype=np.float64)
4545
if self.x.shape != self.y.shape or self.x.ndim != 1:
46-
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}")
4749

4850
self.mask = None
4951
self._edges = None
@@ -58,13 +60,25 @@ def __init__(self, x, y, triangles=None, mask=None):
5860
else:
5961
# Triangulation specified. Copy, since we may correct triangle
6062
# orientation.
61-
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
6268
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
63-
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}')
6472
if self.triangles.max() >= len(self.x):
65-
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()}')
6677
if self.triangles.min() < 0:
67-
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()}')
6882

6983
if mask is not None:
7084
self.mask = np.asarray(mask, dtype=bool)

lib/matplotlib/tri/tripcolor.py

+72-52
Original file line numberDiff line numberDiff line change
@@ -21,62 +21,82 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
2121
optionally the *triangles* and a *mask*. See `.Triangulation` for an
2222
explanation of these parameters.
2323
24+
If neither of *triangulation* or *triangles* are given, the triangulation
25+
is calculated on the fly. In this case, it does not make sense to provide
26+
colors at the triangle faces via *C* or *facecolors* because there are
27+
multiple possible triangulations for a group of points and you don't know
28+
which triangles will be constructed.
29+
2430
Parameters
2531
----------
2632
triangulation : `.Triangulation`
2733
An already created triangular grid.
2834
x, y, triangles, mask
29-
Parameters specifying defining the triangular grid. See
30-
`.Triangulation`.
31-
32-
33-
The next argument must be *C*, the array of color values, either
34-
one per point in the triangulation if color values are defined at
35-
points, or one per triangle in the triangulation if color values
36-
are defined at triangles. If there are the same number of points
37-
and triangles in the triangulation it is assumed that color
38-
values are defined at points; to force the use of color values at
39-
triangles use the kwarg ``facecolors=C`` instead of just ``C``.
40-
41-
*shading* may be 'flat' (the default) or 'gouraud'. If *shading*
42-
is 'flat' and C values are defined at points, the color values
43-
used for each triangle are from the mean C of the triangle's
44-
three points. If *shading* is 'gouraud' then color values must be
45-
defined at points.
46-
47-
48-
49-
tripcolor(x, y, [triangles], C, [mask=mask], ...)
50-
51-
52-
The remaining kwargs are the same as for `~.Axes.pcolor`.
35+
Parameters defining the triangular grid. See `.Triangulation`.
36+
This is mutually exclusive with specifying *triangulation*.
37+
C : array-like
38+
The color values, either for the points or for the triangles. Which one
39+
is automatically inferred from the length of *C*, i.e. does it match
40+
the number of points or the number of triangles. If there are the same
41+
number of points and triangles in the triangulation it is assumed that
42+
color values are defined at points; to force the use of color values at
43+
triangles use the keyword argument ``facecolors=C`` instead of just
44+
``C``.
45+
This parameter is position-only.
46+
facecolors : array-like, optional
47+
Can be used alternatively to *C* to specify colors at the triangle
48+
faces. This parameter takes precedence over *C*.
49+
shading : {'flat', 'gouraud'}, default: 'flat'
50+
If 'flat' and the color values *C* are defined at points, the color
51+
values used for each triangle are from the mean C of the triangle's
52+
three points. If *shading* is 'gouraud' then color values must be
53+
defined at points.
54+
other_parameters
55+
All other parameters are the same as for `~.Axes.pcolor`.
56+
57+
Notes
58+
-----
59+
It is possible to pass the triangles positionally, i.e.
60+
``tripcolor(x, y, triangles, C, ...)``. However, this is discouraged.
61+
For more clarity, pass *triangles* via keyword argument.
5362
"""
5463
_api.check_in_list(['flat', 'gouraud'], shading=shading)
5564

5665
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs)
5766

58-
# C is the colors array defined at either points or faces (i.e. triangles).
59-
# If facecolors is None, C are defined at points.
60-
# If facecolors is not None, C are defined at faces.
67+
# Parse the color to be in one of (the other variable will be None):
68+
# - facecolors: if specified at the triangle faces
69+
# - point_colors: if specified at the points
6170
if facecolors is not None:
62-
C = facecolors
71+
if args:
72+
_api.warn_external(
73+
"Positional parameter C has no effect when the keyword "
74+
"facecolors is given")
75+
point_colors = None
76+
if len(facecolors) != len(tri.triangles):
77+
raise ValueError("The length of facecolors must match the number "
78+
"of triangles")
6379
else:
80+
# Color from positional parameter C
81+
if not args:
82+
raise ValueError(
83+
"Missing color parameter. Please pass C positionally or "
84+
"facecolors via keyword")
85+
elif len(args) > 1:
86+
_api.warn_external(
87+
"Additional positional parameters {args[1:]!r} are ignored")
6488
C = np.asarray(args[0])
65-
66-
# If there are a different number of points and triangles in the
67-
# triangulation, can omit facecolors kwarg as it is obvious from
68-
# length of C whether it refers to points or faces.
69-
# Do not do this for gouraud shading.
70-
if (facecolors is None and len(C) == len(tri.triangles) and
71-
len(C) != len(tri.x) and shading != 'gouraud'):
72-
facecolors = C
73-
74-
# Check length of C is OK.
75-
if ((facecolors is None and len(C) != len(tri.x)) or
76-
(facecolors is not None and len(C) != len(tri.triangles))):
77-
raise ValueError('Length of color values array must be the same '
78-
'as either the number of triangulation points '
79-
'or triangles')
89+
if len(C) == len(tri.x):
90+
# having this before the len(tri.triangles) comparison gives
91+
# precedence to nodes if there are as many nodes as triangles
92+
point_colors = C
93+
facecolors = None
94+
elif len(C) == len(tri.triangles):
95+
point_colors = None
96+
facecolors = C
97+
else:
98+
raise ValueError('The length of C must match either the number '
99+
'of points or the number of triangles')
80100

81101
# Handling of linewidths, shading, edgecolors and antialiased as
82102
# in Axes.pcolor
@@ -97,13 +117,11 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
97117

98118
if shading == 'gouraud':
99119
if facecolors is not None:
100-
raise ValueError('Gouraud shading does not support the use '
101-
'of facecolors kwarg')
102-
if len(C) != len(tri.x):
103-
raise ValueError('For gouraud shading, the length of color '
104-
'values array must be the same as the '
105-
'number of triangulation points')
120+
raise ValueError(
121+
"shading='gouraud' can only be used when the colors "
122+
"are specified at the points, not at the faces.")
106123
collection = TriMesh(tri, **kwargs)
124+
colors = point_colors
107125
else:
108126
# Vertices of triangles.
109127
maskedTris = tri.get_masked_triangles()
@@ -112,15 +130,17 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
112130
# Color values.
113131
if facecolors is None:
114132
# One color per triangle, the mean of the 3 vertex color values.
115-
C = C[maskedTris].mean(axis=1)
133+
colors = point_colors[maskedTris].mean(axis=1)
116134
elif tri.mask is not None:
117135
# Remove color values of masked triangles.
118-
C = C[~tri.mask]
136+
colors = facecolors[~tri.mask]
137+
else:
138+
colors = facecolors
119139

120140
collection = PolyCollection(verts, **kwargs)
121141

122142
collection.set_alpha(alpha)
123-
collection.set_array(C)
143+
collection.set_array(colors)
124144
_api.check_isinstance((Normalize, None), norm=norm)
125145
collection.set_cmap(cmap)
126146
collection.set_norm(norm)

0 commit comments

Comments
 (0)