Skip to content

Commit 6e281f0

Browse files
committed
Merge pull request matplotlib#5031 from efiring/subslice
support subslicing when x is masked or has nans; closes matplotlib#5016
2 parents e8e75eb + b62f0b0 commit 6e281f0

File tree

1 file changed

+42
-25
lines changed

1 file changed

+42
-25
lines changed

lib/matplotlib/lines.py

+42-25
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,16 @@ class Line2D(Artist):
237237
def __str__(self):
238238
if self._label != "":
239239
return "Line2D(%s)" % (self._label)
240-
elif hasattr(self, '_x') and len(self._x) > 3:
240+
elif self._x is None:
241+
return "Line2D()"
242+
elif len(self._x) > 3:
241243
return "Line2D((%g,%g),(%g,%g),...,(%g,%g))"\
242244
% (self._x[0], self._y[0], self._x[0],
243245
self._y[0], self._x[-1], self._y[-1])
244-
elif hasattr(self, '_x'):
246+
else:
245247
return "Line2D(%s)"\
246248
% (",".join(["(%g,%g)" % (x, y) for x, y
247249
in zip(self._x, self._y)]))
248-
else:
249-
return "Line2D()"
250250

251251
def __init__(self, xdata, ydata,
252252
linewidth=None, # all Nones default to rc
@@ -373,6 +373,14 @@ def __init__(self, xdata, ydata,
373373
self._yorig = np.asarray([])
374374
self._invalidx = True
375375
self._invalidy = True
376+
self._x = None
377+
self._y = None
378+
self._xy = None
379+
self._path = None
380+
self._transformed_path = None
381+
self._subslice = False
382+
self._x_filled = None # used in subslicing; only x is needed
383+
376384
self.set_data(xdata, ydata)
377385

378386
def __getstate__(self):
@@ -598,7 +606,7 @@ def recache(self, always=False):
598606
if always or self._invalidx:
599607
xconv = self.convert_xunits(self._xorig)
600608
if ma.isMaskedArray(self._xorig):
601-
x = ma.asarray(xconv, np.float_)
609+
x = ma.asarray(xconv, np.float_).filled(np.nan)
602610
else:
603611
x = np.asarray(xconv, np.float_)
604612
x = x.ravel()
@@ -607,7 +615,7 @@ def recache(self, always=False):
607615
if always or self._invalidy:
608616
yconv = self.convert_yunits(self._yorig)
609617
if ma.isMaskedArray(self._yorig):
610-
y = ma.asarray(yconv, np.float_)
618+
y = ma.asarray(yconv, np.float_).filled(np.nan)
611619
else:
612620
y = np.asarray(yconv, np.float_)
613621
y = y.ravel()
@@ -622,24 +630,30 @@ def recache(self, always=False):
622630
if len(x) != len(y):
623631
raise RuntimeError('xdata and ydata must be the same length')
624632

625-
x = x.reshape((len(x), 1))
626-
y = y.reshape((len(y), 1))
633+
self._xy = np.empty((len(x), 2), dtype=np.float_)
634+
self._xy[:, 0] = x
635+
self._xy[:, 1] = y
627636

628-
if ma.isMaskedArray(x) or ma.isMaskedArray(y):
629-
self._xy = ma.concatenate((x, y), 1)
630-
else:
631-
self._xy = np.concatenate((x, y), 1)
632637
self._x = self._xy[:, 0] # just a view
633638
self._y = self._xy[:, 1] # just a view
634639

635640
self._subslice = False
636-
if (self.axes and len(x) > 100 and self._is_sorted(x) and
641+
if (self.axes and len(x) > 1000 and self._is_sorted(x) and
637642
self.axes.name == 'rectilinear' and
638643
self.axes.get_xscale() == 'linear' and
639644
self._markevery is None and
640645
self.get_clip_on() is True):
641646
self._subslice = True
642-
if hasattr(self, '_path'):
647+
nanmask = np.isnan(x)
648+
if nanmask.any():
649+
self._x_filled = self._x.copy()
650+
indices = np.arange(len(x))
651+
self._x_filled[nanmask] = np.interp(indices[nanmask],
652+
indices[~nanmask], self._x[~nanmask])
653+
else:
654+
self._x_filled = self._x
655+
656+
if self._path is not None:
643657
interpolation_steps = self._path._interpolation_steps
644658
else:
645659
interpolation_steps = 1
@@ -650,13 +664,14 @@ def recache(self, always=False):
650664

651665
def _transform_path(self, subslice=None):
652666
"""
653-
Puts a TransformedPath instance at self._transformed_path,
667+
Puts a TransformedPath instance at self._transformed_path;
654668
all invalidation of the transform is then handled by the
655669
TransformedPath instance.
656670
"""
657671
# Masked arrays are now handled by the Path class itself
658672
if subslice is not None:
659-
_path = Path(self._xy[subslice, :])
673+
_steps = self._path._interpolation_steps
674+
_path = Path(self._xy[subslice, :], _interpolation_steps=_steps)
660675
else:
661676
_path = self._path
662677
self._transformed_path = TransformedPath(_path, self.get_transform())
@@ -682,10 +697,11 @@ def set_transform(self, t):
682697
self.stale = True
683698

684699
def _is_sorted(self, x):
685-
"""return true if x is sorted"""
700+
"""return True if x is sorted in ascending order"""
701+
# We don't handle the monotonically decreasing case.
686702
if len(x) < 2:
687-
return 1
688-
return np.amin(x[1:] - x[0:-1]) >= 0
703+
return True
704+
return np.nanmin(x[1:] - x[:-1]) >= 0
689705

690706
@allow_rasterization
691707
def draw(self, renderer):
@@ -697,13 +713,14 @@ def draw(self, renderer):
697713
self.recache()
698714
self.ind_offset = 0 # Needed for contains() method.
699715
if self._subslice and self.axes:
700-
# Need to handle monotonically decreasing case also...
701716
x0, x1 = self.axes.get_xbound()
702-
i0, = self._x.searchsorted([x0], 'left')
703-
i1, = self._x.searchsorted([x1], 'right')
717+
i0, = self._x_filled.searchsorted([x0], 'left')
718+
i1, = self._x_filled.searchsorted([x1], 'right')
704719
subslice = slice(max(i0 - 1, 0), i1 + 1)
705-
self.ind_offset = subslice.start
706-
self._transform_path(subslice)
720+
# Don't remake the Path unless it will be sufficiently smaller.
721+
if subslice.start > 100 or len(self._x) - subslice.stop > 100:
722+
self.ind_offset = subslice.start
723+
self._transform_path(subslice)
707724

708725
transf_path = self._get_transformed_path()
709726

@@ -1432,7 +1449,7 @@ def __init__(self, line):
14321449
:class:`matplotlib.axes.Axes` instance and should have the
14331450
picker property set.
14341451
"""
1435-
if not hasattr(line, 'axes'):
1452+
if line.axes is None:
14361453
raise RuntimeError('You must first add the line to the Axes')
14371454

14381455
if line.get_picker() is None:

0 commit comments

Comments
 (0)