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

Skip to content

Commit 2857fde

Browse files
committed
Fix subslice optimization for long, fully nan lines.
The subslice optimizer does not handle the case of fully nan inputs, so the relevant check in recache() is not whether x is sorted but really whether x is sorted and not fully nan. Fix that. Previously, `plt.plot([np.nan] * 2000, range(2000))` would crash.
1 parent ffd3b12 commit 2857fde

File tree

4 files changed

+39
-42
lines changed

4 files changed

+39
-42
lines changed

lib/matplotlib/lines.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ class Line2D(Artist):
268268

269269
zorder = 2
270270

271+
_subslice_optim_min_size = 1000
272+
271273
def __str__(self):
272274
if self._label != "":
273275
return f"Line2D({self._label})"
@@ -677,12 +679,14 @@ def recache(self, always=False):
677679
self._x, self._y = self._xy.T # views
678680

679681
self._subslice = False
680-
if (self.axes and len(x) > 1000 and self._is_sorted(x) and
681-
self.axes.name == 'rectilinear' and
682-
self.axes.get_xscale() == 'linear' and
683-
self._markevery is None and
684-
self.get_clip_on() and
685-
self.get_transform() == self.axes.transData):
682+
if (self.axes
683+
and len(x) > self._subslice_optim_min_size
684+
and _path.is_sorted_and_has_non_nan(x)
685+
and self.axes.name == 'rectilinear'
686+
and self.axes.get_xscale() == 'linear'
687+
and self._markevery is None
688+
and self.get_clip_on()
689+
and self.get_transform() == self.axes.transData):
686690
self._subslice = True
687691
nanmask = np.isnan(x)
688692
if nanmask.any():
@@ -731,11 +735,6 @@ def set_transform(self, t):
731735
self._invalidy = True
732736
super().set_transform(t)
733737

734-
def _is_sorted(self, x):
735-
"""Return whether x is sorted in ascending order."""
736-
# We don't handle the monotonically decreasing case.
737-
return _path.is_sorted(x)
738-
739738
@allow_rasterization
740739
def draw(self, renderer):
741740
# docstring inherited

lib/matplotlib/tests/test_lines.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import matplotlib
1616
import matplotlib as mpl
17+
from matplotlib import _path
1718
import matplotlib.lines as mlines
1819
from matplotlib.markers import MarkerStyle
1920
from matplotlib.path import Path
@@ -244,11 +245,12 @@ def test_lw_scaling():
244245
ax.plot(th, j*np.ones(50) + .1 * lw, linestyle=ls, lw=lw, **sty)
245246

246247

247-
def test_nan_is_sorted():
248-
line = mlines.Line2D([], [])
249-
assert line._is_sorted(np.array([1, 2, 3]))
250-
assert line._is_sorted(np.array([1, np.nan, 3]))
251-
assert not line._is_sorted([3, 5] + [np.nan] * 100 + [0, 2])
248+
def test_is_sorted_and_has_non_nan():
249+
assert _path.is_sorted_and_has_non_nan(np.array([1, 2, 3]))
250+
assert _path.is_sorted_and_has_non_nan(np.array([1, np.nan, 3]))
251+
assert not _path.is_sorted_and_has_non_nan([3, 5] + [np.nan] * 100 + [0, 2])
252+
n = 2 * mlines.Line2D._subslice_optim_min_size
253+
plt.plot([np.nan] * n, range(n))
252254

253255

254256
@check_figures_equal()

src/_path.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,24 +1223,28 @@ bool convert_to_string(PathIterator &path,
12231223
}
12241224

12251225
template<class T>
1226-
bool is_sorted(PyArrayObject *array)
1226+
bool is_sorted_and_has_non_nan(PyArrayObject *array)
12271227
{
1228-
npy_intp size = PyArray_DIM(array, 0);
1228+
char* ptr = PyArray_BYTES(array);
1229+
npy_intp size = PyArray_DIM(array, 0),
1230+
stride = PyArray_STRIDE(array, 0);
12291231
using limits = std::numeric_limits<T>;
12301232
T last = limits::has_infinity ? -limits::infinity() : limits::min();
1233+
bool found_non_nan = false;
12311234

1232-
for (npy_intp i = 0; i < size; ++i) {
1233-
T current = *(T *)PyArray_GETPTR1(array, i);
1235+
for (npy_intp i = 0; i < size; ++i, ptr += stride) {
1236+
T current = *(T*)ptr;
12341237
// The following tests !isnan(current), but also works for integral
12351238
// types. (The isnan(IntegralType) overload is absent on MSVC.)
12361239
if (current == current) {
1240+
found_non_nan = true;
12371241
if (current < last) {
12381242
return false;
12391243
}
12401244
last = current;
12411245
}
12421246
}
1243-
return true;
1247+
return found_non_nan;
12441248
};
12451249

12461250

src/_path_wrapper.cpp

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -689,14 +689,14 @@ static PyObject *Py_convert_to_string(PyObject *self, PyObject *args)
689689
}
690690

691691

692-
const char *Py_is_sorted__doc__ =
693-
"is_sorted(array)\n"
692+
const char *Py_is_sorted_and_has_non_nan__doc__ =
693+
"is_sorted_and_has_non_nan(array, /)\n"
694694
"--\n\n"
695-
"Return whether the 1D *array* is monotonically increasing, ignoring NaNs.\n";
695+
"Return whether the 1D *array* is monotonically increasing, ignoring NaNs,\n"
696+
"and has at least one non-nan value.";
696697

697-
static PyObject *Py_is_sorted(PyObject *self, PyObject *obj)
698+
static PyObject *Py_is_sorted_and_has_non_nan(PyObject *self, PyObject *obj)
698699
{
699-
npy_intp size;
700700
bool result;
701701

702702
PyArrayObject *array = (PyArrayObject *)PyArray_FromAny(
@@ -706,38 +706,30 @@ static PyObject *Py_is_sorted(PyObject *self, PyObject *obj)
706706
return NULL;
707707
}
708708

709-
size = PyArray_DIM(array, 0);
710-
711-
if (size < 2) {
712-
Py_DECREF(array);
713-
Py_RETURN_TRUE;
714-
}
715-
716-
/* Handle just the most common types here, otherwise coerce to
717-
double */
709+
/* Handle just the most common types here, otherwise coerce to double */
718710
switch (PyArray_TYPE(array)) {
719711
case NPY_INT:
720-
result = is_sorted<npy_int>(array);
712+
result = is_sorted_and_has_non_nan<npy_int>(array);
721713
break;
722714
case NPY_LONG:
723-
result = is_sorted<npy_long>(array);
715+
result = is_sorted_and_has_non_nan<npy_long>(array);
724716
break;
725717
case NPY_LONGLONG:
726-
result = is_sorted<npy_longlong>(array);
718+
result = is_sorted_and_has_non_nan<npy_longlong>(array);
727719
break;
728720
case NPY_FLOAT:
729-
result = is_sorted<npy_float>(array);
721+
result = is_sorted_and_has_non_nan<npy_float>(array);
730722
break;
731723
case NPY_DOUBLE:
732-
result = is_sorted<npy_double>(array);
724+
result = is_sorted_and_has_non_nan<npy_double>(array);
733725
break;
734726
default:
735727
Py_DECREF(array);
736728
array = (PyArrayObject *)PyArray_FromObject(obj, NPY_DOUBLE, 1, 1);
737729
if (array == NULL) {
738730
return NULL;
739731
}
740-
result = is_sorted<npy_double>(array);
732+
result = is_sorted_and_has_non_nan<npy_double>(array);
741733
}
742734

743735
Py_DECREF(array);
@@ -765,7 +757,7 @@ static PyMethodDef module_functions[] = {
765757
{"convert_path_to_polygons", (PyCFunction)Py_convert_path_to_polygons, METH_VARARGS|METH_KEYWORDS, Py_convert_path_to_polygons__doc__},
766758
{"cleanup_path", (PyCFunction)Py_cleanup_path, METH_VARARGS, Py_cleanup_path__doc__},
767759
{"convert_to_string", (PyCFunction)Py_convert_to_string, METH_VARARGS, Py_convert_to_string__doc__},
768-
{"is_sorted", (PyCFunction)Py_is_sorted, METH_O, Py_is_sorted__doc__},
760+
{"is_sorted_and_has_non_nan", (PyCFunction)Py_is_sorted_and_has_non_nan, METH_O, Py_is_sorted_and_has_non_nan__doc__},
769761
{NULL}
770762
};
771763

0 commit comments

Comments
 (0)