Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43c8913

Browse files
committed
Add non 2D paths wip [skip ci]
1 parent 04bda58 commit 43c8913

File tree

2 files changed

+88
-25
lines changed

2 files changed

+88
-25
lines changed

lib/matplotlib/path.py

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,13 @@ class Path:
9797
CLOSEPOLY: 1}
9898

9999
def __init__(self, vertices, codes=None, _interpolation_steps=1,
100-
closed=False, readonly=False):
100+
closed=False, readonly=False, dims=2):
101101
"""
102102
Create a new path with the given vertices and codes.
103103
104104
Parameters
105105
----------
106-
vertices : (N, 2) array-like
106+
vertices : (N, dims) array-like
107107
The path vertices, as an array, masked array or sequence of pairs.
108108
Masked values, if any, will be converted to NaNs, which are then
109109
handled correctly by the Agg PathIterator and other consumers of
@@ -125,9 +125,14 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1,
125125
readonly : bool, optional
126126
Makes the path behave in an immutable way and sets the vertices
127127
and codes as read-only arrays.
128+
dims : int, optional
128129
"""
130+
if dims <= 1:
131+
raise ValueError("Path must be at least 2D")
132+
self._dims = dims
133+
129134
vertices = _to_unmasked_float_array(vertices)
130-
_api.check_shape((None, 2), vertices=vertices)
135+
_api.check_shape((None, dims), vertices=vertices)
131136

132137
if codes is not None:
133138
codes = np.asarray(codes, self.code_type)
@@ -178,6 +183,7 @@ def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None):
178183
pth._vertices = _to_unmasked_float_array(verts)
179184
pth._codes = codes
180185
pth._readonly = False
186+
pth._dims = pth._vertices.shape[1]
181187
if internals_from is not None:
182188
pth._should_simplify = internals_from._should_simplify
183189
pth._simplify_threshold = internals_from._simplify_threshold
@@ -210,13 +216,15 @@ def _update_values(self):
210216

211217
@property
212218
def vertices(self):
213-
"""The vertices of the `Path` as an (N, 2) array."""
219+
"""The vertices of the `Path` as an (N, dims) array."""
214220
return self._vertices
215221

216222
@vertices.setter
217223
def vertices(self, vertices):
218224
if self._readonly:
219225
raise AttributeError("Can't set vertices on a readonly Path")
226+
if not _api.check_shape((None, self._dims), vertices=vertices):
227+
raise ValueError("Vertices shape does not match path dimensions")
220228
self._vertices = vertices
221229
self._update_values()
222230

@@ -239,6 +247,26 @@ def codes(self, codes):
239247
self._codes = codes
240248
self._update_values()
241249

250+
@property
251+
def dims(self):
252+
"""
253+
The dimensions of vertices in the `Path`.
254+
"""
255+
return self._dims
256+
257+
@dims.setter
258+
def dims(self, dims):
259+
if dims <= 2:
260+
raise ValueError("Path must be at least 2D")
261+
262+
if dims < self._dims:
263+
self._vertices = self._vertices[:, :dims]
264+
elif dims > self._dims:
265+
self._vertices = np.pad(self._vertices,
266+
((0, 0), (0, dims - self._dims)),
267+
mode='constant', constant_values=np.nan)
268+
self._dims = dims
269+
242270
@property
243271
def simplify_threshold(self):
244272
"""
@@ -298,17 +326,17 @@ def make_compound_path_from_polys(cls, XY):
298326
299327
Parameters
300328
----------
301-
XY : (numpolys, numsides, 2) array
329+
XY : (numpolys, numsides, dims) array
302330
"""
303331
# for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for
304332
# the CLOSEPOLY; the vert for the closepoly is ignored but we still
305333
# need it to keep the codes aligned with the vertices
306-
numpolys, numsides, two = XY.shape
307-
if two != 2:
308-
raise ValueError("The third dimension of 'XY' must be 2")
334+
numpolys, numsides, dims = XY.shape
335+
if dims < 2:
336+
raise ValueError("The third dimension of 'XY' must be at least 2")
309337
stride = numsides + 1
310338
nverts = numpolys * stride
311-
verts = np.zeros((nverts, 2))
339+
verts = np.zeros((nverts, dims))
312340
codes = np.full(nverts, cls.LINETO, dtype=cls.code_type)
313341
codes[0::stride] = cls.MOVETO
314342
codes[numsides::stride] = cls.CLOSEPOLY
@@ -323,6 +351,9 @@ def make_compound_path(cls, *args):
323351
"""
324352
if not args:
325353
return Path(np.empty([0, 2], dtype=np.float32))
354+
if not all([path._dims != args[0]._dims for path in args]):
355+
raise ValueError("Paths provided must be the same dimension")
356+
326357
vertices = np.concatenate([path.vertices for path in args])
327358
codes = np.empty(len(vertices), dtype=cls.code_type)
328359
i = 0
@@ -338,6 +369,16 @@ def make_compound_path(cls, *args):
338369
not_stop_mask = codes != cls.STOP # Remove STOPs, as internal STOPs are a bug.
339370
return cls(vertices[not_stop_mask], codes[not_stop_mask])
340371

372+
@classmethod
373+
def _project_to_2d(cls, path, transform=None):
374+
if transform is not None:
375+
path = transform.transform_path(path)
376+
377+
if path._dims > 2:
378+
path = Path._fast_from_codes_and_verts(path.vertices[:, 2], path.codes,
379+
path)
380+
return path
381+
341382
def __repr__(self):
342383
return f"Path({self.vertices!r}, {self.codes!r})"
343384

@@ -478,6 +519,10 @@ def cleaned(self, transform=None, remove_nans=False, clip=None,
478519
--------
479520
Path.iter_segments : for details of the keyword arguments.
480521
"""
522+
# Not implemented for non 2D
523+
if self._dims != 2:
524+
return self
525+
481526
vertices, codes = _path.cleanup_path(
482527
self, transform, remove_nans, clip, snap, stroke_width, simplify,
483528
curves, sketch)
@@ -497,7 +542,7 @@ def transformed(self, transform):
497542
automatically update when the transform changes.
498543
"""
499544
return Path(transform.transform(self.vertices), self.codes,
500-
self._interpolation_steps)
545+
self._interpolation_steps, dims=transform.output_dims)
501546

502547
def contains_point(self, point, transform=None, radius=0.0):
503548
"""
@@ -540,14 +585,22 @@ def contains_point(self, point, transform=None, radius=0.0):
540585
"""
541586
if transform is not None:
542587
transform = transform.frozen()
588+
589+
# Transform the path, and then toss out the z dimension
590+
if self.dims > 2:
591+
pth = Path._project_to_2d(self, transform)
592+
transform = None
593+
543594
# `point_in_path` does not handle nonlinear transforms, so we
544595
# transform the path ourselves. If *transform* is affine, letting
545596
# `point_in_path` handle the transform avoids allocating an extra
546597
# buffer.
547-
if transform and not transform.is_affine:
598+
elif transform and not transform.is_affine:
548599
self = transform.transform_path(self)
600+
pth = self
549601
transform = None
550-
return _path.point_in_path(point[0], point[1], radius, self, transform)
602+
603+
return _path.point_in_path(point[0], point[1], radius, pth, transform)
551604

552605
def contains_points(self, points, transform=None, radius=0.0):
553606
"""
@@ -590,7 +643,11 @@ def contains_points(self, points, transform=None, radius=0.0):
590643
"""
591644
if transform is not None:
592645
transform = transform.frozen()
593-
result = _path.points_in_path(points, radius, self, transform)
646+
pth = self
647+
if self._dims > 2:
648+
pth = Path._project_to_2d(self, transform)
649+
transform = None
650+
result = _path.points_in_path(points, radius, pth, transform)
594651
return result.astype('bool')
595652

596653
def contains_path(self, path, transform=None):
@@ -602,7 +659,15 @@ def contains_path(self, path, transform=None):
602659
"""
603660
if transform is not None:
604661
transform = transform.frozen()
605-
return _path.path_in_path(self, None, path, transform)
662+
663+
a_pth = Path._project_to_2d(self, None)
664+
if path._dims > 2:
665+
b_pth = Path._project_to_2d(path, transform)
666+
transform = None
667+
else:
668+
b_pth = path
669+
670+
return _path.path_in_path(a_pth, None, b_pth, transform)
606671

607672
def get_extents(self, transform=None, **kwargs):
608673
"""
@@ -652,7 +717,9 @@ def intersects_path(self, other, filled=True):
652717
If *filled* is True, then this also returns True if one path completely
653718
encloses the other (i.e., the paths are treated as filled).
654719
"""
655-
return _path.path_intersects_path(self, other, filled)
720+
a = Path._project_to_2d(self)
721+
b = Path._project_to_2d(other)
722+
return _path.path_intersects_path(a, b, filled)
656723

657724
def intersects_bbox(self, bbox, filled=True):
658725
"""
@@ -663,8 +730,9 @@ def intersects_bbox(self, bbox, filled=True):
663730
664731
The bounding box is always considered filled.
665732
"""
733+
pth = Path._project_to_2d(self)
666734
return _path.path_intersects_rectangle(
667-
self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
735+
pth, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
668736

669737
def interpolated(self, steps):
670738
"""
@@ -683,7 +751,7 @@ def interpolated(self, steps):
683751
new_codes[0::steps] = codes
684752
else:
685753
new_codes = None
686-
return Path(vertices, new_codes)
754+
return Path(vertices, new_codes, dims=vertices.shape[1])
687755

688756
def to_polygons(self, transform=None, width=0, height=0, closed_only=True):
689757
"""

lib/matplotlib/transforms.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,8 +1608,6 @@ def transform_path(self, path):
16081608
In some cases, this transform may insert curves into the path
16091609
that began as line segments.
16101610
"""
1611-
if self.input_dims != 2 or self.output_dims != 2:
1612-
raise NotImplementedError('Only defined in 2D')
16131611
return self.transform_path_affine(self.transform_path_non_affine(path))
16141612

16151613
def transform_path_affine(self, path):
@@ -1620,8 +1618,6 @@ def transform_path_affine(self, path):
16201618
``transform_path(path)`` is equivalent to
16211619
``transform_path_affine(transform_path_non_affine(values))``.
16221620
"""
1623-
if self.input_dims != 2 or self.output_dims != 2:
1624-
raise NotImplementedError('Only defined in 2D')
16251621
return self.get_affine().transform_path_affine(path)
16261622

16271623
def transform_path_non_affine(self, path):
@@ -1632,10 +1628,9 @@ def transform_path_non_affine(self, path):
16321628
``transform_path(path)`` is equivalent to
16331629
``transform_path_affine(transform_path_non_affine(values))``.
16341630
"""
1635-
if self.input_dims != 2 or self.output_dims != 2:
1636-
raise NotImplementedError('Only defined in 2D')
16371631
x = self.transform_non_affine(path.vertices)
1638-
return Path._fast_from_codes_and_verts(x, path.codes, path)
1632+
return Path._fast_from_codes_and_verts(x, path.codes, path,
1633+
dims=self.output_dims)
16391634

16401635
def transform_angles(self, angles, pts, radians=False, pushoff=1e-5):
16411636
"""
@@ -1817,7 +1812,7 @@ def transform_path(self, path):
18171812
def transform_path_affine(self, path):
18181813
# docstring inherited
18191814
return Path(self.transform_affine(path.vertices),
1820-
path.codes, path._interpolation_steps)
1815+
path.codes, path._interpolation_steps, dims=self.output_dims)
18211816

18221817
def transform_path_non_affine(self, path):
18231818
# docstring inherited

0 commit comments

Comments
 (0)