Skip to content

Cleanup tripcolor() #22356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions lib/matplotlib/tests/test_triangulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,67 @@
from matplotlib.testing.decorators import image_comparison, check_figures_equal


class TestTriangulationParams:
x = [-1, 0, 1, 0]
y = [0, -1, 0, 1]
triangles = [[0, 1, 2], [0, 2, 3]]
mask = [False, True]

@pytest.mark.parametrize('args, kwargs, expected', [
([x, y], {}, [x, y, None, None]),
([x, y, triangles], {}, [x, y, triangles, None]),
([x, y], dict(triangles=triangles), [x, y, triangles, None]),
([x, y], dict(mask=mask), [x, y, None, mask]),
([x, y, triangles], dict(mask=mask), [x, y, triangles, mask]),
([x, y], dict(triangles=triangles, mask=mask), [x, y, triangles, mask])
])
def test_extract_triangulation_params(self, args, kwargs, expected):
other_args = [1, 2]
other_kwargs = {'a': 3, 'b': '4'}
x_, y_, triangles_, mask_, args_, kwargs_ = \
mtri.Triangulation._extract_triangulation_params(
args + other_args, {**kwargs, **other_kwargs})
x, y, triangles, mask = expected
assert x_ is x
assert y_ is y
assert_array_equal(triangles_, triangles)
assert mask_ is mask
assert args_ == other_args
assert kwargs_ == other_kwargs


def test_extract_triangulation_positional_mask():
# mask cannot be passed positionally
mask = [True]
args = [[0, 2, 1], [0, 0, 1], [[0, 1, 2]], mask]
x_, y_, triangles_, mask_, args_, kwargs_ = \
mtri.Triangulation._extract_triangulation_params(args, {})
assert mask_ is None
assert args_ == [mask]
# the positional mask must be caught downstream because this must pass
# unknown args through


def test_triangulation_init():
x = [-1, 0, 1, 0]
y = [0, -1, 0, 1]
with pytest.raises(ValueError, match="x and y must be equal-length"):
mtri.Triangulation(x, [1, 2])
with pytest.raises(
ValueError,
match=r"triangles must be a \(N, 3\) int array, but found shape "
r"\(3,\)"):
mtri.Triangulation(x, y, [0, 1, 2])
with pytest.raises(
ValueError,
match=r"triangles must be a \(N, 3\) int array, not 'other'"):
mtri.Triangulation(x, y, 'other')
with pytest.raises(ValueError, match="found value 99"):
mtri.Triangulation(x, y, [[0, 1, 99]])
with pytest.raises(ValueError, match="found value -1"):
mtri.Triangulation(x, y, [[0, 1, -1]])


def test_delaunay():
# No duplicate points, regular grid.
nx = 5
Expand Down Expand Up @@ -177,6 +238,49 @@ def test_tripcolor():
plt.title('facecolors')


def test_tripcolor_color():
x = [-1, 0, 1, 0]
y = [0, -1, 0, 1]
fig, ax = plt.subplots()
with pytest.raises(ValueError, match="Missing color parameter"):
ax.tripcolor(x, y)
with pytest.raises(ValueError, match="The length of C must match either"):
ax.tripcolor(x, y, [1, 2, 3])
with pytest.raises(ValueError,
match="length of facecolors must match .* triangles"):
ax.tripcolor(x, y, facecolors=[1, 2, 3, 4])
with pytest.raises(ValueError,
match="'gouraud' .* at the points.* not at the faces"):
ax.tripcolor(x, y, facecolors=[1, 2], shading='gouraud')
with pytest.raises(ValueError,
match="'gouraud' .* at the points.* not at the faces"):
ax.tripcolor(x, y, [1, 2], shading='gouraud') # faces
with pytest.raises(ValueError,
match=r"pass C positionally or facecolors via keyword"):
ax.tripcolor(x, y, C=[1, 2, 3, 4])

# smoke test for valid color specifications (via C or facecolors)
ax.tripcolor(x, y, [1, 2, 3, 4]) # edges
ax.tripcolor(x, y, [1, 2, 3, 4], shading='gouraud') # edges
ax.tripcolor(x, y, [1, 2]) # faces
ax.tripcolor(x, y, facecolors=[1, 2]) # faces


def test_tripcolor_warnings():
x = [-1, 0, 1, 0]
y = [0, -1, 0, 1]
C = [0.4, 0.5]
fig, ax = plt.subplots()
# additional parameters
with pytest.warns(UserWarning, match="Additional positional parameters"):
ax.tripcolor(x, y, C, 'unused_positional')
# facecolors takes precednced over C
with pytest.warns(UserWarning, match="Positional parameter C .*no effect"):
ax.tripcolor(x, y, C, facecolors=C)
with pytest.warns(UserWarning, match="Positional parameter C .*no effect"):
ax.tripcolor(x, y, 'interpreted as C', facecolors=C)


def test_no_modify():
# Test that Triangulation does not modify triangles array passed to it.
triangles = np.array([[3, 2, 0], [3, 1, 0]], dtype=np.int32)
Expand Down
84 changes: 54 additions & 30 deletions lib/matplotlib/tri/triangulation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import numpy as np

from matplotlib import _api


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

self.mask = None
self._edges = None
Expand All @@ -56,13 +60,25 @@ def __init__(self, x, y, triangles=None, mask=None):
else:
# Triangulation specified. Copy, since we may correct triangle
# orientation.
self.triangles = np.array(triangles, dtype=np.int32, order='C')
try:
self.triangles = np.array(triangles, dtype=np.int32, order='C')
except ValueError as e:
raise ValueError('triangles must be a (N, 3) int array, not '
f'{triangles!r}') from e
if self.triangles.ndim != 2 or self.triangles.shape[1] != 3:
raise ValueError('triangles must be a (?, 3) array')
raise ValueError(
'triangles must be a (N, 3) int array, but found shape '
f'{self.triangles.shape!r}')
if self.triangles.max() >= len(self.x):
raise ValueError('triangles max element is out of bounds')
raise ValueError(
'triangles are indices into the points and must be in the '
f'range 0 <= i < {len(self.x)} but found value '
f'{self.triangles.max()}')
if self.triangles.min() < 0:
raise ValueError('triangles min element is out of bounds')
raise ValueError(
'triangles are indices into the points and must be in the '
f'range 0 <= i < {len(self.x)} but found value '
f'{self.triangles.min()}')

if mask is not None:
self.mask = np.asarray(mask, dtype=bool)
Expand Down Expand Up @@ -135,35 +151,43 @@ def get_from_args_and_kwargs(*args, **kwargs):
"""
if isinstance(args[0], Triangulation):
triangulation, *args = args
if 'triangles' in kwargs:
_api.warn_external(
"Passing the keyword 'triangles' has no effect when also "
"passing a Triangulation")
if 'mask' in kwargs:
_api.warn_external(
"Passing the keyword 'mask' has no effect when also "
"passing a Triangulation")
else:
x, y, *args = args

# Check triangles in kwargs then args.
triangles = kwargs.pop('triangles', None)
from_args = False
if triangles is None and args:
triangles = args[0]
from_args = True

if triangles is not None:
try:
triangles = np.asarray(triangles, dtype=np.int32)
except ValueError:
triangles = None

if triangles is not None and (triangles.ndim != 2 or
triangles.shape[1] != 3):
triangles = None

if triangles is not None and from_args:
args = args[1:] # Consumed first item in args.

# Check for mask in kwargs.
mask = kwargs.pop('mask', None)

x, y, triangles, mask, args, kwargs = \
Triangulation._extract_triangulation_params(args, kwargs)
triangulation = Triangulation(x, y, triangles, mask)
return triangulation, args, kwargs

@staticmethod
def _extract_triangulation_params(args, kwargs):
x, y, *args = args
# Check triangles in kwargs then args.
triangles = kwargs.pop('triangles', None)
from_args = False
if triangles is None and args:
triangles = args[0]
from_args = True
if triangles is not None:
try:
triangles = np.asarray(triangles, dtype=np.int32)
except ValueError:
triangles = None
if triangles is not None and (triangles.ndim != 2 or
triangles.shape[1] != 3):
triangles = None
if triangles is not None and from_args:
args = args[1:] # Consumed first item in args.
# Check for mask in kwargs.
mask = kwargs.pop('mask', None)
return x, y, triangles, mask, args, kwargs

def get_trifinder(self):
"""
Return the default `matplotlib.tri.TriFinder` of this
Expand Down
Loading