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,7 +149,10 @@ class PlotCurveItem(GraphicsObject):
if frac >= 1.0:
# include complete data range
# first try faster nanmin/max function, then cut out infs if needed.
b = (np.nanmin(d), np.nanmax(d))
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)
d = d[mask]

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,28 +114,30 @@ 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.
decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best
visual representation of the data but is slower.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
clipToView (bool) If True, only plot data that is visible within the X range of
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.
identical *deprecated*
================ =====================================================================
================= =====================================================================
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.
decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best
visual representation of the data but is slower.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
clipToView (bool) If True, only plot data that is visible within the X range of
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()