From c8e6920443dddc2b9bb2a0a5546d30076b94e65b Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Thu, 3 Jun 2021 20:18:07 +0800 Subject: [PATCH 1/3] use multiple polygons for "finite" --- pyqtgraph/functions.py | 68 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 455856fa..1ab54a48 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1755,6 +1755,20 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): use_qpolygonf = connect == 'all' + isfinite = None + if connect == 'finite': + isfinite = np.isfinite(x) & np.isfinite(y) + if not finiteCheck: + # if user specified to skip finite check, then that forces use_qpolygonf + use_qpolygonf = True + else: + # otherwise use a heuristic + # if non-finite aren't that many, then use_qpolyponf + nonfinite_cnt = n - np.sum(isfinite) + if nonfinite_cnt / n < 2 / 100: + use_qpolygonf = True + finiteCheck = False + if use_qpolygonf: backstore = create_qpolygonf(n) arr = np.frombuffer(ndarray_from_qpolygonf(backstore), dtype=[('x', 'f8'), ('y', 'f8')]) @@ -1774,9 +1788,9 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): # this behavior started in Qt 5.12.3 and was introduced in this commit # https://github.com/qt/qtbase/commit/c04bd30de072793faee5166cff866a4c4e0a9dd7 # We therefore replace non-finite values - isfinite = None if finiteCheck: - isfinite = np.isfinite(x) & np.isfinite(y) + if isfinite is None: + isfinite = np.isfinite(x) & np.isfinite(y) if not np.all(isfinite): # credit: Divakar https://stackoverflow.com/a/41191127/643629 mask = ~isfinite @@ -1801,9 +1815,53 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): # A point will anyway not connect to an invalid point regardless of the # 'c' value of the invalid point. Therefore, we should set 'c' to 0 for # the next point of an invalid point. - if isfinite is None: - isfinite = np.isfinite(x) & np.isfinite(y) - arr[1:]['c'] = isfinite[:-1] + if not use_qpolygonf: + arr[1:]['c'] = isfinite[:-1] + else: + sidx = np.nonzero(~isfinite)[0] + 1 + chunks = np.split(arr, sidx) # note: the chunks are views + + # create a single polygon able to hold the largest chunk + maxlen = max(len(chunk) for chunk in chunks) + subpoly = create_qpolygonf(maxlen) + subarr = np.frombuffer(ndarray_from_qpolygonf(subpoly), dtype=arr.dtype) + + # resize and fill do not change the capacity + if hasattr(subpoly, 'resize'): + subpoly_resize = subpoly.resize + else: + # PyQt will be less efficient + subpoly_resize = lambda n, v=QtCore.QPointF() : subpoly.fill(v, n) + + # notes: + # - we backfill the non-finite in order to get the same image as the + # old codepath on the CI. somehow P1--P2 gets rendered differently + # from P1--P2--P2 + # - we do not generate MoveTo(s) that are not followed by a LineTo, + # thus the QPainterPath can be different from the old codepath's + + # all chunks except the last chunk have a trailing non-finite + for chunk in chunks[:-1]: + lc = len(chunk) + if lc <= 1: + # len 1 means we have a string of non-finite + continue + subpoly_resize(lc) + subarr[:lc] = chunk + subarr[lc-1] = subarr[lc-2] # fill non-finite with its neighbour + path.addPolygon(subpoly) + + # handle last chunk, which is either all-finite or empty + for chunk in chunks[-1:]: + lc = len(chunk) + if lc <= 1: + # can't draw a line with just 1 point + continue + subpoly_resize(lc) + subarr[:lc] = chunk + path.addPolygon(subpoly) + + return path elif connect == 'array': arr[1:]['c'] = connect_array[:-1] else: From 8997cfa07c7a0ff4209f60702b028a81f46e14ca Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Jun 2021 12:48:08 +0800 Subject: [PATCH 2/3] test the QPolygonF codepath --- tests/graphicsItems/test_PlotCurveItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/graphicsItems/test_PlotCurveItem.py b/tests/graphicsItems/test_PlotCurveItem.py index 9c51a4a0..c97811cc 100644 --- a/tests/graphicsItems/test_PlotCurveItem.py +++ b/tests/graphicsItems/test_PlotCurveItem.py @@ -25,6 +25,9 @@ def test_PlotCurveItem(): c.setData(data, connect='finite') assertImageApproved(p, 'plotcurveitem/connectfinite', "Plot curve with finite points connected.") + + c.setData(data, connect='finite', skipFiniteCheck=True) + assertImageApproved(p, 'plotcurveitem/connectfinite', "Plot curve with finite points connected using QPolygonF.") c.setData(data, connect=np.array([1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,0])) assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.") From ecc3563f6df9669236c4e85a3936db493050562e Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Sat, 5 Jun 2021 20:13:07 +0800 Subject: [PATCH 3/3] special-case all-finite --- pyqtgraph/functions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1ab54a48..15c2cf12 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1768,6 +1768,8 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True): if nonfinite_cnt / n < 2 / 100: use_qpolygonf = True finiteCheck = False + if nonfinite_cnt == 0: + connect = 'all' if use_qpolygonf: backstore = create_qpolygonf(n)