dynamic range limiting in PlotDataItem (#1140)

* dynamic range limiting in PlotDataItem

* revised version of cynamic range limiting

* replaced == with is operator

* removed unicode +- character, converted to ascii

* code/docstring cleanup

* clean state with changes

* silenced numpy all-NaN warnings

* reverted PlotWidget.py to original

* reverted PlotWidget.py to original

* reverted PlotWidget.py to original

* rewrapped/reformated setDynamicRangeLimits docstring

Co-authored-by: Ogi Moore <ognyan.moore@gmail.com>
This commit is contained in:
Nils Nemitz 2020-10-19 14:34:41 +09:00 committed by GitHub
parent 39f9c6a6aa
commit 65e90faec5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 210 additions and 140 deletions

View File

@ -6,6 +6,7 @@ try:
except:
HAVE_OPENGL = False
import warnings
import numpy as np
from .GraphicsObject import GraphicsObject
from .. import functions as fn
@ -148,6 +149,9 @@ class PlotCurveItem(GraphicsObject):
if frac >= 1.0:
# include complete data range
# first try faster nanmin/max function, then cut out infs if needed.
with warnings.catch_warnings():
# All-NaN data is acceptable; Explicit numpy warning is not needed.
warnings.simplefilter("ignore")
b = (np.nanmin(d), np.nanmax(d))
if any(np.isinf(b)):
mask = np.isfinite(d)

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import warnings
import numpy as np
from .. import metaarray as metaarray
from ..Qt import QtCore
@ -52,6 +53,7 @@ class PlotDataItem(GraphicsObject):
**Data initialization arguments:** (x,y data AND may include spot style)
============================ =========================================
PlotDataItem(recarray) numpy array with ``dtype=[('x', float),
('y', float), ...]``
@ -73,6 +75,7 @@ class PlotDataItem(GraphicsObject):
shadowPen Pen for secondary line to draw behind the primary line. disabled by default.
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
fillLevel Fill the area between the curve and fillLevel
fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn.
fillBrush Fill to use when fillLevel is specified.
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
@ -88,6 +91,7 @@ class PlotDataItem(GraphicsObject):
step mode is not enabled.
Passing True is a deprecated equivalent to "center".
(added in version 0.9.9)
============ ==============================================================================
**Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information)
@ -110,7 +114,7 @@ class PlotDataItem(GraphicsObject):
**Optimization keyword arguments:**
================ =====================================================================
================= =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False.
@ -130,8 +134,10 @@ class PlotDataItem(GraphicsObject):
the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible
at any time.
dynamicRangeLimit (float or None) Limit off-screen positions of data points at large
magnification to avoids display errors. Disabled if None.
identical *deprecated*
================ =====================================================================
================= =====================================================================
**Meta-info keyword arguments:**
@ -156,7 +162,7 @@ class PlotDataItem(GraphicsObject):
self.curve.sigClicked.connect(self.curveClicked)
self.scatter.sigClicked.connect(self.scatterClicked)
self._dataRect = None
#self.clear()
self.opts = {
'connect': 'all',
@ -189,6 +195,7 @@ class PlotDataItem(GraphicsObject):
'downsampleMethod': 'peak',
'autoDownsampleFactor': 5., # draw ~5 samples per pixel
'clipToView': False,
'dynamicRangeLimit': 1e6,
'data': None,
}
@ -232,6 +239,7 @@ class PlotDataItem(GraphicsObject):
self.updateItems()
self.informViewBoundsChanged()
def setDerivativeMode(self, mode):
if self.opts['derivativeMode'] == mode:
return
@ -315,8 +323,6 @@ class PlotDataItem(GraphicsObject):
#self.scatter.setSymbolPen(pen)
self.updateItems()
def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush:
@ -377,6 +383,24 @@ class PlotDataItem(GraphicsObject):
self.xDisp = self.yDisp = None
self.updateItems()
def setDynamicRangeLimit(self, limit):
"""
Limit the off-screen positions of data points at large magnification
This avoids errors with plots not displaying because their visibility is incorrectly determined. The default setting repositions far-off points to be within +-1E+06 times the viewport height.
=============== ================================================================
**Arguments:**
limit (float or None) Maximum allowed vertical distance of plotted
points in units of viewport height.
'None' disables the check for a minimal increase in performance.
Default is 1E+06.
=============== ================================================================
"""
if limit == self.opts['dynamicRangeLimit']:
return # avoid update if there is no change
self.opts['dynamicRangeLimit'] = limit # can be None
self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs):
"""
@ -497,6 +521,7 @@ class PlotDataItem(GraphicsObject):
self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by
self.yData = y.view(np.ndarray)
self._dataRect = None
self.xClean = self.yClean = None
self.xDisp = None
self.yDisp = None
@ -534,6 +559,7 @@ class PlotDataItem(GraphicsObject):
self.curve.hide()
if scatterArgs['symbol'] is not None:
## check against `True` too for backwards compatibility
if self.opts.get('stepMode', False) in ("center", True):
x = 0.5 * (x[:-1] + x[1:])
@ -557,6 +583,7 @@ class PlotDataItem(GraphicsObject):
if self.opts['logMode'][0]:
x=x[1:]
y=y[1:]
if self.opts['derivativeMode']: # plot dV/dt
y = np.diff(self.yData)/np.diff(self.xData)
x = x[:-1]
@ -609,7 +636,6 @@ class PlotDataItem(GraphicsObject):
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]
@ -632,11 +658,47 @@ class PlotDataItem(GraphicsObject):
y1[:,1] = y2.min(axis=1)
y = y1.reshape(n*2)
if self.opts['dynamicRangeLimit'] is not None:
view_range = self.viewRect()
if view_range is not None:
data_range = self.dataRect()
if data_range is not None:
view_height = view_range.height()
lim = self.opts['dynamicRangeLimit']
if data_range.height() > lim * view_height:
min_val = view_range.top() - lim * view_height
max_val = view_range.bottom() + lim * view_height
y = np.clip(y, a_min=min_val, a_max=max_val)
self.xDisp = x
self.yDisp = y
return self.xDisp, self.yDisp
def dataRect(self):
"""
Returns a bounding rectangle (as QRectF) for the full set of data.
Will return None if there is no data or if all values (x or y) are NaN.
"""
if self._dataRect is not None:
return self._dataRect
if self.xData is None or self.yData is None:
return None
with warnings.catch_warnings():
# All-NaN data is handled by returning None; Explicit numpy warning is not needed.
warnings.simplefilter("ignore")
ymin = np.nanmin(self.yData)
if np.isnan( ymin ):
return None # most likely case for all-NaN data
xmin = np.nanmin(self.xData)
if np.isnan( xmin ):
return None # less likely case for all-NaN data
ymax = np.nanmax(self.yData)
xmax = np.nanmax(self.xData)
self._dataRect = QtCore.QRectF(
QtCore.QPointF(xmin,ymin),
QtCore.QPointF(xmax,ymax) )
return self._dataRect
def dataBounds(self, ax, frac=1.0, orthoRange=None):
"""
Returns the range occupied by the data (along a specific axis) in this item.
@ -693,6 +755,7 @@ class PlotDataItem(GraphicsObject):
#self.yClean = None
self.xDisp = None
self.yDisp = None
self._dataRect = None
self.curve.clear()
self.scatter.clear()
@ -708,7 +771,10 @@ class PlotDataItem(GraphicsObject):
def viewRangeChanged(self):
# view range has changed; re-plot if needed
if self.opts['clipToView'] or self.opts['autoDownsample']:
if( self.opts['clipToView']
or self.opts['autoDownsample']
or self.opts['dynamicRangeLimit'] is not None
):
self.xDisp = self.yDisp = None
self.updateItems()