From 8c13a3e7e37243e024f7b4d9fcc9a870cf8d94c4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 3 Jul 2013 11:20:49 -0400 Subject: [PATCH] copy from acq4 --- pyqtgraph/Point.py | 6 + pyqtgraph/SRTTransform.py | 5 +- pyqtgraph/SRTTransform3D.py | 19 +- pyqtgraph/debug.py | 9 + pyqtgraph/flowchart/library/Operators.py | 14 +- pyqtgraph/functions.py | 5 + pyqtgraph/graphicsItems/AxisItem.py | 33 +++- pyqtgraph/graphicsItems/GraphicsItem.py | 1 + pyqtgraph/graphicsItems/PlotCurveItem.py | 1 + pyqtgraph/graphicsItems/PlotDataItem.py | 179 ++++++++++++++---- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 90 +++++++-- .../PlotItem/plotConfigTemplate.ui | 153 ++++++++++----- .../PlotItem/plotConfigTemplate_pyqt.py | 81 +++++--- .../PlotItem/plotConfigTemplate_pyside.py | 81 +++++--- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 89 +++++++-- pyqtgraph/metaarray/MetaArray.py | 3 + pyqtgraph/multiprocess/remoteproxy.py | 31 +-- pyqtgraph/widgets/ScatterPlotWidget.py | 9 +- 19 files changed, 613 insertions(+), 204 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index ea35d119..682f19f7 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -80,6 +80,12 @@ class Point(QtCore.QPointF): def __div__(self, a): return self._math_('__div__', a) + def __truediv__(self, a): + return self._math_('__truediv__', a) + + def __rtruediv__(self, a): + return self._math_('__rtruediv__', a) + def __rpow__(self, a): return self._math_('__rpow__', a) diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index a861f940..efb24f60 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform): self._state['angle'] = angle self.update() - def __div__(self, t): + def __truediv__(self, t): """A / B == B^-1 * A""" dt = t.inverted()[0] * self return SRTTransform(dt) + def __div__(self, t): + return self.__truediv__(t) + def __mul__(self, t): return SRTTransform(QtGui.QTransform.__mul__(self, t)) diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 77583b5a..7d87dcb8 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D): m = self.matrix().reshape(4,4) ## translation is 4th column self._state['pos'] = m[:3,3] - ## scale is vector-length of first three columns scale = (m[:3,:3]**2).sum(axis=0)**0.5 ## see whether there is an inversion @@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D): print("Scale: %s" % str(scale)) print("Original matrix: %s" % str(m)) raise - eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) + eigIndex = np.argwhere(np.abs(evals-1) < 1e-6) if len(eigIndex) < 1: print("eigenvalues: %s" % str(evals)) print("eigenvectors: %s" % str(evecs)) print("index: %s, %s" % (str(eigIndex), str(evals-1))) raise Exception("Could not determine rotation axis.") - axis = evecs[eigIndex[0,0]].real + axis = evecs[:,eigIndex[0,0]].real axis /= ((axis**2).sum())**0.5 self._state['axis'] = axis ## trace(r) == 2 cos(angle) + 1, so: - self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi + cos = (r.trace()-1)*0.5 ## this only gets us abs(angle) + + ## The off-diagonal values can be used to correct the angle ambiguity, + ## but we need to figure out which element to use: + axisInd = np.argmax(np.abs(axis)) + rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd] + + ## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd]; + ## solve for sin(angle) + sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd]) + + ## finally, we get the complete angle from arctan(sin/cos) + self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi if self._state['angle'] == 0: self._state['axis'] = (0,0,1) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index ae2b21ac..a175be9c 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -28,6 +28,15 @@ def ftrace(func): return rv return w +def warnOnException(func): + """Decorator which catches/ignores exceptions and prints a stack trace.""" + def w(*args, **kwds): + try: + func(*args, **kwds) + except: + printExc('Ignored exception:') + return w + def getExc(indent=4, prefix='| '): tb = traceback.format_exc() lines = [] diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 412af573..579d2cd2 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -24,7 +24,15 @@ class BinOpNode(Node): }) def process(self, **args): - fn = getattr(args['A'], self.fn) + if isinstance(self.fn, tuple): + for name in self.fn: + try: + fn = getattr(args['A'], name) + break + except AttributeError: + pass + else: + fn = getattr(args['A'], self.fn) out = fn(args['B']) if out is NotImplemented: raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) @@ -60,5 +68,7 @@ class DivideNode(BinOpNode): """Returns A / B. Does not check input types.""" nodeName = 'Divide' def __init__(self, name): - BinOpNode.__init__(self, name, '__div__') + # try truediv first, followed by div + BinOpNode.__init__(self, name, ('__truediv__', '__div__')) + diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5f820a9a..4168836e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -264,6 +264,7 @@ def mkPen(*args, **kargs): color = kargs.get('color', None) width = kargs.get('width', 1) style = kargs.get('style', None) + dash = kargs.get('dash', None) cosmetic = kargs.get('cosmetic', True) hsv = kargs.get('hsv', None) @@ -291,6 +292,8 @@ def mkPen(*args, **kargs): pen.setCosmetic(cosmetic) if style is not None: pen.setStyle(style) + if dash is not None: + pen.setDashPattern(dash) return pen def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): @@ -1948,6 +1951,8 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): s2 = spacing**2 yvals = np.empty(len(data)) + if len(data) == 0: + return yvals yvals[0] = 0 for i in range(1,len(data)): x = data[i] # current x value to be placed diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e31030df..97f0ef1c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -42,12 +42,18 @@ class AxisItem(GraphicsWidget): self.label.rotate(-90) self.style = { - 'tickTextOffset': 3, ## spacing between text and axis + 'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis 'tickTextWidth': 30, ## space reserved for tick text 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed 'tickFont': None, 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. + (0, 0.8), ## never fill more than 80% of the axis + (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis + (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis + (6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis + ] } self.textWidth = 30 ## Keeps track of maximum width / height of tick text @@ -209,14 +215,14 @@ class AxisItem(GraphicsWidget): ## to accomodate. if self.orientation in ['left', 'right']: mx = max(self.textWidth, x) - if mx > self.textWidth: + if mx > self.textWidth or mx < self.textWidth-10: self.textWidth = mx if self.style['autoExpandTextSpace'] is True: self.setWidth() #return True ## size has changed else: mx = max(self.textHeight, x) - if mx > self.textHeight: + if mx > self.textHeight or mx < self.textHeight-10: self.textHeight = mx if self.style['autoExpandTextSpace'] is True: self.setHeight() @@ -236,7 +242,7 @@ class AxisItem(GraphicsWidget): h = self.textHeight else: h = self.style['tickTextHeight'] - h += max(0, self.tickLength) + self.style['tickTextOffset'] + h += max(0, self.tickLength) + self.style['tickTextOffset'][1] if self.label.isVisible(): h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) @@ -252,7 +258,7 @@ class AxisItem(GraphicsWidget): w = self.textWidth else: w = self.style['tickTextWidth'] - w += max(0, self.tickLength) + self.style['tickTextOffset'] + w += max(0, self.tickLength) + self.style['tickTextOffset'][0] if self.label.isVisible(): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) @@ -430,7 +436,7 @@ class AxisItem(GraphicsWidget): return [] ## decide optimal minor tick spacing in pixels (this is just aesthetics) - pixelSpacing = np.log(size+10) * 5 + pixelSpacing = size / np.log(size) optimalTickCount = max(2., size / pixelSpacing) ## optimal minor tick spacing @@ -720,7 +726,7 @@ class AxisItem(GraphicsWidget): - textOffset = self.style['tickTextOffset'] ## spacing between axis and text + textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text #if self.style['autoExpandTextSpace'] is True: #textWidth = self.textWidth #textHeight = self.textHeight @@ -728,7 +734,7 @@ class AxisItem(GraphicsWidget): #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text - + textSize2 = 0 textRects = [] textSpecs = [] ## list of draw for i in range(len(tickLevels)): @@ -770,9 +776,16 @@ class AxisItem(GraphicsWidget): textSize = np.sum([r.width() for r in textRects]) textSize2 = np.max([r.height() for r in textRects]) - ## If the strings are too crowded, stop drawing text now + ## If the strings are too crowded, stop drawing text now. + ## We use three different crowding limits based on the number + ## of texts drawn so far. textFillRatio = float(textSize) / lengthInPixels - if textFillRatio > 0.7: + finished = False + for nTexts, limit in self.style['textFillLimits']: + if len(textSpecs) >= nTexts and textFillRatio >= limit: + finished = True + break + if finished: break #spacing, values = tickLevels[best] diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 40ff6bc5..a129436e 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -533,6 +533,7 @@ class GraphicsItem(object): def viewTransformChanged(self): """ Called whenever the transformation matrix of the view has changed. + (eg, the view range has changed or the view was resized) """ pass diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d707a347..4c66bf72 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -375,6 +375,7 @@ class PlotCurveItem(GraphicsObject): return QtGui.QPainterPath() return self.path + @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) if self.xData is None: diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 76b74359..1ae528ba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -84,24 +84,28 @@ 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 (int) Sub-sample data by selecting every nth sample before plotting - onlyVisible (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. - autoResample (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. - sampleRate (float) The sample rate of the data along the X axis (for data with - a fixed sample rate). Providing this value improves performance of - the *onlyVisible* and *autoResample* options. - 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. + identical *deprecated* + ================ ===================================================================== **Meta-info keyword arguments:** @@ -131,7 +135,6 @@ class PlotDataItem(GraphicsObject): self.opts = { 'fftMode': False, 'logMode': [False, False], - 'downsample': False, 'alphaHint': 1.0, 'alphaMode': False, @@ -149,6 +152,11 @@ class PlotDataItem(GraphicsObject): 'antialias': pg.getConfigOption('antialias'), 'pointMode': None, + 'downsample': 1, + 'autoDownsample': False, + 'downsampleMethod': 'peak', + 'clipToView': False, + 'data': None, } self.setData(*args, **kargs) @@ -175,6 +183,7 @@ class PlotDataItem(GraphicsObject): return self.opts['fftMode'] = mode self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -183,6 +192,7 @@ class PlotDataItem(GraphicsObject): return self.opts['logMode'] = [xMode, yMode] self.xDisp = self.yDisp = None + self.xClean = self.yClean = None self.updateItems() self.informViewBoundsChanged() @@ -269,13 +279,51 @@ class PlotDataItem(GraphicsObject): #self.scatter.setSymbolSize(symbolSize) self.updateItems() - def setDownsampling(self, ds): - if self.opts['downsample'] == ds: + def setDownsampling(self, ds=None, auto=None, method=None): + """ + Set the downsampling mode of this item. Downsampling reduces the number + of samples drawn to increase performance. + + =========== ================================================================= + Arguments + ds (int) Reduce visible plot samples by this factor. To disable, + set ds=1. + auto (bool) If True, automatically pick *ds* based on visible range + mode '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. + =========== ================================================================= + """ + changed = False + if ds is not None: + if self.opts['downsample'] != ds: + changed = True + self.opts['downsample'] = ds + + if auto is not None and self.opts['autoDownsample'] != auto: + self.opts['autoDownsample'] = auto + changed = True + + if method is not None: + if self.opts['downsampleMethod'] != method: + changed = True + self.opts['downsampleMethod'] = method + + if changed: + self.xDisp = self.yDisp = None + self.updateItems() + + def setClipToView(self, clip): + if self.opts['clipToView'] == clip: return - self.opts['downsample'] = ds + self.opts['clipToView'] = clip self.xDisp = self.yDisp = None self.updateItems() + def setData(self, *args, **kargs): """ Clear any data displayed by this item and display new data. @@ -315,7 +363,7 @@ class PlotDataItem(GraphicsObject): raise Exception('Invalid data type %s' % type(data)) elif len(args) == 2: - seq = ('listOfValues', 'MetaArray') + seq = ('listOfValues', 'MetaArray', 'empty') if dataType(args[0]) not in seq or dataType(args[1]) not in seq: raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) if not isinstance(args[0], np.ndarray): @@ -376,6 +424,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.xClean = self.yClean = None self.xDisp = None self.yDisp = None prof.mark('set data') @@ -423,23 +472,28 @@ class PlotDataItem(GraphicsObject): def getData(self): if self.xData is None: return (None, None) - if self.xDisp is None: + + if self.xClean is None: nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) if any(nanMask): self.dataMask = ~nanMask - x = self.xData[self.dataMask] - y = self.yData[self.dataMask] + self.xClean = self.xData[self.dataMask] + self.yClean = self.yData[self.dataMask] else: self.dataMask = None - x = self.xData - y = self.yData - + self.xClean = self.xData + self.yClean = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] + if self.xDisp is None: + x = self.xClean + y = self.yClean + + + #ds = self.opts['downsample'] + #if isinstance(ds, int) and ds > 1: + #x = x[::ds] + ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + #y = y[::ds] if self.opts['fftMode']: f = np.fft.fft(y) / len(y) y = abs(f[1:len(f)/2]) @@ -457,6 +511,53 @@ class PlotDataItem(GraphicsObject): y = y[self.dataMask] else: self.dataMask = None + + ds = self.opts['downsample'] + if not isinstance(ds, int): + ds = 1 + + if self.opts['autoDownsample']: + # this option presumes that x-values have uniform spacing + range = self.viewRect() + if range is not None: + dx = float(x[-1]-x[0]) / (len(x)-1) + x0 = (range.left()-x[0]) / dx + x1 = (range.right()-x[0]) / dx + width = self.getViewBox().width() + ds = int(max(1, int(0.2 * (x1-x0) / width))) + ## downsampling is expensive; delay until after clipping. + + if self.opts['clipToView']: + # this option presumes that x-values have uniform spacing + range = self.viewRect() + if range is not None: + 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] + + if ds > 1: + if self.opts['downsampleMethod'] == 'subsample': + x = x[::ds] + y = y[::ds] + elif self.opts['downsampleMethod'] == 'mean': + n = len(x) / ds + x = x[:n*ds:ds] + y = y[:n*ds].reshape(n,ds).mean(axis=1) + elif self.opts['downsampleMethod'] == 'peak': + n = len(x) / ds + x1 = np.empty((n,2)) + x1[:] = x[:n*ds:ds,np.newaxis] + x = x1.reshape(n*2) + y1 = np.empty((n,2)) + y2 = y[:n*ds].reshape((n, ds)) + y1[:,0] = y2.max(axis=1) + y1[:,1] = y2.min(axis=1) + y = y1.reshape(n*2) + + self.xDisp = x self.yDisp = y #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() @@ -542,6 +643,8 @@ class PlotDataItem(GraphicsObject): #self.scatters = [] self.xData = None self.yData = None + self.xClean = None + self.yClean = None self.xDisp = None self.yDisp = None self.curve.setData([]) @@ -557,6 +660,14 @@ class PlotDataItem(GraphicsObject): self.sigClicked.emit(self) self.sigPointsClicked.emit(self, points) + def viewRangeChanged(self): + # view range has changed; re-plot if needed + if self.opts['clipToView'] or self.opts['autoDownsample']: + self.xDisp = self.yDisp = None + self.updateItems() + + + def dataType(obj): if hasattr(obj, '__len__') and len(obj) == 0: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c226b9c4..ff3dc7b3 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -256,6 +256,11 @@ class PlotItem(GraphicsWidget): c.logYCheck.toggled.connect(self.updateLogMode) c.downsampleSpin.valueChanged.connect(self.updateDownsampling) + c.downsampleCheck.toggled.connect(self.updateDownsampling) + c.autoDownsampleCheck.toggled.connect(self.updateDownsampling) + c.subsampleRadio.toggled.connect(self.updateDownsampling) + c.meanRadio.toggled.connect(self.updateDownsampling) + c.clipToViewCheck.toggled.connect(self.updateDownsampling) self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) self.ctrl.averageGroup.toggled.connect(self.avgToggled) @@ -526,7 +531,8 @@ class PlotItem(GraphicsWidget): (alpha, auto) = self.alphaState() item.setAlpha(alpha, auto) item.setFftMode(self.ctrl.fftCheck.isChecked()) - item.setDownsampling(self.downsampleMode()) + item.setDownsampling(*self.downsampleMode()) + item.setClipToView(self.clipToViewMode()) item.setPointMode(self.pointMode()) ## Hide older plots if needed @@ -568,8 +574,8 @@ class PlotItem(GraphicsWidget): :func:`InfiniteLine.__init__() `. Returns the item created. """ - angle = 0 if x is None else 90 - pos = x if x is not None else y + pos = kwds.get('pos', x if x is not None else y) + angle = kwds.get('angle', 0 if x is None else 90) line = InfiniteLine(pos, angle, **kwds) self.addItem(line) if z is not None: @@ -941,23 +947,81 @@ class PlotItem(GraphicsWidget): self.enableAutoRange() self.recomputeAverages() + def setDownsampling(self, ds=None, auto=None, mode=None): + """Change the default downsampling mode for all PlotDataItems managed by this plot. + =========== ================================================================= + Arguments + ds (int) Reduce visible plot samples by this factor, or + (bool) To enable/disable downsampling without changing the value. + auto (bool) If True, automatically pick *ds* based on visible range + mode '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. + =========== ================================================================= + """ + if ds is not None: + if ds is False: + self.ctrl.downsampleCheck.setChecked(False) + elif ds is True: + self.ctrl.downsampleCheck.setChecked(True) + else: + self.ctrl.downsampleCheck.setChecked(True) + self.ctrl.downsampleSpin.setValue(ds) + + if auto is not None: + if auto and ds is not False: + self.ctrl.downsampleCheck.setChecked(True) + self.ctrl.autoDownsampleCheck.setChecked(auto) + + if mode is not None: + if mode == 'subsample': + self.ctrl.subsampleRadio.setChecked(True) + elif mode == 'mean': + self.ctrl.meanRadio.setChecked(True) + elif mode == 'peak': + self.ctrl.peakRadio.setChecked(True) + else: + raise ValueError("mode argument must be 'subsample', 'mean', or 'peak'.") + def updateDownsampling(self): - ds = self.downsampleMode() + ds, auto, method = self.downsampleMode() + clip = self.ctrl.clipToViewCheck.isChecked() for c in self.curves: - c.setDownsampling(ds) + c.setDownsampling(ds, auto, method) + c.setClipToView(clip) self.recomputeAverages() - def downsampleMode(self): - if self.ctrl.decimateGroup.isChecked(): - if self.ctrl.manualDecimateRadio.isChecked(): - ds = self.ctrl.downsampleSpin.value() - else: - ds = True + if self.ctrl.downsampleCheck.isChecked(): + ds = self.ctrl.downsampleSpin.value() else: - ds = False - return ds + ds = 1 + + auto = self.ctrl.downsampleCheck.isChecked() and self.ctrl.autoDownsampleCheck.isChecked() + + if self.ctrl.subsampleRadio.isChecked(): + method = 'subsample' + elif self.ctrl.meanRadio.isChecked(): + method = 'mean' + elif self.ctrl.peakRadio.isChecked(): + method = 'peak' + + return ds, auto, method + + def setClipToView(self, clip): + """Set the default clip-to-view mode for all PlotDataItems managed by this plot. + If *clip* is True, then PlotDataItems will attempt to draw only points within the visible + range of the ViewBox.""" + self.ctrl.clipToViewCheck.setChecked(clip) + + def clipToViewMode(self): + return self.ctrl.clipToViewCheck.isChecked() + + def updateDecimation(self): if self.ctrl.maxTracesCheck.isChecked(): diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui index 516ec721..dffc62d0 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 258 - 605 + 481 + 840 @@ -16,8 +16,8 @@ - 10 - 200 + 0 + 640 242 182 @@ -46,21 +46,15 @@ - + - 0 - 70 - 242 - 160 + 10 + 140 + 191 + 171 - - Downsample - - - true - 0 @@ -68,40 +62,17 @@ 0 - - + + + + Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced. + - Manual - - - true + Clip to View - - - - 1 - - - 100000 - - - 1 - - - - - - - Auto - - - false - - - - + If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. @@ -111,14 +82,34 @@ - + + + + Downsample + + + + + + + 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. + + + Peak + + + true + + + + If multiple curves are displayed in this plot, check "Max Traces" and set this value to limit the number of traces that are displayed. - + If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden). @@ -128,6 +119,74 @@ + + + + Downsample by taking the mean of N samples. + + + Mean + + + + + + + Downsample by taking the first of N samples. This method is fastest and least accurate. + + + Subsample + + + + + + + Automatically downsample data based on the visible range. This assumes X values are uniformly spaced. + + + Auto + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 30 + 20 + + + + + + + + Downsample data before plotting. (plot every Nth sample) + + + x + + + 1 + + + 100000 + + + 1 + + + diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py index d34cd297..5335ee76 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Sun Sep 9 14:41:32 2012 -# by: PyQt4 UI code generator 4.9.1 +# Created: Mon Jul 1 23:21:08 2013 +# by: PyQt4 UI code generator 4.9.3 # # WARNING! All changes made in this file will be lost! @@ -17,9 +17,9 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(258, 605) + Form.resize(481, 840) self.averageGroup = QtGui.QGroupBox(Form) - self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182)) self.averageGroup.setCheckable(True) self.averageGroup.setChecked(False) self.averageGroup.setObjectName(_fromUtf8("averageGroup")) @@ -30,37 +30,50 @@ class Ui_Form(object): self.avgParamList = QtGui.QListWidget(self.averageGroup) self.avgParamList.setObjectName(_fromUtf8("avgParamList")) self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.decimateGroup = QtGui.QGroupBox(Form) - self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) - self.decimateGroup.setCheckable(True) + self.decimateGroup = QtGui.QFrame(Form) + self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171)) self.decimateGroup.setObjectName(_fromUtf8("decimateGroup")) self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) self.gridLayout_4.setMargin(0) self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio")) - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) + self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup) + self.clipToViewCheck.setObjectName(_fromUtf8("clipToViewCheck")) + self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3) + self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) + self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2) + self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck")) + self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3) + self.peakRadio = QtGui.QRadioButton(self.decimateGroup) + self.peakRadio.setChecked(True) + self.peakRadio.setObjectName(_fromUtf8("peakRadio")) + self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2) + self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) + self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) + self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3) + self.meanRadio = QtGui.QRadioButton(self.decimateGroup) + self.meanRadio.setObjectName(_fromUtf8("meanRadio")) + self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2) + self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup) + self.subsampleRadio.setObjectName(_fromUtf8("subsampleRadio")) + self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2) + self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.autoDownsampleCheck.setChecked(True) + self.autoDownsampleCheck.setObjectName(_fromUtf8("autoDownsampleCheck")) + self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1) + spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum) + self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1) self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) self.downsampleSpin.setMinimum(1) self.downsampleSpin.setMaximum(100000) self.downsampleSpin.setProperty("value", 1) self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio")) - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) - self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) - self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1) self.transformGroup = QtGui.QFrame(Form) self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) self.transformGroup.setObjectName(_fromUtf8("transformGroup")) @@ -129,14 +142,24 @@ class Ui_Form(object): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "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.", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8)) self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py index 85b563a7..b8e0b19e 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' # -# Created: Sun Sep 9 14:41:32 2012 -# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# Created: Mon Jul 1 23:21:08 2013 +# by: pyside-uic 0.2.13 running on PySide 1.1.2 # # WARNING! All changes made in this file will be lost! @@ -12,9 +12,9 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(258, 605) + Form.resize(481, 840) self.averageGroup = QtGui.QGroupBox(Form) - self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182)) self.averageGroup.setCheckable(True) self.averageGroup.setChecked(False) self.averageGroup.setObjectName("averageGroup") @@ -25,37 +25,50 @@ class Ui_Form(object): self.avgParamList = QtGui.QListWidget(self.averageGroup) self.avgParamList.setObjectName("avgParamList") self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.decimateGroup = QtGui.QGroupBox(Form) - self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) - self.decimateGroup.setCheckable(True) + self.decimateGroup = QtGui.QFrame(Form) + self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171)) self.decimateGroup.setObjectName("decimateGroup") self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName("gridLayout_4") - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName("manualDecimateRadio") - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) + self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup) + self.clipToViewCheck.setObjectName("clipToViewCheck") + self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3) + self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName("maxTracesCheck") + self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2) + self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3) + self.peakRadio = QtGui.QRadioButton(self.decimateGroup) + self.peakRadio.setChecked(True) + self.peakRadio.setObjectName("peakRadio") + self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2) + self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName("maxTracesSpin") + self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName("forgetTracesCheck") + self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3) + self.meanRadio = QtGui.QRadioButton(self.decimateGroup) + self.meanRadio.setObjectName("meanRadio") + self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2) + self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup) + self.subsampleRadio.setObjectName("subsampleRadio") + self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2) + self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup) + self.autoDownsampleCheck.setChecked(True) + self.autoDownsampleCheck.setObjectName("autoDownsampleCheck") + self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1) + spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum) + self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1) self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) self.downsampleSpin.setMinimum(1) self.downsampleSpin.setMaximum(100000) self.downsampleSpin.setProperty("value", 1) self.downsampleSpin.setObjectName("downsampleSpin") - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName("autoDecimateRadio") - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) - self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName("maxTracesCheck") - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName("maxTracesSpin") - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) - self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.forgetTracesCheck.setObjectName("forgetTracesCheck") - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1) self.transformGroup = QtGui.QFrame(Form) self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) self.transformGroup.setObjectName("transformGroup") @@ -124,14 +137,24 @@ class Ui_Form(object): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "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.", None, QtGui.QApplication.UnicodeUTF8)) + self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8)) self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8)) + self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8)) + self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8)) + self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8)) self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 29bfeaac..a6a46bf5 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -15,7 +15,7 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']]) +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { @@ -32,6 +32,9 @@ for k, c in coords.items(): for x,y in c[1:]: Symbols[k].lineTo(x, y) Symbols[k].closeSubpath() +tr = QtGui.QTransform() +tr.rotate(45) +Symbols['x'] = tr.map(Symbols['+']) def drawSymbol(painter, symbol, size, pen, brush): @@ -689,7 +692,8 @@ class ScatterPlotItem(GraphicsObject): def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - + + @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): #p.setPen(fn.mkPen('r')) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 338cdde4..29bd6d23 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -141,6 +141,12 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.hide() self.addItem(self.rbScaleBox, ignoreBounds=True) + ## show target rect for debugging + self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1) + self.target.setPen(fn.mkPen('r')) + self.target.setParentItem(self) + self.target.hide() + self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" @@ -275,6 +281,9 @@ class ViewBox(GraphicsWidget): """ if item.zValue() < self.zValue(): item.setZValue(self.zValue()+1) + scene = self.scene() + if scene is not None and scene is not item.scene(): + scene.addItem(item) ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616 item.setParentItem(self.childGroup) if not ignoreBounds: self.addedItems.append(item) @@ -294,7 +303,7 @@ class ViewBox(GraphicsWidget): for i in self.addedItems[:]: self.removeItem(i) for ch in self.childGroup.childItems(): - ch.setParent(None) + ch.setParentItem(None) def resizeEvent(self, ev): #self.setRange(self.range, padding=0) @@ -389,10 +398,28 @@ class ViewBox(GraphicsWidget): p = (mx-mn) * xpad mn -= p mx += p - if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] changed[ax] = True + + aspect = self.state['aspectLocked'] # size ratio / view ratio + if aspect is not False and len(changes) == 1: + ## need to adjust orthogonal target range to match + size = [self.width(), self.height()] + tr1 = self.state['targetRange'][ax] + tr2 = self.state['targetRange'][1-ax] + if size[1] == 0 or aspect == 0: + ratio = 1.0 + else: + ratio = (size[0] / float(size[1])) / aspect + if ax == 0: + ratio = 1.0 / ratio + w = (tr1[1]-tr1[0]) * ratio + d = 0.5 * (w - (tr2[1]-tr2[0])) + self.state['targetRange'][1-ax] = [tr2[0]-d, tr2[1]+d] + + + if any(changed) and disableAutoRange: if all(changed): @@ -406,6 +433,8 @@ class ViewBox(GraphicsWidget): self.sigStateChanged.emit(self) + self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) + if update: self.updateMatrix(changed) @@ -494,7 +523,7 @@ class ViewBox(GraphicsWidget): scale = Point(scale) if self.state['aspectLocked'] is not False: - scale[0] = self.state['aspectLocked'] * scale[1] + scale[0] = scale[1] vr = self.targetRect() if center is None: @@ -706,6 +735,7 @@ class ViewBox(GraphicsWidget): else: if self.autoRangeEnabled()[axis] is False: slot() + self.sigStateChanged.emit(self) @@ -807,13 +837,17 @@ class ViewBox(GraphicsWidget): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. By default, the ratio is set to 1; x and y both have the same scaling. - This ratio can be overridden (width/height), or use None to lock in the current ratio. + This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio. """ if not lock: self.state['aspectLocked'] = False else: + rect = self.rect() vr = self.viewRect() - currentRatio = vr.width() / vr.height() + if rect.height() == 0 or vr.width() == 0 or vr.height() == 0: + currentRatio = 1.0 + else: + currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height()) if ratio is None: ratio = currentRatio self.state['aspectLocked'] = ratio @@ -1092,10 +1126,10 @@ class ViewBox(GraphicsWidget): xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding() - if xr is None or xr == (None, None) or np.isnan(xr).any() or np.isinf(xr).any(): + if xr is None or (xr[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any(): useX = False xr = (0,0) - if yr is None or yr == (None, None) or np.isnan(yr).any() or np.isinf(yr).any(): + if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any(): useY = False yr = (0,0) @@ -1194,32 +1228,41 @@ class ViewBox(GraphicsWidget): if changed is None: changed = [False, False] changed = list(changed) - #print "udpateMatrix:" - #print " range:", self.range tr = self.targetRect() - bounds = self.rect() #boundingRect() - #print bounds + bounds = self.rect() ## set viewRect, given targetRect and possibly aspect ratio constraint - if self.state['aspectLocked'] is False or bounds.height() == 0: + aspect = self.state['aspectLocked'] + if aspect is False or bounds.height() == 0: self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] else: - viewRatio = bounds.width() / bounds.height() - targetRatio = self.state['aspectLocked'] * tr.width() / tr.height() + ## aspect is (widget w/h) / (view range w/h) + + ## This is the view range aspect ratio we have requested + targetRatio = tr.width() / tr.height() + ## This is the view range aspect ratio we need to obey aspect constraint + viewRatio = (bounds.width() / bounds.height()) / aspect + if targetRatio > viewRatio: - ## target is wider than view - dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height()) + ## view range needs to be taller than target + dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: changed[1] = True - self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]] + self.state['viewRange'] = [ + self.state['targetRange'][0][:], + [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + ] else: - dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width()) + ## view range needs to be wider than target + dx = 0.5 * (tr.height() * viewRatio - tr.width()) if dx != 0: changed[0] = True - self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]] + self.state['viewRange'] = [ + [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], + self.state['targetRange'][1][:] + ] vr = self.viewRect() - #print " bounds:", bounds if vr.height() == 0 or vr.width() == 0: return scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) @@ -1253,6 +1296,12 @@ class ViewBox(GraphicsWidget): p.setPen(self.border) #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) p.drawPath(bounds) + + #p.setPen(fn.mkPen('r')) + #path = QtGui.QPainterPath() + #path.addRect(self.targetRect()) + #tr = self.mapFromView(path) + #p.drawPath(tr) def updateBackground(self): bg = self.state['background'] diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 0797c75e..f55c60dc 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -328,6 +328,9 @@ class MetaArray(object): def __div__(self, b): return self._binop('__div__', b) + def __truediv__(self, b): + return self._binop('__truediv__', b) + def _binop(self, op, b): if isinstance(b, MetaArray): b = b.asarray() diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 6cd65f6e..d0d75c1e 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -887,6 +887,12 @@ class ObjectProxy(object): def __div__(self, *args): return self._getSpecialAttr('__div__')(*args) + def __truediv__(self, *args): + return self._getSpecialAttr('__truediv__')(*args) + + def __floordiv__(self, *args): + return self._getSpecialAttr('__floordiv__')(*args) + def __mul__(self, *args): return self._getSpecialAttr('__mul__')(*args) @@ -902,6 +908,12 @@ class ObjectProxy(object): def __idiv__(self, *args): return self._getSpecialAttr('__idiv__')(*args, _callSync='off') + def __itruediv__(self, *args): + return self._getSpecialAttr('__itruediv__')(*args, _callSync='off') + + def __ifloordiv__(self, *args): + return self._getSpecialAttr('__ifloordiv__')(*args, _callSync='off') + def __imul__(self, *args): return self._getSpecialAttr('__imul__')(*args, _callSync='off') @@ -914,17 +926,11 @@ class ObjectProxy(object): def __lshift__(self, *args): return self._getSpecialAttr('__lshift__')(*args) - def __floordiv__(self, *args): - return self._getSpecialAttr('__pow__')(*args) - def __irshift__(self, *args): - return self._getSpecialAttr('__rshift__')(*args, _callSync='off') + return self._getSpecialAttr('__irshift__')(*args, _callSync='off') def __ilshift__(self, *args): - return self._getSpecialAttr('__lshift__')(*args, _callSync='off') - - def __ifloordiv__(self, *args): - return self._getSpecialAttr('__pow__')(*args, _callSync='off') + return self._getSpecialAttr('__ilshift__')(*args, _callSync='off') def __eq__(self, *args): return self._getSpecialAttr('__eq__')(*args) @@ -974,6 +980,12 @@ class ObjectProxy(object): def __rdiv__(self, *args): return self._getSpecialAttr('__rdiv__')(*args) + def __rfloordiv__(self, *args): + return self._getSpecialAttr('__rfloordiv__')(*args) + + def __rtruediv__(self, *args): + return self._getSpecialAttr('__rtruediv__')(*args) + def __rmul__(self, *args): return self._getSpecialAttr('__rmul__')(*args) @@ -986,9 +998,6 @@ class ObjectProxy(object): def __rlshift__(self, *args): return self._getSpecialAttr('__rlshift__')(*args) - def __rfloordiv__(self, *args): - return self._getSpecialAttr('__rpow__')(*args) - def __rand__(self, *args): return self._getSpecialAttr('__rand__')(*args) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index fe785e04..e9e24dd7 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -190,10 +190,15 @@ class ScatterPlotWidget(QtGui.QSplitter): for ax in [0,1]: if not enum[ax]: continue - for i in range(int(xy[ax].max())+1): + imax = int(xy[ax].max()) if len(xy[ax]) > 0 else 0 + for i in range(imax+1): keymask = xy[ax] == i scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True) - scatter *= 0.2 / np.abs(scatter).max() + if len(scatter) == 0: + continue + smax = np.abs(scatter).max() + if smax != 0: + scatter *= 0.2 / smax xy[ax][keymask] += scatter if self.scatterPlot is not None: