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: except:
HAVE_OPENGL = False HAVE_OPENGL = False
import warnings
import numpy as np import numpy as np
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from .. import functions as fn from .. import functions as fn
@ -148,7 +149,10 @@ class PlotCurveItem(GraphicsObject):
if frac >= 1.0: if frac >= 1.0:
# include complete data range # include complete data range
# first try faster nanmin/max function, then cut out infs if needed. # 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)): if any(np.isinf(b)):
mask = np.isfinite(d) mask = np.isfinite(d)
d = d[mask] d = d[mask]

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import warnings
import numpy as np import numpy as np
from .. import metaarray as metaarray from .. import metaarray as metaarray
from ..Qt import QtCore from ..Qt import QtCore
@ -13,33 +14,33 @@ from .. import getConfigOption
class PlotDataItem(GraphicsObject): class PlotDataItem(GraphicsObject):
""" """
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
GraphicsItem for displaying plot curves, scatter plots, or both. GraphicsItem for displaying plot curves, scatter plots, or both.
While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or
:class:`ScatterPlotItem <pyqtgraph.ScatterPlotItem>` individually, this class :class:`ScatterPlotItem <pyqtgraph.ScatterPlotItem>` individually, this class
provides a unified interface to both. Instances of :class:`PlotDataItem` are provides a unified interface to both. Instances of :class:`PlotDataItem` are
usually created by plot() methods such as :func:`pyqtgraph.plot` and usually created by plot() methods such as :func:`pyqtgraph.plot` and
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`. :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
============================== ============================================== ============================== ==============================================
**Signals:** **Signals:**
sigPlotChanged(self) Emitted when the data in this item is updated. sigPlotChanged(self) Emitted when the data in this item is updated.
sigClicked(self) Emitted when the item is clicked. sigClicked(self) Emitted when the item is clicked.
sigPointsClicked(self, points) Emitted when a plot point is clicked sigPointsClicked(self, points) Emitted when a plot point is clicked
Sends the list of points under the mouse. Sends the list of points under the mouse.
============================== ============================================== ============================== ==============================================
""" """
sigPlotChanged = QtCore.Signal(object) sigPlotChanged = QtCore.Signal(object)
sigClicked = QtCore.Signal(object) sigClicked = QtCore.Signal(object)
sigPointsClicked = QtCore.Signal(object, object) sigPointsClicked = QtCore.Signal(object, object)
def __init__(self, *args, **kargs): def __init__(self, *args, **kargs):
""" """
There are many different ways to create a PlotDataItem: There are many different ways to create a PlotDataItem:
**Data initialization arguments:** (x,y data only) **Data initialization arguments:** (x,y data only)
=================================== ====================================== =================================== ======================================
PlotDataItem(xValues, yValues) x and y values may be any sequence PlotDataItem(xValues, yValues) x and y values may be any sequence
(including ndarray) of real numbers (including ndarray) of real numbers
@ -49,8 +50,9 @@ class PlotDataItem(GraphicsObject):
PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where
``x=data[:,0]`` and ``y=data[:,1]`` ``x=data[:,0]`` and ``y=data[:,1]``
=================================== ====================================== =================================== ======================================
**Data initialization arguments:** (x,y data AND may include spot style) **Data initialization arguments:** (x,y data AND may include spot style)
============================ ========================================= ============================ =========================================
PlotDataItem(recarray) numpy array with ``dtype=[('x', float), PlotDataItem(recarray) numpy array with ``dtype=[('x', float),
@ -73,6 +75,7 @@ class PlotDataItem(GraphicsObject):
shadowPen Pen for secondary line to draw behind the primary line. disabled by default. 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>` May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
fillLevel Fill the area between the curve and fillLevel fillLevel Fill the area between the curve and fillLevel
fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn. fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn.
fillBrush Fill to use when fillLevel is specified. fillBrush Fill to use when fillLevel is specified.
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>` May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
@ -88,53 +91,56 @@ class PlotDataItem(GraphicsObject):
step mode is not enabled. step mode is not enabled.
Passing True is a deprecated equivalent to "center". Passing True is a deprecated equivalent to "center".
(added in version 0.9.9) (added in version 0.9.9)
============ ============================================================================== ============ ==============================================================================
**Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information) **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information)
============ ===================================================== ============ =====================================================
symbol Symbol to use for drawing points OR list of symbols, symbol Symbol to use for drawing points OR list of symbols,
one per point. Default is no symbol. one per point. Default is no symbol.
Options are o, s, t, d, +, or any QPainterPath Options are o, s, t, d, +, or any QPainterPath
symbolPen Outline pen for drawing points OR list of pens, one symbolPen Outline pen for drawing points OR list of pens, one
per point. May be any single argument accepted by per point. May be any single argument accepted by
:func:`mkPen() <pyqtgraph.mkPen>` :func:`mkPen() <pyqtgraph.mkPen>`
symbolBrush Brush for filling points OR list of brushes, one per symbolBrush Brush for filling points OR list of brushes, one per
point. May be any single argument accepted by point. May be any single argument accepted by
:func:`mkBrush() <pyqtgraph.mkBrush>` :func:`mkBrush() <pyqtgraph.mkBrush>`
symbolSize Diameter of symbols OR list of diameters. symbolSize Diameter of symbols OR list of diameters.
pxMode (bool) If True, then symbolSize is specified in pxMode (bool) If True, then symbolSize is specified in
pixels. If False, then symbolSize is pixels. If False, then symbolSize is
specified in data coordinates. specified in data coordinates.
============ ===================================================== ============ =====================================================
**Optimization keyword arguments:** **Optimization keyword arguments:**
================ ===================================================================== ================= =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance. antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False. will be rendered antialiased even if this is set to False.
decimate deprecated. decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples. downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate. This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples. 'mean': Downsample by taking the mean of N samples.
'peak': Downsample by drawing a saw wave that follows the min 'peak': Downsample by drawing a saw wave that follows the min
and max of the original data. This method produces the best and max of the original data. This method produces the best
visual representation of the data but is slower. visual representation of the data but is slower.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead viewing very high-density data, but increases the initial overhead
and memory usage. and memory usage.
clipToView (bool) If True, only plot data that is visible within the X range of clipToView (bool) If True, only plot data that is visible within the X range of
the containing ViewBox. This can improve performance when plotting the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible very large data sets where only a fraction of the data is visible
at any time. at any time.
identical *deprecated* 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:** **Meta-info keyword arguments:**
========== ================================================ ========== ================================================
name name of dataset. This would appear in a legend name name of dataset. This would appear in a legend
========== ================================================ ========== ================================================
@ -152,57 +158,58 @@ class PlotDataItem(GraphicsObject):
self.scatter = ScatterPlotItem() self.scatter = ScatterPlotItem()
self.curve.setParentItem(self) self.curve.setParentItem(self)
self.scatter.setParentItem(self) self.scatter.setParentItem(self)
self.curve.sigClicked.connect(self.curveClicked) self.curve.sigClicked.connect(self.curveClicked)
self.scatter.sigClicked.connect(self.scatterClicked) self.scatter.sigClicked.connect(self.scatterClicked)
self._dataRect = None
#self.clear() #self.clear()
self.opts = { self.opts = {
'connect': 'all', 'connect': 'all',
'fftMode': False, 'fftMode': False,
'logMode': [False, False], 'logMode': [False, False],
'derivativeMode': False, 'derivativeMode': False,
'phasemapMode': False, 'phasemapMode': False,
'alphaHint': 1.0, 'alphaHint': 1.0,
'alphaMode': False, 'alphaMode': False,
'pen': (200,200,200), 'pen': (200,200,200),
'shadowPen': None, 'shadowPen': None,
'fillLevel': None, 'fillLevel': None,
'fillOutline': False, 'fillOutline': False,
'fillBrush': None, 'fillBrush': None,
'stepMode': None, 'stepMode': None,
'symbol': None, 'symbol': None,
'symbolSize': 10, 'symbolSize': 10,
'symbolPen': (200,200,200), 'symbolPen': (200,200,200),
'symbolBrush': (50, 50, 150), 'symbolBrush': (50, 50, 150),
'pxMode': True, 'pxMode': True,
'antialias': getConfigOption('antialias'), 'antialias': getConfigOption('antialias'),
'pointMode': None, 'pointMode': None,
'downsample': 1, 'downsample': 1,
'autoDownsample': False, 'autoDownsample': False,
'downsampleMethod': 'peak', 'downsampleMethod': 'peak',
'autoDownsampleFactor': 5., # draw ~5 samples per pixel 'autoDownsampleFactor': 5., # draw ~5 samples per pixel
'clipToView': False, 'clipToView': False,
'dynamicRangeLimit': 1e6,
'data': None, 'data': None,
} }
self.setData(*args, **kargs) self.setData(*args, **kargs)
def implements(self, interface=None): def implements(self, interface=None):
ints = ['plotData'] ints = ['plotData']
if interface is None: if interface is None:
return ints return ints
return interface in ints return interface in ints
def name(self): def name(self):
return self.opts.get('name', None) return self.opts.get('name', None)
def boundingRect(self): def boundingRect(self):
return QtCore.QRectF() ## let child items handle this return QtCore.QRectF() ## let child items handle this
@ -213,7 +220,7 @@ class PlotDataItem(GraphicsObject):
self.opts['alphaMode'] = auto self.opts['alphaMode'] = auto
self.setOpacity(alpha) self.setOpacity(alpha)
#self.update() #self.update()
def setFftMode(self, mode): def setFftMode(self, mode):
if self.opts['fftMode'] == mode: if self.opts['fftMode'] == mode:
return return
@ -222,7 +229,7 @@ class PlotDataItem(GraphicsObject):
self.xClean = self.yClean = None self.xClean = self.yClean = None
self.updateItems() self.updateItems()
self.informViewBoundsChanged() self.informViewBoundsChanged()
def setLogMode(self, xMode, yMode): def setLogMode(self, xMode, yMode):
if self.opts['logMode'] == [xMode, yMode]: if self.opts['logMode'] == [xMode, yMode]:
return return
@ -232,6 +239,7 @@ class PlotDataItem(GraphicsObject):
self.updateItems() self.updateItems()
self.informViewBoundsChanged() self.informViewBoundsChanged()
def setDerivativeMode(self, mode): def setDerivativeMode(self, mode):
if self.opts['derivativeMode'] == mode: if self.opts['derivativeMode'] == mode:
return return
@ -255,7 +263,7 @@ class PlotDataItem(GraphicsObject):
return return
self.opts['pointMode'] = mode self.opts['pointMode'] = mode
self.update() self.update()
def setPen(self, *args, **kargs): def setPen(self, *args, **kargs):
""" """
| Sets the pen used to draw lines between points. | Sets the pen used to draw lines between points.
@ -268,11 +276,11 @@ class PlotDataItem(GraphicsObject):
#c.setPen(pen) #c.setPen(pen)
#self.update() #self.update()
self.updateItems() self.updateItems()
def setShadowPen(self, *args, **kargs): def setShadowPen(self, *args, **kargs):
""" """
| Sets the shadow pen used to draw lines between points (this is for enhancing contrast or | Sets the shadow pen used to draw lines between points (this is for enhancing contrast or
emphacizing data). emphacizing data).
| This line is drawn behind the primary pen (see :func:`setPen() <pyqtgraph.PlotDataItem.setPen>`) | This line is drawn behind the primary pen (see :func:`setPen() <pyqtgraph.PlotDataItem.setPen>`)
and should generally be assigned greater width than the primary pen. and should generally be assigned greater width than the primary pen.
| *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>` | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>`
@ -283,17 +291,17 @@ class PlotDataItem(GraphicsObject):
#c.setPen(pen) #c.setPen(pen)
#self.update() #self.update()
self.updateItems() self.updateItems()
def setFillBrush(self, *args, **kargs): def setFillBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['fillBrush'] == brush: if self.opts['fillBrush'] == brush:
return return
self.opts['fillBrush'] = brush self.opts['fillBrush'] = brush
self.updateItems() self.updateItems()
def setBrush(self, *args, **kargs): def setBrush(self, *args, **kargs):
return self.setFillBrush(*args, **kargs) return self.setFillBrush(*args, **kargs)
def setFillLevel(self, level): def setFillLevel(self, level):
if self.opts['fillLevel'] == level: if self.opts['fillLevel'] == level:
return return
@ -306,7 +314,7 @@ class PlotDataItem(GraphicsObject):
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol) #self.scatter.setSymbol(symbol)
self.updateItems() self.updateItems()
def setSymbolPen(self, *args, **kargs): def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs) pen = fn.mkPen(*args, **kargs)
if self.opts['symbolPen'] == pen: if self.opts['symbolPen'] == pen:
@ -314,9 +322,7 @@ class PlotDataItem(GraphicsObject):
self.opts['symbolPen'] = pen self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen) #self.scatter.setSymbolPen(pen)
self.updateItems() self.updateItems()
def setSymbolBrush(self, *args, **kargs): def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush: if self.opts['symbolBrush'] == brush:
@ -324,8 +330,8 @@ class PlotDataItem(GraphicsObject):
self.opts['symbolBrush'] = brush self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush) #self.scatter.setSymbolBrush(brush)
self.updateItems() self.updateItems()
def setSymbolSize(self, size): def setSymbolSize(self, size):
if self.opts['symbolSize'] == size: if self.opts['symbolSize'] == size:
return return
@ -336,8 +342,8 @@ class PlotDataItem(GraphicsObject):
def setDownsampling(self, ds=None, auto=None, method=None): def setDownsampling(self, ds=None, auto=None, method=None):
""" """
Set the downsampling mode of this item. Downsampling reduces the number Set the downsampling mode of this item. Downsampling reduces the number
of samples drawn to increase performance. of samples drawn to increase performance.
============== ================================================================= ============== =================================================================
**Arguments:** **Arguments:**
ds (int) Reduce visible plot samples by this factor. To disable, ds (int) Reduce visible plot samples by this factor. To disable,
@ -356,28 +362,46 @@ class PlotDataItem(GraphicsObject):
if self.opts['downsample'] != ds: if self.opts['downsample'] != ds:
changed = True changed = True
self.opts['downsample'] = ds self.opts['downsample'] = ds
if auto is not None and self.opts['autoDownsample'] != auto: if auto is not None and self.opts['autoDownsample'] != auto:
self.opts['autoDownsample'] = auto self.opts['autoDownsample'] = auto
changed = True changed = True
if method is not None: if method is not None:
if self.opts['downsampleMethod'] != method: if self.opts['downsampleMethod'] != method:
changed = True changed = True
self.opts['downsampleMethod'] = method self.opts['downsampleMethod'] = method
if changed: if changed:
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setClipToView(self, clip): def setClipToView(self, clip):
if self.opts['clipToView'] == clip: if self.opts['clipToView'] == clip:
return return
self.opts['clipToView'] = clip self.opts['clipToView'] = clip
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() 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): def setData(self, *args, **kargs):
""" """
Clear any data displayed by this item and display new data. Clear any data displayed by this item and display new data.
@ -421,7 +445,7 @@ class PlotDataItem(GraphicsObject):
x = data.xvals(0).view(np.ndarray) x = data.xvals(0).view(np.ndarray)
else: else:
raise Exception('Invalid data type %s' % type(data)) raise Exception('Invalid data type %s' % type(data))
elif len(args) == 2: elif len(args) == 2:
seq = ('listOfValues', 'MetaArray', 'empty') seq = ('listOfValues', 'MetaArray', 'empty')
dtyp = dataType(args[0]), dataType(args[1]) dtyp = dataType(args[0]), dataType(args[1])
@ -443,45 +467,45 @@ class PlotDataItem(GraphicsObject):
y = np.array(args[1]) y = np.array(args[1])
else: else:
y = args[1].view(np.ndarray) y = args[1].view(np.ndarray)
if 'x' in kargs: if 'x' in kargs:
x = kargs['x'] x = kargs['x']
if 'y' in kargs: if 'y' in kargs:
y = kargs['y'] y = kargs['y']
profiler('interpret data') profiler('interpret data')
## pull in all style arguments. ## pull in all style arguments.
## Use self.opts to fill in anything not present in kargs. ## Use self.opts to fill in anything not present in kargs.
if 'name' in kargs: if 'name' in kargs:
self.opts['name'] = kargs['name'] self.opts['name'] = kargs['name']
if 'connect' in kargs: if 'connect' in kargs:
self.opts['connect'] = kargs['connect'] self.opts['connect'] = kargs['connect']
## if symbol pen/brush are given with no symbol, then assume symbol is 'o' ## if symbol pen/brush are given with no symbol, then assume symbol is 'o'
if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs):
kargs['symbol'] = 'o' kargs['symbol'] = 'o'
if 'brush' in kargs: if 'brush' in kargs:
kargs['fillBrush'] = kargs['brush'] kargs['fillBrush'] = kargs['brush']
for k in list(self.opts.keys()): for k in list(self.opts.keys()):
if k in kargs: if k in kargs:
self.opts[k] = kargs[k] self.opts[k] = kargs[k]
#curveArgs = {} #curveArgs = {}
#for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: #for k in ['pen', 'shadowPen', 'fillLevel', 'brush']:
#if k in kargs: #if k in kargs:
#self.opts[k] = kargs[k] #self.opts[k] = kargs[k]
#curveArgs[k] = self.opts[k] #curveArgs[k] = self.opts[k]
#scatterArgs = {} #scatterArgs = {}
#for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: #for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]:
#if k in kargs: #if k in kargs:
#self.opts[k] = kargs[k] #self.opts[k] = kargs[k]
#scatterArgs[v] = self.opts[k] #scatterArgs[v] = self.opts[k]
if y is None: if y is None:
self.updateItems() self.updateItems()
@ -489,51 +513,53 @@ class PlotDataItem(GraphicsObject):
return return
if y is not None and x is None: if y is not None and x is None:
x = np.arange(len(y)) x = np.arange(len(y))
if not isinstance(x, np.ndarray): if not isinstance(x, np.ndarray):
x = np.array(x) x = np.array(x)
if not isinstance(y, np.ndarray): if not isinstance(y, np.ndarray):
y = np.array(y) y = np.array(y)
self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by 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.yData = y.view(np.ndarray)
self._dataRect = None
self.xClean = self.yClean = None self.xClean = self.yClean = None
self.xDisp = None self.xDisp = None
self.yDisp = None self.yDisp = None
profiler('set data') profiler('set data')
self.updateItems() self.updateItems()
profiler('update items') profiler('update items')
self.informViewBoundsChanged() self.informViewBoundsChanged()
#view = self.getViewBox() #view = self.getViewBox()
#if view is not None: #if view is not None:
#view.itemBoundsChanged(self) ## inform view so it can update its range if it wants #view.itemBoundsChanged(self) ## inform view so it can update its range if it wants
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
profiler('emit') profiler('emit')
def updateItems(self): def updateItems(self):
curveArgs = {} curveArgs = {}
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillOutline', 'fillOutline'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]: for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillOutline', 'fillOutline'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]:
curveArgs[v] = self.opts[k] curveArgs[v] = self.opts[k]
scatterArgs = {} scatterArgs = {}
for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data'), ('pxMode', 'pxMode'), ('antialias', 'antialias')]: for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data'), ('pxMode', 'pxMode'), ('antialias', 'antialias')]:
if k in self.opts: if k in self.opts:
scatterArgs[v] = self.opts[k] scatterArgs[v] = self.opts[k]
x,y = self.getData() x,y = self.getData()
#scatterArgs['mask'] = self.dataMask #scatterArgs['mask'] = self.dataMask
if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None): if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None):
self.curve.setData(x=x, y=y, **curveArgs) self.curve.setData(x=x, y=y, **curveArgs)
self.curve.show() self.curve.show()
else: else:
self.curve.hide() self.curve.hide()
if scatterArgs['symbol'] is not None: if scatterArgs['symbol'] is not None:
## check against `True` too for backwards compatibility ## check against `True` too for backwards compatibility
if self.opts.get('stepMode', False) in ("center", True): if self.opts.get('stepMode', False) in ("center", True):
x = 0.5 * (x[:-1] + x[1:]) x = 0.5 * (x[:-1] + x[1:])
@ -546,17 +572,18 @@ class PlotDataItem(GraphicsObject):
def getData(self): def getData(self):
if self.xData is None: if self.xData is None:
return (None, None) return (None, None)
if self.xDisp is None: if self.xDisp is None:
x = self.xData x = self.xData
y = self.yData y = self.yData
if self.opts['fftMode']: if self.opts['fftMode']:
x,y = self._fourierTransform(x, y) x,y = self._fourierTransform(x, y)
# Ignore the first bin for fft data if we have a logx scale # Ignore the first bin for fft data if we have a logx scale
if self.opts['logMode'][0]: if self.opts['logMode'][0]:
x=x[1:] x=x[1:]
y=y[1:] y=y[1:]
if self.opts['derivativeMode']: # plot dV/dt if self.opts['derivativeMode']: # plot dV/dt
y = np.diff(self.yData)/np.diff(self.xData) y = np.diff(self.yData)/np.diff(self.xData)
x = x[:-1] x = x[:-1]
@ -569,11 +596,11 @@ class PlotDataItem(GraphicsObject):
x = np.log10(x) x = np.log10(x)
if self.opts['logMode'][1]: if self.opts['logMode'][1]:
y = np.log10(y) y = np.log10(y)
ds = self.opts['downsample'] ds = self.opts['downsample']
if not isinstance(ds, int): if not isinstance(ds, int):
ds = 1 ds = 1
if self.opts['autoDownsample']: if self.opts['autoDownsample']:
# this option presumes that x-values have uniform spacing # this option presumes that x-values have uniform spacing
range = self.viewRect() range = self.viewRect()
@ -586,7 +613,7 @@ class PlotDataItem(GraphicsObject):
if width != 0.0: if width != 0.0:
ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor'])))) ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor']))))
## downsampling is expensive; delay until after clipping. ## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']: if self.opts['clipToView']:
view = self.getViewBox() view = self.getViewBox()
if view is None or not view.autoRangeEnabled()[0]: if view is None or not view.autoRangeEnabled()[0]:
@ -598,8 +625,8 @@ class PlotDataItem(GraphicsObject):
dx = float(x[-1]-x[0]) / (len(x)-1) dx = float(x[-1]-x[0]) / (len(x)-1)
x0 = np.clip(int((range.left()-x[0])/dx) - 1*ds, 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) 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 # if data has been clipped too strongly (in case of non-uniform
# spacing of x-values), refine the clipping region as required # spacing of x-values), refine the clipping region as required
# worst case performance: O(log(n)) # worst case performance: O(log(n))
# best case performance: O(1) # best case performance: O(1)
@ -609,10 +636,9 @@ class PlotDataItem(GraphicsObject):
if x[x1] < range.right(): if x[x1] < range.right():
x1 = np.searchsorted(x, range.right()) + 2*ds x1 = np.searchsorted(x, range.right()) + 2*ds
x1 = np.clip(x1, a_min=0, a_max=len(x)) x1 = np.clip(x1, a_min=0, a_max=len(x))
x = x[x0:x1] x = x[x0:x1]
y = y[x0:x1] y = y[x0:x1]
if ds > 1: if ds > 1:
if self.opts['downsampleMethod'] == 'subsample': if self.opts['downsampleMethod'] == 'subsample':
x = x[::ds] x = x[::ds]
@ -631,12 +657,48 @@ class PlotDataItem(GraphicsObject):
y1[:,0] = y2.max(axis=1) y1[:,0] = y2.max(axis=1)
y1[:,1] = y2.min(axis=1) y1[:,1] = y2.min(axis=1)
y = y1.reshape(n*2) 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.xDisp = x
self.yDisp = y self.yDisp = y
return self.xDisp, self.yDisp 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): def dataBounds(self, ax, frac=1.0, orthoRange=None):
""" """
Returns the range occupied by the data (along a specific axis) in this item. Returns the range occupied by the data (along a specific axis) in this item.
@ -645,18 +707,18 @@ class PlotDataItem(GraphicsObject):
=============== ============================================================= =============== =============================================================
**Arguments:** **Arguments:**
ax (0 or 1) the axis for which to return this item's data range ax (0 or 1) the axis for which to return this item's data range
frac (float 0.0-1.0) Specifies what fraction of the total data frac (float 0.0-1.0) Specifies what fraction of the total data
range to return. By default, the entire range is returned. range to return. By default, the entire range is returned.
This allows the ViewBox to ignore large spikes in the data This allows the ViewBox to ignore large spikes in the data
when auto-scaling. when auto-scaling.
orthoRange ([min,max] or None) Specifies that only the data within the orthoRange ([min,max] or None) Specifies that only the data within the
given range (orthogonal to *ax*) should me measured when given range (orthogonal to *ax*) should me measured when
returning the data range. (For example, a ViewBox might ask returning the data range. (For example, a ViewBox might ask
what is the y-range of all data with x-values between min what is the y-range of all data with x-values between min
and max) and max)
=============== ============================================================= =============== =============================================================
""" """
range = [None, None] range = [None, None]
if self.curve.isVisible(): if self.curve.isVisible():
range = self.curve.dataBounds(ax, frac, orthoRange) range = self.curve.dataBounds(ax, frac, orthoRange)
@ -667,7 +729,7 @@ class PlotDataItem(GraphicsObject):
r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1])) r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1]))
] ]
return range return range
def pixelPadding(self): def pixelPadding(self):
""" """
Return the size in pixels that this item may draw beyond the values returned by dataBounds(). Return the size in pixels that this item may draw beyond the values returned by dataBounds().
@ -679,7 +741,7 @@ class PlotDataItem(GraphicsObject):
elif self.scatter.isVisible(): elif self.scatter.isVisible():
pad = max(pad, self.scatter.pixelPadding()) pad = max(pad, self.scatter.pixelPadding())
return pad return pad
def clear(self): def clear(self):
#for i in self.curves+self.scatters: #for i in self.curves+self.scatters:
@ -693,25 +755,29 @@ class PlotDataItem(GraphicsObject):
#self.yClean = None #self.yClean = None
self.xDisp = None self.xDisp = None
self.yDisp = None self.yDisp = None
self._dataRect = None
self.curve.clear() self.curve.clear()
self.scatter.clear() self.scatter.clear()
def appendData(self, *args, **kargs): def appendData(self, *args, **kargs):
pass pass
def curveClicked(self): def curveClicked(self):
self.sigClicked.emit(self) self.sigClicked.emit(self)
def scatterClicked(self, plt, points): def scatterClicked(self, plt, points):
self.sigClicked.emit(self) self.sigClicked.emit(self)
self.sigPointsClicked.emit(self, points) self.sigPointsClicked.emit(self, points)
def viewRangeChanged(self): def viewRangeChanged(self):
# view range has changed; re-plot if needed # 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.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def _fourierTransform(self, x, y): def _fourierTransform(self, x, y):
## Perform fourier transform. If x values are not sampled uniformly, ## Perform fourier transform. If x values are not sampled uniformly,
## then use np.interp to resample before taking fft. ## then use np.interp to resample before taking fft.
@ -727,7 +793,7 @@ class PlotDataItem(GraphicsObject):
x = np.fft.rfftfreq(n, d) x = np.fft.rfftfreq(n, d)
y = np.abs(f) y = np.abs(f)
return x, y return x, y
def dataType(obj): def dataType(obj):
if hasattr(obj, '__len__') and len(obj) == 0: if hasattr(obj, '__len__') and len(obj) == 0:
return 'empty' return 'empty'
@ -735,7 +801,7 @@ def dataType(obj):
return 'dictOfLists' return 'dictOfLists'
elif isSequence(obj): elif isSequence(obj):
first = obj[0] first = obj[0]
if (hasattr(obj, 'implements') and obj.implements('MetaArray')): if (hasattr(obj, 'implements') and obj.implements('MetaArray')):
return 'MetaArray' return 'MetaArray'
elif isinstance(obj, np.ndarray): elif isinstance(obj, np.ndarray):
@ -752,13 +818,13 @@ def dataType(obj):
return 'listOfDicts' return 'listOfDicts'
else: else:
return 'listOfValues' return 'listOfValues'
def isSequence(obj): def isSequence(obj):
return hasattr(obj, '__iter__') or isinstance(obj, np.ndarray) or (hasattr(obj, 'implements') and obj.implements('MetaArray')) return hasattr(obj, '__iter__') or isinstance(obj, np.ndarray) or (hasattr(obj, 'implements') and obj.implements('MetaArray'))
#class TableData: #class TableData:
#""" #"""
#Class for presenting multiple forms of tabular data through a consistent interface. #Class for presenting multiple forms of tabular data through a consistent interface.
@ -768,7 +834,7 @@ def isSequence(obj):
#- dict-of-lists #- dict-of-lists
#- dict (single record) #- dict (single record)
#Note: if all the values in this record are lists, it will be interpreted as multiple records #Note: if all the values in this record are lists, it will be interpreted as multiple records
#Data can be accessed and modified by column, by row, or by value #Data can be accessed and modified by column, by row, or by value
#data[columnName] #data[columnName]
#data[rowId] #data[rowId]
@ -776,7 +842,7 @@ def isSequence(obj):
#data[columnName] = [value, value, ...] #data[columnName] = [value, value, ...]
#data[rowId] = {columnName: value, ...} #data[rowId] = {columnName: value, ...}
#""" #"""
#def __init__(self, data): #def __init__(self, data):
#self.data = data #self.data = data
#if isinstance(data, np.ndarray): #if isinstance(data, np.ndarray):
@ -797,13 +863,13 @@ def isSequence(obj):
#self.mode = data.mode #self.mode = data.mode
#else: #else:
#raise TypeError(type(data)) #raise TypeError(type(data))
#for fn in ['__getitem__', '__setitem__']: #for fn in ['__getitem__', '__setitem__']:
#setattr(self, fn, getattr(self, '_TableData'+fn+self.mode)) #setattr(self, fn, getattr(self, '_TableData'+fn+self.mode))
#def originalData(self): #def originalData(self):
#return self.data #return self.data
#def toArray(self): #def toArray(self):
#if self.mode == 'array': #if self.mode == 'array':
#return self.data #return self.data
@ -818,13 +884,13 @@ def isSequence(obj):
#for i in xrange(1, len(self)): #for i in xrange(1, len(self)):
#arr[i] = tuple(self[i].values()) #arr[i] = tuple(self[i].values())
#return arr #return arr
#def __getitem__array(self, arg): #def __getitem__array(self, arg):
#if isinstance(arg, tuple): #if isinstance(arg, tuple):
#return self.data[arg[0]][arg[1]] #return self.data[arg[0]][arg[1]]
#else: #else:
#return self.data[arg] #return self.data[arg]
#def __getitem__list(self, arg): #def __getitem__list(self, arg):
#if isinstance(arg, basestring): #if isinstance(arg, basestring):
#return [d.get(arg, None) for d in self.data] #return [d.get(arg, None) for d in self.data]
@ -835,7 +901,7 @@ def isSequence(obj):
#return self.data[arg[0]][arg[1]] #return self.data[arg[0]][arg[1]]
#else: #else:
#raise TypeError(type(arg)) #raise TypeError(type(arg))
#def __getitem__dict(self, arg): #def __getitem__dict(self, arg):
#if isinstance(arg, basestring): #if isinstance(arg, basestring):
#return self.data[arg] #return self.data[arg]
@ -866,7 +932,7 @@ def isSequence(obj):
#self.data[arg[0]][arg[1]] = val #self.data[arg[0]][arg[1]] = val
#else: #else:
#raise TypeError(type(arg)) #raise TypeError(type(arg))
#def __setitem__dict(self, arg, val): #def __setitem__dict(self, arg, val):
#if isinstance(arg, basestring): #if isinstance(arg, basestring):
#if len(val) != len(self.data[arg]): #if len(val) != len(self.data[arg]):
@ -887,7 +953,7 @@ def isSequence(obj):
#return (args[1], args[0]) #return (args[1], args[0])
#else: #else:
#return args #return args
#def __iter__(self): #def __iter__(self):
#for i in xrange(len(self)): #for i in xrange(len(self)):
#yield self[i] #yield self[i]
@ -909,6 +975,6 @@ def isSequence(obj):
#return list(names) #return list(names)
#elif self.mode == 'dict': #elif self.mode == 'dict':
#return self.data.keys() #return self.data.keys()
#def keys(self): #def keys(self):
#return self.columnNames() #return self.columnNames()