From f470a830d0346c8b0dec1574d81d713e862e8aec Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Sat, 14 Mar 2015 17:30:56 -0600 Subject: [PATCH 001/235] Optionally provide custom PlotItem to PlotWidget. --- pyqtgraph/widgets/PlotWidget.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index e27bce60..c23331a3 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -43,7 +43,7 @@ class PlotWidget(GraphicsView): For all other methods, use :func:`getPlotItem `. """ - def __init__(self, parent=None, background='default', **kargs): + def __init__(self, parent=None, background='default', plotItem=None, **kargs): """When initializing PlotWidget, *parent* and *background* are passed to :func:`GraphicsWidget.__init__() ` and all others are passed @@ -51,7 +51,10 @@ class PlotWidget(GraphicsView): GraphicsView.__init__(self, parent, background=background) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False) - self.plotItem = PlotItem(**kargs) + if plotItem is None: + self.plotItem = PlotItem(**kargs) + else: + self.plotItem = plotItem self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. From 0908d98d651324dc300bd8f5fd112b3e85e597f1 Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Sat, 14 Mar 2015 22:06:05 -0600 Subject: [PATCH 002/235] Speed up ViewBox panning. Noticeable when a large number of items are present. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 900c2038..c8db827b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1266,8 +1266,10 @@ class ViewBox(GraphicsWidget): ## update shape of scale box self.updateScaleBox(ev.buttonDownPos(), ev.pos()) else: - tr = dif*mask - tr = self.mapToView(tr) - self.mapToView(Point(0,0)) + tr = self.childGroup.transform() + tr = fn.invertQTransform(tr) + tr = tr.map(dif*mask) - tr.map(Point(0,0)) + x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None From 96e06f212a8b553035fa91cbbc017237c700cf6c Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Mon, 16 Mar 2015 14:03:58 -0600 Subject: [PATCH 003/235] Provide widgetGroupInterface to GradientWidget --- pyqtgraph/widgets/GradientWidget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py index ce0cbeb9..77881b30 100644 --- a/pyqtgraph/widgets/GradientWidget.py +++ b/pyqtgraph/widgets/GradientWidget.py @@ -71,4 +71,7 @@ class GradientWidget(GraphicsView): ### wrap methods from GradientEditorItem return getattr(self.item, attr) + def widgetGroupInterface(self): + return (self.sigGradientChanged, self.saveState, self.restoreState) + From bc3acdd5fd5a7da448142c32487fe4d16ca2ab27 Mon Sep 17 00:00:00 2001 From: Soloviev Denis Date: Wed, 4 May 2016 21:53:55 +0500 Subject: [PATCH 004/235] fix legendItem drag --- pyqtgraph/graphicsItems/LegendItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..31bcafb6 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -128,6 +128,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def mouseDragEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: + ev.accept() dpos = ev.pos() - ev.lastPos() self.autoAnchor(self.pos() + dpos) From 0a25fb087488cae38df42d0a1cdb19af3959642b Mon Sep 17 00:00:00 2001 From: Felix Schill Date: Sat, 21 May 2016 17:50:05 +0200 Subject: [PATCH 005/235] clearing _needUpdate flag in GLImageItem to prevent redundant re-upload of textures --- pyqtgraph/opengl/items/GLImageItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 59ddaf6f..56cfaf99 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -73,6 +73,7 @@ class GLImageItem(GLGraphicsItem): def paint(self): if self._needUpdate: self._updateTexture() + self._needUpdate = False glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) From 0e33ddc28b3148589f8a51d714a6ba342c5b24ce Mon Sep 17 00:00:00 2001 From: Nick Irvine Date: Wed, 20 Jul 2016 16:47:07 -0700 Subject: [PATCH 006/235] Allow MetaArray.__array__ to accept an optional dtype arg Fixes #359 --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..70300c7f 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -358,7 +358,7 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self): + def __array__(self, dtype=None): ## supports np.array(metaarray_instance) return self.asarray() From 0bc711b31f2a2cf0f06716885bd61f4ea223c3e6 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 10 Nov 2016 11:22:52 +0100 Subject: [PATCH 007/235] Revert "ignore wheel events in GraphicsView if mouse disabled" This reverts commit f49c179275e86786af70b38b8c5085e38d4e6cce. On Qt 5.7 ignoring the initial `wheelEvent` when `phase() == Qt.ScrollBegin` suppresses all intermediate events (`Qt.ScrollUpdate`) from being delivered to the view. This makes ViewBox zooming unresponsive. --- pyqtgraph/widgets/GraphicsView.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f8cbb5..45cc0254 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -325,7 +325,6 @@ class GraphicsView(QtGui.QGraphicsView): def wheelEvent(self, ev): QtGui.QGraphicsView.wheelEvent(self, ev) if not self.mouseEnabled: - ev.ignore() return sc = 1.001 ** ev.delta() #self.scale *= sc From 2d754672a0c696c0d97da9e7f2148f8ab99fe86e Mon Sep 17 00:00:00 2001 From: Karl Bedrich Date: Thu, 10 Nov 2016 17:01:15 +0000 Subject: [PATCH 008/235] NEW show/hide gradient ticks NEW link gradientEditorItem to other gradients --- pyqtgraph/graphicsItems/GradientEditorItem.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 6ce06b61..55c689d4 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -450,7 +450,20 @@ class GradientEditorItem(TickSliderItem): self.addTick(1, QtGui.QColor(255,0,0), True) self.setColorMode('rgb') self.updateGradient() - + self.linkedGradients = {} + + def showTicks(self, show=True): + for tick in self.ticks.keys(): + if show: + tick.show() + orig = getattr(self, '_allowAdd_backup', None) + if orig: + self.allowAdd = orig + else: + self._allowAdd_backup = self.allowAdd + self.allowAdd = False #block tick creation + tick.hide() + def setOrientation(self, orientation): ## public """ @@ -753,7 +766,9 @@ class GradientEditorItem(TickSliderItem): for t in self.ticks: c = t.color ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) - state = {'mode': self.colorMode, 'ticks': ticks} + state = {'mode': self.colorMode, + 'ticks': ticks, + 'ticksVisible': next(iter(self.ticks)).isVisible()} return state def restoreState(self, state): @@ -778,6 +793,8 @@ class GradientEditorItem(TickSliderItem): for t in state['ticks']: c = QtGui.QColor(*t[1]) self.addTick(t[0], c, finish=False) + self.showTicks( state.get('ticksVisible', + next(iter(self.ticks)).isVisible()) ) self.updateGradient() self.sigGradientChangeFinished.emit(self) @@ -793,6 +810,18 @@ class GradientEditorItem(TickSliderItem): self.updateGradient() self.sigGradientChangeFinished.emit(self) + def linkGradient(self, slaveGradient, connect=True): + if connect: + fn = lambda g, slave=slaveGradient:slave.restoreState( + g.saveState()) + self.linkedGradients[id(slaveGradient)] = fn + self.sigGradientChanged.connect(fn) + self.sigGradientChanged.emit(self) + else: + fn = self.linkedGradients.get(id(slaveGradient), None) + if fn: + self.sigGradientChanged.disconnect(fn) + class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsObject instead results in ## activating this bug: https://bugreports.qt-project.org/browse/PYSIDE-86 From 8a40c228486c31636db403912c55a600d07eb213 Mon Sep 17 00:00:00 2001 From: kiwi0fruit Date: Thu, 22 Jun 2017 16:00:54 +0700 Subject: [PATCH 009/235] Bug in RawImageWidget.py For example: it prevents integration of this widget to Enaml. --- pyqtgraph/widgets/RawImageWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 657701f9..ae6448c6 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -21,7 +21,7 @@ class RawImageWidget(QtGui.QWidget): """ Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. """ - QtGui.QWidget.__init__(self, parent=None) + QtGui.QWidget.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) self.scaled = scaled self.opts = None @@ -69,7 +69,7 @@ if HAVE_OPENGL: Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking. """ def __init__(self, parent=None, scaled=False): - QtOpenGL.QGLWidget.__init__(self, parent=None) + QtOpenGL.QGLWidget.__init__(self, parent) self.scaled = scaled self.image = None self.uploaded = False From 54ddb79e89d3ed27639f866bd534ecc9d671b563 Mon Sep 17 00:00:00 2001 From: kiwi0fruit Date: Tue, 27 Jun 2017 20:53:08 +0700 Subject: [PATCH 010/235] Bug-fix and small changes in RawImageWidget.py 1. Bug was in the `def paintGL(self)` method (at least with PySide1): image was mirrored upside down. 2. Added support for `setConfigOptions(imageAxisOrder='row-major')` 3. Small cosmetic changes --- pyqtgraph/widgets/RawImageWidget.py | 87 +++++++++++++++++------------ 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index ae6448c6..a51bfb1d 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -1,32 +1,43 @@ +# -*- coding: utf-8 -*- +""" +RawImageWidget.py +Copyright 2010-2016 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + from ..Qt import QtCore, QtGui + try: from ..Qt import QtOpenGL from OpenGL.GL import * + HAVE_OPENGL = True -except Exception: +except (ImportError, AttributeError): # Would prefer `except ImportError` here, but some versions of pyopengl generate # AttributeError upon import HAVE_OPENGL = False -from .. import functions as fn -import numpy as np +from .. import getConfigOption, functions as fn + class RawImageWidget(QtGui.QWidget): """ - Widget optimized for very fast video display. + Widget optimized for very fast video display. Generally using an ImageItem inside GraphicsView is fast enough. On some systems this may provide faster video. See the VideoSpeedTest example for benchmarking. """ + def __init__(self, parent=None, scaled=False): """ - Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. + Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. + This also greatly reduces the speed at which it will draw frames. """ QtGui.QWidget.__init__(self, parent) - self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) self.scaled = scaled self.opts = None self.image = None - + def setImage(self, img, *args, **kargs): """ img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). @@ -43,22 +54,22 @@ class RawImageWidget(QtGui.QWidget): argb, alpha = fn.makeARGB(self.opts[0], *self.opts[1], **self.opts[2]) self.image = fn.makeQImage(argb, alpha) self.opts = () - #if self.pixmap is None: - #self.pixmap = QtGui.QPixmap.fromImage(self.image) + # if self.pixmap is None: + # self.pixmap = QtGui.QPixmap.fromImage(self.image) p = QtGui.QPainter(self) if self.scaled: rect = self.rect() ar = rect.width() / float(rect.height()) imar = self.image.width() / float(self.image.height()) if ar > imar: - rect.setWidth(int(rect.width() * imar/ar)) + rect.setWidth(int(rect.width() * imar / ar)) else: - rect.setHeight(int(rect.height() * ar/imar)) - + rect.setHeight(int(rect.height() * ar / imar)) + p.drawImage(rect, self.image) else: p.drawImage(QtCore.QPointF(), self.image) - #p.drawPixmap(self.rect(), self.pixmap) + # p.drawPixmap(self.rect(), self.pixmap) p.end() @@ -67,7 +78,10 @@ if HAVE_OPENGL: """ Similar to RawImageWidget, but uses a GL widget to do all drawing. Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking. + + Checks if setConfigOptions(imageAxisOrder='row-major') was set. """ + def __init__(self, parent=None, scaled=False): QtOpenGL.QGLWidget.__init__(self, parent) self.scaled = scaled @@ -75,6 +89,7 @@ if HAVE_OPENGL: self.uploaded = False self.smooth = False self.opts = None + self.row_major = getConfigOption('imageAxisOrder') == 'row-major' def setImage(self, img, *args, **kargs): """ @@ -88,7 +103,7 @@ if HAVE_OPENGL: def initializeGL(self): self.texture = glGenTextures(1) - + def uploadTexture(self): glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) @@ -100,17 +115,22 @@ if HAVE_OPENGL: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) - #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) - shape = self.image.shape - - ### Test texture dimensions first - #glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) - #if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: - #raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) - - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.image.transpose((1,0,2))) + # glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + + if self.row_major: + image = self.image + else: + image = self.image.transpose((1, 0, 2)) + + # ## Test texture dimensions first + # shape = self.image.shape + # glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + # if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: + # raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.shape[1], image.shape[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, image) glDisable(GL_TEXTURE_2D) - + def paintGL(self): if self.image is None: if self.opts is None: @@ -118,26 +138,23 @@ if HAVE_OPENGL: img, args, kwds = self.opts kwds['useRGBA'] = True self.image, alpha = fn.makeARGB(img, *args, **kwds) - + if not self.uploaded: self.uploadTexture() - + glViewport(0, 0, self.width(), self.height()) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) - glColor4f(1,1,1,1) + glColor4f(1, 1, 1, 1) glBegin(GL_QUADS) - glTexCoord2f(0,0) - glVertex3f(-1,-1,0) - glTexCoord2f(1,0) + glTexCoord2f(0, 1) + glVertex3f(-1, -1, 0) + glTexCoord2f(1, 1) glVertex3f(1, -1, 0) - glTexCoord2f(1,1) + glTexCoord2f(1, 0) glVertex3f(1, 1, 0) - glTexCoord2f(0,1) + glTexCoord2f(0, 0) glVertex3f(-1, 1, 0) glEnd() glDisable(GL_TEXTURE_3D) - - - From 3fc6eff76f3c1ba73199c4718163e04f6c841d10 Mon Sep 17 00:00:00 2001 From: Ben Deverett Date: Wed, 6 Sep 2017 23:30:55 -0400 Subject: [PATCH 011/235] added fps class variable to ImageView to enable consistent playback frame rate --- pyqtgraph/imageview/ImageView.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..b1a95659 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -162,6 +162,7 @@ class ImageView(QtGui.QWidget): self.keysPressed = {} self.playTimer = QtCore.QTimer() self.playRate = 0 + self.fps = 1 # 1 Hz by default self.lastPlayTime = 0 self.normRgn = LinearRegionItem() @@ -349,11 +350,14 @@ class ImageView(QtGui.QWidget): self.image = None self.imageItem.clear() - def play(self, rate): + def play(self, rate=None): """Begin automatically stepping frames forward at the given rate (in fps). This can also be accessed by pressing the spacebar.""" #print "play:", rate + if rate is None: + rate = self.fps self.playRate = rate + if rate == 0: self.playTimer.stop() return @@ -400,9 +404,7 @@ class ImageView(QtGui.QWidget): #print ev.key() if ev.key() == QtCore.Qt.Key_Space: if self.playRate == 0: - fps = (self.getProcessedImage().shape[0]-1) / (self.tVals[-1] - self.tVals[0]) - self.play(fps) - #print fps + self.play() else: self.play(0) ev.accept() From f90a21e9e679952b4e67e66cff025a53019051ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20M=C3=BCller?= Date: Mon, 20 Nov 2017 11:20:17 +0100 Subject: [PATCH 012/235] Information is spelled with an r, even in comments --- pyqtgraph/Point.py | 2 +- pyqtgraph/Vector.py | 2 +- pyqtgraph/WidgetGroup.py | 2 +- pyqtgraph/configfile.py | 2 +- pyqtgraph/debug.py | 2 +- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/MultiPlotItem.py | 2 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/graphicsWindows.py | 2 +- pyqtgraph/imageview/ImageView.py | 2 +- pyqtgraph/metaarray/MetaArray.py | 2 +- pyqtgraph/pgcollections.py | 2 +- pyqtgraph/ptime.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 2 +- pyqtgraph/widgets/MultiPlotWidget.py | 2 +- pyqtgraph/widgets/PlotWidget.py | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 4d04f01c..9e6491eb 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -2,7 +2,7 @@ """ Point.py - Extension of QPointF which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index f2898e80..c4a31428 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -2,7 +2,7 @@ """ Vector.py - Extension of QVector3D which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtGui, QtCore, USE_PYSIDE diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index d7e265c5..9371bb97 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -2,7 +2,7 @@ """ WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class addresses the problem of having to save and restore the state of a large group of widgets. diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 7b20db1d..b8a8c44c 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -2,7 +2,7 @@ """ configfile.py - Human-readable text configuration file library Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Used for reading and writing dictionary objects to a python-like configuration file format. Data structures may be nested and contain any data type as long diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0da24d7c..d6da1691 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -2,7 +2,7 @@ """ debug.py - Functions to aid in debugging Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import print_function diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..5c107052 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2,7 +2,7 @@ """ functions.py - Miscellaneous functions with no other home Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import division diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index be775d4a..065a605e 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -2,7 +2,7 @@ """ MultiPlotItem.py - Graphics item used for displaying an array of PlotItems Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from numpy import ndarray diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 41011df3..adc1950a 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -2,7 +2,7 @@ """ PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class is one of the workhorses of pyqtgraph. It implements a graphics item with plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 81a4e651..a913aa2e 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2,7 +2,7 @@ """ ROI.py - Interactive graphics items for GraphicsView (ROI widgets) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Implements a series of graphics items which display movable/scalable/rotatable shapes for use as region-of-interest markers. ROI class automatically handles extraction diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 1aa3f3f4..3a5953cf 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -2,7 +2,7 @@ """ graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore, QtGui diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..36ae5c73 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -2,7 +2,7 @@ """ ImageView.py - Widget for basic image dispay and analysis Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Widget used for displaying 2D or 3D data. Features: - float or int (including 16-bit int) image display via ImageItem diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..7984f6a8 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -2,7 +2,7 @@ """ MetaArray.py - Class encapsulating ndarray with meta data Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. MetaArray is an array class based on numpy.ndarray that allows storage of per-axis meta data such as axis values, names, units, column names, etc. It also enables several diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index 76850622..b1fc779f 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -2,7 +2,7 @@ """ advancedTypes.py - Basic data structures not included with python Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Includes: - OrderedDict - Dictionary which preserves the order of its elements diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index 1de8282f..4e761afe 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -2,7 +2,7 @@ """ ptime.py - Precision time function made os-independent (should have been taken care of by python) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f8cbb5..92547ef0 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -2,7 +2,7 @@ """ GraphicsView.py - Extension of QGraphicsView Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui, USE_PYSIDE diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index d1f56034..21258839 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -2,7 +2,7 @@ """ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiPlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore from .GraphicsView import GraphicsView diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 964307ae..8711e1f6 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -2,7 +2,7 @@ """ PlotWidget.py - Convenience class--GraphicsView widget displaying a single PlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui From 01d6e4b8e24821582007e85ad0e55f839734dfe0 Mon Sep 17 00:00:00 2001 From: Xinfa Zhu Date: Mon, 18 Dec 2017 16:16:49 -0600 Subject: [PATCH 013/235] Add name label to GradientEditorItem --- pyqtgraph/graphicsItems/GradientEditorItem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index f359ff11..79d33c45 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -434,8 +434,14 @@ class GradientEditorItem(TickSliderItem): label = QtGui.QLabel() label.setPixmap(px) label.setContentsMargins(1, 1, 1, 1) + labelName = QtGui.QLabel(g) + hbox = QtGui.QHBoxLayout() + hbox.addWidget(labelName) + hbox.addWidget(label) + widget = QtGui.QWidget() + widget.setLayout(hbox) act = QtGui.QWidgetAction(self) - act.setDefaultWidget(label) + act.setDefaultWidget(widget) act.triggered.connect(self.contextMenuClicked) act.name = g self.menu.addAction(act) From 482dd2ee335d6dd3146417f7d520d7a0b413340f Mon Sep 17 00:00:00 2001 From: Sebastian Pauka Date: Tue, 30 Oct 2018 10:22:49 +1100 Subject: [PATCH 014/235] Terminate FileForwarder thread on process end Previously on windows the FileForwarder threads would continue to run and eat up a lot of CPU once the child process they were forwarding dies. This commit shuts down those threads when the child process is killed. --- pyqtgraph/multiprocess/processes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index d841ea40..6e815edc 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -166,6 +166,14 @@ class Process(RemoteEventHandler): raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) self.conn.close() + + # Close remote polling threads, otherwise they will spin continuously + if hasattr(self, "_stdoutForwarder"): + self._stdoutForwarder.finish.set() + self._stderrForwarder.finish.set() + self._stdoutForwarder.join() + self._stderrForwarder.join() + self.debugMsg('Child process exited. (%d)' % self.proc.returncode) def debugMsg(self, msg, *args): @@ -473,23 +481,24 @@ class FileForwarder(threading.Thread): self.lock = threading.Lock() self.daemon = True self.color = color + self.finish = threading.Event() self.start() def run(self): if self.output == 'stdout' and self.color is not False: - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: cprint.cout(self.color, line, -1) elif self.output == 'stderr' and self.color is not False: - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: cprint.cerr(self.color, line, -1) else: if isinstance(self.output, str): self.output = getattr(sys, self.output) - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: - self.output.write(line) + self.output.write(line.decode('utf8')) From 7e09022e67caaa350466ed51dba825516e0ef06d Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Fri, 21 Dec 2018 01:45:00 -0500 Subject: [PATCH 015/235] add legend for bar graph items --- pyqtgraph/graphicsItems/LegendItem.py | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 200820fc..91b6e9a3 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -147,29 +147,28 @@ class ItemSample(GraphicsWidget): return QtCore.QRectF(0, 0, 20, 20) def paint(self, p, *args): - #p.setRenderHint(p.Antialiasing) # only if the data is antialiased. + # p.setRenderHint(p.Antialiasing) # only if the data is antialiased. opts = self.item.opts - - if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: - p.setBrush(fn.mkBrush(opts['fillBrush'])) - p.setPen(fn.mkPen(None)) - p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - + if not isinstance(self.item, ScatterPlotItem): p.setPen(fn.mkPen(opts['pen'])) p.drawLine(2, 18, 18, 2) - + + if opts.get('fillLevel', None) is not None and opts.get('fillBrush', None) is not None: + p.setBrush(fn.mkBrush(opts['fillBrush'])) + p.setPen(fn.mkPen(opts['fillBrush'])) + p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2, 18), QtCore.QPointF(18, 2), QtCore.QPointF(18, 18)])) + symbol = opts.get('symbol', None) if symbol is not None: if isinstance(self.item, PlotDataItem): opts = self.item.scatter.opts - - pen = fn.mkPen(opts['pen']) - brush = fn.mkBrush(opts['brush']) - size = opts['size'] - - p.translate(10,10) - path = drawSymbol(p, symbol, size, pen, brush) + p.translate(10, 10) + drawSymbol(p, symbol, opts['size'], fn.mkPen(opts['pen']), fn.mkBrush(opts['brush'])) + + if isinstance(self.item, BarGraphItem): + p.setBrush(fn.mkBrush(opts['brush'])) + p.drawRect(QtCore.QRectF(2, 2, 18, 18)) From 3f93e30b312c8966b695fde07054c62f34f9896c Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Fri, 21 Dec 2018 01:47:23 -0500 Subject: [PATCH 016/235] Update Legend.py add legend examples for line plot, bar graph and scatter plot. --- examples/Legend.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/examples/Legend.py b/examples/Legend.py index f7841151..3759c2e9 100644 --- a/examples/Legend.py +++ b/examples/Legend.py @@ -7,17 +7,37 @@ import initExample ## Add path to library (just for examples; you do not need th import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui +import numpy as np -plt = pg.plot() -plt.setWindowTitle('pyqtgraph example: Legend') -plt.addLegend() -#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) -#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case +win = pg.plot() +win.setWindowTitle('pyqtgraph example: BarGraphItem') -c1 = plt.plot([1,3,2,4], pen='r', symbol='o', symbolPen='r', symbolBrush=0.5, name='red plot') -c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot') -#l.addItem(c1, 'red plot') -#l.addItem(c2, 'green plot') +# # option1: only for .plot(), following c1,c2 for example----------------------- +# win.addLegend() + +# bar graph +x = np.arange(10) +y = np.sin(x+2) * 3 +bg1 = pg.BarGraphItem(x=x, height=y, width=0.3, brush='b', pen='w', name='bar') +win.addItem(bg1) + +# curve +c1 = win.plot([np.random.randint(0,8) for i in range(10)], pen='r', symbol='t', symbolPen='r', symbolBrush='g', name='curve1') +c2 = win.plot([2,1,4,3,1,3,2,4,3,2], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='curve2') + +# scatter plot +s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120), name='scatter') +spots = [{'pos': [i, np.random.randint(-3, 3)], 'data': 1} for i in range(10)] +s1.addPoints(spots) +win.addItem(s1) + +# # option2: generic method------------------------------------------------ +legend = pg.LegendItem((80,60), offset=(70,20)) +legend.setParentItem(win.graphicsItem()) +legend.addItem(bg1, 'bar') +legend.addItem(c1, 'curve1') +legend.addItem(c2, 'curve2') +legend.addItem(s1, 'scatter') ## Start Qt event loop unless running in interactive mode or using pyside. From 365c13fedc26f316149f9d8f114598b29d65703f Mon Sep 17 00:00:00 2001 From: SamSchott Date: Thu, 14 Mar 2019 22:41:10 +0000 Subject: [PATCH 017/235] Clipping: don't assume that x-values have uniform spacing Do not assume that x-values have uniform spacing -- this can cause problems especially with large datasets and non-uniform spacing (e.g., time-dependent readings from an instrument). Use `np.searchsorted` instead to find the first and last data index in the view range. This only assumes that x-values are in ascending order. This prevents potentially too strong clipping. --- pyqtgraph/graphicsItems/PlotDataItem.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6797af64..69d7dc6e 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -540,15 +540,16 @@ class PlotDataItem(GraphicsObject): if self.opts['clipToView']: view = self.getViewBox() if view is None or not view.autoRangeEnabled()[0]: - # this option presumes that x-values have uniform spacing + # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - dx = float(x[-1]-x[0]) / (len(x)-1) # clip to visible region extended by downsampling value - x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) - x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) - x = x[x0:x1] - y = y[x0:x1] + idx = np.searchsorted(x, [range.left(), range.right()]) + idx = idx + np.array([-2*ds, 2*ds]) + idx = np.clip(idx, a_min=0, a_max=len(x)) + + x = x[idx[0]:idx[1]] + y = y[idx[0]:idx[1]] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From b773b020a51e720ab07cedc654fb642e23b9011a Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Mon, 25 Mar 2019 18:02:04 -0400 Subject: [PATCH 018/235] Update LegendItem.py --- pyqtgraph/graphicsItems/LegendItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 91b6e9a3..418c428f 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -6,6 +6,7 @@ from ..Point import Point from .ScatterPlotItem import ScatterPlotItem, drawSymbol from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor +from .BarGraphItem import BarGraphItem __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): From af57d16bb51e24bd6623aeac3dd41404eedc149f Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 19 May 2019 00:28:18 +0200 Subject: [PATCH 019/235] Removed system_site_packages from Travis CI system_site_packages are opposed to the used conda installation. They are both unnecessary and lead to Travis build errors. --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c7b7769..a4d85e47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,6 @@ sudo: false notifications: email: false -virtualenv: - system_site_packages: true - - env: # Enable python 2 and python 3 builds # Note that the 2.6 build doesn't get flake8, and runs old versions of From f23889d5940b5b9be6c76be8c3e78e80689e223d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 13:53:00 +0100 Subject: [PATCH 020/235] only use `np.searchsorted` quicker method fails --- pyqtgraph/graphicsItems/PlotDataItem.py | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 69d7dc6e..7c7c2fba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -543,13 +543,25 @@ class PlotDataItem(GraphicsObject): # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - # clip to visible region extended by downsampling value - idx = np.searchsorted(x, [range.left(), range.right()]) - idx = idx + np.array([-2*ds, 2*ds]) - idx = np.clip(idx, a_min=0, a_max=len(x)) - - x = x[idx[0]:idx[1]] - y = y[idx[0]:idx[1]] + # clip to visible region extended by downsampling value, assuming + # uniform spacing of x-values, has O(1) performance + dx = float(x[-1]-x[0]) / (len(x)-1) + x0 = np.clip(int((range.left()-x[0])/dx) - 1*ds, 0, len(x)-1) + x1 = np.clip(int((range.right()-x[0])/dx) + 2*ds, 0, len(x)-1) + + # if data has been clipped too strongly (in case of non-uniform + # spacing of x-values), refine the clipping region as required + # worst case performance: O(log(n)) + # best case performance: O(1) + if x[x0] > range.left(): + x0 = np.searchsorted(x, range.left()) - 1*ds + x0 = np.clip(x0, a_min=0, a_max=len(x)) + if x[x1] < range.right(): + x1 = np.searchsorted(x, range.right()) + 2*ds + x1 = np.clip(x1, a_min=0, a_max=len(x)) + + x = x[x0:x1] + y = y[x0:x1] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From b491f82006ddfb44551327da9308ad15cb86674f Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 25 Apr 2016 09:14:12 +0300 Subject: [PATCH 021/235] Bugfix: ViewBox border drawing - Fixed border overlapping (issue #316) - Added new method ViewBox.setBorder to complete API --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 27fb8268..45722e79 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -14,6 +14,7 @@ from ...Qt import isQObjectAlive __all__ = ['ViewBox'] + class WeakList(object): def __init__(self): @@ -34,10 +35,12 @@ class WeakList(object): yield d i -= 1 + class ChildGroup(ItemGroup): def __init__(self, parent): ItemGroup.__init__(self, parent) + self.setFlag(self.ItemClipsChildrenToShape) # Used as callback to inform ViewBox when items are added/removed from # the group. @@ -64,6 +67,12 @@ class ChildGroup(ItemGroup): listener.itemsChanged() return ret + def shape(self): + return self.mapFromParent(self.parentItem().shape()) + + def boundingRect(self): + return self.mapRectFromParent(self.parentItem().boundingRect()) + class ViewBox(GraphicsWidget): """ @@ -185,6 +194,11 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() + self.borderRect = QtGui.QGraphicsRectItem(self.rect()) + self.borderRect.setParentItem(self) + self.borderRect.setZValue(1e3) + self.borderRect.setPen(self.border) + ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -428,8 +442,10 @@ class ViewBox(GraphicsWidget): self.updateViewRange() self._matrixNeedsUpdate = True self.background.setRect(self.rect()) + self.borderRect.setRect(self.rect()) self.sigStateChanged.emit(self) self.sigResized.emit(self) + self.childGroup.prepareGeometryChange() def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" @@ -571,7 +587,7 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True - + self.sigStateChanged.emit(self) def setYRange(self, min, max, padding=None, update=True): @@ -1054,6 +1070,19 @@ class ViewBox(GraphicsWidget): def xInverted(self): return self.state['xInverted'] + def setBorder(self, *args, **kwds): + """ + Set the pen used to draw border around the view + + If border is None, then no border will be drawn. + + Added in version 0.9.10 + + See :func:`mkPen ` for arguments. + """ + self.border = fn.mkPen(*args, **kwds) + self.borderRect.setPen(self.border) + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. @@ -1499,7 +1528,7 @@ class ViewBox(GraphicsWidget): if any(changed): self._matrixNeedsUpdate = True self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: From 001390c160a71ad0d4f98752c057c506010bfc92 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 25 Apr 2016 11:35:24 +0300 Subject: [PATCH 022/235] Bugfix: GraphicsLayout border drawing - Fixed border overlapping (issue #316) --- pyqtgraph/graphicsItems/GraphicsLayout.py | 48 ++++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 6ec38fb5..c0db5890 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -23,6 +23,7 @@ class GraphicsLayout(GraphicsWidget): self.setLayout(self.layout) self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item + self.itemBorders = {} ## {item1: QtGui.QGraphicsRectItem, ...} border rects self.currentRow = 0 self.currentCol = 0 self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) @@ -39,8 +40,10 @@ class GraphicsLayout(GraphicsWidget): See :func:`mkPen ` for arguments. """ self.border = fn.mkPen(*args, **kwds) - self.update() - + + for borderRect in self.itemBorders.values(): + borderRect.setPen(self.border) + def nextRow(self): """Advance to next row for automatic item placement""" self.currentRow += 1 @@ -119,7 +122,17 @@ class GraphicsLayout(GraphicsWidget): self.rows[row2] = {} self.rows[row2][col2] = item self.items[item].append((row2, col2)) - + + borderRect = QtGui.QGraphicsRectItem() + + borderRect.setParentItem(self) + borderRect.setZValue(1e3) + borderRect.setPen(fn.mkPen(self.border)) + + self.itemBorders[item] = borderRect + + item.geometryChanged.connect(self._updateItemBorder) + self.layout.addItem(item, row, col, rowspan, colspan) self.nextColumn() @@ -129,15 +142,7 @@ class GraphicsLayout(GraphicsWidget): def boundingRect(self): return self.rect() - - def paint(self, p, *args): - if self.border is None: - return - p.setPen(fn.mkPen(self.border)) - for i in self.items: - r = i.mapRectToParent(i.boundingRect()) - p.drawRect(r) - + def itemIndex(self, item): for i in range(self.layout.count()): if self.layout.itemAt(i).graphicsItem() is item: @@ -150,13 +155,16 @@ class GraphicsLayout(GraphicsWidget): self.layout.removeAt(ind) self.scene().removeItem(item) - for r,c in self.items[item]: + for r, c in self.items[item]: del self.rows[r][c] del self.items[item] + + item.geometryChanged.disconnect(self._updateItemBorder) + del self.itemBorders[item] + self.update() def clear(self): - items = [] for i in list(self.items.keys()): self.removeItem(i) @@ -168,4 +176,14 @@ class GraphicsLayout(GraphicsWidget): def setSpacing(self, *args): self.layout.setSpacing(*args) - \ No newline at end of file + + def _updateItemBorder(self): + if self.border is None: + return + + item = self.sender() + if item is None: + return + + r = item.mapRectToParent(item.boundingRect()) + self.itemBorders[item].setRect(r) From 1955ae024c66c5ac3407bd3eb94812a945fa0b83 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 24 Jun 2019 16:36:23 +0300 Subject: [PATCH 023/235] Fix: object has no attribute 'border' --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 45722e79..a542e916 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -194,6 +194,8 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() + self.border = fn.mkPen(border) + self.borderRect = QtGui.QGraphicsRectItem(self.rect()) self.borderRect.setParentItem(self) self.borderRect.setZValue(1e3) @@ -221,7 +223,6 @@ class ViewBox(GraphicsWidget): self.setAspectLocked(lockAspect) - self.border = fn.mkPen(border) if enableMenu: self.menu = ViewBoxMenu(self) else: From d870a34359682251dc22079161b003b5ad060c77 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 14:49:32 +0100 Subject: [PATCH 024/235] add a test for clipping --- .../graphicsItems/tests/test_PlotDataItem.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index b506a654..adc525d9 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -64,3 +64,25 @@ def test_clear_in_step_mode(): c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) w.addItem(c) c.clear() + +def test_clipping(): + y = np.random.normal(size=150) + x = np.exp2(np.linspace(5, 10, 150)) # non-uniform spacing + + w = pg.PlotWidget(autoRange=True, downsample=5) + c = pg.PlotDataItem(x, y) + w.addItem(c) + w.show() + + c.setClipToView(True) + + w.setXRange(200, 600) + + for x_min in range(100, 2**10 - 100, 100): + w.setXRange(x_min, x_min + 100) + + xDisp, _ = c.getData() + vr = c.viewRect() + + assert xDisp[0] <= vr.left() + assert xDisp[-1] >= vr.right() From 8067ee25d5b97ed0a093e2d8e4ff1d2cc01ec2d4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 24 Jun 2019 15:39:12 -0700 Subject: [PATCH 025/235] Work around PySide setOverrideCursor bug in BusyCursor --- pyqtgraph/widgets/BusyCursor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index e7a26810..f6bbc84c 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore, QT_LIB __all__ = ['BusyCursor'] @@ -17,7 +17,12 @@ class BusyCursor(object): app = QtCore.QCoreApplication.instance() isGuiThread = (app is not None) and (QtCore.QThread.currentThread() == app.thread()) if isGuiThread and QtGui.QApplication.instance() is not None: - QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + if QT_LIB == 'PySide': + # pass CursorShape rather than QCursor for PySide + # see https://bugreports.qt.io/browse/PYSIDE-243 + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + else: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) BusyCursor.active.append(self) self._active = True else: @@ -27,4 +32,3 @@ class BusyCursor(object): if self._active: BusyCursor.active.pop(-1) QtGui.QApplication.restoreOverrideCursor() - \ No newline at end of file From 55d9e8888b77c50a1ba568c9dd3153f5f288976f Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 25 Jun 2019 16:08:18 -0700 Subject: [PATCH 026/235] Allow last image in stack to be selected by slider in ImageView --- pyqtgraph/imageview/ImageView.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 81463b7a..512d503b 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -740,7 +740,7 @@ class ImageView(QtGui.QWidget): return (0,0) t = slider.value() - + xv = self.tVals if xv is None: ind = int(t) @@ -748,7 +748,7 @@ class ImageView(QtGui.QWidget): if len(xv) < 2: return (0,0) totTime = xv[-1] + (xv[-1]-xv[-2]) - inds = np.argwhere(xv < t) + inds = np.argwhere(xv <= t) if len(inds) < 1: return (0,t) ind = inds[-1,0] From a04d31731d65556d6e8372f374118822dbd04d0f Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 25 Jun 2019 22:06:36 -0700 Subject: [PATCH 027/235] Preventing conda from updating --- azure-test-template.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 496ec10b..c2b6e58c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -88,14 +88,15 @@ jobs: inputs: environmentName: 'test-environment-$(python.version)' packageSpecs: 'python=$(python.version)' + updateConda: false - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest six coverage --yes --quiet else - pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage + pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi pip install pytest-xdist pytest-cov pytest-faulthandler displayName: "Install Dependencies" From fe637512b52ec8b2f1849302c471de7c42d099c8 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 25 Jun 2019 23:05:38 -0700 Subject: [PATCH 028/235] Skip GL* examples if on macOS with python2 and qt4 bindings, and update readme --- README.md | 4 +++- examples/test_examples.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e5b3a9c7..28143078 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Requirements * `numpy`, `scipy` * Optional * `pyopengl` for 3D graphics + * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` * `hdf5` for large hdf5 binary format support * Known to run on Windows, Linux, and macOS. @@ -41,7 +42,8 @@ Below is a table of the configurations we test and have confidence pyqtgraph wil | 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible. +* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible +* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work Support ------- diff --git a/examples/test_examples.py b/examples/test_examples.py index 0856b4ff..bb4682f1 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -56,10 +56,17 @@ exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), - "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671") + "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671"), + 'GLVolumeItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLIsosurface.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLSurfacePlot.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLScatterPlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLshaders.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLLinePlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLMeshItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLImageItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939") } - @pytest.mark.parametrize( "frontend, f", [ From 26963ffbc4744559e409385ce922e46f4261fd04 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 24 Jun 2019 16:17:48 -0700 Subject: [PATCH 029/235] Fix pg.exit test in case pyqtgraph is not installed --- .travis.yml | 15 ++++++++------- pyqtgraph/tests/test_exit_crash.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0da455d8..e44739c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ install: - if [ "${QT}" == "pyside" ]; then conda install pyside --yes; fi; - - pip install pytest-xdist # multi-thread py.test + - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats - pip install pytest-faulthandler # activate faulthandler @@ -132,9 +132,15 @@ script: # Check system info - python -c "import pyqtgraph as pg; pg.systemInfo()" + + # Check install works + - start_test "install test"; + python setup.py --quiet install; + check_output "install test"; + # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov pyqtgraph -sv; + PYTHONPATH=. pytest --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd @@ -164,11 +170,6 @@ script: check_output "style check"; fi; - # Check install works - - start_test "install test"; - python setup.py --quiet install; - check_output "install test"; - # Check double-install fails # Note the bash -c is because travis strips off the ! otherwise. - start_test "double install test"; diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 7c472104..50924908 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -74,5 +74,5 @@ def test_pg_exit(): pg.plot() pg.exit() """) - rc = call_with_timeout([sys.executable, '-c', code], timeout=5) + rc = call_with_timeout([sys.executable, '-c', code], timeout=5, shell=False) assert rc == 0 From d9f0da5a7c8679095f2716fdc9cd6880855fc159 Mon Sep 17 00:00:00 2001 From: Jeffrey Nichols Date: Thu, 27 Jun 2019 14:45:37 -0400 Subject: [PATCH 030/235] Fix for AxisItem using old scale to create label --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b34052ae..3a92e2b1 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -448,11 +448,11 @@ class AxisItem(GraphicsWidget): if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. scale = 1.0 prefix = '' + self.autoSIPrefixScale = scale self.setLabel(unitPrefix=prefix) else: - scale = 1.0 + self.autoSIPrefixScale = 1.0 - self.autoSIPrefixScale = scale self.picture = None self.update() From c37956b29a728e3b8552eb2fe4b229614f78c2f0 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Fri, 28 Jun 2019 01:51:20 +0200 Subject: [PATCH 031/235] Corrected documentation for heightColor shader --- pyqtgraph/opengl/shaders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py index 8922cd21..7ada939c 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -140,9 +140,9 @@ def initShaders(): ## colors fragments by z-value. ## This is useful for coloring surface plots by height. ## This shader uses a uniform called "colorMap" to determine how to map the colors: - ## red = pow(z * colorMap[0] + colorMap[1], colorMap[2]) - ## green = pow(z * colorMap[3] + colorMap[4], colorMap[5]) - ## blue = pow(z * colorMap[6] + colorMap[7], colorMap[8]) + ## red = pow(colorMap[0]*(z + colorMap[1]), colorMap[2]) + ## green = pow(colorMap[3]*(z + colorMap[4]), colorMap[5]) + ## blue = pow(colorMap[6]*(z + colorMap[7]), colorMap[8]) ## (set the values like this: shader['uniformMap'] = array([...]) ShaderProgram('heightColor', [ VertexShader(""" From 23b4e174f073bb1d444ff8390641dc2c7c52e0a0 Mon Sep 17 00:00:00 2001 From: Billy SU Date: Fri, 28 Jun 2019 12:51:54 +0800 Subject: [PATCH 032/235] Add Dock test and remove outdated comments (#659) * Add test for Dock closable arg * Remove outdated and debug comments * Add test for hideTitle dock --- pyqtgraph/dockarea/Dock.py | 18 ------------------ pyqtgraph/dockarea/tests/test_dock.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index ddeb0c4a..a7234073 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -89,27 +89,15 @@ class Dock(QtGui.QWidget, DockDrop): The actual size will be determined by comparing this Dock's stretch value to the rest of the docks it shares space with. """ - #print "setStretch", self, x, y - #self._stretch = (x, y) if x is None: x = 0 if y is None: y = 0 - #policy = self.sizePolicy() - #policy.setHorizontalStretch(x) - #policy.setVerticalStretch(y) - #self.setSizePolicy(policy) self._stretch = (x, y) self.sigStretchChanged.emit() - #print "setStretch", self, x, y, self.stretch() def stretch(self): - #policy = self.sizePolicy() - #return policy.horizontalStretch(), policy.verticalStretch() return self._stretch - - #def stretch(self): - #return self._stretch def hideTitleBar(self): """ @@ -150,7 +138,6 @@ class Dock(QtGui.QWidget, DockDrop): By default ('auto'), the orientation is determined based on the aspect ratio of the Dock. """ - #print self.name(), "setOrientation", o, force if o == 'auto' and self.autoOrient: if self.container().type() == 'tab': o = 'horizontal' @@ -165,19 +152,16 @@ class Dock(QtGui.QWidget, DockDrop): def updateStyle(self): ## updates orientation and appearance of title bar - #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() if self.labelHidden: self.widgetArea.setStyleSheet(self.nStyle) elif self.orientation == 'vertical': self.label.setOrientation('vertical') if self.moveLabel: - #print self.name(), "reclaim label" self.topLayout.addWidget(self.label, 1, 0) self.widgetArea.setStyleSheet(self.vStyle) else: self.label.setOrientation('horizontal') if self.moveLabel: - #print self.name(), "reclaim label" self.topLayout.addWidget(self.label, 0, 1) self.widgetArea.setStyleSheet(self.hStyle) @@ -203,7 +187,6 @@ class Dock(QtGui.QWidget, DockDrop): def startDrag(self): self.drag = QtGui.QDrag(self) mime = QtCore.QMimeData() - #mime.setPlainText("asd") self.drag.setMimeData(mime) self.widgetArea.setStyleSheet(self.dragStyle) self.update() @@ -220,7 +203,6 @@ class Dock(QtGui.QWidget, DockDrop): if self._container is not None: # ask old container to close itself if it is no longer needed self._container.apoptose() - #print self.name(), "container changed" self._container = c if c is None: self.area = None diff --git a/pyqtgraph/dockarea/tests/test_dock.py b/pyqtgraph/dockarea/tests/test_dock.py index 949f3f0e..3fb47075 100644 --- a/pyqtgraph/dockarea/tests/test_dock.py +++ b/pyqtgraph/dockarea/tests/test_dock.py @@ -14,3 +14,15 @@ def test_dock(): assert dock.name() == name # no surprises in return type. assert type(dock.name()) == type(name) + +def test_closable_dock(): + name = "Test close dock" + dock = da.Dock(name=name, closable=True) + + assert dock.label.closeButton != None + +def test_hide_title_dock(): + name = "Test hide title dock" + dock = da.Dock(name=name, hideTitle=True) + + assert dock.labelHidden == True From caf7378f3860968fb7c8b6680aa299cb32cacfde Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 28 Jun 2019 10:59:03 -0300 Subject: [PATCH 033/235] FIX: fix disconnect method of SignalProxy class. --- pyqtgraph/SignalProxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 7463dfc3..46b44887 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -81,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot()) + self.sigDelayed.disconnect(self.slot) except: pass From a4cecf4a222927aa9b989fb6bc7059a6ce9b28c8 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 30 Jun 2019 17:50:11 +0200 Subject: [PATCH 034/235] Call parent __init__ as soon as possible for CtrlNode --- pyqtgraph/flowchart/library/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 8b3376c3..1f5613c9 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -91,14 +91,15 @@ class CtrlNode(Node): sigStateChanged = QtCore.Signal(object) def __init__(self, name, ui=None, terminals=None): + if terminals is None: + terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} + Node.__init__(self, name=name, terminals=terminals) + if ui is None: if hasattr(self, 'uiTemplate'): ui = self.uiTemplate else: ui = [] - if terminals is None: - terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} - Node.__init__(self, name=name, terminals=terminals) self.ui, self.stateGroup, self.ctrls = generateUi(ui) self.stateGroup.sigChanged.connect(self.changed) From 98e66a855e88df9002de04c17ea99b990ef86bad Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 30 Jun 2019 12:05:57 -0700 Subject: [PATCH 035/235] Remove pytest-faulthandler from test dependencies --- .travis.yml | 1 - CONTRIBUTING.md | 1 - azure-test-template.yml | 4 ++-- pytest.ini | 3 +-- tox.ini | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index e44739c4..c75f4523 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,7 +63,6 @@ install: fi; - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats - - pip install pytest-faulthandler # activate faulthandler # Debugging helpers - uname -a diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ca5e0bf..3d27ad10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,6 @@ Please use the following guidelines when preparing changes: * pytest * pytest-cov * pytest-xdist -* pytest-faulthandler * Optional: pytest-xvfb ### Tox diff --git a/azure-test-template.yml b/azure-test-template.yml index c2b6e58c..d66c304c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -98,7 +98,7 @@ jobs: else pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi - pip install pytest-xdist pytest-cov pytest-faulthandler + pip install pytest-xdist pytest-cov displayName: "Install Dependencies" - bash: | @@ -194,4 +194,4 @@ jobs: inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' \ No newline at end of file + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' diff --git a/pytest.ini b/pytest.ini index fa664793..1814592b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,7 +4,6 @@ xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 xvfb_args=-ac +extension GLX +render -addopts = --faulthandler-timeout=15 filterwarnings = # comfortable skipping these warnings runtime warnings @@ -12,4 +11,4 @@ filterwarnings = ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning - ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning \ No newline at end of file + ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning diff --git a/tox.ini b/tox.ini index 6bbb5566..9091c8cb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,6 @@ deps= {[base]deps} pytest-cov pytest-xdist - pytest-faulthandler pyside2-pip: pyside2 pyqt5-pip: pyqt5 From df0467961e2141f91bdead670a53cce1a398bde4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 30 Jun 2019 13:30:58 -0700 Subject: [PATCH 036/235] Install pytest-faulthandler for py27 and add timeout --- .travis.yml | 7 ++++++- azure-test-template.yml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c75f4523..642181f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,6 +64,11 @@ install: - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats + # faulthandler support not built in to pytest for python 2.7 + - if [ "${PYTHON}" == "2.7" ]; then + pip install pytest-faulthandler; + fi; + # Debugging helpers - uname -a - cat /etc/issue @@ -139,7 +144,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv; + PYTHONPATH=. pytest --cov pyqtgraph -sv -o faulthandler_timeout=15; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/azure-test-template.yml b/azure-test-template.yml index d66c304c..38b7d8b2 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -99,6 +99,10 @@ jobs: pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi pip install pytest-xdist pytest-cov + if [ $(python.version) == "2.7" ] + then + pip install pytest-faulthandler + fi displayName: "Install Dependencies" - bash: | @@ -170,7 +174,8 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html \ + -o faulthandler_timeout=15 displayName: 'Unit tests' env: AZURE: 1 From 2a0f866f7cffeaedb2a4120bb02285bbde1931c5 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 1 Jul 2019 09:21:40 -0700 Subject: [PATCH 037/235] Add timeout option back to ini and remove command line option --- .travis.yml | 2 +- CONTRIBUTING.md | 4 ++++ azure-test-template.yml | 3 +-- pytest.ini | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 642181f7..bec90488 100644 --- a/.travis.yml +++ b/.travis.yml @@ -144,7 +144,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv -o faulthandler_timeout=15; + PYTHONPATH=. pytest --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d27ad10..d602b89b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,10 @@ Please use the following guidelines when preparing changes: * pytest-xdist * Optional: pytest-xvfb +If you have pytest < 5, you may also want to install the pytest-faulthandler +plugin to output extra debugging information in case of test failures. This +isn't necessary with pytest 5+ as the plugin was merged into core pytest. + ### Tox As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin. diff --git a/azure-test-template.yml b/azure-test-template.yml index 38b7d8b2..f9d6e7ef 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -174,8 +174,7 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html \ - -o faulthandler_timeout=15 + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 diff --git a/pytest.ini b/pytest.ini index 1814592b..f53aea00 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,7 @@ xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 xvfb_args=-ac +extension GLX +render +faulthandler_timeout = 15 filterwarnings = # comfortable skipping these warnings runtime warnings From ac0e9dc99df33da1ac436ef602775df62086be96 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 1 Jul 2019 11:25:29 -0700 Subject: [PATCH 038/235] Add timeout for pytest<5 --- .travis.yml | 1 + azure-test-template.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index bec90488..173fa668 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,6 +67,7 @@ install: # faulthandler support not built in to pytest for python 2.7 - if [ "${PYTHON}" == "2.7" ]; then pip install pytest-faulthandler; + export PYTEST_ADDOPTS="--faulthandler-timeout=15"; fi; # Debugging helpers diff --git a/azure-test-template.yml b/azure-test-template.yml index f9d6e7ef..c1e6c1b0 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -102,6 +102,7 @@ jobs: if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler + export PYTEST_ADDOPTS="--faulthandler-timeout=15" fi displayName: "Install Dependencies" From dc9aa84ce3c799c1a085594634ce729d7b297615 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Jul 2019 18:30:00 -0700 Subject: [PATCH 039/235] Add a faster method for computing pseudoscatter --- pyqtgraph/functions.py | 56 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index fe3f9910..ebd29eed 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2311,14 +2311,62 @@ def invertQTransform(tr): raise Exception("Transform is not invertible.") return inv[0] + +def pseudoScatter(data, spacing=None, shuffle=True, bidir=False, method='exact'): + """Return an array of position values needed to make beeswarm or column scatter plots. -def pseudoScatter(data, spacing=None, shuffle=True, bidir=False): - """ - Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots. + Used for examining the distribution of values in an array. - Given a list of x-values, construct a set of y-values such that an x,y scatter-plot + Given an array of x-values, construct an array of y-values such that an x,y scatter-plot will not have overlapping points (it will look similar to a histogram). """ + if method == 'exact': + return _pseudoScatterExact(data, spacing=spacing, shuffle=shuffle, bidir=bidir) + elif method == 'histogram': + return _pseudoScatterHistogram(data, spacing=spacing, shuffle=shuffle, bidir=bidir) + + +def _pseudoScatterHistogram(data, spacing=None, shuffle=True, bidir=False): + """Works by binning points into a histogram and spreading them out to fill the bin. + + Faster method, but can produce blocky results. + """ + inds = np.arange(len(data)) + if shuffle: + np.random.shuffle(inds) + + data = data[inds] + + if spacing is None: + spacing = 2.*np.std(data)/len(data)**0.5 + + yvals = np.empty(len(data)) + + dmin = data.min() + dmax = data.max() + nbins = int((dmax-dmin) / spacing) + 1 + bins = np.linspace(dmin, dmax, nbins) + dx = bins[1] - bins[0] + dbins = ((data - bins[0]) / dx).astype(int) + binCounts = {} + + for i,j in enumerate(dbins): + c = binCounts.get(j, -1) + 1 + binCounts[j] = c + yvals[i] = c + + if bidir is True: + for i in range(nbins): + yvals[dbins==i] -= binCounts.get(i, 0) * 0.5 + + return yvals[np.argsort(inds)] ## un-shuffle values before returning + + +def _pseudoScatterExact(data, spacing=None, shuffle=True, bidir=False): + """Works by stacking points up one at a time, searching for the lowest position available at each point. + + This method produces nice, smooth results but can be prohibitively slow for large datasets. + """ inds = np.arange(len(data)) if shuffle: np.random.shuffle(inds) From cff9cfa98d174aab906d60120a8d8c50602d7423 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Jul 2019 19:00:18 -0700 Subject: [PATCH 040/235] Reduce test window size for OSX compatibility --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index bb705c18..1d831b02 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -63,7 +63,7 @@ def test_ViewBox(): assertMapping(vb, view1, size1) # test tall resize - win.resize(400, 800) + win.resize(200, 400) app.processEvents() w = vb.geometry().width() h = vb.geometry().height() From 138fdd0af2362a5dfdbdb77db083b16b2a22765e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 2 Jul 2019 03:57:45 -0700 Subject: [PATCH 041/235] relax image comparison for failing windows test (#979) relax image comparison for failing windows test --- pyqtgraph/graphicsItems/tests/test_ROI.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 8cc2efd5..b22ad530 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,3 +1,4 @@ +import sys import numpy as np import pytest import pyqtgraph as pg @@ -133,7 +134,12 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.') + # on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10 + if sys.platform == 'win32' and pg.Qt.QT_LIB in ('PyQt4', 'PySide'): + pxCount = 10 + else: + pxCount=-1 + assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount) roi.setState(initState) img1.resetTransform() From 73c440a4db6cef93fa47e8aefd7d0f502e281e7e Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Tue, 2 Jul 2019 14:01:32 +0300 Subject: [PATCH 042/235] remove deprecated call to time.clock (#980) remove deprecated call to time.clock for python3 --- pyqtgraph/ptime.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index 1de8282f..f86bdd88 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -7,22 +7,29 @@ Distributed under MIT/X11 license. See license.txt for more infomation. import sys -import time as systime + +if sys.version_info[0] < 3: + from time import clock + from time import time as system_time +else: + from time import perf_counter as clock + from time import time as system_time + START_TIME = None time = None def winTime(): """Return the current time in seconds with high precision (windows version, use Manager.time() to stay platform independent).""" - return systime.clock() + START_TIME + return clock() - START_TIME #return systime.time() def unixTime(): """Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent).""" - return systime.time() + return system_time() if sys.platform.startswith('win'): - cstart = systime.clock() ### Required to start the clock in windows - START_TIME = systime.time() - cstart + cstart = clock() ### Required to start the clock in windows + START_TIME = system_time() - cstart time = winTime else: From b0bc5b8931b8430b82e7fab4114811176272a994 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 2 Jul 2019 16:26:51 +0200 Subject: [PATCH 043/235] Add issue template (#976) * Create issue template * Refined issue template Following suggestions by ixjlyons --- .github/ISSUE_TEMPLATE/bug_report.md | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..85ea5b79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + + +### Short description + + +### Code to reproduce + +```python +import pyqtgraph as pg +import numpy as np +``` + +### Expected behavior + + +### Real behavior + + +``` +An error occurred? +Post the full traceback inside these 'code fences'! +``` + +### Tested environment(s) + + * PyQtGraph version: + * Qt Python binding: + * Python version: + * NumPy version: + * Operating system: + * Installation method: + +### Additional context From a38bdc06ded32ebd8c0581fe69bd609b3e575be3 Mon Sep 17 00:00:00 2001 From: "Daryl.Xu" Date: Wed, 10 Jul 2019 10:38:04 +0800 Subject: [PATCH 044/235] Fix typo --- doc/source/mouse_interaction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 3aea2527..c1bec45d 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -10,7 +10,7 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: * **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes visible in the scene, then right-dragging over the axis will _only_ affect that axis. * **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. * **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). * **Wheel spin:** Zooms the scene in and out. From e24d23af5c1d2a85b95f7f2c72aeb4a1a8035f70 Mon Sep 17 00:00:00 2001 From: "Daryl.Xu" Date: Wed, 10 Jul 2019 11:00:33 +0800 Subject: [PATCH 045/235] Fix typo --- doc/source/mouse_interaction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 0e149f0c..71a76415 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -10,7 +10,7 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: * Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes visible in the scene, then right-dragging over the axis will _only_ affect that axis. * Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. * Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). * Wheel spin: Zooms the scene in and out. From 6648db031e2d578fe40dcd60de14123d0f92b7e5 Mon Sep 17 00:00:00 2001 From: Kevin Newman <47572615+kevinanewman@users.noreply.github.com> Date: Wed, 17 Jul 2019 08:37:16 -0400 Subject: [PATCH 046/235] Update LegendItem.py Propose adding a clear() method (or equivalent) for easier legend re-use with dynamically updated plots... --- pyqtgraph/graphicsItems/LegendItem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index efb700a1..ce5bd883 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -101,6 +101,15 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): label.close() self.updateSize() # redraq box + def clear(self): + """ + Removes all items from the legend. + + Useful for reusing and dynamically updating charts and their legends. + """ + while self.items != []: + self.removeItem(self.items[0][1].text) + def updateSize(self): if self.size is not None: return From a655e974ff13f6993ffbf21104af9604d342d8f6 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 20 Jul 2019 20:33:11 +0200 Subject: [PATCH 047/235] Call multiprocess.connection.Connection.send_bytes with bytes --- pyqtgraph/multiprocess/remoteproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index b1077674..f0d993cb 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -458,7 +458,7 @@ class RemoteEventHandler(object): ## follow up by sending byte messages if byteData is not None: for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! - self.conn.send_bytes(obj) + self.conn.send_bytes(bytes(obj)) self.debugMsg(' sent %d byte messages', len(byteData)) self.debugMsg(' call sync: %s', callSync) From 2312c5fbfc9ca48c882b3a86299eeb10f5ef3eae Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 29 Jul 2019 17:50:38 +0200 Subject: [PATCH 048/235] Add all important and accepted PRs to changelog --- CHANGELOG | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2cb16918..4f2d4ff5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,10 @@ pyqtgraph-0.11.0 (in development) - #683: Allow data filter entries to be updated after they are created - #685: Add option to set enum default values in DataFilterWidget - #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers) + - #813,814,817: Performance improvements + - #837: Added options for field variables in ColorMapWidget + - #840, 932: Improve clipping behavior + - #922: Curve fill for fill-patches API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -76,6 +80,8 @@ pyqtgraph-0.11.0 (in development) - #589: Remove SpiralROI (this was unintentionally added in the first case) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home + - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #963: Last image in image-stack can now be selected with the z-slider Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -154,13 +160,34 @@ pyqtgraph-0.11.0 (in development) - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. + - #763: Fix OverflowError when using Auto Downsampling. + - #767: Fix Image display for images with the same value everywhere. + - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. + - #782: Fix missing FileForwarder thread termination. + - #815: Fixed mirroring of x-axis with "invert Axis" submenu. + - #824: Fix several issues related with mouse movement and GraphicsView. + - #832: Fix Permission error in tests due to unclosed filehandle. + - #836: Fix tickSpacing bug that lead to axis not being drawn. + - #861: Fix crash of PlotWidget if empty ErrorBarItem is added. + - #868: Fix segfault on repeated closing of matplotlib exporter. + - #875,876,887,934,947,980: Fix deprecation warnings. + - #886: Fix flowchart saving on python3. + - #888: Fix TreeWidget.topLevelItems in python3. + - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. + - #949: Fix multiline parameters (such as arrays) reading from config files. + - #951: Fix event firing from scale handler. + - #952: Fix RotateFree handle dragging + - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #971: Fix a segfault stemming from incorrect signal disconnection Maintenance: - Lots of new unit tests - Lots of code cleanup + - A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911) - #546: Add check for EINTR during example testing to avoid sporadic test failures on travis - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string + - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI pyqtgraph-0.10.0 From 18661cab0dfeba857a96a6eaf404f0caaddc0615 Mon Sep 17 00:00:00 2001 From: Axel Jacobsen Date: Tue, 6 Aug 2019 09:32:43 -0700 Subject: [PATCH 049/235] BarGraphItem CSV export and documentation --- doc/source/graphicsItems/bargraphitem.rst | 8 ++++++++ pyqtgraph/graphicsItems/BarGraphItem.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 doc/source/graphicsItems/bargraphitem.rst diff --git a/doc/source/graphicsItems/bargraphitem.rst b/doc/source/graphicsItems/bargraphitem.rst new file mode 100644 index 00000000..4959385b --- /dev/null +++ b/doc/source/graphicsItems/bargraphitem.rst @@ -0,0 +1,8 @@ +BarGraphItem +============ + +.. autoclass:: pyqtgraph.BarGraphItem + :members: + + .. automethod:: pyqtgraph.BarGraphItem.__init__ + diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 657222ba..4e820cb8 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -40,6 +40,7 @@ class BarGraphItem(GraphicsObject): y0=None, x1=None, y1=None, + name=None, height=None, width=None, pen=None, @@ -166,3 +167,15 @@ class BarGraphItem(GraphicsObject): if self.picture is None: self.drawPicture() return self._shape + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def name(self): + return self.opts.get('name', None) + + def getData(self): + return self.opts.get('x'), self.opts.get('height') From 27d94cae923d1371736a4b6ea650559cbf317d1f Mon Sep 17 00:00:00 2001 From: Axel Jacobsen Date: Tue, 6 Aug 2019 11:55:14 -0700 Subject: [PATCH 050/235] enforce utf-8 encoding for casting QByteArray to str remove print statements --- pyqtgraph/exporters/ImageExporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index a43a3d88..69c02508 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -48,7 +48,7 @@ class ImageExporter(Exporter): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: if QT_LIB in ['PySide', 'PySide2']: - filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] + filter = ["*."+str(f, encoding='utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] else: filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] @@ -105,7 +105,7 @@ class ImageExporter(Exporter): elif toBytes: return self.png else: - self.png.save(fileName) + return self.png.save(fileName) ImageExporter.register() From abd028436ae7b1c9d63f2c23366b26cb6bd0a55d Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Thu, 8 Aug 2019 16:50:29 +0200 Subject: [PATCH 051/235] Always convert PlotDataItem data to NumPy array --- pyqtgraph/graphicsItems/PlotDataItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 4bc9540f..bf119879 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -448,9 +448,9 @@ class PlotDataItem(GraphicsObject): if y is not None and x is None: x = np.arange(len(y)) - if isinstance(x, list): + if not isinstance(x, np.ndarray): x = np.array(x) - if isinstance(y, list): + if not isinstance(y, np.ndarray): y = np.array(y) self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by From 4e6629f3524ed80b8ead8f7adc307cf9fb95fc16 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Thu, 8 Aug 2019 17:39:11 +0200 Subject: [PATCH 052/235] Fix: ImageView sigTimeChanged was only emitted on mouse interaction --- pyqtgraph/imageview/ImageView.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 512d503b..2495d898 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -131,7 +131,7 @@ class ImageView(QtGui.QWidget): self.scene = self.ui.graphicsView.scene() self.ui.histogram.setLevelMode(levelMode) - self.ignoreTimeLine = False + self.ignorePlaying = False if view is None: self.view = ViewBox() @@ -498,11 +498,11 @@ class ImageView(QtGui.QWidget): def setCurrentIndex(self, ind): """Set the currently displayed frame index.""" - self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) - self.updateImage() - self.ignoreTimeLine = True - self.timeLine.setValue(self.tVals[self.currentIndex]) - self.ignoreTimeLine = False + index = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) + self.ignorePlaying = True + # Implicitly call timeLineChanged + self.timeLine.setValue(self.tVals[index]) + self.ignorePlaying = False def jumpFrames(self, n): """Move video frame ahead n frames (may be negative)""" @@ -696,16 +696,13 @@ class ImageView(QtGui.QWidget): return norm def timeLineChanged(self): - #(ind, time) = self.timeIndex(self.ui.timeSlider) - if self.ignoreTimeLine: - return - self.play(0) + if not self.ignorePlaying: + self.play(0) + (ind, time) = self.timeIndex(self.timeLine) if ind != self.currentIndex: self.currentIndex = ind self.updateImage() - #self.timeLine.setPos(time) - #self.emit(QtCore.SIGNAL('timeChanged'), ind, time) self.sigTimeChanged.emit(ind, time) def updateImage(self, autoHistogramRange=True): From 652ae9e64a5375b7d4e989bb379a6aeb686a299c Mon Sep 17 00:00:00 2001 From: 2xB <2xb@users.noreply.github.com> Date: Thu, 15 Aug 2019 02:41:51 +0200 Subject: [PATCH 053/235] Fix: GLScatterPlotItem and GLImageItem initializeGL only executed once --- pyqtgraph/opengl/items/GLImageItem.py | 3 +++ pyqtgraph/opengl/items/GLScatterPlotItem.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 59ddaf6f..d72448a1 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -29,8 +29,11 @@ class GLImageItem(GLGraphicsItem): GLGraphicsItem.__init__(self) self.setData(data) self.setGLOptions(glOptions) + self.texture = None def initializeGL(self): + if self.texture is not None: + return glEnable(GL_TEXTURE_2D) self.texture = glGenTextures(1) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index fe794d48..828b0f0c 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -20,6 +20,7 @@ class GLScatterPlotItem(GLGraphicsItem): self.pxMode = True #self.vbo = {} ## VBO does not appear to improve performance very much. self.setData(**kwds) + self.shader = None def setData(self, **kwds): """ @@ -54,6 +55,8 @@ class GLScatterPlotItem(GLGraphicsItem): self.update() def initializeGL(self): + if self.shader is not None: + return ## Generate texture for rendering points w = 64 From 8d2c16901b92813f8bebcd6d6cc988fd37bff9f3 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 05:16:01 +0200 Subject: [PATCH 054/235] Merge master into develop (#981) * Information is spelled with an r, even in comments --- pyqtgraph/Point.py | 2 +- pyqtgraph/Vector.py | 2 +- pyqtgraph/WidgetGroup.py | 2 +- pyqtgraph/configfile.py | 2 +- pyqtgraph/debug.py | 2 +- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/MultiPlotItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/imageview/ImageView.py | 2 +- pyqtgraph/metaarray/MetaArray.py | 2 +- pyqtgraph/pgcollections.py | 2 +- pyqtgraph/ptime.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 2 +- pyqtgraph/widgets/MultiPlotWidget.py | 2 +- pyqtgraph/widgets/PlotWidget.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 3fb43cac..3b4dacf3 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -2,7 +2,7 @@ """ Point.py - Extension of QPointF which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index 0c980a61..f2166c45 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -2,7 +2,7 @@ """ Vector.py - Extension of QVector3D which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtGui, QtCore, QT_LIB diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index 09c30854..2792aa98 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -2,7 +2,7 @@ """ WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class addresses the problem of having to save and restore the state of a large group of widgets. diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 275a4fdb..0cc8f030 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -2,7 +2,7 @@ """ configfile.py - Human-readable text configuration file library Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Used for reading and writing dictionary objects to a python-like configuration file format. Data structures may be nested and contain any data type as long diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 3ddcae37..bc6d6895 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -2,7 +2,7 @@ """ debug.py - Functions to aid in debugging Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import print_function diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6f67cfff..8ce2c404 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2,7 +2,7 @@ """ functions.py - Miscellaneous functions with no other home Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import division diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index be775d4a..065a605e 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -2,7 +2,7 @@ """ MultiPlotItem.py - Graphics item used for displaying an array of PlotItems Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from numpy import ndarray diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index fafb5592..7863dfef 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2,7 +2,7 @@ """ ROI.py - Interactive graphics items for GraphicsView (ROI widgets) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Implements a series of graphics items which display movable/scalable/rotatable shapes for use as region-of-interest markers. ROI class automatically handles extraction diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 512d503b..6affc990 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -2,7 +2,7 @@ """ ImageView.py - Widget for basic image dispay and analysis Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Widget used for displaying 2D or 3D data. Features: - float or int (including 16-bit int) image display via ImageItem diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 15d374a6..f157c588 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -2,7 +2,7 @@ """ MetaArray.py - Class encapsulating ndarray with meta data Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. MetaArray is an array class based on numpy.ndarray that allows storage of per-axis meta data such as axis values, names, units, column names, etc. It also enables several diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index ef3db258..49ed4ed6 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -2,7 +2,7 @@ """ advancedTypes.py - Basic data structures not included with python Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Includes: - OrderedDict - Dictionary which preserves the order of its elements diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index f86bdd88..eb012934 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -2,7 +2,7 @@ """ ptime.py - Precision time function made os-independent (should have been taken care of by python) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 7b8c5986..4aa2e90a 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -2,7 +2,7 @@ """ GraphicsView.py - Extension of QGraphicsView Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui, QT_LIB diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index d1f56034..21258839 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -2,7 +2,7 @@ """ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiPlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore from .GraphicsView import GraphicsView diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 6e10b13a..5208e3b3 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -2,7 +2,7 @@ """ PlotWidget.py - Convenience class--GraphicsView widget displaying a single PlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui From 08c0de768bc205a720eeebf171cbe4919b346015 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 07:32:06 +0200 Subject: [PATCH 055/235] Warn if visible GraphicsView is garbage collected (#942) * Warn if visible window is garbage collected * (Py)Qt does not rely on Python GC * Only warn if deleted widget has no parents (if it is a standalone window) * Hide windows when closing * Only implement GraphicsView.__del__ if it does not prevent circular reference garbage collection --- pyqtgraph/graphicsItems/tests/test_ImageItem.py | 2 +- pyqtgraph/graphicsItems/tests/test_ROI.py | 6 ++++-- pyqtgraph/widgets/GraphicsView.py | 9 ++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/pyqtgraph/graphicsItems/tests/test_ImageItem.py index ca197c6e..91926fe4 100644 --- a/pyqtgraph/graphicsItems/tests/test_ImageItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -121,7 +121,7 @@ def test_ImageItem(transpose=False): assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') assert img._lastDownsample == (1, 4) - view.hide() + w.hide() def test_ImageItem_axisorder(): # All image tests pass again using the opposite axis order diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index b22ad530..33a18217 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -153,6 +153,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): # allow the roi to be re-used roi.scene().removeItem(roi) + win.hide() def test_PolyLineROI(): rois = [ @@ -234,5 +235,6 @@ def test_PolyLineROI(): r.setState(initState) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') assert len(r.getState()['points']) == 3 - - \ No newline at end of file + + plt.hide() + diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 4aa2e90a..b3b921cd 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -15,6 +15,7 @@ except ImportError: from ..Point import Point import sys, os +import warnings from .FileDialog import FileDialog from ..GraphicsScene import GraphicsScene import numpy as np @@ -396,5 +397,11 @@ class GraphicsView(QtGui.QGraphicsView): def dragEnterEvent(self, ev): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events - + def _del(self): + if self.parentWidget() is None and self.isVisible(): + msg = "Visible window deleted. To prevent this, store a reference to the window object." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + +if sys.version_info[0] == 3 and sys.version_info[1] >= 4: + GraphicsView.__del__ = GraphicsView._del From cd3f7fd68e97d0d9466135fca446be5cb50734d7 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 18:55:55 +0200 Subject: [PATCH 056/235] Adding setter for GLGridItem.color (#992) * adding option to set grid color on demand * Update after setColor * Made GLGridItem color attribute private * Init GLGridItem color with fn.Color * Added docstring --- pyqtgraph/opengl/items/GLGridItem.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 0da9f61e..9dcff070 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -3,6 +3,7 @@ import numpy as np from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from ... import QtGui +from ... import functions as fn __all__ = ['GLGridItem'] @@ -13,7 +14,7 @@ class GLGridItem(GLGraphicsItem): Displays a wire-frame grid. """ - def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'): + def __init__(self, size=None, color=(255, 255, 255, 76.5), antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) self.antialias = antialias @@ -21,7 +22,7 @@ class GLGridItem(GLGraphicsItem): size = QtGui.QVector3D(20,20,1) self.setSize(size=size) self.setSpacing(1, 1, 1) - self.color = color + self.setColor(color) def setSize(self, x=None, y=None, z=None, size=None): """ @@ -53,6 +54,14 @@ class GLGridItem(GLGraphicsItem): def spacing(self): return self.__spacing[:] + def setColor(self, color): + """Set the color of the grid. Arguments are the same as those accepted by functions.mkColor()""" + self.__color = fn.Color(color) + self.update() + + def color(self): + return self.__color + def paint(self): self.setupGLState() @@ -68,7 +77,7 @@ class GLGridItem(GLGraphicsItem): xs,ys,zs = self.spacing() xvals = np.arange(-x/2., x/2. + xs*0.001, xs) yvals = np.arange(-y/2., y/2. + ys*0.001, ys) - glColor4f(*self.color) + glColor4f(*self.color().glColor()) for x in xvals: glVertex3f(x, yvals[0], 0) glVertex3f(x, yvals[-1], 0) From 05aa3e93934887da5f5abc9ba6c79619761e47b1 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 18:57:40 +0200 Subject: [PATCH 057/235] Add 'fillOutline' option to draw an outline around a filled area (#999) --- examples/histogram.py | 2 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 9 +++++++-- pyqtgraph/graphicsItems/PlotDataItem.py | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index a25f0947..85fbe3f0 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -22,7 +22,7 @@ y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) ## Using stepMode=True causes the plot to draw two lines for each sample. ## notice that len(x) == len(y)+1 -plt1.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150)) +plt1.plot(x, y, stepMode=True, fillLevel=0, fillOutline=True, brush=(0,0,255,150)) ## Now draw all points as a nicely-spaced scatter plot y = pg.pseudoScatter(vals, spacing=0.15) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 673d8334..fb3f6ea6 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -61,6 +61,7 @@ class PlotCurveItem(GraphicsObject): self.opts = { 'shadowPen': None, 'fillLevel': None, + 'fillOutline': False, 'brush': None, 'stepMode': False, 'name': None, @@ -291,7 +292,7 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None self.invalidateBounds() self.update() - + def setData(self, *args, **kargs): """ =============== ======================================================== @@ -305,6 +306,8 @@ class PlotCurveItem(GraphicsObject): :func:`mkPen ` is allowed. fillLevel (float or None) Fill the area 'under' the curve to *fillLevel* + fillOutline (bool) If True, an outline surrounding the *fillLevel* + area is drawn. brush QBrush to use when filling. Any single argument accepted by :func:`mkBrush ` is allowed. antialias (bool) Whether to use antialiasing when drawing. This @@ -394,6 +397,8 @@ class PlotCurveItem(GraphicsObject): self.setShadowPen(kargs['shadowPen']) if 'fillLevel' in kargs: self.setFillLevel(kargs['fillLevel']) + if 'fillOutline' in kargs: + self.opts['fillOutline'] = kargs['fillOutline'] if 'brush' in kargs: self.setBrush(kargs['brush']) if 'antialias' in kargs: @@ -501,7 +506,7 @@ class PlotCurveItem(GraphicsObject): p.setPen(sp) p.drawPath(path) p.setPen(cp) - if self.fillPath is not None: + if self.opts['fillOutline'] and self.fillPath is not None: p.drawPath(self.fillPath) else: p.drawPath(path) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index bf119879..172e3beb 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -67,6 +67,8 @@ class PlotDataItem(GraphicsObject): shadowPen Pen for secondary line to draw behind the primary line. disabled by default. May be any single argument accepted by :func:`mkPen() ` fillLevel Fill the area between the curve and fillLevel + fillOutline (bool) If True, an outline surrounding the *fillLevel* + area is drawn. fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` stepMode If True, two orthogonal lines are drawn for each sample @@ -154,6 +156,7 @@ class PlotDataItem(GraphicsObject): 'pen': (200,200,200), 'shadowPen': None, 'fillLevel': None, + 'fillOutline': False, 'fillBrush': None, 'stepMode': None, @@ -474,7 +477,7 @@ class PlotDataItem(GraphicsObject): def updateItems(self): curveArgs = {} - for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('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] scatterArgs = {} From d77ad273c71870881c3fda3d2b49d3488f100425 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 20:52:45 +0200 Subject: [PATCH 058/235] Fix: Item on ViewBox causes duplicate paint calls (#1017) * Fix: Item on ViewBox causes duplicate paint calls * Assure call of ViewBox.updateMatrix on resizeEvent * Fix: Disable autorange on "ViewBox.setRange" before updateAutoRange is called (Called via updateViewRange -> update -> prepareForPaint) --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 44 +++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index a542e916..6c6e3718 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -281,23 +281,10 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - - def checkSceneChange(self): - # ViewBox needs to receive sigPrepareForPaint from its scene before - # being painted. However, we have no way of being informed when the - # scene has changed in order to make this connection. The usual way - # to do this is via itemChange(), but bugs prevent this approach - # (see above). Instead, we simply check at every paint to see whether - # (the scene has changed. - scene = self.scene() - if scene == self._lastScene: - return - if self._lastScene is not None and hasattr(self._lastScene, 'sigPrepareForPaint'): - self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint) - if scene is not None and hasattr(scene, 'sigPrepareForPaint'): - scene.sigPrepareForPaint.connect(self.prepareForPaint) + + def update(self, *args, **kwargs): self.prepareForPaint() - self._lastScene = scene + GraphicsWidget.update(self, *args, **kwargs) def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) @@ -437,13 +424,20 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): self._matrixNeedsUpdate = True + self.updateMatrix() + self.linkedXChanged() self.linkedYChanged() + self.updateAutoRange() self.updateViewRange() + self._matrixNeedsUpdate = True + self.updateMatrix() + self.background.setRect(self.rect()) self.borderRect.setRect(self.rect()) + self.sigStateChanged.emit(self) self.sigResized.emit(self) self.childGroup.prepareGeometryChange() @@ -529,6 +523,14 @@ class ViewBox(GraphicsWidget): # Update axes one at a time changed = [False, False] + + # Disable auto-range for each axis that was requested to be set + if disableAutoRange: + xOff = False if setRequested[0] else None + yOff = False if setRequested[1] else None + self.enableAutoRange(x=xOff, y=yOff) + changed.append(True) + for ax, range in changes.items(): mn = min(range) mx = max(range) @@ -569,13 +571,6 @@ class ViewBox(GraphicsWidget): lockY = False self.updateViewRange(lockX, lockY) - # Disable auto-range for each axis that was requested to be set - if disableAutoRange: - xOff = False if setRequested[0] else None - yOff = False if setRequested[1] else None - self.enableAutoRange(x=xOff, y=yOff) - changed.append(True) - # If nothing has changed, we are done. if any(changed): # Update target rect for debugging @@ -1548,7 +1543,6 @@ class ViewBox(GraphicsWidget): def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return - ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() @@ -1577,8 +1571,6 @@ class ViewBox(GraphicsWidget): self.sigTransformChanged.emit(self) ## segfaults here: 1 def paint(self, p, opt, widget): - self.checkSceneChange() - if self.border is not None: bounds = self.shape() p.setPen(self.border) From b7b431de8d31c1d894ffc368ff5cd562fcc13678 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:36:34 +0200 Subject: [PATCH 059/235] FIX: Curves are automatically set visible when one is deleted (#987) * Do not automatically set all curves visible * Improved array iteration in PlotItem.updateDecimation --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 9703f286..2158b1a1 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -924,15 +924,13 @@ class PlotItem(GraphicsWidget): curves = self.curves[:] split = len(curves) - numCurves - for i in range(len(curves)): - if numCurves == -1 or i >= split: - curves[i].show() - else: + for curve in curves[split:]: + if numCurves != -1: if self.ctrl.forgetTracesCheck.isChecked(): - curves[i].clear() + curve.clear() self.removeItem(curves[i]) else: - curves[i].hide() + curve.hide() def updateAlpha(self, *args): (alpha, auto) = self.alphaState() From ff30a82298a842e823a5373f80192df4097d7e3d Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:38:05 +0200 Subject: [PATCH 060/235] Fix: colormap: Support various arguments as color (#1014) * colormap: Support various arguments as color * Using mapping for speed and consistency (suggested by @j9ac9k) --- pyqtgraph/colormap.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 585d7ea1..eb423634 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,6 +1,7 @@ import numpy as np from .Qt import QtGui, QtCore from .python2_3 import basestring +from .functions import mkColor class ColorMap(object): @@ -56,9 +57,9 @@ class ColorMap(object): =============== ============================================================== **Arguments:** pos Array of positions where each color is defined - color Array of RGBA colors. - Integer data types are interpreted as 0-255; float data types - are interpreted as 0.0-1.0 + color Array of colors. + Values are interpreted via + :func:`mkColor() `. mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG) indicating the color space that should be used when interpolating between stops. Note that the last mode value is @@ -68,7 +69,11 @@ class ColorMap(object): self.pos = np.array(pos) order = np.argsort(self.pos) self.pos = self.pos[order] - self.color = np.array(color)[order] + self.color = np.apply_along_axis( + func1d = lambda x: mkColor(x).getRgb(), + axis = -1, + arr = color, + )[order] if mode is None: mode = np.ones(len(pos)) self.mode = mode @@ -225,7 +230,7 @@ class ColorMap(object): x = np.linspace(start, stop, nPts) table = self.map(x, mode) - if not alpha: + if not alpha and mode != self.QCOLOR: return table[:,:3] else: return table From 31f1ae586bb788b2a927356c78d9bef96afc6f3e Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:43:21 +0200 Subject: [PATCH 061/235] Fix: Allow wrapped GraphicsLayoutWidget to be deleted before its wrapping Python object (#1022) --- pyqtgraph/widgets/GraphicsView.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index b3b921cd..3c553feb 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -399,9 +399,12 @@ class GraphicsView(QtGui.QGraphicsView): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events def _del(self): - if self.parentWidget() is None and self.isVisible(): - msg = "Visible window deleted. To prevent this, store a reference to the window object." - warnings.warn(msg, RuntimeWarning, stacklevel=2) + try: + if self.parentWidget() is None and self.isVisible(): + msg = "Visible window deleted. To prevent this, store a reference to the window object." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + except RuntimeError: + pass if sys.version_info[0] == 3 and sys.version_info[1] >= 4: GraphicsView.__del__ = GraphicsView._del From 3a863fff9ab711ba7db67e9ef83fc9dce77e1448 Mon Sep 17 00:00:00 2001 From: Pepijn Kenter Date: Sun, 18 Aug 2019 07:19:11 +0200 Subject: [PATCH 062/235] Fixes incorrect default value for scale parameter in makeARGB. (#793) * Fix incorrect default value for scale paremter in makeARGB. * update tests to pass with codebase change --- pyqtgraph/functions.py | 2 +- pyqtgraph/tests/test_functions.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8ce2c404..2c11b647 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1074,7 +1074,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # Decide on maximum scaled value if scale is None: if lut is not None: - scale = lut.shape[0] - 1 + scale = lut.shape[0] else: scale = 255. diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 68f3dc24..e013fe42 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -206,15 +206,15 @@ def test_makeARGB(): # lut smaller than maxint lut = np.arange(128).astype(np.uint8) im2, alpha = pg.makeARGB(im1, lut=lut) - checkImage(im2, np.linspace(0, 127, 256).astype('ubyte'), alpha, False) + checkImage(im2, np.linspace(0, 127.5, 256, dtype='ubyte'), alpha, False) # lut + levels lut = np.arange(256)[::-1].astype(np.uint8) im2, alpha = pg.makeARGB(im1, lut=lut, levels=[-128, 384]) - checkImage(im2, np.linspace(192, 65.5, 256).astype('ubyte'), alpha, False) + checkImage(im2, np.linspace(191.5, 64.5, 256, dtype='ubyte'), alpha, False) im2, alpha = pg.makeARGB(im1, lut=lut, levels=[64, 192]) - checkImage(im2, np.clip(np.linspace(385.5, -126.5, 256), 0, 255).astype('ubyte'), alpha, False) + checkImage(im2, np.clip(np.linspace(384.5, -127.5, 256), 0, 255).astype('ubyte'), alpha, False) # uint8 data + uint16 LUT lut = np.arange(4096)[::-1].astype(np.uint16) // 16 From 80f8af2432efb5a20437cb91ff448713aeb0d474 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 19:17:34 +0200 Subject: [PATCH 063/235] Use Qt5 QWheelEvent functions if necessary (#924) * Make QWheelEvent code consistently compatible with Qt5 * Add documentation * Removed old TODO message * Init remote QWheelEvent only with relative position, minor code simplifications * RemoteGraphicsView Renderer assumes to be at (0,0) * Orientation serialized as boolean --- pyqtgraph/widgets/GraphicsView.py | 11 +++- pyqtgraph/widgets/RemoteGraphicsView.py | 79 +++++++++++++++++++++---- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 3c553feb..1be1a274 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -327,7 +327,16 @@ class GraphicsView(QtGui.QGraphicsView): if not self.mouseEnabled: ev.ignore() return - sc = 1.001 ** ev.delta() + + delta = 0 + if QT_LIB in ['PyQt4', 'PySide']: + delta = ev.delta() + else: + delta = ev.angleDelta().x() + if delta == 0: + delta = ev.angleDelta().y() + + sc = 1.001 ** delta #self.scale *= sc #self.updateMatrix() self.scale(sc, sc) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index edf4db3c..9be1b531 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -8,6 +8,56 @@ import numpy as np import mmap, tempfile, ctypes, atexit, sys, random __all__ = ['RemoteGraphicsView'] + +class SerializableWheelEvent: + """ + Contains all information of a QWheelEvent, is serializable and can generate QWheelEvents. + + Methods have the functionality of their QWheelEvent equivalent. + """ + def __init__(self, _pos, _globalPos, _delta, _buttons, _modifiers, _orientation): + self._pos = _pos + self._globalPos = _globalPos + self._delta = _delta + self._buttons = _buttons + self._modifiers = _modifiers + self._orientation_vertical = _orientation == QtCore.Qt.Vertical + + def pos(self): + return self._pos + + def globalPos(self): + return self._globalPos + + def delta(self): + return self._delta + + def orientation(self): + if self._orientation_vertical: + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + def angleDelta(self): + if self._orientation_vertical: + return QtCore.QPoint(0, self._delta) + else: + return QtCore.QPoint(self._delta, 0) + + def buttons(self): + return QtCore.Qt.MouseButtons(self._buttons) + + def modifiers(self): + return QtCore.Qt.KeyboardModifiers(self._modifiers) + + def toQWheelEvent(self): + """ + Generate QWheelEvent from SerializableWheelEvent. + """ + if QT_LIB in ['PyQt4', 'PySide']: + return QtGui.QWheelEvent(self.pos(), self.globalPos(), self.delta(), self.buttons(), self.modifiers(), self.orientation()) + else: + return QtGui.QWheelEvent(self.pos(), self.globalPos(), QtCore.QPoint(), self.angleDelta(), self.delta(), self.orientation(), self.buttons(), self.modifiers()) class RemoteGraphicsView(QtGui.QWidget): """ @@ -97,22 +147,34 @@ class RemoteGraphicsView(QtGui.QWidget): p.end() def mousePressEvent(self, ev): - self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mousePressEvent(self, ev) def mouseReleaseEvent(self, ev): - self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseReleaseEvent(self, ev) def mouseMoveEvent(self, ev): - self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseMoveEvent(self, ev) def wheelEvent(self, ev): - self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), int(ev.orientation()), _callSync='off') + delta = 0 + orientation = QtCore.Qt.Horizontal + if QT_LIB in ['PyQt4', 'PySide']: + delta = ev.delta() + orientation = ev.orientation() + else: + delta = ev.angleDelta().x() + if delta == 0: + orientation = QtCore.Qt.Vertical + delta = ev.angleDelta().y() + + serializableEvent = SerializableWheelEvent(ev.pos(), ev.pos(), delta, int(ev.buttons()), int(ev.modifiers()), orientation) + self._view.wheelEvent(serializableEvent, _callSync='off') ev.accept() return QtGui.QWidget.wheelEvent(self, ev) @@ -251,12 +313,9 @@ class Renderer(GraphicsView): btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) - - def wheelEvent(self, pos, gpos, d, btns, mods, ori): - btns = QtCore.Qt.MouseButtons(btns) - mods = QtCore.Qt.KeyboardModifiers(mods) - ori = (None, QtCore.Qt.Horizontal, QtCore.Qt.Vertical)[ori] - return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) + + def wheelEvent(self, ev): + return GraphicsView.wheelEvent(self, ev.toQWheelEvent()) def keyEvent(self, typ, mods, text, autorep, count): typ = QtCore.QEvent.Type(typ) From fd11e1352d9e0b15d67520354bbbf40c615bf996 Mon Sep 17 00:00:00 2001 From: Paul Debus Date: Sun, 18 Aug 2019 21:16:31 +0200 Subject: [PATCH 064/235] fix encoding error in checkOpenGLVersion (#787) --- pyqtgraph/opengl/GLViewWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 1a70d735..f123b151 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -427,7 +427,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def checkOpenGLVersion(self, msg): ## Only to be called from within exception handler. ver = glGetString(GL_VERSION).split()[0] - if int(ver.split('.')[0]) < 2: + if int(ver.split(b'.')[0]) < 2: from .. import debug debug.printExc() raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) From 982fb3427a68e9aaf0021a3fb60fa36c680a7209 Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 29 Jul 2019 17:50:38 +0200 Subject: [PATCH 065/235] Add all important and accepted PRs to changelog --- CHANGELOG | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2cb16918..4f2d4ff5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,10 @@ pyqtgraph-0.11.0 (in development) - #683: Allow data filter entries to be updated after they are created - #685: Add option to set enum default values in DataFilterWidget - #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers) + - #813,814,817: Performance improvements + - #837: Added options for field variables in ColorMapWidget + - #840, 932: Improve clipping behavior + - #922: Curve fill for fill-patches API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -76,6 +80,8 @@ pyqtgraph-0.11.0 (in development) - #589: Remove SpiralROI (this was unintentionally added in the first case) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home + - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #963: Last image in image-stack can now be selected with the z-slider Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -154,13 +160,34 @@ pyqtgraph-0.11.0 (in development) - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. + - #763: Fix OverflowError when using Auto Downsampling. + - #767: Fix Image display for images with the same value everywhere. + - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. + - #782: Fix missing FileForwarder thread termination. + - #815: Fixed mirroring of x-axis with "invert Axis" submenu. + - #824: Fix several issues related with mouse movement and GraphicsView. + - #832: Fix Permission error in tests due to unclosed filehandle. + - #836: Fix tickSpacing bug that lead to axis not being drawn. + - #861: Fix crash of PlotWidget if empty ErrorBarItem is added. + - #868: Fix segfault on repeated closing of matplotlib exporter. + - #875,876,887,934,947,980: Fix deprecation warnings. + - #886: Fix flowchart saving on python3. + - #888: Fix TreeWidget.topLevelItems in python3. + - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. + - #949: Fix multiline parameters (such as arrays) reading from config files. + - #951: Fix event firing from scale handler. + - #952: Fix RotateFree handle dragging + - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #971: Fix a segfault stemming from incorrect signal disconnection Maintenance: - Lots of new unit tests - Lots of code cleanup + - A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911) - #546: Add check for EINTR during example testing to avoid sporadic test failures on travis - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string + - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI pyqtgraph-0.10.0 From 614c4d05293d91f71e6107df197941c7b7c04049 Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 19 Aug 2019 13:27:16 +0200 Subject: [PATCH 066/235] Added newly merged PRs from the last 3 weeks. --- CHANGELOG | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4f2d4ff5..89c8eb13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,7 @@ pyqtgraph-0.11.0 (in development) - #837: Added options for field variables in ColorMapWidget - #840, 932: Improve clipping behavior - #922: Curve fill for fill-patches + - #996: Allow the update of LegendItem API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -81,7 +82,11 @@ pyqtgraph-0.11.0 (in development) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #942: If the visible GraphicsView is garbage collected, a warning is issued. - #963: Last image in image-stack can now be selected with the z-slider + - #992: Added a setter for GlGridItem.color. + - #999: Make outline around fillLevel optional. + - #1014: Enable various arguments as color in colormap. Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -164,6 +169,8 @@ pyqtgraph-0.11.0 (in development) - #767: Fix Image display for images with the same value everywhere. - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. - #782: Fix missing FileForwarder thread termination. + - #787: Fix encoding errors in checkOpenGLVersion. + - #793: Fix wrong default scaling in makeARGB - #815: Fixed mirroring of x-axis with "invert Axis" submenu. - #824: Fix several issues related with mouse movement and GraphicsView. - #832: Fix Permission error in tests due to unclosed filehandle. @@ -173,12 +180,21 @@ pyqtgraph-0.11.0 (in development) - #875,876,887,934,947,980: Fix deprecation warnings. - #886: Fix flowchart saving on python3. - #888: Fix TreeWidget.topLevelItems in python3. + - #924: Fix QWheelEvent in RemoteGraphicsView with pyqt5. - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. - #949: Fix multiline parameters (such as arrays) reading from config files. - #951: Fix event firing from scale handler. - #952: Fix RotateFree handle dragging - #968: Fix Si units in AxisItem leading to an incorrect unit. - - #971: Fix a segfault stemming from incorrect signal disconnection + - #971: Fix a segfault stemming from incorrect signal disconnection. + - #974: Fix recursion error when instancing CtrlNode. + - #987: Fix visibility reset when PlotItems are removed. + - #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8. + - #1010: Fix matplotlib/CSV export. + - #1015: Iterators are now converted to NumPy arrays. + - #1016: Fix synchronisation of multiple ImageViews with time axis. + - #1017: Fix duplicate paint calls emitted by Items on ViewBox. + - #1019: Fix disappearing GLGridItems when PlotItems are removed and readded. Maintenance: - Lots of new unit tests From 584c4516f0c4386ba01edf6fee2b1f392f88cfaf Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Thu, 29 Aug 2019 13:56:25 -0700 Subject: [PATCH 067/235] Expand CI + pre-commit (#991) * Initial attempt at extra checks in CI land * Adding flake8 config * Adding pre-commit configuration and explanation in CONTRIBUTING.md --- .flake8 | 49 ++++++++ .pre-commit-config.yaml | 11 ++ CONTRIBUTING.md | 16 ++- azure-pipelines.yml | 82 ++++++++++-- azure-test-template.yml | 93 +++++++------- examples/test_examples.py | 157 ++++++++++++++++++----- setup.py | 7 +- tools/setupHelpers.py | 256 ++++++++++++++++++++++---------------- 8 files changed, 469 insertions(+), 202 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..0556c925 --- /dev/null +++ b/.flake8 @@ -0,0 +1,49 @@ +[flake8] +exclude = .git,.tox,__pycache__,doc,old,build,dist +show_source = True +statistics = True +verbose = 2 +select = + E101, + E112, + E122, + E125, + E133, + E223, + E224, + E242, + E273, + E274, + E901, + E902, + W191, + W601, + W602, + W603, + W604, + E124, + E231, + E211, + E261, + E271, + E272, + E304, + F401, + F402, + F403, + F404, + E501, + E502, + E702, + E703, + E711, + E712, + E721, + F811, + F812, + F821, + F822, + F823, + F831, + F841, + W292 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c2f8f9a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + sha: master + hooks: + - id: check-added-large-files + args: ['--maxkb=100'] + - id: check-case-conflict + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: mixed-line-ending + args: [--fix=lf] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d602b89b..9af2e508 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to PyQtGraph -Contributions to pyqtgraph are welcome! +Contributions to pyqtgraph are welcome! Please use the following guidelines when preparing changes: @@ -13,11 +13,13 @@ Please use the following guidelines when preparing changes: ## Documentation -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. * Documentation is generated with sphinx; please check that docstring changes compile correctly ## Style guidelines +### Rules + * PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. * Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8. * Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt @@ -33,9 +35,15 @@ Please use the following guidelines when preparing changes: ============== ======================================================== ``` - QObject subclasses that implement new signals should also describe + QObject subclasses that implement new signals should also describe these in a similar table. - + +### Pre-Commit + +PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to ensure it conforms to various standards, such as `flake8`, utf-8 encoding pragma, line-ending fixers, and so on. If any of the checks fail, the commit will be rejected, and you will have the opportunity to make the necessary fixes before adding and committing a file again. This ensures that every commit made conforms to (most) of the styling standards that the library enforces; and you will most likely pass the code style checks by the CI. + +To make use of `pre-commit`, have it available in your `$PATH` and run `pre-commit install` from the root directory of PyQtGraph. + ## Testing Setting up a test environment ### Dependencies diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b91f515a..657189f8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,3 @@ -############################################################################################ -# This config was rectrieved in no small part from https://github.com/slaclab/pydm -############################################################################################ - trigger: branches: include: @@ -20,19 +16,83 @@ pr: variables: OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' + DEFAULT_MERGE_BRANCH: 'develop' -jobs: - - template: azure-test-template.yml - parameters: - name: Linux +stages: +- stage: "pre_test" + jobs: + - job: check_diff_size + pool: vmImage: 'Ubuntu 16.04' + steps: + - bash: | + git config --global advice.detachedHead false + mkdir ~/repo-clone && cd ~/repo-clone + git init + git remote add -t $(Build.SourceBranchName) origin $(Build.Repository.Uri) + git remote add -t ${DEFAULT_MERGE_BRANCH} upstream https://github.com/${OFFICIAL_REPO}.git + + git fetch origin $(Build.SourceBranchName) + git fetch upstream ${DEFAULT_MERGE_BRANCH} + + git checkout $(Build.SourceBranchName) + MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` + echo -e "Merge Size ${MERGE_SIZE}" + + git checkout ${DEFAULT_MERGE_BRANCH} + TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` + echo -e "Target Size ${TARGET_SIZE}" + + if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then + SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`; + else + SIZE_DIFF=0; + fi; + echo -e "Estimated content size difference = ${SIZE_DIFF} kB" && + test ${SIZE_DIFF} -lt 100; + displayName: 'Diff Size Check' + continueOnError: true + + - job: "style_check" + pool: + vmImage: "Ubuntu 16.04" + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: 3.7 + - bash: | + pip install flake8 + python setup.py style + displayName: 'flake8 check' + continueOnError: true + + - job: "build_wheel" + pool: + vmImage: 'Ubuntu 16.04' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: 3.7 + - script: | + python -m pip install setuptools wheel + python setup.py bdist_wheel --universal + displayName: "Build Python Wheel" + continueOnError: false + - publish: dist + artifact: wheel + +- stage: "test" + jobs: - template: azure-test-template.yml parameters: - name: Windows + name: linux + vmImage: 'Ubuntu 16.04' + - template: azure-test-template.yml + parameters: + name: windows vmImage: 'vs2017-win2016' - - template: azure-test-template.yml parameters: - name: MacOS + name: macOS vmImage: 'macOS-10.13' diff --git a/azure-test-template.yml b/azure-test-template.yml index c1e6c1b0..81e4399c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -26,16 +26,22 @@ jobs: python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" - Python37-PyQt-5.12: + Python37-PyQt-5.13: python.version: '3.7' qt.bindings: "PyQt5" install.method: "pip" - Python37-PySide2-5.12: + Python37-PySide2-5.13: python.version: "3.7" qt.bindings: "PySide2" install.method: "pip" steps: + - task: DownloadPipelineArtifact@2 + inputs: + source: 'current' + artifact: wheel + path: 'dist' + - task: ScreenResolutionUtility@1 inputs: displaySettings: 'specific' @@ -43,6 +49,11 @@ jobs: height: '1080' condition: eq(variables['agent.os'], 'Windows_NT' ) + - task: UsePythonVersion@0 + inputs: + versionSpec: $(python.version) + condition: eq(variables['install.method'], 'pip') + - script: | curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe 7z x mesa3d-19.1.0-release-msvc.exe @@ -60,75 +71,71 @@ jobs: displayName: "Install Windows-Mesa OpenGL DLL" condition: eq(variables['agent.os'], 'Windows_NT') - - task: UsePythonVersion@0 - inputs: - versionSpec: $(python.version) - condition: eq(variables['install.method'], 'pip') - - bash: | if [ $(agent.os) == 'Linux' ] then - echo '##vso[task.prependpath]/usr/share/miniconda/bin' + echo "##vso[task.prependpath]$CONDA/bin" + if [ $(python.version) == '2.7' ] + then + echo "Grabbing Older Miniconda" + wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-Linux-x86_64.sh -O Miniconda.sh + bash Miniconda.sh -b -p $CONDA -f + fi elif [ $(agent.os) == 'Darwin' ] then - echo '##vso[task.prependpath]$CONDA/bin' - sudo install -d -m 0777 /usr/local/miniconda/envs + sudo chown -R $USER $CONDA + echo "##vso[task.prependpath]$CONDA/bin" + if [ $(python.version) == '2.7' ] + then + echo "Grabbing Older Miniconda" + wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-MacOSX-x86_64.sh -O Miniconda.sh + bash Miniconda.sh -b -p $CONDA -f + fi elif [ $(agent.os) == 'Windows_NT' ] then - echo "##vso[task.prependpath]$env:CONDA\Scripts" + echo "##vso[task.prependpath]$CONDA/Scripts" else echo 'Just what OS are you using?' fi - displayName: 'Add Conda to $PATH' + displayName: 'Add Conda To $PATH' condition: eq(variables['install.method'], 'conda' ) - - task: CondaEnvironment@0 - displayName: 'Create Conda Environment' - condition: eq(variables['install.method'], 'conda') - inputs: - environmentName: 'test-environment-$(python.version)' - packageSpecs: 'python=$(python.version)' - updateConda: false - - bash: | if [ $(install.method) == "conda" ] then + conda create --name test-environment-$(python.version) python=$(python.version) --yes + echo "Conda Info:" + conda info + echo "Installing qt-bindings" source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest six coverage --yes --quiet + + if [ $(agent.os) == "Linux" ] && [ $(python.version) == "2.7" ] + then + conda install $(qt.bindings) --yes + else + conda install -c conda-forge $(qt.bindings) --yes + fi + echo "Installing remainder of dependencies" + conda install -c conda-forge numpy scipy six pyopengl --yes else - pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage + pip install $(qt.bindings) numpy scipy pyopengl six fi - pip install pytest-xdist pytest-cov + echo "" + pip install pytest pytest-xdist pytest-cov coverage if [ $(python.version) == "2.7" ] then - pip install pytest-faulthandler + pip install pytest-faulthandler==1.6.0 export PYTEST_ADDOPTS="--faulthandler-timeout=15" fi displayName: "Install Dependencies" - + - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) fi - pip install setuptools wheel - python setup.py bdist_wheel - pip install dist/*.whl - displayName: 'Build Wheel and Install' - - - task: CopyFiles@2 - inputs: - contents: 'dist/**' - targetFolder: $(Build.ArtifactStagingDirectory) - cleanTargetFolder: true - displayName: "Copy Binary Wheel Distribution To Artifacts" - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Binary Wheel' - condition: always() - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory)/dist - artifactName: Distributions + python -m pip install --no-index --find-links=dist pyqtgraph + displayName: 'Install Wheel' - bash: | sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm diff --git a/examples/test_examples.py b/examples/test_examples.py index bb4682f1..c6fef377 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils @@ -5,7 +6,6 @@ from collections import namedtuple import errno import importlib import itertools -import pkgutil import pytest import os, sys import subprocess @@ -41,7 +41,12 @@ if os.getenv('TRAVIS') is not None: files = sorted(set(utils.buildFileList(utils.examples))) -frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} +frontends = { + Qt.PYQT4: False, + Qt.PYQT5: False, + Qt.PYSIDE: False, + Qt.PYSIDE2: False +} # sort out which of the front ends are available for frontend in frontends.keys(): try: @@ -50,48 +55,136 @@ for frontend in frontends.keys(): except ImportError: pass -installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) +installedFrontends = sorted([ + frontend for frontend, isPresent in frontends.items() if isPresent +]) exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) -conditionalExampleTests = { - "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), - "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), - "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671"), - 'GLVolumeItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLIsosurface.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLSurfacePlot.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLScatterPlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLshaders.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLLinePlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLMeshItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLImageItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939") +conditionalExamples = { + "hdf5.py": exceptionCondition( + False, + reason="Example requires user interaction" + ), + "RemoteSpeedTest.py": exceptionCondition( + False, + reason="Test is being problematic on CI machines" + ), + "optics_demos.py": exceptionCondition( + not frontends[Qt.PYSIDE], + reason=( + "Test fails due to PySide bug: ", + "https://bugreports.qt.io/browse/PYSIDE-671" + ) + ), + 'GLVolumeItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLIsosurface.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLSurfacePlot.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLScatterPlotItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLshaders.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLLinePlotItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLMeshItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLImageItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ) } @pytest.mark.parametrize( - "frontend, f", - [ - pytest.param( - frontend, + "frontend, f", + [ + pytest.param( + frontend, f, - marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, - reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), - ) - for frontend, f, in itertools.product(installedFrontends, files) - ], - ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)] + marks=pytest.mark.skipif( + conditionalExamples[f[1]].condition is False, + reason=conditionalExamples[f[1]].reason + ) if f[1] in conditionalExamples.keys() else (), + ) + for frontend, f, in itertools.product(installedFrontends, files) + ], + ids = [ + " {} - {} ".format(f[1], frontend) + for frontend, f in itertools.product( + installedFrontends, + files + ) + ] ) def testExamples(frontend, f, graphicsSystem=None): # runExampleFile(f[0], f[1], sys.executable, frontend) name, file = f global path - fn = os.path.join(path,file) + fn = os.path.join(path, file) os.chdir(path) sys.stdout.write("{} ".format(name)) sys.stdout.flush() import1 = "import %s" % frontend if frontend != '' else '' import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + graphicsSystem = ( + '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + ) code = """ try: %s @@ -123,7 +216,7 @@ except: stderr=subprocess.PIPE, stdout=subprocess.PIPE) process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? + process.stdin.close() output = '' fail = False while True: @@ -146,10 +239,14 @@ except: process.kill() #res = process.communicate() res = (process.stdout.read(), process.stderr.read()) - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + if (fail or + 'exception' in res[1].decode().lower() or + 'error' in res[1].decode().lower()): print(res[0].decode()) print(res[1].decode()) - pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) + pytest.fail("{}\n{}\nFailed {} Example Test Located in {} " + .format(res[0].decode(), res[1].decode(), name, file), + pytrace=False) if __name__ == "__main__": pytest.cmdline.main() diff --git a/setup.py b/setup.py index a59f7dd5..38ee477a 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ +# -*- coding: utf-8 -*- DESCRIPTION = """\ -PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and +PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PyQt5/PySide/PySide2 and numpy. It is intended for use in mathematics / scientific / engineering applications. @@ -12,14 +13,13 @@ setupOpts = dict( name='pyqtgraph', description='Scientific Graphics and GUI Library for Python', long_description=DESCRIPTION, - license='MIT', + license = 'MIT', url='http://www.pyqtgraph.org', author='Luke Campagnola', author_email='luke.campagnola@gmail.com', classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", @@ -145,4 +145,3 @@ setup( ], **setupOpts ) - diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 4afec66b..6ebbfa46 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -10,14 +10,15 @@ except ImportError: output = proc.stdout.read() proc.wait() if proc.returncode != 0: - ex = Exception("Process had nonzero return value %d" % proc.returncode) + ex = Exception("Process had nonzero return value " + + "%d " % proc.returncode) ex.returncode = proc.returncode ex.output = output raise ex return output # Maximum allowed repository size difference (in kB) following merge. -# This is used to prevent large files from being inappropriately added to +# This is used to prevent large files from being inappropriately added to # the repository history. MERGE_SIZE_LIMIT = 100 @@ -42,19 +43,19 @@ FLAKE_MANDATORY = set([ 'E901', # SyntaxError or IndentationError 'E902', # IOError - + 'W191', # indentation contains tabs - + 'W601', # .has_key() is deprecated, use ‘in’ 'W602', # deprecated form of raising exception 'W603', # ‘<>’ is deprecated, use ‘!=’ - 'W604', # backticks are deprecated, use ‘repr()’ + 'W604', # backticks are deprecated, use ‘repr()’ ]) FLAKE_RECOMMENDED = set([ 'E124', # closing bracket does not match visual indentation 'E231', # missing whitespace after ‘,’ - + 'E211', # whitespace before ‘(‘ 'E261', # at least two spaces before inline comment 'E271', # multiple spaces after keyword @@ -65,10 +66,10 @@ FLAKE_RECOMMENDED = set([ 'F402', # import module from line N shadowed by loop variable 'F403', # ‘from module import *’ used; unable to detect undefined names 'F404', # future import(s) name after other statements - + 'E501', # line too long (82 > 79 characters) 'E502', # the backslash is redundant between brackets - + 'E702', # multiple statements on one line (semicolon) 'E703', # statement ends with a semicolon 'E711', # comparison to None should be ‘if cond is None:’ @@ -82,7 +83,7 @@ FLAKE_RECOMMENDED = set([ 'F823', # local variable name ... referenced before assignment 'F831', # duplicate argument name in function definition 'F841', # local variable name is assigned to but never used - + 'W292', # no newline at end of file ]) @@ -93,7 +94,7 @@ FLAKE_OPTIONAL = set([ 'E126', # continuation line over-indented for hanging indent 'E127', # continuation line over-indented for visual indent 'E128', # continuation line under-indented for visual indent - + 'E201', # whitespace after ‘(‘ 'E202', # whitespace before ‘)’ 'E203', # whitespace before ‘:’ @@ -105,19 +106,19 @@ FLAKE_OPTIONAL = set([ 'E228', # missing whitespace around modulo operator 'E241', # multiple spaces after ‘,’ 'E251', # unexpected spaces around keyword / parameter equals - 'E262', # inline comment should start with ‘# ‘ - + 'E262', # inline comment should start with ‘# ‘ + 'E301', # expected 1 blank line, found 0 'E302', # expected 2 blank lines, found 0 'E303', # too many blank lines (3) - + 'E401', # multiple imports on one line 'E701', # multiple statements on one line (colon) - + 'W291', # trailing whitespace 'W293', # blank line contains whitespace - + 'W391', # blank line at end of file ]) @@ -128,23 +129,10 @@ FLAKE_IGNORE = set([ ]) -#def checkStyle(): - #try: - #out = check_output(['flake8', '--select=%s' % FLAKE_TESTS, '--statistics', 'pyqtgraph/']) - #ret = 0 - #print("All style checks OK.") - #except Exception as e: - #out = e.output - #ret = e.returncode - #print(out.decode('utf-8')) - #return ret - - def checkStyle(): """ Run flake8, checking only lines that are modified since the last git commit. """ - test = [ 1,2,3 ] - + # First check _all_ code against mandatory error codes print('flake8: check all code against mandatory error set...') errors = ','.join(FLAKE_MANDATORY) @@ -154,39 +142,47 @@ def checkStyle(): output = proc.stdout.read().decode('utf-8') ret = proc.wait() printFlakeOutput(output) - + # Check for DOS newlines print('check line endings in all files...') count = 0 allowedEndings = set([None, '\n']) for path, dirs, files in os.walk('.'): + if path.startswith("." + os.path.sep + ".tox"): + continue for f in files: if os.path.splitext(f)[1] not in ('.py', '.rst'): continue filename = os.path.join(path, f) fh = open(filename, 'U') - x = fh.readlines() - endings = set(fh.newlines if isinstance(fh.newlines, tuple) else (fh.newlines,)) + _ = fh.readlines() + endings = set( + fh.newlines + if isinstance(fh.newlines, tuple) + else (fh.newlines,) + ) endings -= allowedEndings if len(endings) > 0: - print("\033[0;31m" + "File has invalid line endings: %s" % filename + "\033[0m") + print("\033[0;31m" + + "File has invalid line endings: " + + "%s" % filename + "\033[0m") ret = ret | 2 count += 1 print('checked line endings in %d files' % count) - - + + # Next check new code with optional error codes print('flake8: check new code against recommended error set...') diff = subprocess.check_output(['git', 'diff']) - proc = subprocess.Popen(['flake8', '--diff', #'--show-source', + proc = subprocess.Popen(['flake8', '--diff', # '--show-source', '--ignore=' + errors], - stdin=subprocess.PIPE, + stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(diff) proc.stdin.close() output = proc.stdout.read().decode('utf-8') ret |= printFlakeOutput(output) - + if ret == 0: print('style test passed.') else: @@ -244,14 +240,20 @@ def unitTests(): return ret -def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, targetRepo=None): +def checkMergeSize( + sourceBranch=None, + targetBranch=None, + sourceRepo=None, + targetRepo=None +): """ - Check that a git merge would not increase the repository size by MERGE_SIZE_LIMIT. + Check that a git merge would not increase the repository size by + MERGE_SIZE_LIMIT. """ if sourceBranch is None: sourceBranch = getGitBranch() sourceRepo = '..' - + if targetBranch is None: if sourceBranch == 'develop': targetBranch = 'develop' @@ -259,38 +261,38 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target else: targetBranch = 'develop' targetRepo = '..' - + workingDir = '__merge-test-clone' - env = dict(TARGET_BRANCH=targetBranch, - SOURCE_BRANCH=sourceBranch, - TARGET_REPO=targetRepo, + env = dict(TARGET_BRANCH=targetBranch, + SOURCE_BRANCH=sourceBranch, + TARGET_REPO=targetRepo, SOURCE_REPO=sourceRepo, WORKING_DIR=workingDir, ) - + print("Testing merge size difference:\n" " SOURCE: {SOURCE_REPO} {SOURCE_BRANCH}\n" " TARGET: {TARGET_BRANCH} {TARGET_REPO}".format(**env)) - + setup = """ mkdir {WORKING_DIR} && cd {WORKING_DIR} && git init && git remote add -t {TARGET_BRANCH} target {TARGET_REPO} && - git fetch target {TARGET_BRANCH} && - git checkout -qf target/{TARGET_BRANCH} && + git fetch target {TARGET_BRANCH} && + git checkout -qf target/{TARGET_BRANCH} && git gc -q --aggressive """.format(**env) - + checkSize = """ - cd {WORKING_DIR} && + cd {WORKING_DIR} && du -s . | sed -e "s/\t.*//" """.format(**env) - + merge = """ cd {WORKING_DIR} && - git pull -q {SOURCE_REPO} {SOURCE_BRANCH} && + git pull -q {SOURCE_REPO} {SOURCE_BRANCH} && git gc -q --aggressive """.format(**env) - + try: print("Check out target branch:\n" + setup) check_call(setup, shell=True) @@ -300,13 +302,17 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target check_call(merge, shell=True) mergeSize = int(check_output(checkSize, shell=True)) print("MERGE SIZE: %d kB" % mergeSize) - + diff = mergeSize - targetSize if diff <= MERGE_SIZE_LIMIT: print("DIFFERENCE: %d kB [OK]" % diff) return 0 else: - print("\033[0;31m" + "DIFFERENCE: %d kB [exceeds %d kB]" % (diff, MERGE_SIZE_LIMIT) + "\033[0m") + print("\033[0;31m" + + "DIFFERENCE: %d kB [exceeds %d kB]" % ( + diff, + MERGE_SIZE_LIMIT) + + "\033[0m") return 2 finally: if os.path.isdir(workingDir): @@ -327,7 +333,11 @@ def mergeTests(): def listAllPackages(pkgroot): path = os.getcwd() n = len(path.split(os.path.sep)) - subdirs = [i[0].split(os.path.sep)[n:] for i in os.walk(os.path.join(path, pkgroot)) if '__init__.py' in i[2]] + subdirs = [ + i[0].split(os.path.sep)[n:] + for i in os.walk(os.path.join(path, pkgroot)) + if '__init__.py' in i[2] + ] return ['.'.join(p) for p in subdirs] @@ -338,48 +348,61 @@ def getInitVersion(pkgroot): init = open(initfile).read() m = re.search(r'__version__ = (\S+)\n', init) if m is None or len(m.groups()) != 1: - raise Exception("Cannot determine __version__ from init file: '%s'!" % initfile) + raise Exception("Cannot determine __version__ from init file: " + + "'%s'!" % initfile) version = m.group(1).strip('\'\"') return version def gitCommit(name): """Return the commit ID for the given name.""" - commit = check_output(['git', 'show', name], universal_newlines=True).split('\n')[0] + commit = check_output( + ['git', 'show', name], + universal_newlines=True).split('\n')[0] assert commit[:7] == 'commit ' return commit[7:] def getGitVersion(tagPrefix): """Return a version string with information about this git checkout. - If the checkout is an unmodified, tagged commit, then return the tag version. - If this is not a tagged commit, return the output of ``git describe --tags``. + If the checkout is an unmodified, tagged commit, then return the tag + version + + If this is not a tagged commit, return the output of + ``git describe --tags`` + If this checkout has been modified, append "+" to the version. """ path = os.getcwd() if not os.path.isdir(os.path.join(path, '.git')): return None - - v = check_output(['git', 'describe', '--tags', '--dirty', '--match=%s*'%tagPrefix]).strip().decode('utf-8') - + + v = check_output(['git', + 'describe', + '--tags', + '--dirty', + '--match=%s*'%tagPrefix]).strip().decode('utf-8') + # chop off prefix assert v.startswith(tagPrefix) v = v[len(tagPrefix):] # split up version parts parts = v.split('-') - + # has working tree been modified? modified = False if parts[-1] == 'dirty': modified = True parts = parts[:-1] - + # have commits been added on top of last tagged version? # (git describe adds -NNN-gXXXXXXX if this is the case) local = None - if len(parts) > 2 and re.match(r'\d+', parts[-2]) and re.match(r'g[0-9a-f]{7}', parts[-1]): + if (len(parts) > 2 and + re.match(r'\d+', parts[-2]) and + re.match(r'g[0-9a-f]{7}', parts[-1])): local = parts[-1] parts = parts[:-2] - + gitVersion = '-'.join(parts) if local is not None: gitVersion += '+' + local @@ -389,7 +412,10 @@ def getGitVersion(tagPrefix): return gitVersion def getGitBranch(): - m = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)) + m = re.search( + r'\* (.*)', + check_output(['git', 'branch'], + universal_newlines=True)) if m is None: return '' else: @@ -397,32 +423,33 @@ def getGitBranch(): def getVersionStrings(pkg): """ - Returns 4 version strings: - + Returns 4 version strings: + * the version string to use for this build, * version string requested with --force-version (or None) * version string that describes the current git checkout (or None). - * version string in the pkg/__init__.py, - + * version string in the pkg/__init__.py, + The first return value is (forceVersion or gitVersion or initVersion). """ - + ## Determine current version string from __init__.py initVersion = getInitVersion(pkgroot=pkg) - ## If this is a git checkout, try to generate a more descriptive version string + # If this is a git checkout + # try to generate a more descriptive version string try: gitVersion = getGitVersion(tagPrefix=pkg+'-') except: gitVersion = None - sys.stderr.write("This appears to be a git checkout, but an error occurred " - "while attempting to determine a version string for the " - "current commit.\n") + sys.stderr.write("This appears to be a git checkout, but an error " + "occurred while attempting to determine a version " + "string for the current commit.\n") sys.excepthook(*sys.exc_info()) # See whether a --force-version flag was given forcedVersion = None - for i,arg in enumerate(sys.argv): + for i, arg in enumerate(sys.argv): if arg.startswith('--force-version'): if arg == '--force-version': forcedVersion = sys.argv[i+1] @@ -431,8 +458,8 @@ def getVersionStrings(pkg): elif arg.startswith('--force-version='): forcedVersion = sys.argv[i].replace('--force-version=', '') sys.argv.pop(i) - - + + ## Finally decide on a version string to use: if forcedVersion is not None: version = forcedVersion @@ -443,7 +470,8 @@ def getVersionStrings(pkg): _, local = gitVersion.split('+') if local != '': version = version + '+' + local - sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version) + sys.stderr.write("Detected git commit; " + + "will use version string: '%s'\n" % version) return version, forcedVersion, gitVersion, initVersion @@ -457,29 +485,31 @@ class DebCommand(Command): maintainer = "Luke Campagnola " debTemplate = "debian" debDir = "deb_build" - + user_options = [] - + def initialize_options(self): self.cwd = None - + def finalize_options(self): self.cwd = os.getcwd() - + def run(self): version = self.distribution.get_version() pkgName = self.distribution.get_name() debName = "python-" + pkgName debDir = self.debDir - - assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd - + + assert os.getcwd() == self.cwd, 'Must be in package root: ' + + '%s' % self.cwd + if os.path.isdir(debDir): raise Exception('DEB build dir already exists: "%s"' % debDir) sdist = "dist/%s-%s.tar.gz" % (pkgName, version) if not os.path.isfile(sdist): - raise Exception("No source distribution; run `setup.py sdist` first.") - + raise Exception("No source distribution; " + + "run `setup.py sdist` first.") + # copy sdist to build directory and extract os.mkdir(debDir) renamedSdist = '%s_%s.orig.tar.gz' % (debName, version) @@ -489,16 +519,20 @@ class DebCommand(Command): if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: raise Exception("Error extracting source distribution.") buildDir = '%s/%s-%s' % (debDir, pkgName, version) - + # copy debian control structure print("copytree %s => %s" % (self.debTemplate, buildDir+'/debian')) shutil.copytree(self.debTemplate, buildDir+'/debian') - + # Write new changelog - chlog = generateDebianChangelog(pkgName, 'CHANGELOG', version, self.maintainer) + chlog = generateDebianChangelog( + pkgName, + 'CHANGELOG', + version, + self.maintainer) print("write changelog %s" % buildDir+'/debian/changelog') open(buildDir+'/debian/changelog', 'w').write(chlog) - + # build package print('cd %s; debuild -us -uc' % buildDir) if os.system('cd %s; debuild -us -uc' % buildDir) != 0: @@ -521,43 +555,45 @@ class DebugCommand(Command): class TestCommand(Command): - description = "Run all package tests and exit immediately with informative return code." + description = "Run all package tests and exit immediately with ", \ + "informative return code." user_options = [] - + def run(self): sys.exit(unitTests()) - + def initialize_options(self): pass - + def finalize_options(self): pass - + class StyleCommand(Command): - description = "Check all code for style, exit immediately with informative return code." + description = "Check all code for style, exit immediately with ", \ + "informative return code." user_options = [] - + def run(self): sys.exit(checkStyle()) - + def initialize_options(self): pass - + def finalize_options(self): pass - + class MergeTestCommand(Command): - description = "Run all tests needed to determine whether the current code is suitable for merge." + description = "Run all tests needed to determine whether the current ",\ + "code is suitable for merge." user_options = [] - + def run(self): sys.exit(mergeTests()) - + def initialize_options(self): pass - + def finalize_options(self): pass - From bbc11b96a9b3089ec23366a4afde5b804bc804cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Makovi=C4=8Dka?= Date: Thu, 12 Sep 2019 23:50:43 +0200 Subject: [PATCH 068/235] Configurable GridItem tick spacing and pen color (#101) --- pyqtgraph/graphicsItems/GridItem.py | 123 ++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 87f90a62..0b1eb525 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -3,6 +3,7 @@ from .UIGraphicsItem import * import numpy as np from ..Point import Point from .. import functions as fn +from .. import getConfigOption __all__ = ['GridItem'] class GridItem(UIGraphicsItem): @@ -12,16 +13,75 @@ class GridItem(UIGraphicsItem): Displays a rectangular grid of lines indicating major divisions within a coordinate system. Automatically determines what divisions to use. """ - - def __init__(self): + + def __init__(self, pen='default', textPen='default'): UIGraphicsItem.__init__(self) #QtGui.QGraphicsItem.__init__(self, *args) #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - + + self.opts = {} + + self.setPen(pen) + self.setTextPen(textPen) + self.setTickSpacing(x=[None, None, None], y=[None, None, None]) + + + def setPen(self, *args, **kwargs): + """Set the pen used to draw the grid.""" + if kwargs == {} and (args == () or args == ('default',)): + self.opts['pen'] = fn.mkPen(getConfigOption('foreground')) + else: + self.opts['pen'] = fn.mkPen(*args, **kwargs) + self.picture = None - - + self.update() + + + def setTextPen(self, *args, **kwargs): + """Set the pen used to draw the texts.""" + if kwargs == {} and (args == () or args == ('default',)): + self.opts['textPen'] = fn.mkPen(getConfigOption('foreground')) + else: + if args == (None,): + self.opts['textPen'] = None + else: + self.opts['textPen'] = fn.mkPen(*args, **kargs) + + self.picture = None + self.update() + + + def setTickSpacing(self, x=None, y=None): + """ + Set the grid tick spacing to use. + + Tick spacing for each axis shall be specified as an array of + descending values, one for each tick scale. When the value + is set to None, grid line distance is chosen automatically + for this particular level. + + Example: + Default setting of 3 scales for each axis: + setTickSpacing(x=[None, None, None], y=[None, None, None]) + + Single scale with distance of 1.0 for X axis, Two automatic + scales for Y axis: + setTickSpacing(x=[1.0], y=[None, None]) + + Single scale with distance of 1.0 for X axis, Two scales + for Y axis, one with spacing of 1.0, other one automatic: + setTickSpacing(x=[1.0], y=[1.0, None]) + """ + self.opts['tickSpacing'] = (x or self.opts['tickSpacing'][0], + y or self.opts['tickSpacing'][1]) + + self.grid_depth = max([len(s) for s in self.opts['tickSpacing']]) + + self.picture = None + self.update() + + def viewRangeChanged(self): UIGraphicsItem.viewRangeChanged(self) self.picture = None @@ -48,7 +108,6 @@ class GridItem(UIGraphicsItem): p = QtGui.QPainter() p.begin(self.picture) - dt = fn.invertQTransform(self.viewTransform()) vr = self.getViewWidget().rect() unit = self.pixelWidth(), self.pixelHeight() dim = [vr.width(), vr.height()] @@ -62,10 +121,22 @@ class GridItem(UIGraphicsItem): x = ul[1] ul[1] = br[1] br[1] = x - for i in [2,1,0]: ## Draw three different scales of grid + + lastd = [None, None] + for i in range(self.grid_depth - 1, -1, -1): dist = br-ul nlTarget = 10.**i + d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) + for ax in range(0,2): + ts = self.opts['tickSpacing'][ax] + try: + if ts[i] is not None: + d[ax] = ts[i] + except IndexError: + pass + lastd[ax] = d[ax] + ul1 = np.floor(ul / d) * d br1 = np.ceil(br / d) * d dist = br1-ul1 @@ -76,12 +147,25 @@ class GridItem(UIGraphicsItem): #print " d", d #print " nl", nl for ax in range(0,2): ## Draw grid for both axes + if i >= len(self.opts['tickSpacing'][ax]): + continue + if d[ax] < lastd[ax]: + continue + ppl = dim[ax] / nl[ax] - c = np.clip(3.*(ppl-3), 0., 30.) - linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) - textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) - #linePen.setCosmetic(True) - #linePen.setWidth(1) + c = np.clip(5.*(ppl-3), 0., 50.) + + linePen = self.opts['pen'] + lineColor = self.opts['pen'].color() + lineColor.setAlpha(c) + linePen.setColor(lineColor) + + textPen = self.opts['textPen'] + if textPen is not None: + textColor = self.opts['textPen'].color() + textColor.setAlpha(c * 2) + textPen.setColor(textColor) + bx = (ax+1) % 2 for x in range(0, int(nl[ax])): linePen.setCosmetic(False) @@ -102,8 +186,7 @@ class GridItem(UIGraphicsItem): if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]): continue p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) - if i < 2: - p.setPen(textPen) + if i < 2 and textPen is not None: if ax == 0: x = p1[0] + unit[0] y = ul[1] + unit[1] * 8. @@ -114,7 +197,13 @@ class GridItem(UIGraphicsItem): tr = self.deviceTransform() #tr.scale(1.5, 1.5) p.setWorldTransform(fn.invertQTransform(tr)) - for t in texts: - x = tr.map(t[0]) + Point(0.5, 0.5) - p.drawText(x, t[1]) + + if textPen is not None and len(texts) > 0: + # if there is at least one text, then c is set + textColor.setAlpha(c * 2) + p.setPen(QtGui.QPen(textColor)) + for t in texts: + x = tr.map(t[0]) + Point(0.5, 0.5) + p.drawText(x, t[1]) + p.end() From e3884ebd20fd6580633e01ba141a6af200abc90e Mon Sep 17 00:00:00 2001 From: miranis <33010847+miranis@users.noreply.github.com> Date: Fri, 13 Sep 2019 05:24:48 +0200 Subject: [PATCH 069/235] Update GraphicsScene.py (#599) In lines 174 and 191 cev[0] is being accessed when cev is an empty list. I get this error when inheriting from GraphicsLayoutWidget and overloading mouseDoubleClickEvent. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 01b6b808..785031d5 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -183,12 +183,14 @@ class GraphicsScene(QtGui.QGraphicsScene): if int(ev.buttons() & btn) == 0: continue if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet - cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] - dist = Point(ev.scenePos() - cev.scenePos()).length() - if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime): - continue - init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True - self.dragButtons.append(int(btn)) + cev = [e for e in self.clickEvents if int(e.button()) == int(btn)] + if cev: + cev = cev[0] + dist = Point(ev.scenePos() - cev.scenePos()).length() + if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime): + continue + init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True + self.dragButtons.append(int(btn)) ## If we have dragged buttons, deliver a drag event if len(self.dragButtons) > 0: @@ -208,10 +210,11 @@ class GraphicsScene(QtGui.QGraphicsScene): self.dragButtons.remove(ev.button()) else: cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())] - if self.sendClickEvent(cev[0]): - #print "sent click event" - ev.accept() - self.clickEvents.remove(cev[0]) + if cev: + if self.sendClickEvent(cev[0]): + #print "sent click event" + ev.accept() + self.clickEvents.remove(cev[0]) if int(ev.buttons()) == 0: self.dragItem = None From 8309b5301480a623c7bf4c70fba12a47979b1bf0 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:00:38 +0200 Subject: [PATCH 070/235] Fix: Reset ParentItem to None on removing from PlotItem/ViewBox (#1031) --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 2158b1a1..f3849b99 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -563,8 +563,8 @@ class PlotItem(GraphicsWidget): if item in self.dataItems: self.dataItems.remove(item) - if item.scene() is not None: - self.vb.removeItem(item) + self.vb.removeItem(item) + if item in self.curves: self.curves.remove(item) self.updateDecimation() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6c6e3718..9c71d7db 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -399,10 +399,12 @@ 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) self.updateAutoRange() @@ -413,7 +415,12 @@ class ViewBox(GraphicsWidget): self.addedItems.remove(item) except: pass - self.scene().removeItem(item) + + scene = self.scene() + if scene is not None: + scene.removeItem(item) + item.setParentItem(None) + self.updateAutoRange() def clear(self): From bfd36dc2038d77cf33ec5bef0c6a5fcb6948c9bf Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:30:39 +0200 Subject: [PATCH 071/235] Prevent element-wise string comparison (code by @flutefreak7) (#1024) --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 1758bb4d..b05c2f70 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -488,7 +488,7 @@ class ImageItem(GraphicsObject): step = (step, step) stepData = self.image[::step[0], ::step[1]] - if 'auto' == bins: + if isinstance(bins, str) and bins == 'auto': mn = np.nanmin(stepData) mx = np.nanmax(stepData) if mx == mn: From 061a30e827a85fa20dbf94a141e67bf48d19aff7 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:58:49 +0200 Subject: [PATCH 072/235] Correctly include SI units for log AxisItems (#972) --- pyqtgraph/graphicsItems/AxisItem.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3a92e2b1..088ba6b8 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -444,7 +444,11 @@ class AxisItem(GraphicsWidget): def updateAutoSIPrefix(self): if self.label.isVisible(): - (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale))) + if self.logMode: + _range = 10**np.array(self.range) + else: + _range = self.range + (scale, prefix) = fn.siScale(max(abs(_range[0]*self.scale), abs(_range[1]*self.scale))) if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. scale = 1.0 prefix = '' @@ -771,7 +775,7 @@ class AxisItem(GraphicsWidget): return strings def logTickStrings(self, values, scale, spacing): - return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] + return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] def generateDrawSpecs(self, p): """ From 3edbef6c57702fc77acf2d60980c74a58ab84fc5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 9 Jul 2016 20:25:52 -0400 Subject: [PATCH 073/235] Ensure exported images use integer dimensions. It seems that the parameter tree doesn't enforce the int type very strongly. Also, use some local variables more often. --- pyqtgraph/exporters/ImageExporter.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 69c02508..e600afc9 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -58,17 +58,17 @@ class ImageExporter(Exporter): filter.insert(0, p) self.fileSaveDialog(filter=filter) return - - targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) - sourceRect = self.getSourceRect() - - - #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) - #self.png.fill(pyqtgraph.mkColor(self.params['background'])) - w, h = self.params['width'], self.params['height'] + + w = int(self.params['width']) + h = int(self.params['height']) if w == 0 or h == 0: - raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) - bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte) + raise Exception("Cannot export image with size=0 (requested " + "export size is %dx%d)" % (w, h)) + + targetRect = QtCore.QRect(0, 0, w, h) + sourceRect = self.getSourceRect() + + bg = np.empty((h, w, 4), dtype=np.ubyte) color = self.params['background'] bg[:,:,0] = color.blue() bg[:,:,1] = color.green() From d726a9693ec73c863b761fa9cadaac6ba32de971 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2019 00:27:29 -0400 Subject: [PATCH 074/235] Fix UnboundLocalError in VideoSpeedTest. --- examples/VideoSpeedTest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index f123ccc3..7131f9d1 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -103,6 +103,7 @@ def mkData(): dt = np.float loc = 1.0 scale = 0.1 + mx = 1.0 if ui.rgbCheck.isChecked(): data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale) From 2e900898904ba1ec903d83c4d24114f4dc3b34e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2019 00:28:19 -0400 Subject: [PATCH 075/235] Fix undefined reduce call. --- pyqtgraph/opengl/items/GLBarGraphItem.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/opengl/items/GLBarGraphItem.py b/pyqtgraph/opengl/items/GLBarGraphItem.py index b3060dc9..42d05fb7 100644 --- a/pyqtgraph/opengl/items/GLBarGraphItem.py +++ b/pyqtgraph/opengl/items/GLBarGraphItem.py @@ -8,7 +8,7 @@ class GLBarGraphItem(GLMeshItem): pos is (...,3) array of the bar positions (the corner of each bar) size is (...,3) array of the sizes of each bar """ - nCubes = reduce(lambda a,b: a*b, pos.shape[:-1]) + nCubes = np.prod(pos.shape[:-1]) cubeVerts = np.mgrid[0:2,0:2,0:2].reshape(3,8).transpose().reshape(1,8,3) cubeFaces = np.array([ [0,1,2], [3,2,1], @@ -22,8 +22,5 @@ class GLBarGraphItem(GLMeshItem): verts = cubeVerts * size + pos faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1) md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3)) - - GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) - - \ No newline at end of file + GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) From c94b1cb99eeb49475ce759f7485727c5f2165c2f Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 13 Sep 2019 22:04:48 -0700 Subject: [PATCH 076/235] Always update transform when setting angle of a TextItem (#970) * Always update transform when setting angle of a TextItem * Add test to check TextItem.setAngle * Relax test a bit but still check that setAngle has an effect * Add docstring to setAngle * Remove unneeded numpy testing function imports --- pyqtgraph/graphicsItems/TextItem.py | 15 ++++++++---- .../graphicsItems/tests/test_TextItem.py | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_TextItem.py diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index b2587ded..9dc17960 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -110,9 +110,16 @@ class TextItem(GraphicsObject): self.updateTextPos() def setAngle(self, angle): + """ + Set the angle of the text in degrees. + + This sets the rotation angle of the text as a whole, measured + counter-clockwise from the x axis of the parent. Note that this rotation + angle does not depend on horizontal/vertical scaling of the parent. + """ self.angle = angle - self.updateTransform() - + self.updateTransform(force=True) + def setAnchor(self, anchor): self.anchor = Point(anchor) self.updateTextPos() @@ -169,7 +176,7 @@ class TextItem(GraphicsObject): p.setRenderHint(p.Antialiasing, True) p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect())) - def updateTransform(self): + def updateTransform(self, force=False): # update transform such that this item has the correct orientation # and scaling relative to the scene, but inherits its position from its # parent. @@ -181,7 +188,7 @@ class TextItem(GraphicsObject): else: pt = p.sceneTransform() - if pt == self._lastTransform: + if not force and pt == self._lastTransform: return t = pt.inverted()[0] diff --git a/pyqtgraph/graphicsItems/tests/test_TextItem.py b/pyqtgraph/graphicsItems/tests/test_TextItem.py new file mode 100644 index 00000000..6667dfc5 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_TextItem.py @@ -0,0 +1,23 @@ +import pytest +import pyqtgraph as pg + +app = pg.mkQApp() + + +def test_TextItem_setAngle(): + plt = pg.plot() + plt.setXRange(-10, 10) + plt.setYRange(-20, 20) + item = pg.TextItem(text="test") + plt.addItem(item) + + t1 = item.transform() + + item.setAngle(30) + app.processEvents() + + t2 = item.transform() + + assert t1 != t2 + assert not t1.isRotating() + assert t2.isRotating() From 8c137a1caf30bdf8f25ece4859a19bcfcceb88be Mon Sep 17 00:00:00 2001 From: Aikhjarto Date: Sat, 14 Sep 2019 07:12:23 +0200 Subject: [PATCH 077/235] fix: circular texture was slightly off-center (#1012) --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 828b0f0c..636c1621 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -61,7 +61,7 @@ class GLScatterPlotItem(GLGraphicsItem): ## Generate texture for rendering points w = 64 def fn(x,y): - r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 + r = ((x-(w-1)/2.)**2 + (y-(w-1)/2.)**2) ** 0.5 return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) pData = np.empty((w, w, 4)) pData[:] = 255 From df28c41d4b92f868cab1ae021ac1fd4e146182f5 Mon Sep 17 00:00:00 2001 From: lidstrom83 Date: Fri, 13 Sep 2019 23:08:28 -0700 Subject: [PATCH 078/235] Make DockArea compatible with Qt Designer (#158) --- pyqtgraph/dockarea/DockArea.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index b7b0659e..ff3f22ab 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -9,9 +9,9 @@ from ..python2_3 import basestring class DockArea(Container, QtGui.QWidget, DockDrop): - def __init__(self, temporary=False, home=None): + def __init__(self, parent=None, temporary=False, home=None): Container.__init__(self, self) - QtGui.QWidget.__init__(self) + QtGui.QWidget.__init__(self, parent=parent) DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom']) self.layout = QtGui.QVBoxLayout() self.layout.setContentsMargins(0,0,0,0) From b7f1aa88cd69dcf6685171efc48e0f1abfb863eb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 22 Sep 2019 17:03:30 -0700 Subject: [PATCH 079/235] Cast to int after division --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fb3f6ea6..20db0abe 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore try: from ..Qt import QtOpenGL @@ -570,7 +571,7 @@ class PlotCurveItem(GraphicsObject): gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1]) + gl.glDrawArrays(gl.GL_LINE_STRIP, 0, int(pos.size / pos.shape[-1])) finally: gl.glDisableClientState(gl.GL_VERTEX_ARRAY) finally: @@ -638,4 +639,3 @@ class ROIPlotItem(PlotCurveItem): def roiChangedEvent(self): d = self.getRoiData() self.updateData(d, self.xVals) - From ed264802a2d9e7cab0846e75a7a028e332e99def Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 23 Sep 2019 21:19:01 -0700 Subject: [PATCH 080/235] Raise AttributeError in __getattr__ --- pyqtgraph/graphicsWindows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index b6598685..f1315005 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -48,7 +48,7 @@ class TabWindow(QtGui.QMainWindow): if hasattr(self.cw, attr): return getattr(self.cw, attr) else: - raise NameError(attr) + raise AttributeError(attr) class PlotWindow(PlotWidget): From b57d45df6de3eaafcb0b50017d3d8badb6d2f05e Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 23 Sep 2019 21:25:52 -0700 Subject: [PATCH 081/235] The lazier way --- pyqtgraph/graphicsWindows.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index f1315005..b6a321ee 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -45,10 +45,7 @@ class TabWindow(QtGui.QMainWindow): self.show() def __getattr__(self, attr): - if hasattr(self.cw, attr): - return getattr(self.cw, attr) - else: - raise AttributeError(attr) + return getattr(self.cw, attr) class PlotWindow(PlotWidget): From aa3a5d39958291d13f69f5fa78ffd9b698780937 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Thu, 26 Sep 2019 20:08:43 +0300 Subject: [PATCH 082/235] remote legacy work-around for old numpy errors (#1046) * remote legacy work-around for old numpy errors * forgot to remove the numpy_fix import * require numyp >= 1.8.0 --- pyqtgraph/__init__.py | 3 --- pyqtgraph/numpy_fix.py | 22 ---------------------- setup.py | 2 +- 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 pyqtgraph/numpy_fix.py diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index b1aa98aa..bdb4fe15 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -29,9 +29,6 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] ## helpers for 2/3 compatibility from . import python2_3 -## install workarounds for numpy bugs -from . import numpy_fix - ## in general openGL is poorly supported with Qt+GraphicsView. ## we only enable it where the performance benefit is critical. ## Note this only applies to 2D graphics; 3D graphics always use OpenGL. diff --git a/pyqtgraph/numpy_fix.py b/pyqtgraph/numpy_fix.py deleted file mode 100644 index 2fa8ef1f..00000000 --- a/pyqtgraph/numpy_fix.py +++ /dev/null @@ -1,22 +0,0 @@ -try: - import numpy as np - - ## Wrap np.concatenate to catch and avoid a segmentation fault bug - ## (numpy trac issue #2084) - if not hasattr(np, 'concatenate_orig'): - np.concatenate_orig = np.concatenate - def concatenate(vals, *args, **kwds): - """Wrapper around numpy.concatenate (see pyqtgraph/numpy_fix.py)""" - dtypes = [getattr(v, 'dtype', None) for v in vals] - names = [getattr(dt, 'names', None) for dt in dtypes] - if len(dtypes) < 2 or all([n is None for n in names]): - return np.concatenate_orig(vals, *args, **kwds) - if any([dt != dtypes[0] for dt in dtypes[1:]]): - raise TypeError("Cannot concatenate structured arrays of different dtype.") - return np.concatenate_orig(vals, *args, **kwds) - - np.concatenate = concatenate - -except ImportError: - pass - diff --git a/setup.py b/setup.py index 38ee477a..aa1bb787 100644 --- a/setup.py +++ b/setup.py @@ -141,7 +141,7 @@ setup( package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source package_data={'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg']}, install_requires = [ - 'numpy', + 'numpy>=1.8.0', ], **setupOpts ) From 96a4270a30b0fb273b1ae916a7ab9373ced77690 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 27 Sep 2019 22:02:54 +0200 Subject: [PATCH 083/235] Fix HistogramLUTWidget with background parameter (#953) * Fix HistogramLUTWidget with background parameter HistogramLUTWidget cannot be initialized with the `background` parameter, because all parameters are also passed to the constructor of HistogramLUTItem which does not have a `background` parameter. This pull request fixes that issue by defining `background` explicitly as parameter in the function header. Closes #175 * Added test for HistogramLUTWidget initialization with background * Fixed Python2 compatibility * Do not pg.exit() after test * Moved test_histogramlutwidget to widget tests --- pyqtgraph/widgets/HistogramLUTWidget.py | 2 +- .../widgets/tests/test_histogramlutwidget.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 pyqtgraph/widgets/tests/test_histogramlutwidget.py diff --git a/pyqtgraph/widgets/HistogramLUTWidget.py b/pyqtgraph/widgets/HistogramLUTWidget.py index 9aec837c..5259900c 100644 --- a/pyqtgraph/widgets/HistogramLUTWidget.py +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -13,7 +13,7 @@ __all__ = ['HistogramLUTWidget'] class HistogramLUTWidget(GraphicsView): def __init__(self, parent=None, *args, **kargs): - background = kargs.get('background', 'default') + background = kargs.pop('background', 'default') GraphicsView.__init__(self, parent, useOpenGL=False, background=background) self.item = HistogramLUTItem(*args, **kargs) self.setCentralItem(self.item) diff --git a/pyqtgraph/widgets/tests/test_histogramlutwidget.py b/pyqtgraph/widgets/tests/test_histogramlutwidget.py new file mode 100644 index 00000000..f8a381a7 --- /dev/null +++ b/pyqtgraph/widgets/tests/test_histogramlutwidget.py @@ -0,0 +1,44 @@ +""" +HistogramLUTWidget test: + +Tests the creation of a HistogramLUTWidget. +""" + +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui +import numpy as np + +def testHistogramLUTWidget(): + pg.mkQApp() + + win = QtGui.QMainWindow() + win.show() + + cw = QtGui.QWidget() + win.setCentralWidget(cw) + + l = QtGui.QGridLayout() + cw.setLayout(l) + l.setSpacing(0) + + v = pg.GraphicsView() + vb = pg.ViewBox() + vb.setAspectLocked() + v.setCentralItem(vb) + l.addWidget(v, 0, 0, 3, 1) + + w = pg.HistogramLUTWidget(background='w') + l.addWidget(w, 0, 1) + + data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0)) + for i in range(32): + for j in range(32): + data[i*8, j*8] += .1 + img = pg.ImageItem(data) + vb.addItem(img) + vb.autoRange() + + w.setImageItem(img) + + QtGui.QApplication.processEvents() + From 071e4295357d6cb0cd7c2c2a1cc50a2ba8547c16 Mon Sep 17 00:00:00 2001 From: Mi! Date: Fri, 27 Sep 2019 22:31:47 +0200 Subject: [PATCH 084/235] makeRGBA/ImageItem: Applying alpha mask on numpy.nan data values (#406) * Applying alpha mask on numpy.nan data values * Typesafe, checking for `data.dtype.kind` --- pyqtgraph/functions.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 2c11b647..5cbb177e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -78,7 +78,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): pref = SI_PREFIXES_ASCII[m+8] p = .001**m - return (p, pref) + return (p, pref) def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): @@ -1035,7 +1035,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ============== ================================================================================== """ profile = debug.Profiler() - if data.ndim not in (2, 3): raise TypeError("data must be 2D or 3D") if data.ndim == 3 and data.shape[2] > 4: @@ -1083,7 +1082,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): dtype = np.ubyte else: dtype = np.min_scalar_type(lut.shape[0]-1) - + + # awkward, but fastest numpy native nan evaluation + # + nanMask = None + if data.dtype.kind == 'f' and np.isnan(data.min()): + nanMask = np.isnan(data) # Apply levels if given if levels is not None: if isinstance(levels, np.ndarray) and levels.ndim == 2: @@ -1106,10 +1110,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal == maxVal: maxVal = np.nextafter(maxVal, 2*maxVal) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) - profile() - # apply LUT if given if lut is not None: data = applyLookupTable(data, lut) @@ -1152,7 +1154,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): imgData[..., 3] = 255 else: alpha = True - + + # apply nan mask through alpha channel + if nanMask is not None: + alpha = True + imgData[nanMask, 3] = 0 + profile() return imgData, alpha From 61ec73a741ebee50c1ba8cf55bbb4ea9e7b685bb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 27 Sep 2019 13:37:40 -0700 Subject: [PATCH 085/235] Close windows at the end of test functions (#1042) * Close windows at the end of test functions * Can't show window deletion warning during interpreter shutdown starting --- pyqtgraph/exporters/tests/test_svg.py | 1 + pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 ++ pyqtgraph/graphicsItems/tests/test_AxisItem.py | 2 ++ pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py | 2 ++ pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py | 3 ++- pyqtgraph/graphicsItems/tests/test_PlotDataItem.py | 2 ++ pyqtgraph/widgets/GraphicsView.py | 6 +++++- 7 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_svg.py b/pyqtgraph/exporters/tests/test_svg.py index 2261f7df..62946368 100644 --- a/pyqtgraph/exporters/tests/test_svg.py +++ b/pyqtgraph/exporters/tests/test_svg.py @@ -28,6 +28,7 @@ def test_plotscene(): ex.export(fileName=tempfilename) # clean up after the test is done os.unlink(tempfilename) + w.close() def test_simple(): tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 1d831b02..5a8ca141 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -71,6 +71,8 @@ def test_ViewBox(): size1 = QRectF(0, h, w, -h) assertMapping(vb, view1, size1) + win.close() + skipreason = "Skipping this test until someone has time to fix it." @pytest.mark.skipif(True, reason=skipreason) diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py index f076890d..22dccdb4 100644 --- a/pyqtgraph/graphicsItems/tests/test_AxisItem.py +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -28,3 +28,5 @@ def test_AxisItem_stopAxisAtTick(monkeypatch): monkeypatch.setattr(left, "drawPicture", test_left) plot.show() + app.processEvents() + plot.close() diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py index 4ee25e45..2b922c1e 100644 --- a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -35,3 +35,5 @@ def test_ErrorBarItem_defer_data(): r_clear_ebi = plot.viewRect() assert r_clear_ebi == r_no_ebi + + plot.close() diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py index a3c34b11..6d60d3e1 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -27,7 +27,8 @@ def test_PlotCurveItem(): c.setData(data, connect=np.array([1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,0])) assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.") - + + p.close() if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index adc525d9..894afc74 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -86,3 +86,5 @@ def test_clipping(): assert xDisp[0] <= vr.left() assert xDisp[-1] >= vr.right() + + w.close() diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 1be1a274..86b43222 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -411,7 +411,11 @@ class GraphicsView(QtGui.QGraphicsView): try: if self.parentWidget() is None and self.isVisible(): msg = "Visible window deleted. To prevent this, store a reference to the window object." - warnings.warn(msg, RuntimeWarning, stacklevel=2) + try: + warnings.warn(msg, RuntimeWarning, stacklevel=2) + except TypeError: + # warnings module not available during interpreter shutdown + pass except RuntimeError: pass From ed6586c7ddd3ad95eadd40cb8fda7c4c1d4c746b Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 30 Sep 2019 18:15:03 +0200 Subject: [PATCH 086/235] Removed unnecessary enlargement of bounding box (#1048) --- pyqtgraph/graphicsItems/InfiniteLine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 7aeb1620..36505026 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -314,8 +314,8 @@ class InfiniteLine(GraphicsObject): length = br.width() left = br.left() + length * self.span[0] right = br.left() + length * self.span[1] - br.setLeft(left - w) - br.setRight(right + w) + br.setLeft(left) + br.setRight(right) br = br.normalized() vs = self.getViewBox().size() From f2740f7e69087f462b457336b081217ad82cd29e Mon Sep 17 00:00:00 2001 From: Agamemnon Krasoulis Date: Tue, 22 Oct 2019 17:45:45 +0100 Subject: [PATCH 087/235] Fix typo in documentation (#1062) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 20db0abe..05b11b4d 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -273,7 +273,7 @@ class PlotCurveItem(GraphicsObject): self.update() def setShadowPen(self, *args, **kargs): - """Set the shadow pen used to draw behind tyhe primary pen. + """Set the shadow pen used to draw behind the primary pen. This pen must have a larger width than the primary pen to be visible. """ From a84953530f2090917c77442709887acfaa9730f0 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 28 Oct 2019 13:59:20 +0100 Subject: [PATCH 088/235] Fix: setEnableMenu in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9c71d7db..a12eb519 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -324,7 +324,8 @@ class ViewBox(GraphicsWidget): if self.state['enableMenu'] and self.menu is None: self.menu = ViewBoxMenu(self) self.updateViewLists() - else: + elif not self.state['enableMenu'] and self.menu is not None: + self.menu.setParent(None) self.menu = None self.updateViewRange() @@ -380,11 +381,10 @@ class ViewBox(GraphicsWidget): def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu - if enableMenu: - if self.menu is None: - self.menu = ViewBoxMenu(self) - self.updateViewLists() - else: + if enableMenu and self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + elif not enableMenu and self.menu is not None: self.menu.setParent(None) self.menu = None self.sigStateChanged.emit(self) From cb4d9b23b2144e6b23b8022c973f249c7c855586 Mon Sep 17 00:00:00 2001 From: wuyuanyi135 Date: Sun, 3 Nov 2019 00:36:58 -0400 Subject: [PATCH 089/235] fix flowchart context menu redundant menu (#1060) --- pyqtgraph/flowchart/Flowchart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 5aeeac38..2e7ed0eb 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -834,9 +834,9 @@ class FlowchartWidget(dockarea.DockArea): def buildMenu(self, pos=None): def buildSubMenu(node, rootMenu, subMenus, pos=None): for section, node in node.items(): - menu = QtGui.QMenu(section) - rootMenu.addMenu(menu) - if isinstance(node, OrderedDict): + if isinstance(node, OrderedDict): + menu = QtGui.QMenu(section) + rootMenu.addMenu(menu) buildSubMenu(node, menu, subMenus, pos=pos) subMenus.append(menu) else: From b1df230964ce5b9bdc7140b888939f0c12b40376 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sun, 3 Nov 2019 07:51:20 +0300 Subject: [PATCH 090/235] Remove 'global' for CONFIG_OPTIONS because it is redundant for dict (#1055) --- pyqtgraph/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index bdb4fe15..aad5c3c8 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -64,7 +64,6 @@ CONFIG_OPTIONS = { def setConfigOption(opt, value): - global CONFIG_OPTIONS if opt not in CONFIG_OPTIONS: raise KeyError('Unknown configuration option "%s"' % opt) if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'): From 684882455773f410e07c0dd16977e5696edaf6ce Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Sun, 3 Nov 2019 06:00:06 +0100 Subject: [PATCH 091/235] add bookkeeping exporter parameters (#1023) --- pyqtgraph/GraphicsScene/exportDialog.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 8085c5bf..045698fe 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -23,6 +23,8 @@ class ExportDialog(QtGui.QWidget): self.currentExporter = None self.scene = scene + self.exporterParameters = {} + self.selectBox = QtGui.QGraphicsRectItem() self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine)) self.selectBox.hide() @@ -121,7 +123,18 @@ class ExportDialog(QtGui.QWidget): return expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) - params = exp.parameters() + + if prev: + oldtext = str(prev.text()) + self.exporterParameters[oldtext] = self.currentExporter.parameters() + newtext = str(item.text()) + if newtext in self.exporterParameters.keys(): + params = self.exporterParameters[newtext] + exp.params = params + else: + params = exp.parameters() + self.exporterParameters[newtext] = params + if params is None: self.ui.paramTree.clear() else: From 50cf2f561f10e0ff88d767a4f03c4d48ec530115 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Wed, 6 Nov 2019 10:58:00 +0100 Subject: [PATCH 092/235] Move common code to _applyEnableMenu --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index a12eb519..27e64b56 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -321,13 +321,7 @@ class ViewBox(GraphicsWidget): self.state.update(state) - if self.state['enableMenu'] and self.menu is None: - self.menu = ViewBoxMenu(self) - self.updateViewLists() - elif not self.state['enableMenu'] and self.menu is not None: - self.menu.setParent(None) - self.menu = None - + self._applyMenuEnabled() self.updateViewRange() self.sigStateChanged.emit(self) @@ -381,16 +375,20 @@ class ViewBox(GraphicsWidget): def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu + self._applyMenuEnabled() + self.sigStateChanged.emit(self) + + def menuEnabled(self): + return self.state.get('enableMenu', True) + + def _applyMenuEnabled(self): + enableMenu = self.state.get("enableMenu", True) if enableMenu and self.menu is None: self.menu = ViewBoxMenu(self) self.updateViewLists() elif not enableMenu and self.menu is not None: self.menu.setParent(None) self.menu = None - self.sigStateChanged.emit(self) - - def menuEnabled(self): - return self.state.get('enableMenu', True) def addItem(self, item, ignoreBounds=False): """ From a65b8c91f7fc207b17466e77ab814848b186b077 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Wed, 6 Nov 2019 10:59:51 +0100 Subject: [PATCH 093/235] Add simple test for setEnableMenu --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 5a8ca141..9495bfc3 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -74,6 +74,14 @@ def test_ViewBox(): win.close() +def test_ViewBox_setMenuEnabled(): + init_viewbox() + vb.setMenuEnabled(True) + assert vb.menu is not None + vb.setMenuEnabled(False) + assert vb.menu is None + + skipreason = "Skipping this test until someone has time to fix it." @pytest.mark.skipif(True, reason=skipreason) def test_limits_and_resize(): From f5e25622a788d931a6cf4ff59d35d23f24e703a9 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 08:36:16 -0800 Subject: [PATCH 094/235] Validate min/max text inputs in ViewBoxMenu (#1074) --- .../graphicsItems/ViewBox/ViewBoxMenu.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 74a861d0..1f44bdd6 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ...Qt import QtCore, QtGui, QT_LIB from ...python2_3 import asUnicode from ...WidgetGroup import WidgetGroup @@ -48,8 +49,8 @@ class ViewBoxMenu(QtGui.QMenu): connects = [ (ui.mouseCheck.toggled, 'MouseToggled'), (ui.manualRadio.clicked, 'ManualClicked'), - (ui.minText.editingFinished, 'MinTextChanged'), - (ui.maxText.editingFinished, 'MaxTextChanged'), + (ui.minText.editingFinished, 'RangeTextChanged'), + (ui.maxText.editingFinished, 'RangeTextChanged'), (ui.autoRadio.clicked, 'AutoClicked'), (ui.autoPercentSpin.valueChanged, 'AutoSpinChanged'), (ui.linkCombo.currentIndexChanged, 'LinkComboChanged'), @@ -162,14 +163,10 @@ class ViewBoxMenu(QtGui.QMenu): def xManualClicked(self): self.view().enableAutoRange(ViewBox.XAxis, False) - def xMinTextChanged(self): + def xRangeTextChanged(self): self.ctrl[0].manualRadio.setChecked(True) - self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + self.view().setXRange(*self._validateRangeText(0), padding=0) - def xMaxTextChanged(self): - self.ctrl[0].manualRadio.setChecked(True) - self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) - def xAutoClicked(self): val = self.ctrl[0].autoPercentSpin.value() * 0.01 self.view().enableAutoRange(ViewBox.XAxis, val) @@ -194,13 +191,9 @@ class ViewBoxMenu(QtGui.QMenu): def yManualClicked(self): self.view().enableAutoRange(ViewBox.YAxis, False) - def yMinTextChanged(self): + def yRangeTextChanged(self): self.ctrl[1].manualRadio.setChecked(True) - self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) - - def yMaxTextChanged(self): - self.ctrl[1].manualRadio.setChecked(True) - self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + self.view().setYRange(*self._validateRangeText(1), padding=0) def yAutoClicked(self): val = self.ctrl[1].autoPercentSpin.value() * 0.01 @@ -265,6 +258,20 @@ class ViewBoxMenu(QtGui.QMenu): if changed: c.setCurrentIndex(0) c.currentIndexChanged.emit(c.currentIndex()) + + def _validateRangeText(self, axis): + """Validate range text inputs. Return current value(s) if invalid.""" + inputs = (self.ctrl[axis].minText.text(), + self.ctrl[axis].maxText.text()) + vals = self.view().viewRange()[axis] + for i, text in enumerate(inputs): + try: + vals[i] = float(text) + except ValueError: + # could not convert string to float + pass + return vals + from .ViewBox import ViewBox From faef56c3e7801b80bfb75a8bb9c8fc5c7dfdb955 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 08:45:42 -0800 Subject: [PATCH 095/235] Qulogic py3 fixes (#1073) * py3k: Remove reduce calls. * py3k: Remove compatibility sortList function. Sorting by key has existed since Python 2.4. * Remove unnecessary sys.path manipulation. This file doesn't have any __main__ code to run anyway. * Use context manager --- pyqtgraph/GraphicsScene/GraphicsScene.py | 7 +- pyqtgraph/__init__.py | 3 +- pyqtgraph/canvas/Canvas.py | 4 - pyqtgraph/configfile.py | 25 ++-- pyqtgraph/console/Console.py | 7 +- pyqtgraph/exporters/CSVExporter.py | 43 +++--- pyqtgraph/graphicsItems/GradientEditorItem.py | 8 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 134 ++++++++++-------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 18 ++- pyqtgraph/metaarray/MetaArray.py | 5 +- pyqtgraph/multiprocess/parallelizer.py | 17 +-- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- pyqtgraph/pixmaps/compile.py | 6 +- pyqtgraph/python2_3.py | 35 +---- pyqtgraph/reload.py | 21 ++- pyqtgraph/tests/test_exit_crash.py | 4 +- pyqtgraph/widgets/TableWidget.py | 3 +- pyqtgraph/widgets/ValueLabel.py | 3 +- 18 files changed, 163 insertions(+), 182 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 785031d5..b61f3a1b 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,6 +1,6 @@ +# -*- coding: utf-8 -*- import weakref from ..Qt import QtCore, QtGui -from ..python2_3 import sortList, cmp from ..Point import Point from .. import functions as fn from .. import ptime as ptime @@ -454,7 +454,7 @@ class GraphicsScene(QtGui.QGraphicsScene): return 0 return item.zValue() + absZValue(item.parentItem()) - sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a))) + items2.sort(key=absZValue, reverse=True) return items2 @@ -563,6 +563,3 @@ class GraphicsScene(QtGui.QGraphicsScene): @staticmethod def translateGraphicsItems(items): return list(map(GraphicsScene.translateGraphicsItem, items)) - - - diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index aad5c3c8..5f816245 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -95,7 +95,8 @@ def systemInfo(): if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision') if os.path.exists(lastRevFile): - rev = open(lastRevFile, 'r').read().strip() + with open(lastRevFile, 'r') as fd: + rev = fd.read().strip() print("pyqtgraph: %s; %s" % (__version__, rev)) print("config:") diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 72d70d7e..2ec13b19 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -if __name__ == '__main__': - import sys, os - md = os.path.dirname(os.path.abspath(__file__)) - sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path from ..Qt import QtGui, QtCore, QT_LIB from ..graphicsItems.ROI import ROI diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 0cc8f030..6ae8a0c5 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -39,10 +39,10 @@ class ParseError(Exception): def writeConfigFile(data, fname): s = genString(data) - fd = open(fname, 'w') - fd.write(s) - fd.close() - + with open(fname, 'w') as fd: + fd.write(s) + + def readConfigFile(fname): #cwd = os.getcwd() global GLOBAL_PATH @@ -55,9 +55,8 @@ def readConfigFile(fname): try: #os.chdir(newDir) ## bad. - fd = open(fname) - s = asUnicode(fd.read()) - fd.close() + with open(fname) as fd: + s = asUnicode(fd.read()) s = s.replace("\r\n", "\n") s = s.replace("\r", "\n") data = parseString(s)[1] @@ -73,9 +72,8 @@ def readConfigFile(fname): def appendConfigFile(data, fname): s = genString(data) - fd = open(fname, 'a') - fd.write(s) - fd.close() + with open(fname, 'a') as fd: + fd.write(s) def genString(data, indent=''): @@ -194,8 +192,6 @@ def measureIndent(s): if __name__ == '__main__': import tempfile - fn = tempfile.mktemp() - tf = open(fn, 'w') cf = """ key: 'value' key2: ##comment @@ -205,8 +201,9 @@ key2: ##comment key22: [1,2,3] key23: 234 #comment """ - tf.write(cf) - tf.close() + fn = tempfile.mktemp() + with open(fn, 'w') as tf: + tf.write(cf) print("=== Test:===") num = 1 for line in cf.split('\n'): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 477beb77..aac32d63 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys, re, os, time, traceback, subprocess import pickle @@ -98,12 +99,14 @@ class ConsoleWidget(QtGui.QWidget): def loadHistory(self): """Return the list of previously-invoked command strings (or None).""" if self.historyFile is not None: - return pickle.load(open(self.historyFile, 'rb')) + with open(self.historyFile, 'rb') as pf: + return pickle.load(pf) def saveHistory(self, history): """Store the list of previously-invoked command strings.""" if self.historyFile is not None: - pickle.dump(open(self.historyFile, 'wb'), history) + with open(self.historyFile, 'wb') as pf: + pickle.dump(pf, history) def runCmd(self, cmd): self.stdout = sys.stdout diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index b87f0182..c7591932 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from .Exporter import Exporter from ..parametertree import Parameter @@ -29,7 +30,6 @@ class CSVExporter(Exporter): self.fileSaveDialog(filter=["*.csv", "*.tsv"]) return - fd = open(fileName, 'w') data = [] header = [] @@ -55,28 +55,29 @@ class CSVExporter(Exporter): sep = ',' else: sep = '\t' - - fd.write(sep.join(header) + '\n') - i = 0 - numFormat = '%%0.%dg' % self.params['precision'] - numRows = max([len(d[0]) for d in data]) - for i in range(numRows): - for j, d in enumerate(data): - # write x value if this is the first column, or if we want x - # for all rows - if appendAllX or j == 0: - if d is not None and i < len(d[0]): - fd.write(numFormat % d[0][i] + sep) + + with open(fileName, 'w') as fd: + fd.write(sep.join(header) + '\n') + i = 0 + numFormat = '%%0.%dg' % self.params['precision'] + numRows = max([len(d[0]) for d in data]) + for i in range(numRows): + for j, d in enumerate(data): + # write x value if this is the first column, or if we want + # x for all rows + if appendAllX or j == 0: + if d is not None and i < len(d[0]): + fd.write(numFormat % d[0][i] + sep) + else: + fd.write(' %s' % sep) + + # write y value + if d is not None and i < len(d[1]): + fd.write(numFormat % d[1][i] + sep) else: fd.write(' %s' % sep) - - # write y value - if d is not None and i < len(d[1]): - fd.write(numFormat % d[1][i] + sep) - else: - fd.write(' %s' % sep) - fd.write('\n') - fd.close() + fd.write('\n') + CSVExporter.register() diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index fc1d638c..b360b2f7 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -1,14 +1,14 @@ +# -*- coding: utf-8 -*- +import operator import weakref import numpy as np from ..Qt import QtGui, QtCore -from ..python2_3 import sortList from .. import functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget from ..widgets.SpinBox import SpinBox from ..pgcollections import OrderedDict from ..colormap import ColorMap -from ..python2_3 import cmp __all__ = ['TickSliderItem', 'GradientEditorItem'] @@ -352,8 +352,7 @@ class TickSliderItem(GraphicsWidget): def listTicks(self): """Return a sorted list of all the Tick objects on the slider.""" ## public - ticks = list(self.ticks.items()) - sortList(ticks, lambda a,b: cmp(a[1], b[1])) ## see pyqtgraph.python2_3.sortList + ticks = sorted(self.ticks.items(), key=operator.itemgetter(1)) return ticks @@ -944,4 +943,3 @@ class TickMenu(QtGui.QMenu): # self.fracPosSpin.blockSignals(True) # self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick())) # self.fracPosSpin.blockSignals(False) - diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f3849b99..cf588912 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -677,7 +677,6 @@ class PlotItem(GraphicsWidget): xRange = rect.left(), rect.right() svg = "" - fh = open(fileName, 'w') dx = max(rect.right(),0) - min(rect.left(),0) ymn = min(rect.top(), rect.bottom()) @@ -691,52 +690,68 @@ class PlotItem(GraphicsWidget): sy *= 1000 sy *= -1 - fh.write('\n') - fh.write('\n' % (rect.left()*sx, rect.right()*sx)) - fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) + with open(fileName, 'w') as fh: + # fh.write('\n' % (rect.left() * sx, + # rect.top() * sx, + # rect.width() * sy, + # rect.height()*sy)) + fh.write('\n') + fh.write('\n' % ( + rect.left() * sx, rect.right() * sx)) + fh.write('\n' % ( + rect.top() * sy, rect.bottom() * sy)) - for item in self.curves: - if isinstance(item, PlotCurveItem): - color = fn.colorStr(item.pen.color()) - opacity = item.pen.color().alpha() / 255. - color = color[:6] - x, y = item.getData() - mask = (x > xRange[0]) * (x < xRange[1]) - mask[:-1] += mask[1:] - m2 = mask.copy() - mask[1:] += m2[:-1] - x = x[mask] - y = y[mask] - - x *= sx - y *= sy - - fh.write('') - - for item in self.dataItems: - if isinstance(item, ScatterPlotItem): - - pRect = item.boundingRect() - vRect = pRect.intersected(rect) - - for point in item.points(): - pos = point.pos() - if not rect.contains(pos): - continue - color = fn.colorStr(point.brush.color()) - opacity = point.brush.color().alpha() / 255. + for item in self.curves: + if isinstance(item, PlotCurveItem): + color = fn.colorStr(item.pen.color()) + opacity = item.pen.color().alpha() / 255. color = color[:6] - x = pos.x() * sx - y = pos.y() * sy - - fh.write('\n' % (x, y, color, opacity)) - - fh.write("\n") - + x, y = item.getData() + mask = (x > xRange[0]) * (x < xRange[1]) + mask[:-1] += mask[1:] + m2 = mask.copy() + mask[1:] += m2[:-1] + x = x[mask] + y = y[mask] + + x *= sx + y *= sy + + # fh.write('\n' % ( + # color, )) + fh.write('') + # fh.write("") + + for item in self.dataItems: + if isinstance(item, ScatterPlotItem): + pRect = item.boundingRect() + vRect = pRect.intersected(rect) + + for point in item.points(): + pos = point.pos() + if not rect.contains(pos): + continue + color = fn.colorStr(point.brush.color()) + opacity = point.brush.color().alpha() / 255. + color = color[:6] + x = pos.x() * sx + y = pos.y() * sy + + fh.write('\n' % ( + x, y, color, opacity)) + + fh.write("\n") + def writeSvg(self, fileName=None): if fileName is None: self._chooseFilenameDialog(handler=self.writeSvg) @@ -766,22 +781,21 @@ class PlotItem(GraphicsWidget): fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) - fd = open(fileName, 'w') data = [c.getData() for c in self.curves] - i = 0 - while True: - done = True - for d in data: - if i < len(d[0]): - fd.write('%g,%g,'%(d[0][i], d[1][i])) - done = False - else: - fd.write(' , ,') - fd.write('\n') - if done: - break - i += 1 - fd.close() + with open(fileName, 'w') as fd: + i = 0 + while True: + done = True + for d in data: + if i < len(d[0]): + fd.write('%g,%g,' % (d[0][i], d[1][i])) + done = False + else: + fd.write(' , ,') + fd.write('\n') + if done: + break + i += 1 def saveState(self): state = self.stateGroup.state() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9c71d7db..2cd6c28f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1,9 +1,10 @@ +# -*- coding: utf-8 -*- import weakref import sys from copy import deepcopy import numpy as np from ...Qt import QtGui, QtCore -from ...python2_3 import sortList, basestring, cmp +from ...python2_3 import basestring from ...Point import Point from ... import functions as fn from .. ItemGroup import ItemGroup @@ -1603,16 +1604,13 @@ class ViewBox(GraphicsWidget): self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - - def cmpViews(a, b): - wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) - alpha = cmp(a.name, b.name) - return wins + alpha - + + def view_key(view): + return (view.window() is self.window(), view.name) + ## make a sorted list of all named views - nv = list(ViewBox.NamedViews.values()) - sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList - + nv = sorted(ViewBox.NamedViews.values(), key=view_key) + if self in nv: nv.remove(self) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index f157c588..690ff49d 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -12,7 +12,6 @@ More info at http://www.scipy.org/Cookbook/MetaArray import types, copy, threading, os, re import pickle -from functools import reduce import numpy as np from ..python2_3 import basestring #import traceback @@ -844,7 +843,7 @@ class MetaArray(object): frames = [] frameShape = list(meta['shape']) frameShape[dynAxis] = 1 - frameSize = reduce(lambda a,b: a*b, frameShape) + frameSize = np.prod(frameShape) n = 0 while True: ## Extract one non-blank line @@ -1298,7 +1297,7 @@ class MetaArray(object): #frames = [] #frameShape = list(meta['shape']) #frameShape[dynAxis] = 1 - #frameSize = reduce(lambda a,b: a*b, frameShape) + #frameSize = np.prod(frameShape) #n = 0 #while True: ### Extract one non-blank line diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 989bd4f8..b0f064bd 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os, sys, time, multiprocessing, re from .processes import ForkedProcess from .remoteproxy import ClosedError @@ -213,14 +214,14 @@ class Parallelize(object): try: cores = {} pid = None - - for line in open('/proc/cpuinfo'): - m = re.match(r'physical id\s+:\s+(\d+)', line) - if m is not None: - pid = m.groups()[0] - m = re.match(r'cpu cores\s+:\s+(\d+)', line) - if m is not None: - cores[pid] = int(m.groups()[0]) + with open('/proc/cpuinfo') as fd: + for line in fd: + m = re.match(r'physical id\s+:\s+(\d+)', line) + if m is not None: + pid = m.groups()[0] + m = re.match(r'cpu cores\s+:\s+(\d+)', line) + if m is not None: + cores[pid] = int(m.groups()[0]) return sum(cores.values()) except: return multiprocessing.cpu_count() diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 636c1621..463ad742 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -123,7 +123,7 @@ class GLScatterPlotItem(GLGraphicsItem): try: pos = self.pos #if pos.ndim > 2: - #pos = pos.reshape((reduce(lambda a,b: a*b, pos.shape[:-1]), pos.shape[-1])) + #pos = pos.reshape((-1, pos.shape[-1])) glVertexPointerf(pos) if isinstance(self.color, np.ndarray): diff --git a/pyqtgraph/pixmaps/compile.py b/pyqtgraph/pixmaps/compile.py index fa0d2408..68fd2da1 100644 --- a/pyqtgraph/pixmaps/compile.py +++ b/pyqtgraph/pixmaps/compile.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import numpy as np from PyQt4 import QtGui import os, pickle, sys @@ -14,6 +15,5 @@ for f in os.listdir(path): arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) pixmaps[f] = pickle.dumps(arr) ver = sys.version_info[0] -fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') -fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) - +with open(os.path.join(path, 'pixmapData_%d.py' % (ver, )), 'w') as fh: + fh.write("import numpy as np; pixmapData=%s" % (repr(pixmaps), )) diff --git a/pyqtgraph/python2_3.py b/pyqtgraph/python2_3.py index ae4667eb..952b49b1 100644 --- a/pyqtgraph/python2_3.py +++ b/pyqtgraph/python2_3.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Helper functions that smooth out the differences between python 2 and 3. """ @@ -13,46 +14,12 @@ def asUnicode(x): return unicode(x) else: return str(x) - -def cmpToKey(mycmp): - 'Convert a cmp= function into a key= function' - class K(object): - def __init__(self, obj, *args): - self.obj = obj - def __lt__(self, other): - return mycmp(self.obj, other.obj) < 0 - def __gt__(self, other): - return mycmp(self.obj, other.obj) > 0 - def __eq__(self, other): - return mycmp(self.obj, other.obj) == 0 - def __le__(self, other): - return mycmp(self.obj, other.obj) <= 0 - def __ge__(self, other): - return mycmp(self.obj, other.obj) >= 0 - def __ne__(self, other): - return mycmp(self.obj, other.obj) != 0 - return K -def sortList(l, cmpFunc): - if sys.version_info[0] == 2: - l.sort(cmpFunc) - else: - l.sort(key=cmpToKey(cmpFunc)) if sys.version_info[0] == 3: basestring = str - def cmp(a,b): - if a>b: - return 1 - elif b > a: - return -1 - else: - return 0 xrange = range else: import __builtin__ basestring = __builtin__.basestring - cmp = __builtin__.cmp xrange = __builtin__.xrange - - \ No newline at end of file diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index f6c630b9..b0c875f1 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -306,7 +306,8 @@ if __name__ == '__main__': import os if not os.path.isdir('test1'): os.mkdir('test1') - open('test1/__init__.py', 'w') + with open('test1/__init__.py', 'w'): + pass modFile1 = "test1/test1.py" modCode1 = """ import sys @@ -345,8 +346,10 @@ def fn(): print("fn: %s") """ - open(modFile1, 'w').write(modCode1%(1,1)) - open(modFile2, 'w').write(modCode2%"message 1") + with open(modFile1, 'w') as f: + f.write(modCode1 % (1, 1)) + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 1", )) import test1.test1 as test1 import test2 print("Test 1 originals:") @@ -382,7 +385,8 @@ def fn(): c1.fn() os.remove(modFile1+'c') - open(modFile1, 'w').write(modCode1%(2,2)) + with open(modFile1, 'w') as f: + f.write(modCode1 %(2, 2)) print("\n----RELOAD test1-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) @@ -393,7 +397,8 @@ def fn(): os.remove(modFile2+'c') - open(modFile2, 'w').write(modCode2%"message 2") + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 2", )) print("\n----RELOAD test2-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) @@ -429,8 +434,10 @@ def fn(): os.remove(modFile1+'c') os.remove(modFile2+'c') - open(modFile1, 'w').write(modCode1%(3,3)) - open(modFile2, 'w').write(modCode2%"message 3") + with open(modFile1, 'w') as f: + f.write(modCode1 % (3, 3)) + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 3", )) print("\n----RELOAD-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 50924908..5a10a0a3 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import sys import subprocess @@ -59,7 +60,8 @@ def test_exit_crash(): print(name) argstr = initArgs.get(name, "") - open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) + with open(tmp, 'w') as f: + f.write(code.format(path=path, classname=name, args=argstr)) proc = subprocess.Popen([sys.executable, tmp]) assert proc.wait() == 0 diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 90b56139..0378b5fc 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -355,7 +355,8 @@ class TableWidget(QtGui.QTableWidget): fileName = fileName[0] # Qt4/5 API difference if fileName == '': return - open(str(fileName), 'w').write(data) + with open(fileName, 'w') as fd: + fd.write(data) def contextMenuEvent(self, ev): self.contextMenu.popup(ev.globalPos()) diff --git a/pyqtgraph/widgets/ValueLabel.py b/pyqtgraph/widgets/ValueLabel.py index 4e5b3011..b24fb16c 100644 --- a/pyqtgraph/widgets/ValueLabel.py +++ b/pyqtgraph/widgets/ValueLabel.py @@ -1,7 +1,6 @@ from ..Qt import QtCore, QtGui from ..ptime import time from .. import functions as fn -from functools import reduce __all__ = ['ValueLabel'] @@ -54,7 +53,7 @@ class ValueLabel(QtGui.QLabel): self.averageTime = t def averageValue(self): - return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values)) + return sum(v[1] for v in self.values) / float(len(self.values)) def paintEvent(self, ev): From ec445e76015143b49642722d7c3bd9746eaf8f45 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 09:01:49 -0800 Subject: [PATCH 096/235] HDF5Exporter handling of ragged curves with tests (#1072) * HDF5Exporter handles ragged curves by saving them into different datasets based on their names. * Add HDF5Exporter tests * Document HDF5Exporter * Fix tmp file path --- azure-test-template.yml | 4 +- doc/source/exporting.rst | 7 ++- pyqtgraph/exporters/HDF5Exporter.py | 31 ++++++----- pyqtgraph/exporters/tests/test_hdf5.py | 71 ++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 pyqtgraph/exporters/tests/test_hdf5.py diff --git a/azure-test-template.yml b/azure-test-template.yml index 81e4399c..5d204009 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -116,9 +116,9 @@ jobs: conda install -c conda-forge $(qt.bindings) --yes fi echo "Installing remainder of dependencies" - conda install -c conda-forge numpy scipy six pyopengl --yes + conda install -c conda-forge numpy scipy six pyopengl h5py --yes else - pip install $(qt.bindings) numpy scipy pyopengl six + pip install $(qt.bindings) numpy scipy pyopengl six h5py fi echo "" pip install pytest pytest-xdist pytest-cov coverage diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst index ccd017d7..0bb1c82a 100644 --- a/doc/source/exporting.rst +++ b/doc/source/exporting.rst @@ -30,8 +30,13 @@ Export Formats for export. * Printer - Exports to the operating system's printing service. This exporter is provided for completeness, but is not well supported due to problems with Qt's printing system. +* HDF5 - Exports data from a :class:`~pyqtgraph.PlotItem` to a HDF5 file if + h5py_ is installed. This exporter supports :class:`~pyqtgraph.PlotItem` + objects containing multiple curves, stacking the data into a single HDF5 + dataset based on the ``columnMode`` parameter. If data items aren't the same + size, each one is given its own dataset. - +.. _h5py: https://www.h5py.org/ Exporting from the API ---------------------- diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py index 584a9f71..2a2ac19c 100644 --- a/pyqtgraph/exporters/HDF5Exporter.py +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -44,20 +44,27 @@ class HDF5Exporter(Exporter): data = [] appendAllX = self.params['columnMode'] == '(x,y) per plot' - #print dir(self.item.curves[0]) - tlen = 0 - for i, c in enumerate(self.item.curves): - d = c.getData() - if i > 0 and len(d[0]) != tlen: - raise ValueError ("HDF5 Export requires all curves in plot to have same length") - if appendAllX or i == 0: - data.append(d[0]) - tlen = len(d[0]) - data.append(d[1]) + # Check if the arrays are ragged + len_first = len(self.item.curves[0].getData()[0]) if self.item.curves[0] else None + ragged = any(len(i.getData()[0]) != len_first for i in self.item.curves) + if ragged: + dgroup = fd.create_group(dsname) + for i, c in enumerate(self.item.curves): + d = c.getData() + fdata = numpy.array([d[0], d[1]]).astype('double') + cname = c.name() if c.name() is not None else str(i) + dset = dgroup.create_dataset(cname, data=fdata) + else: + for i, c in enumerate(self.item.curves): + d = c.getData() + if appendAllX or i == 0: + data.append(d[0]) + data.append(d[1]) + + fdata = numpy.array(data).astype('double') + dset = fd.create_dataset(dsname, data=fdata) - fdata = numpy.array(data).astype('double') - dset = fd.create_dataset(dsname, data=fdata) fd.close() if HAVE_HDF5: diff --git a/pyqtgraph/exporters/tests/test_hdf5.py b/pyqtgraph/exporters/tests/test_hdf5.py new file mode 100644 index 00000000..69bb8ae7 --- /dev/null +++ b/pyqtgraph/exporters/tests/test_hdf5.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +import pytest +import pyqtgraph as pg +from pyqtgraph.exporters import HDF5Exporter +import numpy as np +from numpy.testing import assert_equal +import h5py +import os + + +@pytest.fixture +def tmp_h5(tmp_path): + yield tmp_path / "data.h5" + + +@pytest.mark.parametrize("combine", [False, True]) +def test_HDF5Exporter(tmp_h5, combine): + # Basic test of functionality: multiple curves with shared x array. Tests + # both options for stacking the data (columnMode). + x = np.linspace(0, 1, 100) + y1 = np.sin(x) + y2 = np.cos(x) + + plt = pg.plot() + plt.plot(x=x, y=y1) + plt.plot(x=x, y=y2) + + ex = HDF5Exporter(plt.plotItem) + + if combine: + ex.parameters()['columnMode'] = '(x,y,y,y) for all plots' + + ex.export(fileName=tmp_h5) + + with h5py.File(tmp_h5, 'r') as f: + # should be a single dataset with the name of the exporter + dset = f[ex.parameters()['Name']] + assert isinstance(dset, h5py.Dataset) + + if combine: + assert_equal(np.array([x, y1, y2]), dset) + else: + assert_equal(np.array([x, y1, x, y2]), dset) + + +def test_HDF5Exporter_unequal_lengths(tmp_h5): + # Test export with multiple curves of different size. The exporter should + # detect this and create multiple hdf5 datasets under a group. + x1 = np.linspace(0, 1, 10) + y1 = np.sin(x1) + x2 = np.linspace(0, 1, 100) + y2 = np.cos(x2) + + plt = pg.plot() + plt.plot(x=x1, y=y1, name='plot0') + plt.plot(x=x2, y=y2) + + ex = HDF5Exporter(plt.plotItem) + ex.export(fileName=tmp_h5) + + with h5py.File(tmp_h5, 'r') as f: + # should be a group with the name of the exporter + group = f[ex.parameters()['Name']] + assert isinstance(group, h5py.Group) + + # should be a dataset under the group with the name of the PlotItem + assert_equal(np.array([x1, y1]), group['plot0']) + + # should be a dataset under the group with a default name that's the + # index of the curve in the PlotItem + assert_equal(np.array([x2, y2]), group['1']) From 220393339323ff9d23e5f3070c3ebd00f93e46a9 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Tue, 12 Nov 2019 09:02:08 -0800 Subject: [PATCH 097/235] Declare scipy optional (#1067) * Replace use of scipy.random with numpy.random * Update README to reflect scipy being an optional depenency --- README.md | 3 ++- examples/MultiPlotWidget.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28143078..914523fd 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ Requirements * PyQt 4.8+, PySide, PyQt5, or PySide2 * python 2.7, or 3.x * Required - * `numpy`, `scipy` + * `numpy` * Optional + * `scipy` for image processing * `pyopengl` for 3D graphics * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` diff --git a/examples/MultiPlotWidget.py b/examples/MultiPlotWidget.py index 5ab4b21d..67cb83ee 100644 --- a/examples/MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -3,8 +3,7 @@ ## Add path to library (just for examples; you do not need this) import initExample - -from scipy import random +import numpy as np from numpy import linspace from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg @@ -22,7 +21,7 @@ pw = MultiPlotWidget() mw.setCentralWidget(pw) mw.show() -data = random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) +data = np.random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) ma = MetaArray(data, info=[ {'name': 'Signal', 'cols': [ {'name': 'Col1', 'units': 'V'}, From 15a1f5af94872347ef8eb61aba2beca271157b18 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Tue, 19 Nov 2019 18:14:53 +0200 Subject: [PATCH 098/235] improve performance of updateData PlotCurveItem (saves about 2us per call) (#1079) * improve performance of updateData PlotCurveItem (saves about 2us per call) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 05b11b4d..fea3834f 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -293,7 +293,7 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None self.invalidateBounds() self.update() - + def setData(self, *args, **kargs): """ =============== ======================================================== @@ -358,7 +358,7 @@ class PlotCurveItem(GraphicsObject): kargs[k] = data if not isinstance(data, np.ndarray) or data.ndim > 1: raise Exception("Plot data must be 1D ndarray.") - if 'complex' in str(data.dtype): + if data.dtype.kind == 'c': raise Exception("Can not plot complex data types.") profiler("data checks") From 60c760a2e0515e042a8d307fe30348b58ad86b6a Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Tue, 19 Nov 2019 08:15:27 -0800 Subject: [PATCH 099/235] Add RemoteGraphicsView to __init__.py (#1066) --- pyqtgraph/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 5f816245..da14a83b 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -261,6 +261,7 @@ from .widgets.LayoutWidget import * from .widgets.TableWidget import * from .widgets.ProgressDialog import * from .widgets.GroupBox import GroupBox +from .widgets.RemoteGraphicsView import RemoteGraphicsView from .imageview import * from .WidgetGroup import * From 267a0af8e76dc751d60c7e2fd863a7e64a9ab6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Tue, 19 Nov 2019 17:21:36 +0100 Subject: [PATCH 100/235] Reset currentRow and currentCol on clear (#1076) --- pyqtgraph/graphicsItems/GraphicsLayout.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index c0db5890..c3722ec0 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -167,6 +167,8 @@ class GraphicsLayout(GraphicsWidget): def clear(self): for i in list(self.items.keys()): self.removeItem(i) + self.currentRow = 0 + self.currentCol = 0 def setContentsMargins(self, *args): # Wrap calls to layout. This should happen automatically, but there From b1b2f4662b611321b3f07b3d2d1a5b032a6d4274 Mon Sep 17 00:00:00 2001 From: boylea Date: Tue, 19 Nov 2019 20:03:15 -0800 Subject: [PATCH 101/235] Fixed image scatter plot export bug (#88) --- pyqtgraph/graphicsItems/GraphicsItem.py | 5 +++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 628b495b..439d94ad 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -98,8 +98,9 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - return self._exportOpts['painter'].deviceTransform() * self.sceneTransform() - + scaler = self._exportOpts['resolutionScale'] + return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1) + if viewportTransform is None: view = self.getViewWidget() if view is None: diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 67fafd83..bfc41969 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -781,7 +781,7 @@ class ScatterPlotItem(GraphicsObject): pts = pts[:,viewMask] for i, rec in enumerate(data): p.resetTransform() - p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']) + p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: From 455fdc2a2a9fa192eddd0a00f5f09d54c0dfe2b5 Mon Sep 17 00:00:00 2001 From: rwalroth <31414518+rwalroth@users.noreply.github.com> Date: Tue, 19 Nov 2019 20:05:45 -0800 Subject: [PATCH 102/235] Allowed actions to diplay title instead of name (#1069) ActionParameterItem changed so that if there is a title it will be displayed, otherwise displays name. --- pyqtgraph/parametertree/parameterTypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8d65767d..b728fb8e 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -612,7 +612,10 @@ class ActionParameterItem(ParameterItem): self.layout = QtGui.QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) - self.button = QtGui.QPushButton(param.name()) + title = param.opts.get('title', None) + if title is None: + title = param.name() + self.button = QtGui.QPushButton(title) #self.layout.addSpacing(100) self.layout.addWidget(self.button) self.layout.addStretch() From c95ab570b11cd749bc54d6ae5fa3991d91f6c252 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Wed, 20 Nov 2019 04:43:27 +0000 Subject: [PATCH 103/235] set color of tick-labels separately (#841) --- pyqtgraph/graphicsItems/AxisItem.py | 33 +++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 088ba6b8..da57403f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -17,7 +17,7 @@ class AxisItem(GraphicsWidget): If maxTickLength is negative, ticks point into the plot. """ - def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): + def __init__(self, orientation, pen=None, textPen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): """ ============== =============================================================== **Arguments:** @@ -28,6 +28,7 @@ class AxisItem(GraphicsWidget): to be linked to the visible range of a ViewBox. showValues (bool) Whether to display values adjacent to ticks pen (QPen) Pen used when drawing ticks. + textPen (QPen) Pen used when drawing tick labels. text The text (excluding units) to display on the label for this axis. units The units for this axis. Units should generally be given @@ -97,6 +98,11 @@ class AxisItem(GraphicsWidget): else: self.setPen(pen) + if textPen is None: + self.setTextPen() + else: + self.setTextPen(pen) + self._linkedView = None if linkView is not None: self.linkToView(linkView) @@ -405,6 +411,25 @@ class AxisItem(GraphicsWidget): self.setLabel() self.update() + def textPen(self): + if self._textPen is None: + return fn.mkPen(getConfigOption('foreground')) + return fn.mkPen(self._textPen) + + def setTextPen(self, *args, **kwargs): + """ + Set the pen used for drawing text. + If no arguments are given, the default foreground color will be used. + """ + self.picture = None + if args or kwargs: + self._textPen = fn.mkPen(*args, **kwargs) + else: + self._textPen = fn.mkPen(getConfigOption('foreground')) + self.labelStyle['color'] = '#' + fn.colorStr(self._textPen.color())[:6] + self.setLabel() + self.update() + def setScale(self, scale=None): """ Set the value scaling for this axis. @@ -1048,13 +1073,13 @@ class AxisItem(GraphicsWidget): p.drawLine(p1, p2) profiler('draw ticks') - ## Draw all text + # Draw all text if self.tickFont is not None: p.setFont(self.tickFont) - p.setPen(self.pen()) + p.setPen(self.textPen()) for rect, flags, text in textSpecs: p.drawText(rect, flags, text) - #p.drawRect(rect) + profiler('draw text') def show(self): From c0ae44bc2d8f719dc267dc32e1496d5a38d8fd4a Mon Sep 17 00:00:00 2001 From: SamSchott Date: Wed, 20 Nov 2019 05:42:31 +0000 Subject: [PATCH 104/235] Nicer legend (#958) * More customizable and nicer legend. - Give kwargs for legend frame and background colors instead of hard-coded values. - Reduce spacing for more compact legend - Give separate kwarg `labelTextColor`. - New method to clear all legend items. - New methods to get and change `offset` relative to the legend's parent. - Horizontal instead of tilted lines for legend pictures. --- pyqtgraph/graphicsItems/LegendItem.py | 175 ++++++++++++++++++-------- 1 file changed, 123 insertions(+), 52 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index ce5bd883..d8986011 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -8,6 +8,7 @@ from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor __all__ = ['LegendItem'] + class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Displays a legend used for describing the contents of a plot. @@ -19,47 +20,120 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): legend.setParentItem(plotItem) """ - def __init__(self, size=None, offset=None): + def __init__(self, size=None, offset=None, horSpacing=25, verSpacing=0, pen=None, + brush=None, labelTextColor=None, **kwargs): """ ============== =============================================================== **Arguments:** size Specifies the fixed size (width, height) of the legend. If - this argument is omitted, the legend will autimatically resize + this argument is omitted, the legend will automatically resize to fit its contents. offset Specifies the offset position relative to the legend's parent. Positive values offset from the left or top; negative values offset from the right or bottom. If offset is None, the legend must be anchored manually by calling anchor() or positioned by calling setPos(). + horSpacing Specifies the spacing between the line symbol and the label. + verSpacing Specifies the spacing between individual entries of the legend + vertically. (Can also be negative to have them really close) + pen Pen to use when drawing legend border. Any single argument + accepted by :func:`mkPen ` is allowed. + brush QBrush to use as legend background filling. Any single argument + accepted by :func:`mkBrush ` is allowed. + labelTextColor Pen to use when drawing legend text. Any single argument + accepted by :func:`mkPen ` is allowed. ============== =============================================================== - + """ - - + + GraphicsWidget.__init__(self) GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemIgnoresTransformations) self.layout = QtGui.QGraphicsGridLayout() + self.layout.setVerticalSpacing(verSpacing) + self.layout.setHorizontalSpacing(horSpacing) + self.setLayout(self.layout) self.items = [] self.size = size - self.offset = offset if size is not None: self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1])) - + + self.opts = { + 'pen': fn.mkPen(pen), + 'brush': fn.mkBrush(brush), + 'labelTextColor': labelTextColor, + 'offset': offset, + } + + self.opts.update(kwargs) + + def offset(self): + return self.opts['offset'] + + def setOffset(self, offset): + self.opts['offset'] = offset + + offset = Point(self.opts['offset']) + anchorx = 1 if offset[0] <= 0 else 0 + anchory = 1 if offset[1] <= 0 else 0 + anchor = (anchorx, anchory) + self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) + + def pen(self): + return self.opts['pen'] + + def setPen(self, *args, **kargs): + """ + Sets the pen used to draw lines between points. + *pen* can be a QPen or any argument accepted by + :func:`pyqtgraph.mkPen() ` + """ + pen = fn.mkPen(*args, **kargs) + self.opts['pen'] = pen + + self.paint() + + def brush(self): + return self.opts['brush'] + + def setBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + if self.opts['brush'] == brush: + return + self.opts['brush'] = brush + + self.paint() + + def labelTextColor(self): + return self.opts['labelTextColor'] + + def setLabelTextColor(self, *args, **kargs): + """ + Sets the color of the label text. + *pen* can be a QPen or any argument accepted by + :func:`pyqtgraph.mkColor() ` + """ + self.opts['labelTextColor'] = fn.mkColor(*args, **kargs) + for sample, label in self.items: + label.setAttr('color', self.opts['labelTextColor']) + + self.paint() + def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) if self.offset is not None: - offset = Point(self.offset) + offset = Point(self.opts['offset']) anchorx = 1 if offset[0] <= 0 else 0 anchory = 1 if offset[1] <= 0 else 0 anchor = (anchorx, anchory) self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) return ret - + def addItem(self, item, name): """ - Add a new entry to the legend. + Add a new entry to the legend. ============== ======================================================== **Arguments:** @@ -70,36 +144,45 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): title The title to display for this item. Simple HTML allowed. ============== ======================================================== """ - label = LabelItem(name) + label = LabelItem(name, color=self.opts['labelTextColor'], justify='left') if isinstance(item, ItemSample): sample = item else: - sample = ItemSample(item) + sample = ItemSample(item) + row = self.layout.rowCount() self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) self.updateSize() - + def removeItem(self, item): """ - Removes one item from the legend. + Removes one item from the legend. ============== ======================================================== **Arguments:** item The item to remove or its name. ============== ======================================================== """ - # Thanks, Ulrich! - # cycle for a match for sample, label in self.items: if sample.item is item or label.text == item: - self.items.remove( (sample, label) ) # remove from itemlist + self.items.remove((sample, label)) # remove from itemlist self.layout.removeItem(sample) # remove from layout sample.close() # remove from drawing self.layout.removeItem(label) label.close() self.updateSize() # redraq box + return # return after first match + + def clear(self): + """Removes all items from legend.""" + for sample, label in self.items: + self.layout.removeItem(sample) + self.layout.removeItem(label) + + self.items = [] + self.updateSize() def clear(self): """ @@ -113,29 +196,20 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def updateSize(self): if self.size is not None: return - - height = 0 - width = 0 - #print("-------") - for sample, label in self.items: - height += max(sample.height(), label.height()) + 3 - width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() + - label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width())) - #print(width, height) - #print width, height - self.setGeometry(0, 0, width+25, height) - + + self.setGeometry(0, 0, 0, 0) + def boundingRect(self): return QtCore.QRectF(0, 0, self.width(), self.height()) - + def paint(self, p, *args): - p.setPen(fn.mkPen(255,255,255,100)) - p.setBrush(fn.mkBrush(100,100,100,50)) + p.setPen(self.opts['pen']) + p.setBrush(self.opts['brush']) p.drawRect(self.boundingRect()) def hoverEvent(self, ev): ev.acceptDrags(QtCore.Qt.LeftButton) - + def mouseDragEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: ev.accept() @@ -145,42 +219,39 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): class ItemSample(GraphicsWidget): """ Class responsible for drawing a single item in a LegendItem (sans label). - + This may be subclassed to draw custom graphics in a Legend. """ ## Todo: make this more generic; let each item decide how it should be represented. def __init__(self, item): GraphicsWidget.__init__(self) self.item = item - + def boundingRect(self): return QtCore.QRectF(0, 0, 20, 20) - + def paint(self, p, *args): - #p.setRenderHint(p.Antialiasing) # only if the data is antialiased. opts = self.item.opts - - if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: - p.setBrush(fn.mkBrush(opts['fillBrush'])) - p.setPen(fn.mkPen(None)) - p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - + + if opts['antialias']: + p.setRenderHint(p.Antialiasing) + if not isinstance(self.item, ScatterPlotItem): p.setPen(fn.mkPen(opts['pen'])) - p.drawLine(2, 18, 18, 2) - + p.drawLine(0, 11, 20, 11) + symbol = opts.get('symbol', None) if symbol is not None: if isinstance(self.item, PlotDataItem): opts = self.item.scatter.opts - + pen = fn.mkPen(opts['pen']) brush = fn.mkBrush(opts['brush']) size = opts['size'] - - p.translate(10,10) + + p.translate(10, 10) path = drawSymbol(p, symbol, size, pen, brush) - - - - + + + + From ae61d3582e8b80eb0ec4d446f3e03767a29b21a6 Mon Sep 17 00:00:00 2001 From: "Paul B. Manis" Date: Tue, 24 Jul 2018 20:01:27 -0400 Subject: [PATCH 105/235] Py2/3 MetaArray adjustments, first pass --- pyqtgraph/metaarray/MetaArray.py | 42 +++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 690ff49d..cecea39f 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -26,7 +26,9 @@ except: USE_HDF5 = False HAVE_HDF5 = False - +if HAVE_HDF5: + import h5py.highlevel + def axis(name=None, cols=None, values=None, units=None): """Convenience function for generating axis descriptions when defining MetaArrays""" ax = {} @@ -102,7 +104,7 @@ class MetaArray(object): since the actual values are described (name and units) in the column info for the first axis. """ - version = '2' + version = u'2' # Default hdf5 compression to use when writing # 'gzip' is widely available and somewhat slow @@ -740,7 +742,7 @@ class MetaArray(object): ## decide which read function to use with open(filename, 'rb') as fd: magic = fd.read(8) - if magic == '\x89HDF\r\n\x1a\n': + if magic == b'\x89HDF\r\n\x1a\n': fd.close() self._readHDF5(filename, **kwargs) self._isHDF = True @@ -765,7 +767,7 @@ class MetaArray(object): """Read meta array from the top of a file. Read lines until a blank line is reached. This function should ideally work for ALL versions of MetaArray. """ - meta = '' + meta = u'' ## Read meta information until the first blank line while True: line = fd.readline().strip() @@ -776,6 +778,20 @@ class MetaArray(object): #print ret return ret + def fix_info(self, info): + """ + Recursive version + """ + if isinstance(info, list): + for i in range(len(info)): + info[i] = self.fix_info(info[i]) + elif isinstance(info, dict): + for k in info.keys(): + info[k] = self.fix_info(info[k]) + elif isinstance(info, bytes): # change all bytestrings to string and remove internal quotes + info = info.decode('utf-8').replace("\'", '') + return info + def _readData1(self, fd, meta, mmap=False, **kwds): ## Read array data from the file descriptor for MetaArray v1 files ## read in axis values for any axis that specifies a length @@ -786,7 +802,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) if not kwds.get("readAllData", True): return ## the remaining data is the actual array @@ -814,7 +830,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) if not kwds.get("readAllData", True): return @@ -901,7 +917,7 @@ class MetaArray(object): del ax['values_type'] #subarr = subarr.view(subtype) #subarr._info = meta['info'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) self._data = subarr #raise Exception() ## stress-testing #return subarr @@ -934,10 +950,14 @@ class MetaArray(object): f = h5py.File(fileName, mode) ver = f.attrs['MetaArray'] + try: + ver = ver.decode('utf-8') + except: + pass if ver > MetaArray.version: print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version))) meta = MetaArray.readHDF5Meta(f['info']) - self._info = meta + self._info = self.fix_info(meta) if writable or not readAllData: ## read all data, convert to ndarray, close file self._data = f['data'] @@ -962,7 +982,7 @@ class MetaArray(object): MetaArray._h5py_metaarray = proc._import('pyqtgraph.metaarray') ma = MetaArray._h5py_metaarray.MetaArray(file=fileName) self._data = ma.asarray()._getValue() - self._info = ma._info._getValue() + self._info = self.fix_info(ma._info._getValue()) #print MetaArray._hdf5Process #import inspect #print MetaArray, id(MetaArray), inspect.getmodule(MetaArray) @@ -1010,6 +1030,10 @@ class MetaArray(object): data[k] = val typ = root.attrs['_metaType_'] + try: + typ = typ.decode('utf-8') + except: + pass del data['_metaType_'] if typ == 'dict': From c484c8641710b82acfdb255f124f9534407108c0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Aug 2018 08:57:47 -0700 Subject: [PATCH 106/235] don't modify info from v1 files, move info correction to hdf reading --- pyqtgraph/metaarray/MetaArray.py | 67 ++++++++++---------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index cecea39f..6ce9b05b 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -14,21 +14,19 @@ import types, copy, threading, os, re import pickle import numpy as np from ..python2_3 import basestring -#import traceback + ## By default, the library will use HDF5 when writing files. ## This can be overridden by setting USE_HDF5 = False USE_HDF5 = True try: - import h5py + import h5py.highlevel HAVE_HDF5 = True except: USE_HDF5 = False HAVE_HDF5 = False -if HAVE_HDF5: - import h5py.highlevel - + def axis(name=None, cols=None, values=None, units=None): """Convenience function for generating axis descriptions when defining MetaArrays""" ax = {} @@ -777,20 +775,6 @@ class MetaArray(object): ret = eval(meta) #print ret return ret - - def fix_info(self, info): - """ - Recursive version - """ - if isinstance(info, list): - for i in range(len(info)): - info[i] = self.fix_info(info[i]) - elif isinstance(info, dict): - for k in info.keys(): - info[k] = self.fix_info(info[k]) - elif isinstance(info, bytes): # change all bytestrings to string and remove internal quotes - info = info.decode('utf-8').replace("\'", '') - return info def _readData1(self, fd, meta, mmap=False, **kwds): ## Read array data from the file descriptor for MetaArray v1 files @@ -802,7 +786,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] if not kwds.get("readAllData", True): return ## the remaining data is the actual array @@ -830,7 +814,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] if not kwds.get("readAllData", True): return @@ -901,10 +885,8 @@ class MetaArray(object): newSubset = list(subset[:]) newSubset[dynAxis] = slice(dStart, dStop) if dStop > dStart: - #print n, data.shape, " => ", newSubset, data[tuple(newSubset)].shape frames.append(data[tuple(newSubset)].copy()) else: - #data = data[subset].copy() ## what's this for?? frames.append(data) n += inf['numFrames'] @@ -915,12 +897,8 @@ class MetaArray(object): ax['values'] = np.array(xVals, dtype=ax['values_type']) del ax['values_len'] del ax['values_type'] - #subarr = subarr.view(subtype) - #subarr._info = meta['info'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] self._data = subarr - #raise Exception() ## stress-testing - #return subarr def _readHDF5(self, fileName, readAllData=None, writable=False, **kargs): if 'close' in kargs and readAllData is None: ## for backward compatibility @@ -957,7 +935,7 @@ class MetaArray(object): if ver > MetaArray.version: print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version))) meta = MetaArray.readHDF5Meta(f['info']) - self._info = self.fix_info(meta) + self._info = meta if writable or not readAllData: ## read all data, convert to ndarray, close file self._data = f['data'] @@ -982,12 +960,7 @@ class MetaArray(object): MetaArray._h5py_metaarray = proc._import('pyqtgraph.metaarray') ma = MetaArray._h5py_metaarray.MetaArray(file=fileName) self._data = ma.asarray()._getValue() - self._info = self.fix_info(ma._info._getValue()) - #print MetaArray._hdf5Process - #import inspect - #print MetaArray, id(MetaArray), inspect.getmodule(MetaArray) - - + self._info = ma._info._getValue() @staticmethod def mapHDF5Array(data, writable=False): @@ -999,9 +972,6 @@ class MetaArray(object): if off is None: raise Exception("This dataset uses chunked storage; it can not be memory-mapped. (store using mappable=True)") return np.memmap(filename=data.file.filename, offset=off, dtype=data.dtype, shape=data.shape, mode=mode) - - - @staticmethod def readHDF5Meta(root, mmap=False): @@ -1010,6 +980,8 @@ class MetaArray(object): ## Pull list of values from attributes and child objects for k in root.attrs: val = root.attrs[k] + if isinstance(val, bytes): + val = val.decode() if isinstance(val, basestring): ## strings need to be re-evaluated to their original types try: val = eval(val) @@ -1047,21 +1019,24 @@ class MetaArray(object): return d2 else: raise Exception("Don't understand metaType '%s'" % typ) - - def write(self, fileName, **opts): + def write(self, fileName, version=2, **opts): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis. compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape - """ - - if USE_HDF5 and HAVE_HDF5: + """ + if version == 1: + return self.writeMa(fileName, **opts) + elif USE_HDF5 and HAVE_HDF5: return self.writeHDF5(fileName, **opts) else: - return self.writeMa(fileName, **opts) + if not HAVE_HDF5: + raise Exception("h5py is required for writing .ma version 2 files") + else: + raise Exception("HDF5 is required for writing .ma version 2 files, but it has been disabled.") def writeMeta(self, fileName): """Used to re-write meta info to the given file. @@ -1074,7 +1049,6 @@ class MetaArray(object): self.writeHDF5Meta(f, 'info', self._info) f.close() - def writeHDF5(self, fileName, **opts): ## default options for writing datasets comp = self.defaultCompression @@ -1110,8 +1084,7 @@ class MetaArray(object): ## update options if they were passed in for k in dsOpts: if k in opts: - dsOpts[k] = opts[k] - + dsOpts[k] = opts[k] ## If mappable is in options, it disables chunking/compression if opts.get('mappable', False): From e58b7d4708290ae9af242dc1fd3d879677ebe2d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Aug 2018 09:02:48 -0700 Subject: [PATCH 107/235] minor correction --- pyqtgraph/metaarray/MetaArray.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 6ce9b05b..63aee2ec 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -1020,7 +1020,7 @@ class MetaArray(object): else: raise Exception("Don't understand metaType '%s'" % typ) - def write(self, fileName, version=2, **opts): + def write(self, fileName, **opts): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. @@ -1028,15 +1028,12 @@ class MetaArray(object): compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape """ - if version == 1: + if USE_HDF5 is False: return self.writeMa(fileName, **opts) - elif USE_HDF5 and HAVE_HDF5: + elif HAVE_HDF5 is True: return self.writeHDF5(fileName, **opts) else: - if not HAVE_HDF5: - raise Exception("h5py is required for writing .ma version 2 files") - else: - raise Exception("HDF5 is required for writing .ma version 2 files, but it has been disabled.") + raise Exception("h5py is required for writing .ma hdf5 files, but it could not be imported.") def writeMeta(self, fileName): """Used to re-write meta info to the given file. From 542f4b446b98e26a08d1a815e0534578a06ed0d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 3 Aug 2018 18:18:28 -0700 Subject: [PATCH 108/235] Add eq() support for comparing dict, list, tuple --- pyqtgraph/functions.py | 20 ++++++++++++++++++++ pyqtgraph/tests/test_functions.py | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5cbb177e..ef2d7449 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -424,6 +424,8 @@ def eq(a, b): 3. When comparing arrays, returns False if the array shapes are not the same. 4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas the == operator would return a boolean array). + 5. Collections (dict, list, etc.) must have the same type to be considered equal. One + consequence is that comparing a dict to an OrderedDict will always return False. """ if a is b: return True @@ -440,6 +442,24 @@ def eq(a, b): if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype): return False + # Recursively handle common containers + if isinstance(a, dict) and isinstance(b, dict): + if type(a) != type(b) or len(a) != len(b): + return False + if a.keys() != b.keys(): + return False + for k,v in a.items(): + if not eq(v, b[k]): + return False + return True + if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + if type(a) != type(b) or len(a) != len(b): + return False + for v1,v2 in zip(a, b): + if not eq(v1, v2): + return False + return True + # Test for equivalence. # If the test raises a recognized exception, then return Falase try: diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index e013fe42..fcd16254 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,11 +1,15 @@ import pyqtgraph as pg import numpy as np import sys +from copy import deepcopy +from collections import OrderedDict from numpy.testing import assert_array_almost_equal, assert_almost_equal import pytest + np.random.seed(12345) + def testSolve3D(): p1 = np.array([[0,0,0,1], [1,0,0,1], @@ -356,6 +360,29 @@ def test_eq(): assert eq(a4, a4.copy()) assert not eq(a4, a4.T) + # test containers + + assert not eq({'a': 1}, {'a': 1, 'b': 2}) + assert not eq({'a': 1}, {'a': 2}) + d1 = {'x': 1, 'y': np.nan, 3: ['a', np.nan, a3, 7, 2.3], 4: a4} + d2 = deepcopy(d1) + assert eq(d1, d2) + assert eq(OrderedDict(d1), OrderedDict(d2)) + assert not eq(OrderedDict(d1), d2) + items = list(d1.items()) + assert not eq(OrderedDict(items), OrderedDict(reversed(items))) + + assert not eq([1,2,3], [1,2,3,4]) + l1 = [d1, np.inf, -np.inf, np.nan] + l2 = deepcopy(l1) + t1 = tuple(l1) + t2 = tuple(l2) + assert eq(l1, l2) + assert eq(t1, t2) + + assert eq(set(range(10)), set(range(10))) + assert not eq(set(range(10)), set(range(9))) + if __name__ == '__main__': test_interpolateArray() \ No newline at end of file From 7cb27594a5ec54c23b3eca2b6127975af757eac9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 6 Aug 2018 09:05:50 -0700 Subject: [PATCH 109/235] fix dict keys comparison --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ef2d7449..8a29d107 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -446,7 +446,7 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if a.keys() != b.keys(): + if set(a.keys()) != set(b.keys()): return False for k,v in a.items(): if not eq(v, b[k]): From a8529e48f35a6cc55ec68b53bcd6a7b37deb0b52 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 6 Aug 2018 09:17:23 -0700 Subject: [PATCH 110/235] faster keys comparison --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8a29d107..ef959466 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -446,7 +446,7 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if set(a.keys()) != set(b.keys()): + if sorted(a.keys()) != sorted(b.keys()): return False for k,v in a.items(): if not eq(v, b[k]): From 477feb777bfa31c6427351208df7adeced489b1c Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 20 Nov 2019 21:22:31 -0800 Subject: [PATCH 111/235] import h5py.highlevel is deprecated, use import h5py instead --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 63aee2ec..1410e40c 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -20,7 +20,7 @@ from ..python2_3 import basestring ## This can be overridden by setting USE_HDF5 = False USE_HDF5 = True try: - import h5py.highlevel + import h5py HAVE_HDF5 = True except: USE_HDF5 = False From 71c4807559b3a129ee360062257ad50406ca1271 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 20 Nov 2019 21:22:46 -0800 Subject: [PATCH 112/235] fix dict eq() checks --- pyqtgraph/functions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ef959466..6cf5f98d 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -11,6 +11,7 @@ import numpy as np import decimal, re import ctypes import sys, struct +from .pgcollections import OrderedDict from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, QT_LIB from . import getConfigOption, setConfigOptions @@ -446,11 +447,15 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if sorted(a.keys()) != sorted(b.keys()): + if set(a.keys()) != set(b.keys()): return False - for k,v in a.items(): + for k, v in a.items(): if not eq(v, b[k]): return False + if isinstance(a, OrderedDict) or sys.version_info >= (3, 7): + for a_item, b_item in zip(a.items(), b.items()): + if not eq(a_item, b_item): + return False return True if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): if type(a) != type(b) or len(a) != len(b): From 770ce06dc1b4d721bd7afb1a815384901f5bc0d1 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Thu, 21 Nov 2019 08:42:44 -0800 Subject: [PATCH 113/235] Revert "Allow MetaArray.__array__ to accept an optional dtype arg" --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 18f7250a..1410e40c 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -357,7 +357,7 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self, dtype=None): + def __array__(self): ## supports np.array(metaarray_instance) return self.asarray() From ef4ca9e9ea94c65390cf10206ec73fc9cbb186d7 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 21 Nov 2019 08:46:25 -0800 Subject: [PATCH 114/235] Incorporated correction luke suggested --- pyqtgraph/metaarray/MetaArray.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 1410e40c..374c9acf 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -357,9 +357,12 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self): + def __array__(self, dtype=None): ## supports np.array(metaarray_instance) - return self.asarray() + if dtype is None: + return self.asarray() + else: + return self.asarray().astype(dtype) def view(self, typ): ## deprecated; kept for backward compatibility From b02ada024b45de93eeab2218029094223b0ed9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Sat, 23 Nov 2019 13:10:49 +0100 Subject: [PATCH 115/235] fix error on SVG export of scatter plots: KeyError: 'resolutionScale' --- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 439d94ad..541ab13b 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -98,7 +98,7 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - scaler = self._exportOpts['resolutionScale'] + scaler = self._exportOpts.get('resolutionScale', 1.0) return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1) if viewportTransform is None: From 2a01c3848a55357ef6c04c09427511c4c70a0a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Mon, 25 Nov 2019 14:14:15 +0100 Subject: [PATCH 116/235] fix wrong offset when drawing symbol --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index bfc41969..aa2cabba 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -781,7 +781,7 @@ class ScatterPlotItem(GraphicsObject): pts = pts[:,viewMask] for i, rec in enumerate(data): p.resetTransform() - p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']/2) + p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: From 57909aab454dc5da35e7dedb5bd762589d63134c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Tue, 3 Dec 2019 11:51:44 +0100 Subject: [PATCH 117/235] dump ExportDialog.exporterParameters, b/c it prevents correct aspect ratio on image export (close #1087) --- pyqtgraph/GraphicsScene/exportDialog.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 045698fe..61f2233d 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -22,8 +22,6 @@ class ExportDialog(QtGui.QWidget): self.shown = False self.currentExporter = None self.scene = scene - - self.exporterParameters = {} self.selectBox = QtGui.QGraphicsRectItem() self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine)) @@ -124,16 +122,7 @@ class ExportDialog(QtGui.QWidget): expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) - if prev: - oldtext = str(prev.text()) - self.exporterParameters[oldtext] = self.currentExporter.parameters() - newtext = str(item.text()) - if newtext in self.exporterParameters.keys(): - params = self.exporterParameters[newtext] - exp.params = params - else: - params = exp.parameters() - self.exporterParameters[newtext] = params + params = exp.parameters() if params is None: self.ui.paramTree.clear() From 9b6102900dea5d9a1e62cec90f0f12968a0b4c99 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sun, 15 Dec 2019 21:54:36 -0600 Subject: [PATCH 118/235] Update Changelog with PRs merged since August --- CHANGELOG | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 89c8eb13..9a3bcf4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ pyqtgraph-0.11.0 (in development) New Features: + - #101: GridItem formatting options - #410: SpinBox custom formatting options - #415: ROI.getArrayRegion supports nearest-neighbor interpolation (especially handy for label images) - #428: DataTreeWidget: @@ -59,12 +60,17 @@ pyqtgraph-0.11.0 (in development) - #813,814,817: Performance improvements - #837: Added options for field variables in ColorMapWidget - #840, 932: Improve clipping behavior + - #841: Set color of tick-labels separately - #922: Curve fill for fill-patches - #996: Allow the update of LegendItem + - #1023: Add bookkeeping exporter parameters + - #1072: HDF5Exporter handling of ragged curves with tests API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because widgets can be placed into a new window just by calling show(). + - #158: Make DockArea compatible with Qt Designer + - #406: Applying alpha mask on numpy.nan data values - #566: ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. The result is visually the same, but children of ArrowItem are no longer rotated (this allows screen-aligned text to be attached more easily). @@ -83,12 +89,23 @@ pyqtgraph-0.11.0 (in development) - #657: When a floating Dock window is closed, the dock is now returned home - #771: Suppress RuntimeWarning for arrays containing zeros in logscale - #942: If the visible GraphicsView is garbage collected, a warning is issued. + - #958: Nicer Legend - #963: Last image in image-stack can now be selected with the z-slider - #992: Added a setter for GlGridItem.color. - #999: Make outline around fillLevel optional. - #1014: Enable various arguments as color in colormap. + - #1044: Raise AttributeError in __getattr__ in graphicsWindows (deprecated) + - #1055: Remove global for CONFIG_OPTIONS in setConfigOption + - #1066: Add RemoteGraphicsView to __init__.py + - #1069: Allow actions to display title instead of name + - #1074: Validate min/max text inputs in ViewBoxMenu + - #1076: Reset currentRow and currentCol on GraphicsLayout.clear() + - #1079: Improve performance of updateData PlotCurveItem + - #1082: Allow MetaArray.__array__ to accept an optional dtype arg Bugfixes: + - #88: Fixed image scatterplot export + - #356: Fix some NumPy warnings - #408: Fix `cleanup` when the running qt application is not a QApplication - #410: SpinBox fixes - fixed bug with exponents disappearing after edit @@ -104,7 +121,7 @@ pyqtgraph-0.11.0 (in development) - fixed spinbox height too small for font size - ROI subclass getArrayRegion methods are a bit more consistent (still need work) - #424: Fix crash when running pyqtgraph with python -OO - - #429: fix fft premature slicing away of 0 freq bin + - #429: Fix fft premature slicing away of 0 freq bin - #458: Fixed image export problems with new numpy API - #478: Fixed PySide image memory leak - #475: Fixed unicode error when exporting to SVG with non-ascii symbols @@ -137,17 +154,18 @@ pyqtgraph-0.11.0 (in development) - #592,595: Fix InvisibleRootItem issues introduced in #518 - #596: Fix polyline click causing lines to bedrawn to the wrong node - #598: Better ParameterTree support for dark themes + - #599: Prevent invalid list access in GraphicsScene - #623: Fix PyQt5 / ScatterPlot issue with custom symbols - #626: Fix OpenGL texture state leaking to wrong items - #627: Fix ConsoleWidget stack handling on python 3.5 - #633: Fix OpenGL cylinder geometry - #637: Fix TypeError in isosurface - #641,642: Fix SVG export on Qt5 / high-DPI displays - - #645: scatterplotwidget behaves nicely when data contains infs + - #645: ScatterPlotWidget behaves nicely when data contains infs - #653: ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581 - #648: fix color ignored in GLGridItem - - #671: fixed SVG export failing if the first value of a plot is nan - - #674: fixed parallelizer leaking file handles + - #671: Fixed SVG export failing if the first value of a plot is nan + - #674: Fixed parallelizer leaking file handles - #675: Gracefully handle case where image data has size==0 - #679: Fix overflow in Point.length() - #682: Fix: mkQApp returned None if a QApplication was already created elsewhere @@ -164,6 +182,7 @@ pyqtgraph-0.11.0 (in development) it was causing auto range to be disabled. - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events + - #742: Fix Metaarray in python 3 - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. - #763: Fix OverflowError when using Auto Downsampling. - #767: Fix Image display for images with the same value everywhere. @@ -185,16 +204,29 @@ pyqtgraph-0.11.0 (in development) - #949: Fix multiline parameters (such as arrays) reading from config files. - #951: Fix event firing from scale handler. - #952: Fix RotateFree handle dragging + - #953: Fix HistogramLUTWidget with background parameter - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #970: Always update transform when setting angle of a TextItem - #971: Fix a segfault stemming from incorrect signal disconnection. + - #972: Correctly include SI units for log AxisItems - #974: Fix recursion error when instancing CtrlNode. - #987: Fix visibility reset when PlotItems are removed. - #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8. - #1010: Fix matplotlib/CSV export. + - #1012: Fix circular texture centering - #1015: Iterators are now converted to NumPy arrays. - #1016: Fix synchronisation of multiple ImageViews with time axis. - #1017: Fix duplicate paint calls emitted by Items on ViewBox. - #1019: Fix disappearing GLGridItems when PlotItems are removed and readded. + - #1024: Prevent element-wise string comparison + - #1031: Reset ParentItem to None on removing from PlotItem/ViewBox + - #1044: Fix PlotCurveItem.paintGL + - #1048: Fix bounding box for InfiniteLine + - #1062: Fix flowchart context menu redundant menu + - #1062: Fix a typo + - #1073: Fix Python3 compatibility + - #1083: Fix SVG export of scatter plots + - #1085: Fix ofset when drawing symbol Maintenance: - Lots of new unit tests @@ -204,6 +236,10 @@ pyqtgraph-0.11.0 (in development) - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI + - #991: Use Azure Pipelines to do style checks, Add .pre-commit-config.yaml + - #1042: Close windows at the end of test functions + - #1046: Establish minimum numpy version, remove legacy workarounds + - #1067: Make scipy dependency optional pyqtgraph-0.10.0 From 61104cd43c91feadb51f28ea16c2a33e6de74756 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 24 Dec 2019 10:04:31 -0800 Subject: [PATCH 119/235] Fix small oversight in LegendItem --- pyqtgraph/graphicsItems/LegendItem.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index d8986011..5c3083a2 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from .GraphicsWidget import GraphicsWidget from .LabelItem import LabelItem from ..Qt import QtGui, QtCore @@ -123,7 +124,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) - if self.offset is not None: + if self.opts['offset'] is not None: offset = Point(self.opts['offset']) anchorx = 1 if offset[0] <= 0 else 0 anchory = 1 if offset[1] <= 0 else 0 @@ -251,7 +252,3 @@ class ItemSample(GraphicsWidget): p.translate(10, 10) path = drawSymbol(p, symbol, size, pen, brush) - - - - From 65d2ac58e035083ba480aef4ef112f24ff6e40ac Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Wed, 8 Jan 2020 21:19:09 +0100 Subject: [PATCH 120/235] fix for nextafter --- pyqtgraph/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6cf5f98d..45e9aad6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1134,7 +1134,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal != 0 or maxVal != scale: if minVal == maxVal: maxVal = np.nextafter(maxVal, 2*maxVal) - data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) + rng = maxVal-minVal + rng = 1 if rng == 0 else rng + data = rescaleData(data, scale/rng, minVal, dtype=dtype) profile() # apply LUT if given From 660ac675f117918cf590c71e341941f468c9738c Mon Sep 17 00:00:00 2001 From: Xinfa Joseph Zhu Date: Thu, 9 Jan 2020 15:23:49 -0600 Subject: [PATCH 121/235] Fix typo bug --- pyqtgraph/opengl/MeshData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 5bab4626..95ba88af 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -371,7 +371,7 @@ class MeshData(object): #pass def _computeEdges(self): - if not self.hasFaceIndexedData: + if not self.hasFaceIndexedData(): ## generate self._edges from self._faces nf = len(self._faces) edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) From 74294502bd2adead205af9576049a79233c14b12 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Fri, 10 Jan 2020 11:36:06 +0100 Subject: [PATCH 122/235] doc: Fix small mistake in introduction --- doc/source/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index 70161173..741acd30 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -73,7 +73,7 @@ How does it compare to... such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. -* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting +* pyqwt5: About as fast as pyqtgraph, but not quite as complete for plotting functionality. Image handling in pyqtgraph is much more complete (again, no ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is more portable than pyqwt, which often lags behind pyqt in development (I From adba81a8d86c362817727eda0b0da3738068cf92 Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sun, 9 Feb 2020 23:43:58 +0100 Subject: [PATCH 123/235] Syntax highlighting for examples. --- examples/__main__.py | 2 + examples/syntax.py | 186 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 examples/syntax.py diff --git a/examples/__main__.py b/examples/__main__.py index ffc38ff7..3867fbd3 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -10,6 +10,7 @@ from pyqtgraph.python2_3 import basestring from pyqtgraph.Qt import QtGui, QT_LIB from .utils import buildFileList, path, examples +from .syntax import PythonHighlighter if QT_LIB == 'PySide': @@ -33,6 +34,7 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) + self.hl = PythonHighlighter(self.ui.codeView.document()) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) self.codeBtn.hide() diff --git a/examples/syntax.py b/examples/syntax.py new file mode 100644 index 00000000..cd2cccf1 --- /dev/null +++ b/examples/syntax.py @@ -0,0 +1,186 @@ +# based on https://github.com/art1415926535/PyQt5-syntax-highlighting + +from pyqtgraph.Qt import QtCore, QtGui + +QRegExp = QtCore.QRegExp + +QFont = QtGui.QFont +QColor = QtGui.QColor +QTextCharFormat = QtGui.QTextCharFormat +QSyntaxHighlighter = QtGui.QSyntaxHighlighter + + +def format(color, style=''): + """ + Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + if type(color) is not str: + _color.setRgb(color[0], color[1], color[2]) + else: + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + return _format + + +# Syntax styles that can be shared by all languages +STYLES = { + 'keyword': format('blue'), + 'operator': format('red'), + 'brace': format('darkGray'), + 'defclass': format('black', 'bold'), + 'string': format('magenta'), + 'string2': format('darkMagenta'), + 'comment': format('darkGreen', 'italic'), + 'self': format('black', 'italic'), + 'numbers': format('brown'), +} + + +class PythonHighlighter(QSyntaxHighlighter): + """Syntax highlighter for the Python language. + """ + # Python keywords + keywords = [ + 'and', 'assert', 'break', 'class', 'continue', 'def', + 'del', 'elif', 'else', 'except', 'exec', 'finally', + 'for', 'from', 'global', 'if', 'import', 'in', + 'is', 'lambda', 'not', 'or', 'pass', 'print', + 'raise', 'return', 'try', 'while', 'yield', + 'None', 'True', 'False', + ] + + # Python operators + operators = [ + '=', + # Comparison + '==', '!=', '<', '<=', '>', '>=', + # Arithmetic + '\+', '-', '\*', '/', '//', '\%', '\*\*', + # In-place + '\+=', '-=', '\*=', '/=', '\%=', + # Bitwise + '\^', '\|', '\&', '\~', '>>', '<<', + ] + + # Python braces + braces = [ + '\{', '\}', '\(', '\)', '\[', '\]', + ] + + def __init__(self, document): + QSyntaxHighlighter.__init__(self, document) + + # Multi-line strings (expression, flag, style) + # FIXME: The triple-quotes in these two lines will mess up the + # syntax highlighting from this point onward + self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) + self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + + rules = [] + + # Keyword, operator, and brace rules + rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + for w in PythonHighlighter.keywords] + rules += [(r'%s' % o, 0, STYLES['operator']) + for o in PythonHighlighter.operators] + rules += [(r'%s' % b, 0, STYLES['brace']) + for b in PythonHighlighter.braces] + + # All other rules + rules += [ + + # 'self' + (r'\bself\b', 0, STYLES['self']), + + # 'def' followed by an identifier + (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + # 'class' followed by an identifier + (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + + # Numeric literals + (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + + # Double-quoted string, possibly containing escape sequences + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + # Single-quoted string, possibly containing escape sequences + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + + # From '#' until a newline + (r'#[^\n]*', 0, STYLES['comment']), + + ] + + # Build a QRegExp for each pattern + self.rules = [(QRegExp(pat), index, fmt) + for (pat, index, fmt) in rules] + + def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + # Do other syntax formatting + for expression, nth, format in self.rules: + index = expression.indexIn(text, 0) + + while index >= 0: + # We actually want the index of the nth match + index = expression.pos(nth) + length = len(expression.cap(nth)) + self.setFormat(index, length, format) + index = expression.indexIn(text, index + length) + + self.setCurrentBlockState(0) + + # Do multi-line strings + in_multiline = self.match_multiline(text, *self.tri_single) + if not in_multiline: + in_multiline = self.match_multiline(text, *self.tri_double) + + def match_multiline(self, text, delimiter, in_state, style): + """Do highlighting of multi-line strings. ``delimiter`` should be a + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and + ``in_state`` should be a unique integer to represent the corresponding + state changes when inside those strings. Returns True if we're still + inside a multi-line string when this function is finished. + """ + # If inside triple-single quotes, start at 0 + if self.previousBlockState() == in_state: + start = 0 + add = 0 + # Otherwise, look for the delimiter on this line + else: + start = delimiter.indexIn(text) + # Move past this match + add = delimiter.matchedLength() + + # As long as there's a delimiter match on this line... + while start >= 0: + # Look for the ending delimiter + end = delimiter.indexIn(text, start + add) + # Ending delimiter on this line? + if end >= add: + length = end - start + add + delimiter.matchedLength() + self.setCurrentBlockState(0) + # No; multi-line string + else: + self.setCurrentBlockState(in_state) + length = len(text) - start + add + # Apply formatting + self.setFormat(start, length, style) + # Look for the next match + start = delimiter.indexIn(text, start + length) + + # Return True if still inside a multi-line string, False otherwise + if self.currentBlockState() == in_state: + return True + else: + return False From 07af12d489ec82b5bbc4bfa14176a4b63dc7947c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 09:28:48 -0800 Subject: [PATCH 124/235] Update CI Config --- azure-pipelines.yml | 14 +++++----- azure-test-template.yml | 62 +++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 657189f8..b9abcf86 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,7 +23,7 @@ stages: jobs: - job: check_diff_size pool: - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' steps: - bash: | git config --global advice.detachedHead false @@ -56,7 +56,7 @@ stages: - job: "style_check" pool: - vmImage: "Ubuntu 16.04" + vmImage: "Ubuntu 18.04" steps: - task: UsePythonVersion@0 inputs: @@ -69,11 +69,11 @@ stages: - job: "build_wheel" pool: - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' steps: - task: UsePythonVersion@0 inputs: - versionSpec: 3.7 + versionSpec: 3.8 - script: | python -m pip install setuptools wheel python setup.py bdist_wheel --universal @@ -87,12 +87,12 @@ stages: - template: azure-test-template.yml parameters: name: linux - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' - template: azure-test-template.yml parameters: name: windows - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' - template: azure-test-template.yml parameters: name: macOS - vmImage: 'macOS-10.13' + vmImage: 'macOS-10.15' diff --git a/azure-test-template.yml b/azure-test-template.yml index 5d204009..bc1a16df 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -22,17 +22,13 @@ jobs: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python36-PySide2-5.9: - python.version: "3.6" - qt.bindings: "pyside2" - install.method: "conda" - Python37-PyQt-5.13: - python.version: '3.7' - qt.bindings: "PyQt5" - install.method: "pip" Python37-PySide2-5.13: python.version: "3.7" - qt.bindings: "PySide2" + qt.bindings: "pyside2" + install.method: "conda" + Python38-PyQt-5.14: + python.version: '3.8' + qt.bindings: "PyQt5" install.method: "pip" steps: @@ -75,22 +71,10 @@ jobs: if [ $(agent.os) == 'Linux' ] then echo "##vso[task.prependpath]$CONDA/bin" - if [ $(python.version) == '2.7' ] - then - echo "Grabbing Older Miniconda" - wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-Linux-x86_64.sh -O Miniconda.sh - bash Miniconda.sh -b -p $CONDA -f - fi elif [ $(agent.os) == 'Darwin' ] then sudo chown -R $USER $CONDA echo "##vso[task.prependpath]$CONDA/bin" - if [ $(python.version) == '2.7' ] - then - echo "Grabbing Older Miniconda" - wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-MacOSX-x86_64.sh -O Miniconda.sh - bash Miniconda.sh -b -p $CONDA -f - fi elif [ $(agent.os) == 'Windows_NT' ] then echo "##vso[task.prependpath]$CONDA/Scripts" @@ -103,25 +87,31 @@ jobs: - bash: | if [ $(install.method) == "conda" ] then - conda create --name test-environment-$(python.version) python=$(python.version) --yes - echo "Conda Info:" - conda info - echo "Installing qt-bindings" + conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet source activate test-environment-$(python.version) - - if [ $(agent.os) == "Linux" ] && [ $(python.version) == "2.7" ] + conda config --env --set always_yes true + if [ $(python.version) == '2.7' ] && [ $(agent.os) != 'Windows_NT' ] then - conda install $(qt.bindings) --yes - else - conda install -c conda-forge $(qt.bindings) --yes + conda config --set restore_free_channel true fi - echo "Installing remainder of dependencies" - conda install -c conda-forge numpy scipy six pyopengl h5py --yes + + if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ]) + then + conda config --prepend channels conda-forge + fi + conda info + if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] + then + pip install --upgrade pip==19.3.1 + conda install setuptools=44.0.0 --yes --quiet + conda install nomkl + fi + conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else - pip install $(qt.bindings) numpy scipy pyopengl six h5py + pip install $(qt.bindings) numpy scipy pyopengl h5py six fi echo "" - pip install pytest pytest-xdist pytest-cov coverage + pip install pytest pytest-cov coverage if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler==1.6.0 @@ -180,9 +170,9 @@ jobs: mkdir -p "$SCREENSHOT_DIR" # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" - pytest . -sv \ + pytest . -v \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html + --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 From 1549959902a2f7fadc0a01722c7e86d4fbd8762f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 16:24:33 -0800 Subject: [PATCH 125/235] Skipping problematic test on py2/qt4/linux --- azure-test-template.yml | 6 ------ pyqtgraph/graphicsItems/tests/test_ROI.py | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index bc1a16df..7ca6ba45 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -100,12 +100,6 @@ jobs: conda config --prepend channels conda-forge fi conda info - if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] - then - pip install --upgrade pip==19.3.1 - conda install setuptools=44.0.0 --yes --quiet - conda install nomkl - fi conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl h5py six diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 33a18217..2fc61d11 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -4,11 +4,15 @@ import pytest import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtTest from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow - +import pytest +import platform +import six app = pg.mkQApp() +reason = ("Test fails intermittently, will be deprecating py2/qt4 support soon anyway") +@pytest.mark.skipif(six.PY2 and pg.Qt.QT_LIB in {"PySide", "PyQt4"} and platform.system() == "Linux", reason=reason) def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) @@ -33,7 +37,7 @@ def test_getArrayRegion(transpose=False): finally: pg.setConfigOptions(imageAxisOrder=origMode) - +@pytest.mark.skipif(six.PY2 and pg.Qt.QT_LIB in {"PySide", "PyQt4"} and platform.system() == "Linux", reason=reason) def test_getArrayRegion_axisorder(): test_getArrayRegion(transpose=True) From f0d1c4eda1f776bb472489202ad5831666c34604 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 16:24:33 -0800 Subject: [PATCH 126/235] Skipping problematic test on py2/qt4/linux --- azure-pipelines.yml | 1 + azure-test-template.yml | 17 +++++++---------- pyqtgraph/graphicsItems/tests/test_ROI.py | 7 +++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b9abcf86..eb379119 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,6 +17,7 @@ pr: variables: OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' DEFAULT_MERGE_BRANCH: 'develop' + disable.coverage.autogenerate: 'true' stages: - stage: "pre_test" diff --git a/azure-test-template.yml b/azure-test-template.yml index bc1a16df..04fd7e42 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -83,39 +83,35 @@ jobs: fi displayName: 'Add Conda To $PATH' condition: eq(variables['install.method'], 'conda' ) + continueOnError: false - bash: | if [ $(install.method) == "conda" ] then + conda update --all --yes --quiet conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet source activate test-environment-$(python.version) conda config --env --set always_yes true - if [ $(python.version) == '2.7' ] && [ $(agent.os) != 'Windows_NT' ] + if [ $(python.version) == '2.7' ] then conda config --set restore_free_channel true fi - if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ]) then conda config --prepend channels conda-forge fi conda info - if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] - then - pip install --upgrade pip==19.3.1 - conda install setuptools=44.0.0 --yes --quiet - conda install nomkl - fi conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl h5py six fi - echo "" - pip install pytest pytest-cov coverage + pip install pytest pytest-cov coverage pytest-xdist if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler==1.6.0 export PYTEST_ADDOPTS="--faulthandler-timeout=15" + else + pip install pytest pytest-cov coverage fi displayName: "Install Dependencies" @@ -171,6 +167,7 @@ jobs: # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -v \ + -n 1 \ --junitxml=junit/test-results.xml \ --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 33a18217..10c6009b 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,14 +1,14 @@ +# -*- coding: utf-8 -*- import sys import numpy as np import pytest import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtTest from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow - +import pytest app = pg.mkQApp() - def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) @@ -33,7 +33,6 @@ def test_getArrayRegion(transpose=False): finally: pg.setConfigOptions(imageAxisOrder=origMode) - def test_getArrayRegion_axisorder(): test_getArrayRegion(transpose=True) @@ -135,7 +134,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): img2.setImage(rgn[0, ..., 0]) app.processEvents() # on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10 - if sys.platform == 'win32' and pg.Qt.QT_LIB in ('PyQt4', 'PySide'): + if pg.Qt.QT_LIB in {'PyQt4', 'PySide'}: pxCount = 10 else: pxCount=-1 From 428af4950d9d942ceb12dfd276abc0bad734f148 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 24 Feb 2020 23:00:09 -0800 Subject: [PATCH 127/235] unskip py3 tests, weakref works fine in a list --- pyqtgraph/tests/test_ref_cycles.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index e05c4ef1..b04390ca 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Test for unwanted reference cycles @@ -9,9 +10,7 @@ import six import pytest app = pg.mkQApp() -skipreason = ('unclear why test is failing on python 3. skipping until someone ' - 'has time to fix it. Or pyside is being used. This test is ' - 'failing on pyside for an unknown reason too.') +skipreason = ('This test is failing on pyside for an unknown reason.') def assert_alldead(refs): for ref in refs: @@ -37,10 +36,10 @@ def mkrefs(*objs): for o in obj: allObjs[id(o)] = o - return map(weakref.ref, allObjs.values()) + return list(map(weakref.ref, allObjs.values())) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -58,7 +57,7 @@ def test_PlotWidget(): for i in range(5): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -71,7 +70,7 @@ def test_ImageView(): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() From 19ae94765fd783885397da8dbbae4c59891f6027 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 24 Feb 2020 23:00:42 -0800 Subject: [PATCH 128/235] Skip tests involving loadUi with pyside2 5.14 --- examples/test_examples.py | 3 ++- pyqtgraph/tests/test_qt.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index c6fef377..f10fe358 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import -from pyqtgraph import Qt from . import utils from collections import namedtuple +from pyqtgraph import Qt import errno import importlib import itertools @@ -150,6 +150,7 @@ conditionalExamples = { ) } +@pytest.mark.skipif(Qt.QT_LIB == "PySide2" and "Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") @pytest.mark.parametrize( "frontend, f", [ diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index c86cd500..9a4f373b 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pyqtgraph as pg import gc, os import pytest @@ -14,6 +15,7 @@ def test_isQObjectAlive(): @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' 'packaged with conda') +@pytest.mark.skipif(pg.Qt.QT_LIB == "PySide2" and "pg.Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) From 3195ed4c8faacf621de2c93f581d9218f4eb9006 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Feb 2020 10:06:02 -0800 Subject: [PATCH 129/235] Skip some tests on pyside2 --- pyqtgraph/tests/test_ref_cycles.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index b04390ca..121a09e4 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -10,7 +10,7 @@ import six import pytest app = pg.mkQApp() -skipreason = ('This test is failing on pyside for an unknown reason.') +skipreason = ('This test is failing on pyside and pyside2 for an unknown reason.') def assert_alldead(refs): for ref in refs: @@ -35,11 +35,10 @@ def mkrefs(*objs): obj = [obj] for o in obj: allObjs[id(o)] = o - - return list(map(weakref.ref, allObjs.values())) + return [weakref.ref(obj) for obj in allObjs.values()] -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -57,7 +56,7 @@ def test_PlotWidget(): for i in range(5): assert_alldead(mkobjs()) -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -65,12 +64,12 @@ def test_ImageView(): iv.setImage(data) return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) - for i in range(5): + gc.collect() assert_alldead(mkobjs()) -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() From 8930adc27e233995efc527043503d3cf25bde8e6 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Feb 2020 10:06:15 -0800 Subject: [PATCH 130/235] Update tox config --- tox.ini | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/tox.ini b/tox.ini index 9091c8cb..f5c8e7a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,16 @@ [tox] envlist = - ; qt 5.12.x - py{27,37}-pyside2-pip - py{35,37}-pyqt5-pip + ; qt latest + py{37,38}-{pyqt5,pyside2}_latest - ; qt 5.9.7 - py{27,37}-pyqt5-conda - py{27,37}-pyside2-conda + ; qt 5.12.x (LTS) + py{36,37}-{pyqt5,pyside2}_512 - ; qt 5.6.2 - py35-pyqt5-conda - ; consider dropping support... - ; py35-pyside2-conda + ; qt 5.9.7 (LTS) + py36-{pyqt5,pyside2}_59_conda ; qt 4.8.7 - py{27,36}-pyqt4-conda - py{27,36}-pyside-conda - + py27-{pyqt4,pyside}_conda [base] deps = @@ -34,17 +28,22 @@ deps= {[base]deps} pytest-cov pytest-xdist - pyside2-pip: pyside2 - pyqt5-pip: pyqt5 + h5py + pyside2_512: pyside2>=5.12,<5.13 + pyqt5_512: pyqt5>=5.12,<5.13 + pyside2_latest: pyside2 + pyqt5_latest: pyqt5 conda_deps= - pyside2-conda: pyside2 - pyside-conda: pyside - pyqt5-conda: pyqt - pyqt4-conda: pyqt=4 - + pyside2_59_conda: pyside2=5.9 + pyqt5_59_conda: pyqt=5.9 + pyqt4_conda: pyqt=4 + pyside_conda: pyside + conda_channels= conda-forge + free + commands= python -c "import pyqtgraph as pg; pg.systemInfo()" pytest {posargs:.} From 7199a4f4ce32ab31566426100f0bc6cb31efd3a9 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:25:34 -0800 Subject: [PATCH 131/235] deepcopy(dict) does not necessarily preserve insertion order --- pyqtgraph/tests/test_functions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index fcd16254..6a6aaa33 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -367,8 +367,10 @@ def test_eq(): d1 = {'x': 1, 'y': np.nan, 3: ['a', np.nan, a3, 7, 2.3], 4: a4} d2 = deepcopy(d1) assert eq(d1, d2) - assert eq(OrderedDict(d1), OrderedDict(d2)) - assert not eq(OrderedDict(d1), d2) + d1_ordered = OrderedDict(d1) + d2_ordered = deepcopy(d1_ordered) + assert eq(d1_ordered, d2_ordered) + assert not eq(d1_ordered, d2) items = list(d1.items()) assert not eq(OrderedDict(items), OrderedDict(reversed(items))) From 6ed8a405feb25dba8511c1f281ae52e0be134f2a Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:27:10 -0800 Subject: [PATCH 132/235] Address FutureWarning about implicit float to int conversions --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- pyqtgraph/graphicsItems/GradientEditorItem.py | 2 +- pyqtgraph/graphicsItems/GridItem.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index da57403f..e97f66d3 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -921,7 +921,7 @@ class AxisItem(GraphicsWidget): p2[axis] += tickLength*tickDir tickPen = self.pen() color = tickPen.color() - color.setAlpha(lineAlpha) + color.setAlpha(int(lineAlpha)) tickPen.setColor(color) tickSpecs.append((tickPen, Point(p1), Point(p2))) profiler('compute ticks') @@ -1078,7 +1078,7 @@ class AxisItem(GraphicsWidget): p.setFont(self.tickFont) p.setPen(self.textPen()) for rect, flags, text in textSpecs: - p.drawText(rect, flags, text) + p.drawText(rect, flags.__int__(), text) profiler('draw text') diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index b360b2f7..1cb11d1c 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -654,7 +654,7 @@ class GradientEditorItem(TickSliderItem): s = s1 * (1.-f) + s2 * f v = v1 * (1.-f) + v2 * f c = QtGui.QColor() - c.setHsv(h,s,v) + c.setHsv(*map(int, [h,s,v])) if toQColor: return c else: diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 0b1eb525..db64cbbf 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -153,7 +153,7 @@ class GridItem(UIGraphicsItem): continue ppl = dim[ax] / nl[ax] - c = np.clip(5.*(ppl-3), 0., 50.) + c = np.clip(5 * (ppl-3), 0., 50.).astype(int) linePen = self.opts['pen'] lineColor = self.opts['pen'].color() From ae776a807d6dcf5e1ab2124ab6bc7af9cb84af27 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:28:36 -0800 Subject: [PATCH 133/235] Filter out expected warnings --- pytest.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index f53aea00..355e9dfd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,4 +12,8 @@ filterwarnings = ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning - ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning + ignore:.*'U' mode is deprecated.*:DeprecationWarning + # py36/pyside2_512 specific issue + ignore:split\(\) requires a non-empty pattern match\.:FutureWarning + # pyqtgraph specific warning we want to ignore during testing + ignore:Visible window deleted. To prevent this, store a reference to the window object. \ No newline at end of file From 87d6eae84d917aeb32ad5ffea3f0f36baaccd09c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:29:16 -0800 Subject: [PATCH 134/235] Remove py2 pip warning message --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index f5c8e7a6..130085ba 100644 --- a/tox.ini +++ b/tox.ini @@ -24,10 +24,10 @@ deps = [testenv] passenv = DISPLAY XAUTHORITY +setenv = PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command deps= {[base]deps} pytest-cov - pytest-xdist h5py pyside2_512: pyside2>=5.12,<5.13 pyqt5_512: pyqt5>=5.12,<5.13 @@ -46,4 +46,4 @@ conda_channels= commands= python -c "import pyqtgraph as pg; pg.systemInfo()" - pytest {posargs:.} + pytest {posargs:} From 1d552feaf08dc7e25912b415a21d2ee126e246cb Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:48:24 -0800 Subject: [PATCH 135/235] Update readme and contributing files --- CONTRIBUTING.md | 22 +++++++--------------- README.md | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9af2e508..461e9b14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,11 +9,14 @@ Please use the following guidelines when preparing changes: * The preferred method for submitting changes is by github pull request against the "develop" branch. * Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected. * For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort. - * Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy. +* The following deprecations are being considered by the maintainers + * `pyqtgraph.opengl` may be deprecated and replaced with `VisPy` functionality + * After v0.11, pyqtgraph will adopt [NEP-29](https://numpy.org/neps/nep-0029-deprecation_policy.html) which will effectively mean that python2 support will be deprecated + * Qt4 will be deprecated shortly, as well as Qt5<5.9 (and potentially <5.12) ## Documentation -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. * Documentation is generated with sphinx; please check that docstring changes compile correctly ## Style guidelines @@ -55,9 +58,7 @@ To make use of `pre-commit`, have it available in your `$PATH` and run `pre-comm * pytest-xdist * Optional: pytest-xvfb -If you have pytest < 5, you may also want to install the pytest-faulthandler -plugin to output extra debugging information in case of test failures. This -isn't necessary with pytest 5+ as the plugin was merged into core pytest. +If you have `pytest<5` (used in python2), you may also want to install `pytest-faulthandler==1.6` plugin to output extra debugging information in case of test failures. This isn't necessary with `pytest>=5` ### Tox @@ -68,13 +69,4 @@ As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make ### Continous Integration -For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations - -* Python2.7 with PyQt4 -* Python2.7 with PySide -* Python3.6 with PyQt5-5.9 -* Python3.6 with PySide2-5.9 -* Python3.7 with PyQt5-5.12 -* Python3.7 with PySide2-5.12 - -More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) +For our Continuous Integration, we utilize Azure Pipelines. Tested configurations are visible on [README](README.md). More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) diff --git a/README.md b/README.md index 914523fd..b461f4f6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) - PyQtGraph ========= @@ -20,7 +19,8 @@ Requirements ------------ * PyQt 4.8+, PySide, PyQt5, or PySide2 -* python 2.7, or 3.x + * PySide2 5.14 does not have loadUiType functionality, and thus the example application will not work. You can follow along with restoring that functionality [here](https://bugreports.qt.io/browse/PYSIDE-1223). +* Python 2.7, or 3.x * Required * `numpy` * Optional @@ -34,14 +34,15 @@ Requirements Qt Bindings Test Matrix ----------------------- -Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings. +The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images. -| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 | -| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | -| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | -| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | -| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | +| :----------- | :----------------: | :----------------: | :----------------: | :----------------: | +| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | +| PySide1 | :white_check_mark: | :x: | :x: | :x: | +| PyQt-5.9 | :x: | :white_check_mark: | :x: | :x: | +| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | +| PyQt-5.14 | :x: | :x: | :x: | :white_check_mark: | * pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible * on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work From 3158c5b4db320853bfad6028d617184f0de74323 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 29 Feb 2020 14:38:19 -0800 Subject: [PATCH 136/235] Use int() instead of .__int__() --- pyqtgraph/graphicsItems/AxisItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e97f66d3..2601ecae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..python2_3 import asUnicode import numpy as np @@ -1078,7 +1079,7 @@ class AxisItem(GraphicsWidget): p.setFont(self.tickFont) p.setPen(self.textPen()) for rect, flags, text in textSpecs: - p.drawText(rect, flags.__int__(), text) + p.drawText(rect, int(flags), text) profiler('draw text') From 6985be2a6fe2133c9229bafc439f013bc5825ec5 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sun, 1 Mar 2020 17:46:01 +0100 Subject: [PATCH 137/235] replaced incompatible string construction --- pyqtgraph/exporters/ImageExporter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index e600afc9..a8d235a8 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -47,10 +47,7 @@ class ImageExporter(Exporter): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - if QT_LIB in ['PySide', 'PySide2']: - filter = ["*."+str(f, encoding='utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] - else: - filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] + filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] for p in preferred[::-1]: if p in filter: From 3509d79c0f9fc9cfd9b96caa542dea955b7d2d09 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Fri, 6 Mar 2020 15:02:39 +0000 Subject: [PATCH 138/235] bug fix for `setPen`, `setBrush`, ... Fixes a bug where `setPen`, `setBrush` and `setLabelTextColor` would fail because they call `LegendItem.paint` without a pen. They should instead call `LegendItem.update`. --- pyqtgraph/graphicsItems/LegendItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 5c3083a2..7d60f37a 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -94,7 +94,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): pen = fn.mkPen(*args, **kargs) self.opts['pen'] = pen - self.paint() + self.update() def brush(self): return self.opts['brush'] @@ -105,7 +105,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): return self.opts['brush'] = brush - self.paint() + self.update() def labelTextColor(self): return self.opts['labelTextColor'] @@ -120,7 +120,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): for sample, label in self.items: label.setAttr('color', self.opts['labelTextColor']) - self.paint() + self.update() def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) From db6341de129d3249ca72140e0d7ef15b6bb5988b Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 6 Mar 2020 10:35:19 -0800 Subject: [PATCH 139/235] Removing use of travis CI --- .travis.yml | 195 ---------------------------------------------------- 1 file changed, 195 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 173fa668..00000000 --- a/.travis.yml +++ /dev/null @@ -1,195 +0,0 @@ -language: python -sudo: false -# Credit: Original .travis.yml lifted from VisPy - -# Here we use anaconda for 2.6 and 3.3, since it provides the simplest -# interface for running different versions of Python. We could also use -# it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which -# allows for more complete testing. -notifications: - email: false - -env: - # Enable python 2 and python 3 builds - # Note that the python 2.6 support ended. - - PYTHON=2.7 QT=pyqt4 TEST=extra - - PYTHON=2.7 QT=pyside TEST=standard - - PYTHON=3.5 QT=pyqt5 TEST=standard - # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda - #- PYTHON=3.2 QT=pyqt5 TEST=standard - -services: - - xvfb - -before_install: - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi - - chmod +x miniconda.sh - - ./miniconda.sh -b -p /home/travis/mc - - export PATH=/home/travis/mc/bin:$PATH - - # not sure what is if block is for - - if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then - GIT_TARGET_EXTRA="+refs/heads/${TRAVIS_BRANCH}"; - GIT_SOURCE_EXTRA="+refs/pull/${TRAVIS_PULL_REQUEST}/merge"; - else - GIT_TARGET_EXTRA=""; - GIT_SOURCE_EXTRA=""; - fi; - - # to aid in debugging - - echo ${TRAVIS_BRANCH} - - echo ${TRAVIS_REPO_SLUG} - - echo ${GIT_TARGET_EXTRA} - - echo ${GIT_SOURCE_EXTRA} - -install: - - export GIT_FULL_HASH=`git rev-parse HEAD` - - conda update conda --yes - - conda create -n test_env python=${PYTHON} --yes - - source activate test_env - - conda install numpy scipy pyopengl pytest flake8 six coverage --yes - - echo ${QT} - - echo ${TEST} - - echo ${PYTHON} - - - if [ "${QT}" == "pyqt5" ]; then - conda install pyqt --yes; - fi; - - if [ "${QT}" == "pyqt4" ]; then - conda install pyqt=4 --yes; - fi; - - if [ "${QT}" == "pyside" ]; then - conda install pyside --yes; - fi; - - pip install pytest-xdist # multi-thread pytest - - pip install pytest-cov # add coverage stats - - # faulthandler support not built in to pytest for python 2.7 - - if [ "${PYTHON}" == "2.7" ]; then - pip install pytest-faulthandler; - export PYTEST_ADDOPTS="--faulthandler-timeout=15"; - fi; - - # Debugging helpers - - uname -a - - cat /etc/issue - - if [ "${PYTHON}" == "2.7" ]; then - python --version; - else - python3 --version; - fi; - -before_script: - # We need to create a (fake) display on Travis, let's use a funny resolution - - export DISPLAY=:99.0 - - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render - - # Make sure everyone uses the correct python (this is handled by conda) - - which python - - python --version - - pwd - - ls - # Help color output from each test - - RESET='\033[0m'; - RED='\033[00;31m'; - GREEN='\033[00;32m'; - YELLOW='\033[00;33m'; - BLUE='\033[00;34m'; - PURPLE='\033[00;35m'; - CYAN='\033[00;36m'; - WHITE='\033[00;37m'; - start_test() { - echo -e "${BLUE}======== Starting $1 ========${RESET}"; - }; - check_output() { - ret=$?; - if [ $ret == 0 ]; then - echo -e "${GREEN}>>>>>> $1 passed <<<<<<${RESET}"; - else - echo -e "${RED}>>>>>> $1 FAILED <<<<<<${RESET}"; - fi; - return $ret; - }; - - - if [ "${TEST}" == "extra" ]; then - start_test "repo size check"; - mkdir ~/repo-clone && cd ~/repo-clone && - git init && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git && - git fetch origin ${GIT_TARGET_EXTRA} && - git checkout -qf FETCH_HEAD && - git tag travis-merge-target && - git gc --aggressive && - TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` && - git pull origin ${GIT_SOURCE_EXTRA} && - git gc --aggressive && - MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` && - if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then - SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`; - else - SIZE_DIFF=0; - fi; - fi; - -script: - - - source activate test_env - - # Check system info - - python -c "import pyqtgraph as pg; pg.systemInfo()" - - - # Check install works - - start_test "install test"; - python setup.py --quiet install; - check_output "install test"; - - # Run unit tests - - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv; - check_output "unit tests"; - - echo "test script finished. Current directory:" - - pwd - - # check line endings - - if [ "${TEST}" == "extra" ]; then - start_test "line ending check"; - ! find ./ -name "*.py" | xargs file | grep CRLF && - ! find ./ -name "*.rst" | xargs file | grep CRLF; - check_output "line ending check"; - fi; - - # Check repo size does not expand too much - - if [ "${TEST}" == "extra" ]; then - start_test "repo size check"; - echo -e "Estimated content size difference = ${SIZE_DIFF} kB" && - test ${SIZE_DIFF} -lt 100; - check_output "repo size check"; - fi; - - # Check for style issues - - if [ "${TEST}" == "extra" ]; then - start_test "style check"; - cd ~/repo-clone && - git reset -q travis-merge-target && - python setup.py style && - check_output "style check"; - fi; - - # Check double-install fails - # Note the bash -c is because travis strips off the ! otherwise. - - start_test "double install test"; - bash -c "! python setup.py --quiet install"; - check_output "double install test"; - - # Check we can import pg - - start_test "import test"; - echo "import sys; print(sys.path)" | python && - cd /; echo "import pyqtgraph.examples" | python; - check_output "import test"; - -after_success: - - cd /home/travis/build/pyqtgraph/pyqtgraph - - pip install codecov --upgrade # add coverage integration service - - codecov - - pip install coveralls --upgrade # add another coverage integration service - - coveralls From 221d5d88305e42dc59557138d3f2a3d640058420 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 6 Mar 2020 10:35:31 -0800 Subject: [PATCH 140/235] No longer usign mailmap --- .mailmap | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .mailmap diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 025cf940..00000000 --- a/.mailmap +++ /dev/null @@ -1,12 +0,0 @@ -Luke Campagnola Luke Campagnola <> -Luke Campagnola Luke Campagnola -Megan Kratz meganbkratz@gmail.com <> -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Ingo Breßler Ingo Breßler -Ingo Breßler Ingo B. - From 412698c8bb0358d182c1e4480a1647e7a0aaf2ba Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sat, 7 Mar 2020 22:42:01 +0100 Subject: [PATCH 141/235] Dark mode support. --- examples/__main__.py | 32 +++++++++++- examples/syntax.py | 118 +++++++++++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 3867fbd3..22dd7ef0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -22,6 +22,16 @@ elif QT_LIB == 'PyQt5': else: from .exampleLoaderTemplate_pyqt import Ui_Form +class App(QtGui.QApplication): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.paletteChanged.connect(self.onPaletteChange) + self.onPaletteChange(self.palette()) + + def onPaletteChange(self, palette): + self.dark_mode = palette.base().color().name().lower() != "#ffffff" + class ExampleLoader(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) @@ -34,6 +44,7 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) + #self.simulate_black_mode() self.hl = PythonHighlighter(self.ui.codeView.document()) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) @@ -53,6 +64,25 @@ class ExampleLoader(QtGui.QMainWindow): self.ui.codeView.textChanged.connect(self.codeEdited) self.codeBtn.clicked.connect(self.runEditedCode) + def simulate_black_mode(self): + """ + used to simulate MacOS "black mode" on other platforms + intended for debug only, as it manage only the QPlainTextEdit + """ + # first, a dark background + c = QtGui.QColor('#171717') + p = self.ui.codeView.palette() + p.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, c) + p.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, c) + self.ui.codeView.setPalette(p) + # then, a light font + f = QtGui.QTextCharFormat() + f.setForeground(QtGui.QColor('white')) + self.ui.codeView.setCurrentCharFormat(f) + # finally, override application automatic detection + app = QtGui.QApplication.instance() + app.dark_mode = True + def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) @@ -117,7 +147,7 @@ class ExampleLoader(QtGui.QMainWindow): self.loadFile(edited=True) def run(): - app = QtGui.QApplication([]) + app = App([]) loader = ExampleLoader() app.exec_() diff --git a/examples/syntax.py b/examples/syntax.py index cd2cccf1..95417827 100644 --- a/examples/syntax.py +++ b/examples/syntax.py @@ -30,17 +30,75 @@ def format(color, style=''): return _format -# Syntax styles that can be shared by all languages -STYLES = { - 'keyword': format('blue'), - 'operator': format('red'), - 'brace': format('darkGray'), - 'defclass': format('black', 'bold'), - 'string': format('magenta'), - 'string2': format('darkMagenta'), - 'comment': format('darkGreen', 'italic'), - 'self': format('black', 'italic'), - 'numbers': format('brown'), +class LightThemeColors: + + Red = "#B71C1C" + Pink = "#FCE4EC" + Purple = "#4A148C" + DeepPurple = "#311B92" + Indigo = "#1A237E" + Blue = "#0D47A1" + LightBlue = "#01579B" + Cyan = "#006064" + Teal = "#004D40" + Green = "#1B5E20" + LightGreen = "#33691E" + Lime = "#827717" + Yellow = "#F57F17" + Amber = "#FF6F00" + Orange = "#E65100" + DeepOrange = "#BF360C" + Brown = "#3E2723" + Grey = "#212121" + BlueGrey = "#263238" + + +class DarkThemeColors: + + Red = "#F44336" + Pink = "#F48FB1" + Purple = "#CE93D8" + DeepPurple = "#B39DDB" + Indigo = "#9FA8DA" + Blue = "#90CAF9" + LightBlue = "#81D4FA" + Cyan = "#80DEEA" + Teal = "#80CBC4" + Green = "#A5D6A7" + LightGreen = "#C5E1A5" + Lime = "#E6EE9C" + Yellow = "#FFF59D" + Amber = "#FFE082" + Orange = "#FFCC80" + DeepOrange = "#FFAB91" + Brown = "#BCAAA4" + Grey = "#EEEEEE" + BlueGrey = "#B0BEC5" + + +LIGHT_STYLES = { + 'keyword': format(LightThemeColors.Blue, 'bold'), + 'operator': format(LightThemeColors.Red, 'bold'), + 'brace': format(LightThemeColors.Purple), + 'defclass': format(LightThemeColors.Indigo, 'bold'), + 'string': format(LightThemeColors.Amber), + 'string2': format(LightThemeColors.DeepPurple), + 'comment': format(LightThemeColors.Green, 'italic'), + 'self': format(LightThemeColors.Blue, 'bold'), + 'numbers': format(LightThemeColors.Teal), +} + + +DARK_STYLES = { + 'keyword': format(DarkThemeColors.Blue, 'bold'), + 'operator': format(DarkThemeColors.Red, 'bold'), + 'brace': format(DarkThemeColors.Purple), + 'defclass': format(DarkThemeColors.Indigo, 'bold'), + 'string': format(DarkThemeColors.Amber), + 'string2': format(DarkThemeColors.DeepPurple), + 'comment': format(DarkThemeColors.Green, 'italic'), + 'self': format(DarkThemeColors.Blue, 'bold'), + 'numbers': format(DarkThemeColors.Teal), } @@ -54,7 +112,7 @@ class PythonHighlighter(QSyntaxHighlighter): 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'yield', - 'None', 'True', 'False', + 'None', 'True', 'False', 'async', 'await', ] # Python operators @@ -81,42 +139,42 @@ class PythonHighlighter(QSyntaxHighlighter): # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward - self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) - self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + self.tri_single = (QRegExp("'''"), 1, 'string2') + self.tri_double = (QRegExp('"""'), 2, 'string2') rules = [] # Keyword, operator, and brace rules - rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + rules += [(r'\b%s\b' % w, 0, 'keyword') for w in PythonHighlighter.keywords] - rules += [(r'%s' % o, 0, STYLES['operator']) + rules += [(r'%s' % o, 0, 'operator') for o in PythonHighlighter.operators] - rules += [(r'%s' % b, 0, STYLES['brace']) + rules += [(r'%s' % b, 0, 'brace') for b in PythonHighlighter.braces] # All other rules rules += [ # 'self' - (r'\bself\b', 0, STYLES['self']), + (r'\bself\b', 0, 'self'), # 'def' followed by an identifier - (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + (r'\bdef\b\s*(\w+)', 1, 'defclass'), # 'class' followed by an identifier - (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + (r'\bclass\b\s*(\w+)', 1, 'defclass'), # Numeric literals - (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), - (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), - (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+[lL]?\b', 0, 'numbers'), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, 'numbers'), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, 'numbers'), # Double-quoted string, possibly containing escape sequences - (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, 'string'), # Single-quoted string, possibly containing escape sequences - (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, 'string'), # From '#' until a newline - (r'#[^\n]*', 0, STYLES['comment']), + (r'#[^\n]*', 0, 'comment'), ] @@ -124,12 +182,18 @@ class PythonHighlighter(QSyntaxHighlighter): self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] + @property + def styles(self): + app = QtGui.QApplication.instance() + return DARK_STYLES if app.dark_mode else LIGHT_STYLES + def highlightBlock(self, text): """Apply syntax highlighting to the given block of text. """ # Do other syntax formatting for expression, nth, format in self.rules: index = expression.indexIn(text, 0) + format = self.styles[format] while index >= 0: # We actually want the index of the nth match @@ -175,7 +239,7 @@ class PythonHighlighter(QSyntaxHighlighter): self.setCurrentBlockState(in_state) length = len(text) - start + add # Apply formatting - self.setFormat(start, length, style) + self.setFormat(start, length, self.styles[style]) # Look for the next match start = delimiter.indexIn(text, start + length) From d0b92349ddfde42f5261c78d610cc655216eea62 Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sun, 8 Mar 2020 10:34:54 +0100 Subject: [PATCH 142/235] Intercept light/dark modes transitions on MacOS. --- examples/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/__main__.py b/examples/__main__.py index 22dd7ef0..df390cb9 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -44,8 +44,9 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) - #self.simulate_black_mode() self.hl = PythonHighlighter(self.ui.codeView.document()) + app = QtGui.QApplication.instance() + app.paletteChanged.connect(self.updateTheme) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) self.codeBtn.hide() @@ -83,6 +84,9 @@ class ExampleLoader(QtGui.QMainWindow): app = QtGui.QApplication.instance() app.dark_mode = True + def updateTheme(self): + self.hl = PythonHighlighter(self.ui.codeView.document()) + def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) From 3ba76475d484bd79af070f8354205fadf4f87437 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 2 Apr 2020 22:55:33 -0700 Subject: [PATCH 143/235] Added ImageExporter test for py2-pyside fix --- pyqtgraph/exporters/tests/test_image.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pyqtgraph/exporters/tests/test_image.py diff --git a/pyqtgraph/exporters/tests/test_image.py b/pyqtgraph/exporters/tests/test_image.py new file mode 100644 index 00000000..6f52eceb --- /dev/null +++ b/pyqtgraph/exporters/tests/test_image.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import pyqtgraph as pg +from pyqtgraph.exporters import ImageExporter + +app = pg.mkQApp() + + +def test_ImageExporter_filename_dialog(): + """Tests ImageExporter code path that opens a file dialog. Regression test + for pull request 1133.""" + p = pg.plot() + exp = ImageExporter(p.getPlotItem()) + exp.export() From db67a256a925c5a2811b74d537d39bce201b4251 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 Apr 2020 10:06:25 -0700 Subject: [PATCH 144/235] Miscellaneous doc fixups (#1142) * Miscellaneous doc cleanup * Moved dockarea up a level (like flowchart, parametertree). Removed extraneous parametertree doc --- doc/source/apireference.rst | 2 + doc/source/conf.py | 3 +- doc/source/dockarea.rst | 11 ++ doc/source/graphicsItems/index.rst | 2 +- doc/source/graphicswindow.rst | 20 ++-- doc/source/installation.rst | 107 +++++++++++--------- doc/source/plotting.rst | 4 +- doc/source/widgets/dockarea.rst | 5 - doc/source/widgets/index.rst | 2 - doc/source/widgets/parametertree.rst | 5 - pyqtgraph/flowchart/Flowchart.py | 3 +- pyqtgraph/functions.py | 11 +- pyqtgraph/graphicsItems/AxisItem.py | 4 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 31 +++--- pyqtgraph/graphicsItems/InfiniteLine.py | 4 +- pyqtgraph/graphicsItems/LinearRegionItem.py | 21 ++-- pyqtgraph/graphicsItems/PlotDataItem.py | 37 ++++--- pyqtgraph/graphicsWindows.py | 11 +- pyqtgraph/widgets/GraphicsLayoutWidget.py | 30 +++--- 19 files changed, 169 insertions(+), 144 deletions(-) create mode 100644 doc/source/dockarea.rst delete mode 100644 doc/source/widgets/dockarea.rst delete mode 100644 doc/source/widgets/parametertree.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index c4dc64aa..c52c8df1 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -13,5 +13,7 @@ Contents: 3dgraphics/index colormap parametertree/index + dockarea graphicsscene/index flowchart/index + graphicswindow diff --git a/doc/source/conf.py b/doc/source/conf.py index 3ec48f75..dd5e0718 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -93,7 +93,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -216,4 +216,3 @@ man_pages = [ ('index', 'pyqtgraph', 'pyqtgraph Documentation', ['Luke Campagnola'], 1) ] - diff --git a/doc/source/dockarea.rst b/doc/source/dockarea.rst new file mode 100644 index 00000000..384581d3 --- /dev/null +++ b/doc/source/dockarea.rst @@ -0,0 +1,11 @@ +Dock Area Module +================ + +.. automodule:: pyqtgraph.dockarea + :members: + +.. autoclass:: pyqtgraph.dockarea.DockArea + :members: + +.. autoclass:: pyqtgraph.dockarea.Dock + :members: diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index 7042d27e..eec86610 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -24,6 +24,7 @@ Contents: axisitem textitem errorbaritem + bargraphitem arrowitem fillbetweenitem curvepoint @@ -42,4 +43,3 @@ Contents: graphicsitem uigraphicsitem graphicswidgetanchor - diff --git a/doc/source/graphicswindow.rst b/doc/source/graphicswindow.rst index 3d5641c3..0602ae7e 100644 --- a/doc/source/graphicswindow.rst +++ b/doc/source/graphicswindow.rst @@ -1,8 +1,16 @@ -Basic display widgets -===================== +Deprecated Window Classes +========================= - - GraphicsWindow - - GraphicsView - - GraphicsLayoutItem - - ViewBox +.. automodule:: pyqtgraph.graphicsWindows +.. autoclass:: pyqtgraph.GraphicsWindow + :members: + +.. autoclass:: pyqtgraph.TabWindow + :members: + +.. autoclass:: pyqtgraph.PlotWindow + :members: + +.. autoclass:: pyqtgraph.ImageWindow + :members: diff --git a/doc/source/installation.rst b/doc/source/installation.rst index e3e1f1fc..fd9f5288 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -1,55 +1,70 @@ Installation ============ -There are many different ways to install pyqtgraph, depending on your needs: - -* The most common way to install pyqtgraph is with pip:: - - $ pip install pyqtgraph - - Some users may need to call ``pip3`` instead. This method should work on - all platforms. -* To get access to the very latest features and bugfixes you have three choice:: - - 1. Clone pyqtgraph from github:: - - $ git clone https://github.com/pyqtgraph/pyqtgraph - - Now you can install pyqtgraph from the source:: - - $ python setup.py install - - 2. Directly install from GitHub repo:: - - $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop - - You can change to ``develop`` of the above command to the branch - name or the commit you prefer. - - 3. - You can simply place the pyqtgraph folder someplace importable, such as - inside the root of another project. PyQtGraph does not need to be "built" or - compiled in any way. - -* Packages for pyqtgraph are also available in a few other forms: - - * **Anaconda**: ``conda install pyqtgraph`` - * **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or - download the .deb file linked at the top of the pyqtgraph web page. - * **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) - * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. - - -Requirements -============ - PyQtGraph depends on: - + * Python 2.7 or Python 3.x * A Qt library such as PyQt4, PyQt5, PySide, or PySide2 * numpy -The easiest way to meet these dependencies is with ``pip`` or with a scientific python -distribution like Anaconda. +The easiest way to meet these dependencies is with ``pip`` or with a scientific +python distribution like Anaconda. -.. _pyqtgraph: http://www.pyqtgraph.org/ +There are many different ways to install pyqtgraph, depending on your needs: + +pip +--- + +The most common way to install pyqtgraph is with pip:: + + $ pip install pyqtgraph + +Some users may need to call ``pip3`` instead. This method should work on all +platforms. + +conda +----- + +pyqtgraph is on the default Anaconda channel:: + + $ conda install pyqtgraph + +It is also available in the conda-forge channel:: + + $ conda install -c conda-forge pyqtgraph + +From Source +----------- + +To get access to the very latest features and bugfixes you have three choices: + +1. Clone pyqtgraph from github:: + + $ git clone https://github.com/pyqtgraph/pyqtgraph + $ cd pyqtgraph + + Now you can install pyqtgraph from the source:: + + $ pip install . + +2. Directly install from GitHub repo:: + + $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop + + You can change ``develop`` of the above command to the branch name or the + commit you prefer. + +3. You can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. + +Other Packages +-------------- + +Packages for pyqtgraph are also available in a few other forms: + +* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or + download the .deb file linked at the top of the pyqtgraph web page. +* **Arch Linux:** https://www.archlinux.org/packages/community/any/python-pyqtgraph/ +* **Windows:** Download and run the .exe installer file linked at the top of the + pyqtgraph web page: http://pyqtgraph.org diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index 8a99663a..956f5b97 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -41,7 +41,7 @@ There are several classes invloved in displaying plot data. Most of these classe * :class:`AxisItem ` - Displays axis values, ticks, and labels. Most commonly used with PlotItem. * Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) * :class:`PlotWidget ` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. - * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single :class:`~pyqtgraph.GraphicsLayout`. Most of the methods provided by :class:`~pyqtgraph.GraphicsLayout` are also available through GraphicsLayoutWidget. .. image:: images/plottingClasses.png @@ -69,5 +69,3 @@ Create/show a plot widget, display three data curves:: for i in range(3): plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens - - diff --git a/doc/source/widgets/dockarea.rst b/doc/source/widgets/dockarea.rst deleted file mode 100644 index 09a6acca..00000000 --- a/doc/source/widgets/dockarea.rst +++ /dev/null @@ -1,5 +0,0 @@ -dockarea module -=============== - -.. automodule:: pyqtgraph.dockarea - :members: diff --git a/doc/source/widgets/index.rst b/doc/source/widgets/index.rst index 9cfbc0c4..e5acb7f0 100644 --- a/doc/source/widgets/index.rst +++ b/doc/source/widgets/index.rst @@ -12,11 +12,9 @@ Contents: plotwidget imageview - dockarea spinbox gradientwidget histogramlutwidget - parametertree consolewidget colormapwidget scatterplotwidget diff --git a/doc/source/widgets/parametertree.rst b/doc/source/widgets/parametertree.rst deleted file mode 100644 index 565b930b..00000000 --- a/doc/source/widgets/parametertree.rst +++ /dev/null @@ -1,5 +0,0 @@ -parametertree module -==================== - -.. automodule:: pyqtgraph.parametertree - :members: diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 2e7ed0eb..e269c62f 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -508,7 +508,7 @@ class Flowchart(Node): self.sigStateChanged.emit() def loadFile(self, fileName=None, startDir=None): - """Load a flowchart (*.fc) file. + """Load a flowchart (``*.fc``) file. """ if fileName is None: if startDir is None: @@ -938,4 +938,3 @@ class FlowchartWidget(dockarea.DockArea): class FlowchartNode(Node): pass - diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 45e9aad6..8e1de665 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -934,10 +934,12 @@ def solveBilinearTransform(points1, points2): return matrix def rescaleData(data, scale, offset, dtype=None, clip=None): - """Return data rescaled and optionally cast to a new dtype:: - + """Return data rescaled and optionally cast to a new dtype. + + The scaling operation is:: + data => (data-offset) * scale - + """ if dtype is None: dtype = data.dtype @@ -2503,6 +2505,3 @@ class SignalBlock(object): def __exit__(self, *args): if self.reconnect: self.signal.connect(self.slot) - - - diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 2601ecae..6b2c63df 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -36,7 +36,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== =============================================================== """ @@ -256,7 +256,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 687c2e3f..ad39b60e 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. """ @@ -32,22 +33,20 @@ class HistogramLUTItem(GraphicsWidget): - Movable region over histogram to select black/white levels - Gradient editor to define color lookup table for single-channel images - Parameters - ---------- - image : ImageItem or None - If *image* is provided, then the control will be automatically linked to - the image and changes to the control will be immediately reflected in - the image's appearance. - fillHistogram : bool - By default, the histogram is rendered with a fill. - For performance, set *fillHistogram* = False. - rgbHistogram : bool - Sets whether the histogram is computed once over all channels of the - image, or once per channel. - levelMode : 'mono' or 'rgba' - If 'mono', then only a single set of black/whilte level lines is drawn, - and the levels apply to all channels in the image. If 'rgba', then one - set of levels is drawn for each channel. + ================ =========================================================== + image (:class:`~pyqtgraph.ImageItem` or ``None``) If *image* is + provided, then the control will be automatically linked to + the image and changes to the control will be immediately + reflected in the image's appearance. + fillHistogram (bool) By default, the histogram is rendered with a fill. + For performance, set ``fillHistogram=False`` + rgbHistogram (bool) Sets whether the histogram is computed once over all + channels of the image, or once per channel. + levelMode 'mono' or 'rgba'. If 'mono', then only a single set of + black/white level lines is drawn, and the levels apply to + all channels in the image. If 'rgba', then one set of + levels is drawn for each channel. + ================ =========================================================== """ sigLookupTableChanged = QtCore.Signal(object) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 36505026..37d84c7e 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..Point import Point from .GraphicsObject import GraphicsObject @@ -160,7 +161,8 @@ class InfiniteLine(GraphicsObject): ============= ========================================================= **Arguments** marker String indicating the style of marker to add: - '<|', '|>', '>|', '|<', '<|>', '>|<', '^', 'v', 'o' + ``'<|'``, ``'|>'``, ``'>|'``, ``'|<'``, ``'<|>'``, + ``'>|<'``, ``'^'``, ``'v'``, ``'o'`` position Position (0.0-1.0) along the visible extent of the line to place the marker. Default is 0.5. size Size of the marker in pixels. Default is 10.0. diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 9903dac5..56ff5748 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject from .InfiniteLine import InfiniteLine @@ -54,17 +55,17 @@ class LinearRegionItem(GraphicsObject): False, they are static. bounds Optional [min, max] bounding values for the region span Optional [min, max] giving the range over the view to draw - the region. For example, with a vertical line, use span=(0.5, 1) - to draw only on the top half of the view. + the region. For example, with a vertical line, use + ``span=(0.5, 1)`` to draw only on the top half of the + view. swapMode Sets the behavior of the region when the lines are moved such that - their order reverses: - * "block" means the user cannot drag one line past the other - * "push" causes both lines to be moved if one would cross the other - * "sort" means that lines may trade places, but the output of - getRegion always gives the line positions in ascending order. - * None means that no attempt is made to handle swapped line - positions. - The default is "sort". + their order reverses. "block" means the user cannot drag + one line past the other. "push" causes both lines to be + moved if one would cross the other. "sort" means that + lines may trade places, but the output of getRegion + always gives the line positions in ascending order. None + means that no attempt is made to handle swapped line + positions. The default is "sort". ============== ===================================================================== """ diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 172e3beb..58a218c7 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import numpy as np from .. import metaarray as metaarray from ..Qt import QtCore @@ -40,25 +41,30 @@ class PlotDataItem(GraphicsObject): **Data initialization arguments:** (x,y data only) =================================== ====================================== - PlotDataItem(xValues, yValues) x and y values may be any sequence (including ndarray) of real numbers - PlotDataItem(yValues) y values only -- x will be automatically set to range(len(y)) + PlotDataItem(xValues, yValues) x and y values may be any sequence + (including ndarray) of real numbers + PlotDataItem(yValues) y values only -- x will be + automatically set to range(len(y)) PlotDataItem(x=xValues, y=yValues) x and y given by keyword arguments - PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1] + PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where + ``x=data[:,0]`` and ``y=data[:,1]`` =================================== ====================================== **Data initialization arguments:** (x,y data AND may include spot style) - =========================== ========================================= - PlotDataItem(recarray) numpy array with dtype=[('x', float), ('y', float), ...] - PlotDataItem(list-of-dicts) [{'x': x, 'y': y, ...}, ...] - PlotDataItem(dict-of-lists) {'x': [...], 'y': [...], ...} - PlotDataItem(MetaArray) 1D array of Y values with X sepecified as axis values - OR 2D array with a column 'y' and extra columns as needed. - =========================== ========================================= + ============================ ========================================= + PlotDataItem(recarray) numpy array with ``dtype=[('x', float), + ('y', float), ...]`` + PlotDataItem(list-of-dicts) ``[{'x': x, 'y': y, ...}, ...]`` + PlotDataItem(dict-of-lists) ``{'x': [...], 'y': [...], ...}`` + PlotDataItem(MetaArray) 1D array of Y values with X sepecified as + axis values OR 2D array with a column 'y' + and extra columns as needed. + ============================ ========================================= **Line style keyword arguments:** - ========== ============================================================================== + ============ ============================================================================== connect Specifies how / whether vertexes should be connected. See :func:`arrayToQPath() ` pen Pen to use for drawing line between points. @@ -67,15 +73,14 @@ class PlotDataItem(GraphicsObject): shadowPen Pen for secondary line to draw behind the primary line. disabled by default. May be any single argument accepted by :func:`mkPen() ` fillLevel Fill the area between the curve and fillLevel - fillOutline (bool) If True, an outline surrounding the *fillLevel* - area is drawn. - fillBrush Fill to use when fillLevel is specified. + fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn. + fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` stepMode If True, two orthogonal lines are drawn for each sample as steps. This is commonly used when drawing histograms. - Note that in this case, `len(x) == len(y) + 1` + Note that in this case, ``len(x) == len(y) + 1`` (added in version 0.9.9) - ========== ============================================================================== + ============ ============================================================================== **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index b6a321ee..4033baf3 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -15,11 +15,10 @@ from .widgets.GraphicsView import GraphicsView class GraphicsWindow(GraphicsLayoutWidget): """ - (deprecated; use GraphicsLayoutWidget instead) + (deprecated; use :class:`~pyqtgraph.GraphicsLayoutWidget` instead) - Convenience subclass of :class:`GraphicsLayoutWidget - `. This class is intended for use from - the interactive python prompt. + Convenience subclass of :class:`~pyqtgraph.GraphicsLayoutWidget`. This class + is intended for use from the interactive python prompt. """ def __init__(self, title=None, size=(800,600), **kargs): mkQApp() @@ -50,7 +49,7 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): """ - (deprecated; use PlotWidget instead) + (deprecated; use :class:`~pyqtgraph.PlotWidget` instead) """ def __init__(self, title=None, **kargs): mkQApp() @@ -66,7 +65,7 @@ class PlotWindow(PlotWidget): class ImageWindow(ImageView): """ - (deprecated; use ImageView instead) + (deprecated; use :class:`~pyqtgraph.ImageView` instead) """ def __init__(self, *args, **kargs): mkQApp() diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index 3b41a3ca..6249ba26 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, mkQApp from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView @@ -17,21 +18,20 @@ class GraphicsLayoutWidget(GraphicsView): p2 = w.addPlot(row=0, col=1) v = w.addViewBox(row=1, col=0, colspan=2) - Parameters - ---------- - parent : QWidget or None - The parent widget (see QWidget.__init__) - show : bool - If True, then immediately show the widget after it is created. - If the widget has no parent, then it will be shown inside a new window. - size : (width, height) tuple - Optionally resize the widget. Note: if this widget is placed inside a - layout, then this argument has no effect. - title : str or None - If specified, then set the window title for this widget. - kargs : - All extra arguments are passed to - :func:`GraphicsLayout.__init__() ` + ========= ================================================================= + parent (QWidget or None) The parent widget. + show (bool) If True, then immediately show the widget after it is + created. If the widget has no parent, then it will be shown + inside a new window. + size (width, height) tuple. Optionally resize the widget. Note: if + this widget is placed inside a layout, then this argument has no + effect. + title (str or None) If specified, then set the window title for this + widget. + kargs All extra arguments are passed to + :meth:`GraphicsLayout.__init__ + ` + ========= ================================================================= This class wraps several methods from its internal GraphicsLayout: From a5dd549be1e120adce676d0f327b36db4e6777df Mon Sep 17 00:00:00 2001 From: lcmcninch Date: Fri, 3 Apr 2020 18:33:21 -0400 Subject: [PATCH 145/235] Pass showAxRect keyword arguments to setRange to allow caller to set padding, etc. (#1145) Co-authored-by: Luke McNinch --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e504be56..bf2bb5b5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1303,8 +1303,11 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.scale(r.width(), r.height()) self.rbScaleBox.show() - def showAxRect(self, ax): - self.setRange(ax.normalized()) # be sure w, h are correct coordinates + def showAxRect(self, ax, **kwargs): + """Set the visible range to the given rectangle + Passes keyword arguments to setRange + """ + self.setRange(ax.normalized(), **kwargs) # be sure w, h are correct coordinates self.sigRangeChangedManually.emit(self.state['mouseEnabled']) def allChildren(self, item=None): From 61967bd7f7b2f8db482f52669e443878f39151a3 Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Thu, 19 Dec 2019 21:35:07 +0100 Subject: [PATCH 146/235] add nanfix --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8e1de665..b82e482b 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1113,7 +1113,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # awkward, but fastest numpy native nan evaluation # nanMask = None - if data.dtype.kind == 'f' and np.isnan(data.min()): + if data.ndim == 2 and data.dtype.kind == 'f' and np.isnan(data.min()): nanMask = np.isnan(data) # Apply levels if given if levels is not None: From daeacad71ff680658d4071d2823378377fff035f Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Sat, 7 Mar 2020 10:16:49 +0100 Subject: [PATCH 147/235] Make nanMask compatible with 3D data --- pyqtgraph/functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b82e482b..3863b51a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1113,7 +1113,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # awkward, but fastest numpy native nan evaluation # nanMask = None - if data.ndim == 2 and data.dtype.kind == 'f' and np.isnan(data.min()): + if data.dtype.kind == 'f' and np.isnan(data.min()): + nanMask = np.isnan(data) + if data.ndim > 2: + nanMask = np.any(nanMask, axis=-1) nanMask = np.isnan(data) # Apply levels if given if levels is not None: From 6f34da586dfac635bea3ceb0f207574843b1004c Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Fri, 13 Mar 2020 11:01:47 +0100 Subject: [PATCH 148/235] remove second nanMask = np.isnan(data) --- pyqtgraph/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 3863b51a..e788afa7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1117,7 +1117,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): nanMask = np.isnan(data) if data.ndim > 2: nanMask = np.any(nanMask, axis=-1) - nanMask = np.isnan(data) # Apply levels if given if levels is not None: if isinstance(levels, np.ndarray) and levels.ndim == 2: From 988e5c12223b708b334b561feacd97913a4854dc Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 Apr 2020 10:21:55 -0700 Subject: [PATCH 149/235] Test makeARGB with nans --- pyqtgraph/tests/test_functions.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 6a6aaa33..f9320ef2 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pyqtgraph as pg import numpy as np import sys @@ -270,6 +271,30 @@ def test_makeARGB(): im2, alpha = pg.makeARGB(im1, lut=lut, levels=(1, 17)) checkImage(im2, np.linspace(127.5, 0, 256).astype('ubyte'), alpha, False) + # nans in image + + # 2d input image, one pixel is nan + im1 = np.ones((10, 12)) + im1[3, 5] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1)) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent + assert im2[0, 0, 3] == 255 # doesn't affect other pixels + + # 3d RGB input image, any color channel of a pixel is nan + im1 = np.ones((10, 12, 3)) + im1[3, 5, 1] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1)) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent + assert im2[0, 0, 3] == 255 # doesn't affect other pixels + + # 3d RGBA input image, any color channel of a pixel is nan + im1 = np.ones((10, 12, 4)) + im1[3, 5, 1] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1), useRGBA=True) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent # test sanity checks class AssertExc(object): @@ -387,4 +412,4 @@ def test_eq(): if __name__ == '__main__': - test_interpolateArray() \ No newline at end of file + test_interpolateArray() From 1e81f3dad08cf13832d20da4b2bbcf544dbbe6b9 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 8 Apr 2020 01:14:36 +0200 Subject: [PATCH 150/235] SVGExporter: Correct image pixelation. --- pyqtgraph/exporters/SVGExporter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index b0e9b1c0..6f0035bb 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -69,6 +69,13 @@ xmlHeader = """\ pyqtgraph SVG export Generated with Qt and pyqtgraph + """ def generateSvg(item, options={}): From 71636e351868ec9d83fb1c2fb1628d4a37939911 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 8 Apr 2020 17:10:32 +0200 Subject: [PATCH 151/235] Fix: Update axes after data is set --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fea3834f..e0af8bed 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -365,11 +365,12 @@ class PlotCurveItem(GraphicsObject): #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot + self.yData = kargs['y'].view(np.ndarray) + self.xData = kargs['x'].view(np.ndarray) + self.invalidateBounds() self.prepareGeometryChange() self.informViewBoundsChanged() - self.yData = kargs['y'].view(np.ndarray) - self.xData = kargs['x'].view(np.ndarray) profiler('copy') From be1ed14bd0c2641cfb357cdc61a9418dd07267b7 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 12 Apr 2020 00:43:16 +0200 Subject: [PATCH 152/235] pg.mkQApp: Pass default application name to Qt, added documentation --- pyqtgraph/Qt.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 0941c3c7..ec5a79cc 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -329,9 +329,18 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(): - global QAPP +def mkQApp(name="pyqtgraph", qt_args=[]): + """ + Creates new QApplication or returns current instance if existing. + + ============== ================================================================================= + **Arguments:** + name Application name, passed to Qt + qt_args Array of command line arguments passed to Qt + ============== ================================================================================= + """ + global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - QAPP = QtGui.QApplication([]) + QAPP = QtGui.QApplication([name] + qt_args) return QAPP From 4f1bf8bb18b3f5f994155d5f0b03a60ed58b8040 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 12 Apr 2020 02:47:23 +0200 Subject: [PATCH 153/235] GroupParameterItem: Did not pass changed options to ParameterItem `ParameterItem` handles visibility changes in `optsChanged`. `GroupParameterItem` overrides this function, but never calls the super function, leading in visibility changes not being applied. This PR fixes this by calling said function. Fixes #788 --- pyqtgraph/parametertree/parameterTypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index b728fb8e..a8e3781d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -437,8 +437,10 @@ class GroupParameterItem(ParameterItem): else: ParameterItem.addChild(self, child) - def optsChanged(self, param, changed): - if 'addList' in changed: + def optsChanged(self, param, opts): + ParameterItem.optsChanged(self, param, opts) + + if 'addList' in opts: self.updateAddList() def updateAddList(self): From a703155a21f455469839324745aa4e565a3f29d4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 11 Apr 2020 20:15:00 -0700 Subject: [PATCH 154/235] Replace default list arg with None --- pyqtgraph/Qt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ec5a79cc..cc8b3d0a 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ This module exists to smooth out some of the differences between PySide and PyQt4: @@ -329,7 +330,7 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(name="pyqtgraph", qt_args=[]): +def mkQApp(name="pyqtgraph", qt_args=None): """ Creates new QApplication or returns current instance if existing. @@ -342,5 +343,8 @@ def mkQApp(name="pyqtgraph", qt_args=[]): global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - QAPP = QtGui.QApplication([name] + qt_args) + args = [name] + if qt_args is not None: + args.extend(qt_args) + QAPP = QtGui.QApplication(args) return QAPP From ec66c34fc90fb18d2ed3b40fba2b63779a3f0e63 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Tue, 14 Apr 2020 01:37:09 +0200 Subject: [PATCH 155/235] GraphicsLayout: Always call layout.activate() after adding items Items added to a `GraphicsLayout` only learn their size information after the internal `QGraphicsGridLayout` recalculates the layout. This is happening as a slot in the Qt event queue. Not having updated geometry bounds directly after adding an item leads to multiple issues when not executing the Qt event loop in time (see below). This commit fixes that by always calling `layout.activate()` after adding items, updating item sizes directly. This is a follow-up to PR #1167, where introducing a direct call to `processEvents` was suspected to be able to cause side effects. Notifying @j9ac9k and @campagnola, as they were involved in #1167. Fixes #8 Fixes #1136 --- pyqtgraph/graphicsItems/GraphicsLayout.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index c3722ec0..9c209352 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -134,6 +134,9 @@ class GraphicsLayout(GraphicsWidget): item.geometryChanged.connect(self._updateItemBorder) self.layout.addItem(item, row, col, rowspan, colspan) + self.layout.activate() # Update layout, recalculating bounds. + # Allows some PyQtGraph features to also work without Qt event loop. + self.nextColumn() def getItem(self, row, col): From a697b5584a949f35725f0f21666ff03f0e24b489 Mon Sep 17 00:00:00 2001 From: Marcel Schumacher Date: Tue, 14 Apr 2020 17:24:54 +0200 Subject: [PATCH 156/235] Fixed a possible race condition with linked views --- pyqtgraph/graphicsItems/AxisItem.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 6b2c63df..dcb74c8f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1098,23 +1098,26 @@ class AxisItem(GraphicsWidget): self._updateHeight() def wheelEvent(self, ev): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return if self.orientation in ['left', 'right']: - self.linkedView().wheelEvent(ev, axis=1) + lv.wheelEvent(ev, axis=1) else: - self.linkedView().wheelEvent(ev, axis=0) + lv.wheelEvent(ev, axis=0) ev.accept() def mouseDragEvent(self, event): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return if self.orientation in ['left', 'right']: - return self.linkedView().mouseDragEvent(event, axis=1) + return lv.mouseDragEvent(event, axis=1) else: - return self.linkedView().mouseDragEvent(event, axis=0) + return lv.mouseDragEvent(event, axis=0) def mouseClickEvent(self, event): - if self.linkedView() is None: + lv = self.linkedView() + if lv is None: return - return self.linkedView().mouseClickEvent(event) + return lv.mouseClickEvent(event) From a76d9daec25724c8bf22c61a2ebb3c9b6bff6a4d Mon Sep 17 00:00:00 2001 From: Lev Maximov Date: Tue, 28 Apr 2020 01:43:22 +0700 Subject: [PATCH 157/235] Date axis item (#1154) * Add DateAxisItem * Change style to camelCase * Fix missing first tick for negative timestamps * Add ms precision, auto skipping Auto skipping allows a zoom level to skip ticks automatically if the maximum number of ticks/pt is exceeded * fixes suggested by @goetzc * workaround for negative argument to utcfromtimestamp on windows * attachToPlotItem method * default date axis orientation * Use new DateAxisItem in Plot Customization example * attachToPlotItem bugfix * examples of DateAxisItem * modified description of customPlot example * added descriptions to the new examples, reformatted their code, included the first one into utils.py * typo * Refactored code for setting axis items into new function Replaces "DateAxisItem.attachToPlotItem" * Fix string comparison with == * Doc: Slightly more text for DateAxisItem, small improvement for PlotItem * Make PlotWidget.setAxisItems official * Fix typo in docstring * renamed an example * merge bug fix * Revert "merge bug fix" This reverts commit 876b5a7cdb50cd824b4a5218427081b3ce5c2fe4. * Real bug fix * support for dates upto -1e13..1e13 * Automatically limit DateAxisItem to a range from -1e12 to 1e12 years Very large years (|y|>1e13) cause infinite loop, and since nobody needs time 100 times larger than the age of the universe anyways, this constrains it to 1e12. Following suggestion by @axil: https://github.com/pyqtgraph/pyqtgraph/pull/1154#issuecomment-612662168 * Also catch ValueErrors occuring on Linux before OverfloeErrors While zooming out, before hitting OverflowErrors, utctimestamp produces ValueErrors (at least on my Linux machine), so they are also catched. * Fix: Timestamp 0 corresponds to year 1970 For large years, x axis labels jump by 1970 years if it is not accounted for timestamp 0 to be equal to year 1970. * Fix: When zooming into extreme dates, OSError occurs This commit catches the OSError like the other observed errors * Disable stepping below years for dates outside *_REGULAR_TIMESTAMP 2 reasons: First: At least on my Linux machine, zooming into those dates creates infinite loops. Second: Nobody needs sub-year-precision for those extreme years anyways. * Adapt zoom level sizes based on current font size and screen resolution This is somewhat experimental. With this commit, no longer 60 px are assumed as width for all zoom levels, but the current font and display resolution are considered to calculate the width of ticks in each zoom level. See the new function `updateZoomLevels` for details. Before calling this function, overridden functions `paint` and `generateDrawSpecs` provide information over the current display and font via `self.fontScaleFactor` and `self.fontMetrics`. * Meaningful error meassage when adding axis to multiple PlotItems As @axil noted in the DateAxisItem PR, currently users get a segmentation fault when one tries to add an axis to multiple PlotItems. This commit adds a meaningful RuntimeError message for that case. * setZoomLevelForDensity: Refactoring and calculating optimal spacing on the fly * DateTimeAxis Fix: 1970 shows when zooming far out * Refactoring: Make zoomLevels a customizable dict again * updated the dateaxisitem example * Fix: Get screen resolution in a way that also works for Qt 4 This is both a simplification in code and an improvement in backwards compatibility with Qt 4. * DateAxisItem Fix: Also resolve time below 0.5 seconds * unix line endings in examples * DateTimeAxis Fix: For years < 1 and > 9999, stepping broke Stepping was off by 1970 years for years < 1 and > 9999, resulting in a gap in ticks visible when zooming out. Fixed by subtracting the usual 1970 years. * DateTimeAxis Fix: Zooming out too far causes infinite loop Fixed by setting default limits to +/- 1e10 years. Should still be enough. * improved second dateaxisitem example * 1..9999 years limit * DateTimeAxis: Use OrderedDict to stay compatible with Python < 3-6 * DateAxisItem: Use font height to determine spacing for vertical axes * window title * added dateaxisitem.rst * updated index.rst Co-authored-by: Lukas Heiniger Co-authored-by: Lev Maximov Co-authored-by: 2xB <2xB@users.noreply.github.com> --- doc/source/graphicsItems/dateaxisitem.rst | 8 + doc/source/graphicsItems/index.rst | 1 + doc/source/graphicsItems/make | 1 + examples/DateAxisItem.py | 33 ++ examples/DateAxisItem_QtDesigner.py | 48 +++ examples/DateAxisItem_QtDesigner.ui | 44 +++ examples/customPlot.py | 42 +-- examples/utils.py | 1 + pyqtgraph/__init__.py | 1 + pyqtgraph/graphicsItems/AxisItem.py | 17 +- pyqtgraph/graphicsItems/DateAxisItem.py | 319 +++++++++++++++++++ pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 74 +++-- pyqtgraph/widgets/PlotWidget.py | 5 +- 13 files changed, 531 insertions(+), 63 deletions(-) create mode 100644 doc/source/graphicsItems/dateaxisitem.rst create mode 100644 examples/DateAxisItem.py create mode 100644 examples/DateAxisItem_QtDesigner.py create mode 100644 examples/DateAxisItem_QtDesigner.ui create mode 100644 pyqtgraph/graphicsItems/DateAxisItem.py diff --git a/doc/source/graphicsItems/dateaxisitem.rst b/doc/source/graphicsItems/dateaxisitem.rst new file mode 100644 index 00000000..9da36c6f --- /dev/null +++ b/doc/source/graphicsItems/dateaxisitem.rst @@ -0,0 +1,8 @@ +DateAxisItem +============ + +.. autoclass:: pyqtgraph.DateAxisItem + :members: + + .. automethod:: pyqtgraph.DateAxisItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index eec86610..390d8f17 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -43,3 +43,4 @@ Contents: graphicsitem uigraphicsitem graphicswidgetanchor + dateaxisitem diff --git a/doc/source/graphicsItems/make b/doc/source/graphicsItems/make index 293db0d6..9d9f9954 100644 --- a/doc/source/graphicsItems/make +++ b/doc/source/graphicsItems/make @@ -2,6 +2,7 @@ files = """ArrowItem AxisItem ButtonItem CurvePoint +DateAxisItem GradientEditorItem GradientLegend GraphicsLayout diff --git a/examples/DateAxisItem.py b/examples/DateAxisItem.py new file mode 100644 index 00000000..7bbaafff --- /dev/null +++ b/examples/DateAxisItem.py @@ -0,0 +1,33 @@ +""" +Demonstrates the usage of DateAxisItem to display properly-formatted +timestamps on x-axis which automatically adapt to current zoom level. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import time +from datetime import datetime, timedelta + +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui + +app = QtGui.QApplication([]) + +# Create a plot with a date-time axis +w = pg.PlotWidget(axisItems = {'bottom': pg.DateAxisItem()}) +w.showGrid(x=True, y=True) + +# Plot sin(1/x^2) with timestamps in the last 100 years +now = time.time() +x = np.linspace(2*np.pi, 1000*2*np.pi, 8301) +w.plot(now-(2*np.pi/x)**2*100*np.pi*1e7, np.sin(x), symbol='o') + +w.setWindowTitle('pyqtgraph example: DateAxisItem') +w.show() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/DateAxisItem_QtDesigner.py b/examples/DateAxisItem_QtDesigner.py new file mode 100644 index 00000000..f6f17489 --- /dev/null +++ b/examples/DateAxisItem_QtDesigner.py @@ -0,0 +1,48 @@ +""" +Demonstrates the usage of DateAxisItem in a layout created with Qt Designer. + +The spotlight here is on the 'setAxisItems' method, without which +one would have to subclass plotWidget in order to attach a dateaxis to it. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import sys +import time + +import numpy as np +from PyQt5 import QtWidgets, QtCore, uic +import pyqtgraph as pg + +pg.setConfigOption('background', 'w') +pg.setConfigOption('foreground', 'k') + +BLUE = pg.mkPen('#1f77b4') + +Design, _ = uic.loadUiType('DateAxisItem_QtDesigner.ui') + +class ExampleApp(QtWidgets.QMainWindow, Design): + def __init__(self): + super().__init__() + self.setupUi(self) + now = time.time() + # Plot random values with timestamps in the last 6 months + timestamps = np.linspace(now - 6*30*24*3600, now, 100) + self.curve = self.plotWidget.plot(x=timestamps, y=np.random.rand(100), + symbol='o', symbolSize=5, pen=BLUE) + # 'o' circle 't' triangle 'd' diamond '+' plus 's' square + self.plotWidget.setAxisItems({'bottom': pg.DateAxisItem()}) + self.plotWidget.showGrid(x=True, y=True) + +app = QtWidgets.QApplication(sys.argv) +app.setStyle(QtWidgets.QStyleFactory.create('Fusion')) +app.setPalette(QtWidgets.QApplication.style().standardPalette()) +window = ExampleApp() +window.setWindowTitle('pyqtgraph example: DateAxisItem_QtDesigner') +window.show() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/DateAxisItem_QtDesigner.ui b/examples/DateAxisItem_QtDesigner.ui new file mode 100644 index 00000000..91f77ba9 --- /dev/null +++ b/examples/DateAxisItem_QtDesigner.ui @@ -0,0 +1,44 @@ + + + MainWindow + + + + 0 + 0 + 536 + 381 + + + + MainWindow + + + + + + + + + + + + 0 + 0 + 536 + 18 + + + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/examples/customPlot.py b/examples/customPlot.py index b523fd17..c5e05f91 100644 --- a/examples/customPlot.py +++ b/examples/customPlot.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -This example demonstrates the creation of a plot with a customized -AxisItem and ViewBox. +This example demonstrates the creation of a plot with +DateAxisItem and a customized ViewBox. """ @@ -12,40 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np import time -class DateAxis(pg.AxisItem): - def tickStrings(self, values, scale, spacing): - strns = [] - rng = max(values)-min(values) - #if rng < 120: - # return pg.AxisItem.tickStrings(self, values, scale, spacing) - if rng < 3600*24: - string = '%H:%M:%S' - label1 = '%b %d -' - label2 = ' %b %d, %Y' - elif rng >= 3600*24 and rng < 3600*24*30: - string = '%d' - label1 = '%b - ' - label2 = '%b, %Y' - elif rng >= 3600*24*30 and rng < 3600*24*30*24: - string = '%b' - label1 = '%Y -' - label2 = ' %Y' - elif rng >=3600*24*30*24: - string = '%Y' - label1 = '' - label2 = '' - for x in values: - try: - strns.append(time.strftime(string, time.localtime(x))) - except ValueError: ## Windows can't handle dates before 1970 - strns.append('') - try: - label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values))) - except ValueError: - label = '' - #self.setLabel(text=label) - return strns - class CustomViewBox(pg.ViewBox): def __init__(self, *args, **kwds): pg.ViewBox.__init__(self, *args, **kwds) @@ -65,10 +31,10 @@ class CustomViewBox(pg.ViewBox): app = pg.mkQApp() -axis = DateAxis(orientation='bottom') +axis = pg.DateAxisItem(orientation='bottom') vb = CustomViewBox() -pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox

Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") +pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem and custom ViewBox
Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") dates = np.arange(8) * (3600*24*356) pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o') pw.show() diff --git a/examples/utils.py b/examples/utils.py index 494b686b..041d17d7 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -14,6 +14,7 @@ examples = OrderedDict([ ('Crosshair / Mouse interaction', 'crosshair.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), + ('Timestamps on x axis', 'DateAxisItem.py'), ('Image Analysis', 'imageAnalysis.py'), ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index da14a83b..45e00c83 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -219,6 +219,7 @@ from .graphicsItems.ViewBox import * from .graphicsItems.ArrowItem import * from .graphicsItems.ImageItem import * from .graphicsItems.AxisItem import * +from .graphicsItems.DateAxisItem import * from .graphicsItems.LabelItem import * from .graphicsItems.CurvePoint import * from .graphicsItems.GraphicsWidgetAnchor import * diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 6b2c63df..3faf83a4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -507,20 +507,29 @@ class AxisItem(GraphicsWidget): def linkToView(self, view): """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view.""" - oldView = self.linkedView() + self.unlinkFromView() + self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: + view.sigYRangeChanged.connect(self.linkedViewChanged) + else: + view.sigXRangeChanged.connect(self.linkedViewChanged) + + view.sigResized.connect(self.linkedViewChanged) + + def unlinkFromView(self): + """Unlink this axis from a ViewBox.""" + oldView = self.linkedView() + self._linkedView = None if self.orientation in ['right', 'left']: if oldView is not None: oldView.sigYRangeChanged.disconnect(self.linkedViewChanged) - view.sigYRangeChanged.connect(self.linkedViewChanged) else: if oldView is not None: oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) - view.sigXRangeChanged.connect(self.linkedViewChanged) if oldView is not None: oldView.sigResized.disconnect(self.linkedViewChanged) - view.sigResized.connect(self.linkedViewChanged) def linkedViewChanged(self, view, newRange=None): if self.orientation in ['right', 'left']: diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py new file mode 100644 index 00000000..9d692dac --- /dev/null +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -0,0 +1,319 @@ +import sys +import numpy as np +import time +from datetime import datetime, timedelta + +from .AxisItem import AxisItem +from ..pgcollections import OrderedDict + +__all__ = ['DateAxisItem', 'ZoomLevel'] + +MS_SPACING = 1/1000.0 +SECOND_SPACING = 1 +MINUTE_SPACING = 60 +HOUR_SPACING = 3600 +DAY_SPACING = 24 * HOUR_SPACING +WEEK_SPACING = 7 * DAY_SPACING +MONTH_SPACING = 30 * DAY_SPACING +YEAR_SPACING = 365 * DAY_SPACING + +if sys.platform == 'win32': + _epoch = datetime.utcfromtimestamp(0) + def utcfromtimestamp(timestamp): + return _epoch + timedelta(seconds=timestamp) +else: + utcfromtimestamp = datetime.utcfromtimestamp + +MIN_REGULAR_TIMESTAMP = (datetime(1, 1, 1) - datetime(1970,1,1)).total_seconds() +MAX_REGULAR_TIMESTAMP = (datetime(9999, 1, 1) - datetime(1970,1,1)).total_seconds() +SEC_PER_YEAR = 365.25*24*3600 + +def makeMSStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + val *= 1000 + f = stepSize * 1000 + return (val // (n*f) + 1) * (n*f) / 1000.0 + return stepper + +def makeSStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + return (val // (n*stepSize) + 1) * (n*stepSize) + return stepper + +def makeMStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + d = utcfromtimestamp(val) + base0m = (d.month + n*stepSize - 1) + d = datetime(d.year + base0m // 12, base0m % 12 + 1, 1) + return (d - datetime(1970, 1, 1)).total_seconds() + return stepper + +def makeYStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + d = utcfromtimestamp(val) + next_year = (d.year // (n*stepSize) + 1) * (n*stepSize) + if next_year > 9999: + return np.inf + next_date = datetime(next_year, 1, 1) + return (next_date - datetime(1970, 1, 1)).total_seconds() + return stepper + +class TickSpec: + """ Specifies the properties for a set of date ticks and computes ticks + within a given utc timestamp range """ + def __init__(self, spacing, stepper, format, autoSkip=None): + """ + ============= ========================================================== + Arguments + spacing approximate (average) tick spacing + stepper a stepper function that takes a utc time stamp and a step + steps number n to compute the start of the next unit. You + can use the make_X_stepper functions to create common + steppers. + format a strftime compatible format string which will be used to + convert tick locations to date/time strings + autoSkip list of step size multipliers to be applied when the tick + density becomes too high. The tick spec automatically + applies additional powers of 10 (10, 100, ...) to the list + if necessary. Set to None to switch autoSkip off + ============= ========================================================== + + """ + self.spacing = spacing + self.step = stepper + self.format = format + self.autoSkip = autoSkip + + def makeTicks(self, minVal, maxVal, minSpc): + ticks = [] + n = self.skipFactor(minSpc) + x = self.step(minVal, n) + while x <= maxVal: + ticks.append(x) + x = self.step(x, n) + return (np.array(ticks), n) + + def skipFactor(self, minSpc): + if self.autoSkip is None or minSpc < self.spacing: + return 1 + factors = np.array(self.autoSkip, dtype=np.float) + while True: + for f in factors: + spc = self.spacing * f + if spc > minSpc: + return int(f) + factors *= 10 + + +class ZoomLevel: + """ Generates the ticks which appear in a specific zoom level """ + def __init__(self, tickSpecs, exampleText): + """ + ============= ========================================================== + tickSpecs a list of one or more TickSpec objects with decreasing + coarseness + ============= ========================================================== + + """ + self.tickSpecs = tickSpecs + self.utcOffset = 0 + self.exampleText = exampleText + + def tickValues(self, minVal, maxVal, minSpc): + # return tick values for this format in the range minVal, maxVal + # the return value is a list of tuples (, [tick positions]) + # minSpc indicates the minimum spacing (in seconds) between two ticks + # to fullfill the maxTicksPerPt constraint of the DateAxisItem at the + # current zoom level. This is used for auto skipping ticks. + allTicks = [] + valueSpecs = [] + # back-project (minVal maxVal) to UTC, compute ticks then offset to + # back to local time again + utcMin = minVal - self.utcOffset + utcMax = maxVal - self.utcOffset + for spec in self.tickSpecs: + ticks, skipFactor = spec.makeTicks(utcMin, utcMax, minSpc) + # reposition tick labels to local time coordinates + ticks += self.utcOffset + # remove any ticks that were present in higher levels + tick_list = [x for x in ticks.tolist() if x not in allTicks] + allTicks.extend(tick_list) + valueSpecs.append((spec.spacing, tick_list)) + # if we're skipping ticks on the current level there's no point in + # producing lower level ticks + if skipFactor > 1: + break + return valueSpecs + + +YEAR_MONTH_ZOOM_LEVEL = ZoomLevel([ + TickSpec(YEAR_SPACING, makeYStepper(1), '%Y', autoSkip=[1, 5, 10, 25]), + TickSpec(MONTH_SPACING, makeMStepper(1), '%b') +], "YYYY") +MONTH_DAY_ZOOM_LEVEL = ZoomLevel([ + TickSpec(MONTH_SPACING, makeMStepper(1), '%b'), + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%d', autoSkip=[1, 5]) +], "MMM") +DAY_HOUR_ZOOM_LEVEL = ZoomLevel([ + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'), + TickSpec(HOUR_SPACING, makeSStepper(HOUR_SPACING), '%H:%M', autoSkip=[1, 6]) +], "MMM 00") +HOUR_MINUTE_ZOOM_LEVEL = ZoomLevel([ + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'), + TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M', + autoSkip=[1, 5, 15]) +], "MMM 00") +HMS_ZOOM_LEVEL = ZoomLevel([ + TickSpec(SECOND_SPACING, makeSStepper(SECOND_SPACING), '%H:%M:%S', + autoSkip=[1, 5, 15, 30]) +], "99:99:99") +MS_ZOOM_LEVEL = ZoomLevel([ + TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M:%S'), + TickSpec(MS_SPACING, makeMSStepper(MS_SPACING), '%S.%f', + autoSkip=[1, 5, 10, 25]) +], "99:99:99") + +class DateAxisItem(AxisItem): + """ + **Bases:** :class:`AxisItem ` + + An AxisItem that displays dates from unix timestamps. + + The display format is adjusted automatically depending on the current time + density (seconds/point) on the axis. For more details on changing this + behaviour, see :func:`setZoomLevelForDensity() `. + + Can be added to an existing plot e.g. via + :func:`setAxisItems({'bottom':axis}) `. + + """ + + def __init__(self, orientation='bottom', **kwargs): + """ + Create a new DateAxisItem. + + For `orientation` and `**kwargs`, see + :func:`AxisItem.__init__ `. + + """ + + super(DateAxisItem, self).__init__(orientation, **kwargs) + # Set the zoom level to use depending on the time density on the axis + self.utcOffset = time.timezone + + self.zoomLevels = OrderedDict([ + (np.inf, YEAR_MONTH_ZOOM_LEVEL), + (5 * 3600*24, MONTH_DAY_ZOOM_LEVEL), + (6 * 3600, DAY_HOUR_ZOOM_LEVEL), + (15 * 60, HOUR_MINUTE_ZOOM_LEVEL), + (30, HMS_ZOOM_LEVEL), + (1, MS_ZOOM_LEVEL), + ]) + + def tickStrings(self, values, scale, spacing): + tickSpecs = self.zoomLevel.tickSpecs + tickSpec = next((s for s in tickSpecs if s.spacing == spacing), None) + try: + dates = [utcfromtimestamp(v - self.utcOffset) for v in values] + except (OverflowError, ValueError, OSError): + # should not normally happen + return ['%g' % ((v-self.utcOffset)//SEC_PER_YEAR + 1970) for v in values] + + formatStrings = [] + for x in dates: + try: + s = x.strftime(tickSpec.format) + if '%f' in tickSpec.format: + # we only support ms precision + s = s[:-3] + elif '%Y' in tickSpec.format: + s = s.lstrip('0') + formatStrings.append(s) + except ValueError: # Windows can't handle dates before 1970 + formatStrings.append('') + return formatStrings + + def tickValues(self, minVal, maxVal, size): + density = (maxVal - minVal) / size + self.setZoomLevelForDensity(density) + values = self.zoomLevel.tickValues(minVal, maxVal, minSpc=self.minSpacing) + return values + + def setZoomLevelForDensity(self, density): + """ + Setting `zoomLevel` and `minSpacing` based on given density of seconds per pixel + + The display format is adjusted automatically depending on the current time + density (seconds/point) on the axis. You can customize the behaviour by + overriding this function or setting a different set of zoom levels + than the default one. The `zoomLevels` variable is a dictionary with the + maximal distance of ticks in seconds which are allowed for each zoom level + before the axis switches to the next coarser level. To create custom + zoom levels, override this function and provide custom `zoomLevelWidths` and + `zoomLevels`. + """ + padding = 10 + + # Size in pixels a specific tick label will take + if self.orientation in ['bottom', 'top']: + def sizeOf(text): + return self.fontMetrics.boundingRect(text).width() + padding*self.fontScaleFactor + else: + def sizeOf(text): + return self.fontMetrics.boundingRect(text).height() + padding*self.fontScaleFactor + + # Fallback zoom level: Years/Months + self.zoomLevel = YEAR_MONTH_ZOOM_LEVEL + for maximalSpacing, zoomLevel in self.zoomLevels.items(): + size = sizeOf(zoomLevel.exampleText) + + # Test if zoom level is too fine grained + if maximalSpacing/size < density: + break + + self.zoomLevel = zoomLevel + + # Set up zoomLevel + self.zoomLevel.utcOffset = self.utcOffset + + # Calculate minimal spacing of items on the axis + size = sizeOf(self.zoomLevel.exampleText) + self.minSpacing = density*size + + def linkToView(self, view): + super(DateAxisItem, self).linkToView(view) + + # Set default limits + _min = MIN_REGULAR_TIMESTAMP + _max = MAX_REGULAR_TIMESTAMP + + if self.orientation in ['right', 'left']: + view.setLimits(yMin=_min, yMax=_max) + else: + view.setLimits(xMin=_min, xMax=_max) + + def generateDrawSpecs(self, p): + # Get font metrics from QPainter + # Not happening in "paint", as the QPainter p there is a different one from the one here, + # so changing that font could cause unwanted side effects + if self.tickFont is not None: + p.setFont(self.tickFont) + + self.fontMetrics = p.fontMetrics() + + # Get font scale factor by current window resolution + self.fontScaleFactor = p.device().logicalDpiX() / 96 + + return super(DateAxisItem, self).generateDrawSpecs(p) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index cf588912..dd864c49 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -95,7 +95,7 @@ class PlotItem(GraphicsWidget): def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): """ Create a new PlotItem. All arguments are optional. - Any extra keyword arguments are passed to PlotItem.plot(). + Any extra keyword arguments are passed to :func:`PlotItem.plot() `. ============== ========================================================================================== **Arguments:** @@ -153,20 +153,9 @@ class PlotItem(GraphicsWidget): self.legend = None - ## Create and place axis items - if axisItems is None: - axisItems = {} + # Initialize axis items self.axes = {} - for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - if k in axisItems: - axis = axisItems[k] - else: - axis = AxisItem(orientation=k, parent=self) - axis.linkToView(self.vb) - self.axes[k] = {'item': axis, 'pos': pos} - self.layout.addItem(axis, *pos) - axis.setZValue(-1000) - axis.setFlag(axis.ItemNegativeZStacksBehindParent) + self.setAxisItems(axisItems) self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) @@ -254,11 +243,6 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - self.hideAxis('right') - self.hideAxis('top') - self.showAxis('left') - self.showAxis('bottom') - if labels is None: labels = {} for label in list(self.axes.keys()): @@ -300,6 +284,58 @@ class PlotItem(GraphicsWidget): locals()[m] = _create_method(m) del _create_method + + def setAxisItems(self, axisItems=None): + """ + Place axis items as given by `axisItems`. Initializes non-existing axis items. + + ============== ========================================================================================== + **Arguments:**< + *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items + for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') + and the values must be instances of AxisItem (or at least compatible with AxisItem). + ============== ========================================================================================== + """ + + + if axisItems is None: + axisItems = {} + + # Array containing visible axis items + # Also containing potentially hidden axes, but they are not touched so it does not matter + visibleAxes = ['left', 'bottom'] + visibleAxes.append(axisItems.keys()) # Note that it does not matter that this adds + # some values to visibleAxes a second time + + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + if k in self.axes: + if k not in axisItems: + continue # Nothing to do here + + # Remove old axis + oldAxis = self.axes[k]['item'] + self.layout.removeItem(oldAxis) + oldAxis.scene().removeItem(oldAxis) + oldAxis.unlinkFromView() + + # Create new axis + if k in axisItems: + axis = axisItems[k] + if axis.scene() is not None: + if axis != self.axes[k]["item"]: + raise RuntimeError("Can't add an axis to multiple plots.") + else: + axis = AxisItem(orientation=k, parent=self) + + # Set up new axis + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) + + axisVisible = k in visibleAxes + self.showAxis(k, axisVisible) def setLogMode(self, x=None, y=None): """ diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 5208e3b3..d00ee704 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -24,6 +24,7 @@ class PlotWidget(GraphicsView): :func:`addItem `, :func:`removeItem `, :func:`clear `, + :func:`setAxisItems `, :func:`setXRange `, :func:`setYRange `, :func:`setRange `, @@ -55,7 +56,7 @@ class PlotWidget(GraphicsView): self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setAxisItems', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'setLimits', 'register', 'unregister', 'viewRect']: @@ -96,4 +97,4 @@ class PlotWidget(GraphicsView): return self.plotItem - \ No newline at end of file + From 02b7532706ea36a811ad64a85063e6469fcacc11 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 20 Dec 2018 18:40:36 +0100 Subject: [PATCH 158/235] Remove use of GraphicsScene._addressCache in translateGraphicsItem Use QGraphicsItem.toQGrapicsObject on the item instead. This probably is not even needed since PyQt 4.9 --- pyqtgraph/GraphicsScene/GraphicsScene.py | 61 +++++++----------------- pyqtgraph/graphicsItems/GraphicsItem.py | 15 +++--- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index b61f3a1b..b67e44ef 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import weakref +import warnings + from ..Qt import QtCore, QtGui from ..Point import Point from .. import functions as fn @@ -88,15 +90,11 @@ class GraphicsScene(QtGui.QGraphicsScene): @classmethod def registerObject(cls, obj): - """ - Workaround for PyQt bug in qgraphicsscene.items() - All subclasses of QGraphicsObject must register themselves with this function. - (otherwise, mouse interaction with those objects will likely fail) - """ - if HAVE_SIP and isinstance(obj, sip.wrapper): - cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - - + warnings.warn( + "'registerObject' is deprecated and does nothing.", + DeprecationWarning, stacklevel=2 + ) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) @@ -368,46 +366,15 @@ class GraphicsScene(QtGui.QGraphicsScene): return ev.isAccepted() def items(self, *args): - #print 'args:', args items = QtGui.QGraphicsScene.items(self, *args) - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - items2 = list(map(self.translateGraphicsItem, items)) - #if HAVE_SIP and isinstance(self, sip.wrapper): - #items2 = [] - #for i in items: - #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) - #i2 = GraphicsScene._addressCache.get(addr, i) - ##print i, "==>", i2 - #items2.append(i2) - #print 'items:', items - return items2 + return self.translateGraphicsItems(items) def selectedItems(self, *args): items = QtGui.QGraphicsScene.selectedItems(self, *args) - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - #if HAVE_SIP and isinstance(self, sip.wrapper): - #items2 = [] - #for i in items: - #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) - #i2 = GraphicsScene._addressCache.get(addr, i) - ##print i, "==>", i2 - #items2.append(i2) - items2 = list(map(self.translateGraphicsItem, items)) - - #print 'items:', items - return items2 + return self.translateGraphicsItems(items) def itemAt(self, *args): item = QtGui.QGraphicsScene.itemAt(self, *args) - - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - #if HAVE_SIP and isinstance(self, sip.wrapper): - #addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) - #item = GraphicsScene._addressCache.get(addr, item) - #return item return self.translateGraphicsItem(item) def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False): @@ -554,10 +521,14 @@ class GraphicsScene(QtGui.QGraphicsScene): @staticmethod def translateGraphicsItem(item): - ## for fixing pyqt bugs where the wrong item is returned + # This function is intended as a workaround for a problem with older + # versions of PyQt (< 4.9?), where methods returning 'QGraphicsItem *' + # lose the type of the QGraphicsObject subclasses and instead return + # generic QGraphicsItem wrappers. if HAVE_SIP and isinstance(item, sip.wrapper): - addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) - item = GraphicsScene._addressCache.get(addr, item) + obj = item.toGraphicsObject() + if obj is not None: + item = obj return item @staticmethod diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 541ab13b..3337a367 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -19,8 +19,8 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ _pixelVectorGlobalCache = LRUCache(100, 70) - - def __init__(self, register=True): + + def __init__(self, register=None): if not hasattr(self, '_qtBaseClass'): for b in self.__class__.__bases__: if issubclass(b, QtGui.QGraphicsItem): @@ -28,15 +28,18 @@ class GraphicsItem(object): break if not hasattr(self, '_qtBaseClass'): raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self)) - + self._pixelVectorCache = [None, None] self._viewWidget = None self._viewBox = None self._connectedView = None self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options. - if register: - GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - + if register is not None and register: + warnings.warn( + "'register' argument is deprecated and does nothing", + DeprecationWarning, stacklevel=2 + ) + def getViewWidget(self): """ Return the view widget for this item. From f7364f52b3218a3ea85d3d6ad179ec28e600b11b Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Mon, 4 May 2020 23:42:03 +0300 Subject: [PATCH 159/235] improve SymbolAtlas.getSymbolCoords performance (#1184) * remote legacy work-around for old numpy errors * forgot to remove the numpy_fix import * require numyp >= 1.8.0 * improve performance of updateData PlotCurveItem (saves about 2us per call) * improve ScatterPlotItem performance --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index aa2cabba..a774a30f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -122,26 +122,35 @@ class SymbolAtlas(object): """ Given a list of spot records, return an object representing the coordinates of that symbol within the atlas """ - sourceRect = np.empty(len(opts), dtype=object) + + sourceRect = [] keyi = None sourceRecti = None - for i, rec in enumerate(opts): - key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? + symbol_map = self.symbolMap + + for i, rec in enumerate(opts.tolist()): + size, symbol, pen, brush = rec[2: 6] + + key = id(symbol), size, id(pen), id(brush) if key == keyi: - sourceRect[i] = sourceRecti + sourceRect.append(sourceRecti) else: try: - sourceRect[i] = self.symbolMap[key] + sourceRect.append(symbol_map[key]) except KeyError: newRectSrc = QtCore.QRectF() - newRectSrc.pen = rec['pen'] - newRectSrc.brush = rec['brush'] - newRectSrc.symbol = rec[3] - self.symbolMap[key] = newRectSrc + newRectSrc.pen = pen + newRectSrc.brush = brush + newRectSrc.symbol = symbol + + symbol_map[key] = newRectSrc self.atlasValid = False - sourceRect[i] = newRectSrc + sourceRect.append(newRectSrc) + keyi = key sourceRecti = newRectSrc + + sourceRect = np.array(sourceRect, dtype=object) return sourceRect def buildAtlas(self): From 96be1bd23ffac47d21b6884688f8089d41a181b5 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 4 May 2020 23:58:29 +0200 Subject: [PATCH 160/235] Fix: AxisItem tickFont is defined in two places while only one is used (#1180) To set the tick font of `AxisItem`s, there are two options: `setStyle({"tickFont":...})` and `setTickFont(...)`. The first option sets `AxisItem.style['tickFont']`, the second sets `self.tickFont`. Only `self.tickFont` is actually used. This PR replaces all occurrences of the second variable with the first variable, so both options work again. Also, documentation from `setStyle` is copied to `setTickFont`. Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/graphicsItems/AxisItem.py | 11 +++++++---- pyqtgraph/graphicsItems/DateAxisItem.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3faf83a4..2c6a15e3 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -82,7 +82,6 @@ class AxisItem(GraphicsWidget): self.labelUnitPrefix = unitPrefix self.labelStyle = args self.logMode = False - self.tickFont = None self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickSpacing = None # used to override default tickSpacing method @@ -205,7 +204,11 @@ class AxisItem(GraphicsWidget): self.update() def setTickFont(self, font): - self.tickFont = font + """ + (QFont or None) Determines the font used for tick values. + Use None for the default font. + """ + self.style['tickFont'] = font self.picture = None self.prepareGeometryChange() ## Need to re-allocate space depending on font size? @@ -1084,8 +1087,8 @@ class AxisItem(GraphicsWidget): profiler('draw ticks') # Draw all text - if self.tickFont is not None: - p.setFont(self.tickFont) + if self.style['tickFont'] is not None: + p.setFont(self.style['tickFont']) p.setPen(self.textPen()) for rect, flags, text in textSpecs: p.drawText(rect, int(flags), text) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 9d692dac..7cf9be2c 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -308,8 +308,8 @@ class DateAxisItem(AxisItem): # Get font metrics from QPainter # Not happening in "paint", as the QPainter p there is a different one from the one here, # so changing that font could cause unwanted side effects - if self.tickFont is not None: - p.setFont(self.tickFont) + if self.style['tickFont'] is not None: + p.setFont(self.style['tickFont']) self.fontMetrics = p.fontMetrics() From 720fa5f3c2e77dd1b5d18af26f65de41188b282e Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 5 May 2020 18:16:07 +0200 Subject: [PATCH 161/235] DateAxisItem: AxisItem unlinking tests and doc fixed (#1179) * Added test_AxisItem by @mliberty1 As found in https://github.com/pyqtgraph/pyqtgraph/pull/917 * test_AxisItem: Fit to current implementation * DateAxisItem: Fix documentation to zoomLevels zoomLevels is not intended to be set by the user (see discussion in converstation from https://github.com/pyqtgraph/pyqtgraph/pull/1154/files#diff-aefdb23660d0963df0dff3a116baded8 ). Also, `zoomLevelWidths` does currently not exist. This commit adapts the documentation to reflect that. * DateAxisItem: Do not publish ZoomLevel * DateAxisItem testing: Removed unnecessary monkeypatch fixture Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/graphicsItems/DateAxisItem.py | 7 +-- .../graphicsItems/tests/test_AxisItem.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 7cf9be2c..a5132fd9 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from .AxisItem import AxisItem from ..pgcollections import OrderedDict -__all__ = ['DateAxisItem', 'ZoomLevel'] +__all__ = ['DateAxisItem'] MS_SPACING = 1/1000.0 SECOND_SPACING = 1 @@ -260,9 +260,8 @@ class DateAxisItem(AxisItem): overriding this function or setting a different set of zoom levels than the default one. The `zoomLevels` variable is a dictionary with the maximal distance of ticks in seconds which are allowed for each zoom level - before the axis switches to the next coarser level. To create custom - zoom levels, override this function and provide custom `zoomLevelWidths` and - `zoomLevels`. + before the axis switches to the next coarser level. To customize the zoom level + selection, override this function. """ padding = 10 diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py index 22dccdb4..8d89259a 100644 --- a/pyqtgraph/graphicsItems/tests/test_AxisItem.py +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -30,3 +30,60 @@ def test_AxisItem_stopAxisAtTick(monkeypatch): plot.show() app.processEvents() plot.close() + + +def test_AxisItem_viewUnlink(): + plot = pg.PlotWidget() + view = plot.plotItem.getViewBox() + axis = plot.getAxis("bottom") + assert axis.linkedView() == view + axis.unlinkFromView() + assert axis.linkedView() is None + + +class FakeSignal: + + def __init__(self): + self.calls = [] + + def connect(self, *args, **kwargs): + self.calls.append('connect') + + def disconnect(self, *args, **kwargs): + self.calls.append('disconnect') + + +class FakeView: + + def __init__(self): + self.sigYRangeChanged = FakeSignal() + self.sigXRangeChanged = FakeSignal() + self.sigResized = FakeSignal() + + +def test_AxisItem_bottomRelink(): + axis = pg.AxisItem('bottom') + fake_view = FakeView() + axis.linkToView(fake_view) + assert axis.linkedView() == fake_view + assert fake_view.sigYRangeChanged.calls == [] + assert fake_view.sigXRangeChanged.calls == ['connect'] + assert fake_view.sigResized.calls == ['connect'] + axis.unlinkFromView() + assert fake_view.sigYRangeChanged.calls == [] + assert fake_view.sigXRangeChanged.calls == ['connect', 'disconnect'] + assert fake_view.sigResized.calls == ['connect', 'disconnect'] + + +def test_AxisItem_leftRelink(): + axis = pg.AxisItem('left') + fake_view = FakeView() + axis.linkToView(fake_view) + assert axis.linkedView() == fake_view + assert fake_view.sigYRangeChanged.calls == ['connect'] + assert fake_view.sigXRangeChanged.calls == [] + assert fake_view.sigResized.calls == ['connect'] + axis.unlinkFromView() + assert fake_view.sigYRangeChanged.calls == ['connect', 'disconnect'] + assert fake_view.sigXRangeChanged.calls == [] + assert fake_view.sigResized.calls == ['connect', 'disconnect'] From 5bebf697b0805520e2dc0410cd45b76cf312d111 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 10 May 2020 08:39:17 -0700 Subject: [PATCH 162/235] Disable remove ROI menu action in handle context menu (#1197) --- pyqtgraph/graphicsItems/ROI.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 7863dfef..43bb921d 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -758,6 +758,9 @@ class ROI(GraphicsObject): remAct.triggered.connect(self.removeClicked) self.menu.addAction(remAct) self.menu.remAct = remAct + # ROI menu may be requested when showing the handle context menu, so + # return the menu but disable it if the ROI isn't removable + self.menu.setEnabled(self.contextMenuEnabled()) return self.menu def removeClicked(self): From 14075e6223ae6064c1edef21e9c721744e45dc32 Mon Sep 17 00:00:00 2001 From: Maxim Millen Date: Mon, 11 May 2020 03:42:04 +1200 Subject: [PATCH 163/235] Added support for plot curve to handle both fill and connect args. (#1188) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index e0af8bed..c3a58da2 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -167,7 +167,7 @@ class PlotCurveItem(GraphicsObject): b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) ## adjust for fill level - if ax == 1 and self.opts['fillLevel'] is not None: + if ax == 1 and self.opts['fillLevel'] not in [None, 'enclosed']: b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) ## Add pen width only if it is non-cosmetic. @@ -480,9 +480,10 @@ class PlotCurveItem(GraphicsObject): if x is None: x,y = self.getData() p2 = QtGui.QPainterPath(self.path) - p2.lineTo(x[-1], self.opts['fillLevel']) - p2.lineTo(x[0], self.opts['fillLevel']) - p2.lineTo(x[0], y[0]) + if self.opts['fillLevel'] != 'enclosed': + p2.lineTo(x[-1], self.opts['fillLevel']) + p2.lineTo(x[0], self.opts['fillLevel']) + p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 From 10cb80a2ae89c6d092150b1b1529225b15c236b7 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 20:23:10 -0700 Subject: [PATCH 164/235] Add dependencies for docs build --- doc/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/requirements.txt diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..d9335fc8 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,6 @@ +pyside2 +numpy +scipy +h5py +sphinx +sphinx_rtd_theme From f2e91d1b9a2390b8001e19ce84a0aa5cc4937eab Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 20:45:25 -0700 Subject: [PATCH 165/235] Add matplotlib and pyopengl to docs dependencies --- doc/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index d9335fc8..a98d86f2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,5 +2,7 @@ pyside2 numpy scipy h5py +matplotlib +pyopengl sphinx sphinx_rtd_theme From 9a9be68d9092ab08d6a6cf85d76ba69b7da09288 Mon Sep 17 00:00:00 2001 From: Chris Billington Date: Fri, 15 May 2020 14:31:42 -0400 Subject: [PATCH 166/235] mkQApp: Use sys.argv if non-empty and always set given name (#1199) Always pass `sys.argv`, if non-empty, to `QApplication` constructor. This allows code to continue to rely on the fact that the application name is by default set from `sys.argv[0]`, which is important for example on Linux where this determines the WM_CLASS X attribute used by desktop environments to match applications to their launchers. If `sys.argv` is empty, as it is in an interactive Python session, pass `["pyqtgraph"]` in its place as a sensible default for the application name, which causes issues if not set (issue #1165). If a `name` is given, set it using `setApplicationName()` instead of via the argument list. This ensures it will be set even if the singleton `QApplication` already existed prior to calling `mkQApp()`. --- pyqtgraph/Qt.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index cc8b3d0a..702bc2bd 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -330,21 +330,19 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(name="pyqtgraph", qt_args=None): +def mkQApp(name=None): """ Creates new QApplication or returns current instance if existing. - ============== ================================================================================= + ============== ======================================================== **Arguments:** - name Application name, passed to Qt - qt_args Array of command line arguments passed to Qt - ============== ================================================================================= + name (str) Application name, passed to Qt + ============== ======================================================== """ global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - args = [name] - if qt_args is not None: - args.extend(qt_args) - QAPP = QtGui.QApplication(args) + QAPP = QtGui.QApplication(sys.argv or ["pyqtgraph"]) + if name is not None: + QAPP.setApplicationName(name) return QAPP From 5353acdb1c988d7a67baad46ae2fa16a276e0ee0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 21:17:57 -0700 Subject: [PATCH 167/235] Static paths not used for docs. Fix malformed table in docstring --- doc/source/conf.py | 2 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index dd5e0718..e59e5efd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -122,7 +122,7 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index dd864c49..73aa29cb 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -290,7 +290,7 @@ class PlotItem(GraphicsWidget): Place axis items as given by `axisItems`. Initializes non-existing axis items. ============== ========================================================================================== - **Arguments:**< + **Arguments:** *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') and the values must be instances of AxisItem (or at least compatible with AxisItem). From ae8fc195da919adcc7466746d8daf1040deee849 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 16 May 2020 10:14:52 -0700 Subject: [PATCH 168/235] Disable inherited docstrings --- doc/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index e59e5efd..a979488a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -88,6 +88,7 @@ pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +autodoc_inherit_docstrings = False # -- Options for HTML output --------------------------------------------------- From 54ade7dfb8a341b40c9f2d099216321145e12159 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 17 May 2020 16:06:00 -0700 Subject: [PATCH 169/235] Add readthedocs config file as recommended --- .readthedocs.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..795d359a --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +# Read the Docs configuration file +# https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +python: + version: 3 + install: + - requirements: doc/requirements.txt + +sphinx: + fail_on_warning: true From 9d844f3a423d8846fa9742eb837d41848030ec09 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 17 May 2020 20:10:47 -0700 Subject: [PATCH 170/235] Mock dependencies that aren't strictly needed for docs build --- doc/requirements.txt | 3 --- doc/source/conf.py | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a98d86f2..60d1d1e7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,5 @@ pyside2 numpy -scipy -h5py -matplotlib pyopengl sphinx sphinx_rtd_theme diff --git a/doc/source/conf.py b/doc/source/conf.py index a979488a..3da573eb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -89,6 +89,11 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] autodoc_inherit_docstrings = False +autodoc_mock_imports = [ + "scipy", + "h5py", + "matplotlib", +] # -- Options for HTML output --------------------------------------------------- From 8b66d0e20f58655c1350f327effe6f6ac6ccca85 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 18 May 2020 14:53:41 -0700 Subject: [PATCH 171/235] Updated README with readthedocs link and badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b461f4f6..d082d7ee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) +[![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest) PyQtGraph ========= @@ -72,4 +73,4 @@ Documentation The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. -The official documentation lives at http://pyqtgraph.org/documentation +The official documentation lives at https://pyqtgraph.readthedocs.io From c349c3665bdd0aecd8bfc02a6b55e31dc6e54d4f Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:11:11 -0600 Subject: [PATCH 172/235] fix for roi getting wrong data when imageAxisOrder='row-major' --- pyqtgraph/imageview/ImageView.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index daa9b06d..c3878afd 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -586,7 +586,8 @@ class ImageView(QtGui.QWidget): # Extract image data from ROI axes = (self.axes['x'], self.axes['y']) - data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) + #data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, returnMappedCoords=True) if data is None: return From 360bcad47b2c25ae2c7cffb8958bb97953b016db Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:12:09 -0600 Subject: [PATCH 173/235] fix for mismatched axis exception when imageAxisOrder='row-major' --- pyqtgraph/imageview/ImageView.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c3878afd..8809def9 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -595,7 +595,13 @@ class ImageView(QtGui.QWidget): if self.axes['t'] is None: # Average across y-axis of ROI data = data.mean(axis=axes[1]) - coords = coords[:,:,0] - coords[:,0:1,0] + + if axes == (0,1): ## there's probably a better way to do this slicing dynamically, but I'm not sure what it is. + coords = coords[:,:,0] - coords[:,0:1,0] + elif axes == (1,0): ## we're in row-major order mode + coords = coords[:,0,:] - coords[:,0,0:1] + else: + raise Exception("Need to implement a better way to handle these axes: %s" %str(self.axes)) xvals = (coords**2).sum(axis=0) ** 0.5 else: # Average data within entire ROI for each frame From ca2e5849c21bf99f29c32606fb5ed7866a1b0861 Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:25:15 -0600 Subject: [PATCH 174/235] better conditional handling so as not to break something that was working before --- pyqtgraph/imageview/ImageView.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 8809def9..f93c1fea 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -595,13 +595,10 @@ class ImageView(QtGui.QWidget): if self.axes['t'] is None: # Average across y-axis of ROI data = data.mean(axis=axes[1]) - - if axes == (0,1): ## there's probably a better way to do this slicing dynamically, but I'm not sure what it is. - coords = coords[:,:,0] - coords[:,0:1,0] - elif axes == (1,0): ## we're in row-major order mode + if axes == (1,0): ## we're in row-major order mode -- there's probably a better way to do this slicing dynamically, but I've not figured it out yet. coords = coords[:,0,:] - coords[:,0,0:1] - else: - raise Exception("Need to implement a better way to handle these axes: %s" %str(self.axes)) + else: #default to old way + coords = coords[:,:,0] - coords[:,0:1,0] xvals = (coords**2).sum(axis=0) ** 0.5 else: # Average data within entire ROI for each frame From 4052f0dd11da29e35fd08c80492fafa600100278 Mon Sep 17 00:00:00 2001 From: Marko Bausch Date: Fri, 22 May 2020 15:17:33 +0200 Subject: [PATCH 175/235] Added context menu option to paramtree --- examples/parametertree.py | 10 ++++++++++ pyqtgraph/parametertree/Parameter.py | 10 +++++++++- pyqtgraph/parametertree/ParameterItem.py | 23 ++++++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/examples/parametertree.py b/examples/parametertree.py index 8d8a7352..acfeac4d 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -96,6 +96,16 @@ params = [ {'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True}, {'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True}, ]}, + {'name': 'Custom context menu', 'type': 'group', 'children': [ + {'name': 'List contextMenu', 'type': 'float', 'value': 0, 'context': [ + 'menu1', + 'menu2' + ]}, + {'name': 'Dict contextMenu', 'type': 'float', 'value': 0, 'context': { + 'changeName': 'Title', + 'internal': 'What the user sees', + }}, + ]}, ComplexParameter(name='Custom parameter group (reciprocal values)'), ScalableGroup(name="Expandable Parameter Group", children=[ {'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"}, diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 654a33db..882fabaf 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -55,6 +55,7 @@ class Parameter(QtCore.QObject): sigDefaultChanged(self, default) Emitted when this parameter's default value has changed sigNameChanged(self, name) Emitted when this parameter's name has changed sigOptionsChanged(self, opts) Emitted when any of this parameter's options have changed + sigContextMenu(self, name) Emitted when a context menu was clicked =================================== ========================================================= """ ## name, type, limits, etc. @@ -81,7 +82,8 @@ class Parameter(QtCore.QObject): ## (but only if monitorChildren() is called) sigTreeStateChanged = QtCore.Signal(object, object) # self, changes # changes = [(param, change, info), ...] - + sigContextMenu = QtCore.Signal(object, object) # self, name + # bad planning. #def __new__(cls, *args, **opts): #try: @@ -199,6 +201,8 @@ class Parameter(QtCore.QObject): self.sigDefaultChanged.connect(lambda param, data: self.emitStateChanged('default', data)) self.sigNameChanged.connect(lambda param, data: self.emitStateChanged('name', data)) self.sigOptionsChanged.connect(lambda param, data: self.emitStateChanged('options', data)) + self.sigContextMenu.connect(lambda param, data: self.emitStateChanged('contextMenu', data)) + #self.watchParam(self) ## emit treechange signals if our own state changes @@ -206,6 +210,10 @@ class Parameter(QtCore.QObject): """Return the name of this Parameter.""" return self.opts['name'] + def contextMenu(self, name): + """"A context menu entry was clicked""" + self.sigContextMenu.emit(self, name) + def setName(self, name): """Attempt to change the name of this parameter; return the actual name. (The parameter may reject the name change or automatically pick a different name)""" diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index c149c411..4199b18b 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -47,6 +47,17 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.addAction('Rename').triggered.connect(self.editName) if opts.get('removable', False): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) + + # context menu + context = opts.get('context', None) + if isinstance(context, list): + for name in context: + self.contextMenu.addAction(name).triggered.connect( + self.contextMenuTriggered(name)) + elif isinstance(context, dict): + for name, title in context.items(): + self.contextMenu.addAction(title).triggered.connect( + self.contextMenuTriggered(name)) ## handle movable / dropEnabled options if opts.get('movable', False): @@ -57,7 +68,7 @@ class ParameterItem(QtGui.QTreeWidgetItem): ## flag used internally during name editing self.ignoreNameColumnChange = False - + def valueChanged(self, param, val): ## called when the parameter's value has changed @@ -106,7 +117,8 @@ class ParameterItem(QtGui.QTreeWidgetItem): pass def contextMenuEvent(self, ev): - if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False): + if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ + and "context" not in self.param.opts: return self.contextMenu.popup(ev.globalPos()) @@ -149,7 +161,12 @@ class ParameterItem(QtGui.QTreeWidgetItem): #print opts if 'visible' in opts: self.setHidden(not opts['visible']) - + + def contextMenuTriggered(self, name): + def trigger(): + self.param.contextMenu(name) + return trigger + def editName(self): self.treeWidget().editItem(self, 0) From 6a76f40869a02d9561d008cf5fa1db0abc67b5b9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 May 2020 11:59:31 -0700 Subject: [PATCH 176/235] Add support for running pyside2-uic binary to dynamically compile ui files --- pyqtgraph/Qt.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 702bc2bd..693ac46e 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -10,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import os, sys, re, time +import os, sys, re, time, subprocess from .python2_3 import asUnicode @@ -105,28 +105,45 @@ def _loadUiType(uiFile): if QT_LIB == "PYSIDE": import pysideuic else: - import pyside2uic as pysideuic - import xml.etree.ElementTree as xml + try: + import pyside2uic as pysideuic + except ImportError: + # later vserions of pyside2 have dropped pysideuic; use the uic binary instead. + pysideuic = None + # get class names from ui file + import xml.etree.ElementTree as xml parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text - - with open(uiFile, 'r') as f: + + # convert ui file to python code + if pysideuic is None: + uipy = subprocess.check_output(['pyside2-uic', uiFile]) + else: o = _StringIO() - frame = {} + with open(uiFile, 'r') as f: + pysideuic.compileUi(f, o, indent=0) + uipy = o.getvalue() - pysideuic.compileUi(f, o, indent=0) - pyc = compile(o.getvalue(), '', 'exec') - exec(pyc, frame) + # exceute python code + pyc = compile(uipy, '', 'exec') + frame = {} + exec(pyc, frame) - #Fetch the base_class and form class based on their type in the xml from designer - form_class = frame['Ui_%s'%form_class] - base_class = eval('QtGui.%s'%widget_class) + # fetch the base_class and form class based on their type in the xml from designer + form_class = frame['Ui_%s'%form_class] + base_class = eval('QtGui.%s'%widget_class) return form_class, base_class +def _pyside2uic(uiFile): + glob = {} + mod = exec(uipy, globals=glob) + + + if QT_LIB == PYSIDE: from PySide import QtGui, QtCore From d1c384876cb40cac291feb700cb5fb5b6437b137 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 May 2020 13:32:24 -0700 Subject: [PATCH 177/235] Remove junk code --- pyqtgraph/Qt.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 693ac46e..ef19fc63 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -138,12 +138,6 @@ def _loadUiType(uiFile): return form_class, base_class -def _pyside2uic(uiFile): - glob = {} - mod = exec(uipy, globals=glob) - - - if QT_LIB == PYSIDE: from PySide import QtGui, QtCore From 369d7a11d20be768faefeef2d4a437f5bcc5aa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=A5nsson?= Date: Sun, 10 May 2020 23:10:10 +0200 Subject: [PATCH 178/235] Fix PixelVectors cache --- pyqtgraph/graphicsItems/GraphicsItem.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 3337a367..bc49f48d 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -188,24 +188,23 @@ class GraphicsItem(object): ## (such as when looking at unix timestamps), we can get floating-point errors. dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1) + if direction is None: + direction = QtCore.QPointF(1, 0) + elif direction.manhattanLength() == 0: + raise Exception("Cannot compute pixel length for 0-length vector.") + + key = (dt.m11(), dt.m21(), dt.m12(), dt.m22(), direction.x(), direction.y()) + ## check local cache - if direction is None and dt == self._pixelVectorCache[0]: + if key == self._pixelVectorCache[0]: return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy* - + ## check global cache - #key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32()) - key = (dt.m11(), dt.m21(), dt.m12(), dt.m22()) pv = self._pixelVectorGlobalCache.get(key, None) - if direction is None and pv is not None: - self._pixelVectorCache = [dt, pv] + if pv is not None: + self._pixelVectorCache = [key, pv] return tuple(map(Point,pv)) ## return a *copy* - - if direction is None: - direction = QtCore.QPointF(1, 0) - if direction.manhattanLength() == 0: - raise Exception("Cannot compute pixel length for 0-length vector.") - ## attempt to re-scale direction vector to fit within the precision of the coordinate system ## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'. ## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen. From 50099613d58bb85197248bf1dff88138e95b2e9d Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 28 May 2020 19:31:49 -0700 Subject: [PATCH 179/235] Pin PyVirtualDisplay Version --- azure-test-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 04fd7e42..5d6c01e6 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -129,7 +129,7 @@ jobs: then source activate test-environment-$(python.version) fi - pip install pytest-xvfb + pip install PyVirtualDisplay==0.2.5 pytest-xvfb displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) From 61942453220ac50f6d290277ab6054b54c0cf456 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Sat, 30 May 2020 09:08:40 +0300 Subject: [PATCH 180/235] improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance (#1198) --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 39 ++++++++++++++-------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index a774a30f..af6efcc8 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -15,6 +15,7 @@ from ..pgcollections import OrderedDict from .. import debug from ..python2_3 import basestring + __all__ = ['ScatterPlotItem', 'SpotItem'] @@ -128,8 +129,12 @@ class SymbolAtlas(object): sourceRecti = None symbol_map = self.symbolMap - for i, rec in enumerate(opts.tolist()): - size, symbol, pen, brush = rec[2: 6] + symbols = opts['symbol'].tolist() + sizes = opts['size'].tolist() + pens = opts['pen'].tolist() + brushes = opts['brush'].tolist() + + for symbol, size, pen, brush in zip(symbols, sizes, pens, brushes): key = id(symbol), size, id(pen), id(brush) if key == keyi: @@ -560,6 +565,7 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() def updateSpots(self, dataSet=None): + if dataSet is None: dataSet = self.data @@ -610,8 +616,6 @@ class ScatterPlotItem(GraphicsObject): recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) return recs - - def measureSpotSizes(self, dataSet): for rec in dataSet: ## keep track of the maximum spot size and pixel size @@ -630,7 +634,6 @@ class ScatterPlotItem(GraphicsObject): self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] - def clear(self): """Remove all spots from the scatter plot""" #self.clearItems() @@ -757,8 +760,10 @@ class ScatterPlotItem(GraphicsObject): if self.opts['pxMode'] is True: p.resetTransform() + data = self.data + # Map point coordinates to device - pts = np.vstack([self.data['x'], self.data['y']]) + pts = np.vstack([data['x'], data['y']]) pts = self.mapPointsToDevice(pts) if pts is None: return @@ -770,25 +775,31 @@ class ScatterPlotItem(GraphicsObject): # Draw symbols from pre-rendered atlas atlas = self.fragmentAtlas.getAtlas() + target_rect = data['targetRect'] + source_rect = data['sourceRect'] + widths = data['width'] + # Update targetRects if necessary - updateMask = viewMask & np.equal(self.data['targetRect'], None) + updateMask = viewMask & np.equal(target_rect, None) if np.any(updateMask): updatePts = pts[:,updateMask] - width = self.data[updateMask]['width']*2 - self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) + width = widths[updateMask] * 2 + target_rect[updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) - data = self.data[viewMask] if QT_LIB == 'PyQt4': - p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) + p.drawPixmapFragments( + target_rect[viewMask].tolist(), + source_rect[viewMask].tolist(), + atlas + ) else: - list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) + list(imap(p.drawPixmap, target_rect[viewMask].tolist(), repeat(atlas), source_rect[viewMask].tolist())) else: # render each symbol individually p.setRenderHint(p.Antialiasing, aa) - data = self.data[viewMask] pts = pts[:,viewMask] - for i, rec in enumerate(data): + for i, rec in enumerate(data[viewMask]): p.resetTransform() p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) From ddb597a3ddda0f18a345792f9f802564893e22d4 Mon Sep 17 00:00:00 2001 From: christuart Date: Sat, 30 May 2020 07:35:58 +0100 Subject: [PATCH 181/235] Fix selection of FlowchartWidget input/output nodes from issue #808 (#809) Co-authored-by: Chris Stuart --- pyqtgraph/flowchart/Flowchart.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index e269c62f..2c7b9d59 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -763,6 +763,9 @@ class FlowchartCtrlWidget(QtGui.QWidget): item = self.items[node] self.ui.ctrlList.setCurrentItem(item) + def clearSelection(self): + self.ui.ctrlList.selectionModel().clearSelection() + class FlowchartWidget(dockarea.DockArea): """Includes the actual graphical flowchart and debugging interface""" @@ -890,7 +893,10 @@ class FlowchartWidget(dockarea.DockArea): item = items[0] if hasattr(item, 'node') and isinstance(item.node, Node): n = item.node - self.ctrl.select(n) + if n in self.ctrl.items: + self.ctrl.select(n) + else: + self.ctrl.clearSelection() data = {'outputs': n.outputValues(), 'inputs': n.inputValues()} self.selNameLabel.setText(n.name()) if hasattr(n, 'nodeName'): From 3f6424cc573832476a62a758c320b3a115e8c3ed Mon Sep 17 00:00:00 2001 From: patricev Date: Sat, 30 May 2020 08:38:03 +0200 Subject: [PATCH 182/235] Update Data.py (#1071) * Update Data.py Python eval not working with python 3 - bug fix with the exec() part --- pyqtgraph/flowchart/library/Data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 18f1c948..b133b159 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -2,6 +2,7 @@ from ..Node import Node from ...Qt import QtGui, QtCore import numpy as np +import sys from .common import * from ...SRTTransform import SRTTransform from ...Point import Point @@ -238,7 +239,12 @@ class EvalNode(Node): fn = "def fn(**args):\n" run = "\noutput=fn(**args)\n" text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run - exec(text) + if sys.version_info.major == 2: + exec(text) + elif sys.version_info.major == 3: + ldict = locals() + exec(text, globals(), ldict) + output = ldict['output'] except: print("Error processing node: %s" % self.name()) raise From 9d1fbb6a3e2a7a31a567500c23f3e3115bc9b538 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 May 2020 23:42:35 -0700 Subject: [PATCH 183/235] Add warning about PySide 5.14, avoid a confusing error message that would appear with 5.14 --- examples/PlotWidget.py | 2 +- pyqtgraph/Qt.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py index e52a893d..38bbc73c 100644 --- a/examples/PlotWidget.py +++ b/examples/PlotWidget.py @@ -13,7 +13,7 @@ import numpy as np import pyqtgraph as pg #QtGui.QApplication.setGraphicsSystem('raster') -app = QtGui.QApplication([]) +app = pg.mkQApp() mw = QtGui.QMainWindow() mw.setWindowTitle('pyqtgraph example: PlotWidget') mw.resize(800,800) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ef19fc63..25cb488f 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -10,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import os, sys, re, time, subprocess +import os, sys, re, time, subprocess, warnings from .python2_3 import asUnicode @@ -119,6 +119,8 @@ def _loadUiType(uiFile): # convert ui file to python code if pysideuic is None: + if PySide2.__version__[:5].split('.')[:2] == ['5', '14']: + warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') uipy = subprocess.check_output(['pyside2-uic', uiFile]) else: o = _StringIO() From dfe83dc1c819477523453adb106a0ea83d9518f1 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 30 May 2020 07:15:46 -0700 Subject: [PATCH 184/235] Skipping this test on python 5.9 configs --- pyqtgraph/tests/test_exit_crash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 5a10a0a3..41703ce6 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -67,7 +67,7 @@ def test_exit_crash(): os.remove(tmp) - +@pytest.mark.skipif(pg.Qt.QtVersion.startswith("5.9"), reason="Functionality not well supported, failing only on this config") def test_pg_exit(): # test the pg.exit() function code = textwrap.dedent(""" From 55e1f2c52082b2007de42036cc6f4f7b583d38ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20M=C3=A5nsson?= Date: Sat, 30 May 2020 16:25:43 +0200 Subject: [PATCH 185/235] Add cache for mapRectFromView --- pyqtgraph/graphicsItems/GraphicsItem.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index bc49f48d..1a522446 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -19,6 +19,7 @@ class GraphicsItem(object): The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task. """ _pixelVectorGlobalCache = LRUCache(100, 70) + _mapRectFromViewGlobalCache = LRUCache(100, 70) def __init__(self, register=None): if not hasattr(self, '_qtBaseClass'): @@ -367,8 +368,21 @@ class GraphicsItem(object): vt = self.viewTransform() if vt is None: return None - vt = fn.invertQTransform(vt) - return vt.mapRect(obj) + + cache = self._mapRectFromViewGlobalCache + k = ( + vt.m11(), vt.m12(), vt.m13(), + vt.m21(), vt.m22(), vt.m23(), + vt.m31(), vt.m32(), vt.m33(), + ) + + try: + inv_vt = cache[k] + except KeyError: + inv_vt = fn.invertQTransform(vt) + cache[k] = inv_vt + + return inv_vt.mapRect(obj) def pos(self): return Point(self._qtBaseClass.pos(self)) From 2a6f3f019315efe5bceeb2dbdfccecdd85b63a51 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 30 May 2020 17:09:25 +0200 Subject: [PATCH 186/235] import numpy as np for lines 44 and 51 (#1161) * import numpy as np for lines 44 and 51 --- pyqtgraph/PlotData.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/PlotData.py b/pyqtgraph/PlotData.py index e5faadda..f2760508 100644 --- a/pyqtgraph/PlotData.py +++ b/pyqtgraph/PlotData.py @@ -1,3 +1,4 @@ +import numpy as np class PlotData(object): @@ -50,7 +51,3 @@ class PlotData(object): mn = np.min(self[field]) self.minVals[field] = mn return mn - - - - \ No newline at end of file From 6c61e2445ece118d68824128b4c6f2da000b3a90 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 30 May 2020 09:09:01 -0700 Subject: [PATCH 187/235] Get docs version and copyright year dynamically --- doc/source/conf.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 3da573eb..a6e2cf8c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,7 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os +from datetime import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -19,6 +21,7 @@ import sys, os path = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(path, '..', '..')) sys.path.insert(0, os.path.join(path, '..', 'extensions')) +import pyqtgraph # -- General configuration ----------------------------------------------------- @@ -43,16 +46,16 @@ master_doc = 'index' # General information about the project. project = 'pyqtgraph' -copyright = '2011, Luke Campagnola' +copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.10.0' +version = pyqtgraph.__version__ # The full version, including alpha/beta/rc tags. -release = '0.10.0' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7d979bcf9440ef5a45f4da332fb68abfe938a220 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 30 May 2020 09:22:27 -0700 Subject: [PATCH 188/235] Check for missing ptree widget before accessing --- pyqtgraph/parametertree/parameterTypes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index a8e3781d..bb16d956 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -426,10 +426,13 @@ class GroupParameterItem(ParameterItem): def treeWidgetChanged(self): ParameterItem.treeWidgetChanged(self) - self.treeWidget().setFirstItemColumnSpanned(self, True) + tw = self.treeWidget() + if tw is None: + return + tw.setFirstItemColumnSpanned(self, True) if self.addItem is not None: - self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox) - self.treeWidget().setFirstItemColumnSpanned(self.addItem, True) + tw.setItemWidget(self.addItem, 0, self.addWidgetBox) + tw.setFirstItemColumnSpanned(self.addItem, True) def addChild(self, child): ## make sure added childs are actually inserted before add btn if self.addItem is not None: @@ -664,8 +667,12 @@ class TextParameterItem(WidgetParameterItem): ## TODO: fix so that superclass method can be called ## (WidgetParameter should just natively support this style) #WidgetParameterItem.treeWidgetChanged(self) - self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) - self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + tw = self.treeWidget() + if tw is None: + return + + tw.setFirstItemColumnSpanned(self.subItem, True) + tw.setItemWidget(self.subItem, 0, self.textBox) # for now, these are copied from ParameterItem.treeWidgetChanged self.setHidden(not self.param.opts.get('visible', True)) From 7672b5b72596687295488fd1b1164ece113c71db Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 30 May 2020 22:01:39 +0200 Subject: [PATCH 189/235] Fix: Parameter tree ignores user-set 'expanded' state (#1175) * Fix: Parameter tree ignores user-set 'expanded' state When setting the 'expanded' state of parameters, this change is not applied in the graphically visible tree. This commit changes that behaviour by adding a clause in `ParameterItem.optsChanged` to react to that. Fixes #1130 * ParameterTree: Add option to synchronize "expanded" state As seen in #1130, there is interest in synchronizing the "expanded" state of `Parameter`s in `ParameterTree`s. As a default, this would lead to users being forced to always have multiple `ParameterTree`s to be expanded in the exact same way. Since that might not be desirable, this commit adds an option to customize whether synchronization of the "expanded" state should happen. * Fix: Sync Parameter options "renamable" and "removable" with ParameterTrees Currently, `Parameter` options `renamable` and `removable` are only considered when building a new `ParameterTree`. This commit makes changes in those options reflected in the corresponding `ParameterItem`s. * ParameterTree: Reflect changes in Parameter option 'tip' * Parameter: When setting "syncExpanded", update "expanded" state directly Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/parametertree/Parameter.py | 12 ++-- pyqtgraph/parametertree/ParameterItem.py | 70 +++++++++++++++-------- pyqtgraph/parametertree/ParameterTree.py | 10 ++++ pyqtgraph/parametertree/parameterTypes.py | 10 ++-- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 882fabaf..9ef30477 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -137,9 +137,12 @@ class Parameter(QtCore.QObject): (default=False) removable If True, the user may remove this Parameter. (default=False) - expanded If True, the Parameter will appear expanded when - displayed in a ParameterTree (its children will be - visible). (default=True) + expanded If True, the Parameter will initially be expanded in + ParameterTrees: Its children will be visible. + (default=True) + syncExpanded If True, the `expanded` state of this Parameter is + synchronized with all ParameterTrees it is displayed in. + (default=False) title (str or None) If specified, then the parameter will be displayed to the user using this string as its name. However, the parameter will still be referred to @@ -161,6 +164,7 @@ class Parameter(QtCore.QObject): 'removable': False, 'strictNaming': False, # forces name to be usable as a python variable 'expanded': True, + 'syncExpanded': False, 'title': None, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } @@ -461,7 +465,7 @@ class Parameter(QtCore.QObject): Set any arbitrary options on this parameter. The exact behavior of this function will depend on the parameter type, but most parameters will accept a common set of options: value, name, limits, - default, readonly, removable, renamable, visible, enabled, and expanded. + default, readonly, removable, renamable, visible, enabled, expanded and syncExpanded. See :func:`Parameter.__init__ ` for more information on default options. diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index 4199b18b..ecafd577 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -34,30 +34,20 @@ class ParameterItem(QtGui.QTreeWidgetItem): param.sigOptionsChanged.connect(self.optsChanged) param.sigParentChanged.connect(self.parentChanged) - opts = param.opts + self.updateFlags() + + ## flag used internally during name editing + self.ignoreNameColumnChange = False + + def updateFlags(self): + ## called when Parameter opts changed + opts = self.param.opts - ## Generate context menu for renaming/removing parameter - self.contextMenu = QtGui.QMenu() - self.contextMenu.addSeparator() flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled if opts.get('renamable', False): - if param.opts.get('title', None) is not None: + if opts.get('title', None) is not None: raise Exception("Cannot make parameter with both title != None and renamable == True.") flags |= QtCore.Qt.ItemIsEditable - self.contextMenu.addAction('Rename').triggered.connect(self.editName) - if opts.get('removable', False): - self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) - - # context menu - context = opts.get('context', None) - if isinstance(context, list): - for name in context: - self.contextMenu.addAction(name).triggered.connect( - self.contextMenuTriggered(name)) - elif isinstance(context, dict): - for name, title in context.items(): - self.contextMenu.addAction(title).triggered.connect( - self.contextMenuTriggered(name)) ## handle movable / dropEnabled options if opts.get('movable', False): @@ -65,9 +55,6 @@ class ParameterItem(QtGui.QTreeWidgetItem): if opts.get('dropEnabled', False): flags |= QtCore.Qt.ItemIsDropEnabled self.setFlags(flags) - - ## flag used internally during name editing - self.ignoreNameColumnChange = False def valueChanged(self, param, val): @@ -120,7 +107,26 @@ class ParameterItem(QtGui.QTreeWidgetItem): if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ and "context" not in self.param.opts: return - + + ## Generate context menu for renaming/removing parameter + self.contextMenu = QtGui.QMenu() # Put in global name space to prevent garbage collection + self.contextMenu.addSeparator() + if self.param.opts.get('renamable', False): + self.contextMenu.addAction('Rename').triggered.connect(self.editName) + if self.param.opts.get('removable', False): + self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) + + # context menu + context = opts.get('context', None) + if isinstance(context, list): + for name in context: + self.contextMenu.addAction(name).triggered.connect( + self.contextMenuTriggered(name)) + elif isinstance(context, dict): + for name, title in context.items(): + self.contextMenu.addAction(title).triggered.connect( + self.contextMenuTriggered(name)) + self.contextMenu.popup(ev.globalPos()) def columnChangedEvent(self, col): @@ -141,6 +147,10 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back. finally: self.ignoreNameColumnChange = False + + def expandedChangedEvent(self, expanded): + if self.param.opts['syncExpanded']: + self.param.setOpts(expanded=expanded) def nameChanged(self, param, name): ## called when the parameter's name has changed. @@ -158,10 +168,22 @@ class ParameterItem(QtGui.QTreeWidgetItem): def optsChanged(self, param, opts): """Called when any options are changed that are not name, value, default, or limits""" - #print opts if 'visible' in opts: self.setHidden(not opts['visible']) + if 'expanded' in opts: + if self.param.opts['syncExpanded']: + if self.isExpanded() != opts['expanded']: + self.setExpanded(opts['expanded']) + + if 'syncExpanded' in opts: + if opts['syncExpanded']: + if self.isExpanded() != self.param.opts['expanded']: + self.setExpanded(self.param.opts['expanded']) + + self.updateFlags() + + def contextMenuTriggered(self, name): def trigger(): self.param.contextMenu(name) diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py index ef7c1030..de6ab126 100644 --- a/pyqtgraph/parametertree/ParameterTree.py +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -28,6 +28,8 @@ class ParameterTree(TreeWidget): self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) self.setHeaderHidden(not showHeader) self.itemChanged.connect(self.itemChangedEvent) + self.itemExpanded.connect(self.itemExpandedEvent) + self.itemCollapsed.connect(self.itemCollapsedEvent) self.lastSel = None self.setRootIsDecorated(False) @@ -134,6 +136,14 @@ class ParameterTree(TreeWidget): def itemChangedEvent(self, item, col): if hasattr(item, 'columnChangedEvent'): item.columnChangedEvent(col) + + def itemExpandedEvent(self, item): + if hasattr(item, 'expandedChangedEvent'): + item.expandedChangedEvent(True) + + def itemCollapsedEvent(self, item): + if hasattr(item, 'expandedChangedEvent'): + item.expandedChangedEvent(False) def selectionChanged(self, *args): sel = self.selectedItems() diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index bb16d956..f1c05179 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -44,10 +44,6 @@ class WidgetParameterItem(ParameterItem): self.widget = w self.eventProxy = EventProxy(w, self.widgetEventFilter) - opts = self.param.opts - if 'tip' in opts: - w.setToolTip(opts['tip']) - self.defaultBtn = QtGui.QPushButton() self.defaultBtn.setFixedWidth(20) self.defaultBtn.setFixedHeight(20) @@ -73,6 +69,7 @@ class WidgetParameterItem(ParameterItem): w.sigChanging.connect(self.widgetValueChanging) ## update value shown in widget. + opts = self.param.opts if opts.get('value', None) is not None: self.valueChanged(self, opts['value'], force=True) else: @@ -80,6 +77,8 @@ class WidgetParameterItem(ParameterItem): self.widgetValueChanged() self.updateDefaultBtn() + + self.optsChanged(self.param, self.param.opts) def makeWidget(self): """ @@ -280,6 +279,9 @@ class WidgetParameterItem(ParameterItem): if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): self.widget.setEnabled(not opts['readonly']) + if 'tip' in opts: + self.widget.setToolTip(opts['tip']) + ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): # send only options supported by spinbox From 949df4da16db563840d17d5287cb0a685fbfc544 Mon Sep 17 00:00:00 2001 From: Israel Brewster Date: Sat, 30 May 2020 12:09:09 -0800 Subject: [PATCH 190/235] Fix aspectRatio and zoom range issues when zooming (#1093) * Check and enforce view limits in the setRange function * Check limits when setting aspectRatio - This change is required due to moving the limit checking out of the updateViewRange function. - If the original logic remained, aspect ratio could be lost due to "squshing" the requested view into the viewBox * Add tests for ViewBox zooming limits and aspect ratio * - Move test code to proper location and fix instantiation of QApplication Co-authored-by: Israel Brewster --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 176 ++++++++------- .../ViewBox/tests/test_ViewBoxZoom.py | 200 ++++++++++++++++++ 2 files changed, 297 insertions(+), 79 deletions(-) create mode 100644 pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index bf2bb5b5..94aa6243 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -282,7 +282,7 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - + def update(self, *args, **kwargs): self.prepareForPaint() GraphicsWidget.update(self, *args, **kwargs) @@ -398,12 +398,12 @@ 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) self.updateAutoRange() @@ -414,12 +414,12 @@ class ViewBox(GraphicsWidget): self.addedItems.remove(item) except: pass - + scene = self.scene() if scene is not None: scene.removeItem(item) item.setParentItem(None) - + self.updateAutoRange() def clear(self): @@ -431,19 +431,19 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): self._matrixNeedsUpdate = True self.updateMatrix() - + self.linkedXChanged() self.linkedYChanged() - + self.updateAutoRange() self.updateViewRange() - + self._matrixNeedsUpdate = True self.updateMatrix() - + self.background.setRect(self.rect()) self.borderRect.setRect(self.rect()) - + self.sigStateChanged.emit(self) self.sigResized.emit(self) self.childGroup.prepareGeometryChange() @@ -536,7 +536,11 @@ class ViewBox(GraphicsWidget): yOff = False if setRequested[1] else None self.enableAutoRange(x=xOff, y=yOff) changed.append(True) - + + limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) + minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] + maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] + for ax, range in changes.items(): mn = min(range) mx = max(range) @@ -564,6 +568,39 @@ class ViewBox(GraphicsWidget): mn -= p mx += p + # max range cannot be larger than bounds, if they are given + if limits[ax][0] is not None and limits[ax][1] is not None: + if maxRng[ax] is not None: + maxRng[ax] = min(maxRng[ax], limits[ax][1] - limits[ax][0]) + else: + maxRng[ax] = limits[ax][1] - limits[ax][0] + + # If we have limits, we will have at least a max range as well + if maxRng[ax] is not None or minRng[ax] is not None: + diff = mx - mn + if maxRng[ax] is not None and diff > maxRng[ax]: + delta = maxRng[ax] - diff + elif minRng[ax] is not None and diff < minRng[ax]: + delta = minRng[ax] - diff + else: + delta = 0 + + mn -= delta / 2. + mx += delta / 2. + + # Make sure our requested area is within limits, if any + if limits[ax][0] is not None or limits[ax][1] is not None: + lmn, lmx = limits[ax] + if lmn is not None and mn < lmn: + delta = lmn - mn # Shift the requested view to match our lower limit + mn = lmn + mx += delta + elif lmx is not None and mx > lmx: + delta = lmx - mx + mx = lmx + mn += delta + + # Set target range if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] @@ -1443,40 +1480,6 @@ class ViewBox(GraphicsWidget): aspect = self.state['aspectLocked'] # size ratio / view ratio tr = self.targetRect() bounds = self.rect() - if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: - - ## This is the view range aspect ratio we have requested - targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 - ## This is the view range aspect ratio we need to obey aspect constraint - viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect - viewRatio = 1 if viewRatio == 0 else viewRatio - - # Decide which range to keep unchanged - #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] - if forceX: - ax = 0 - elif forceY: - ax = 1 - else: - # if we are not required to keep a particular axis unchanged, - # then make the entire target range visible - ax = 0 if targetRatio > viewRatio else 1 - - if ax == 0: - ## view range needs to be taller than target - dy = 0.5 * (tr.width() / viewRatio - tr.height()) - if dy != 0: - changed[1] = True - viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] - else: - ## view range needs to be wider than target - dx = 0.5 * (tr.height() * viewRatio - tr.width()) - if dx != 0: - changed[0] = True - viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] - - - # ----------- Make corrections for view limits ----------- limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits']) minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]] @@ -1489,43 +1492,58 @@ class ViewBox(GraphicsWidget): # max range cannot be larger than bounds, if they are given if limits[axis][0] is not None and limits[axis][1] is not None: if maxRng[axis] is not None: - maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0]) + maxRng[axis] = min(maxRng[axis], limits[axis][1] - limits[axis][0]) else: - maxRng[axis] = limits[axis][1]-limits[axis][0] + maxRng[axis] = limits[axis][1] - limits[axis][0] - #print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis]) - #print "Starting range:", viewRange[axis] + if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]: - # Apply xRange, yRange - diff = viewRange[axis][1] - viewRange[axis][0] - if maxRng[axis] is not None and diff > maxRng[axis]: - delta = maxRng[axis] - diff - changed[axis] = True - elif minRng[axis] is not None and diff < minRng[axis]: - delta = minRng[axis] - diff - changed[axis] = True + ## This is the view range aspect ratio we have requested + targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 + ## This is the view range aspect ratio we need to obey aspect constraint + viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect + viewRatio = 1 if viewRatio == 0 else viewRatio + + # Calculate both the x and y ranges that would be needed to obtain the desired aspect ratio + dy = 0.5 * (tr.width() / viewRatio - tr.height()) + dx = 0.5 * (tr.height() * viewRatio - tr.width()) + + rangeY = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy] + rangeX = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx] + + canidateRange = [rangeX, rangeY] + + # Decide which range to try to keep unchanged + #print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange'] + if forceX: + ax = 0 + elif forceY: + ax = 1 else: - delta = 0 + # if we are not required to keep a particular axis unchanged, + # then try to make the entire target range visible + ax = 0 if targetRatio > viewRatio else 1 + target = 0 if ax == 1 else 1 + # See if this choice would cause out-of-range issues + if maxRng is not None or minRng is not None: + diff = canidateRange[target][1] - canidateRange[target][0] + if maxRng[target] is not None and diff > maxRng[target] or \ + minRng[target] is not None and diff < minRng[target]: + # tweak the target range down so we can still pan properly + self.state['targetRange'][ax] = canidateRange[ax] + ax = target # Switch the "fixed" axes - viewRange[axis][0] -= delta/2. - viewRange[axis][1] += delta/2. + if ax == 0: + ## view range needs to be taller than target + if dy != 0: + changed[1] = True + viewRange[1] = rangeY + else: + ## view range needs to be wider than target + if dx != 0: + changed[0] = True + viewRange[0] = rangeX - #print "after applying min/max:", viewRange[axis] - - # Apply xLimits, yLimits - mn, mx = limits[axis] - if mn is not None and viewRange[axis][0] < mn: - delta = mn - viewRange[axis][0] - viewRange[axis][0] += delta - viewRange[axis][1] += delta - changed[axis] = True - elif mx is not None and viewRange[axis][1] > mx: - delta = mx - viewRange[axis][1] - viewRange[axis][0] += delta - viewRange[axis][1] += delta - changed[axis] = True - - #print "after applying edge limits:", viewRange[axis] changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)] self.state['viewRange'] = viewRange @@ -1605,13 +1623,13 @@ class ViewBox(GraphicsWidget): self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - + def view_key(view): return (view.window() is self.window(), view.name) - + ## make a sorted list of all named views nv = sorted(ViewBox.NamedViews.values(), key=view_key) - + if self in nv: nv.remove(self) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py new file mode 100644 index 00000000..4bad9ee1 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +import pyqtgraph as pg +import pytest + +app = pg.mkQApp() + +def test_zoom_normal(): + vb = pg.ViewBox() + testRange = pg.QtCore.QRect(0, 0, 10, 20) + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + +def test_zoom_limit(): + """Test zooming with X and Y limits set""" + vb = pg.ViewBox() + vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10) + + # Try zooming within limits. Should return unmodified + testRange = pg.QtCore.QRect(0, 0, 9, 9) + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + # And outside limits. both view range and targetRange should be set to limits + testRange = pg.QtCore.QRect(-5, -5, 16, 20) + vb.setRange(testRange, padding=0) + + expected = [[0, 10], [0, 10]] + vbState = vb.getState() + + assert vbState['targetRange'] == expected + assert vbState['viewRange'] == expected + +def test_zoom_range_limit(): + """Test zooming with XRange and YRange limits set, but no X and Y limits""" + vb = pg.ViewBox() + vb.setLimits(minXRange=5, maxXRange=10, minYRange=5, maxYRange=10) + + # Try something within limits + testRange = pg.QtCore.QRect(-15, -15, 7, 7) + vb.setRange(testRange, padding=0) + + expected = [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + vbViewRange = vb.getState()['viewRange'] + assert vbViewRange == expected + + # and outside limits + testRange = pg.QtCore.QRect(-15, -15, 17, 17) + + # Code should center the required width reduction, so move each side by 3 + expected = [[testRange.left() + 3, testRange.right() - 3], + [testRange.top() + 3, testRange.bottom() - 3]] + + vb.setRange(testRange, padding=0) + vbViewRange = vb.getState()['viewRange'] + vbTargetRange = vb.getState()['targetRange'] + + assert vbViewRange == expected + assert vbTargetRange == expected + +def test_zoom_ratio(): + """Test zooming with a fixed aspect ratio set""" + vb = pg.ViewBox(lockAspect=1) + + # Give the viewbox a size of the proper aspect ratio to keep things easy + vb.setFixedHeight(10) + vb.setFixedWidth(10) + + # request a range with a good ratio + testRange = pg.QtCore.QRect(0, 0, 10, 10) + vb.setRange(testRange, padding=0) + expected = [[testRange.left(), testRange.right()], + [testRange.top(), testRange.bottom()]] + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Assert that the width and height are equal, since we locked the aspect ratio at 1 + assert viewWidth == viewHeight + + # and for good measure, that it is the same as the test range + assert viewRange == expected + + # Now try to set to something with a different aspect ratio + testRange = pg.QtCore.QRect(0, 0, 10, 20) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Don't really care what we got here, as long as the width and height are the same + assert viewWidth == viewHeight + +def test_zoom_ratio2(): + """Slightly more complicated zoom ratio test, where the view box shape does not match the ratio""" + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # more or less random requested range + testRange = pg.QtCore.QRect(0, 0, 10, 15) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # View width should be twice as wide as the height, + # since the viewbox is twice as wide as it is tall. + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits1(): + """Test zoom with both ratio and limits set""" + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Try to zoom too tall + testRange = pg.QtCore.QRect(0, 0, 6, 10) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits2(): + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Same thing, but out-of-range the other way + testRange = pg.QtCore.QRect(0, 0, 16, 6) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + +def test_zoom_ratio_with_limits_out_of_range(): + vb = pg.ViewBox(lockAspect=1) + + # twice as wide as tall + vb.setFixedHeight(10) + vb.setFixedWidth(20) + + # set some limits + vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5) + + # Request something completely out-of-range and out-of-aspect + testRange = pg.QtCore.QRect(10, 10, 25, 100) + vb.setRange(testRange, padding=0) + + viewRange = vb.getState()['viewRange'] + viewWidth = viewRange[0][1] - viewRange[0][0] + viewHeight = viewRange[1][1] - viewRange[1][0] + + # Make sure our view is within limits and the proper aspect ratio + assert viewRange[0][0] >= -5 + assert viewRange[0][1] <= 5 + assert viewRange[1][0] >= -5 + assert viewRange[1][1] <= 5 + assert viewWidth == 2 * viewHeight + + +if __name__ == "__main__": + setup_module(None) + test_zoom_ratio() From e08ac110f594a7cbe55f38608542237e0a855ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Sat, 30 May 2020 22:53:38 +0200 Subject: [PATCH 191/235] pretty-print log-scale axes labels (#1097) * pretty-print log-scale axes labels * only pretty-print in python 3 --- pyqtgraph/graphicsItems/AxisItem.py | 33 ++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 44541673..29f3ad62 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -4,6 +4,7 @@ from ..python2_3 import asUnicode import numpy as np from ..Point import Point from .. import debug as debug +import sys import weakref from .. import functions as fn from .. import getConfigOption @@ -813,7 +814,37 @@ class AxisItem(GraphicsWidget): return strings def logTickStrings(self, values, scale, spacing): - return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] + estrings = ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] + + if sys.version_info < (3, 0): + # python 2 does not support unicode strings like that + return estrings + else: # python 3+ + convdict = {"0": "⁰", + "1": "¹", + "2": "²", + "3": "³", + "4": "⁴", + "5": "⁵", + "6": "⁶", + "7": "⁷", + "8": "⁸", + "9": "⁹", + } + dstrings = [] + for e in estrings: + if e.count("e"): + v, p = e.split("e") + sign = "⁻" if p[0] == "-" else "" + pot = "".join([convdict[pp] for pp in p[1:].lstrip("0")]) + if v == "1": + v = "" + else: + v = v + "·" + dstrings.append(v + "10" + sign + pot) + else: + dstrings.append(e) + return dstrings def generateDrawSpecs(self, p): """ From 173a755b6c28ee276caf36fa1578fff6844e9c82 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 30 May 2020 21:13:20 -0700 Subject: [PATCH 192/235] Encode csv export header as unicode --- pyqtgraph/exporters/CSVExporter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index c7591932..33c6ec69 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -3,6 +3,7 @@ from ..Qt import QtGui, QtCore from .Exporter import Exporter from ..parametertree import Parameter from .. import PlotItem +from ..python2_3 import asUnicode __all__ = ['CSVExporter'] @@ -57,7 +58,7 @@ class CSVExporter(Exporter): sep = '\t' with open(fileName, 'w') as fd: - fd.write(sep.join(header) + '\n') + fd.write(sep.join(map(asUnicode, header)) + '\n') i = 0 numFormat = '%%0.%dg' % self.params['precision'] numRows = max([len(d[0]) for d in data]) From ed009d3779d6d2f4f31b9c2dca255993f9162e01 Mon Sep 17 00:00:00 2001 From: ChristophRose <42769515+ChristophRose@users.noreply.github.com> Date: Fri, 31 Aug 2018 09:16:37 +0200 Subject: [PATCH 193/235] Check lastDownsample in viewTransformChanged Add a check in the viewTransformChanged function to only force a rerender when the downsampling factor changed. Previously simply moving the image around or zooming in/out without changing the downsampling factor would force a complete rerendering of the image, which was very slow with large images. This way, the expensive rerender is only forced if necessary. --- pyqtgraph/graphicsItems/ImageItem.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index b05c2f70..4b3a94cc 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -51,6 +51,7 @@ class ImageItem(GraphicsObject): self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None self.autoDownsample = False + self._lastDownsample = (1, 1) self.axisOrder = getConfigOption('imageAxisOrder') @@ -551,8 +552,19 @@ class ImageItem(GraphicsObject): def viewTransformChanged(self): if self.autoDownsample: - self.qimage = None - self.update() + o = self.mapToDevice(QtCore.QPointF(0,0)) + x = self.mapToDevice(QtCore.QPointF(1,0)) + y = self.mapToDevice(QtCore.QPointF(0,1)) + w = Point(x-o).length() + h = Point(y-o).length() + if w == 0 or h == 0: + self.qimage = None + return + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + if (xds, yds) != self._lastDownsample: + self.qimage = None + self.update() def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: From 1f9ccccfd08086b25d3d944437282662ef6d8dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Font=C3=A1n=20Correa?= Date: Mon, 3 Jul 2017 10:30:39 +0200 Subject: [PATCH 194/235] Fix Dock close event QLabel still running with no parent --- pyqtgraph/dockarea/Dock.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index a7234073..15c87652 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -5,10 +5,10 @@ from ..widgets.VerticalLabel import VerticalLabel from ..python2_3 import asUnicode class Dock(QtGui.QWidget, DockDrop): - + sigStretchChanged = QtCore.Signal() sigClosed = QtCore.Signal(object) - + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) @@ -68,9 +68,9 @@ class Dock(QtGui.QWidget, DockDrop): }""" self.setAutoFillBackground(False) self.widgetArea.setStyleSheet(self.hStyle) - + self.setStretch(*size) - + if widget is not None: self.addWidget(widget) @@ -82,7 +82,7 @@ class Dock(QtGui.QWidget, DockDrop): return ['dock'] else: return name == 'dock' - + def setStretch(self, x=None, y=None): """ Set the 'target' size for this Dock. @@ -109,7 +109,7 @@ class Dock(QtGui.QWidget, DockDrop): if 'center' in self.allowedAreas: self.allowedAreas.remove('center') self.updateStyle() - + def showTitleBar(self): """ Show the title bar for this Dock. @@ -130,7 +130,7 @@ class Dock(QtGui.QWidget, DockDrop): Sets the text displayed in title bar for this Dock. """ self.label.setText(text) - + def setOrientation(self, o='auto', force=False): """ Sets the orientation of the title bar for this Dock. @@ -149,7 +149,7 @@ class Dock(QtGui.QWidget, DockDrop): self.orientation = o self.label.setOrientation(o) self.updateStyle() - + def updateStyle(self): ## updates orientation and appearance of title bar if self.labelHidden: @@ -192,7 +192,7 @@ class Dock(QtGui.QWidget, DockDrop): self.update() action = self.drag.exec_() self.updateStyle() - + def float(self): self.area.floatDock(self) @@ -223,6 +223,7 @@ class Dock(QtGui.QWidget, DockDrop): def close(self): """Remove this dock from the DockArea it lives inside.""" self.setParent(None) + QtGui.QLabel.close(self.label) self.label.setParent(None) self._container.apoptose() self._container = None @@ -247,10 +248,10 @@ class Dock(QtGui.QWidget, DockDrop): class DockLabel(VerticalLabel): - + sigClicked = QtCore.Signal(object, object) sigCloseClicked = QtCore.Signal() - + def __init__(self, text, dock, showCloseButton): self.dim = False self.fixedWidth = False @@ -277,7 +278,7 @@ class DockLabel(VerticalLabel): fg = '#fff' bg = '#66c' border = '#55B' - + if self.orientation == 'vertical': self.vStyle = """DockLabel { background-color : %s; @@ -311,7 +312,7 @@ class DockLabel(VerticalLabel): if self.dim != d: self.dim = d self.updateStyle() - + def setOrientation(self, o): VerticalLabel.setOrientation(self, o) self.updateStyle() @@ -321,12 +322,12 @@ class DockLabel(VerticalLabel): self.pressPos = ev.pos() self.startedDrag = False ev.accept() - + def mouseMoveEvent(self, ev): if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): self.dock.startDrag() ev.accept() - + def mouseReleaseEvent(self, ev): ev.accept() if not self.startedDrag: @@ -335,7 +336,7 @@ class DockLabel(VerticalLabel): def mouseDoubleClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: self.dock.float() - + def resizeEvent (self, ev): if self.closeButton: if self.orientation == 'vertical': From c90354667920db2f439dd4b8884f19f719e18760 Mon Sep 17 00:00:00 2001 From: Zach Lowry Date: Sun, 31 May 2020 19:39:51 -0500 Subject: [PATCH 195/235] Fix duplicate menus in GradientEditorItem (#444) * Fix duplicate menus in GradientEditorItem Add call to ev.accept in Tivk.mouseClickEvent to prevent parent menu from opening on a right click of a Tick. Co-authored-by: Ogi --- pyqtgraph/graphicsItems/GradientEditorItem.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 12233aad..4bd51aa7 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -874,8 +874,8 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO self.view().tickMoveFinished(self) def mouseClickEvent(self, ev): - if ev.button() == QtCore.Qt.RightButton and self.moving: - ev.accept() + ev.accept() + if ev.button() == QtCore.Qt.RightButton and self.moving: self.setPos(self.startPosition) self.view().tickMoved(self, self.startPosition) self.moving = False @@ -883,7 +883,6 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO self.sigMoved.emit(self) else: self.view().tickClicked(self, ev) - ##remove def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): From 245d89033eeeb481c3531bc90410ca609f19271c Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Mon, 1 Jun 2020 00:09:16 -0700 Subject: [PATCH 196/235] Identify pyqt5 515 ci issue (#1221) * move forward pyvirtualdisplay * Try installing things per QTBUG-84489 * Debug plugins to 1 * Removing all the other packages, adding libxcb-xfixes0 * adding libxcb-icccm4 per plugin debug * adding libxcb-image0, restoring pyvirtualdisplay to older version * now adding libxcb-keysyms1 * libxcb-randr0 * adding libxcb-render-util0 * adding libxcb-xinerama0 * Restore Configs, Properly Name Latest Pipeline --- azure-test-template.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 5d6c01e6..5cdae342 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -26,7 +26,7 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python38-PyQt-5.14: + Python38-PyQt-Latest: python.version: '3.8' qt.bindings: "PyQt5" install.method: "pip" @@ -124,7 +124,9 @@ jobs: displayName: 'Install Wheel' - bash: | - sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm + sudo apt-get install -y libxkbcommon-x11-dev + # workaround for QTBUG-84489 + sudo apt-get install -y libxcb-xfixes0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) @@ -134,6 +136,7 @@ jobs: condition: eq(variables['agent.os'], 'Linux' ) - bash: | + export QT_DEBUG_PLUGINS=1 if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) From 68b8dbac1aba2b456fa584b48640f144c5ece411 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:05:39 +0200 Subject: [PATCH 197/235] moved some functionality from method 'export' to new method (#390) * moved some functionality from method 'export' to new method 'getSupportedFormats' making it accessible from outside --- pyqtgraph/exporters/ImageExporter.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index a8d235a8..cacddee1 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -44,15 +44,20 @@ class ImageExporter(Exporter): def parameters(self): return self.params - + + @staticmethod + def getSupportedImageFormats(): + filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] + preferred = ['*.png', '*.tif', '*.jpg'] + for p in preferred[::-1]: + if p in filter: + filter.remove(p) + filter.insert(0, p) + return filter + def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] - preferred = ['*.png', '*.tif', '*.jpg'] - for p in preferred[::-1]: - if p in filter: - filter.remove(p) - filter.insert(0, p) + filter = self.getSupportedImageFormats() self.fileSaveDialog(filter=filter) return From bb21791c710ccd11c889a6641672adc7fbbdcf3e Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:12:52 +0200 Subject: [PATCH 198/235] changed structure to redefine axis via plotitem.setAxes (#391) * changed structure to redefine axis via plotitem.setAxes * cleanuup * remove old axesitems before adding new ones * DEBUGGED plotitem.setAxes NEW AxisItem.setOrientation (needed by plotitem.setAxes) show/hide right axes after .setAxes() Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/AxisItem.py | 29 +++++++++-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 51 +++++++++++++++++--- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 29f3ad62..c02e6e0c 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -45,11 +45,8 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None - self.orientation = orientation - if orientation not in ['left', 'right', 'top', 'bottom']: - raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") - if orientation in ['left', 'right']: - self.label.rotate(-90) + self.orientation = None + self.setOrientation(orientation) self.style = { 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis @@ -111,6 +108,27 @@ class AxisItem(GraphicsWidget): self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) + def setOrientation(self, orientation): + """ + orientation = 'left', 'right', 'top', 'bottom' + """ + if orientation != self.orientation: + if orientation not in ['left', 'right', 'top', 'bottom']: + raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") + #rotate absolute allows to change orientation multiple times: + if orientation in ['left', 'right']: + self.label.setRotation(-90) + if self.orientation: + self._updateWidth() + self.setMaximumHeight(16777215) + else: + self.label.setRotation(0) + if self.orientation: + self._updateHeight() + self.setMaximumWidth(16777215) + self.orientation = orientation + + def setStyle(self, **kwds): """ Set various style options. @@ -514,6 +532,7 @@ class AxisItem(GraphicsWidget): self.unlinkFromView() self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: view.sigYRangeChanged.connect(self.linkedViewChanged) else: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 73aa29cb..4142fa3f 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -153,10 +153,10 @@ class PlotItem(GraphicsWidget): self.legend = None - # Initialize axis items + ## Create and place axis items self.axes = {} - self.setAxisItems(axisItems) - + self.setAxes(axisItems) + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide @@ -242,7 +242,7 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - + if labels is None: labels = {} for label in list(self.axes.keys()): @@ -258,8 +258,45 @@ class PlotItem(GraphicsWidget): self.setTitle(title) if len(kargs) > 0: - self.plot(**kargs) + self.plot(**kargs) + def setAxes(self, axisItems): + """ + Create and place axis items + For valid values for axisItems see __init__ + """ + for v in self.axes.values(): + item = v['item'] + self.layout.removeItem(item) + self.vb.removeItem(item) + + self.axes = {} + if axisItems is None: + axisItems = {} + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + axis = axisItems.get(k, None) + if axis: + axis.setOrientation(k) + else: + axis = AxisItem(orientation=k) + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) + #show/hide axes: + all_dir = ['left', 'bottom', 'right', 'top'] + if axisItems: + to_show = list(axisItems.keys()) + to_hide = [a for a in all_dir if a not in to_show] + else: + to_show = all_dir[:2] + to_hide = all_dir[2:] + for a in to_hide: + self.hideAxis(a) + for a in to_show: + self.showAxis(a) + def implements(self, interface=None): return interface in ['ViewBoxWrapper'] @@ -1123,8 +1160,8 @@ class PlotItem(GraphicsWidget): Show or hide one of the plot's axes. axis must be one of 'left', 'bottom', 'right', or 'top' """ - s = self.getScale(axis) - p = self.axes[axis]['pos'] + s = self.getAxis(axis) + #p = self.axes[axis]['pos'] if show: s.show() else: From 983cc1695e1b011e961a9d92a062355697010405 Mon Sep 17 00:00:00 2001 From: Adam Strzelecki Date: Mon, 1 Jun 2020 20:23:18 +0200 Subject: [PATCH 199/235] Patch/window handling (#468) * Do not wrap PlotView/ImageView There is no need to wrap PlotView/ImageView into QMainWindow, since only purpose of the QMainWindow is some default menu toolbar & menu handling, that is not used by PyQtGraph anyway. Moreover, every parent-less Qt widget can become window, so this change just use PlotView/ImageView as windows, removing extra complexity, eg. method forwarding, self.win property. Another benefit of this change, it that these windows get initial dimensions and titles as they were designed in .ui file. * Properly cleanup on ImageView.close() We should not close explicitly child widgets or clear scene, otherwise Qt will deallocate children views, and cause "wrapped C/C++ object of type ImageItem has been deleted" error next time we call close() and/or some other methods. All children, including self.ui.roiPlot, self.ui.graphicsView will be closed together with its parent, so there is no need to close them explicitly. So the purpose of close it to reclaim the memory, but not to make the existing ImageView object dysfunctional. * Remove references to plot & image windows after close PyQtGraph images and plots module list variables are currently holding references to all plots and image windows returned directly from main module. This does not seem to be documented however, and causes the Qt windows to be not released from memory, even if user releases all own references. This change removes the references from images/plots list once window is closed, so when there is no other reference, window and all related memory is reclaimed. * Change all UI forms title from Form to PyQtGraph Co-authored-by: Ogi Moore --- examples/ScatterPlotSpeedTestTemplate.ui | 2 +- examples/ScatterPlotSpeedTestTemplate_pyqt.py | 2 +- .../ScatterPlotSpeedTestTemplate_pyside.py | 2 +- examples/designerExample.ui | 2 +- examples/exampleLoaderTemplate.ui | 2 +- examples/exampleLoaderTemplate_pyqt.py | 2 +- examples/exampleLoaderTemplate_pyqt5.py | 2 +- examples/exampleLoaderTemplate_pyside.py | 2 +- pyqtgraph/__init__.py | 18 +++++++++- pyqtgraph/canvas/CanvasTemplate.ui | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 2 +- pyqtgraph/canvas/CanvasTemplate_pyside.py | 2 +- pyqtgraph/canvas/TransformGuiTemplate.ui | 2 +- pyqtgraph/canvas/TransformGuiTemplate_pyqt.py | 2 +- .../canvas/TransformGuiTemplate_pyqt5.py | 2 +- .../canvas/TransformGuiTemplate_pyside.py | 2 +- pyqtgraph/flowchart/FlowchartCtrlTemplate.ui | 2 +- .../flowchart/FlowchartCtrlTemplate_pyqt.py | 2 +- .../flowchart/FlowchartCtrlTemplate_pyqt5.py | 2 +- .../flowchart/FlowchartCtrlTemplate_pyside.py | 2 +- pyqtgraph/flowchart/FlowchartTemplate.ui | 2 +- pyqtgraph/flowchart/FlowchartTemplate_pyqt.py | 2 +- .../flowchart/FlowchartTemplate_pyqt5.py | 2 +- .../flowchart/FlowchartTemplate_pyside.py | 2 +- .../PlotItem/plotConfigTemplate.ui | 2 +- .../PlotItem/plotConfigTemplate_pyqt.py | 2 +- .../PlotItem/plotConfigTemplate_pyqt5.py | 2 +- .../PlotItem/plotConfigTemplate_pyside.py | 2 +- .../graphicsItems/ViewBox/axisCtrlTemplate.ui | 2 +- .../ViewBox/axisCtrlTemplate_pyqt.py | 2 +- .../ViewBox/axisCtrlTemplate_pyqt5.py | 2 +- .../ViewBox/axisCtrlTemplate_pyside.py | 2 +- pyqtgraph/graphicsWindows.py | 33 ++++++++++--------- pyqtgraph/imageview/ImageView.py | 8 ++--- pyqtgraph/imageview/ImageViewTemplate.ui | 2 +- pyqtgraph/imageview/ImageViewTemplate_pyqt.py | 2 +- .../imageview/ImageViewTemplate_pyqt5.py | 2 +- .../imageview/ImageViewTemplate_pyside.py | 2 +- pyqtgraph/tests/uictest.ui | 2 +- 40 files changed, 74 insertions(+), 59 deletions(-) diff --git a/examples/ScatterPlotSpeedTestTemplate.ui b/examples/ScatterPlotSpeedTestTemplate.ui index 6b87e85d..5cdccf0f 100644 --- a/examples/ScatterPlotSpeedTestTemplate.ui +++ b/examples/ScatterPlotSpeedTestTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/ScatterPlotSpeedTestTemplate_pyqt.py b/examples/ScatterPlotSpeedTestTemplate_pyqt.py index 22136690..896525eb 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyqt.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyqt.py @@ -41,7 +41,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8)) self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/ScatterPlotSpeedTestTemplate_pyside.py b/examples/ScatterPlotSpeedTestTemplate_pyside.py index 690b0990..798ebccd 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyside.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyside.py @@ -36,7 +36,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8)) self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/designerExample.ui b/examples/designerExample.ui index 41d06089..0f1695af 100644 --- a/examples/designerExample.ui +++ b/examples/designerExample.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui index f12459ba..c26dbddf 100644 --- a/examples/exampleLoaderTemplate.ui +++ b/examples/exampleLoaderTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py index 732a3ea1..f5521a8f 100644 --- a/examples/exampleLoaderTemplate_pyqt.py +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -89,7 +89,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.graphicsSystemCombo.setItemText(0, _translate("Form", "default", None)) self.graphicsSystemCombo.setItemText(1, _translate("Form", "native", None)) self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster", None)) diff --git a/examples/exampleLoaderTemplate_pyqt5.py b/examples/exampleLoaderTemplate_pyqt5.py index 14ded4d9..090447c2 100644 --- a/examples/exampleLoaderTemplate_pyqt5.py +++ b/examples/exampleLoaderTemplate_pyqt5.py @@ -78,7 +78,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.graphicsSystemCombo.setItemText(0, _translate("Form", "default")) self.graphicsSystemCombo.setItemText(1, _translate("Form", "native")) self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster")) diff --git a/examples/exampleLoaderTemplate_pyside.py b/examples/exampleLoaderTemplate_pyside.py index 62296827..d1705d23 100644 --- a/examples/exampleLoaderTemplate_pyside.py +++ b/examples/exampleLoaderTemplate_pyside.py @@ -78,7 +78,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(0, QtGui.QApplication.translate("Form", "default", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(1, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) self.graphicsSystemCombo.setItemText(2, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 45e00c83..f834e637 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -413,12 +413,20 @@ def plot(*args, **kargs): dataArgs[k] = kargs[k] w = PlotWindow(**pwArgs) + w.sigClosed.connect(_plotWindowClosed) if len(args) > 0 or len(dataArgs) > 0: w.plot(*args, **dataArgs) plots.append(w) w.show() return w - + +def _plotWindowClosed(w): + w.close() + try: + plots.remove(w) + except ValueError: + pass + def image(*args, **kargs): """ Create and return an :class:`ImageWindow ` @@ -429,11 +437,19 @@ def image(*args, **kargs): """ mkQApp() w = ImageWindow(*args, **kargs) + w.sigClosed.connect(_imageWindowClosed) images.append(w) w.show() return w show = image ## for backward compatibility +def _imageWindowClosed(w): + w.close() + try: + images.remove(w) + except ValueError: + pass + def dbg(*args, **kwds): """ Create a console window and begin watching for exceptions. diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index bfdacf38..15fdf7a9 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 3569c8e7..823265aa 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -91,7 +91,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.autoRangeBtn.setText(_translate("Form", "Auto Range", None)) self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None)) self.redirectCheck.setText(_translate("Form", "Redirect", None)) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py index 03310d39..83afc772 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -79,7 +79,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.autoRangeBtn.setText(_translate("Form", "Auto Range")) self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.")) self.redirectCheck.setText(_translate("Form", "Redirect")) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 570d5bd1..c728efac 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -80,7 +80,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate.ui b/pyqtgraph/canvas/TransformGuiTemplate.ui index d8312388..c63979e0 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate.ui +++ b/pyqtgraph/canvas/TransformGuiTemplate.ui @@ -17,7 +17,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index c6cf82e4..7cbb3652 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -59,7 +59,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.translateLabel.setText(_translate("Form", "Translate:", None)) self.rotateLabel.setText(_translate("Form", "Rotate:", None)) self.scaleLabel.setText(_translate("Form", "Scale:", None)) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py index 6b1f239b..2af0499a 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py @@ -46,7 +46,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.translateLabel.setText(_translate("Form", "Translate:")) self.rotateLabel.setText(_translate("Form", "Rotate:")) self.scaleLabel.setText(_translate("Form", "Scale:")) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index e430b61a..76620342 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -46,7 +46,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8)) self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8)) self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui index 0361ad3e..6a9a203a 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py index 8afd43f8..3d8bcf56 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py @@ -69,7 +69,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.loadBtn.setText(_translate("Form", "Load..", None)) self.saveBtn.setText(_translate("Form", "Save", None)) self.saveAsBtn.setText(_translate("Form", "As..", None)) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py index b661918d..958f2aaf 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py @@ -56,7 +56,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.loadBtn.setText(_translate("Form", "Load..")) self.saveBtn.setText(_translate("Form", "Save")) self.saveAsBtn.setText(_translate("Form", "As..")) diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py index b722000e..2db10f6a 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py @@ -55,7 +55,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8)) self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/flowchart/FlowchartTemplate.ui b/pyqtgraph/flowchart/FlowchartTemplate.ui index 8b0c19da..22934b91 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate.ui +++ b/pyqtgraph/flowchart/FlowchartTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py index 06b10bfe..e6084eee 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py @@ -62,7 +62,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py index ba754305..448a00ff 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py @@ -49,7 +49,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) from ..widgets.DataTreeWidget import DataTreeWidget from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py index 2c693c60..47f97f85 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py @@ -48,7 +48,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui index dffc62d0..12d8033e 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py index e09c9978..5ecc0438 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -148,7 +148,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.averageGroup.setToolTip(_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)) self.averageGroup.setTitle(_translate("Form", "Average", None)) self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None)) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py index e9fdff24..817221f2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py @@ -135,7 +135,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.averageGroup.setToolTip(_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).")) self.averageGroup.setTitle(_translate("Form", "Average")) self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.")) diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py index aff31211..d0fd1edd 100644 --- a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -134,7 +134,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", 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.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)) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui index 297fce75..01bdf93e 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui @@ -17,7 +17,7 @@ - Form + PyQtGraph diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py index 5d952741..b54153fc 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py @@ -78,7 +78,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.label.setText(_translate("Form", "Link Axis:", None)) self.linkCombo.setToolTip(_translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None)) self.autoPercentSpin.setToolTip(_translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None)) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py index 78da6eea..0a28e7f6 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py @@ -65,7 +65,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.label.setText(_translate("Form", "Link Axis:")) self.linkCombo.setToolTip(_translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

")) self.autoPercentSpin.setToolTip(_translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

")) diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py index 9ddeb5d1..c90206b5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py @@ -64,7 +64,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "

Links this axis with another view. When linked, both views will display the same data range.

", None, QtGui.QApplication.UnicodeUTF8)) self.autoPercentSpin.setToolTip(QtGui.QApplication.translate("Form", "

Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.

", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 4033baf3..aa62f4f1 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -48,38 +48,39 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): + sigClosed = QtCore.Signal(object) + """ (deprecated; use :class:`~pyqtgraph.PlotWidget` instead) """ def __init__(self, title=None, **kargs): mkQApp() - self.win = QtGui.QMainWindow() PlotWidget.__init__(self, **kargs) - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) if title is not None: - self.win.setWindowTitle(title) - self.win.show() + self.setWindowTitle(title) + self.show() + + def closeEvent(self, event): + PlotWidget.closeEvent(self, event) + self.sigClosed.emit(self) class ImageWindow(ImageView): + sigClosed = QtCore.Signal(object) + """ (deprecated; use :class:`~pyqtgraph.ImageView` instead) """ def __init__(self, *args, **kargs): mkQApp() - self.win = QtGui.QMainWindow() - self.win.resize(800,600) + ImageView.__init__(self) if 'title' in kargs: - self.win.setWindowTitle(kargs['title']) + self.setWindowTitle(kargs['title']) del kargs['title'] - ImageView.__init__(self, self.win) if len(args) > 0 or len(kargs) > 0: self.setImage(*args, **kargs) - self.win.setCentralWidget(self) - for m in ['resize']: - setattr(self, m, getattr(self.win, m)) - #for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']: - #setattr(self, m, getattr(self.cw, m)) - self.win.show() + self.show() + + def closeEvent(self, event): + ImageView.closeEvent(self, event) + self.sigClosed.emit(self) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index daa9b06d..e9058bdb 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -411,11 +411,9 @@ class ImageView(QtGui.QWidget): def close(self): """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" - self.ui.roiPlot.close() - self.ui.graphicsView.close() - self.scene.clear() - del self.image - del self.imageDisp + self.clear() + self.imageDisp = None + self.imageItem.setParent(None) super(ImageView, self).close() self.setParent(None) diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui index 927bda30..ece77864 100644 --- a/pyqtgraph/imageview/ImageViewTemplate.ui +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -11,7 +11,7 @@
- Form + PyQtGraph diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index 8c9d5633..8a34c1d8 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -146,7 +146,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(_translate("Form", "Form", None)) + Form.setWindowTitle(_translate("Form", "PyQtGraph", None)) self.roiBtn.setText(_translate("Form", "ROI", None)) self.menuBtn.setText(_translate("Form", "Menu", None)) self.normGroup.setTitle(_translate("Form", "Normalization", None)) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py index 1d076a9e..87f3f254 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py @@ -134,7 +134,7 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) + Form.setWindowTitle(_translate("Form", "PyQtGraph")) self.roiBtn.setText(_translate("Form", "ROI")) self.menuBtn.setText(_translate("Form", "Norm")) self.normGroup.setTitle(_translate("Form", "Normalization")) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py index 6d6c9632..9980e2ba 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -132,7 +132,7 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8)) self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8)) self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/pyqtgraph/tests/uictest.ui b/pyqtgraph/tests/uictest.ui index 25d14f2b..a183bdae 100644 --- a/pyqtgraph/tests/uictest.ui +++ b/pyqtgraph/tests/uictest.ui @@ -11,7 +11,7 @@ - Form + PyQtGraph From ca9b0c7910e999a774af38ae98f114bc32810468 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Mon, 1 Jun 2020 20:24:18 +0200 Subject: [PATCH 200/235] new method 'getAxpectRatio' with code taken from 'setAspectLocked' (#392) Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 94aa6243..e665deef 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -233,6 +233,17 @@ class ViewBox(GraphicsWidget): if name is None: self.updateViewLists() + def getAspectRatio(self): + '''return the current aspect ratio''' + rect = self.rect() + vr = self.viewRect() + 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()) + return currentRatio + def register(self, name): """ Add this ViewBox to the registered list of views. @@ -1134,12 +1145,7 @@ class ViewBox(GraphicsWidget): return self.state['aspectLocked'] = False else: - rect = self.rect() - vr = self.viewRect() - 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()) + currentRatio = self.getAspectRatio() if ratio is None: ratio = currentRatio if self.state['aspectLocked'] == ratio: # nothing to change From ed36a0194baa88fa4e7440e8d67e84633caf7d31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Jun 2020 18:38:50 -0700 Subject: [PATCH 201/235] py3 fix for scatterplotwidget.setselectedfields --- pyqtgraph/widgets/ScatterPlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index bf8a0f42..08f6d02b 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -96,7 +96,7 @@ class ScatterPlotWidget(QtGui.QSplitter): try: self.fieldList.clearSelection() for f in fields: - i = self.fields.keys().index(f) + i = list(self.fields.keys()).index(f) item = self.fieldList.item(i) item.setSelected(True) finally: From ab96ca1d30cb4619e723f2c45c1995e3123e0a8f Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 2 Jun 2020 22:44:17 -0700 Subject: [PATCH 202/235] Examples Should Be Tested on PySide2 5.14.2.2 --- examples/test_examples.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index f10fe358..a9fecca2 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -150,7 +150,12 @@ conditionalExamples = { ) } -@pytest.mark.skipif(Qt.QT_LIB == "PySide2" and "Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") +@pytest.mark.skipif( + Qt.QT_LIB == "PySide2" + and tuple(map(int, Qt.PySide2.__version__.split("."))) >= (5, 14) + and tuple(map(int, Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), + reason="new PySide2 doesn't have loadUi functionality" +) @pytest.mark.parametrize( "frontend, f", [ From f8c107e7b268b1429ac10390bfad14433c867a2e Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 20:18:17 -0700 Subject: [PATCH 203/235] Do not emit loadUiType warning for pyside2 5.14.2.2 --- pyqtgraph/Qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 25cb488f..6035d7ac 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -119,7 +119,8 @@ def _loadUiType(uiFile): # convert ui file to python code if pysideuic is None: - if PySide2.__version__[:5].split('.')[:2] == ['5', '14']: + pyside2version = tuple(map(int, PySide2.__version__.split("."))) + if pyside2version >= (5, 14) and pyside2version < (5, 14, 2, 2): warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') uipy = subprocess.check_output(['pyside2-uic', uiFile]) else: From 3ed8c495990d24f75712297e7b884cc77352723b Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 21:22:01 -0700 Subject: [PATCH 204/235] test_loadUiType should run on 5.14.2.2 --- pyqtgraph/tests/test_qt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index 9a4f373b..3ecf9db8 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -15,7 +15,12 @@ def test_isQObjectAlive(): @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' 'packaged with conda') -@pytest.mark.skipif(pg.Qt.QT_LIB == "PySide2" and "pg.Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") +@pytest.mark.skipif( + pg.Qt.QT_LIB == "PySide2" + and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) >= (5, 14) + and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), + reason="new PySide2 doesn't have loadUi functionality" +) def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) From a171a098ad16b42cdcb2dc27dc1380ece0b12062 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 21:27:49 -0700 Subject: [PATCH 205/235] Expand CI to test latest PySide2 --- README.md | 17 +++++++++-------- azure-test-template.yml | 8 ++++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d082d7ee..5ab066e2 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,16 @@ Qt Bindings Test Matrix The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images. -| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | -| :----------- | :----------------: | :----------------: | :----------------: | :----------------: | -| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | -| PySide1 | :white_check_mark: | :x: | :x: | :x: | -| PyQt-5.9 | :x: | :white_check_mark: | :x: | :x: | -| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | -| PyQt-5.14 | :x: | :x: | :x: | :white_check_mark: | +| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | +| :------------- | :----------------: | :----------------: | :----------------: | :----------------: | +| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | +| PySide1 | :white_check_mark: | :x: | :x: | :x: | +| PyQt5-5.9 | :x: | :white_check_mark: | :x: | :x: | +| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | +| PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: | +| PySide2-Latest | :x: | :x: | :x: | :white_check_mark: | -* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible +* pyqtgraph has had some incompatibilities with PySide2-5.6, and we recommend you avoid those bindings if possible * on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work Support diff --git a/azure-test-template.yml b/azure-test-template.yml index 04fd7e42..d3966499 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -18,7 +18,7 @@ jobs: python.version: '2.7' qt.bindings: "pyside" install.method: "conda" - Python36-PyQt-5.9: + Python36-PyQt5-5.9: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" @@ -26,10 +26,14 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python38-PyQt-5.14: + Python38-PyQt5-Latest: python.version: '3.8' qt.bindings: "PyQt5" install.method: "pip" + Python38-PySide2-Latest: + python.version: '3.8' + qt.bindings: "PySide2" + install.method: "pip" steps: - task: DownloadPipelineArtifact@2 From d282f8aba8e09c70b44c4b5ad5c0ab9a1183c32b Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 5 Jun 2020 20:57:20 -0700 Subject: [PATCH 206/235] Remove workaround for memory leak in QImage (#1223) Co-authored-by: Ognyan Moore --- pyqtgraph/functions.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index e788afa7..193cce6a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1261,30 +1261,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if QT_LIB in ['PySide', 'PySide2']: ch = ctypes.c_char.from_buffer(imgData, 0) - - # Bug in PySide + Python 3 causes refcount for image data to be improperly - # incremented, which leads to leaked memory. As a workaround, we manually - # reset the reference count after creating the QImage. - # See: https://bugreports.qt.io/browse/PYSIDE-140 - - # Get initial reference count (PyObject struct has ob_refcnt as first element) - rcount = ctypes.c_long.from_address(id(ch)).value img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) - if sys.version[0] == '3': - # Reset refcount only on python 3. Technically this would have no effect - # on python 2, but this is a nasty hack, and checking for version here - # helps to mitigate possible unforseen consequences. - ctypes.c_long.from_address(id(ch)).value = rcount else: - #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) ## So we first attempt the 4.9.6 API, then fall back to 4.9.3 - #addr = ctypes.c_char.from_buffer(imgData, 0) - #try: - #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) - #except TypeError: - #addr = ctypes.addressof(addr) - #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) try: img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat) except: @@ -1297,16 +1277,6 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): img.data = imgData return img - #try: - #buf = imgData.data - #except AttributeError: ## happens when image data is non-contiguous - #buf = imgData.data - - #profiler() - #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) - #profiler() - #qimage.data = imgData - #return qimage def imageToArray(img, copy=False, transpose=True): """ From c0b9bfa040575ea6bd0580a87aa3e3efdbd99d7f Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 5 Jun 2020 21:00:18 -0700 Subject: [PATCH 207/235] Remove commented out line --- pyqtgraph/imageview/ImageView.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index f93c1fea..8324cbbc 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -586,7 +586,6 @@ class ImageView(QtGui.QWidget): # Extract image data from ROI axes = (self.axes['x'], self.axes['y']) - #data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, returnMappedCoords=True) if data is None: return From d86bb65520f92061814668cab937df36c2e0bfe5 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:52:55 +0200 Subject: [PATCH 208/235] ParameterTree: Fix custom context menu This issue was introduced in merging develop into #1175. While refactoring for the merge, the change in namespace was not correctly attributed, leading to the parameter `opts` to be assumed in local namespace when it isn't. --- pyqtgraph/parametertree/ParameterItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index ecafd577..ab5fad96 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -117,7 +117,7 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) # context menu - context = opts.get('context', None) + context = self.param.opts.get('context', None) if isinstance(context, list): for name in context: self.contextMenu.addAction(name).triggered.connect( From 78929adbea2944d661cfdd3981ec582e1ba5fe4f Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 6 Jun 2020 16:04:05 +0200 Subject: [PATCH 209/235] ParameterItem: self.param.opts -> opts Using `opts` as alias for `self.param.opts`, following the style of `updateFlags`. --- pyqtgraph/parametertree/ParameterItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index ab5fad96..b697b956 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -104,20 +104,22 @@ class ParameterItem(QtGui.QTreeWidgetItem): pass def contextMenuEvent(self, ev): - if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\ - and "context" not in self.param.opts: + opts = self.param.opts + + if not opts.get('removable', False) and not opts.get('renamable', False)\ + and "context" not in opts: return ## Generate context menu for renaming/removing parameter self.contextMenu = QtGui.QMenu() # Put in global name space to prevent garbage collection self.contextMenu.addSeparator() - if self.param.opts.get('renamable', False): + if opts.get('renamable', False): self.contextMenu.addAction('Rename').triggered.connect(self.editName) - if self.param.opts.get('removable', False): + if opts.get('removable', False): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) # context menu - context = self.param.opts.get('context', None) + context = opts.get('context', None) if isinstance(context, list): for name in context: self.contextMenu.addAction(name).triggered.connect( From 120d251a25c95034d304605b6ed941e64b8fabda Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 6 Jun 2020 15:56:01 -0700 Subject: [PATCH 210/235] Minor improvements to LegendItem. - Adds doc strings for user-facing methods so they appear in the documentation. - Allows PlotItem.addLegend to accept the same arguments as LegendItem constructor for convenience. - Fixes a bug for adding a BarGraphItem (which doesn't have an antialias option) to LegendItem --- pyqtgraph/graphicsItems/LegendItem.py | 56 ++++++++++---------- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 12 +++-- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 7d60f37a..da830782 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -13,10 +13,13 @@ __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Displays a legend used for describing the contents of a plot. - LegendItems are most commonly created by calling PlotItem.addLegend(). - Note that this item should not be added directly to a PlotItem. Instead, - Make it a direct descendant of the PlotItem:: + LegendItems are most commonly created by calling :meth:`PlotItem.addLegend + `. + + Note that this item should *not* be added directly to a PlotItem (via + :meth:`PlotItem.addItem `). Instead, make it a + direct descendant of the PlotItem:: legend.setParentItem(plotItem) @@ -46,8 +49,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): ============== =============================================================== """ - - GraphicsWidget.__init__(self) GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemIgnoresTransformations) @@ -71,9 +72,11 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.opts.update(kwargs) def offset(self): + """Get the offset position relative to the parent.""" return self.opts['offset'] def setOffset(self, offset): + """Set the offset position relative to the parent.""" self.opts['offset'] = offset offset = Point(self.opts['offset']) @@ -83,13 +86,13 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) def pen(self): + """Get the QPen used to draw the border around the legend.""" return self.opts['pen'] def setPen(self, *args, **kargs): - """ - Sets the pen used to draw lines between points. - *pen* can be a QPen or any argument accepted by - :func:`pyqtgraph.mkPen() ` + """Set the pen used to draw a border around the legend. + + Accepts the same arguments as :func:`~pyqtgraph.mkPen`. """ pen = fn.mkPen(*args, **kargs) self.opts['pen'] = pen @@ -97,9 +100,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def brush(self): + """Get the QBrush used to draw the legend background.""" return self.opts['brush'] def setBrush(self, *args, **kargs): + """Set the brush used to draw the legend background. + + Accepts the same arguments as :func:`~pyqtgraph.mkBrush`. + """ brush = fn.mkBrush(*args, **kargs) if self.opts['brush'] == brush: return @@ -108,13 +116,13 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def labelTextColor(self): + """Get the QColor used for the item labels.""" return self.opts['labelTextColor'] def setLabelTextColor(self, *args, **kargs): - """ - Sets the color of the label text. - *pen* can be a QPen or any argument accepted by - :func:`pyqtgraph.mkColor() ` + """Set the color of the item labels. + + Accepts the same arguments as :func:`~pyqtgraph.mkColor`. """ self.opts['labelTextColor'] = fn.mkColor(*args, **kargs) for sample, label in self.items: @@ -123,6 +131,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def setParentItem(self, p): + """Set the parent.""" ret = GraphicsWidget.setParentItem(self, p) if self.opts['offset'] is not None: offset = Point(self.opts['offset']) @@ -138,10 +147,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): ============== ======================================================== **Arguments:** - item A PlotDataItem from which the line and point style - of the item will be determined or an instance of - ItemSample (or a subclass), allowing the item display - to be customized. + item A :class:`~pyqtgraph.PlotDataItem` from which the line + and point style of the item will be determined or an + instance of ItemSample (or a subclass), allowing the + item display to be customized. title The title to display for this item. Simple HTML allowed. ============== ======================================================== """ @@ -177,7 +186,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): return # return after first match def clear(self): - """Removes all items from legend.""" + """Remove all items from the legend.""" for sample, label in self.items: self.layout.removeItem(sample) self.layout.removeItem(label) @@ -185,15 +194,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.items = [] self.updateSize() - def clear(self): - """ - Removes all items from the legend. - - Useful for reusing and dynamically updating charts and their legends. - """ - while self.items != []: - self.removeItem(self.items[0][1].text) - def updateSize(self): if self.size is not None: return @@ -234,7 +234,7 @@ class ItemSample(GraphicsWidget): def paint(self, p, *args): opts = self.item.opts - if opts['antialias']: + if opts.get('antialias'): p.setRenderHint(p.Antialiasing) if not isinstance(self.item, ScatterPlotItem): diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 4142fa3f..77f4cc15 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -682,17 +682,19 @@ class PlotItem(GraphicsWidget): return item - def addLegend(self, size=None, offset=(30, 30)): + def addLegend(self, offset=(30, 30), **kwargs): """ - Create a new LegendItem and anchor it over the internal ViewBox. - Plots will be automatically displayed in the legend if they - are created with the 'name' argument. + Create a new :class:`~pyqtgraph.LegendItem` and anchor it over the + internal ViewBox. Plots will be automatically displayed in the legend + if they are created with the 'name' argument. If a LegendItem has already been created using this method, that item will be returned rather than creating a new one. + + Accepts the same arguments as :meth:`~pyqtgraph.LegendItem`. """ if self.legend is None: - self.legend = LegendItem(size, offset) + self.legend = LegendItem(offset=offset, **kwargs) self.legend.setParentItem(self.vb) return self.legend From 3cad91b5f1d50bddbea6d2c945d9041c3b3d0f19 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 6 Jun 2020 16:22:16 -0700 Subject: [PATCH 211/235] Wrap text in tables in docs. --- doc/source/_static/custom.css | 14 ++++++++++++++ doc/source/conf.py | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 doc/source/_static/custom.css diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 00000000..2ad3413b --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,14 @@ +/* Customizations to the theme */ + +/* override table width restrictions */ +/* https://github.com/readthedocs/sphinx_rtd_theme/issues/117 */ +@media screen and (min-width: 768px) { + .wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + max-width: 100%; + } +} diff --git a/doc/source/conf.py b/doc/source/conf.py index a6e2cf8c..04a95afd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -131,7 +131,11 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +html_static_path = ['_static'] + +# add the theme customizations +def setup(app): + app.add_stylesheet("custom.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From f43b2973125a4510fbd1dda300cce2cee249e422 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sat, 6 Jun 2020 22:06:14 -0500 Subject: [PATCH 212/235] Update changelog with changes since v0.11.0rc0 (#1230) * Update changelog with changes since v0.11.0rc0 * tab to spaces --- CHANGELOG | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9a3bcf4e..65b423dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -65,6 +65,10 @@ pyqtgraph-0.11.0 (in development) - #996: Allow the update of LegendItem - #1023: Add bookkeeping exporter parameters - #1072: HDF5Exporter handling of ragged curves with tests + - #1124: Syntax highlighting for examples. + - #1154: Date axis item + - #393: NEW show/hide gradient ticks NEW link gradientEditor to others + - #1211: Add support for running pyside2-uic binary to dynamically compile ui files API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -102,6 +106,32 @@ pyqtgraph-0.11.0 (in development) - #1076: Reset currentRow and currentCol on GraphicsLayout.clear() - #1079: Improve performance of updateData PlotCurveItem - #1082: Allow MetaArray.__array__ to accept an optional dtype arg + - #841: set color of tick-labels separately + - #1111: Add name label to GradientEditorItem + - #1145: Pass showAxRect keyword arguments to setRange + - #1184: improve SymbolAtlas.getSymbolCoords performance + - #1198: improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance + - #1197: Disable remove ROI menu action in handle context menu + - #1188: Added support for plot curve to handle both fill and connect args + - #801: Remove use of GraphicsScene._addressCache in translateGraphicsItem + - Deprecates registerObject meethod of GraphicsScene + - Deprecates regstar argument to GraphicsScene.__init__ + - #1166: pg.mkQApp: Pass non-empty string array to QApplication() as default + - #1199: Pass non-empty sys.argv to QApplication + - #1090: dump ExportDialog.exporterParameters + - #1173: GraphicsLayout: Always call layout.activate() after adding items + - #1097: pretty-print log-scale axes labels + - #755: Check lastDownsample in viewTransformChanged + - #1216: Add cache for mapRectFromView + - #444: Fix duplicate menus in GradientEditorItem + - #151: Optionally provide custom PlotItem to PlotWidget + - #1093: Fix aspectRatio and zoom range issues when zooming + - #390: moved some functionality from method 'export' to new method + - #391: changed structure to redefine axis via plotitem.setAxes + - #468: Patch/window handling + - #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked' + - #1206: Added context menu option to parametertree + - #1228: Minor improvements to LegendItem Bugfixes: - #88: Fixed image scatterplot export @@ -227,6 +257,33 @@ pyqtgraph-0.11.0 (in development) - #1073: Fix Python3 compatibility - #1083: Fix SVG export of scatter plots - #1085: Fix ofset when drawing symbol + - #1101: Fix small oversight in LegendItem + - #1113: Correctly call hasFaceIndexedData function + - #1139: Bug fix in LegendItem for `setPen`, `setBrush` etc (Call update instead of paint) + - #1110: fix for makeARGB error after #955 + - #1063: Fix: AttributeError in ViewBox.setEnableMenu + - #1151: ImageExporter py2-pyside fix with test + - #1133: compatibility-fix for py2/pyside + - #1152: Nanmask fix in makeARGB + - #1159: Fix: Update axes after data is set + - #1156: SVGExporter: Correct image pixelation + - #1169: Replace default list arg with None + - #770: Do not ignore pos argument of setCameraPosition + - #1180: Fix: AxisItem tickFont is defined in two places while only one is used + - #1168: GroupParameterItem: Did not pass changed options to ParameterItem + - #1174: Fixed a possible race condition with linked views + - #809: Fix selection of FlowchartWidget input/output nodes + - #1071: Fix py3 execution in flowchart + - #1212: Fix PixelVectors cache + - #1161: Correctly import numpy where needed + - #1218: Fix ParameterTree.clear() + - #1175: Fix: Parameter tree ignores user-set 'expanded' state + - #1219: Encode csv export header as unicode + - #507: Fix Dock close event QLabel still running with no parent + - #1222: py3 fix for ScatterPlotWidget.setSelectedFields + - #1203: Image axis order bugfix + - #1225: ParameterTree: Fix custom context menu + Maintenance: - Lots of new unit tests @@ -240,6 +297,17 @@ pyqtgraph-0.11.0 (in development) - #1042: Close windows at the end of test functions - #1046: Establish minimum numpy version, remove legacy workarounds - #1067: Make scipy dependency optional + - #1114: doc: Fix small mistake in introduction + - #1131: Update CI/tox and Enable More Tests + - #1142: Miscellaneous doc fixups + - #1179: DateAxisItem: AxisItem unlinking tests and doc fixed + - #1201: Get readthedocs working + - #1214: Pin PyVirtualDisplay Version + - #1215: Skip test when on qt 5.9 + - #1221: Identify pyqt5 5.15 ci issue + - #1223: Remove workaround for memory leak in QImage + - #1217: Get docs version and copyright year dynamically + - #1229: Wrap text in tables in docs pyqtgraph-0.10.0 From 4a5af52fca8c3f2223a8358a1851f9a0cc3d4d39 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sat, 6 Jun 2020 20:34:21 -0700 Subject: [PATCH 213/235] Update README for 0.11 release --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5ab066e2..07787663 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ PyQtGraph A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2 -Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2020 Luke Campagnola, University of North Carolina at Chapel Hill @@ -19,18 +19,14 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for Requirements ------------ -* PyQt 4.8+, PySide, PyQt5, or PySide2 - * PySide2 5.14 does not have loadUiType functionality, and thus the example application will not work. You can follow along with restoring that functionality [here](https://bugreports.qt.io/browse/PYSIDE-1223). * Python 2.7, or 3.x * Required + * PyQt 4.8+, PySide, PyQt5, or PySide2 * `numpy` * Optional * `scipy` for image processing * `pyopengl` for 3D graphics - * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics - * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` * `hdf5` for large hdf5 binary format support -* Known to run on Windows, Linux, and macOS. Qt Bindings Test Matrix ----------------------- @@ -46,8 +42,8 @@ The following table represents the python environments we test in our CI system. | PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: | | PySide2-Latest | :x: | :x: | :x: | :white_check_mark: | -* pyqtgraph has had some incompatibilities with PySide2-5.6, and we recommend you avoid those bindings if possible -* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work +* pyqtgraph has had some incompatibilities with PySide2 versions 5.6-5.11, and we recommend you avoid those versions if possible +* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work reliably Support ------- @@ -60,18 +56,17 @@ Installation Methods * From PyPI: * Last released version: `pip install pyqtgraph` - * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@master` * From conda - * Last released version: `conda install pyqtgraph` + * Last released version: `conda install -c conda-forge pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory anywhere that is importable from your project. -* For installation packages, see the website (pyqtgraph.org) Documentation ------------- -The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. - The official documentation lives at https://pyqtgraph.readthedocs.io + +The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` to launch the examples application. From 5b5749aa0b1535a044f2e9b8c09cd1e75e988ea8 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 7 Jun 2020 20:29:28 -0700 Subject: [PATCH 214/235] Revert "changed structure to redefine axis via plotitem.setAxes (#391)" This reverts commit bb21791c710ccd11c889a6641672adc7fbbdcf3e. --- pyqtgraph/graphicsItems/AxisItem.py | 29 ++--------- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 51 +++----------------- 2 files changed, 12 insertions(+), 68 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c02e6e0c..29f3ad62 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -45,8 +45,11 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None - self.orientation = None - self.setOrientation(orientation) + self.orientation = orientation + if orientation not in ['left', 'right', 'top', 'bottom']: + raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") + if orientation in ['left', 'right']: + self.label.rotate(-90) self.style = { 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis @@ -108,27 +111,6 @@ class AxisItem(GraphicsWidget): self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) - def setOrientation(self, orientation): - """ - orientation = 'left', 'right', 'top', 'bottom' - """ - if orientation != self.orientation: - if orientation not in ['left', 'right', 'top', 'bottom']: - raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") - #rotate absolute allows to change orientation multiple times: - if orientation in ['left', 'right']: - self.label.setRotation(-90) - if self.orientation: - self._updateWidth() - self.setMaximumHeight(16777215) - else: - self.label.setRotation(0) - if self.orientation: - self._updateHeight() - self.setMaximumWidth(16777215) - self.orientation = orientation - - def setStyle(self, **kwds): """ Set various style options. @@ -532,7 +514,6 @@ class AxisItem(GraphicsWidget): self.unlinkFromView() self._linkedView = weakref.ref(view) - if self.orientation in ['right', 'left']: view.sigYRangeChanged.connect(self.linkedViewChanged) else: diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 77f4cc15..c61a35b2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -153,10 +153,10 @@ class PlotItem(GraphicsWidget): self.legend = None - ## Create and place axis items + # Initialize axis items self.axes = {} - self.setAxes(axisItems) - + self.setAxisItems(axisItems) + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide @@ -242,7 +242,7 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - + if labels is None: labels = {} for label in list(self.axes.keys()): @@ -258,45 +258,8 @@ class PlotItem(GraphicsWidget): self.setTitle(title) if len(kargs) > 0: - self.plot(**kargs) + self.plot(**kargs) - def setAxes(self, axisItems): - """ - Create and place axis items - For valid values for axisItems see __init__ - """ - for v in self.axes.values(): - item = v['item'] - self.layout.removeItem(item) - self.vb.removeItem(item) - - self.axes = {} - if axisItems is None: - axisItems = {} - for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, None) - if axis: - axis.setOrientation(k) - else: - axis = AxisItem(orientation=k) - axis.linkToView(self.vb) - self.axes[k] = {'item': axis, 'pos': pos} - self.layout.addItem(axis, *pos) - axis.setZValue(-1000) - axis.setFlag(axis.ItemNegativeZStacksBehindParent) - #show/hide axes: - all_dir = ['left', 'bottom', 'right', 'top'] - if axisItems: - to_show = list(axisItems.keys()) - to_hide = [a for a in all_dir if a not in to_show] - else: - to_show = all_dir[:2] - to_hide = all_dir[2:] - for a in to_hide: - self.hideAxis(a) - for a in to_show: - self.showAxis(a) - def implements(self, interface=None): return interface in ['ViewBoxWrapper'] @@ -1162,8 +1125,8 @@ class PlotItem(GraphicsWidget): Show or hide one of the plot's axes. axis must be one of 'left', 'bottom', 'right', or 'top' """ - s = self.getAxis(axis) - #p = self.axes[axis]['pos'] + s = self.getScale(axis) + p = self.axes[axis]['pos'] if show: s.show() else: From e1f2cdce7441f0351afc72a9f36ab007da8b25c9 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Mon, 8 Jun 2020 18:20:41 -0500 Subject: [PATCH 215/235] Final preparations for 0.11.0 release Intend to tag and upload after this is merged --- CHANGELOG | 6 ++++-- pyqtgraph/__init__.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 65b423dd..efc3ee3b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ -pyqtgraph-0.11.0 (in development) +pyqtgraph-0.11.0 + + NOTICE: This is the _last_ feature release to support Python 2 and Qt 4 (PyQt4 or pyside 1) New Features: - #101: GridItem formatting options @@ -127,7 +129,6 @@ pyqtgraph-0.11.0 (in development) - #151: Optionally provide custom PlotItem to PlotWidget - #1093: Fix aspectRatio and zoom range issues when zooming - #390: moved some functionality from method 'export' to new method - - #391: changed structure to redefine axis via plotitem.setAxes - #468: Patch/window handling - #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked' - #1206: Added context menu option to parametertree @@ -308,6 +309,7 @@ pyqtgraph-0.11.0 (in development) - #1223: Remove workaround for memory leak in QImage - #1217: Get docs version and copyright year dynamically - #1229: Wrap text in tables in docs + - #1231: Update readme for 0.11 release pyqtgraph-0.10.0 diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f834e637..bc36e891 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.11.0.dev0' +__version__ = '0.11.0' ### import all the goodies and add some helper functions for easy CLI use From 66d89433170ada9ed5a9ac0b14afacf2d7dbefca Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 8 Jun 2020 21:36:09 -0700 Subject: [PATCH 216/235] PlotItem doesn't add item if already there --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c61a35b2..79d59235 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import sys +import warnings import weakref import numpy as np import os @@ -514,6 +515,9 @@ class PlotItem(GraphicsWidget): If the item has plot data (PlotDataItem, PlotCurveItem, ScatterPlotItem), it may be included in analysis performed by the PlotItem. """ + if item in self.items: + warnings.warn('Item already added to PlotItem, ignoring.') + return self.items.append(item) vbargs = {} if 'ignoreBounds' in kargs: From fc7921100e6bb66b0367f2c57e7122ace832b918 Mon Sep 17 00:00:00 2001 From: alfon_news Date: Tue, 9 Jun 2020 07:51:14 +0200 Subject: [PATCH 217/235] Fix siScale imprecision errors (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix siScale imprecision errors * Implement 2xB suggested change Co-authored-by: Alberto Fontán Correa Co-authored-by: Ogi Moore --- pyqtgraph/functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 193cce6a..b202e86a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -77,7 +77,8 @@ def siScale(x, minVal=1e-25, allowUnicode=True): pref = SI_PREFIXES[m+8] else: pref = SI_PREFIXES_ASCII[m+8] - p = .001**m + m1 = -3*m + p = 10.**m1 return (p, pref) From 2848d451f67695ab34e7b2832f24c1e948784cc7 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Tue, 9 Jun 2020 08:33:12 +0200 Subject: [PATCH 218/235] draw connector lines between gradient and region with anti-aliasing (#496) Co-authored-by: serkgb Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index ad39b60e..38f1e5b4 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -142,6 +142,7 @@ class HistogramLUTItem(GraphicsWidget): p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) + p.setRenderHint(QtGui.QPainter.Antialiasing) for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]: p.setPen(pen) p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft()) From 258da198867516ba6959e5274a79fa924462e919 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 9 Jun 2020 20:26:21 -0700 Subject: [PATCH 219/235] Use older pytest-xvfb for py2 configs --- azure-test-template.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 902077a6..e1d4e177 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -135,7 +135,12 @@ jobs: then source activate test-environment-$(python.version) fi - pip install PyVirtualDisplay==0.2.5 pytest-xvfb + if [ $(python.version) == "2.7" ] + then + pip install PyVirtualDisplay==0.2.5 pytest-xvfb==1.2.0 + else + pip install pytest-xvfb + fi displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) From e18af48b8dfd67801ad22596127fd0f355d64feb Mon Sep 17 00:00:00 2001 From: Maurice van der Pot Date: Wed, 10 Jun 2020 07:04:29 +0200 Subject: [PATCH 220/235] Implement headWidth parameter for arrows (#385) Although the documentation used to say that specifying tipAngle would override headWidth, headWidth was never used. The new behaviour is that tipAngle will be used, with a default value of 25, unless headWidth is specified. Co-authored-by: Ogi Moore --- examples/Arrow.py | 2 +- pyqtgraph/functions.py | 7 ++++--- pyqtgraph/graphicsItems/ArrowItem.py | 9 +++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/Arrow.py b/examples/Arrow.py index d5ea2a74..2a707fec 100644 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -30,7 +30,7 @@ p2 = cw.addPlot(row=1, col=0) ## variety of arrow shapes a1 = pg.ArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3}) a2 = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, headLen=40, tailLen=40, tailWidth=8, pen=None, brush='y') -a3 = pg.ArrowItem(angle=-60, tipAngle=30, baseAngle=20, headLen=40, tailLen=None, brush=None) +a3 = pg.ArrowItem(angle=-60, baseAngle=20, headLen=40, headWidth=20, tailLen=None, brush=None) a4 = pg.ArrowItem(angle=-20, tipAngle=30, baseAngle=-30, headLen=40, tailLen=None) a2.setPos(10,0) a3.setPos(20,0) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b202e86a..e47aa411 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -389,14 +389,15 @@ def glColor(*args, **kargs): -def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): +def makeArrowPath(headLen=20, headWidth=None, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): """ Construct a path outlining an arrow with the given dimensions. The arrow points in the -x direction with tip positioned at 0,0. - If *tipAngle* is supplied (in degrees), it overrides *headWidth*. + If *headWidth* is supplied, it overrides *tipAngle* (in degrees). If *tailLen* is None, no tail will be drawn. """ - headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.) + if headWidth is None: + headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.) path = QtGui.QPainterPath() path.moveTo(0,0) path.lineTo(headLen, -headWidth) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 897cbc50..b272b7fc 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -28,6 +28,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): 'angle': -150, ## If the angle is 0, the arrow points left 'pos': (0,0), 'headLen': 20, + 'headWidth': None, 'tipAngle': 25, 'baseAngle': 0, 'tailLen': None, @@ -52,10 +53,10 @@ class ArrowItem(QtGui.QGraphicsPathItem): 0; arrow pointing to the left. headLen Length of the arrow head, from tip to base. default=20 - headWidth Width of the arrow head at its base. + headWidth Width of the arrow head at its base. If + headWidth is specified, it overrides tipAngle. tipAngle Angle of the tip of the arrow in degrees. Smaller - values make a 'sharper' arrow. If tipAngle is - specified, ot overrides headWidth. default=25 + values make a 'sharper' arrow. default=25 baseAngle Angle of the base of the arrow head. Default is 0, which means that the base of the arrow head is perpendicular to the arrow tail. @@ -70,7 +71,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): """ self.opts.update(opts) - opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) + opt = dict([(k,self.opts[k]) for k in ['headLen', 'headWidth', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) tr = QtGui.QTransform() tr.rotate(self.opts['angle']) self.path = tr.map(fn.makeArrowPath(**opt)) From 2e8dce2fc2b7b25d6360e6bafd3933cafa85fa54 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 10 Jun 2020 20:08:34 -0700 Subject: [PATCH 221/235] Emit the event with sigClicked in PlotCurveItem --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c3a58da2..b6c6d216 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -613,7 +613,7 @@ class PlotCurveItem(GraphicsObject): return if self.mouseShape().contains(ev.pos()): ev.accept() - self.sigClicked.emit(self) + self.sigClicked.emit(self, ev) From 05f8921555f8957205b900b9ea9213df0d17cad0 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 10 Jun 2020 20:50:04 -0700 Subject: [PATCH 222/235] Implement suggested changes in PR 143 --- pyqtgraph/graphicsItems/ROI.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 43bb921d..fdcada14 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -590,13 +590,13 @@ class ROI(GraphicsObject): ## If a Handle was not supplied, create it now if 'item' not in info or info['item'] is None: h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) - h.setPos(info['pos'] * self.state['size']) info['item'] = h else: h = info['item'] if info['pos'] is None: info['pos'] = h.pos() - + h.setPos(info['pos'] * self.state['size']) + ## connect the handle to this ROI #iid = len(self.handles) h.connectROI(self) @@ -1273,11 +1273,12 @@ class Handle(UIGraphicsItem): sigClicked = QtCore.Signal(object, object) # self, event sigRemoveRequested = QtCore.Signal(object) # self - def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): + def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False, activePen=(255, 255, 0)): self.rois = [] self.radius = radius self.typ = typ self.pen = fn.mkPen(pen) + self.activePen = fn.mkPen(activePen) self.currentPen = self.pen self.pen.setWidth(0) self.pen.setCosmetic(True) @@ -1321,7 +1322,7 @@ class Handle(UIGraphicsItem): hover=True if hover: - self.currentPen = fn.mkPen(255, 255,0) + self.currentPen = self.activePen else: self.currentPen = self.pen self.update() @@ -1374,15 +1375,19 @@ class Handle(UIGraphicsItem): for r in self.rois: r.stateChangeFinished() self.isMoving = False + self.currentPen = self.pen + self.update() elif ev.isStart(): for r in self.rois: r.handleMoveStarted() self.isMoving = True self.startPos = self.scenePos() self.cursorOffset = self.scenePos() - ev.buttonDownScenePos() + self.currentPen = self.activePen if self.isMoving: ## note: isMoving may become False in mid-drag due to right-click. pos = ev.scenePos() + self.cursorOffset + self.currentPen = self.activePen self.movePoint(pos, ev.modifiers(), finish=False) def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): From dbdd5d9a395befc6b589b55f5c625efa7c77812e Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 10 Jun 2020 23:03:43 -0700 Subject: [PATCH 223/235] Peque scatter symbols (#1244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added arrow symbols for the ScatterPlotItem * Fixed arrows rotation in scatter plots * Added new symbols to example Co-authored-by: Miguel Sánchez de León Peque --- examples/ScatterPlot.py | 3 +-- examples/Symbols.py | 4 ++++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 93f184f2..ea86bd19 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -83,7 +83,7 @@ random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%10, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]] s2.addPoints(spots) @@ -120,4 +120,3 @@ if __name__ == '__main__': import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() - diff --git a/examples/Symbols.py b/examples/Symbols.py index 417df35e..a0c57f75 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -29,6 +29,10 @@ plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'") plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'") plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'") +plot.plot([11, 12, 13, 14, 15], pen=(253, 216, 53), symbolBrush=(253, 216, 53), symbolPen='w', symbol='arrow_down', symbolSize=22, name="symbol='arrow_down'") +plot.plot([12, 13, 14, 15, 16], pen=(189, 189, 189), symbolBrush=(189, 189, 189), symbolPen='w', symbol='arrow_left', symbolSize=22, name="symbol='arrow_left'") +plot.plot([13, 14, 15, 16, 17], pen=(187, 26, 95), symbolBrush=(187, 26, 95), symbolPen='w', symbol='arrow_up', symbolSize=22, name="symbol='arrow_up'") +plot.plot([14, 15, 16, 17, 18], pen=(248, 187, 208), symbolBrush=(248, 187, 208), symbolPen='w', symbol='arrow_right', symbolSize=22, name="symbol='arrow_right'") plot.setXRange(-2, 4) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index af6efcc8..1140c36f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from itertools import starmap, repeat try: from itertools import imap @@ -20,7 +21,9 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']]) +name_list = ['o', 's', 't', 't1', 't2', 't3', 'd', '+', 'x', 'p', 'h', 'star', + 'arrow_up', 'arrow_right', 'arrow_down', 'arrow_left'] +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in name_list]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { @@ -41,7 +44,11 @@ coords = { 'star': [(0, -0.5), (-0.1123, -0.1545), (-0.4755, -0.1545), (-0.1816, 0.059), (-0.2939, 0.4045), (0, 0.1910), (0.2939, 0.4045), (0.1816, 0.059), (0.4755, -0.1545), - (0.1123, -0.1545)] + (0.1123, -0.1545)], + 'arrow_down': [ + (-0.125, 0.125), (0, 0), (0.125, 0.125), + (0.05, 0.125), (0.05, 0.5), (-0.05, 0.5), (-0.05, 0.125) + ] } for k, c in coords.items(): Symbols[k].moveTo(*c[0]) @@ -51,7 +58,10 @@ for k, c in coords.items(): tr = QtGui.QTransform() tr.rotate(45) Symbols['x'] = tr.map(Symbols['+']) - +tr.rotate(45) +Symbols['arrow_right'] = tr.map(Symbols['arrow_down']) +Symbols['arrow_up'] = tr.map(Symbols['arrow_right']) +Symbols['arrow_left'] = tr.map(Symbols['arrow_up']) def drawSymbol(painter, symbol, size, pen, brush): if symbol is None: From 12a7c449f10b2c4fb07a3df2ddcc40d678e8fe99 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 10 Jun 2020 23:31:39 -0700 Subject: [PATCH 224/235] Give ability to hide/show individual spots (#1245) Co-authored-by: dlidstrom --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 38 ++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 1140c36f..5bbdffe7 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -267,8 +267,8 @@ class ScatterPlotItem(GraphicsObject): self.picture = None # QPicture used for rendering when pxmode==False self.fragmentAtlas = SymbolAtlas() - - self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)]) + + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float), ('visible', bool)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots @@ -390,6 +390,7 @@ class ScatterPlotItem(GraphicsObject): newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size + newData['visible'] = True if 'spots' in kargs: spots = kargs['spots'] @@ -549,6 +550,28 @@ class ScatterPlotItem(GraphicsObject): if update: self.updateSpots(dataSet) + + def setPointsVisible(self, visible, update=True, dataSet=None, mask=None): + """Set whether or not each spot is visible. + If a list or array is provided, then the visibility for each spot will be set separately. + Otherwise, the argument will be used for all spots.""" + if dataSet is None: + dataSet = self.data + + if isinstance(visible, np.ndarray) or isinstance(visible, list): + visibilities = visible + if mask is not None: + visibilities = visibilities[mask] + if len(visibilities) != len(dataSet): + raise Exception("Number of visibilities does not match number of points (%d != %d)" % (len(visibilities), len(dataSet))) + dataSet['visible'] = visibilities + else: + dataSet['visible'] = visible + + dataSet['sourceRect'] = None + if update: + self.updateSpots(dataSet) + def setPointData(self, data, dataSet=None, mask=None): if dataSet is None: dataSet = self.data @@ -750,6 +773,8 @@ class ScatterPlotItem(GraphicsObject): (pts[0] - w < viewBounds.right()) & (pts[1] + w > viewBounds.top()) & (pts[1] - w < viewBounds.bottom())) ## remove out of view points + + mask &= self.data['visible'] return mask @debug.warnOnException ## raising an exception here causes crash @@ -975,6 +1000,15 @@ class SpotItem(object): self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) self.updateItem() + + def isVisible(self): + return self._data['visible'] + + def setVisible(self, visible): + """Set whether or not this spot is visible.""" + self._data['visible'] = visible + self.updateItem() + def setData(self, data): """Set the user-data associated with this spot""" self._data['data'] = data From 4dc0865bae08f141846ca0f9851c0af25a18ed91 Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 11 Jun 2020 20:55:28 -0700 Subject: [PATCH 225/235] Restore the now-deprecated PlotWindow and ImageWindow classes --- pyqtgraph/graphicsWindows.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index aa62f4f1..e915d1a8 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -55,10 +55,14 @@ class PlotWindow(PlotWidget): """ def __init__(self, title=None, **kargs): mkQApp() + self.win = QtGui.QMainWindow() PlotWidget.__init__(self, **kargs) + self.win.setCentralWidget(self) + for m in ['resize']: + setattr(self, m, getattr(self.win, m)) if title is not None: - self.setWindowTitle(title) - self.show() + self.win.setWindowTitle(title) + self.win.show() def closeEvent(self, event): PlotWidget.closeEvent(self, event) @@ -73,14 +77,20 @@ class ImageWindow(ImageView): """ def __init__(self, *args, **kargs): mkQApp() - ImageView.__init__(self) + self.win = QtGui.QMainWindow() + self.win.resize(800,600) if 'title' in kargs: - self.setWindowTitle(kargs['title']) + self.win.setWindowTitle(kargs['title']) del kargs['title'] + ImageView.__init__(self, self.win) if len(args) > 0 or len(kargs) > 0: self.setImage(*args, **kargs) - self.show() - + + self.win.setCentralWidget(self) + for m in ['resize']: + setattr(self, m, getattr(self.win, m)) + self.win.show() + def closeEvent(self, event): ImageView.closeEvent(self, event) self.sigClosed.emit(self) From 001d91c2f2b2aea66b0259590373f29db5dbec5c Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 11 Jun 2020 22:56:20 -0700 Subject: [PATCH 226/235] Implement PR160 - clear current SpotItems --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 5bbdffe7..63dc61cb 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -379,6 +379,9 @@ class ScatterPlotItem(GraphicsObject): kargs['y'] = [] numPts = 0 + ## Clear current SpotItems since the data references they contain will no longer be current + self.data['item'][...] = None + ## Extend record array oldData = self.data self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype) From 8da7c166c8088989afe0f46ad7990f5b078ca5aa Mon Sep 17 00:00:00 2001 From: carmazine Date: Fri, 12 Jun 2020 22:46:09 +0200 Subject: [PATCH 227/235] Parameterize utcOffset during construction Allows for control over timezone offset in a simple, optional manner --- pyqtgraph/graphicsItems/DateAxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index a5132fd9..846abb90 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -200,7 +200,7 @@ class DateAxisItem(AxisItem): """ - def __init__(self, orientation='bottom', **kwargs): + def __init__(self, orientation='bottom', utcOffset=time.timezone, **kwargs): """ Create a new DateAxisItem. @@ -211,7 +211,7 @@ class DateAxisItem(AxisItem): super(DateAxisItem, self).__init__(orientation, **kwargs) # Set the zoom level to use depending on the time density on the axis - self.utcOffset = time.timezone + self.utcOffset = utcOffset self.zoomLevels = OrderedDict([ (np.inf, YEAR_MONTH_ZOOM_LEVEL), From cee27b62684d4ce59578888e433b01daaac55893 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 12 Jun 2020 22:28:26 -0700 Subject: [PATCH 228/235] fix-incorrect-tick-text-boundaries-calculation --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 29f3ad62..5aca9cc4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -853,8 +853,8 @@ class AxisItem(GraphicsWidget): interpreted by drawPicture(). """ profiler = debug.Profiler() - - #bounds = self.boundingRect() + if self.style['tickFont'] is not None: + p.setFont(self.style['tickFont']) bounds = self.mapRectFromParent(self.geometry()) linkedView = self.linkedView() From 3a758cac961c18fd13a8d4160d2ed299e14bced6 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Sat, 13 Jun 2020 07:40:20 +0200 Subject: [PATCH 229/235] NEW options for LegendItem (#395) * NEW options for LegendItem * * changed 'drawFrame' into 'frame' * added **kwargs to plotItem.addLegend * added (frame=False, colCount=2) in legend example * more elegant solution for legend.getLabel * repaired getLabel ItemSample.item == plotitem Co-authored-by: Ogi Moore --- examples/Legend.py | 2 +- pyqtgraph/graphicsItems/LegendItem.py | 78 +++++++++++++++++--- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 + 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/examples/Legend.py b/examples/Legend.py index 3759c2e9..9239f1ae 100644 --- a/examples/Legend.py +++ b/examples/Legend.py @@ -13,7 +13,7 @@ win = pg.plot() win.setWindowTitle('pyqtgraph example: BarGraphItem') # # option1: only for .plot(), following c1,c2 for example----------------------- -# win.addLegend() +# win.addLegend(frame=False, rowCount=1, colCount=2) # bar graph x = np.arange(10) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 6afaed4d..67604c45 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -26,7 +26,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ def __init__(self, size=None, offset=None, horSpacing=25, verSpacing=0, pen=None, - brush=None, labelTextColor=None, **kwargs): + brush=None, labelTextColor=None, frame=True, rowCount=1, colCount=1, **kwargs): """ ============== =============================================================== **Arguments:** @@ -60,6 +60,11 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.setLayout(self.layout) self.items = [] self.size = size + self.offset = offset + self.frame = frame + self.columnCount = colCount + self.rowCount = rowCount + self.curRow = 0 if size is not None: self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1])) @@ -159,14 +164,52 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): if isinstance(item, ItemSample): sample = item else: - sample = ItemSample(item) - - row = self.layout.rowCount() + sample = ItemSample(item) self.items.append((sample, label)) - self.layout.addItem(sample, row, 0) - self.layout.addItem(label, row, 1) + self._addItemToLayout(sample, label) self.updateSize() + def _addItemToLayout(self, sample, label): + col = self.layout.columnCount() + row = self.layout.rowCount() + if row: + row -= 1 + nCol = self.columnCount*2 + #FIRST ROW FULL + if col == nCol: + for col in range(0,nCol,2): + #FIND RIGHT COLUMN + if not self.layout.itemAt(row, col): + break + if col+2 == nCol: + #MAKE NEW ROW + col = 0 + row += 1 + self.layout.addItem(sample, row, col) + self.layout.addItem(label, row, col+1) + + def setColumnCount(self, columnCount): + ''' + change the orientation of all items of the legend + ''' + if columnCount != self.columnCount: + self.columnCount = columnCount + self.rowCount = int(len(self.items)/columnCount) + for i in range(self.layout.count()-1,-1,-1): + self.layout.removeAt(i) #clear layout + for sample, label in self.items: + self._addItemToLayout(sample, label) + self.updateSize() + + def getLabel(self, plotItem): + """ + return the labelItem inside the legend for a given plotItem + the label-text can be changed via labenItem.setText + """ + out = [(it, lab) for it, lab in self.items if it.item==plotItem] + try: return out[0][1] + except IndexError: return None + def removeItem(self, item): """ Removes one item from the legend. @@ -198,16 +241,29 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def updateSize(self): if self.size is not None: return - - self.setGeometry(0, 0, 0, 0) + height = 0 + width = 0 + for row in range(self.layout.rowCount()): + row_height = 0 + col_witdh = 0 + for col in range(self.layout.columnCount()): + item = self.layout.itemAt(row, col) + if item: + col_witdh += item.width() + 3 + row_height = max(row_height, item.height()) + width = max(width, col_witdh) + height += row_height + self.setGeometry(0, 0, width, height) + return def boundingRect(self): return QtCore.QRectF(0, 0, self.width(), self.height()) def paint(self, p, *args): - p.setPen(self.opts['pen']) - p.setBrush(self.opts['brush']) - p.drawRect(self.boundingRect()) + if self.frame: + p.setPen(self.opts['pen']) + p.setBrush(self.opts['brush']) + p.drawRect(self.boundingRect()) def hoverEvent(self, ev): ev.acceptDrags(QtCore.Qt.LeftButton) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 79d59235..38a9ba5c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -660,6 +660,7 @@ class PlotItem(GraphicsWidget): Accepts the same arguments as :meth:`~pyqtgraph.LegendItem`. """ + if self.legend is None: self.legend = LegendItem(offset=offset, **kwargs) self.legend.setParentItem(self.vb) From 8b557af23f17572a67ad4aec01c331c679ae79f0 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 13 Jun 2020 21:21:29 -0700 Subject: [PATCH 230/235] Implement diff from PR 317 --- pyqtgraph/graphicsItems/ROI.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index fdcada14..c4b29669 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2121,6 +2121,23 @@ class LineSegmentROI(ROI): def listPoints(self): return [p['item'].pos() for p in self.handles] + + def getState(self): + state = ROI.getState(self) + state['points'] = [Point(h.pos()) for h in self.getHandles()] + return state + + def saveState(self): + state = ROI.saveState(self) + state['points'] = [tuple(h.pos()) for h in self.getHandles()] + return state + + def setState(self, state): + ROI.setState(self, state) + p1 = [state['points'][0][0]+state['pos'][0], state['points'][0][1]+state['pos'][1]] + p2 = [state['points'][1][0]+state['pos'][0], state['points'][1][1]+state['pos'][1]] + self.movePoint(self.getHandles()[0], p1, finish=False) + self.movePoint(self.getHandles()[1], p2) def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) From ad4f796e32552d07e75ef2f4298945e7494ea65e Mon Sep 17 00:00:00 2001 From: Vesna Tanko Date: Mon, 15 Jun 2020 11:03:50 +0200 Subject: [PATCH 231/235] AxisItem: Make painter tick font dependent --- pyqtgraph/graphicsItems/AxisItem.py | 2 ++ .../graphicsItems/tests/test_AxisItem.py | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 29f3ad62..85becaad 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -576,6 +576,8 @@ class AxisItem(GraphicsWidget): try: picture = QtGui.QPicture() painter = QtGui.QPainter(picture) + if self.style["tickFont"]: + painter.setFont(self.style["tickFont"]) specs = self.generateDrawSpecs(painter) profiler('generate specs') if specs is not None: diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py index 8d89259a..6e21396d 100644 --- a/pyqtgraph/graphicsItems/tests/test_AxisItem.py +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -87,3 +87,30 @@ def test_AxisItem_leftRelink(): assert fake_view.sigYRangeChanged.calls == ['connect', 'disconnect'] assert fake_view.sigXRangeChanged.calls == [] assert fake_view.sigResized.calls == ['connect', 'disconnect'] + + +def test_AxisItem_tickFont(monkeypatch): + def collides(textSpecs): + fontMetrics = pg.Qt.QtGui.QFontMetrics(font) + for rect, _, text in textSpecs: + br = fontMetrics.tightBoundingRect(text) + if rect.height() < br.height() or rect.width() < br.width(): + return True + return False + + def test_collision(p, axisSpec, tickSpecs, textSpecs): + assert not collides(textSpecs) + + plot = pg.PlotWidget() + bottom = plot.getAxis("bottom") + left = plot.getAxis("left") + font = bottom.linkedView().font() + font.setPointSize(25) + bottom.setStyle(tickFont=font) + left.setStyle(tickFont=font) + monkeypatch.setattr(bottom, "drawPicture", test_collision) + monkeypatch.setattr(left, "drawPicture", test_collision) + + plot.show() + app.processEvents() + plot.close() From e6c804632021e09a49207736b3c8cd1ace9a8519 Mon Sep 17 00:00:00 2001 From: Chris Lamb Date: Fri, 19 Jun 2020 12:54:21 +0100 Subject: [PATCH 232/235] Make the documentation reproducible Whilst working on the Reproducible Builds effort [0] we noticed that pyqtgraph could not be built reproducibly. This is because it generates copyright years from the current build date and therefore will vary on when you build it. This commit uses SOURCE_DATE_EPOCH [1] for the "current" build date. This was originally filed in Debian as #963124 [2]. [0] https://reproducible-builds.org/ [1] https://reproducible-builds.org/specs/source-date-epoch/ [2] https://bugs.debian.org/963124 --- doc/source/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 04a95afd..4038cba4 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,6 +11,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import time import sys import os from datetime import datetime @@ -46,7 +47,10 @@ master_doc = 'index' # General information about the project. project = 'pyqtgraph' -copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year) +now = datetime.utcfromtimestamp( + int(os.environ.get('SOURCE_DATE_EPOCH', time.time())) +) +copyright = '2011 - {}, Luke Campagnola'.format(now.year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 352a8a425a053e066ac64f194a78e5edcdc73255 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 19 Jun 2020 23:00:02 -0700 Subject: [PATCH 233/235] Add mouse event to PlotCurveItem sigClicked signature --- pyqtgraph/graphicsItems/PlotCurveItem.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b6c6d216..d2b87e09 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -29,15 +29,15 @@ class PlotCurveItem(GraphicsObject): - Fill under curve - Mouse interaction - ==================== =============================================== + ===================== =============================================== **Signals:** - sigPlotChanged(self) Emitted when the data being plotted has changed - sigClicked(self) Emitted when the curve is clicked - ==================== =============================================== + sigPlotChanged(self) Emitted when the data being plotted has changed + sigClicked(self, ev) Emitted when the curve is clicked + ===================== =============================================== """ sigPlotChanged = QtCore.Signal(object) - sigClicked = QtCore.Signal(object) + sigClicked = QtCore.Signal(object, object) def __init__(self, *args, **kargs): """ From 0b981408930ca34fd27f6c1dc3b094909b180544 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 20 Jun 2020 12:27:29 -0700 Subject: [PATCH 234/235] Check for container before setting dock orientation --- pyqtgraph/dockarea/Dock.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 15c87652..083756dc 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtCore, QtGui from .DockDrop import * @@ -138,6 +139,12 @@ class Dock(QtGui.QWidget, DockDrop): By default ('auto'), the orientation is determined based on the aspect ratio of the Dock. """ + # setOrientation may be called before the container is set in some cases + # (via resizeEvent), so there's no need to do anything here until called + # again by containerChanged + if self.container() is None: + return + if o == 'auto' and self.autoOrient: if self.container().type() == 'tab': o = 'horizontal' From f81768ac5960a830817e3e091f61736a6529fb1a Mon Sep 17 00:00:00 2001 From: jeremysee2 <32976023+jeremysee2@users.noreply.github.com> Date: Mon, 22 Jun 2020 04:59:44 +0800 Subject: [PATCH 235/235] Issue #1260: Added exception to checkOpenGLVersion to highlight OpenGL ES incompatibility on Raspberry Pi (#1264) * checkOpenGLVersion exception for OpenGL ES * checkOpenGLVersion exception * checkOpenGLVersion exception * python 2/3 compatibility * Refactoring checkOpenGLVersion Since the original goal of `checkOpenGLVersion` is to re-throw an exception or notify the user about a wrong OpenGL version in another exception, this commit unifies the two exception messages from `checkOpenGLVersion`. Further, it corrects ">" to ">=" in the error message (originally my fault). And it corrects verNumber to be an integer and not a boolean (there was a " < 2" too much at the end of the line). Finally, since the opportunity was there, the method is further refactored, comments and a docstring are added. Co-authored-by: 2xB <2xb@users.noreply.github.com> --- pyqtgraph/opengl/GLViewWidget.py | 38 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index f123b151..82d9c3b1 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -425,15 +425,35 @@ class GLViewWidget(QtOpenGL.QGLWidget): self.keyTimer.stop() def checkOpenGLVersion(self, msg): - ## Only to be called from within exception handler. - ver = glGetString(GL_VERSION).split()[0] - if int(ver.split(b'.')[0]) < 2: - from .. import debug - debug.printExc() - raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) - else: - raise - + """ + Give exception additional context about version support. + + Only to be called from within exception handler. + As this check is only performed on error, + unsupported versions might still work! + """ + + # Check for unsupported version + verString = glGetString(GL_VERSION) + ver = verString.split()[0] + # If not OpenGL ES... + if str(ver.split(b'.')[0]).isdigit(): + verNumber = int(ver.split(b'.')[0]) + # ...and version is supported: + if verNumber >= 2: + # OpenGL version is fine, raise the original exception + raise + + # Print original exception + from .. import debug + debug.printExc() + + # Notify about unsupported version + raise Exception( + msg + "\n" + \ + "pyqtgraph.opengl: Requires >= OpenGL 2.0 (not ES); Found %s" % verString + ) + def readQImage(self): """ Read the current buffer pixels out as a QImage.