From 365c13fedc26f316149f9d8f114598b29d65703f Mon Sep 17 00:00:00 2001 From: SamSchott Date: Thu, 14 Mar 2019 22:41:10 +0000 Subject: [PATCH 1/3] Clipping: don't assume that x-values have uniform spacing Do not assume that x-values have uniform spacing -- this can cause problems especially with large datasets and non-uniform spacing (e.g., time-dependent readings from an instrument). Use `np.searchsorted` instead to find the first and last data index in the view range. This only assumes that x-values are in ascending order. This prevents potentially too strong clipping. --- pyqtgraph/graphicsItems/PlotDataItem.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6797af64..69d7dc6e 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -540,15 +540,16 @@ class PlotDataItem(GraphicsObject): if self.opts['clipToView']: view = self.getViewBox() if view is None or not view.autoRangeEnabled()[0]: - # this option presumes that x-values have uniform spacing + # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - dx = float(x[-1]-x[0]) / (len(x)-1) # clip to visible region extended by downsampling value - x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) - x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) - x = x[x0:x1] - y = y[x0:x1] + idx = np.searchsorted(x, [range.left(), range.right()]) + idx = idx + np.array([-2*ds, 2*ds]) + idx = np.clip(idx, a_min=0, a_max=len(x)) + + x = x[idx[0]:idx[1]] + y = y[idx[0]:idx[1]] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From f23889d5940b5b9be6c76be8c3e78e80689e223d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 13:53:00 +0100 Subject: [PATCH 2/3] only use `np.searchsorted` quicker method fails --- pyqtgraph/graphicsItems/PlotDataItem.py | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 69d7dc6e..7c7c2fba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -543,13 +543,25 @@ class PlotDataItem(GraphicsObject): # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - # clip to visible region extended by downsampling value - idx = np.searchsorted(x, [range.left(), range.right()]) - idx = idx + np.array([-2*ds, 2*ds]) - idx = np.clip(idx, a_min=0, a_max=len(x)) - - x = x[idx[0]:idx[1]] - y = y[idx[0]:idx[1]] + # clip to visible region extended by downsampling value, assuming + # uniform spacing of x-values, has O(1) performance + dx = float(x[-1]-x[0]) / (len(x)-1) + x0 = np.clip(int((range.left()-x[0])/dx) - 1*ds, 0, len(x)-1) + x1 = np.clip(int((range.right()-x[0])/dx) + 2*ds, 0, len(x)-1) + + # if data has been clipped too strongly (in case of non-uniform + # spacing of x-values), refine the clipping region as required + # worst case performance: O(log(n)) + # best case performance: O(1) + if x[x0] > range.left(): + x0 = np.searchsorted(x, range.left()) - 1*ds + x0 = np.clip(x0, a_min=0, a_max=len(x)) + if x[x1] < range.right(): + x1 = np.searchsorted(x, range.right()) + 2*ds + x1 = np.clip(x1, a_min=0, a_max=len(x)) + + x = x[x0:x1] + y = y[x0:x1] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From d870a34359682251dc22079161b003b5ad060c77 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 14:49:32 +0100 Subject: [PATCH 3/3] add a test for clipping --- .../graphicsItems/tests/test_PlotDataItem.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index b506a654..adc525d9 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -64,3 +64,25 @@ def test_clear_in_step_mode(): c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) w.addItem(c) c.clear() + +def test_clipping(): + y = np.random.normal(size=150) + x = np.exp2(np.linspace(5, 10, 150)) # non-uniform spacing + + w = pg.PlotWidget(autoRange=True, downsample=5) + c = pg.PlotDataItem(x, y) + w.addItem(c) + w.show() + + c.setClipToView(True) + + w.setXRange(200, 600) + + for x_min in range(100, 2**10 - 100, 100): + w.setXRange(x_min, x_min + 100) + + xDisp, _ = c.getData() + vr = c.viewRect() + + assert xDisp[0] <= vr.left() + assert xDisp[-1] >= vr.right()