Skip to content

Commit 382eca7

Browse files
committed
Cleanup logic and documentation of tripcolor
This issues more and more precise warnings on usage errors but does not change behavior.
1 parent 39cfe2b commit 382eca7

File tree

3 files changed

+163
-62
lines changed

3 files changed

+163
-62
lines changed

lib/matplotlib/tests/test_triangulation.py

+70-6
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,42 @@ def test_extract_triangulation_params(args, kwargs, expected):
4141
assert kwargs_ == other_kwargs
4242

4343

44+
del x
45+
del y
46+
del triangles
47+
del mask
48+
49+
4450
def test_extract_triangulation_positional_mask():
45-
global x, y, triangles, mask
4651
# mask cannot be passed positionally
52+
mask = [True]
53+
args = [[0, 2, 1], [0, 0, 1], [[0, 1, 2]], mask]
4754
x_, y_, triangles_, mask_, args_, kwargs_ = \
48-
mtri.Triangulation._extract_triangulation_params(x, y, triangles, mask)
55+
mtri.Triangulation._extract_triangulation_params(args, {})
4956
assert mask_ is None
5057
assert args_ == [mask]
5158
# the positional mask has to be catched downstream because this must pass
5259
# unknown args through
5360

5461

55-
del x
56-
del y
57-
del triangles
58-
del mask
62+
def test_triangulation_init():
63+
x = [-1, 0, 1, 0]
64+
y = [0, -1, 0, 1]
65+
with pytest.raises(ValueError, match="x and y must be equal-length"):
66+
mtri.Triangulation(x, [1, 2])
67+
with pytest.raises(
68+
ValueError,
69+
match=r"triangles must be a \(N, 3\) array, but found shape "
70+
r"\(3,\)"):
71+
mtri.Triangulation(x, y, [0, 1, 2])
72+
with pytest.raises(
73+
ValueError,
74+
match=r"triangles must be a \(N, 3\) int array, not 'other'"):
75+
mtri.Triangulation(x, y, 'other')
76+
with pytest.raises(ValueError, match="found value 99"):
77+
mtri.Triangulation(x, y, [[0, 1, 99]])
78+
with pytest.raises(ValueError, match="found value -1"):
79+
mtri.Triangulation(x, y, [[0, 1, -1]])
5980

6081

6182
def test_delaunay():
@@ -223,6 +244,49 @@ def test_tripcolor():
223244
plt.title('facecolors')
224245

225246

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

+74-51
Original file line numberDiff line numberDiff line change
@@ -21,62 +21,85 @@ 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
2935
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`.
36+
`.Triangulation`. This is mutually exclusive with specifying
37+
*triangulation*.
38+
C : array-like
39+
The color values, either for the points or for the triangles. Which one
40+
is automatically inferred from the length of *C*, i.e. does it match
41+
the number of points or the number of triangles. If there are the same
42+
number of points and triangles in the triangulation it is assumed that
43+
color values are defined at points; to force the use of color values at
44+
triangles use the keyword argument ``facecolors=C`` instead of just
45+
``C``.
46+
This parameter is position-only.
47+
facecolors : array-like, optional
48+
Can be used alternatively to *C* to specify colors at the triangle
49+
faces. This parameter takes precedence over *C*.
50+
shading : {'flat', 'gouraud'}, default: 'flat'
51+
If is 'flat' and the color values *C* are defined at points, the color
52+
values used for each triangle are from the mean C of the triangle's
53+
three points. If *shading* is 'gouraud' then color values must be
54+
defined at points.
55+
other parameters
56+
All other parameters are the same as for `~.Axes.pcolor`.
57+
58+
Notes
59+
-----
60+
It is possible to pass the triangles positionally, i.e. using the call
61+
signature ``tripcolor(x, y, triangles, C, [mask=mask], ...)``. However,
62+
this is discouraged. For more clarity, pass *triangles* via keyword
63+
argument.
5364
"""
5465
_api.check_in_list(['flat', 'gouraud'], shading=shading)
5566

5667
tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs)
5768

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.
69+
# Parse the color to be in one of (the other variable will be None):
70+
# - facecolors: if specified at the triangle faces
71+
# - node_colors: if specified at the points
6172
if facecolors is not None:
62-
C = facecolors
73+
print(args)
74+
if args:
75+
_api.warn_external(
76+
"Positional parameter C has no effect when the keyword "
77+
"facecolors is given")
78+
node_colors = None
79+
if len(facecolors) != len(tri.triangles):
80+
raise ValueError("The length of facecolors must match the number "
81+
"of triangles")
6382
else:
83+
# Color from positional parameter C
84+
if not args:
85+
raise ValueError(
86+
"Missing color parameter. Please pass C positionally or "
87+
"facecolors via keyword")
88+
elif len(args) > 1:
89+
_api.warn_external(
90+
"Additional positional parameters {args[1:]!r} are ignored")
6491
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')
92+
if len(C) == len(tri.x):
93+
# having this before the len(tri.triangles) comparison gives
94+
# precedence to nodes if there are as many nodes as triangles
95+
node_colors = C
96+
facecolors = None
97+
elif len(C) == len(tri.triangles):
98+
node_colors = None
99+
facecolors = C
100+
else:
101+
raise ValueError('The length of C must match either the number '
102+
'of points or the number of triangles')
80103

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

98121
if shading == 'gouraud':
99122
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')
123+
raise ValueError(
124+
"shading='gouraud' can only be used when the colors "
125+
"are specified at the points, not at the faces.")
106126
collection = TriMesh(tri, **kwargs)
127+
colors = node_colors
107128
else:
108129
# Vertices of triangles.
109130
maskedTris = tri.get_masked_triangles()
@@ -112,15 +133,17 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None,
112133
# Color values.
113134
if facecolors is None:
114135
# One color per triangle, the mean of the 3 vertex color values.
115-
C = C[maskedTris].mean(axis=1)
136+
colors = node_colors[maskedTris].mean(axis=1)
116137
elif tri.mask is not None:
117138
# Remove color values of masked triangles.
118-
C = C[~tri.mask]
139+
colors = facecolors[~tri.mask]
140+
else:
141+
colors = facecolors
119142

120143
collection = PolyCollection(verts, **kwargs)
121144

122145
collection.set_alpha(alpha)
123-
collection.set_array(C)
146+
collection.set_array(colors)
124147
_api.check_isinstance((Normalize, None), norm=norm)
125148
collection.set_cmap(cmap)
126149
collection.set_norm(norm)

0 commit comments

Comments
 (0)