")
self.output.insertPlainText(strn)
#self.stdout.write(strn)
@@ -275,6 +277,7 @@ class ConsoleWidget(QtGui.QWidget):
def clearExceptionClicked(self):
self.currentTraceback = None
+ self.frames = []
self.ui.exceptionInfoLabel.setText("[No current exception]")
self.ui.exceptionStackList.clear()
self.ui.clearExceptionBtn.setEnabled(False)
@@ -293,14 +296,6 @@ class ConsoleWidget(QtGui.QWidget):
fileName = tb.tb_frame.f_code.co_filename
subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True)
-
- #def allExceptionsHandler(self, *args):
- #self.exceptionHandler(*args)
-
- #def nextExceptionHandler(self, *args):
- #self.ui.catchNextExceptionBtn.setChecked(False)
- #self.exceptionHandler(*args)
-
def updateSysTrace(self):
## Install or uninstall sys.settrace handler
@@ -319,24 +314,81 @@ class ConsoleWidget(QtGui.QWidget):
else:
sys.settrace(self.systrace)
- def exceptionHandler(self, excType, exc, tb):
+ def exceptionHandler(self, excType, exc, tb, systrace=False):
if self.ui.catchNextExceptionBtn.isChecked():
self.ui.catchNextExceptionBtn.setChecked(False)
elif not self.ui.catchAllExceptionsBtn.isChecked():
return
- self.ui.clearExceptionBtn.setEnabled(True)
self.currentTraceback = tb
excMessage = ''.join(traceback.format_exception_only(excType, exc))
self.ui.exceptionInfoLabel.setText(excMessage)
- self.ui.exceptionStackList.clear()
- for index, line in enumerate(traceback.extract_tb(tb)):
- self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
+
+ if systrace:
+ # exceptions caught using systrace don't need the usual
+ # call stack + traceback handling
+ self.setStack(sys._getframe().f_back.f_back)
+ else:
+ self.setStack(frame=sys._getframe().f_back, tb=tb)
+ def setStack(self, frame=None, tb=None):
+ """Display a call stack and exception traceback.
+
+ This allows the user to probe the contents of any frame in the given stack.
+
+ *frame* may either be a Frame instance or None, in which case the current
+ frame is retrieved from ``sys._getframe()``.
+
+ If *tb* is provided then the frames in the traceback will be appended to
+ the end of the stack list. If *tb* is None, then sys.exc_info() will
+ be checked instead.
+ """
+ self.ui.clearExceptionBtn.setEnabled(True)
+
+ if frame is None:
+ frame = sys._getframe().f_back
+
+ if tb is None:
+ tb = sys.exc_info()[2]
+
+ self.ui.exceptionStackList.clear()
+ self.frames = []
+
+ # Build stack up to this point
+ for index, line in enumerate(traceback.extract_stack(frame)):
+ # extract_stack return value changed in python 3.5
+ if 'FrameSummary' in str(type(line)):
+ line = (line.filename, line.lineno, line.name, line._line)
+
+ self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
+ while frame is not None:
+ self.frames.insert(0, frame)
+ frame = frame.f_back
+
+ if tb is None:
+ return
+
+ self.ui.exceptionStackList.addItem('-- exception caught here: --')
+ item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1)
+ item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200)))
+ item.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
+ self.frames.append(None)
+
+ # And finish the rest of the stack up to the exception
+ for index, line in enumerate(traceback.extract_tb(tb)):
+ # extract_stack return value changed in python 3.5
+ if 'FrameSummary' in str(type(line)):
+ line = (line.filename, line.lineno, line.name, line._line)
+
+ self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
+ while tb is not None:
+ self.frames.append(tb.tb_frame)
+ tb = tb.tb_next
+
def systrace(self, frame, event, arg):
if event == 'exception' and self.checkException(*arg):
- self.exceptionHandler(*arg)
+ self.exceptionHandler(*arg, systrace=True)
return self.systrace
def checkException(self, excType, exc, tb):
diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui
index 1a672c5e..1237b5f3 100644
--- a/pyqtgraph/console/template.ui
+++ b/pyqtgraph/console/template.ui
@@ -6,7 +6,7 @@
0
0
- 694
+ 739
497
@@ -86,7 +86,10 @@
0
-
+
+ 2
+
+
0
-
@@ -95,7 +98,7 @@
false
- Clear Exception
+ Clear Stack
@@ -149,7 +152,10 @@
-
- Exception Info
+ Stack Trace
+
+
+ true
diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py
index 354fb1d6..9b39d14a 100644
--- a/pyqtgraph/console/template_pyqt.py
+++ b/pyqtgraph/console/template_pyqt.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file 'template.ui'
+# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui'
#
-# Created: Fri May 02 18:55:28 2014
-# by: PyQt4 UI code generator 4.10.4
+# Created by: PyQt4 UI code generator 4.11.4
#
# WARNING! All changes made in this file will be lost!
@@ -26,7 +25,7 @@ except AttributeError:
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName(_fromUtf8("Form"))
- Form.resize(694, 497)
+ Form.resize(739, 497)
self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setMargin(0)
self.gridLayout.setSpacing(0)
@@ -37,7 +36,6 @@ class Ui_Form(object):
self.layoutWidget = QtGui.QWidget(self.splitter)
self.layoutWidget.setObjectName(_fromUtf8("layoutWidget"))
self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget)
- self.verticalLayout.setMargin(0)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.output = QtGui.QPlainTextEdit(self.layoutWidget)
font = QtGui.QFont()
@@ -68,8 +66,9 @@ class Ui_Form(object):
self.exceptionGroup = QtGui.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup"))
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
- self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
+ self.gridLayout_2.setHorizontalSpacing(2)
+ self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
@@ -96,6 +95,7 @@ class Ui_Form(object):
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck"))
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
+ self.exceptionInfoLabel.setWordWrap(True)
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
@@ -116,12 +116,12 @@ class Ui_Form(object):
self.historyBtn.setText(_translate("Form", "History..", None))
self.exceptionBtn.setText(_translate("Form", "Exceptions..", None))
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None))
- self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
+ self.clearExceptionBtn.setText(_translate("Form", "Clear Stack", None))
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None))
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None))
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None))
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None))
- self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None))
+ self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace", None))
self.label.setText(_translate("Form", "Filter (regex):", None))
from .CmdInput import CmdInput
diff --git a/pyqtgraph/console/template_pyqt5.py b/pyqtgraph/console/template_pyqt5.py
index 1fbc5bed..c8c2cbac 100644
--- a/pyqtgraph/console/template_pyqt5.py
+++ b/pyqtgraph/console/template_pyqt5.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
+# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui'
#
-# Created: Wed Mar 26 15:09:29 2014
-# by: PyQt5 UI code generator 5.0.1
+# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
- Form.resize(710, 497)
+ Form.resize(739, 497)
self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0)
@@ -23,7 +22,6 @@ class Ui_Form(object):
self.layoutWidget = QtWidgets.QWidget(self.splitter)
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
- self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout")
self.output = QtWidgets.QPlainTextEdit(self.layoutWidget)
font = QtGui.QFont()
@@ -54,9 +52,14 @@ class Ui_Form(object):
self.exceptionGroup = QtWidgets.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName("exceptionGroup")
self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup)
- self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
+ self.gridLayout_2.setHorizontalSpacing(2)
+ self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName("gridLayout_2")
+ self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
+ self.clearExceptionBtn.setEnabled(False)
+ self.clearExceptionBtn.setObjectName("clearExceptionBtn")
+ self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup)
self.catchAllExceptionsBtn.setCheckable(True)
self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
@@ -68,24 +71,27 @@ class Ui_Form(object):
self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup)
self.onlyUncaughtCheck.setChecked(True)
self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck")
- self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup)
self.exceptionStackList.setAlternatingRowColors(True)
self.exceptionStackList.setObjectName("exceptionStackList")
- self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
+ self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup)
self.runSelectedFrameCheck.setChecked(True)
self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck")
- self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
+ self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup)
+ self.exceptionInfoLabel.setWordWrap(True)
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel")
- self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
- self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
- self.clearExceptionBtn.setEnabled(False)
- self.clearExceptionBtn.setObjectName("clearExceptionBtn")
- self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
+ self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
- self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
+ self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
+ self.label = QtWidgets.QLabel(self.exceptionGroup)
+ self.label.setObjectName("label")
+ self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
+ self.filterText = QtWidgets.QLineEdit(self.exceptionGroup)
+ self.filterText.setObjectName("filterText")
+ self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form)
@@ -97,11 +103,12 @@ class Ui_Form(object):
self.historyBtn.setText(_translate("Form", "History.."))
self.exceptionBtn.setText(_translate("Form", "Exceptions.."))
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling"))
+ self.clearExceptionBtn.setText(_translate("Form", "Clear Stack"))
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions"))
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception"))
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions"))
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame"))
- self.exceptionInfoLabel.setText(_translate("Form", "Exception Info"))
- self.clearExceptionBtn.setText(_translate("Form", "Clear Exception"))
+ self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace"))
+ self.label.setText(_translate("Form", "Filter (regex):"))
from .CmdInput import CmdInput
diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py
index 2db8ed95..1579cb1f 100644
--- a/pyqtgraph/console/template_pyside.py
+++ b/pyqtgraph/console/template_pyside.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
+# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui'
#
-# Created: Mon Dec 23 10:10:53 2013
-# by: pyside-uic 0.2.14 running on PySide 1.1.2
+# Created: Tue Sep 19 09:45:18 2017
+# by: pyside-uic 0.2.15 running on PySide 1.2.2
#
# WARNING! All changes made in this file will be lost!
@@ -12,7 +12,7 @@ from PySide import QtCore, QtGui
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
- Form.resize(710, 497)
+ Form.resize(739, 497)
self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0)
@@ -54,9 +54,14 @@ class Ui_Form(object):
self.exceptionGroup = QtGui.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName("exceptionGroup")
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
- self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
+ self.gridLayout_2.setHorizontalSpacing(2)
+ self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName("gridLayout_2")
+ self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
+ self.clearExceptionBtn.setEnabled(False)
+ self.clearExceptionBtn.setObjectName("clearExceptionBtn")
+ self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup)
self.catchAllExceptionsBtn.setCheckable(True)
self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
@@ -68,24 +73,27 @@ class Ui_Form(object):
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
self.onlyUncaughtCheck.setChecked(True)
self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck")
- self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
+ self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup)
self.exceptionStackList.setAlternatingRowColors(True)
self.exceptionStackList.setObjectName("exceptionStackList")
- self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
+ self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup)
self.runSelectedFrameCheck.setChecked(True)
self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck")
- self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
+ self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
+ self.exceptionInfoLabel.setWordWrap(True)
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel")
- self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
- self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
- self.clearExceptionBtn.setEnabled(False)
- self.clearExceptionBtn.setObjectName("clearExceptionBtn")
- self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
+ self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
- self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
+ self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
+ self.label = QtGui.QLabel(self.exceptionGroup)
+ self.label.setObjectName("label")
+ self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
+ self.filterText = QtGui.QLineEdit(self.exceptionGroup)
+ self.filterText.setObjectName("filterText")
+ self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form)
@@ -96,11 +104,12 @@ class Ui_Form(object):
self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8))
+ self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8))
self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8))
self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8))
self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8))
self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8))
- self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8))
- self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8))
+ self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8))
+ self.label.setText(QtGui.QApplication.translate("Form", "Filter (regex):", None, QtGui.QApplication.UnicodeUTF8))
from .CmdInput import CmdInput
diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py
index 0da24d7c..61ae9fd5 100644
--- a/pyqtgraph/debug.py
+++ b/pyqtgraph/debug.py
@@ -1186,3 +1186,23 @@ class ThreadColor(object):
c = (len(self.colors) % 15) + 1
self.colors[tid] = c
return self.colors[tid]
+
+
+def enableFaulthandler():
+ """ Enable faulthandler for all threads.
+
+ If the faulthandler package is available, this function disables and then
+ re-enables fault handling for all threads (this is necessary to ensure any
+ new threads are handled correctly), and returns True.
+
+ If faulthandler is not available, then returns False.
+ """
+ try:
+ import faulthandler
+ # necessary to disable first or else new threads may not be handled.
+ faulthandler.disable()
+ faulthandler.enable(all_threads=True)
+ return True
+ except ImportError:
+ return False
+
diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py
index c3225edf..bc0b3648 100644
--- a/pyqtgraph/dockarea/Container.py
+++ b/pyqtgraph/dockarea/Container.py
@@ -17,16 +17,20 @@ class Container(object):
def containerChanged(self, c):
self._container = c
+ if c is None:
+ self.area = None
+ else:
+ self.area = c.area
def type(self):
return None
def insert(self, new, pos=None, neighbor=None):
- # remove from existing parent first
- new.setParent(None)
-
if not isinstance(new, list):
new = [new]
+ for n in new:
+ # remove from existing parent first
+ n.setParent(None)
if neighbor is None:
if pos == 'before':
index = 0
@@ -40,34 +44,37 @@ class Container(object):
index += 1
for n in new:
- #print "change container", n, " -> ", self
- n.containerChanged(self)
#print "insert", n, " -> ", self, index
self._insertItem(n, index)
+ #print "change container", n, " -> ", self
+ n.containerChanged(self)
index += 1
n.sigStretchChanged.connect(self.childStretchChanged)
#print "child added", self
self.updateStretch()
def apoptose(self, propagate=True):
- ##if there is only one (or zero) item in this container, disappear.
+ # if there is only one (or zero) item in this container, disappear.
+ # if propagate is True, then also attempt to apoptose parent containers.
cont = self._container
c = self.count()
if c > 1:
return
- if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top)
- if self is self.area.topContainer:
+ if c == 1: ## if there is one item, give it to the parent container (unless this is the top)
+ ch = self.widget(0)
+ if (self.area is not None and self is self.area.topContainer and not isinstance(ch, Container)) or self.container() is None:
return
- self.container().insert(self.widget(0), 'before', self)
+ self.container().insert(ch, 'before', self)
#print "apoptose:", self
self.close()
if propagate and cont is not None:
cont.apoptose()
-
+
def close(self):
- self.area = None
- self._container = None
self.setParent(None)
+ if self.area is not None and self.area.topContainer is self:
+ self.area.topContainer = None
+ self.containerChanged(None)
def childEvent(self, ev):
ch = ev.child()
@@ -92,7 +99,6 @@ class Container(object):
###Set the stretch values for this container to reflect its contents
pass
-
def stretch(self):
"""Return the stretch factors for this container"""
return self._stretch
diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py
index 4493d075..1d946062 100644
--- a/pyqtgraph/dockarea/Dock.py
+++ b/pyqtgraph/dockarea/Dock.py
@@ -36,6 +36,7 @@ class Dock(QtGui.QWidget, DockDrop):
self.widgetArea.setLayout(self.layout)
self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.widgets = []
+ self._container = None
self.currentRow = 0
#self.titlePos = 'top'
self.raiseOverlay()
@@ -187,9 +188,6 @@ class Dock(QtGui.QWidget, DockDrop):
def name(self):
return self._name
- def container(self):
- return self._container
-
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
"""
Add a new widget to the interior of this Dock.
@@ -202,7 +200,6 @@ class Dock(QtGui.QWidget, DockDrop):
self.layout.addWidget(widget, row, col, rowspan, colspan)
self.raiseOverlay()
-
def startDrag(self):
self.drag = QtGui.QDrag(self)
mime = QtCore.QMimeData()
@@ -216,21 +213,30 @@ class Dock(QtGui.QWidget, DockDrop):
def float(self):
self.area.floatDock(self)
+ def container(self):
+ return self._container
+
def containerChanged(self, c):
+ 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.type() != 'tab':
- self.moveLabel = True
- self.label.setDim(False)
+ if c is None:
+ self.area = None
else:
- self.moveLabel = False
-
- self.setOrientation(force=True)
-
+ self.area = c.area
+ if c.type() != 'tab':
+ self.moveLabel = True
+ self.label.setDim(False)
+ else:
+ self.moveLabel = False
+
+ self.setOrientation(force=True)
+
def raiseDock(self):
"""If this Dock is stacked underneath others, raise it to the top."""
self.container().raiseDock(self)
-
def close(self):
"""Remove this dock from the DockArea it lives inside."""
diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py
index ffe75b61..a55d6bb0 100644
--- a/pyqtgraph/dockarea/DockArea.py
+++ b/pyqtgraph/dockarea/DockArea.py
@@ -61,6 +61,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
if isinstance(relativeTo, basestring):
relativeTo = self.docks[relativeTo]
container = self.getContainer(relativeTo)
+ if container is None:
+ raise TypeError("Dock %s is not contained in a DockArea; cannot add another dock relative to it." % relativeTo)
neighbor = relativeTo
## what container type do we need?
@@ -98,7 +100,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
#print "request insert", dock, insertPos, neighbor
old = dock.container()
container.insert(dock, insertPos, neighbor)
- dock.area = self
self.docks[dock.name()] = dock
if old is not None:
old.apoptose()
@@ -142,23 +143,19 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def insert(self, new, pos=None, neighbor=None):
if self.topContainer is not None:
+ # Adding new top-level container; addContainer() should
+ # take care of giving the old top container a new home.
self.topContainer.containerChanged(None)
self.layout.addWidget(new)
+ new.containerChanged(self)
self.topContainer = new
- #print self, "set top:", new
- new._container = self
self.raiseOverlay()
- #print "Insert top:", new
def count(self):
if self.topContainer is None:
return 0
return 1
-
- #def paintEvent(self, ev):
- #self.drawDockOverlay()
-
def resizeEvent(self, ev):
self.resizeOverlay(self.size())
@@ -180,7 +177,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
area.win.resize(dock.size())
area.moveDock(dock, 'top', None)
-
def removeTempArea(self, area):
self.tempAreas.remove(area)
#print "close window", area.window()
@@ -212,14 +208,20 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
childs.append(self.childState(obj.widget(i)))
return (obj.type(), childs, obj.saveState())
-
- def restoreState(self, state):
+ def restoreState(self, state, missing='error', extra='bottom'):
"""
Restore Dock configuration as generated by saveState.
- Note that this function does not create any Docks--it will only
+ This function does not create any Docks--it will only
restore the arrangement of an existing set of Docks.
+ By default, docks that are described in *state* but do not exist
+ in the dock area will cause an exception to be raised. This behavior
+ can be changed by setting *missing* to 'ignore' or 'create'.
+
+ Extra docks that are in the dockarea but that are not mentioned in
+ *state* will be added to the bottom of the dockarea, unless otherwise
+ specified by the *extra* argument.
"""
## 1) make dict of all docks and list of existing containers
@@ -229,17 +231,22 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
## 2) create container structure, move docks into new containers
if state['main'] is not None:
- self.buildFromState(state['main'], docks, self)
+ self.buildFromState(state['main'], docks, self, missing=missing)
## 3) create floating areas, populate
for s in state['float']:
a = self.addTempArea()
- a.buildFromState(s[0]['main'], docks, a)
+ a.buildFromState(s[0]['main'], docks, a, missing=missing)
a.win.setGeometry(*s[1])
+ a.apoptose() # ask temp area to close itself if it is empty
- ## 4) Add any remaining docks to the bottom
+ ## 4) Add any remaining docks to a float
for d in docks.values():
- self.moveDock(d, 'below', None)
+ if extra == 'float':
+ a = self.addTempArea()
+ a.addDock(d, 'below')
+ else:
+ self.moveDock(d, extra, None)
#print "\nKill old containers:"
## 5) kill old containers
@@ -248,8 +255,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
for a in oldTemps:
a.apoptose()
-
- def buildFromState(self, state, docks, root, depth=0):
+ def buildFromState(self, state, docks, root, depth=0, missing='error'):
typ, contents, state = state
pfx = " " * depth
if typ == 'dock':
@@ -257,7 +263,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
obj = docks[contents]
del docks[contents]
except KeyError:
- raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
+ if missing == 'error':
+ raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
+ elif missing == 'create':
+ obj = Dock(name=contents)
+ elif missing == 'ignore':
+ return
+ else:
+ raise ValueError('"missing" argument must be one of "error", "create", or "ignore".')
+
else:
obj = self.makeContainer(typ)
@@ -266,10 +280,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
if typ != 'dock':
for o in contents:
- self.buildFromState(o, docks, obj, depth+1)
+ self.buildFromState(o, docks, obj, depth+1, missing=missing)
+ # remove this container if possible. (there are valid situations when a restore will
+ # generate empty containers, such as when using missing='ignore')
obj.apoptose(propagate=False)
- obj.restoreState(state) ## this has to be done later?
-
+ obj.restoreState(state) ## this has to be done later?
def findAll(self, obj=None, c=None, d=None):
if obj is None:
@@ -295,14 +310,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
d.update(d2)
return (c, d)
- def apoptose(self):
+ def apoptose(self, propagate=True):
+ # remove top container if possible, close this area if it is temporary.
#print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count()
- if self.topContainer.count() == 0:
+ if self.topContainer is None or self.topContainer.count() == 0:
self.topContainer = None
if self.temporary:
self.home.removeTempArea(self)
#self.close()
-
+
def clear(self):
docks = self.findAll()[1]
for dock in docks.values():
@@ -322,12 +338,38 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def dropEvent(self, *args):
DockDrop.dropEvent(self, *args)
+ def printState(self, state=None, name='Main'):
+ # for debugging
+ if state is None:
+ state = self.saveState()
+ print("=== %s dock area ===" % name)
+ if state['main'] is None:
+ print(" (empty)")
+ else:
+ self._printAreaState(state['main'])
+ for i, float in enumerate(state['float']):
+ self.printState(float[0], name='float %d' % i)
-class TempAreaWindow(QtGui.QMainWindow):
+ def _printAreaState(self, area, indent=0):
+ if area[0] == 'dock':
+ print(" " * indent + area[0] + " " + str(area[1:]))
+ return
+ else:
+ print(" " * indent + area[0])
+ for ch in area[1]:
+ self._printAreaState(ch, indent+1)
+
+
+
+class TempAreaWindow(QtGui.QWidget):
def __init__(self, area, **kwargs):
- QtGui.QMainWindow.__init__(self, **kwargs)
- self.setCentralWidget(area)
+ QtGui.QWidget.__init__(self, **kwargs)
+ self.layout = QtGui.QGridLayout()
+ self.setLayout(self.layout)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.dockarea = area
+ self.layout.addWidget(area)
- def closeEvent(self, *args, **kwargs):
- self.centralWidget().clear()
- QtGui.QMainWindow.closeEvent(self, *args, **kwargs)
+ def closeEvent(self, *args):
+ self.dockarea.clear()
+ QtGui.QWidget.closeEvent(self, *args)
diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py
new file mode 100644
index 00000000..9575c298
--- /dev/null
+++ b/pyqtgraph/dockarea/tests/test_dockarea.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+import pyqtgraph as pg
+from pyqtgraph.ordereddict import OrderedDict
+pg.mkQApp()
+
+import pyqtgraph.dockarea as da
+
+def test_dockarea():
+ a = da.DockArea()
+ d1 = da.Dock("dock 1")
+ a.addDock(d1, 'left')
+
+ assert a.topContainer is d1.container()
+ assert d1.container().container() is a
+ assert d1.area is a
+ assert a.topContainer.widget(0) is d1
+
+ d2 = da.Dock("dock 2")
+ a.addDock(d2, 'right')
+
+ assert a.topContainer is d1.container()
+ assert a.topContainer is d2.container()
+ assert d1.container().container() is a
+ assert d2.container().container() is a
+ assert d2.area is a
+ assert a.topContainer.widget(0) is d1
+ assert a.topContainer.widget(1) is d2
+
+ d3 = da.Dock("dock 3")
+ a.addDock(d3, 'bottom')
+
+ assert a.topContainer is d3.container()
+ assert d2.container().container() is d3.container()
+ assert d1.container().container() is d3.container()
+ assert d1.container().container().container() is a
+ assert d2.container().container().container() is a
+ assert d3.container().container() is a
+ assert d3.area is a
+ assert d2.area is a
+ assert a.topContainer.widget(0) is d1.container()
+ assert a.topContainer.widget(1) is d3
+
+ d4 = da.Dock("dock 4")
+ a.addDock(d4, 'below', d3)
+
+ assert d4.container().type() == 'tab'
+ assert d4.container() is d3.container()
+ assert d3.container().container() is d2.container().container()
+ assert d4.area is a
+ a.printState()
+
+ # layout now looks like:
+ # vcontainer
+ # hcontainer
+ # dock 1
+ # dock 2
+ # tcontainer
+ # dock 3
+ # dock 4
+
+ # test save/restore state
+ state = a.saveState()
+ a2 = da.DockArea()
+ # default behavior is to raise exception if docks are missing
+ with pytest.raises(Exception):
+ a2.restoreState(state)
+
+ # test restore with ignore missing
+ a2.restoreState(state, missing='ignore')
+ assert a2.topContainer is None
+
+ # test restore with auto-create
+ a2.restoreState(state, missing='create')
+ assert a2.saveState() == state
+ a2.printState()
+
+ # double-check that state actually matches the output of saveState()
+ c1 = a2.topContainer
+ assert c1.type() == 'vertical'
+ c2 = c1.widget(0)
+ c3 = c1.widget(1)
+ assert c2.type() == 'horizontal'
+ assert c2.widget(0).name() == 'dock 1'
+ assert c2.widget(1).name() == 'dock 2'
+ assert c3.type() == 'tab'
+ assert c3.widget(0).name() == 'dock 3'
+ assert c3.widget(1).name() == 'dock 4'
+
+ # test restore with docks already present
+ a3 = da.DockArea()
+ a3docks = []
+ for i in range(1, 5):
+ dock = da.Dock('dock %d' % i)
+ a3docks.append(dock)
+ a3.addDock(dock, 'right')
+ a3.restoreState(state)
+ assert a3.saveState() == state
+
+ # test restore with extra docks present
+ a3 = da.DockArea()
+ a3docks = []
+ for i in [1, 2, 5, 4, 3]:
+ dock = da.Dock('dock %d' % i)
+ a3docks.append(dock)
+ a3.addDock(dock, 'left')
+ a3.restoreState(state)
+ a3.printState()
+
+
+ # test a more complex restore
+ a4 = da.DockArea()
+ state1 = {'float': [], 'main':
+ ('horizontal', [
+ ('vertical', [
+ ('horizontal', [
+ ('tab', [
+ ('dock', 'dock1', {}),
+ ('dock', 'dock2', {}),
+ ('dock', 'dock3', {}),
+ ('dock', 'dock4', {})
+ ], {'index': 1}),
+ ('vertical', [
+ ('dock', 'dock5', {}),
+ ('horizontal', [
+ ('dock', 'dock6', {}),
+ ('dock', 'dock7', {})
+ ], {'sizes': [184, 363]})
+ ], {'sizes': [355, 120]})
+ ], {'sizes': [9, 552]})
+ ], {'sizes': [480]}),
+ ('dock', 'dock8', {})
+ ], {'sizes': [566, 69]})
+ }
+
+ state2 = {'float': [], 'main':
+ ('horizontal', [
+ ('vertical', [
+ ('horizontal', [
+ ('dock', 'dock2', {}),
+ ('vertical', [
+ ('dock', 'dock5', {}),
+ ('horizontal', [
+ ('dock', 'dock6', {}),
+ ('dock', 'dock7', {})
+ ], {'sizes': [492, 485]})
+ ], {'sizes': [936, 0]})
+ ], {'sizes': [172, 982]})
+ ], {'sizes': [941]}),
+ ('vertical', [
+ ('dock', 'dock8', {}),
+ ('dock', 'dock4', {}),
+ ('dock', 'dock1', {})
+ ], {'sizes': [681, 225, 25]})
+ ], {'sizes': [1159, 116]})}
+
+ a4.restoreState(state1, missing='create')
+ # dock3 not mentioned in restored state; stays in dockarea by default
+ c, d = a4.findAll()
+ assert d['dock3'].area is a4
+
+ a4.restoreState(state2, missing='ignore', extra='float')
+ a4.printState()
+
+ c, d = a4.findAll()
+ # dock3 not mentioned in restored state; goes to float due to `extra` argument
+ assert d['dock3'].area is not a4
+ assert d['dock1'].container() is d['dock4'].container() is d['dock8'].container()
+ assert d['dock6'].container() is d['dock7'].container()
+ assert a4 is d['dock2'].area is d['dock2'].container().container().container()
+ assert a4 is d['dock5'].area is d['dock5'].container().container().container().container()
+
+ # States should be the same with two exceptions:
+ # dock3 is in a float because it does not appear in state2
+ # a superfluous vertical splitter in state2 has been removed
+ state4 = a4.saveState()
+ state4['main'][1][0] = state4['main'][1][0][1][0]
+ assert clean_state(state4['main']) == clean_state(state2['main'])
+
+
+def clean_state(state):
+ # return state dict with sizes removed
+ ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
+ state = (state[0], ch, {})
+
+
+if __name__ == '__main__':
+ test_dockarea()
diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py
index cc8b5733..584a9f71 100644
--- a/pyqtgraph/exporters/HDF5Exporter.py
+++ b/pyqtgraph/exporters/HDF5Exporter.py
@@ -42,14 +42,20 @@ class HDF5Exporter(Exporter):
dsname = self.params['Name']
fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite"
data = []
-
+
appendAllX = self.params['columnMode'] == '(x,y) per plot'
- for i,c in enumerate(self.item.curves):
+ #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])
-
+
+
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fd.close()
diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py
index 78d93106..ffa59091 100644
--- a/pyqtgraph/exporters/ImageExporter.py
+++ b/pyqtgraph/exporters/ImageExporter.py
@@ -23,10 +23,11 @@ class ImageExporter(Exporter):
bg.setAlpha(0)
self.params = Parameter(name='params', type='group', children=[
- {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
- {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
+ {'name': 'width', 'type': 'int', 'value': int(tr.width()), 'limits': (0, None)},
+ {'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)},
{'name': 'antialias', 'type': 'bool', 'value': True},
{'name': 'background', 'type': 'color', 'value': bg},
+ {'name': 'invertValue', 'type': 'bool', 'value': False}
])
self.params.param('width').sigValueChanged.connect(self.widthChanged)
self.params.param('height').sigValueChanged.connect(self.heightChanged)
@@ -34,12 +35,12 @@ class ImageExporter(Exporter):
def widthChanged(self):
sr = self.getSourceRect()
ar = float(sr.height()) / sr.width()
- self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged)
+ self.params.param('height').setValue(int(self.params['width'] * ar), blockSignal=self.heightChanged)
def heightChanged(self):
sr = self.getSourceRect()
ar = float(sr.width()) / sr.height()
- self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged)
+ self.params.param('width').setValue(int(self.params['height'] * ar), blockSignal=self.widthChanged)
def parameters(self):
return self.params
@@ -67,13 +68,15 @@ class ImageExporter(Exporter):
w, h = self.params['width'], 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['width'], self.params['height'], 4), dtype=np.ubyte)
+ bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte)
color = self.params['background']
bg[:,:,0] = color.blue()
bg[:,:,1] = color.green()
bg[:,:,2] = color.red()
bg[:,:,3] = color.alpha()
- self.png = fn.makeQImage(bg, alpha=True)
+
+ self.png = fn.makeQImage(bg, alpha=True, copy=False, transpose=False)
+ self.bg = bg
## set resolution of image:
origTargetRect = self.getTargetRect()
@@ -91,6 +94,12 @@ class ImageExporter(Exporter):
self.setExportMode(False)
painter.end()
+ if self.params['invertValue']:
+ mn = bg[...,:3].min(axis=2)
+ mx = bg[...,:3].max(axis=2)
+ d = (255 - mx) - mn
+ bg[...,:3] += d[...,np.newaxis]
+
if copy:
QtGui.QApplication.clipboard().setImage(self.png)
elif toBytes:
diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py
index ccf92165..3e060e62 100644
--- a/pyqtgraph/exporters/SVGExporter.py
+++ b/pyqtgraph/exporters/SVGExporter.py
@@ -169,7 +169,7 @@ def _generateItemSvg(item, nodes=None, root=None):
buf = QtCore.QBuffer(arr)
svg = QtSvg.QSvgGenerator()
svg.setOutputDevice(buf)
- dpi = QtGui.QDesktopWidget().physicalDpiX()
+ dpi = QtGui.QDesktopWidget().logicalDpiX()
svg.setResolution(dpi)
p = QtGui.QPainter()
@@ -190,7 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None):
xmlStr = str(arr)
else:
xmlStr = bytes(arr).decode('utf-8')
- doc = xml.parseString(xmlStr)
+ doc = xml.parseString(xmlStr.encode('utf-8'))
try:
## Get top-level group for this item
@@ -372,7 +372,7 @@ def correctCoordinates(node, defs, item):
ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families]))
## correct line widths if needed
- if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke':
+ if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width') != '':
w = float(grp.getAttribute('stroke-width'))
s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True)
w = ((s[0]-s[1])**2).sum()**0.5
diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py
index b623f5c7..cbfd084e 100644
--- a/pyqtgraph/flowchart/Flowchart.py
+++ b/pyqtgraph/flowchart/Flowchart.py
@@ -166,6 +166,8 @@ class Flowchart(Node):
n[oldName].rename(newName)
def createNode(self, nodeType, name=None, pos=None):
+ """Create a new Node and add it to this flowchart.
+ """
if name is None:
n = 0
while True:
@@ -179,6 +181,10 @@ class Flowchart(Node):
return node
def addNode(self, node, name, pos=None):
+ """Add an existing Node to this flowchart.
+
+ See also: createNode()
+ """
if pos is None:
pos = [0, 0]
if type(pos) in [QtCore.QPoint, QtCore.QPointF]:
@@ -189,13 +195,16 @@ class Flowchart(Node):
self.viewBox.addItem(item)
item.moveBy(*pos)
self._nodes[name] = node
- self.widget().addNode(node)
+ if node is not self.inputNode and node is not self.outputNode:
+ self.widget().addNode(node)
node.sigClosed.connect(self.nodeClosed)
node.sigRenamed.connect(self.nodeRenamed)
node.sigOutputChanged.connect(self.nodeOutputChanged)
self.sigChartChanged.emit(self, 'add', node)
def removeNode(self, node):
+ """Remove a Node from this flowchart.
+ """
node.close()
def nodeClosed(self, node):
@@ -233,7 +242,6 @@ class Flowchart(Node):
term2 = self.internalTerminal(term2)
term1.connectTo(term2)
-
def process(self, **args):
"""
Process data through the flowchart, returning the output.
@@ -325,7 +333,6 @@ class Flowchart(Node):
#print "DEPS:", deps
## determine correct node-processing order
- #deps[self] = []
order = fn.toposort(deps)
#print "ORDER1:", order
@@ -349,7 +356,6 @@ class Flowchart(Node):
if lastNode is None or ind > lastInd:
lastNode = n
lastInd = ind
- #tdeps[t] = lastNode
if lastInd is not None:
dels.append((lastInd+1, t))
dels.sort(key=lambda a: a[0], reverse=True)
@@ -404,27 +410,25 @@ class Flowchart(Node):
self.inputWasSet = False
else:
self.sigStateChanged.emit()
-
-
def chartGraphicsItem(self):
- """Return the graphicsItem which displays the internals of this flowchart.
- (graphicsItem() still returns the external-view item)"""
- #return self._chartGraphicsItem
+ """Return the graphicsItem that displays the internal nodes and
+ connections of this flowchart.
+
+ Note that the similar method `graphicsItem()` is inherited from Node
+ and returns the *external* graphical representation of this flowchart."""
return self.viewBox
def widget(self):
+ """Return the control widget for this flowchart.
+
+ This widget provides GUI access to the parameters for each node and a
+ graphical representation of the flowchart.
+ """
if self._widget is None:
self._widget = FlowchartCtrlWidget(self)
self.scene = self._widget.scene()
self.viewBox = self._widget.viewBox()
- #self._scene = QtGui.QGraphicsScene()
- #self._widget.setScene(self._scene)
- #self.scene.addItem(self.chartGraphicsItem())
-
- #ci = self.chartGraphicsItem()
- #self.viewBox.addItem(ci)
- #self.viewBox.autoRange()
return self._widget
def listConnections(self):
@@ -437,10 +441,11 @@ class Flowchart(Node):
return conn
def saveState(self):
+ """Return a serializable data structure representing the current state of this flowchart.
+ """
state = Node.saveState(self)
state['nodes'] = []
state['connects'] = []
- #state['terminals'] = self.saveTerminals()
for name, node in self._nodes.items():
cls = type(node)
@@ -460,6 +465,8 @@ class Flowchart(Node):
return state
def restoreState(self, state, clear=False):
+ """Restore the state of this flowchart from a previous call to `saveState()`.
+ """
self.blockSignals(True)
try:
if clear:
@@ -469,7 +476,6 @@ class Flowchart(Node):
nodes.sort(key=lambda a: a['pos'][0])
for n in nodes:
if n['name'] in self._nodes:
- #self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
self._nodes[n['name']].restoreState(n['state'])
continue
try:
@@ -477,7 +483,6 @@ class Flowchart(Node):
node.restoreState(n['state'])
except:
printExc("Error creating node %s: (continuing anyway)" % n['name'])
- #node.graphicsItem().moveBy(*n['pos'])
self.inputNode.restoreState(state.get('inputNode', {}))
self.outputNode.restoreState(state.get('outputNode', {}))
@@ -490,7 +495,6 @@ class Flowchart(Node):
print(self._nodes[n1].terminals)
print(self._nodes[n2].terminals)
printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2))
-
finally:
self.blockSignals(False)
@@ -498,48 +502,46 @@ class Flowchart(Node):
self.sigChartLoaded.emit()
self.outputChanged()
self.sigStateChanged.emit()
- #self.sigOutputChanged.emit()
def loadFile(self, fileName=None, startDir=None):
+ """Load a flowchart (*.fc) file.
+ """
if fileName is None:
if startDir is None:
startDir = self.filePath
if startDir is None:
startDir = '.'
self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
- #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
- #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.loadFile)
return
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
- #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
fileName = unicode(fileName)
state = configfile.readConfigFile(fileName)
self.restoreState(state, clear=True)
self.viewBox.autoRange()
- #self.emit(QtCore.SIGNAL('fileLoaded'), fileName)
self.sigFileLoaded.emit(fileName)
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
+ """Save this flowchart to a .fc file
+ """
if fileName is None:
if startDir is None:
startDir = self.filePath
if startDir is None:
startDir = '.'
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
- #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
- #self.fileDialog.setDirectory(startDir)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.saveFile)
return
- #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
fileName = unicode(fileName)
configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName)
def clear(self):
+ """Remove all nodes from this flowchart except the original input/output nodes.
+ """
for n in list(self._nodes.values()):
if n is self.inputNode or n is self.outputNode:
continue
@@ -552,18 +554,15 @@ class Flowchart(Node):
self.inputNode.clearTerminals()
self.outputNode.clearTerminals()
-#class FlowchartGraphicsItem(QtGui.QGraphicsItem):
+
class FlowchartGraphicsItem(GraphicsObject):
def __init__(self, chart):
- #print "FlowchartGraphicsItem.__init__"
- #QtGui.QGraphicsItem.__init__(self)
GraphicsObject.__init__(self)
self.chart = chart ## chart is an instance of Flowchart()
self.updateTerminals()
def updateTerminals(self):
- #print "FlowchartGraphicsItem.updateTerminals"
self.terminals = {}
bounds = self.boundingRect()
inp = self.chart.inputs()
@@ -759,6 +758,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
item = self.items[node]
self.ui.ctrlList.setCurrentItem(item)
+
class FlowchartWidget(dockarea.DockArea):
"""Includes the actual graphical flowchart and debugging interface"""
def __init__(self, chart, ctrl):
diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py
index 5236de8d..18f1c948 100644
--- a/pyqtgraph/flowchart/library/Data.py
+++ b/pyqtgraph/flowchart/library/Data.py
@@ -189,31 +189,36 @@ class EvalNode(Node):
self.ui = QtGui.QWidget()
self.layout = QtGui.QGridLayout()
- #self.addInBtn = QtGui.QPushButton('+Input')
- #self.addOutBtn = QtGui.QPushButton('+Output')
self.text = QtGui.QTextEdit()
self.text.setTabStopWidth(30)
self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal")
- #self.layout.addWidget(self.addInBtn, 0, 0)
- #self.layout.addWidget(self.addOutBtn, 0, 1)
self.layout.addWidget(self.text, 1, 0, 1, 2)
self.ui.setLayout(self.layout)
- #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput)
- #self.addInBtn.clicked.connect(self.addInput)
- #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput)
- #self.addOutBtn.clicked.connect(self.addOutput)
self.text.focusOutEvent = self.focusOutEvent
self.lastText = None
def ctrlWidget(self):
return self.ui
- #def addInput(self):
- #Node.addInput(self, 'input', renamable=True)
+ def setCode(self, code):
+ # unindent code; this allows nicer inline code specification when
+ # calling this method.
+ ind = []
+ lines = code.split('\n')
+ for line in lines:
+ stripped = line.lstrip()
+ if len(stripped) > 0:
+ ind.append(len(line) - len(stripped))
+ if len(ind) > 0:
+ ind = min(ind)
+ code = '\n'.join([line[ind:] for line in lines])
- #def addOutput(self):
- #Node.addOutput(self, 'output', renamable=True)
+ self.text.clear()
+ self.text.insertPlainText(code)
+
+ def code(self):
+ return self.text.toPlainText()
def focusOutEvent(self, ev):
text = str(self.text.toPlainText())
@@ -247,10 +252,10 @@ class EvalNode(Node):
def restoreState(self, state):
Node.restoreState(self, state)
- self.text.clear()
- self.text.insertPlainText(state['text'])
+ self.setCode(state['text'])
self.restoreTerminals(state['terminals'])
self.update()
+
class ColumnJoinNode(Node):
"""Concatenates record arrays and/or adds new columns"""
@@ -354,3 +359,117 @@ class ColumnJoinNode(Node):
self.update()
+class Mean(CtrlNode):
+ """Calculate the mean of an array across an axis.
+ """
+ nodeName = 'Mean'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = None if s['axis'] == -1 else s['axis']
+ return data.mean(axis=ax)
+
+
+class Max(CtrlNode):
+ """Calculate the maximum of an array across an axis.
+ """
+ nodeName = 'Max'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = None if s['axis'] == -1 else s['axis']
+ return data.max(axis=ax)
+
+
+class Min(CtrlNode):
+ """Calculate the minimum of an array across an axis.
+ """
+ nodeName = 'Min'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = None if s['axis'] == -1 else s['axis']
+ return data.min(axis=ax)
+
+
+class Stdev(CtrlNode):
+ """Calculate the standard deviation of an array across an axis.
+ """
+ nodeName = 'Stdev'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': -0, 'min': -1, 'max': 1000000}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = None if s['axis'] == -1 else s['axis']
+ return data.std(axis=ax)
+
+
+class Index(CtrlNode):
+ """Select an index from an array axis.
+ """
+ nodeName = 'Index'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}),
+ ('index', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = s['axis']
+ ind = s['index']
+ if ax == 0:
+ # allow support for non-ndarray sequence types
+ return data[ind]
+ else:
+ return data.take(ind, axis=ax)
+
+
+class Slice(CtrlNode):
+ """Select a slice from an array axis.
+ """
+ nodeName = 'Slice'
+ uiTemplate = [
+ ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1e6}),
+ ('start', 'intSpin', {'value': 0, 'min': -1e6, 'max': 1e6}),
+ ('stop', 'intSpin', {'value': -1, 'min': -1e6, 'max': 1e6}),
+ ('step', 'intSpin', {'value': 1, 'min': -1e6, 'max': 1e6}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ ax = s['axis']
+ start = s['start']
+ stop = s['stop']
+ step = s['step']
+ if ax == 0:
+ # allow support for non-ndarray sequence types
+ return data[start:stop:step]
+ else:
+ sl = [slice(None) for i in range(data.ndim)]
+ sl[ax] = slice(start, stop, step)
+ return data[sl]
+
+
+class AsType(CtrlNode):
+ """Convert an array to a different dtype.
+ """
+ nodeName = 'AsType'
+ uiTemplate = [
+ ('dtype', 'combo', {'values': ['float', 'int', 'float32', 'float64', 'float128', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'], 'index': 0}),
+ ]
+
+ def processData(self, data):
+ s = self.stateGroup.state()
+ return data.astype(s['dtype'])
+
diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py
index 9392b037..9a7fa401 100644
--- a/pyqtgraph/flowchart/library/Filters.py
+++ b/pyqtgraph/flowchart/library/Filters.py
@@ -38,7 +38,7 @@ class Bessel(CtrlNode):
nodeName = 'BesselFilter'
uiTemplate = [
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
- ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}),
('bidir', 'check', {'checked': True})
]
@@ -57,10 +57,10 @@ class Butterworth(CtrlNode):
nodeName = 'ButterworthFilter'
uiTemplate = [
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
- ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
- ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('bidir', 'check', {'checked': True})
]
@@ -78,14 +78,14 @@ class ButterworthNotch(CtrlNode):
"""Butterworth notch filter"""
nodeName = 'ButterworthNotchFilter'
uiTemplate = [
- ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
- ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
- ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
- ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
- ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
+ ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
+ ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('bidir', 'check', {'checked': True})
]
@@ -160,19 +160,13 @@ class Gaussian(CtrlNode):
@metaArrayWrapper
def processData(self, data):
+ sigma = self.ctrls['sigma'].value()
try:
import scipy.ndimage
+ return scipy.ndimage.gaussian_filter(data, sigma)
except ImportError:
- raise Exception("GaussianFilter node requires the package scipy.ndimage.")
+ return pgfn.gaussianFilter(data, sigma)
- if hasattr(data, 'implements') and data.implements('MetaArray'):
- info = data.infoCopy()
- filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value())
- if 'values' in info[0]:
- info[0]['values'] = info[0]['values'][:filt.shape[0]]
- return metaarray.MetaArray(filt, info=info)
- else:
- return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
class Derivative(CtrlNode):
"""Returns the pointwise derivative of the input"""
diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py
index 579d2cd2..d1483c16 100644
--- a/pyqtgraph/flowchart/library/Operators.py
+++ b/pyqtgraph/flowchart/library/Operators.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from ..Node import Node
+from .common import CtrlNode
+
class UniOpNode(Node):
"""Generic node for performing any operation like Out = In.fn()"""
@@ -13,11 +15,22 @@ class UniOpNode(Node):
def process(self, **args):
return {'Out': getattr(args['In'], self.fn)()}
-class BinOpNode(Node):
+class BinOpNode(CtrlNode):
"""Generic node for performing any operation like A.fn(B)"""
+
+ _dtypes = [
+ 'float64', 'float32', 'float16',
+ 'int64', 'int32', 'int16', 'int8',
+ 'uint64', 'uint32', 'uint16', 'uint8'
+ ]
+
+ uiTemplate = [
+ ('outputType', 'combo', {'values': ['no change', 'input A', 'input B'] + _dtypes , 'index': 0})
+ ]
+
def __init__(self, name, fn):
self.fn = fn
- Node.__init__(self, name, terminals={
+ CtrlNode.__init__(self, name, terminals={
'A': {'io': 'in'},
'B': {'io': 'in'},
'Out': {'io': 'out', 'bypass': 'A'}
@@ -36,6 +49,18 @@ class BinOpNode(Node):
out = fn(args['B'])
if out is NotImplemented:
raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B']))))
+
+ # Coerce dtype if requested
+ typ = self.stateGroup.state()['outputType']
+ if typ == 'no change':
+ pass
+ elif typ == 'input A':
+ out = out.astype(args['A'].dtype)
+ elif typ == 'input B':
+ out = out.astype(args['B'].dtype)
+ else:
+ out = out.astype(typ)
+
#print " ", fn, out
return {'Out': out}
@@ -71,4 +96,10 @@ class DivideNode(BinOpNode):
# try truediv first, followed by div
BinOpNode.__init__(self, name, ('__truediv__', '__div__'))
+class FloorDivideNode(BinOpNode):
+ """Returns A // B. Does not check input types."""
+ nodeName = 'FloorDivide'
+ def __init__(self, name):
+ BinOpNode.__init__(self, name, '__floordiv__')
+
diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py
index 425fe86c..8b3376c3 100644
--- a/pyqtgraph/flowchart/library/common.py
+++ b/pyqtgraph/flowchart/library/common.py
@@ -30,6 +30,11 @@ def generateUi(opts):
k, t, o = opt
else:
raise Exception("Widget specification must be (name, type) or (name, type, {opts})")
+
+ ## clean out these options so they don't get sent to SpinBox
+ hidden = o.pop('hidden', False)
+ tip = o.pop('tip', None)
+
if t == 'intSpin':
w = QtGui.QSpinBox()
if 'max' in o:
@@ -63,11 +68,12 @@ def generateUi(opts):
w = ColorButton()
else:
raise Exception("Unknown widget type '%s'" % str(t))
- if 'tip' in o:
- w.setToolTip(o['tip'])
+
+ if tip is not None:
+ w.setToolTip(tip)
w.setObjectName(k)
l.addRow(k, w)
- if o.get('hidden', False):
+ if hidden:
w.hide()
label = l.labelForField(w)
label.hide()
diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py
index 839720d1..e83237c6 100644
--- a/pyqtgraph/functions.py
+++ b/pyqtgraph/functions.py
@@ -15,7 +15,7 @@ from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, USE_PYSIDE
from . import getConfigOption, setConfigOptions
from . import debug
-
+from .metaarray import MetaArray
Colors = {
@@ -110,7 +110,7 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal))
-def siParse(s, regex=FLOAT_REGEX):
+def siParse(s, regex=FLOAT_REGEX, suffix=None):
"""Convert a value written in SI notation to a tuple (number, si_prefix, suffix).
Example::
@@ -118,6 +118,12 @@ def siParse(s, regex=FLOAT_REGEX):
siParse('100 μV") # returns ('100', 'μ', 'V')
"""
s = asUnicode(s)
+ s = s.strip()
+ if suffix is not None and len(suffix) > 0:
+ if s[-len(suffix):] != suffix:
+ raise ValueError("String '%s' does not have the expected suffix '%s'" % (s, suffix))
+ s = s[:-len(suffix)] + 'X' # add a fake suffix so the regex still picks up the si prefix
+
m = regex.match(s)
if m is None:
raise ValueError('Cannot parse number "%s"' % s)
@@ -126,15 +132,18 @@ def siParse(s, regex=FLOAT_REGEX):
except IndexError:
sip = ''
- try:
- suf = m.group('suffix')
- except IndexError:
- suf = ''
+ if suffix is None:
+ try:
+ suf = m.group('suffix')
+ except IndexError:
+ suf = ''
+ else:
+ suf = suffix
return m.group('number'), '' if sip is None else sip, '' if suf is None else suf
-def siEval(s, typ=float, regex=FLOAT_REGEX):
+def siEval(s, typ=float, regex=FLOAT_REGEX, suffix=None):
"""
Convert a value written in SI notation to its equivalent prefixless value.
@@ -142,9 +151,9 @@ def siEval(s, typ=float, regex=FLOAT_REGEX):
siEval("100 μV") # returns 0.0001
"""
- val, siprefix, suffix = siParse(s, regex)
+ val, siprefix, suffix = siParse(s, regex, suffix=suffix)
v = typ(val)
- return siApply(val, siprefix)
+ return siApply(v, siprefix)
def siApply(val, siprefix):
@@ -200,7 +209,7 @@ def mkColor(*args):
try:
return Colors[c]
except KeyError:
- raise Exception('No color named "%s"' % c)
+ raise ValueError('No color named "%s"' % c)
if len(c) == 3:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
@@ -235,18 +244,18 @@ def mkColor(*args):
elif len(args[0]) == 2:
return intColor(*args[0])
else:
- raise Exception(err)
+ raise TypeError(err)
elif type(args[0]) == int:
return intColor(args[0])
else:
- raise Exception(err)
+ raise TypeError(err)
elif len(args) == 3:
(r, g, b) = args
a = 255
elif len(args) == 4:
(r, g, b, a) = args
else:
- raise Exception(err)
+ raise TypeError(err)
args = [r,g,b,a]
args = [0 if np.isnan(a) or np.isinf(a) else a for a in args]
@@ -342,7 +351,7 @@ def colorStr(c):
return ('%02x'*4) % colorTuple(c)
-def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs):
+def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255):
"""
Creates a QColor from a single index. Useful for stepping through a predefined list of colors.
@@ -354,7 +363,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi
values = int(values)
ind = int(index) % (hues * values)
indh = ind % hues
- indv = ind / hues
+ indv = ind // hues
if values > 1:
v = minValue + indv * ((maxValue-minValue) / (values-1))
else:
@@ -404,22 +413,53 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
def eq(a, b):
- """The great missing equivalence function: Guaranteed evaluation to a single bool value."""
+ """The great missing equivalence function: Guaranteed evaluation to a single bool value.
+
+ This function has some important differences from the == operator:
+
+ 1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values.
+ 2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur
+ (AtrtibuteError, ValueError).
+ 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).
+ """
if a is b:
return True
-
- try:
- with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10)
- e = a==b
- except ValueError:
+
+ # Avoid comparing large arrays against scalars; this is expensive and we know it should return False.
+ aIsArr = isinstance(a, (np.ndarray, MetaArray))
+ bIsArr = isinstance(b, (np.ndarray, MetaArray))
+ if (aIsArr or bIsArr) and type(a) != type(b):
return False
- except AttributeError:
+
+ # If both inputs are arrays, we can speeed up comparison if shapes / dtypes don't match
+ # NOTE: arrays of dissimilar type should be considered unequal even if they are numerically
+ # equal because they may behave differently when computed on.
+ if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype):
+ return False
+
+ # Test for equivalence.
+ # If the test raises a recognized exception, then return Falase
+ try:
+ try:
+ # Sometimes running catch_warnings(module=np) generates AttributeError ???
+ catcher = warnings.catch_warnings(module=np) # ignore numpy futurewarning (numpy v. 1.10)
+ catcher.__enter__()
+ except Exception:
+ catcher = None
+ e = a==b
+ except (ValueError, AttributeError):
return False
except:
print('failed to evaluate equivalence for:')
print(" a:", str(type(a)), str(a))
print(" b:", str(type(b)), str(b))
raise
+ finally:
+ if catcher is not None:
+ catcher.__exit__(None, None, None)
+
t = type(e)
if t is bool:
return e
@@ -716,26 +756,17 @@ def subArray(data, offset, shape, stride):
the input in the example above to have shape (10, 7) would cause the
output to have shape (2, 3, 7).
"""
- #data = data.flatten()
- data = data[offset:]
+ data = np.ascontiguousarray(data)[offset:]
shape = tuple(shape)
- stride = tuple(stride)
extraShape = data.shape[1:]
- #print data.shape, offset, shape, stride
- for i in range(len(shape)):
- mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),)
- newShape = shape[:i+1]
- if i < len(shape)-1:
- newShape += (stride[i],)
- newShape += extraShape
- #print i, mask, newShape
- #print "start:\n", data.shape, data
- data = data[mask]
- #print "mask:\n", data.shape, data
- data = data.reshape(newShape)
- #print "reshape:\n", data.shape, data
+
+ strides = list(data.strides[::-1])
+ itemsize = strides[-1]
+ for s in stride[1::-1]:
+ strides.append(itemsize * s)
+ strides = tuple(strides[::-1])
- return data
+ return np.ndarray(buffer=data, shape=shape+extraShape, strides=strides, dtype=data.dtype)
def transformToArray(tr):
@@ -1062,7 +1093,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels[i]
if minVal == maxVal:
maxVal += 1e-16
- newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype)
+ rng = maxVal-minVal
+ rng = 1 if rng == 0 else rng
+ newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
data = newData
else:
# Apply level scaling unless it would have no effect on the data
@@ -1188,7 +1221,20 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
if USE_PYSIDE:
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)
@@ -2126,7 +2172,7 @@ def isosurface(data, level):
## compute lookup table of index: vertexes mapping
faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte)
faceTableInds = np.argwhere(nTableFaces == i)
- faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds])
+ faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds])
faceTableI = faceTableI.reshape((len(triTable), i, 3))
faceShiftTables.append(edgeShifts[faceTableI])
diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py
index 77e6195f..897cbc50 100644
--- a/pyqtgraph/graphicsItems/ArrowItem.py
+++ b/pyqtgraph/graphicsItems/ArrowItem.py
@@ -39,7 +39,6 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.setStyle(**defaultOpts)
- self.rotate(self.opts['angle'])
self.moveBy(*self.opts['pos'])
def setStyle(self, **opts):
@@ -72,7 +71,10 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.opts.update(opts)
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
- self.path = fn.makeArrowPath(**opt)
+ tr = QtGui.QTransform()
+ tr.rotate(self.opts['angle'])
+ self.path = tr.map(fn.makeArrowPath(**opt))
+
self.setPath(self.path)
self.setPen(fn.mkPen(self.opts['pen']))
@@ -82,7 +84,8 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
else:
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
-
+
+
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
QtGui.QGraphicsPathItem.paint(self, p, *args)
diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py
index a1d5d029..657222ba 100644
--- a/pyqtgraph/graphicsItems/BarGraphItem.py
+++ b/pyqtgraph/graphicsItems/BarGraphItem.py
@@ -120,7 +120,7 @@ class BarGraphItem(GraphicsObject):
p.setPen(fn.mkPen(pen))
p.setBrush(fn.mkBrush(brush))
- for i in range(len(x0)):
+ for i in range(len(x0 if not np.isscalar(x0) else y0)):
if pens is not None:
p.setPen(fn.mkPen(pens[i]))
if brushes is not None:
diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py
index d45818dc..f88069bc 100644
--- a/pyqtgraph/graphicsItems/GraphicsItem.py
+++ b/pyqtgraph/graphicsItems/GraphicsItem.py
@@ -146,7 +146,8 @@ class GraphicsItem(object):
return parents
def viewRect(self):
- """Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget"""
+ """Return the visible bounds of this item's ViewBox or GraphicsWidget,
+ in the local coordinate system of the item."""
view = self.getViewBox()
if view is None:
return None
diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py
index 31764250..f85b64dd 100644
--- a/pyqtgraph/graphicsItems/HistogramLUTItem.py
+++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py
@@ -25,25 +25,41 @@ __all__ = ['HistogramLUTItem']
class HistogramLUTItem(GraphicsWidget):
"""
This is a graphicsWidget which provides controls for adjusting the display of an image.
+
Includes:
- Image histogram
- 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.
"""
sigLookupTableChanged = QtCore.Signal(object)
sigLevelsChanged = QtCore.Signal(object)
sigLevelChangeFinished = QtCore.Signal(object)
- def __init__(self, image=None, fillHistogram=True):
- """
- If *image* (ImageItem) 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.
- By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False.
- """
+ def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'):
GraphicsWidget.__init__(self)
self.lut = None
self.imageItem = lambda: None # fake a dead weakref
+ self.levelMode = levelMode
+ self.rgbHistogram = rgbHistogram
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
@@ -56,9 +72,26 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient = GradientEditorItem()
self.gradient.setOrientation('right')
self.gradient.loadPreset('grey')
- self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
- self.region.setZValue(1000)
- self.vb.addItem(self.region)
+ self.regions = [
+ LinearRegionItem([0, 1], 'horizontal', swapMode='block'),
+ LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r',
+ brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)),
+ LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g',
+ brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)),
+ LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b',
+ brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)),
+ LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w',
+ brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))]
+ for region in self.regions:
+ region.setZValue(1000)
+ self.vb.addItem(region)
+ region.lines[0].addMarker('<|', 0.5)
+ region.lines[1].addMarker('|>', 0.5)
+ region.sigRegionChanged.connect(self.regionChanging)
+ region.sigRegionChangeFinished.connect(self.regionChanged)
+
+ self.region = self.regions[0] # for backward compatibility.
+
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1)
@@ -67,76 +100,64 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
self.vb.setFlag(self.gradient.ItemStacksBehindParent)
- #self.grid = GridItem()
- #self.vb.addItem(self.grid)
-
self.gradient.sigGradientChanged.connect(self.gradientChanged)
- self.region.sigRegionChanged.connect(self.regionChanging)
- self.region.sigRegionChangeFinished.connect(self.regionChanged)
self.vb.sigRangeChanged.connect(self.viewRangeChanged)
- self.plot = PlotDataItem()
- self.plot.rotate(90)
+ add = QtGui.QPainter.CompositionMode_Plus
+ self.plots = [
+ PlotCurveItem(pen=(200, 200, 200, 100)), # mono
+ PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r
+ PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g
+ PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b
+ PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a
+ ]
+
+ self.plot = self.plots[0] # for backward compatibility.
+ for plot in self.plots:
+ plot.rotate(90)
+ self.vb.addItem(plot)
+
self.fillHistogram(fillHistogram)
+ self._showRegions()
self.vb.addItem(self.plot)
self.autoHistogramRange()
if image is not None:
self.setImageItem(image)
- #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
- if fill:
- self.plot.setFillLevel(level)
- self.plot.setFillBrush(color)
- else:
- self.plot.setFillLevel(None)
-
- #def sizeHint(self, *args):
- #return QtCore.QSizeF(115, 200)
+ colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)]
+ for i,plot in enumerate(self.plots):
+ if fill:
+ plot.setFillLevel(level)
+ plot.setBrush(colors[i])
+ else:
+ plot.setFillLevel(None)
def paint(self, p, *args):
+ if self.levelMode != 'mono':
+ return
+
pen = self.region.lines[0].pen
rgn = self.getLevels()
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())
- for pen in [fn.mkPen('k', width=3), pen]:
+ for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]:
p.setPen(pen)
- p.drawLine(p1, gradRect.bottomLeft())
- p.drawLine(p2, gradRect.topLeft())
+ p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft())
+ p.drawLine(p2 - Point(0, 5), gradRect.topLeft())
p.drawLine(gradRect.topLeft(), gradRect.topRight())
p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
- #p.drawRect(self.boundingRect())
-
def setHistogramRange(self, mn, mx, padding=0.1):
"""Set the Y range on the histogram plot. This disables auto-scaling."""
self.vb.enableAutoRange(self.vb.YAxis, False)
self.vb.setYRange(mn, mx, padding)
- #d = mx-mn
- #mn -= d*padding
- #mx += d*padding
- #self.range = [mn,mx]
- #self.updateRange()
- #self.vb.setMouseEnabled(False, True)
- #self.region.setBounds([mn,mx])
-
def autoHistogramRange(self):
"""Enable auto-scaling on the histogram plot."""
self.vb.enableAutoRange(self.vb.XYAxes)
- #self.range = None
- #self.updateRange()
- #self.vb.setMouseEnabled(False, False)
-
- #def updateRange(self):
- #self.vb.autoRange()
- #if self.range is not None:
- #self.vb.setYRange(*self.range)
- #vr = self.vb.viewRect()
-
- #self.region.setBounds([vr.top(), vr.bottom()])
def setImageItem(self, img):
"""Set an ImageItem to have its levels and LUT automatically controlled
@@ -145,10 +166,8 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem = weakref.ref(img)
img.sigImageChanged.connect(self.imageChanged)
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
- #self.gradientChanged()
self.regionChanged()
self.imageChanged(autoLevel=True)
- #self.vb.autoRange()
def viewRangeChanged(self):
self.update()
@@ -161,14 +180,14 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
self.lut = None
- #if self.imageItem is not None:
- #self.imageItem.setLookupTable(self.gradient.getLookupTable(512))
self.sigLookupTableChanged.emit(self)
def getLookupTable(self, img=None, n=None, alpha=None):
"""Return a lookup table from the color gradient defined by this
HistogramLUTItem.
"""
+ if self.levelMode is not 'mono':
+ return None
if n is None:
if img.dtype == np.uint8:
n = 256
@@ -180,36 +199,148 @@ class HistogramLUTItem(GraphicsWidget):
def regionChanged(self):
if self.imageItem() is not None:
- self.imageItem().setLevels(self.region.getRegion())
+ self.imageItem().setLevels(self.getLevels())
self.sigLevelChangeFinished.emit(self)
- #self.update()
def regionChanging(self):
if self.imageItem() is not None:
- self.imageItem().setLevels(self.region.getRegion())
+ self.imageItem().setLevels(self.getLevels())
self.sigLevelsChanged.emit(self)
self.update()
def imageChanged(self, autoLevel=False, autoRange=False):
- profiler = debug.Profiler()
- h = self.imageItem().getHistogram()
- profiler('get histogram')
- if h[0] is None:
+ if self.imageItem() is None:
return
- self.plot.setData(*h)
- profiler('set plot')
- if autoLevel:
- mn = h[0][0]
- mx = h[0][-1]
- self.region.setRegion([mn, mx])
- profiler('set region')
+
+ if self.levelMode == 'mono':
+ for plt in self.plots[1:]:
+ plt.setVisible(False)
+ self.plots[0].setVisible(True)
+ # plot one histogram for all image data
+ profiler = debug.Profiler()
+ h = self.imageItem().getHistogram()
+ profiler('get histogram')
+ if h[0] is None:
+ return
+ self.plot.setData(*h)
+ profiler('set plot')
+ if autoLevel:
+ mn = h[0][0]
+ mx = h[0][-1]
+ self.region.setRegion([mn, mx])
+ profiler('set region')
+ else:
+ mn, mx = self.imageItem().levels
+ self.region.setRegion([mn, mx])
+ else:
+ # plot one histogram for each channel
+ self.plots[0].setVisible(False)
+ ch = self.imageItem().getHistogram(perChannel=True)
+ if ch[0] is None:
+ return
+ for i in range(1, 5):
+ if len(ch) >= i:
+ h = ch[i-1]
+ self.plots[i].setVisible(True)
+ self.plots[i].setData(*h)
+ if autoLevel:
+ mn = h[0][0]
+ mx = h[0][-1]
+ self.region[i].setRegion([mn, mx])
+ else:
+ # hide channels not present in image data
+ self.plots[i].setVisible(False)
+ # make sure we are displaying the correct number of channels
+ self._showRegions()
def getLevels(self):
"""Return the min and max levels.
- """
- return self.region.getRegion()
- def setLevels(self, mn, mx):
- """Set the min and max levels.
+ For rgba mode, this returns a list of the levels for each channel.
"""
- self.region.setRegion([mn, mx])
+ if self.levelMode == 'mono':
+ return self.region.getRegion()
+ else:
+ nch = self.imageItem().channels()
+ if nch is None:
+ nch = 3
+ return [r.getRegion() for r in self.regions[1:nch+1]]
+
+ def setLevels(self, min=None, max=None, rgba=None):
+ """Set the min/max (bright and dark) levels.
+
+ Arguments may be *min* and *max* for single-channel data, or
+ *rgba* = [(rmin, rmax), ...] for multi-channel data.
+ """
+ if self.levelMode == 'mono':
+ if min is None:
+ min, max = rgba[0]
+ assert None not in (min, max)
+ self.region.setRegion((min, max))
+ else:
+ if rgba is None:
+ raise TypeError("Must specify rgba argument when levelMode != 'mono'.")
+ for i, levels in enumerate(rgba):
+ self.regions[i+1].setRegion(levels)
+
+ def setLevelMode(self, mode):
+ """ Set the method of controlling the image levels offered to the user.
+ Options are 'mono' or 'rgba'.
+ """
+ assert mode in ('mono', 'rgba')
+
+ if mode == self.levelMode:
+ return
+
+ oldLevels = self.getLevels()
+ self.levelMode = mode
+ self._showRegions()
+
+ # do our best to preserve old levels
+ if mode == 'mono':
+ levels = np.array(oldLevels).mean(axis=0)
+ self.setLevels(*levels)
+ else:
+ levels = [oldLevels] * 4
+ self.setLevels(rgba=levels)
+
+ # force this because calling self.setLevels might not set the imageItem
+ # levels if there was no change to the region item
+ self.imageItem().setLevels(self.getLevels())
+
+ self.imageChanged()
+ self.update()
+
+ def _showRegions(self):
+ for i in range(len(self.regions)):
+ self.regions[i].setVisible(False)
+
+ if self.levelMode == 'rgba':
+ imax = 4
+ if self.imageItem() is not None:
+ # Only show rgb channels if connected image lacks alpha.
+ nch = self.imageItem().channels()
+ if nch is None:
+ nch = 3
+ xdif = 1.0 / nch
+ for i in range(1, nch+1):
+ self.regions[i].setVisible(True)
+ self.regions[i].setSpan((i-1) * xdif, i * xdif)
+ self.gradient.hide()
+ elif self.levelMode == 'mono':
+ self.regions[0].setVisible(True)
+ self.gradient.show()
+ else:
+ raise ValueError("Unknown level mode %r" % self.levelMode)
+
+ def saveState(self):
+ return {
+ 'gradient': self.gradient.saveState(),
+ 'levels': self.getLevels(),
+ 'mode': self.levelMode,
+ }
+
+ def restoreState(self, state):
+ self.setLevelMode(state['mode'])
+ self.gradient.restoreState(state['gradient'])
+ self.setLevels(*state['levels'])
diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py
index 3d45ad77..d1e4aa9e 100644
--- a/pyqtgraph/graphicsItems/ImageItem.py
+++ b/pyqtgraph/graphicsItems/ImageItem.py
@@ -98,6 +98,11 @@ class ImageItem(GraphicsObject):
axis = 1 if self.axisOrder == 'col-major' else 0
return self.image.shape[axis]
+ def channels(self):
+ if self.image is None:
+ return None
+ return self.image.shape[2] if self.image.ndim == 3 else 1
+
def boundingRect(self):
if self.image is None:
return QtCore.QRectF(0., 0., 0., 0.)
@@ -214,7 +219,8 @@ class ImageItem(GraphicsObject):
border Sets the pen used when drawing the image border. Default is None.
autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and
- reduces aliasing.
+ reduces aliasing. If autoDownsample is not specified, then ImageItem will
+ choose whether to downsample the image based on its size.
================= =========================================================================
@@ -328,7 +334,7 @@ class ImageItem(GraphicsObject):
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
- return nanmin(data), nanmax(data)
+ return np.nanmin(data), np.nanmax(data)
def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image.
@@ -347,10 +353,15 @@ class ImageItem(GraphicsObject):
profile = debug.Profiler()
if self.image is None or self.image.size == 0:
return
- if isinstance(self.lut, collections.Callable):
- lut = self.lut(self.image)
+
+ # Request a lookup table if this image has only one channel
+ if self.image.ndim == 2 or self.image.shape[2] == 1:
+ if isinstance(self.lut, collections.Callable):
+ lut = self.lut(self.image)
+ else:
+ lut = self.lut
else:
- lut = self.lut
+ lut = None
if self.autoDownsample:
# reduce dimensions of image based on screen resolution
@@ -394,9 +405,12 @@ class ImageItem(GraphicsObject):
lut = self._effectiveLut
levels = None
+ # Convert single-channel image to 2D array
+ if image.ndim == 3 and image.shape[-1] == 1:
+ image = image[..., 0]
+
# Assume images are in column-major order for backward compatibility
# (most images are in row-major order)
-
if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim])
@@ -429,7 +443,8 @@ class ImageItem(GraphicsObject):
self.render()
self.qimage.save(fileName, *args)
- def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
+ def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
+ targetHistogramSize=500, **kwds):
"""Returns x and y arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram().
@@ -445,6 +460,9 @@ class ImageItem(GraphicsObject):
with each bin having an integer width.
* All other types will have *targetHistogramSize* bins.
+ If *perChannel* is True, then the histogram is computed once per channel
+ and the output is a list of the results.
+
This method is also used when automatically computing levels.
"""
if self.image is None:
@@ -457,21 +475,33 @@ class ImageItem(GraphicsObject):
stepData = self.image[::step[0], ::step[1]]
if bins == 'auto':
+ mn = stepData.min()
+ mx = stepData.max()
if stepData.dtype.kind in "ui":
- mn = stepData.min()
- mx = stepData.max()
+ # For integer data, we select the bins carefully to avoid aliasing
step = np.ceil((mx-mn) / 500.)
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
- if len(bins) == 0:
- bins = [mn, mx]
else:
- bins = 500
+ # for float data, let numpy select the bins.
+ bins = np.linspace(mn, mx, 500)
+
+ if len(bins) == 0:
+ bins = [mn, mx]
kwds['bins'] = bins
- stepData = stepData[np.isfinite(stepData)]
- hist = np.histogram(stepData, **kwds)
-
- return hist[1][:-1], hist[0]
+
+ if perChannel:
+ hist = []
+ for i in range(stepData.shape[-1]):
+ stepChan = stepData[..., i]
+ stepChan = stepChan[np.isfinite(stepChan)]
+ h = np.histogram(stepChan, **kwds)
+ hist.append((h[1][:-1], h[0]))
+ return hist
+ else:
+ stepData = stepData[np.isfinite(stepData)]
+ hist = np.histogram(stepData, **kwds)
+ return hist[1][:-1], hist[0]
def setPxMode(self, b):
"""
diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py
index 3da82327..7aeb1620 100644
--- a/pyqtgraph/graphicsItems/InfiniteLine.py
+++ b/pyqtgraph/graphicsItems/InfiniteLine.py
@@ -31,7 +31,8 @@ class InfiniteLine(GraphicsObject):
sigPositionChanged = QtCore.Signal(object)
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None,
- hoverPen=None, label=None, labelOpts=None, name=None):
+ hoverPen=None, label=None, labelOpts=None, span=(0, 1), markers=None,
+ name=None):
"""
=============== ==================================================================
**Arguments:**
@@ -41,22 +42,28 @@ class InfiniteLine(GraphicsObject):
pen Pen to use when drawing line. Can be any arguments that are valid
for :func:`mkPen
`. Default pen is transparent
yellow.
+ hoverPen Pen to use when the mouse cursor hovers over the line.
+ Only used when movable=True.
movable If True, the line can be dragged to a new position by the user.
+ bounds Optional [min, max] bounding values. Bounds are only valid if the
+ line is vertical or horizontal.
hoverPen Pen to use when drawing line when hovering over it. Can be any
arguments that are valid for :func:`mkPen `.
Default pen is red.
- bounds Optional [min, max] bounding values. Bounds are only valid if the
- line is vertical or horizontal.
label Text to be displayed in a label attached to the line, or
None to show no label (default is None). May optionally
include formatting strings to display the line value.
labelOpts A dict of keyword arguments to use when constructing the
text label. See :class:`InfLineLabel`.
+ span Optional tuple (min, max) giving the range over the view to draw
+ the line. For example, with a vertical line, use span=(0.5, 1)
+ to draw only on the top half of the view.
+ markers List of (marker, position, size) tuples, one per marker to display
+ on the line. See the addMarker method.
name Name of the item
=============== ==================================================================
"""
self._boundingRect = None
- self._line = None
self._name = name
@@ -79,11 +86,25 @@ class InfiniteLine(GraphicsObject):
if pen is None:
pen = (200, 200, 100)
self.setPen(pen)
+
if hoverPen is None:
self.setHoverPen(color=(255,0,0), width=self.pen.width())
else:
self.setHoverPen(hoverPen)
+
+ self.span = span
self.currentPen = self.pen
+
+ self.markers = []
+ self._maxMarkerSize = 0
+ if markers is not None:
+ for m in markers:
+ self.addMarker(*m)
+
+ # Cache variables for managing bounds
+ self._endPoints = [0, 1] #
+ self._bounds = None
+ self._lastViewSize = None
if label is not None:
labelOpts = {} if labelOpts is None else labelOpts
@@ -98,7 +119,12 @@ class InfiniteLine(GraphicsObject):
"""Set the (minimum, maximum) allowable values when dragging."""
self.maxRange = bounds
self.setValue(self.value())
-
+
+ def bounds(self):
+ """Return the (minimum, maximum) values allowed when dragging.
+ """
+ return self.maxRange[:]
+
def setPen(self, *args, **kwargs):
"""Set the pen for drawing the line. Allowable arguments are any that are valid
for :func:`mkPen `."""
@@ -115,11 +141,70 @@ class InfiniteLine(GraphicsObject):
If the line is not movable, then hovering is also disabled.
Added in version 0.9.9."""
+ # If user did not supply a width, then copy it from pen
+ widthSpecified = ((len(args) == 1 and
+ (isinstance(args[0], QtGui.QPen) or
+ (isinstance(args[0], dict) and 'width' in args[0]))
+ ) or 'width' in kwargs)
self.hoverPen = fn.mkPen(*args, **kwargs)
+ if not widthSpecified:
+ self.hoverPen.setWidth(self.pen.width())
+
if self.mouseHovering:
self.currentPen = self.hoverPen
self.update()
+
+ def addMarker(self, marker, position=0.5, size=10.0):
+ """Add a marker to be displayed on the line.
+
+ ============= =========================================================
+ **Arguments**
+ marker String indicating the style of marker to add:
+ '<|', '|>', '>|', '|<', '<|>', '>|<', '^', '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.
+ ============= =========================================================
+ """
+ path = QtGui.QPainterPath()
+ if marker == 'o':
+ path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
+ if '<|' in marker:
+ p = QtGui.QPolygonF([Point(0.5, 0), Point(0, -0.5), Point(-0.5, 0)])
+ path.addPolygon(p)
+ path.closeSubpath()
+ if '|>' in marker:
+ p = QtGui.QPolygonF([Point(0.5, 0), Point(0, 0.5), Point(-0.5, 0)])
+ path.addPolygon(p)
+ path.closeSubpath()
+ if '>|' in marker:
+ p = QtGui.QPolygonF([Point(0.5, -0.5), Point(0, 0), Point(-0.5, -0.5)])
+ path.addPolygon(p)
+ path.closeSubpath()
+ if '|<' in marker:
+ p = QtGui.QPolygonF([Point(0.5, 0.5), Point(0, 0), Point(-0.5, 0.5)])
+ path.addPolygon(p)
+ path.closeSubpath()
+ if '^' in marker:
+ p = QtGui.QPolygonF([Point(0, -0.5), Point(0.5, 0), Point(0, 0.5)])
+ path.addPolygon(p)
+ path.closeSubpath()
+ if 'v' in marker:
+ p = QtGui.QPolygonF([Point(0, -0.5), Point(-0.5, 0), Point(0, 0.5)])
+ path.addPolygon(p)
+ path.closeSubpath()
+
+ self.markers.append((path, position, size))
+ self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
+ self.update()
+ def clearMarkers(self):
+ """ Remove all markers from this line.
+ """
+ self.markers = []
+ self._maxMarkerSize = 0
+ self.update()
+
def setAngle(self, angle):
"""
Takes angle argument in degrees.
@@ -128,7 +213,7 @@ class InfiniteLine(GraphicsObject):
Note that the use of value() and setValue() changes if the line is
not vertical or horizontal.
"""
- self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135
+ self.angle = angle #((angle+45) % 180) - 45 ## -45 <= angle < 135
self.resetTransform()
self.rotate(self.angle)
self.update()
@@ -199,35 +284,98 @@ class InfiniteLine(GraphicsObject):
#else:
#print "ignore", change
#return GraphicsObject.itemChange(self, change, val)
+
+ def setSpan(self, mn, mx):
+ if self.span != (mn, mx):
+ self.span = (mn, mx)
+ self.update()
def _invalidateCache(self):
- self._line = None
self._boundingRect = None
+ def _computeBoundingRect(self):
+ #br = UIGraphicsItem.boundingRect(self)
+ vr = self.viewRect() # bounds of containing ViewBox mapped to local coords.
+ if vr is None:
+ return QtCore.QRectF()
+
+ ## add a 4-pixel radius around the line for mouse interaction.
+
+ px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
+ if px is None:
+ px = 0
+ pw = max(self.pen.width() / 2, self.hoverPen.width() / 2)
+ w = max(4, self._maxMarkerSize + pw) + 1
+ w = w * px
+ br = QtCore.QRectF(vr)
+ br.setBottom(-w)
+ br.setTop(w)
+
+ 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 = br.normalized()
+
+ vs = self.getViewBox().size()
+
+ if self._bounds != br or self._lastViewSize != vs:
+ self._bounds = br
+ self._lastViewSize = vs
+ self.prepareGeometryChange()
+
+ self._endPoints = (left, right)
+ self._lastViewRect = vr
+
+ return self._bounds
+
def boundingRect(self):
if self._boundingRect is None:
- #br = UIGraphicsItem.boundingRect(self)
- br = self.viewRect()
- if br is None:
- return QtCore.QRectF()
-
- ## add a 4-pixel radius around the line for mouse interaction.
- px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
- if px is None:
- px = 0
- w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
- br.setBottom(-w)
- br.setTop(w)
-
- br = br.normalized()
- self._boundingRect = br
- self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0)
+ self._boundingRect = self._computeBoundingRect()
return self._boundingRect
def paint(self, p, *args):
- p.setPen(self.currentPen)
- p.drawLine(self._line)
-
+ p.setRenderHint(p.Antialiasing)
+
+ left, right = self._endPoints
+ pen = self.currentPen
+ pen.setJoinStyle(QtCore.Qt.MiterJoin)
+ p.setPen(pen)
+ p.drawLine(Point(left, 0), Point(right, 0))
+
+
+ if len(self.markers) == 0:
+ return
+
+ # paint markers in native coordinate system
+ tr = p.transform()
+ p.resetTransform()
+
+ start = tr.map(Point(left, 0))
+ end = tr.map(Point(right, 0))
+ up = tr.map(Point(left, 1))
+ dif = end - start
+ length = Point(dif).length()
+ angle = np.arctan2(dif.y(), dif.x()) * 180 / np.pi
+
+ p.translate(start)
+ p.rotate(angle)
+
+ up = up - start
+ det = up.x() * dif.y() - dif.x() * up.y()
+ p.scale(1, 1 if det > 0 else -1)
+
+ p.setBrush(fn.mkBrush(self.currentPen.color()))
+ #p.setPen(fn.mkPen(None))
+ tr = p.transform()
+ for path, pos, size in self.markers:
+ p.setTransform(tr)
+ x = length * pos
+ p.translate(x, 0)
+ p.scale(size, size)
+ p.drawPath(path)
+
def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == 0:
return None ## x axis should never be auto-scaled
diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py
index 20d6416e..200820fc 100644
--- a/pyqtgraph/graphicsItems/LegendItem.py
+++ b/pyqtgraph/graphicsItems/LegendItem.py
@@ -81,19 +81,19 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
self.layout.addItem(label, row, 1)
self.updateSize()
- def removeItem(self, name):
+ def removeItem(self, item):
"""
Removes one item from the legend.
============== ========================================================
**Arguments:**
- title The title displayed for this item.
+ item The item to remove or its name.
============== ========================================================
"""
# Thanks, Ulrich!
# cycle for a match
for sample, label in self.items:
- if label.text == name: # hit
+ if sample.item is item or label.text == item:
self.items.remove( (sample, label) ) # remove from itemlist
self.layout.removeItem(sample) # remove from layout
sample.close() # remove from drawing
@@ -110,7 +110,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
#print("-------")
for sample, label in self.items:
height += max(sample.height(), label.height()) + 3
- width = max(width, sample.width()+label.width())
+ 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)
@@ -130,7 +131,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
if ev.button() == QtCore.Qt.LeftButton:
dpos = ev.pos() - ev.lastPos()
self.autoAnchor(self.pos() + dpos)
-
+
+
class ItemSample(GraphicsWidget):
""" Class responsible for drawing a single item in a LegendItem (sans label).
diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py
index e139190b..9903dac5 100644
--- a/pyqtgraph/graphicsItems/LinearRegionItem.py
+++ b/pyqtgraph/graphicsItems/LinearRegionItem.py
@@ -1,14 +1,14 @@
from ..Qt import QtGui, QtCore
-from .UIGraphicsItem import UIGraphicsItem
+from .GraphicsObject import GraphicsObject
from .InfiniteLine import InfiniteLine
from .. import functions as fn
from .. import debug as debug
__all__ = ['LinearRegionItem']
-class LinearRegionItem(UIGraphicsItem):
+class LinearRegionItem(GraphicsObject):
"""
- **Bases:** :class:`UIGraphicsItem `
+ **Bases:** :class:`GraphicsObject `
Used for marking a horizontal or vertical region in plots.
The region can be dragged and is bounded by lines which can be dragged individually.
@@ -26,65 +26,110 @@ class LinearRegionItem(UIGraphicsItem):
sigRegionChanged = QtCore.Signal(object)
Vertical = 0
Horizontal = 1
+ _orientation_axis = {
+ Vertical: 0,
+ Horizontal: 1,
+ 'vertical': 0,
+ 'horizontal': 1,
+ }
- def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
+ def __init__(self, values=(0, 1), orientation='vertical', brush=None, pen=None,
+ hoverBrush=None, hoverPen=None, movable=True, bounds=None,
+ span=(0, 1), swapMode='sort'):
"""Create a new LinearRegionItem.
============== =====================================================================
**Arguments:**
values A list of the positions of the lines in the region. These are not
limits; limits can be set by specifying bounds.
- orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
- If not specified it will be vertical.
+ orientation Options are 'vertical' or 'horizontal', indicating the
+ The default is 'vertical', indicating that the
brush Defines the brush that fills the region. Can be any arguments that
are valid for :func:`mkBrush `. Default is
transparent blue.
+ pen The pen to use when drawing the lines that bound the region.
+ hoverBrush The brush to use when the mouse is hovering over the region.
+ hoverPen The pen to use when the mouse is hovering over the region.
movable If True, the region and individual lines are movable by the user; if
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.
+ 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".
============== =====================================================================
"""
- UIGraphicsItem.__init__(self)
- if orientation is None:
- orientation = LinearRegionItem.Vertical
+ GraphicsObject.__init__(self)
self.orientation = orientation
self.bounds = QtCore.QRectF()
self.blockLineSignal = False
self.moving = False
self.mouseHovering = False
+ self.span = span
+ self.swapMode = swapMode
+ self._bounds = None
- if orientation == LinearRegionItem.Horizontal:
+ # note LinearRegionItem.Horizontal and LinearRegionItem.Vertical
+ # are kept for backward compatibility.
+ lineKwds = dict(
+ movable=movable,
+ bounds=bounds,
+ span=span,
+ pen=pen,
+ hoverPen=hoverPen,
+ )
+
+ if orientation in ('horizontal', LinearRegionItem.Horizontal):
self.lines = [
- InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds),
- InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)]
- elif orientation == LinearRegionItem.Vertical:
+ # rotate lines to 180 to preserve expected line orientation
+ # with respect to region. This ensures that placing a '<|'
+ # marker on lines[0] causes it to point left in vertical mode
+ # and down in horizontal mode.
+ InfiniteLine(QtCore.QPointF(0, values[0]), angle=0, **lineKwds),
+ InfiniteLine(QtCore.QPointF(0, values[1]), angle=0, **lineKwds)]
+ self.lines[0].scale(1, -1)
+ self.lines[1].scale(1, -1)
+ elif orientation in ('vertical', LinearRegionItem.Vertical):
self.lines = [
- InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds),
- InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)]
+ InfiniteLine(QtCore.QPointF(values[0], 0), angle=90, **lineKwds),
+ InfiniteLine(QtCore.QPointF(values[1], 0), angle=90, **lineKwds)]
else:
- raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal')
-
+ raise Exception("Orientation must be 'vertical' or 'horizontal'.")
for l in self.lines:
l.setParentItem(self)
l.sigPositionChangeFinished.connect(self.lineMoveFinished)
- l.sigPositionChanged.connect(self.lineMoved)
+ self.lines[0].sigPositionChanged.connect(lambda: self.lineMoved(0))
+ self.lines[1].sigPositionChanged.connect(lambda: self.lineMoved(1))
if brush is None:
brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50))
self.setBrush(brush)
+ if hoverBrush is None:
+ c = self.brush.color()
+ c.setAlpha(min(c.alpha() * 2, 255))
+ hoverBrush = fn.mkBrush(c)
+ self.setHoverBrush(hoverBrush)
+
self.setMovable(movable)
def getRegion(self):
"""Return the values at the edges of the region."""
- #if self.orientation[0] == 'h':
- #r = (self.bounds.top(), self.bounds.bottom())
- #else:
- #r = (self.bounds.left(), self.bounds.right())
- r = [self.lines[0].value(), self.lines[1].value()]
- return (min(r), max(r))
+ r = (self.lines[0].value(), self.lines[1].value())
+ if self.swapMode == 'sort':
+ return (min(r), max(r))
+ else:
+ return r
def setRegion(self, rgn):
"""Set the values for the edges of the region.
@@ -101,7 +146,8 @@ class LinearRegionItem(UIGraphicsItem):
self.blockLineSignal = False
self.lines[1].setValue(rgn[1])
#self.blockLineSignal = False
- self.lineMoved()
+ self.lineMoved(0)
+ self.lineMoved(1)
self.lineMoveFinished()
def setBrush(self, *br, **kargs):
@@ -111,6 +157,13 @@ class LinearRegionItem(UIGraphicsItem):
self.brush = fn.mkBrush(*br, **kargs)
self.currentBrush = self.brush
+ def setHoverBrush(self, *br, **kargs):
+ """Set the brush that fills the region when the mouse is hovering over.
+ Can have any arguments that are valid
+ for :func:`mkBrush `.
+ """
+ self.hoverBrush = fn.mkBrush(*br, **kargs)
+
def setBounds(self, bounds):
"""Optional [min, max] bounding values for the region. To have no bounds on the
region use [None, None].
@@ -128,81 +181,67 @@ class LinearRegionItem(UIGraphicsItem):
self.movable = m
self.setAcceptHoverEvents(m)
+ def setSpan(self, mn, mx):
+ if self.span == (mn, mx):
+ return
+ self.span = (mn, mx)
+ self.lines[0].setSpan(mn, mx)
+ self.lines[1].setSpan(mn, mx)
+ self.update()
+
def boundingRect(self):
- br = UIGraphicsItem.boundingRect(self)
+ br = self.viewRect() # bounds of containing ViewBox mapped to local coords.
+
rng = self.getRegion()
- if self.orientation == LinearRegionItem.Vertical:
+ if self.orientation in ('vertical', LinearRegionItem.Vertical):
br.setLeft(rng[0])
br.setRight(rng[1])
+ length = br.height()
+ br.setBottom(br.top() + length * self.span[1])
+ br.setTop(br.top() + length * self.span[0])
else:
br.setTop(rng[0])
br.setBottom(rng[1])
- return br.normalized()
+ length = br.width()
+ br.setRight(br.left() + length * self.span[1])
+ br.setLeft(br.left() + length * self.span[0])
+
+ br = br.normalized()
+
+ if self._bounds != br:
+ self._bounds = br
+ self.prepareGeometryChange()
+
+ return br
def paint(self, p, *args):
profiler = debug.Profiler()
- UIGraphicsItem.paint(self, p, *args)
p.setBrush(self.currentBrush)
p.setPen(fn.mkPen(None))
p.drawRect(self.boundingRect())
def dataBounds(self, axis, frac=1.0, orthoRange=None):
- if axis == self.orientation:
+ if axis == self._orientation_axis[self.orientation]:
return self.getRegion()
else:
return None
- def lineMoved(self):
+ def lineMoved(self, i):
if self.blockLineSignal:
return
+
+ # lines swapped
+ if self.lines[0].value() > self.lines[1].value():
+ if self.swapMode == 'block':
+ self.lines[i].setValue(self.lines[1-i].value())
+ elif self.swapMode == 'push':
+ self.lines[1-i].setValue(self.lines[i].value())
+
self.prepareGeometryChange()
- #self.emit(QtCore.SIGNAL('regionChanged'), self)
self.sigRegionChanged.emit(self)
def lineMoveFinished(self):
- #self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
self.sigRegionChangeFinished.emit(self)
-
-
- #def updateBounds(self):
- #vb = self.view().viewRect()
- #vals = [self.lines[0].value(), self.lines[1].value()]
- #if self.orientation[0] == 'h':
- #vb.setTop(min(vals))
- #vb.setBottom(max(vals))
- #else:
- #vb.setLeft(min(vals))
- #vb.setRight(max(vals))
- #if vb != self.bounds:
- #self.bounds = vb
- #self.rect.setRect(vb)
-
- #def mousePressEvent(self, ev):
- #if not self.movable:
- #ev.ignore()
- #return
- #for l in self.lines:
- #l.mousePressEvent(ev) ## pass event to both lines so they move together
- ##if self.movable and ev.button() == QtCore.Qt.LeftButton:
- ##ev.accept()
- ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p)
- ##else:
- ##ev.ignore()
-
- #def mouseReleaseEvent(self, ev):
- #for l in self.lines:
- #l.mouseReleaseEvent(ev)
-
- #def mouseMoveEvent(self, ev):
- ##print "move", ev.pos()
- #if not self.movable:
- #return
- #self.lines[0].blockSignals(True) # only want to update once
- #for l in self.lines:
- #l.mouseMoveEvent(ev)
- #self.lines[0].blockSignals(False)
- ##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta)
- ##self.emit(QtCore.SIGNAL('dragged'), self)
def mouseDragEvent(self, ev):
if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0:
@@ -218,12 +257,9 @@ class LinearRegionItem(UIGraphicsItem):
if not self.moving:
return
- #delta = ev.pos() - ev.lastPos()
self.lines[0].blockSignals(True) # only want to update once
for i, l in enumerate(self.lines):
l.setPos(self.cursorOffsets[i] + ev.pos())
- #l.setPos(l.pos()+delta)
- #l.mouseDragEvent(ev)
self.lines[0].blockSignals(False)
self.prepareGeometryChange()
@@ -242,7 +278,6 @@ class LinearRegionItem(UIGraphicsItem):
self.sigRegionChanged.emit(self)
self.sigRegionChangeFinished.emit(self)
-
def hoverEvent(self, ev):
if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
self.setMouseHover(True)
@@ -255,36 +290,7 @@ class LinearRegionItem(UIGraphicsItem):
return
self.mouseHovering = hover
if hover:
- c = self.brush.color()
- c.setAlpha(c.alpha() * 2)
- self.currentBrush = fn.mkBrush(c)
+ self.currentBrush = self.hoverBrush
else:
self.currentBrush = self.brush
self.update()
-
- #def hoverEnterEvent(self, ev):
- #print "rgn hover enter"
- #ev.ignore()
- #self.updateHoverBrush()
-
- #def hoverMoveEvent(self, ev):
- #print "rgn hover move"
- #ev.ignore()
- #self.updateHoverBrush()
-
- #def hoverLeaveEvent(self, ev):
- #print "rgn hover leave"
- #ev.ignore()
- #self.updateHoverBrush(False)
-
- #def updateHoverBrush(self, hover=None):
- #if hover is None:
- #scene = self.scene()
- #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag)
-
- #if hover:
- #self.currentBrush = fn.mkBrush(255, 0,0,100)
- #else:
- #self.currentBrush = self.brush
- #self.update()
-
diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py
index d66a8a99..9b4e95ef 100644
--- a/pyqtgraph/graphicsItems/PlotCurveItem.py
+++ b/pyqtgraph/graphicsItems/PlotCurveItem.py
@@ -68,6 +68,7 @@ class PlotCurveItem(GraphicsObject):
'antialias': getConfigOption('antialias'),
'connect': 'all',
'mouseWidth': 8, # width of shape responding to mouse click
+ 'compositionMode': None,
}
self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs)
@@ -93,6 +94,24 @@ class PlotCurveItem(GraphicsObject):
self._mouseShape = None
self._boundingRect = None
+ def setCompositionMode(self, mode):
+ """Change the composition mode of the item (see QPainter::CompositionMode
+ in the Qt documentation). This is useful when overlaying multiple items.
+
+ ============================================ ============================================================
+ **Most common arguments:**
+ QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
+ is opaque. Otherwise, it uses the alpha channel to blend
+ the image with the background.
+ QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
+ reflect the lightness or darkness of the background.
+ QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
+ are added together.
+ QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
+ ============================================ ============================================================
+ """
+ self.opts['compositionMode'] = mode
+ self.update()
def getData(self):
return self.xData, self.yData
@@ -132,6 +151,8 @@ class PlotCurveItem(GraphicsObject):
if any(np.isinf(b)):
mask = np.isfinite(d)
d = d[mask]
+ if len(d) == 0:
+ return (None, None)
b = (d.min(), d.max())
elif frac <= 0.0:
@@ -173,7 +194,7 @@ class PlotCurveItem(GraphicsObject):
if self._boundingRect is None:
(xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1)
- if xmn is None:
+ if xmn is None or ymn is None:
return QtCore.QRectF()
px = py = 0.0
@@ -272,7 +293,7 @@ class PlotCurveItem(GraphicsObject):
def setData(self, *args, **kargs):
"""
- ============== ========================================================
+ =============== ========================================================
**Arguments:**
x, y (numpy arrays) Data to show
pen Pen to use when drawing. Any single argument accepted by
@@ -296,7 +317,9 @@ class PlotCurveItem(GraphicsObject):
to be drawn. "finite" causes segments to be omitted if
they are attached to nan or inf values. For any other
connectivity, specify an array of boolean values.
- ============== ========================================================
+ compositionMode See :func:`setCompositionMode
+ `.
+ =============== ========================================================
If non-keyword arguments are used, they will be interpreted as
setData(y) for a single argument and setData(x, y) for two
@@ -309,6 +332,9 @@ class PlotCurveItem(GraphicsObject):
def updateData(self, *args, **kargs):
profiler = debug.Profiler()
+ if 'compositionMode' in kargs:
+ self.setCompositionMode(kargs['compositionMode'])
+
if len(args) == 1:
kargs['y'] = args[0]
elif len(args) == 2:
@@ -428,7 +454,6 @@ class PlotCurveItem(GraphicsObject):
x = None
y = None
path = self.getPath()
-
profiler('generate path')
if self._exportOpts is not False:
@@ -438,6 +463,9 @@ class PlotCurveItem(GraphicsObject):
p.setRenderHint(p.Antialiasing, aa)
+ cmode = self.opts['compositionMode']
+ if cmode is not None:
+ p.setCompositionMode(cmode)
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
if self.fillPath is None:
diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py
index 37245bec..d7ea5100 100644
--- a/pyqtgraph/graphicsItems/PlotDataItem.py
+++ b/pyqtgraph/graphicsItems/PlotDataItem.py
@@ -500,27 +500,10 @@ class PlotDataItem(GraphicsObject):
if self.xData is None:
return (None, None)
- #if self.xClean is None:
- #nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData)
- #if nanMask.any():
- #self.dataMask = ~nanMask
- #self.xClean = self.xData[self.dataMask]
- #self.yClean = self.yData[self.dataMask]
- #else:
- #self.dataMask = None
- #self.xClean = self.xData
- #self.yClean = self.yData
-
if self.xDisp is None:
x = self.xData
y = self.yData
-
- #ds = self.opts['downsample']
- #if isinstance(ds, int) and ds > 1:
- #x = x[::ds]
- ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
- #y = y[::ds]
if self.opts['fftMode']:
x,y = self._fourierTransform(x, y)
# Ignore the first bin for fft data if we have a logx scale
@@ -531,14 +514,6 @@ class PlotDataItem(GraphicsObject):
x = np.log10(x)
if self.opts['logMode'][1]:
y = np.log10(y)
- #if any(self.opts['logMode']): ## re-check for NANs after log
- #nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y)
- #if any(nanMask):
- #self.dataMask = ~nanMask
- #x = x[self.dataMask]
- #y = y[self.dataMask]
- #else:
- #self.dataMask = None
ds = self.opts['downsample']
if not isinstance(ds, int):
@@ -591,8 +566,6 @@ class PlotDataItem(GraphicsObject):
self.xDisp = x
self.yDisp = y
- #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
- #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max()
return self.xDisp, self.yDisp
def dataBounds(self, ax, frac=1.0, orthoRange=None):
@@ -679,10 +652,11 @@ class PlotDataItem(GraphicsObject):
x2 = np.linspace(x[0], x[-1], len(x))
y = np.interp(x2, x, y)
x = x2
- f = np.fft.fft(y) / len(y)
- y = abs(f[1:len(f)/2])
- dt = x[-1] - x[0]
- x = np.linspace(0, 0.5*len(x)/dt, len(y))
+ n = y.size
+ f = np.fft.rfft(y) / n
+ d = float(x[-1]-x[0]) / (len(x)-1)
+ x = np.fft.rfftfreq(n, d)
+ y = np.abs(f)
return x, y
def dataType(obj):
diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
index 41011df3..7321702c 100644
--- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
+++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
@@ -602,6 +602,9 @@ class PlotItem(GraphicsWidget):
#item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged)
+ if self.legend is not None:
+ self.legend.removeItem(item)
+
def clear(self):
"""
Remove all items from the ViewBox.
@@ -646,9 +649,13 @@ class PlotItem(GraphicsWidget):
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.
+
+ If a LegendItem has already been created using this method, that
+ item will be returned rather than creating a new one.
"""
- self.legend = LegendItem(size, offset)
- self.legend.setParentItem(self.vb)
+ if self.legend is None:
+ self.legend = LegendItem(size, offset)
+ self.legend.setParentItem(self.vb)
return self.legend
def scatterPlot(self, *args, **kargs):
diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py
index 963ecb05..9682b6b3 100644
--- a/pyqtgraph/graphicsItems/ROI.py
+++ b/pyqtgraph/graphicsItems/ROI.py
@@ -26,7 +26,8 @@ from .. import getConfigOption
__all__ = [
'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
- 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI',
+ 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI',
+ 'CrosshairROI',
]
@@ -112,7 +113,6 @@ class ROI(GraphicsObject):
sigRemoveRequested = QtCore.Signal(object)
def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False):
- #QObjectWorkaround.__init__(self)
GraphicsObject.__init__(self, parent)
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
pos = Point(pos)
@@ -147,7 +147,6 @@ class ROI(GraphicsObject):
self.translateSnap = translateSnap
self.rotateSnap = rotateSnap
self.scaleSnap = scaleSnap
- #self.setFlag(self.ItemIsSelectable, True)
def getState(self):
return self.stateCopy()
@@ -231,6 +230,9 @@ class ROI(GraphicsObject):
multiple change functions to be called sequentially while minimizing processing overhead
and repeated signals. Setting ``update=False`` also forces ``finish=False``.
"""
+ if update not in (True, False):
+ raise TypeError("update argument must be bool")
+
if y is None:
pos = Point(pos)
else:
@@ -238,6 +240,7 @@ class ROI(GraphicsObject):
if isinstance(y, bool):
raise TypeError("Positional arguments to setPos() must be numerical.")
pos = Point(pos, y)
+
self.state['pos'] = pos
QtGui.QGraphicsItem.setPos(self, pos)
if update:
@@ -247,19 +250,22 @@ class ROI(GraphicsObject):
"""Set the size of the ROI. May be specified as a QPoint, Point, or list of two values.
See setPos() for an explanation of the update and finish arguments.
"""
+ if update not in (True, False):
+ raise TypeError("update argument must be bool")
size = Point(size)
self.prepareGeometryChange()
self.state['size'] = size
if update:
self.stateChanged(finish=finish)
-
+
def setAngle(self, angle, update=True, finish=True):
"""Set the angle of rotation (in degrees) for this ROI.
See setPos() for an explanation of the update and finish arguments.
"""
+ if update not in (True, False):
+ raise TypeError("update argument must be bool")
self.state['angle'] = angle
tr = QtGui.QTransform()
- #tr.rotate(-angle * 180 / np.pi)
tr.rotate(angle)
self.setTransform(tr)
if update:
@@ -307,20 +313,14 @@ class ROI(GraphicsObject):
newState = self.stateCopy()
newState['pos'] = newState['pos'] + pt
- ## snap position
- #snap = kargs.get('snap', None)
- #if (snap is not False) and not (snap is None and self.translateSnap is False):
-
snap = kargs.get('snap', None)
if snap is None:
snap = self.translateSnap
if snap is not False:
newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap)
- #d = ev.scenePos() - self.mapToScene(self.pressPos)
if self.maxBounds is not None:
r = self.stateRect(newState)
- #r0 = self.sceneTransform().mapRect(self.boundingRect())
d = Point(0,0)
if self.maxBounds.left() > r.left():
d[0] = self.maxBounds.left() - r.left()
@@ -332,12 +332,9 @@ class ROI(GraphicsObject):
d[1] = self.maxBounds.bottom() - r.bottom()
newState['pos'] += d
- #self.state['pos'] = newState['pos']
update = kargs.get('update', True)
finish = kargs.get('finish', True)
self.setPos(newState['pos'], update=update, finish=finish)
- #if 'update' not in kargs or kargs['update'] is True:
- #self.stateChanged()
def rotate(self, angle, update=True, finish=True):
"""
@@ -574,7 +571,6 @@ class ROI(GraphicsObject):
## Note: by default, handles are not user-removable even if this method returns True.
return True
-
def getLocalHandlePositions(self, index=None):
"""Returns the position of handles in the ROI's coordinate system.
@@ -620,7 +616,6 @@ class ROI(GraphicsObject):
for h in self.handles:
h['item'].hide()
-
def hoverEvent(self, ev):
hover = False
if not ev.isExit():
@@ -756,11 +751,6 @@ class ROI(GraphicsObject):
else:
raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.")
-
- ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why.
- #p0 = self.mapSceneToParent(p0)
- #p1 = self.mapSceneToParent(p1)
-
## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1)
if 'center' in h:
c = h['center']
@@ -770,8 +760,6 @@ class ROI(GraphicsObject):
if h['type'] == 't':
snap = True if (modifiers & QtCore.Qt.ControlModifier) else None
- #if self.translateSnap or ():
- #snap = Point(self.snapSize, self.snapSize)
self.translate(p1-p0, snap=snap, update=False)
elif h['type'] == 'f':
@@ -779,7 +767,6 @@ class ROI(GraphicsObject):
h['item'].setPos(newPos)
h['pos'] = newPos
self.freeHandleMoved = True
- #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged()
elif h['type'] == 's':
## If a handle and its center have the same x or y value, we can't scale across that axis.
@@ -869,10 +856,8 @@ class ROI(GraphicsObject):
r = self.stateRect(newState)
if not self.maxBounds.contains(r):
return
- #self.setTransform(tr)
self.setPos(newState['pos'], update=False)
self.setAngle(ang, update=False)
- #self.state = newState
## If this is a free-rotate handle, its distance from the center may change.
@@ -897,7 +882,6 @@ class ROI(GraphicsObject):
if ang is None:
return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
- #ang = round(ang / (np.pi/12.)) * (np.pi/12.)
ang = round(ang / 15.) * 15.
hs = abs(h['pos'][scaleAxis] - c[scaleAxis])
@@ -921,10 +905,7 @@ class ROI(GraphicsObject):
r = self.stateRect(newState)
if not self.maxBounds.contains(r):
return
- #self.setTransform(tr)
- #self.setPos(newState['pos'], update=False)
- #self.prepareGeometryChange()
- #self.state = newState
+
self.setState(newState, update=False)
self.stateChanged(finish=finish)
@@ -951,9 +932,6 @@ class ROI(GraphicsObject):
if h['item'] in self.childItems():
p = h['pos']
h['item'].setPos(h['pos'] * self.state['size'])
- #else:
- # trans = self.state['pos']-self.lastState['pos']
- # h['item'].setPos(h['pos'] + h['item'].parentItem().mapFromParent(trans))
self.update()
self.sigRegionChanged.emit(self)
@@ -973,12 +951,10 @@ class ROI(GraphicsObject):
def stateRect(self, state):
r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1])
tr = QtGui.QTransform()
- #tr.rotate(-state['angle'] * 180 / np.pi)
tr.rotate(-state['angle'])
r = tr.mapRect(r)
return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1])
-
def getSnapPosition(self, pos, snap=None):
## Given that pos has been requested, return the nearest snap-to position
## optionally, snap may be passed in to specify a rectangular snap grid.
@@ -998,7 +974,6 @@ class ROI(GraphicsObject):
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
def paint(self, p, opt, widget):
- # p.save()
# Note: don't use self.boundingRect here, because subclasses may need to redefine it.
r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
@@ -1007,7 +982,6 @@ class ROI(GraphicsObject):
p.translate(r.left(), r.top())
p.scale(r.width(), r.height())
p.drawRect(0, 0, 1, 1)
- # p.restore()
def getArraySlice(self, data, img, axes=(0,1), returnSlice=True):
"""Return a tuple of slice objects that can be used to slice the region
@@ -1135,11 +1109,8 @@ class ROI(GraphicsObject):
lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2)
- #pxLen = img.width() / float(data.shape[axes[0]])
##img.width is number of pixels, not width of item.
##need pxWidth and pxHeight instead of pxLen ?
- #sx = pxLen / lvx
- #sy = pxLen / lvy
sx = 1.0 / lvx
sy = 1.0 / lvy
@@ -1169,7 +1140,6 @@ class ROI(GraphicsObject):
if width == 0 or height == 0:
return np.empty((width, height), dtype=float)
- # QImage(width, height, format)
im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
im.fill(0x0)
p = QtGui.QPainter(im)
@@ -1199,27 +1169,6 @@ class ROI(GraphicsObject):
t1 = SRTTransform(relativeTo)
t2 = SRTTransform(st)
return t2/t1
-
-
- #st = self.getState()
-
- ### rotation
- #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358
- #rot = QtGui.QTransform()
- #rot.rotate(-ang)
-
- ### We need to come up with a universal transformation--one that can be applied to other objects
- ### such that all maintain alignment.
- ### More specifically, we need to turn the ROI's position and angle into
- ### a rotation _around the origin_ and a translation.
-
- #p0 = Point(relativeTo['pos'])
-
- ### base position, rotated
- #p1 = rot.map(p0)
-
- #trans = Point(st['pos']) - p1
- #return trans, ang
def applyGlobalTransform(self, tr):
st = self.getState()
@@ -1241,8 +1190,6 @@ class Handle(UIGraphicsItem):
Handles may be dragged to change the position, size, orientation, or other
properties of the ROI they are attached to.
-
-
"""
types = { ## defines number of sides, start angle for each handle type
't': (4, np.pi/4),
@@ -1257,9 +1204,6 @@ class Handle(UIGraphicsItem):
sigRemoveRequested = QtCore.Signal(object) # self
def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False):
- #print " create item with parent", parent
- #self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10)
- #self.setFlags(self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges)
self.rois = []
self.radius = radius
self.typ = typ
@@ -1278,7 +1222,6 @@ class Handle(UIGraphicsItem):
self.deletable = deletable
if deletable:
self.setAcceptedMouseButtons(QtCore.Qt.RightButton)
- #self.updateShape()
self.setZValue(11)
def connectROI(self, roi):
@@ -1287,13 +1230,6 @@ class Handle(UIGraphicsItem):
def disconnectROI(self, roi):
self.rois.remove(roi)
- #for i, r in enumerate(self.roi):
- #if r[0] == roi:
- #self.roi.pop(i)
-
- #def close(self):
- #for r in self.roi:
- #r.removeHandle(self)
def setDeletable(self, b):
self.deletable = b
@@ -1319,21 +1255,12 @@ class Handle(UIGraphicsItem):
else:
self.currentPen = self.pen
self.update()
- #if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
- #self.currentPen = fn.mkPen(255, 255,0)
- #else:
- #self.currentPen = self.pen
- #self.update()
-
-
def mouseClickEvent(self, ev):
## right-click cancels drag
if ev.button() == QtCore.Qt.RightButton and self.isMoving:
self.isMoving = False ## prevents any further motion
self.movePoint(self.startPos, finish=True)
- #for r in self.roi:
- #r[0].cancelMove()
ev.accept()
elif int(ev.button() & self.acceptedMouseButtons()) > 0:
ev.accept()
@@ -1342,12 +1269,6 @@ class Handle(UIGraphicsItem):
self.sigClicked.emit(self, ev)
else:
ev.ignore()
-
- #elif self.deletable:
- #ev.accept()
- #self.raiseContextMenu(ev)
- #else:
- #ev.ignore()
def buildMenu(self):
menu = QtGui.QMenu()
@@ -1418,36 +1339,10 @@ class Handle(UIGraphicsItem):
self.path.lineTo(x, y)
def paint(self, p, opt, widget):
- ### determine rotation of transform
- #m = self.sceneTransform()
- ##mi = m.inverted()[0]
- #v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0))
- #va = np.arctan2(v.y(), v.x())
-
- ### Determine length of unit vector in painter's coords
- ##size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0))
- ##size = (size.x()*size.x() + size.y() * size.y()) ** 0.5
- #size = self.radius
-
- #bounds = QtCore.QRectF(-size, -size, size*2, size*2)
- #if bounds != self.bounds:
- #self.bounds = bounds
- #self.prepareGeometryChange()
p.setRenderHints(p.Antialiasing, True)
p.setPen(self.currentPen)
- #p.rotate(va * 180. / 3.1415926)
- #p.drawPath(self.path)
p.drawPath(self.shape())
- #ang = self.startAng + va
- #dt = 2*np.pi / self.sides
- #for i in range(0, self.sides):
- #x1 = size * cos(ang)
- #y1 = size * sin(ang)
- #x2 = size * cos(ang+dt)
- #y2 = size * sin(ang+dt)
- #ang += dt
- #p.drawLine(Point(x1, y1), Point(x2, y2))
def shape(self):
if self._shape is None:
@@ -1459,18 +1354,10 @@ class Handle(UIGraphicsItem):
return self._shape
def boundingRect(self):
- #print 'roi:', self.roi
s1 = self.shape()
- #print " s1:", s1
- #s2 = self.shape()
- #print " s2:", s2
-
return self.shape().boundingRect()
def generateShape(self):
- ## determine rotation of transform
- #m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene.
- #mi = m.inverted()[0]
dt = self.deviceTransform()
if dt is None:
@@ -1488,22 +1375,15 @@ class Handle(UIGraphicsItem):
return dti.map(tr.map(self.path))
-
def viewTransformChanged(self):
GraphicsObject.viewTransformChanged(self)
self._shape = None ## invalidate shape, recompute later if requested.
self.update()
-
- #def itemChange(self, change, value):
- #if change == self.ItemScenePositionHasChanged:
- #self.updateShape()
class TestROI(ROI):
def __init__(self, pos, size, **args):
- #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1])
ROI.__init__(self, pos, size, **args)
- #self.addTranslateHandle([0, 0])
self.addTranslateHandle([0.5, 0.5])
self.addScaleHandle([1, 1], [0, 0])
self.addScaleHandle([0, 0], [1, 1])
@@ -1513,7 +1393,6 @@ class TestROI(ROI):
self.addRotateHandle([0, 1], [1, 1])
-
class RectROI(ROI):
"""
Rectangular ROI subclass with a single scale handle at the top-right corner.
@@ -1532,14 +1411,12 @@ class RectROI(ROI):
"""
def __init__(self, pos, size, centered=False, sideScalers=False, **args):
- #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
ROI.__init__(self, pos, size, **args)
if centered:
center = [0.5, 0.5]
else:
center = [0, 0]
- #self.addTranslateHandle(center)
self.addScaleHandle([1, 1], center)
if sideScalers:
self.addScaleHandle([1, 0.5], [center[0], 0.5])
@@ -1648,7 +1525,6 @@ class MultiRectROI(QtGui.QGraphicsObject):
rgn = l.getArrayRegion(arr, img, axes=axes, **kwds)
if rgn is None:
continue
- #return None
rgns.append(rgn)
#print l.state['size']
@@ -1733,11 +1609,18 @@ class EllipseROI(ROI):
"""
def __init__(self, pos, size, **args):
- #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
+ self.path = None
ROI.__init__(self, pos, size, **args)
+ self.sigRegionChanged.connect(self._clearPath)
+ self._addHandles()
+
+ def _addHandles(self):
self.addRotateHandle([1.0, 0.5], [0.5, 0.5])
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
+ def _clearPath(self):
+ self.path = None
+
def paint(self, p, opt, widget):
r = self.boundingRect()
p.setRenderHint(QtGui.QPainter.Antialiasing)
@@ -1760,6 +1643,7 @@ class EllipseROI(ROI):
return arr
w = arr.shape[axes[0]]
h = arr.shape[axes[1]]
+
## generate an ellipsoidal mask
mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h))
@@ -1772,8 +1656,27 @@ class EllipseROI(ROI):
return arr * mask
def shape(self):
- self.path = QtGui.QPainterPath()
- self.path.addEllipse(self.boundingRect())
+ if self.path is None:
+ path = QtGui.QPainterPath()
+
+ # Note: Qt has a bug where very small ellipses (radius <0.001) do
+ # not correctly intersect with mouse position (upper-left and
+ # lower-right quadrants are not clickable).
+ #path.addEllipse(self.boundingRect())
+
+ # Workaround: manually draw the path.
+ br = self.boundingRect()
+ center = br.center()
+ r1 = br.width() / 2.
+ r2 = br.height() / 2.
+ theta = np.linspace(0, 2*np.pi, 24)
+ x = center.x() + r1 * np.cos(theta)
+ y = center.y() + r2 * np.sin(theta)
+ path.moveTo(x[0], y[0])
+ for i in range(1, len(x)):
+ path.lineTo(x[i], y[i])
+ self.path = path
+
return self.path
@@ -1790,10 +1693,15 @@ class CircleROI(EllipseROI):
============== =============================================================
"""
- def __init__(self, pos, size, **args):
- ROI.__init__(self, pos, size, **args)
+ def __init__(self, pos, size=None, radius=None, **args):
+ if size is None:
+ if radius is None:
+ raise TypeError("Must provide either size or radius.")
+ size = (radius*2, radius*2)
+ EllipseROI.__init__(self, pos, size, **args)
self.aspectLocked = True
- #self.addTranslateHandle([0.5, 0.5])
+
+ def _addHandles(self):
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
@@ -1804,22 +1712,14 @@ class PolygonROI(ROI):
if pos is None:
pos = [0,0]
ROI.__init__(self, pos, [1,1], **args)
- #ROI.__init__(self, positions[0])
for p in positions:
self.addFreeHandle(p)
self.setZValue(1000)
print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.")
-
def listPoints(self):
return [p['item'].pos() for p in self.handles]
- #def movePoint(self, *args, **kargs):
- #ROI.movePoint(self, *args, **kargs)
- #self.prepareGeometryChange()
- #for h in self.handles:
- #h['pos'] = h['item'].pos()
-
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
@@ -1846,7 +1746,6 @@ class PolygonROI(ROI):
sc['pos'] = Point(self.state['pos'])
sc['size'] = Point(self.state['size'])
sc['angle'] = self.state['angle']
- #sc['handles'] = self.handles
return sc
@@ -2066,13 +1965,16 @@ class LineSegmentROI(ROI):
pos = [0,0]
ROI.__init__(self, pos, [1,1], **args)
- #ROI.__init__(self, positions[0])
if len(positions) > 2:
raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.")
- self.endpoints = []
for i, p in enumerate(positions):
- self.endpoints.append(self.addFreeHandle(p, item=handles[i]))
+ self.addFreeHandle(p, item=handles[i])
+
+ @property
+ def endpoints(self):
+ # must not be cached because self.handles may change.
+ return [h['item'] for h in self.handles]
def listPoints(self):
return [p['item'].pos() for p in self.handles]
@@ -2119,7 +2021,6 @@ class LineSegmentROI(ROI):
See ROI.getArrayRegion() for a description of the arguments.
"""
-
imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints]
rgns = []
coords = []
@@ -2157,85 +2058,11 @@ class _PolyLineSegment(LineSegmentROI):
return LineSegmentROI.hoverEvent(self, ev)
-class SpiralROI(ROI):
- def __init__(self, pos=None, size=None, **args):
- if size == None:
- size = [100e-6,100e-6]
- if pos == None:
- pos = [0,0]
- ROI.__init__(self, pos, size, **args)
- self.translateSnap = False
- self.addFreeHandle([0.25,0], name='a')
- self.addRotateFreeHandle([1,0], [0,0], name='r')
- #self.getRadius()
- #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self.
-
-
- def getRadius(self):
- radius = Point(self.handles[1]['item'].pos()).length()
- #r2 = radius[1]
- #r3 = r2[0]
- return radius
-
- def boundingRect(self):
- r = self.getRadius()
- return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r)
- #return self.bounds
-
- #def movePoint(self, *args, **kargs):
- #ROI.movePoint(self, *args, **kargs)
- #self.prepareGeometryChange()
- #for h in self.handles:
- #h['pos'] = h['item'].pos()/self.state['size'][0]
-
- def stateChanged(self, finish=True):
- ROI.stateChanged(self, finish=finish)
- if len(self.handles) > 1:
- self.path = QtGui.QPainterPath()
- h0 = Point(self.handles[0]['item'].pos()).length()
- a = h0/(2.0*np.pi)
- theta = 30.0*(2.0*np.pi)/360.0
- self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta)))
- x0 = a*theta*cos(theta)
- y0 = a*theta*sin(theta)
- radius = self.getRadius()
- theta += 20.0*(2.0*np.pi)/360.0
- i = 0
- while Point(x0, y0).length() < radius and i < 1000:
- x1 = a*theta*cos(theta)
- y1 = a*theta*sin(theta)
- self.path.lineTo(QtCore.QPointF(x1,y1))
- theta += 20.0*(2.0*np.pi)/360.0
- x0 = x1
- y0 = y1
- i += 1
-
-
- return self.path
-
-
- def shape(self):
- p = QtGui.QPainterPath()
- p.addEllipse(self.boundingRect())
- return p
-
- def paint(self, p, *args):
- p.setRenderHint(QtGui.QPainter.Antialiasing)
- #path = self.shape()
- p.setPen(self.currentPen)
- p.drawPath(self.path)
- p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
- p.drawPath(self.shape())
- p.setPen(QtGui.QPen(QtGui.QColor(0,0,255)))
- p.drawRect(self.boundingRect())
-
-
class CrosshairROI(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
def __init__(self, pos=None, size=None, **kargs):
if size == None:
- #size = [100e-6,100e-6]
size=[1,1]
if pos == None:
pos = [0,0]
@@ -2251,16 +2078,8 @@ class CrosshairROI(ROI):
self.prepareGeometryChange()
def boundingRect(self):
- #size = self.size()
- #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
return self.shape().boundingRect()
- #def getRect(self):
- ### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses
- #size = self.size()
- #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
-
-
def shape(self):
if self._shape is None:
radius = self.getState()['size'][1]
@@ -2274,58 +2093,43 @@ class CrosshairROI(ROI):
stroker.setWidth(10)
outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline)
-
-
- ##h1 = self.handles[0]['item'].pos()
- ##h2 = self.handles[1]['item'].pos()
- #w1 = Point(-0.5, 0)*self.size()
- #w2 = Point(0.5, 0)*self.size()
- #h1 = Point(0, -0.5)*self.size()
- #h2 = Point(0, 0.5)*self.size()
-
- #dh = h2-h1
- #dw = w2-w1
- #if dh.length() == 0 or dw.length() == 0:
- #return p
- #pxv = self.pixelVectors(dh)[1]
- #if pxv is None:
- #return p
-
- #pxv *= 4
-
- #p.moveTo(h1+pxv)
- #p.lineTo(h2+pxv)
- #p.lineTo(h2-pxv)
- #p.lineTo(h1-pxv)
- #p.lineTo(h1+pxv)
-
- #pxv = self.pixelVectors(dw)[1]
- #if pxv is None:
- #return p
-
- #pxv *= 4
-
- #p.moveTo(w1+pxv)
- #p.lineTo(w2+pxv)
- #p.lineTo(w2-pxv)
- #p.lineTo(w1-pxv)
- #p.lineTo(w1+pxv)
return self._shape
def paint(self, p, *args):
- #p.save()
- #r = self.getRect()
radius = self.getState()['size'][1]
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
- #p.translate(r.left(), r.top())
- #p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5
- #p.drawLine(0,5, 10,5)
- #p.drawLine(5,0, 5,10)
- #p.restore()
p.drawLine(Point(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0))
+class RulerROI(LineSegmentROI):
+ def paint(self, p, *args):
+ LineSegmentROI.paint(self, p, *args)
+ h1 = self.handles[0]['item'].pos()
+ h2 = self.handles[1]['item'].pos()
+ p1 = p.transform().map(h1)
+ p2 = p.transform().map(h2)
+
+ vec = Point(h2) - Point(h1)
+ length = vec.length()
+ angle = vec.angle(Point(1, 0))
+
+ pvec = p2 - p1
+ pvecT = Point(pvec.y(), -pvec.x())
+ pos = 0.5 * (p1 + p2) + pvecT * 40 / pvecT.length()
+
+ p.resetTransform()
+
+ txt = fn.siFormat(length, suffix='m') + '\n%0.1f deg' % angle
+ p.drawText(QtCore.QRectF(pos.x()-50, pos.y()-50, 100, 100), QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, txt)
+
+ def boundingRect(self):
+ r = LineSegmentROI.boundingRect(self)
+ pxl = self.pixelLength(Point([1, 0]))
+ if pxl is None:
+ return r
+ pxw = 50 * pxl
+ return r.adjusted(-50, -50, 50, 50)
diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py
index 54667b50..30e6cf89 100644
--- a/pyqtgraph/graphicsItems/ScatterPlotItem.py
+++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py
@@ -126,7 +126,7 @@ class SymbolAtlas(object):
keyi = None
sourceRecti = None
for i, rec in enumerate(opts):
- key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
+ key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
if key == keyi:
sourceRect[i] = sourceRecti
else:
@@ -136,6 +136,7 @@ class SymbolAtlas(object):
newRectSrc = QtCore.QRectF()
newRectSrc.pen = rec['pen']
newRectSrc.brush = rec['brush']
+ newRectSrc.symbol = rec[3]
self.symbolMap[key] = newRectSrc
self.atlasValid = False
sourceRect[i] = newRectSrc
@@ -151,7 +152,7 @@ class SymbolAtlas(object):
images = []
for key, sourceRect in self.symbolMap.items():
if sourceRect.width() == 0:
- img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush)
+ img = renderSymbol(sourceRect.symbol, key[1], sourceRect.pen, sourceRect.brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False)
else:
@@ -251,6 +252,7 @@ class ScatterPlotItem(GraphicsObject):
'pxMode': True,
'useCache': True, ## If useCache is False, symbols are re-drawn on every paint.
'antialias': getConfigOption('antialias'),
+ 'compositionMode': None,
'name': None,
}
@@ -299,6 +301,8 @@ class ScatterPlotItem(GraphicsObject):
*antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are
always rendered with antialiasing (since the rendered symbols can be cached, this
incurs very little performance cost)
+ *compositionMode* If specified, this sets the composition mode used when drawing the
+ scatter plot (see QPainter::CompositionMode in the Qt documentation).
*name* The name of this item. Names are used for automatically
generating LegendItem entries and by some exporters.
====================== ===============================================================================================
@@ -730,7 +734,9 @@ class ScatterPlotItem(GraphicsObject):
@debug.warnOnException ## raising an exception here causes crash
def paint(self, p, *args):
-
+ cmode = self.opts.get('compositionMode', None)
+ if cmode is not None:
+ p.setCompositionMode(cmode)
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect())
@@ -851,11 +857,18 @@ class SpotItem(object):
def __init__(self, data, plot):
#GraphicsItem.__init__(self, register=False)
self._data = data
- self._plot = plot
+ # SpotItems are kept in plot.data["items"] numpy object array which
+ # does not support cyclic garbage collection (numpy issue 6581).
+ # Keeping a strong ref to plot here would leak the cycle
+ self.__plot_ref = weakref.ref(plot)
#self.setParentItem(plot)
#self.setPos(QtCore.QPointF(data['x'], data['y']))
#self.updateItem()
+ @property
+ def _plot(self):
+ return self.__plot_ref()
+
def data(self):
"""Return the user data associated with this spot."""
return self._data['data']
diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py
new file mode 100644
index 00000000..114c9e6e
--- /dev/null
+++ b/pyqtgraph/graphicsItems/TargetItem.py
@@ -0,0 +1,125 @@
+from ..Qt import QtGui, QtCore
+import numpy as np
+from ..Point import Point
+from .. import functions as fn
+from .GraphicsObject import GraphicsObject
+from .TextItem import TextItem
+
+
+class TargetItem(GraphicsObject):
+ """Draws a draggable target symbol (circle plus crosshair).
+
+ The size of TargetItem will remain fixed on screen even as the view is zoomed.
+ Includes an optional text label.
+ """
+ sigDragged = QtCore.Signal(object)
+
+ def __init__(self, movable=True, radii=(5, 10, 10), pen=(255, 255, 0), brush=(0, 0, 255, 100)):
+ GraphicsObject.__init__(self)
+ self._bounds = None
+ self._radii = radii
+ self._picture = None
+ self.movable = movable
+ self.moving = False
+ self.label = None
+ self.labelAngle = 0
+ self.pen = fn.mkPen(pen)
+ self.brush = fn.mkBrush(brush)
+
+ def setLabel(self, label):
+ if label is None:
+ if self.label is not None:
+ self.label.scene().removeItem(self.label)
+ self.label = None
+ else:
+ if self.label is None:
+ self.label = TextItem()
+ self.label.setParentItem(self)
+ self.label.setText(label)
+ self._updateLabel()
+
+ def setLabelAngle(self, angle):
+ self.labelAngle = angle
+ self._updateLabel()
+
+ def boundingRect(self):
+ if self._picture is None:
+ self._drawPicture()
+ return self._bounds
+
+ def dataBounds(self, axis, frac=1.0, orthoRange=None):
+ return [0, 0]
+
+ def viewTransformChanged(self):
+ self._picture = None
+ self.prepareGeometryChange()
+ self._updateLabel()
+
+ def _updateLabel(self):
+ if self.label is None:
+ return
+
+ # find an optimal location for text at the given angle
+ angle = self.labelAngle * np.pi / 180.
+ lbr = self.label.boundingRect()
+ center = lbr.center()
+ a = abs(np.sin(angle) * lbr.height()*0.5)
+ b = abs(np.cos(angle) * lbr.width()*0.5)
+ r = max(self._radii) + 2 + max(a, b)
+ pos = self.mapFromScene(self.mapToScene(QtCore.QPointF(0, 0)) + r * QtCore.QPointF(np.cos(angle), -np.sin(angle)) - center)
+ self.label.setPos(pos)
+
+ def paint(self, p, *args):
+ if self._picture is None:
+ self._drawPicture()
+ self._picture.play(p)
+
+ def _drawPicture(self):
+ self._picture = QtGui.QPicture()
+ p = QtGui.QPainter(self._picture)
+ p.setRenderHint(p.Antialiasing)
+
+ # Note: could do this with self.pixelLength, but this is faster.
+ o = self.mapToScene(QtCore.QPointF(0, 0))
+ px = abs(1.0 / (self.mapToScene(QtCore.QPointF(1, 0)) - o).x())
+ py = abs(1.0 / (self.mapToScene(QtCore.QPointF(0, 1)) - o).y())
+
+ r, w, h = self._radii
+ w = w * px
+ h = h * py
+ rx = r * px
+ ry = r * py
+ rect = QtCore.QRectF(-rx, -ry, rx*2, ry*2)
+ p.setPen(self.pen)
+ p.setBrush(self.brush)
+ p.drawEllipse(rect)
+ p.drawLine(Point(-w, 0), Point(w, 0))
+ p.drawLine(Point(0, -h), Point(0, h))
+ p.end()
+
+ bx = max(w, rx)
+ by = max(h, ry)
+ self._bounds = QtCore.QRectF(-bx, -by, bx*2, by*2)
+
+ def mouseDragEvent(self, ev):
+ if not self.movable:
+ return
+ if ev.button() == QtCore.Qt.LeftButton:
+ if ev.isStart():
+ self.moving = True
+ self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
+ self.startPosition = self.pos()
+ ev.accept()
+
+ if not self.moving:
+ return
+
+ self.setPos(self.cursorOffset + self.mapToParent(ev.pos()))
+ if ev.isFinish():
+ self.moving = False
+ self.sigDragged.emit(self)
+
+ def hoverEvent(self, ev):
+ if self.movable:
+ ev.acceptDrags(QtCore.Qt.LeftButton)
+
diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py
index 1db4a4a2..2b4f256f 100644
--- a/pyqtgraph/graphicsItems/VTickGroup.py
+++ b/pyqtgraph/graphicsItems/VTickGroup.py
@@ -90,7 +90,7 @@ class VTickGroup(UIGraphicsItem):
br = self.boundingRect()
h = br.height()
br.setY(br.y() + self.yrange[0] * h)
- br.setHeight(h - (1.0-self.yrange[1]) * h)
+ br.setHeight((self.yrange[1] - self.yrange[0]) * h)
p.translate(0, br.y())
p.scale(1.0, br.height())
p.setPen(self.pen)
diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
index 4cab8662..bd2a4c45 100644
--- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
+++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
@@ -85,7 +85,6 @@ class ViewBox(GraphicsWidget):
sigXRangeChanged = QtCore.Signal(object, object)
sigRangeChangedManually = QtCore.Signal(object)
sigRangeChanged = QtCore.Signal(object, object)
- #sigActionPositionChanged = QtCore.Signal(object)
sigStateChanged = QtCore.Signal(object)
sigTransformChanged = QtCore.Signal(object)
sigResized = QtCore.Signal(object)
@@ -128,8 +127,6 @@ class ViewBox(GraphicsWidget):
self.name = None
self.linksBlocked = False
self.addedItems = []
- #self.gView = view
- #self.showGrid = showGrid
self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred
self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
@@ -188,9 +185,6 @@ class ViewBox(GraphicsWidget):
self.background.setPen(fn.mkPen(None))
self.updateBackground()
- #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
- # this also enables capture of keyPressEvents.
-
## 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))
@@ -239,7 +233,6 @@ class ViewBox(GraphicsWidget):
ViewBox.updateAllViewLists()
sid = id(self)
self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None)
- #self.destroyed.connect(self.unregister)
def unregister(self):
"""
@@ -288,16 +281,12 @@ class ViewBox(GraphicsWidget):
self.prepareForPaint()
self._lastScene = scene
-
-
-
def prepareForPaint(self):
#autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
# don't check whether auto range is enabled here--only check when setting dirty flag.
if self._autoRangeNeedsUpdate: # and autoRangeEnabled:
self.updateAutoRange()
- if self._matrixNeedsUpdate:
- self.updateMatrix()
+ self.updateMatrix()
def getState(self, copy=True):
"""Return the current state of the ViewBox.
@@ -326,7 +315,6 @@ class ViewBox(GraphicsWidget):
del state['linkedViews']
self.state.update(state)
- #self.updateMatrix()
self.updateViewRange()
self.sigStateChanged.emit(self)
@@ -353,12 +341,6 @@ class ViewBox(GraphicsWidget):
self.state['mouseMode'] = mode
self.sigStateChanged.emit(self)
- #def toggleLeftAction(self, act): ## for backward compatibility
- #if act.text() is 'pan':
- #self.setLeftButtonAction('pan')
- #elif act.text() is 'zoom':
- #self.setLeftButtonAction('rect')
-
def setLeftButtonAction(self, mode='rect'): ## for backward compatibility
if mode.lower() == 'rect':
self.setMouseMode(ViewBox.RectMode)
@@ -405,7 +387,6 @@ class ViewBox(GraphicsWidget):
if not ignoreBounds:
self.addedItems.append(item)
self.updateAutoRange()
- #print "addItem:", item, item.boundingRect()
def removeItem(self, item):
"""Remove an item from this view."""
@@ -423,6 +404,7 @@ class ViewBox(GraphicsWidget):
ch.setParentItem(None)
def resizeEvent(self, ev):
+ self._matrixNeedsUpdate = True
self.linkedXChanged()
self.linkedYChanged()
self.updateAutoRange()
@@ -562,10 +544,6 @@ class ViewBox(GraphicsWidget):
# If nothing has changed, we are done.
if any(changed):
- #if update and self.matrixNeedsUpdate:
- #self.updateMatrix(changed)
- #return
-
self.sigStateChanged.emit(self)
# Update target rect for debugging
@@ -576,26 +554,9 @@ class ViewBox(GraphicsWidget):
# Note that aspect ratio constraints and auto-visible probably do not work together..
if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
self._autoRangeNeedsUpdate = True
- #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated?
elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
self._autoRangeNeedsUpdate = True
- #self.updateAutoRange()
-
- ## Update view matrix only if requested
- #if update:
- #self.updateMatrix(changed)
- ## Otherwise, indicate that the matrix needs to be updated
- #else:
- #self.matrixNeedsUpdate = True
-
- ## Inform linked views that the range has changed <>
- #for ax, range in changes.items():
- #link = self.linkedView(ax)
- #if link is not None:
- #link.linkedViewChanged(self, ax)
-
-
def setYRange(self, min, max, padding=None, update=True):
"""
Set the visible Y range of the view to [*min*, *max*].
@@ -675,10 +636,6 @@ class ViewBox(GraphicsWidget):
for kwd in kwds:
if kwd not in allowed:
raise ValueError("Invalid keyword argument '%s'." % kwd)
- #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
- #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
- #self.state['limits'][kwd] = kwds[kwd]
- #update = True
for axis in [0,1]:
for mnmx in [0,1]:
kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx]
@@ -694,9 +651,6 @@ class ViewBox(GraphicsWidget):
if update:
self.updateViewRange()
-
-
-
def scaleBy(self, s=None, center=None, x=None, y=None):
"""
@@ -762,8 +716,6 @@ class ViewBox(GraphicsWidget):
y = vr.top()+y, vr.bottom()+y
if x is not None or y is not None:
self.setRange(xRange=x, yRange=y, padding=0)
-
-
def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
"""
@@ -773,11 +725,6 @@ class ViewBox(GraphicsWidget):
The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should
be visible (this only works with items implementing a dataRange method, such as PlotDataItem).
"""
- #print "autorange:", axis, enable
- #if not enable:
- #import traceback
- #traceback.print_stack()
-
# support simpler interface:
if x is not None or y is not None:
if x is not None:
@@ -813,10 +760,6 @@ class ViewBox(GraphicsWidget):
self.state['autoRange'][ax] = enable
self._autoRangeNeedsUpdate |= (enable is not False)
self.update()
-
-
- #if needAutoRangeUpdate:
- # self.updateAutoRange()
self.sigStateChanged.emit(self)
@@ -828,6 +771,8 @@ class ViewBox(GraphicsWidget):
return self.state['autoRange'][:]
def setAutoPan(self, x=None, y=None):
+ """Set whether automatic range will only pan (not scale) the view.
+ """
if x is not None:
self.state['autoPan'][0] = x
if y is not None:
@@ -836,6 +781,9 @@ class ViewBox(GraphicsWidget):
self.updateAutoRange()
def setAutoVisible(self, x=None, y=None):
+ """Set whether automatic range uses only visible data when determining
+ the range to show.
+ """
if x is not None:
self.state['autoVisibleOnly'][0] = x
if x is True:
@@ -924,7 +872,6 @@ class ViewBox(GraphicsWidget):
"""Link this view's Y axis to another view. (see LinkView)"""
self.linkView(self.YAxis, view)
-
def linkView(self, axis, view):
"""
Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis.
@@ -1118,7 +1065,6 @@ class ViewBox(GraphicsWidget):
return
self.state['aspectLocked'] = ratio
if ratio != currentRatio: ## If this would change the current range, do that now
- #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
self.updateViewRange()
self.updateAutoRange()
@@ -1130,45 +1076,49 @@ class ViewBox(GraphicsWidget):
Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
(This maps from inside the viewbox to outside)
"""
- if self._matrixNeedsUpdate:
- self.updateMatrix()
+ self.updateMatrix()
m = self.childGroup.transform()
- #m1 = QtGui.QTransform()
- #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
- return m #*m1
+ return m
def mapToView(self, obj):
"""Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
+ self.updateMatrix()
m = fn.invertQTransform(self.childTransform())
return m.map(obj)
def mapFromView(self, obj):
"""Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox"""
+ self.updateMatrix()
m = self.childTransform()
return m.map(obj)
def mapSceneToView(self, obj):
"""Maps from scene coordinates to the coordinate system displayed inside the ViewBox"""
+ self.updateMatrix()
return self.mapToView(self.mapFromScene(obj))
def mapViewToScene(self, obj):
"""Maps from the coordinate system displayed inside the ViewBox to scene coordinates"""
+ self.updateMatrix()
return self.mapToScene(self.mapFromView(obj))
def mapFromItemToView(self, item, obj):
"""Maps *obj* from the local coordinate system of *item* to the view coordinates"""
+ self.updateMatrix()
return self.childGroup.mapFromItem(item, obj)
#return self.mapSceneToView(item.mapToScene(obj))
def mapFromViewToItem(self, item, obj):
"""Maps *obj* from view coordinates to the local coordinate system of *item*."""
+ self.updateMatrix()
return self.childGroup.mapToItem(item, obj)
- #return item.mapFromScene(self.mapViewToScene(obj))
def mapViewToDevice(self, obj):
+ self.updateMatrix()
return self.mapToDevice(self.mapFromView(obj))
def mapDeviceToView(self, obj):
+ self.updateMatrix()
return self.mapToView(self.mapFromDevice(obj))
def viewPixelSize(self):
@@ -1177,25 +1127,9 @@ class ViewBox(GraphicsWidget):
px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()]
return (px.length(), py.length())
-
def itemBoundingRect(self, item):
"""Return the bounding rect of the item in view coordinates"""
return self.mapSceneToView(item.sceneBoundingRect()).boundingRect()
-
- #def viewScale(self):
- #vr = self.viewRect()
- ##print "viewScale:", self.range
- #xd = vr.width()
- #yd = vr.height()
- #if xd == 0 or yd == 0:
- #print "Warning: 0 range in view:", xd, yd
- #return np.array([1,1])
-
- ##cs = self.canvas().size()
- #cs = self.boundingRect()
- #scale = np.array([cs.width() / xd, cs.height() / yd])
- ##print "view scale:", scale
- #return scale
def wheelEvent(self, ev, axis=None):
mask = np.array(self.state['mouseEnabled'], dtype=np.float)
@@ -1206,13 +1140,11 @@ class ViewBox(GraphicsWidget):
s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
- #center = ev.pos()
self._resetTarget()
self.scaleBy(s, center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
ev.accept()
-
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton and self.menuEnabled():
@@ -1251,7 +1183,6 @@ class ViewBox(GraphicsWidget):
if ev.isFinish(): ## This is the final move in the drag; change the view scale now
#print "finish"
self.rbScaleBox.hide()
- #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos))
ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos))
ax = self.childGroup.mapRectFromParent(ax)
self.showAxRect(ax)
@@ -1301,12 +1232,6 @@ class ViewBox(GraphicsWidget):
ctrl-- : moves backward in the zooming stack (if it exists)
"""
- #print ev.key()
- #print 'I intercepted a key press, but did not accept it'
-
- ## not implemented yet ?
- #self.keypress.sigkeyPressEvent.emit()
-
ev.accept()
if ev.text() == '-':
self.scaleHistory(-1)
@@ -1324,7 +1249,6 @@ class ViewBox(GraphicsWidget):
if ptr != self.axHistoryPointer:
self.axHistoryPointer = ptr
self.showAxRect(self.axHistory[ptr])
-
def updateScaleBox(self, p1, p2):
r = QtCore.QRectF(p1, p2)
@@ -1338,14 +1262,6 @@ class ViewBox(GraphicsWidget):
self.setRange(ax.normalized()) # be sure w, h are correct coordinates
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
- #def mouseRect(self):
- #vs = self.viewScale()
- #vr = self.state['viewRange']
- ## Convert positions from screen (view) pixel coordinates to axis coordinates
- #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]),
- #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1])
- #return(ax)
-
def allChildren(self, item=None):
"""Return a list of all children and grandchildren of this ViewBox"""
if item is None:
@@ -1356,8 +1272,6 @@ class ViewBox(GraphicsWidget):
children.extend(self.allChildren(ch))
return children
-
-
def childrenBounds(self, frac=None, orthoRange=(None,None), items=None):
"""Return the bounding range of all children.
[[xmin, xmax], [ymin, ymax]]
@@ -1373,15 +1287,13 @@ class ViewBox(GraphicsWidget):
## First collect all boundary information
itemBounds = []
for item in items:
- if not item.isVisible():
+ if not item.isVisible() or not item.scene() is self.scene():
continue
useX = True
useY = True
if hasattr(item, 'dataBounds'):
- #bounds = self._itemBoundsCache.get(item, None)
- #if bounds is None:
if frac is None:
frac = (1.0, 1.0)
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
@@ -1414,9 +1326,6 @@ class ViewBox(GraphicsWidget):
itemBounds.append((bounds, useX, useY, pxPad))
- #self._itemBoundsCache[item] = (bounds, useX, useY)
- #else:
- #bounds, useX, useY = bounds
else:
if int(item.flags() & item.ItemHasNoContents) > 0:
continue
@@ -1425,8 +1334,6 @@ class ViewBox(GraphicsWidget):
bounds = self.mapFromItemToView(item, bounds).boundingRect()
itemBounds.append((bounds, True, True, 0))
- #print itemBounds
-
## determine tentative new range
range = [None, None]
for bounds, useX, useY, px in itemBounds:
@@ -1442,14 +1349,11 @@ class ViewBox(GraphicsWidget):
range[0] = [bounds.left(), bounds.right()]
profiler()
- #print "range", range
-
## Now expand any bounds that have a pixel margin
## This must be done _after_ we have a good estimate of the new range
## to ensure that the pixel size is roughly accurate.
w = self.width()
h = self.height()
- #print "w:", w, "h:", h
if w > 0 and range[0] is not None:
pxSize = (range[0][1] - range[0][0]) / w
for bounds, useX, useY, px in itemBounds:
@@ -1585,9 +1489,9 @@ class ViewBox(GraphicsWidget):
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
if any(changed):
+ self._matrixNeedsUpdate = True
self.sigRangeChanged.emit(self, self.state['viewRange'])
self.update()
- self._matrixNeedsUpdate = True
# Inform linked views that the range has changed
for ax in [0, 1]:
@@ -1598,6 +1502,9 @@ class ViewBox(GraphicsWidget):
link.linkedViewChanged(self, ax)
def updateMatrix(self, changed=None):
+ if not self._matrixNeedsUpdate:
+ return
+
## Make the childGroup's transform match the requested viewRange.
bounds = self.rect()
@@ -1648,7 +1555,6 @@ class ViewBox(GraphicsWidget):
self.background.show()
self.background.setBrush(fn.mkBrush(bg))
-
def updateViewLists(self):
try:
self.window()
@@ -1662,7 +1568,6 @@ class ViewBox(GraphicsWidget):
## make a sorted list of all named views
nv = list(ViewBox.NamedViews.values())
- #print "new view list:", nv
sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList
if self in nv:
@@ -1676,16 +1581,11 @@ class ViewBox(GraphicsWidget):
for v in nv:
if link == v.name:
self.linkView(ax, v)
- #print "New view list:", nv
- #print "linked views:", self.state['linkedViews']
@staticmethod
def updateAllViewLists():
- #print "Update:", ViewBox.AllViews.keys()
- #print "Update:", ViewBox.NamedViews.keys()
for v in ViewBox.AllViews:
v.updateViewLists()
-
@staticmethod
def forgetView(vid, name):
@@ -1766,4 +1666,5 @@ class ViewBox(GraphicsWidget):
self.scene().removeItem(self.locateGroup)
self.locateGroup = None
+
from .ViewBoxMenu import ViewBoxMenu
diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py
new file mode 100644
index 00000000..dc13bb7a
--- /dev/null
+++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py
@@ -0,0 +1,25 @@
+import numpy as np
+import pyqtgraph as pg
+
+pg.mkQApp()
+
+
+def test_fft():
+ f = 20.
+ x = np.linspace(0, 1, 1000)
+ y = np.sin(2 * np.pi * f * x)
+ pd = pg.PlotDataItem(x, y)
+ pd.setFftMode(True)
+ x, y = pd.getData()
+ assert abs(x[np.argmax(y)] - f) < 0.03
+
+ x = np.linspace(0, 1, 1001)
+ y = np.sin(2 * np.pi * f * x)
+ pd.setData(x, y)
+ x, y = pd.getData()
+ assert abs(x[np.argmax(y)]- f) < 0.03
+
+ pd.setLogMode(True, False)
+ x, y = pd.getData()
+ assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01
+
\ No newline at end of file
diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py
index ddc7f173..8cc2efd5 100644
--- a/pyqtgraph/graphicsItems/tests/test_ROI.py
+++ b/pyqtgraph/graphicsItems/tests/test_ROI.py
@@ -208,15 +208,23 @@ def test_PolyLineROI():
# click segment
mouseClick(plt, pt, QtCore.Qt.LeftButton)
assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.')
+
+ # drag new handle
+ mouseMove(plt, pt+pg.Point(10, -10)) # pg bug: have to move the mouse off/on again to register hover
+ mouseDrag(plt, pt, pt + pg.Point(10, -10), QtCore.Qt.LeftButton)
+ assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_new_handle', 'Drag mouse over created handle.')
+ # clear all points
r.clearPoints()
assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.')
assert len(r.getState()['points']) == 0
+ # call setPoints
r.setPoints(initState['points'])
assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.')
assert len(r.getState()['points']) == 3
+ # call setState
r.setState(initState)
assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.')
assert len(r.getState()['points']) == 3
diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py
index acf6ad72..ba1fb9d7 100644
--- a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py
+++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py
@@ -1,3 +1,4 @@
+from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
app = pg.mkQApp()
@@ -7,9 +8,16 @@ app.processEvents()
def test_scatterplotitem():
plot = pg.PlotWidget()
- # set view range equal to its bounding rect.
+ # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect())
+
+ # test SymbolAtlas accepts custom symbol
+ s = pg.ScatterPlotItem()
+ symbol = QtGui.QPainterPath()
+ symbol.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
+ s.addPoints([{'pos': [0,0], 'data': 1, 'symbol': symbol}])
+
for i, pxMode in enumerate([True, False]):
for j, useCache in enumerate([True, False]):
s = pg.ScatterPlotItem()
@@ -17,14 +25,14 @@ def test_scatterplotitem():
plot.addItem(s)
s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode)
s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
-
+
# Test uniform spot updates
s.setSize(10)
s.setBrush('r')
s.setPen('g')
s.setSymbol('+')
app.processEvents()
-
+
# Test list spot updates
s.setSize([10] * 6)
s.setBrush([pg.mkBrush('r')] * 6)
@@ -55,7 +63,7 @@ def test_scatterplotitem():
def test_init_spots():
plot = pg.PlotWidget()
- # set view range equal to its bounding rect.
+ # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect())
spots = [
@@ -63,28 +71,28 @@ def test_init_spots():
{'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'},
]
s = pg.ScatterPlotItem(spots=spots)
-
+
# Check we can display without errors
plot.addItem(s)
app.processEvents()
plot.clear()
-
+
# check data is correct
spots = s.points()
-
+
defPen = pg.mkPen(pg.getConfigOption('foreground'))
assert spots[0].pos().x() == 0
assert spots[0].pos().y() == 1
assert spots[0].pen() == defPen
assert spots[0].data() is None
-
+
assert spots[1].pos().x() == 1
assert spots[1].pos().y() == 2
assert spots[1].pen() == pg.mkPen(None)
assert spots[1].brush() == pg.mkBrush(None)
assert spots[1].data() == 'zzz'
-
+
if __name__ == '__main__':
test_scatterplotitem()
diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py
index 1aa3f3f4..b6598685 100644
--- a/pyqtgraph/graphicsWindows.py
+++ b/pyqtgraph/graphicsWindows.py
@@ -1,25 +1,22 @@
# -*- coding: utf-8 -*-
"""
-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.
+DEPRECATED: The classes below are convenience classes that create a new window
+containting a single, specific widget. These classes are now unnecessary because
+it is possible to place any widget into its own window by simply calling its
+show() method.
"""
-from .Qt import QtCore, QtGui
+from .Qt import QtCore, QtGui, mkQApp
from .widgets.PlotWidget import *
from .imageview import *
from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget
from .widgets.GraphicsView import GraphicsView
-QAPP = None
-
-def mkQApp():
- if QtGui.QApplication.instance() is None:
- global QAPP
- QAPP = QtGui.QApplication([])
class GraphicsWindow(GraphicsLayoutWidget):
"""
+ (deprecated; use GraphicsLayoutWidget instead)
+
Convenience subclass of :class:`GraphicsLayoutWidget
`. This class is intended for use from
the interactive python prompt.
@@ -34,6 +31,9 @@ class GraphicsWindow(GraphicsLayoutWidget):
class TabWindow(QtGui.QMainWindow):
+ """
+ (deprecated)
+ """
def __init__(self, title=None, size=(800,600)):
mkQApp()
QtGui.QMainWindow.__init__(self)
@@ -52,6 +52,9 @@ class TabWindow(QtGui.QMainWindow):
class PlotWindow(PlotWidget):
+ """
+ (deprecated; use PlotWidget instead)
+ """
def __init__(self, title=None, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
@@ -65,6 +68,9 @@ class PlotWindow(PlotWidget):
class ImageWindow(ImageView):
+ """
+ (deprecated; use ImageView instead)
+ """
def __init__(self, *args, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py
index 5cc00f68..c64953de 100644
--- a/pyqtgraph/imageview/ImageView.py
+++ b/pyqtgraph/imageview/ImageView.py
@@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features:
- ROI plotting
- Image normalization through a variety of methods
"""
-import os
+import os, sys
import numpy as np
from ..Qt import QtCore, QtGui, USE_PYSIDE
@@ -26,6 +26,7 @@ from ..graphicsItems.ROI import *
from ..graphicsItems.LinearRegionItem import *
from ..graphicsItems.InfiniteLine import *
from ..graphicsItems.ViewBox import *
+from ..graphicsItems.VTickGroup import VTickGroup
from ..graphicsItems.GradientEditorItem import addGradientListToDocstring
from .. import ptime as ptime
from .. import debug as debug
@@ -79,7 +80,8 @@ class ImageView(QtGui.QWidget):
sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object)
- def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args):
+ def __init__(self, parent=None, name="ImageView", view=None, imageItem=None,
+ levelMode='mono', *args):
"""
By default, this class creates an :class:`ImageItem ` to display image data
and a :class:`ViewBox ` to contain the ImageItem.
@@ -101,6 +103,9 @@ class ImageView(QtGui.QWidget):
imageItem (ImageItem) If specified, this object will be used to
display the image. Must be an instance of ImageItem
or other compatible object.
+ levelMode See the *levelMode* argument to
+ :func:`HistogramLUTItem.__init__()
+ `
============= =========================================================
Note: to display axis ticks inside the ImageView, instantiate it
@@ -109,8 +114,10 @@ class ImageView(QtGui.QWidget):
pg.ImageView(view=pg.PlotItem())
"""
QtGui.QWidget.__init__(self, parent, *args)
- self.levelMax = 4096
- self.levelMin = 0
+ self._imageLevels = None # [(min, max), ...] per channel image metrics
+ self.levelMin = None # min / max levels across all channels
+ self.levelMax = None
+
self.name = name
self.image = None
self.axes = {}
@@ -118,6 +125,7 @@ class ImageView(QtGui.QWidget):
self.ui = Ui_Form()
self.ui.setupUi(self)
self.scene = self.ui.graphicsView.scene()
+ self.ui.histogram.setLevelMode(levelMode)
self.ignoreTimeLine = False
@@ -151,13 +159,15 @@ class ImageView(QtGui.QWidget):
self.normRoi.setZValue(20)
self.view.addItem(self.normRoi)
self.normRoi.hide()
- self.roiCurve = self.ui.roiPlot.plot()
- self.timeLine = InfiniteLine(0, movable=True)
+ self.roiCurves = []
+ self.timeLine = InfiniteLine(0, movable=True, markers=[('^', 0), ('v', 1)])
self.timeLine.setPen((255, 255, 0, 200))
self.timeLine.setZValue(1)
self.ui.roiPlot.addItem(self.timeLine)
self.ui.splitter.setSizes([self.height()-35, 35])
self.ui.roiPlot.hideAxis('left')
+ self.frameTicks = VTickGroup(yrange=[0.8, 1], pen=0.4)
+ self.ui.roiPlot.addItem(self.frameTicks, ignoreBounds=True)
self.keysPressed = {}
self.playTimer = QtCore.QTimer()
@@ -200,7 +210,7 @@ class ImageView(QtGui.QWidget):
self.roiClicked() ## initialize roi plot to correct shape / visibility
- def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True):
+ def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None):
"""
Set the image to be displayed in the widget.
@@ -208,8 +218,9 @@ class ImageView(QtGui.QWidget):
**Arguments:**
img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and
*notes* below.
- xvals (numpy array) 1D array of z-axis values corresponding to the third axis
- in a 3D image. For video, this array should contain the time of each frame.
+ xvals (numpy array) 1D array of z-axis values corresponding to the first axis
+ in a 3D image. For video, this array should contain the time of each
+ frame.
autoRange (bool) whether to scale/pan the view to fit the image.
autoLevels (bool) whether to update the white/black levels to fit the image.
levels (min, max); the white and black level values to use.
@@ -224,6 +235,10 @@ class ImageView(QtGui.QWidget):
and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
image data.
+ levelMode If specified, this sets the user interaction mode for setting image
+ levels. Options are 'mono', which provides a single level control for
+ all image channels, and 'rgb' or 'rgba', which provide individual
+ controls for each channel.
================== ===========================================================================
**Notes:**
@@ -252,6 +267,8 @@ class ImageView(QtGui.QWidget):
self.image = img
self.imageDisp = None
+ if levelMode is not None:
+ self.ui.histogram.setLevelMode(levelMode)
profiler()
@@ -310,10 +327,9 @@ class ImageView(QtGui.QWidget):
profiler()
if self.axes['t'] is not None:
- #self.ui.roiPlot.show()
self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max())
+ self.frameTicks.setXVals(self.tVals)
self.timeLine.setValue(0)
- #self.ui.roiPlot.setMouseEnabled(False, False)
if len(self.tVals) > 1:
start = self.tVals.min()
stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02
@@ -325,8 +341,7 @@ class ImageView(QtGui.QWidget):
stop = 1
for s in [self.timeLine, self.normRgn]:
s.setBounds([start, stop])
- #else:
- #self.ui.roiPlot.hide()
+
profiler()
self.imageItem.resetTransform()
@@ -364,11 +379,14 @@ class ImageView(QtGui.QWidget):
def autoLevels(self):
"""Set the min/max intensity levels automatically to match the image data."""
- self.setLevels(self.levelMin, self.levelMax)
+ self.setLevels(rgba=self._imageLevels)
- def setLevels(self, min, max):
- """Set the min/max (bright and dark) levels."""
- self.ui.histogram.setLevels(min, max)
+ def setLevels(self, *args, **kwds):
+ """Set the min/max (bright and dark) levels.
+
+ See :func:`HistogramLUTItem.setLevels `.
+ """
+ self.ui.histogram.setLevels(*args, **kwds)
def autoRange(self):
"""Auto scale and pan the view around the image such that the image fills the view."""
@@ -377,12 +395,13 @@ class ImageView(QtGui.QWidget):
def getProcessedImage(self):
"""Returns the image data after it has been processed by any normalization options in use.
- This method also sets the attributes self.levelMin and self.levelMax
- to indicate the range of data in the image."""
+ """
if self.imageDisp is None:
image = self.normalize(self.image)
self.imageDisp = image
- self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp)))
+ self._imageLevels = self.quickMinMax(self.imageDisp)
+ self.levelMin = min([level[0] for level in self._imageLevels])
+ self.levelMax = max([level[1] for level in self._imageLevels])
return self.imageDisp
@@ -469,7 +488,7 @@ class ImageView(QtGui.QWidget):
n = int(self.playRate * dt)
if n != 0:
self.lastPlayTime += (float(n)/self.playRate)
- if self.currentIndex+n > self.image.shape[0]:
+ if self.currentIndex+n > self.image.shape[self.axes['t']]:
self.play(0)
self.jumpFrames(n)
@@ -527,13 +546,15 @@ class ImageView(QtGui.QWidget):
#self.ui.roiPlot.show()
self.ui.roiPlot.setMouseEnabled(True, True)
self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4])
- self.roiCurve.show()
+ for c in self.roiCurves:
+ c.show()
self.roiChanged()
self.ui.roiPlot.showAxis('left')
else:
self.roi.hide()
self.ui.roiPlot.setMouseEnabled(False, False)
- self.roiCurve.hide()
+ for c in self.roiCurves:
+ c.hide()
self.ui.roiPlot.hideAxis('left')
if self.hasTimeAxis():
@@ -557,36 +578,65 @@ class ImageView(QtGui.QWidget):
return
image = self.getProcessedImage()
- if image.ndim == 2:
- axes = (0, 1)
- elif image.ndim == 3:
- axes = (1, 2)
- else:
- return
-
+
+ # 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)
- if data is not None:
- while data.ndim > 1:
- data = data.mean(axis=1)
- if image.ndim == 3:
- self.roiCurve.setData(y=data, x=self.tVals)
+ if data is None:
+ return
+
+ # Convert extracted data into 1D plot data
+ 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]
+ xvals = (coords**2).sum(axis=0) ** 0.5
+ else:
+ # Average data within entire ROI for each frame
+ data = data.mean(axis=max(axes)).mean(axis=min(axes))
+ xvals = self.tVals
+
+ # Handle multi-channel data
+ if data.ndim == 1:
+ plots = [(xvals, data, 'w')]
+ if data.ndim == 2:
+ if data.shape[1] == 1:
+ colors = 'w'
else:
- while coords.ndim > 2:
- coords = coords[:,:,0]
- coords = coords - coords[:,0,np.newaxis]
- xvals = (coords**2).sum(axis=0) ** 0.5
- self.roiCurve.setData(y=data, x=xvals)
+ colors = 'rgbw'
+ plots = []
+ for i in range(data.shape[1]):
+ d = data[:,i]
+ plots.append((xvals, d, colors[i]))
+
+ # Update plot line(s)
+ while len(plots) < len(self.roiCurves):
+ c = self.roiCurves.pop()
+ c.scene().removeItem(c)
+ while len(plots) > len(self.roiCurves):
+ self.roiCurves.append(self.ui.roiPlot.plot())
+ for i in range(len(plots)):
+ x, y, p = plots[i]
+ self.roiCurves[i].setData(x, y, pen=p)
def quickMinMax(self, data):
"""
Estimate the min/max values of *data* by subsampling.
+ Returns [(min, max), ...] with one item per channel
"""
while data.size > 1e6:
ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
- return nanmin(data), nanmax(data)
+
+ cax = self.axes['c']
+ if cax is None:
+ return [(float(nanmin(data)), float(nanmax(data)))]
+ else:
+ return [(float(nanmin(data.take(i, axis=cax))),
+ float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])]
def normalize(self, image):
"""
diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py
index 66ecc460..15d374a6 100644
--- a/pyqtgraph/metaarray/MetaArray.py
+++ b/pyqtgraph/metaarray/MetaArray.py
@@ -748,7 +748,6 @@ class MetaArray(object):
else:
fd.seek(0)
meta = MetaArray._readMeta(fd)
-
if not kwargs.get("readAllData", True):
self._data = np.empty(meta['shape'], dtype=meta['type'])
if 'version' in meta:
@@ -1031,6 +1030,7 @@ class MetaArray(object):
"""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
"""
@@ -1096,7 +1096,6 @@ class MetaArray(object):
'chunks': None,
'compression': None
}
-
## set maximum shape to allow expansion along appendAxis
append = False
@@ -1125,14 +1124,19 @@ class MetaArray(object):
data[tuple(sl)] = self.view(np.ndarray)
## add axis values if they are present.
+ axKeys = ["values"]
+ axKeys.extend(opts.get("appendKeys", []))
axInfo = f['info'][str(ax)]
- if 'values' in axInfo:
- v = axInfo['values']
- v2 = self._info[ax]['values']
- shape = list(v.shape)
- shape[0] += v2.shape[0]
- v.resize(shape)
- v[-v2.shape[0]:] = v2
+ for key in axKeys:
+ if key in axInfo:
+ v = axInfo[key]
+ v2 = self._info[ax][key]
+ shape = list(v.shape)
+ shape[0] += v2.shape[0]
+ v.resize(shape)
+ v[-v2.shape[0]:] = v2
+ else:
+ raise TypeError('Cannot append to axis info key "%s"; this key is not present in the target file.' % key)
f.close()
else:
f = h5py.File(fileName, 'w')
diff --git a/pyqtgraph/multiprocess/__init__.py b/pyqtgraph/multiprocess/__init__.py
index 843b42a3..32a250cb 100644
--- a/pyqtgraph/multiprocess/__init__.py
+++ b/pyqtgraph/multiprocess/__init__.py
@@ -21,4 +21,4 @@ TODO:
from .processes import *
from .parallelizer import Parallelize, CanceledError
-from .remoteproxy import proxy
\ No newline at end of file
+from .remoteproxy import proxy, ClosedError, NoResultError
diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py
index bb71a703..a8a03d41 100644
--- a/pyqtgraph/multiprocess/bootstrap.py
+++ b/pyqtgraph/multiprocess/bootstrap.py
@@ -13,16 +13,31 @@ if __name__ == '__main__':
#print "key:", ' '.join([str(ord(x)) for x in authkey])
path = opts.pop('path', None)
if path is not None:
- ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list.
- while len(sys.path) > 0:
- sys.path.pop()
- sys.path.extend(path)
+ if isinstance(path, str):
+ # if string, just insert this into the path
+ sys.path.insert(0, path)
+ else:
+ # if list, then replace the entire sys.path
+ ## modify sys.path in place--no idea who already has a reference to the existing list.
+ while len(sys.path) > 0:
+ sys.path.pop()
+ sys.path.extend(path)
+
+ pyqtapis = opts.pop('pyqtapis', None)
+ if pyqtapis is not None:
+ import sip
+ for k,v in pyqtapis.items():
+ sip.setapi(k, v)
if opts.pop('pyside', False):
import PySide
targetStr = opts.pop('targetStr')
- target = pickle.loads(targetStr) ## unpickling the target should import everything we need
+ try:
+ target = pickle.loads(targetStr) ## unpickling the target should import everything we need
+ except:
+ print("Current sys.path:", sys.path)
+ raise
target(**opts) ## Send all other options to the target function
sys.exit(0)
diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py
index 934bc6d0..86298023 100644
--- a/pyqtgraph/multiprocess/parallelizer.py
+++ b/pyqtgraph/multiprocess/parallelizer.py
@@ -101,7 +101,10 @@ class Parallelize(object):
else: ## parent
if self.showProgress:
- self.progressDlg.__exit__(None, None, None)
+ try:
+ self.progressDlg.__exit__(None, None, None)
+ except Exception:
+ pass
def runSerial(self):
if self.showProgress:
diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py
index c7e4a80c..1be7e50b 100644
--- a/pyqtgraph/multiprocess/processes.py
+++ b/pyqtgraph/multiprocess/processes.py
@@ -1,4 +1,4 @@
-import subprocess, atexit, os, sys, time, random, socket, signal
+import subprocess, atexit, os, sys, time, random, socket, signal, inspect
import multiprocessing.connection
try:
import cPickle as pickle
@@ -39,7 +39,7 @@ class Process(RemoteEventHandler):
"""
_process_count = 1 # just used for assigning colors to each process for debugging
- def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None):
+ def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None, pyqtapis=None):
"""
============== =============================================================
**Arguments:**
@@ -47,10 +47,12 @@ class Process(RemoteEventHandler):
from the remote process.
target Optional function to call after starting remote process.
By default, this is startEventLoop(), which causes the remote
- process to process requests from the parent process until it
+ process to handle requests from the parent process until it
is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not).
- copySysPath If True, copy the contents of sys.path to the remote process
+ copySysPath If True, copy the contents of sys.path to the remote process.
+ If False, then only the path required to import pyqtgraph is
+ added.
debug If True, print detailed information about communication
with the child process.
wrapStdout If True (default on windows) then stdout and stderr from the
@@ -59,6 +61,8 @@ class Process(RemoteEventHandler):
for a python bug: http://bugs.python.org/issue3905
but has the side effect that child output is significantly
delayed relative to the parent output.
+ pyqtapis Optional dictionary of PyQt API version numbers to set before
+ importing pyqtgraph in the remote process.
============== =============================================================
"""
if target is None:
@@ -82,7 +86,13 @@ class Process(RemoteEventHandler):
port = l.address[1]
## start remote process, instruct it to run target function
- sysPath = sys.path if copySysPath else None
+ if copySysPath:
+ sysPath = sys.path
+ else:
+ # what path do we need to make target importable?
+ mod = inspect.getmodule(target)
+ modroot = sys.modules[mod.__name__.split('.')[0]]
+ sysPath = os.path.abspath(os.path.join(os.path.dirname(modroot.__file__), '..'))
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
@@ -122,7 +132,8 @@ class Process(RemoteEventHandler):
targetStr=targetStr,
path=sysPath,
pyside=USE_PYSIDE,
- debug=procDebug
+ debug=procDebug,
+ pyqtapis=pyqtapis,
)
pickle.dump(data, self.proc.stdin)
self.proc.stdin.close()
@@ -182,7 +193,8 @@ def startEventLoop(name, port, authkey, ppid, debug=False):
HANDLER.processRequests() # exception raised when the loop should exit
time.sleep(0.01)
except ClosedError:
- break
+ HANDLER.debugMsg('Exiting server loop.')
+ sys.exit(0)
class ForkedProcess(RemoteEventHandler):
@@ -321,9 +333,14 @@ class ForkedProcess(RemoteEventHandler):
#os.kill(pid, 9)
try:
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation.
- os.waitpid(self.childPid, 0)
except IOError: ## probably remote process has already quit
pass
+
+ try:
+ os.waitpid(self.childPid, 0)
+ except OSError: ## probably remote process has already quit
+ pass
+
self.hasJoined = True
def kill(self):
@@ -457,21 +474,20 @@ class FileForwarder(threading.Thread):
self.start()
def run(self):
- if self.output == 'stdout':
+ if self.output == 'stdout' and self.color is not False:
while True:
line = self.input.readline()
with self.lock:
cprint.cout(self.color, line, -1)
- elif self.output == 'stderr':
+ elif self.output == 'stderr' and self.color is not False:
while True:
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:
line = self.input.readline()
with self.lock:
self.output.write(line)
-
-
-
diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py
index 208e17f4..bc02da83 100644
--- a/pyqtgraph/multiprocess/remoteproxy.py
+++ b/pyqtgraph/multiprocess/remoteproxy.py
@@ -419,7 +419,7 @@ class RemoteEventHandler(object):
if opts is None:
opts = {}
- assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
+ assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async" (got %r)' % callSync
if reqId is None:
if callSync != 'off': ## requested return value; use the next available request ID
reqId = self.nextRequestId
@@ -466,10 +466,7 @@ class RemoteEventHandler(object):
return req
if callSync == 'sync':
- try:
- return req.result()
- except NoResultError:
- return req
+ return req.result()
def close(self, callSync='off', noCleanup=False, **kwds):
try:
@@ -572,6 +569,10 @@ class RemoteEventHandler(object):
self.proxies[ref] = proxy._proxyId
def deleteProxy(self, ref):
+ if self.send is None:
+ # this can happen during shutdown
+ return
+
with self.proxyLock:
proxyId = self.proxies.pop(ref)
diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py
index e0fee046..540fce7d 100644
--- a/pyqtgraph/opengl/GLViewWidget.py
+++ b/pyqtgraph/opengl/GLViewWidget.py
@@ -16,9 +16,13 @@ class GLViewWidget(QtOpenGL.QGLWidget):
- Axis/grid display
- Export options
+
+ High-DPI displays: Qt5 should automatically detect the correct resolution.
+ For Qt4, specify the ``devicePixelRatio`` argument when initializing the
+ widget (usually this value is 1-2).
"""
- def __init__(self, parent=None):
+ def __init__(self, parent=None, devicePixelRatio=None):
global ShareWidget
if ShareWidget is None:
@@ -37,6 +41,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
'azimuth': 45, ## camera's azimuthal angle in degrees
## (rotation around z-axis 0 points along x-axis)
'viewport': None, ## glViewport params; None == whole widget
+ 'devicePixelRatio': devicePixelRatio,
}
self.setBackgroundColor('k')
self.items = []
@@ -79,10 +84,21 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def getViewport(self):
vp = self.opts['viewport']
+ dpr = self.devicePixelRatio()
if vp is None:
- return (0, 0, self.width(), self.height())
+ return (0, 0, int(self.width() * dpr), int(self.height() * dpr))
else:
- return vp
+ return tuple([int(x * dpr) for x in vp])
+
+ def devicePixelRatio(self):
+ dpr = self.opts['devicePixelRatio']
+ if dpr is not None:
+ return dpr
+
+ if hasattr(QtOpenGL.QGLWidget, 'devicePixelRatio'):
+ return QtOpenGL.QGLWidget.devicePixelRatio(self)
+ else:
+ return 1.0
def resizeGL(self, w, h):
pass
@@ -99,7 +115,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def projectionMatrix(self, region=None):
# Xw = (Xnd + 1) * width/2 + X
if region is None:
- region = (0, 0, self.width(), self.height())
+ dpr = self.devicePixelRatio()
+ region = (0, 0, self.width() * dpr, self.height() * dpr)
x0, y0, w, h = self.getViewport()
dist = self.opts['distance']
@@ -450,6 +467,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region
+ glBindTexture(GL_TEXTURE_2D, tex) # fixes issue #366
## read texture back to array
data = glGetTexImage(GL_TEXTURE_2D, 0, format, type)
diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py
index f83fcdf6..5bab4626 100644
--- a/pyqtgraph/opengl/MeshData.py
+++ b/pyqtgraph/opengl/MeshData.py
@@ -485,7 +485,7 @@ class MeshData(object):
if isinstance(radius, int):
radius = [radius, radius] # convert to list
## compute vertexes
- th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols)
+ th = np.linspace(2 * np.pi, (2 * np.pi)/cols, cols).reshape(1, cols)
r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z
verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z
if offset:
diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py
index 4d6bc9d6..0da9f61e 100644
--- a/pyqtgraph/opengl/items/GLGridItem.py
+++ b/pyqtgraph/opengl/items/GLGridItem.py
@@ -10,10 +10,10 @@ class GLGridItem(GLGraphicsItem):
"""
**Bases:** :class:`GLGraphicsItem `
- Displays a wire-grame grid.
+ Displays a wire-frame grid.
"""
- def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'):
+ def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'):
GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
self.antialias = antialias
@@ -21,6 +21,7 @@ class GLGridItem(GLGraphicsItem):
size = QtGui.QVector3D(20,20,1)
self.setSize(size=size)
self.setSpacing(1, 1, 1)
+ self.color = color
def setSize(self, x=None, y=None, z=None, size=None):
"""
@@ -66,8 +67,8 @@ class GLGridItem(GLGraphicsItem):
x,y,z = self.size()
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(1, 1, 1, .3)
+ yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
+ glColor4f(*self.color)
for x in xvals:
glVertex3f(x, yvals[0], 0)
glVertex3f(x, yvals[-1], 0)
diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py
index 7242b506..fb37037f 100644
--- a/pyqtgraph/ordereddict.py
+++ b/pyqtgraph/ordereddict.py
@@ -20,108 +20,112 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
-from UserDict import DictMixin
+import sys
+if sys.version[0] > '2':
+ from collections import OrderedDict
+else:
+ from UserDict import DictMixin
-class OrderedDict(dict, DictMixin):
+ class OrderedDict(dict, DictMixin):
- def __init__(self, *args, **kwds):
- if len(args) > 1:
- raise TypeError('expected at most 1 arguments, got %d' % len(args))
- try:
- self.__end
- except AttributeError:
- self.clear()
- self.update(*args, **kwds)
+ def __init__(self, *args, **kwds):
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__end
+ except AttributeError:
+ self.clear()
+ self.update(*args, **kwds)
- def clear(self):
- self.__end = end = []
- end += [None, end, end] # sentinel node for doubly linked list
- self.__map = {} # key --> [key, prev, next]
- dict.clear(self)
+ def clear(self):
+ self.__end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.__map = {} # key --> [key, prev, next]
+ dict.clear(self)
- def __setitem__(self, key, value):
- if key not in self:
+ def __setitem__(self, key, value):
+ if key not in self:
+ end = self.__end
+ curr = end[1]
+ curr[2] = end[1] = self.__map[key] = [key, curr, end]
+ dict.__setitem__(self, key, value)
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ key, prev, next = self.__map.pop(key)
+ prev[2] = next
+ next[1] = prev
+
+ def __iter__(self):
+ end = self.__end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __reversed__(self):
end = self.__end
curr = end[1]
- curr[2] = end[1] = self.__map[key] = [key, curr, end]
- dict.__setitem__(self, key, value)
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
- def __delitem__(self, key):
- dict.__delitem__(self, key)
- key, prev, next = self.__map.pop(key)
- prev[2] = next
- next[1] = prev
+ def popitem(self, last=True):
+ if not self:
+ raise KeyError('dictionary is empty')
+ if last:
+ key = reversed(self).next()
+ else:
+ key = iter(self).next()
+ value = self.pop(key)
+ return key, value
- def __iter__(self):
- end = self.__end
- curr = end[2]
- while curr is not end:
- yield curr[0]
- curr = curr[2]
+ def __reduce__(self):
+ items = [[k, self[k]] for k in self]
+ tmp = self.__map, self.__end
+ del self.__map, self.__end
+ inst_dict = vars(self).copy()
+ self.__map, self.__end = tmp
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
- def __reversed__(self):
- end = self.__end
- curr = end[1]
- while curr is not end:
- yield curr[0]
- curr = curr[1]
+ def keys(self):
+ return list(self)
- def popitem(self, last=True):
- if not self:
- raise KeyError('dictionary is empty')
- if last:
- key = reversed(self).next()
- else:
- key = iter(self).next()
- value = self.pop(key)
- return key, value
+ setdefault = DictMixin.setdefault
+ update = DictMixin.update
+ pop = DictMixin.pop
+ values = DictMixin.values
+ items = DictMixin.items
+ iterkeys = DictMixin.iterkeys
+ itervalues = DictMixin.itervalues
+ iteritems = DictMixin.iteritems
- def __reduce__(self):
- items = [[k, self[k]] for k in self]
- tmp = self.__map, self.__end
- del self.__map, self.__end
- inst_dict = vars(self).copy()
- self.__map, self.__end = tmp
- if inst_dict:
- return (self.__class__, (items,), inst_dict)
- return self.__class__, (items,)
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
- def keys(self):
- return list(self)
+ def copy(self):
+ return self.__class__(self)
- setdefault = DictMixin.setdefault
- update = DictMixin.update
- pop = DictMixin.pop
- values = DictMixin.values
- items = DictMixin.items
- iterkeys = DictMixin.iterkeys
- itervalues = DictMixin.itervalues
- iteritems = DictMixin.iteritems
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
- def __repr__(self):
- if not self:
- return '%s()' % (self.__class__.__name__,)
- return '%s(%r)' % (self.__class__.__name__, self.items())
-
- def copy(self):
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- d = cls()
- for key in iterable:
- d[key] = value
- return d
-
- def __eq__(self, other):
- if isinstance(other, OrderedDict):
- if len(self) != len(other):
- return False
- for p, q in zip(self.items(), other.items()):
- if p != q:
+ def __eq__(self, other):
+ if isinstance(other, OrderedDict):
+ if len(self) != len(other):
return False
- return True
- return dict.__eq__(self, other)
+ for p, q in zip(self.items(), other.items()):
+ if p != q:
+ return False
+ return True
+ return dict.__eq__(self, other)
- def __ne__(self, other):
- return not self == other
+ def __ne__(self, other):
+ return not self == other
diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py
index de9a1624..e28085bf 100644
--- a/pyqtgraph/parametertree/Parameter.py
+++ b/pyqtgraph/parametertree/Parameter.py
@@ -162,7 +162,11 @@ class Parameter(QtCore.QObject):
'title': None,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
}
+ value = opts.get('value', None)
+ name = opts.get('name', None)
self.opts.update(opts)
+ self.opts['value'] = None # will be set later.
+ self.opts['name'] = None
self.childs = []
self.names = {} ## map name:child
@@ -172,17 +176,19 @@ class Parameter(QtCore.QObject):
self.blockTreeChangeEmit = 0
#self.monitoringChildren = False ## prevent calling monitorChildren more than once
- if 'value' not in self.opts:
- self.opts['value'] = None
-
- if 'name' not in self.opts or not isinstance(self.opts['name'], basestring):
+ if not isinstance(name, basestring):
raise Exception("Parameter must have a string name specified in opts.")
- self.setName(opts['name'])
+ self.setName(name)
self.addChildren(self.opts.get('children', []))
-
- if 'value' in self.opts and 'default' not in self.opts:
- self.opts['default'] = self.opts['value']
+
+ self.opts['value'] = None
+ if value is not None:
+ self.setValue(value)
+
+ if 'default' not in self.opts:
+ self.opts['default'] = None
+ self.setDefault(self.opts['value'])
## Connect all state changed signals to the general sigStateChanged
self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data))
@@ -255,6 +261,7 @@ class Parameter(QtCore.QObject):
try:
if blockSignal is not None:
self.sigValueChanged.disconnect(blockSignal)
+ value = self._interpretValue(value)
if self.opts['value'] == value:
return value
self.opts['value'] = value
@@ -265,6 +272,9 @@ class Parameter(QtCore.QObject):
return value
+ def _interpretValue(self, v):
+ return v
+
def value(self):
"""
Return the value of this Parameter.
@@ -643,18 +653,19 @@ class Parameter(QtCore.QObject):
"""Return a child parameter.
Accepts the name of the child or a tuple (path, to, child)
- Added in version 0.9.9. Ealier versions used the 'param' method, which is still
- implemented for backward compatibility."""
+ Added in version 0.9.9. Earlier versions used the 'param' method, which is still
+ implemented for backward compatibility.
+ """
try:
param = self.names[names[0]]
except KeyError:
- raise Exception("Parameter %s has no child named %s" % (self.name(), names[0]))
+ raise KeyError("Parameter %s has no child named %s" % (self.name(), names[0]))
if len(names) > 1:
- return param.param(*names[1:])
+ return param.child(*names[1:])
else:
return param
-
+
def param(self, *names):
# for backward compatibility.
return self.child(*names)
diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py
index 24e35e9a..b1d4256a 100644
--- a/pyqtgraph/parametertree/SystemSolver.py
+++ b/pyqtgraph/parametertree/SystemSolver.py
@@ -1,5 +1,7 @@
from ..pgcollections import OrderedDict
import numpy as np
+import copy
+
class SystemSolver(object):
"""
@@ -73,6 +75,12 @@ class SystemSolver(object):
self.__dict__['_currentGets'] = set()
self.reset()
+ def copy(self):
+ sys = type(self)()
+ sys.__dict__['_vars'] = copy.deepcopy(self.__dict__['_vars'])
+ sys.__dict__['_currentGets'] = copy.deepcopy(self.__dict__['_currentGets'])
+ return sys
+
def reset(self):
"""
Reset all variables in the solver to their default state.
@@ -167,6 +175,16 @@ class SystemSolver(object):
elif constraint == 'fixed':
if 'f' not in var[3]:
raise TypeError("Fixed constraints not allowed for '%s'" % name)
+ # This is nice, but not reliable because sometimes there is 1 DOF but we set 2
+ # values simultaneously.
+ # if var[2] is None:
+ # try:
+ # self.get(name)
+ # # has already been computed by the system; adding a fixed constraint
+ # # would overspecify the system.
+ # raise ValueError("Cannot fix parameter '%s'; system would become overconstrained." % name)
+ # except RuntimeError:
+ # pass
var[2] = constraint
elif isinstance(constraint, tuple):
if 'r' not in var[3]:
@@ -177,7 +195,7 @@ class SystemSolver(object):
raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint)
# type checking / massaging
- if var[1] is np.ndarray:
+ if var[1] is np.ndarray and value is not None:
value = np.array(value, dtype=float)
elif var[1] in (int, float, tuple) and value is not None:
value = var[1](value)
@@ -185,9 +203,9 @@ class SystemSolver(object):
# constraint checks
if constraint is True and not self.check_constraint(name, value):
raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2]))
-
+
# invalidate other dependent values
- if var[0] is not None:
+ if var[0] is not None or value is None:
# todo: we can make this more clever..(and might need to)
# we just know that a value of None cannot have dependencies
# (because if anyone else had asked for this value, it wouldn't be
@@ -237,6 +255,31 @@ class SystemSolver(object):
for k in self._vars:
getattr(self, k)
+ def checkOverconstraint(self):
+ """Check whether the system is overconstrained. If so, return the name of
+ the first overconstrained parameter.
+
+ Overconstraints occur when any fixed parameter can be successfully computed by the system.
+ (Ideally, all parameters are either fixed by the user or constrained by the
+ system, but never both).
+ """
+ for k,v in self._vars.items():
+ if v[2] == 'fixed' and 'n' in v[3]:
+ oldval = v[:]
+ self.set(k, None, None)
+ try:
+ self.get(k)
+ return k
+ except RuntimeError:
+ pass
+ finally:
+ self._vars[k] = oldval
+
+ return False
+
+
+
+
def __repr__(self):
state = OrderedDict()
for name, var in self._vars.items():
@@ -378,4 +421,4 @@ if __name__ == '__main__':
camera.solve()
print(camera.saveState())
-
\ No newline at end of file
+
diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py
index 2535b13a..42a18fe0 100644
--- a/pyqtgraph/parametertree/parameterTypes.py
+++ b/pyqtgraph/parametertree/parameterTypes.py
@@ -4,10 +4,11 @@ from .Parameter import Parameter, registerParameterType
from .ParameterItem import ParameterItem
from ..widgets.SpinBox import SpinBox
from ..widgets.ColorButton import ColorButton
+from ..colormap import ColorMap
#from ..widgets.GradientWidget import GradientWidget ## creates import loop
from .. import pixmaps as pixmaps
from .. import functions as fn
-import os
+import os, sys
from ..pgcollections import OrderedDict
class WidgetParameterItem(ParameterItem):
@@ -320,6 +321,26 @@ class SimpleParameter(Parameter):
state['value'] = fn.colorTuple(self.value())
return state
+ def _interpretValue(self, v):
+ fn = {
+ 'int': int,
+ 'float': float,
+ 'bool': bool,
+ 'str': asUnicode,
+ 'color': self._interpColor,
+ 'colormap': self._interpColormap,
+ }[self.opts['type']]
+ return fn(v)
+
+ def _interpColor(self, v):
+ return fn.mkColor(v)
+
+ def _interpColormap(self, v):
+ if not isinstance(v, ColorMap):
+ raise TypeError("Cannot set colormap parameter from object %r" % v)
+ return v
+
+
registerParameterType('int', SimpleParameter, override=True)
registerParameterType('float', SimpleParameter, override=True)
@@ -379,6 +400,7 @@ class GroupParameterItem(ParameterItem):
else:
for c in [0,1]:
self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220)))
+ self.setForeground(c, QtGui.QBrush(QtGui.QColor(50,50,50)))
font = self.font(c)
font.setBold(True)
#font.setPointSize(font.pointSize()+1)
@@ -441,12 +463,15 @@ class GroupParameter(Parameter):
instead of a button.
"""
itemClass = GroupParameterItem
+
+ sigAddNew = QtCore.Signal(object, object) # self, type
def addNew(self, typ=None):
"""
This method is called when the user has requested to add a new item to the group.
+ By default, it emits ``sigAddNew(self, typ)``.
"""
- raise Exception("Must override this function in subclass.")
+ self.sigAddNew.emit(self, typ)
def setAddList(self, vals):
"""Change the list of options available for the user to add to the group."""
@@ -584,6 +609,7 @@ class ActionParameterItem(ParameterItem):
ParameterItem.__init__(self, param, depth)
self.layoutWidget = QtGui.QWidget()
self.layout = QtGui.QHBoxLayout()
+ self.layout.setContentsMargins(0, 0, 0, 0)
self.layoutWidget.setLayout(self.layout)
self.button = QtGui.QPushButton(param.name())
#self.layout.addSpacing(100)
diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py
index dc581019..a654a9ad 100644
--- a/pyqtgraph/parametertree/tests/test_parametertypes.py
+++ b/pyqtgraph/parametertree/tests/test_parametertypes.py
@@ -1,7 +1,19 @@
+# ~*~ coding: utf8 ~*~
+import sys
+import pytest
+from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as pt
import pyqtgraph as pg
+from pyqtgraph.python2_3 import asUnicode
+from pyqtgraph.functions import eq
+import numpy as np
+
app = pg.mkQApp()
+def _getWidget(param):
+ return list(param.items.keys())[0].widget
+
+
def test_opts():
paramSpec = [
dict(name='bool', type='bool', readonly=True),
@@ -12,7 +24,111 @@ def test_opts():
tree = pt.ParameterTree()
tree.setParameters(param)
- assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False
- assert list(param.param('color').items.keys())[0].widget.isEnabled() is False
+ assert _getWidget(param.param('bool')).isEnabled() is False
+ assert _getWidget(param.param('bool')).isEnabled() is False
+def test_types():
+ paramSpec = [
+ dict(name='float', type='float'),
+ dict(name='int', type='int'),
+ dict(name='str', type='str'),
+ dict(name='list', type='list', values=['x','y','z']),
+ dict(name='dict', type='list', values={'x':1, 'y':3, 'z':7}),
+ dict(name='bool', type='bool'),
+ dict(name='color', type='color'),
+ ]
+
+ param = pt.Parameter.create(name='params', type='group', children=paramSpec)
+ tree = pt.ParameterTree()
+ tree.setParameters(param)
+
+ all_objs = {
+ 'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float(5),
+ 'npint': np.int(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True,
+ 'complex': 5+3j, 'str': 'xxx', 'unicode': asUnicode('µ'),
+ 'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'),
+ 'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None
+ }
+ if hasattr(QtCore, 'QString'):
+ all_objs['qstring'] = QtCore.QString('xxxµ')
+
+ # float
+ types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool']
+ check_param_types(param.child('float'), float, float, 0.0, all_objs, types)
+
+ # int
+ types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool']
+ inttyps = int if sys.version[0] >= '3' else (int, long)
+ check_param_types(param.child('int'), inttyps, int, 0, all_objs, types)
+
+ # str (should be able to make a string out of any type)
+ types = all_objs.keys()
+ strtyp = str if sys.version[0] >= '3' else unicode
+ check_param_types(param.child('str'), strtyp, asUnicode, '', all_objs, types)
+
+ # bool (should be able to make a boolean out of any type?)
+ types = all_objs.keys()
+ check_param_types(param.child('bool'), bool, bool, False, all_objs, types)
+
+ # color
+ types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list']
+ init = QtGui.QColor(128, 128, 128, 255)
+ check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types)
+
+
+def check_param_types(param, types, map_func, init, objs, keys):
+ """Check that parameter setValue() accepts or rejects the correct types and
+ that value() returns the correct type.
+
+ Parameters
+ ----------
+ param : Parameter instance
+ types : type or tuple of types
+ The allowed types for this parameter to return from value().
+ map_func : function
+ Converts an input value to the expected output value.
+ init : object
+ The expected initial value of the parameter
+ objs : dict
+ Contains a variety of objects that will be tested as arguments to
+ param.setValue().
+ keys : list
+ The list of keys indicating the valid objects in *objs*. When
+ param.setValue() is teasted with each value from *objs*, we expect
+ an exception to be raised if the associated key is not in *keys*.
+ """
+ val = param.value()
+ if not isinstance(types, tuple):
+ types = (types,)
+ assert val == init and type(val) in types
+
+ # test valid input types
+ good_inputs = [objs[k] for k in keys if k in objs]
+ good_outputs = map(map_func, good_inputs)
+ for x,y in zip(good_inputs, good_outputs):
+ param.setValue(x)
+ val = param.value()
+ if not (eq(val, y) and type(val) in types):
+ raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), "
+ "but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val)))
+
+ # test invalid input types
+ for k,v in objs.items():
+ if k in keys:
+ continue
+ try:
+ param.setValue(v)
+ except (TypeError, ValueError, OverflowError):
+ continue
+ except Exception as exc:
+ raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc))
+
+ raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v))
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py
index c8a41dec..a7552631 100644
--- a/pyqtgraph/tests/image_testing.py
+++ b/pyqtgraph/tests/image_testing.py
@@ -10,11 +10,13 @@ Procedure for unit-testing with images:
$ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
- Any failing tests will
- display the test results, standard image, and the differences between the
- two. If the test result is bad, then press (f)ail. If the test result is
- good, then press (p)ass and the new image will be saved to the test-data
- directory.
+ Any failing tests will display the test results, standard image, and the
+ differences between the two. If the test result is bad, then press (f)ail.
+ If the test result is good, then press (p)ass and the new image will be
+ saved to the test-data directory.
+
+ To check all test results regardless of whether the test failed, set the
+ environment variable PYQTGRAPH_AUDIT_ALL=1.
3. After adding or changing test images, create a new commit:
@@ -42,7 +44,7 @@ Procedure for unit-testing with images:
# pyqtgraph should be tested against. When adding or changing test images,
# create and push a new tag and update this variable. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository.
-testDataTag = 'test-data-6'
+testDataTag = 'test-data-7'
import time
@@ -162,6 +164,8 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
# If the test image does not match, then we go to audit if requested.
try:
+ if stdImage is None:
+ raise Exception("No reference image saved for this test.")
if image.shape[2] != stdImage.shape[2]:
raise Exception("Test result has different channel count than standard image"
"(%d vs %d)" % (image.shape[2], stdImage.shape[2]))
diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py
index 7ad3bf91..68f3dc24 100644
--- a/pyqtgraph/tests/test_functions.py
+++ b/pyqtgraph/tests/test_functions.py
@@ -1,5 +1,6 @@
import pyqtgraph as pg
import numpy as np
+import sys
from numpy.testing import assert_array_almost_equal, assert_almost_equal
import pytest
@@ -293,6 +294,68 @@ def test_makeARGB():
with AssertExc(): # 3d levels not allowed
pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2]))
+
+def test_eq():
+ eq = pg.functions.eq
+
+ zeros = [0, 0.0, np.float(0), np.int(0)]
+ if sys.version[0] < '3':
+ zeros.append(long(0))
+ for i,x in enumerate(zeros):
+ for y in zeros[i:]:
+ assert eq(x, y)
+ assert eq(y, x)
+
+ assert eq(np.nan, np.nan)
+
+ # test
+ class NotEq(object):
+ def __eq__(self, x):
+ return False
+
+ noteq = NotEq()
+ assert eq(noteq, noteq) # passes because they are the same object
+ assert not eq(noteq, NotEq())
+
+
+ # Should be able to test for equivalence even if the test raises certain
+ # exceptions
+ class NoEq(object):
+ def __init__(self, err):
+ self.err = err
+ def __eq__(self, x):
+ raise self.err
+
+ noeq1 = NoEq(AttributeError())
+ noeq2 = NoEq(ValueError())
+ noeq3 = NoEq(Exception())
+
+ assert eq(noeq1, noeq1)
+ assert not eq(noeq1, noeq2)
+ assert not eq(noeq2, noeq1)
+ with pytest.raises(Exception):
+ eq(noeq3, noeq2)
+
+ # test array equivalence
+ # note that numpy has a weird behavior here--np.all() always returns True
+ # if one of the arrays has size=0; eq() will only return True if both arrays
+ # have the same shape.
+ a1 = np.zeros((10, 20)).astype('float')
+ a2 = a1 + 1
+ a3 = a2.astype('int')
+ a4 = np.empty((0, 20))
+ assert not eq(a1, a2) # same shape/dtype, different values
+ assert not eq(a1, a3) # same shape, different dtype and values
+ assert not eq(a1, a4) # different shape (note: np.all gives True if one array has size 0)
+
+ assert not eq(a2, a3) # same values, but different dtype
+ assert not eq(a2, a4) # different shape
+
+ assert not eq(a3, a4) # different shape and dtype
+
+ assert eq(a4, a4.copy())
+ assert not eq(a4, a4.T)
+
if __name__ == '__main__':
test_interpolateArray()
\ No newline at end of file
diff --git a/pyqtgraph/util/mutex.py b/pyqtgraph/util/mutex.py
index 4a193127..c03c65c4 100644
--- a/pyqtgraph/util/mutex.py
+++ b/pyqtgraph/util/mutex.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
-from ..Qt import QtCore
import traceback
+from ..Qt import QtCore
+
class Mutex(QtCore.QMutex):
"""
@@ -17,7 +18,7 @@ class Mutex(QtCore.QMutex):
QtCore.QMutex.__init__(self, *args)
self.l = QtCore.QMutex() ## for serializing access to self.tb
self.tb = []
- self.debug = True ## True to enable debugging functions
+ self.debug = kargs.pop('debug', False) ## True to enable debugging functions
def tryLock(self, timeout=None, id=None):
if timeout is None:
@@ -72,6 +73,16 @@ class Mutex(QtCore.QMutex):
finally:
self.l.unlock()
+ def acquire(self, blocking=True):
+ """Mimics threading.Lock.acquire() to allow this class as a drop-in replacement.
+ """
+ return self.tryLock()
+
+ def release(self):
+ """Mimics threading.Lock.release() to allow this class as a drop-in replacement.
+ """
+ self.unlock()
+
def depth(self):
self.l.lock()
n = len(self.tb)
@@ -91,4 +102,13 @@ class Mutex(QtCore.QMutex):
def __enter__(self):
self.lock()
- return self
\ No newline at end of file
+ return self
+
+
+class RecursiveMutex(Mutex):
+ """Mimics threading.RLock class.
+ """
+ def __init__(self, **kwds):
+ kwds['recursive'] = True
+ Mutex.__init__(self, **kwds)
+
diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py
index f6e28960..7e6bfab7 100644
--- a/pyqtgraph/widgets/ColorMapWidget.py
+++ b/pyqtgraph/widgets/ColorMapWidget.py
@@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree):
def restoreState(self, state):
self.params.restoreState(state)
+ def addColorMap(self, name):
+ """Add a new color mapping and return the created parameter.
+ """
+ return self.params.addNew(name)
+
class ColorMapParameter(ptree.types.GroupParameter):
sigColorMapChanged = QtCore.Signal(object)
@@ -152,7 +157,7 @@ class ColorMapParameter(ptree.types.GroupParameter):
def restoreState(self, state):
if 'fields' in state:
self.setFields(state['fields'])
- for itemState in state['items']:
+ for name, itemState in state['items'].items():
item = self.addNew(itemState['field'])
item.restoreState(itemState)
diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py
index a6828959..6f184c5f 100644
--- a/pyqtgraph/widgets/ComboBox.py
+++ b/pyqtgraph/widgets/ComboBox.py
@@ -102,7 +102,7 @@ class ComboBox(QtGui.QComboBox):
@blockIfUnchanged
def setItems(self, items):
"""
- *items* may be a list or a dict.
+ *items* may be a list, a tuple, or a dict.
If a dict is given, then the keys are used to populate the combo box
and the values will be used for both value() and setValue().
"""
@@ -191,13 +191,13 @@ class ComboBox(QtGui.QComboBox):
@ignoreIndexChange
@blockIfUnchanged
def addItems(self, items):
- if isinstance(items, list):
+ if isinstance(items, list) or isinstance(items, tuple):
texts = items
items = dict([(x, x) for x in items])
elif isinstance(items, dict):
texts = list(items.keys())
else:
- raise TypeError("items argument must be list or dict (got %s)." % type(items))
+ raise TypeError("items argument must be list or dict or tuple (got %s)." % type(items))
for t in texts:
if t in self._items:
@@ -216,3 +216,30 @@ class ComboBox(QtGui.QComboBox):
QtGui.QComboBox.clear(self)
self.itemsChanged()
+ def saveState(self):
+ ind = self.currentIndex()
+ data = self.itemData(ind)
+ #if not data.isValid():
+ if data is not None:
+ try:
+ if not data.isValid():
+ data = None
+ else:
+ data = data.toInt()[0]
+ except AttributeError:
+ pass
+ if data is None:
+ return asUnicode(self.itemText(ind))
+ else:
+ return data
+
+ def restoreState(self, v):
+ if type(v) is int:
+ ind = self.findData(v)
+ if ind > -1:
+ self.setCurrentIndex(ind)
+ return
+ self.setCurrentIndex(self.findText(str(v)))
+
+ def widgetGroupInterface(self):
+ return (self.currentIndexChanged, self.saveState, self.restoreState)
diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py
index cae8be86..23cf930f 100644
--- a/pyqtgraph/widgets/DataFilterWidget.py
+++ b/pyqtgraph/widgets/DataFilterWidget.py
@@ -30,7 +30,12 @@ class DataFilterWidget(ptree.ParameterTree):
def parameters(self):
return self.params
-
+
+ def addFilter(self, name):
+ """Add a new filter and return the created parameter item.
+ """
+ return self.params.addNew(name)
+
class DataFilterParameter(ptree.types.GroupParameter):
@@ -47,10 +52,10 @@ class DataFilterParameter(ptree.types.GroupParameter):
def addNew(self, name):
mode = self.fields[name].get('mode', 'range')
if mode == 'range':
- self.addChild(RangeFilterItem(name, self.fields[name]))
+ child = self.addChild(RangeFilterItem(name, self.fields[name]))
elif mode == 'enum':
- self.addChild(EnumFilterItem(name, self.fields[name]))
-
+ child = self.addChild(EnumFilterItem(name, self.fields[name]))
+ return child
def fieldNames(self):
return self.fields.keys()
diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py
index ec7b9e0d..3b41a3ca 100644
--- a/pyqtgraph/widgets/GraphicsLayoutWidget.py
+++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py
@@ -1,4 +1,4 @@
-from ..Qt import QtGui
+from ..Qt import QtGui, mkQApp
from ..graphicsItems.GraphicsLayout import GraphicsLayout
from .GraphicsView import GraphicsView
@@ -9,6 +9,31 @@ class GraphicsLayoutWidget(GraphicsView):
` with a single :class:`GraphicsLayout
` as its central item.
+ This widget is an easy starting point for generating multi-panel figures.
+ Example::
+
+ w = pg.GraphicsLayoutWidget()
+ p1 = w.addPlot(row=0, col=0)
+ 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__() `
+
+
This class wraps several methods from its internal GraphicsLayout:
:func:`nextRow `
:func:`nextColumn `
@@ -22,9 +47,19 @@ class GraphicsLayoutWidget(GraphicsView):
:func:`itemIndex `
:func:`clear `
"""
- def __init__(self, parent=None, **kargs):
+ def __init__(self, parent=None, show=False, size=None, title=None, **kargs):
+ mkQApp()
GraphicsView.__init__(self, parent)
self.ci = GraphicsLayout(**kargs)
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:
setattr(self, n, getattr(self.ci, n))
self.setCentralItem(self.ci)
+
+ if size is not None:
+ self.resize(*size)
+
+ if title is not None:
+ self.setWindowTitle(title)
+
+ if show is True:
+ self.show()
diff --git a/pyqtgraph/widgets/GroupBox.py b/pyqtgraph/widgets/GroupBox.py
new file mode 100644
index 00000000..14a8dab5
--- /dev/null
+++ b/pyqtgraph/widgets/GroupBox.py
@@ -0,0 +1,91 @@
+from ..Qt import QtGui, QtCore
+from .PathButton import PathButton
+
+class GroupBox(QtGui.QGroupBox):
+ """Subclass of QGroupBox that implements collapse handle.
+ """
+ sigCollapseChanged = QtCore.Signal(object)
+
+ def __init__(self, *args):
+ QtGui.QGroupBox.__init__(self, *args)
+
+ self._collapsed = False
+ # We modify the size policy when the group box is collapsed, so
+ # keep track of the last requested policy:
+ self._lastSizePlocy = self.sizePolicy()
+
+ self.closePath = QtGui.QPainterPath()
+ self.closePath.moveTo(0, -1)
+ self.closePath.lineTo(0, 1)
+ self.closePath.lineTo(1, 0)
+ self.closePath.lineTo(0, -1)
+
+ self.openPath = QtGui.QPainterPath()
+ self.openPath.moveTo(-1, 0)
+ self.openPath.lineTo(1, 0)
+ self.openPath.lineTo(0, 1)
+ self.openPath.lineTo(-1, 0)
+
+ self.collapseBtn = PathButton(path=self.openPath, size=(12, 12), margin=0)
+ self.collapseBtn.setStyleSheet("""
+ border: none;
+ """)
+ self.collapseBtn.setPen('k')
+ self.collapseBtn.setBrush('w')
+ self.collapseBtn.setParent(self)
+ self.collapseBtn.move(3, 3)
+ self.collapseBtn.setFlat(True)
+
+ self.collapseBtn.clicked.connect(self.toggleCollapsed)
+
+ if len(args) > 0 and isinstance(args[0], basestring):
+ self.setTitle(args[0])
+
+ def toggleCollapsed(self):
+ self.setCollapsed(not self._collapsed)
+
+ def collapsed(self):
+ return self._collapsed
+
+ def setCollapsed(self, c):
+ if c == self._collapsed:
+ return
+
+ if c is True:
+ self.collapseBtn.setPath(self.closePath)
+ self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred, closing=True)
+ elif c is False:
+ self.collapseBtn.setPath(self.openPath)
+ self.setSizePolicy(self._lastSizePolicy)
+ else:
+ raise TypeError("Invalid argument %r; must be bool." % c)
+
+ for ch in self.children():
+ if isinstance(ch, QtGui.QWidget) and ch is not self.collapseBtn:
+ ch.setVisible(not c)
+
+ self._collapsed = c
+ self.sigCollapseChanged.emit(c)
+
+ def setSizePolicy(self, *args, **kwds):
+ QtGui.QGroupBox.setSizePolicy(self, *args)
+ if kwds.pop('closing', False) is True:
+ self._lastSizePolicy = self.sizePolicy()
+
+ def setHorizontalPolicy(self, *args):
+ QtGui.QGroupBox.setHorizontalPolicy(self, *args)
+ self._lastSizePolicy = self.sizePolicy()
+
+ def setVerticalPolicy(self, *args):
+ QtGui.QGroupBox.setVerticalPolicy(self, *args)
+ self._lastSizePolicy = self.sizePolicy()
+
+ def setTitle(self, title):
+ # Leave room for button
+ QtGui.QGroupBox.setTitle(self, " " + title)
+
+ def widgetGroupInterface(self):
+ return (self.sigCollapseChanged,
+ GroupBox.collapsed,
+ GroupBox.setCollapsed,
+ True)
diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py
index 52c60e20..ee2e0bca 100644
--- a/pyqtgraph/widgets/PathButton.py
+++ b/pyqtgraph/widgets/PathButton.py
@@ -5,9 +5,11 @@ __all__ = ['PathButton']
class PathButton(QtGui.QPushButton):
- """Simple PushButton extension which paints a QPainterPath on its face"""
- def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30)):
+ """Simple PushButton extension that paints a QPainterPath centered on its face.
+ """
+ def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30), margin=7):
QtGui.QPushButton.__init__(self, parent)
+ self.margin = margin
self.path = None
if pen == 'default':
pen = 'k'
@@ -19,7 +21,6 @@ class PathButton(QtGui.QPushButton):
self.setFixedWidth(size[0])
self.setFixedHeight(size[1])
-
def setBrush(self, brush):
self.brush = fn.mkBrush(brush)
@@ -32,7 +33,7 @@ class PathButton(QtGui.QPushButton):
def paintEvent(self, ev):
QtGui.QPushButton.paintEvent(self, ev)
- margin = 7
+ margin = self.margin
geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin)
rect = self.path.boundingRect()
scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height()))
diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py
index 964307ae..6e10b13a 100644
--- a/pyqtgraph/widgets/PlotWidget.py
+++ b/pyqtgraph/widgets/PlotWidget.py
@@ -76,7 +76,7 @@ class PlotWidget(GraphicsView):
m = getattr(self.plotItem, attr)
if hasattr(m, '__call__'):
return m
- raise NameError(attr)
+ raise AttributeError(attr)
def viewRangeChanged(self, view, range):
#self.emit(QtCore.SIGNAL('viewChanged'), *args)
diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py
index 8c669be4..ae1826bb 100644
--- a/pyqtgraph/widgets/ProgressDialog.py
+++ b/pyqtgraph/widgets/ProgressDialog.py
@@ -2,9 +2,14 @@
from ..Qt import QtGui, QtCore
__all__ = ['ProgressDialog']
+
+
class ProgressDialog(QtGui.QProgressDialog):
"""
- Extends QProgressDialog for use in 'with' statements.
+ Extends QProgressDialog:
+
+ * Adds context management so the dialog may be used in `with` statements
+ * Allows nesting multiple progress dialogs
Example::
@@ -14,7 +19,10 @@ class ProgressDialog(QtGui.QProgressDialog):
if dlg.wasCanceled():
raise Exception("Processing canceled by user")
"""
- def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False):
+
+ allDialogs = []
+
+ def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False):
"""
============== ================================================================
**Arguments:**
@@ -29,8 +37,18 @@ class ProgressDialog(QtGui.QProgressDialog):
and calls to wasCanceled() will always return False.
If ProgressDialog is entered from a non-gui thread, it will
always be disabled.
+ nested (bool) If True, then this progress bar will be displayed inside
+ any pre-existing progress dialogs that also allow nesting.
============== ================================================================
"""
+ # attributes used for nesting dialogs
+ self.nestedLayout = None
+ self._nestableWidgets = None
+ self._nestingReady = False
+ self._topDialog = None
+ self._subBars = []
+ self.nested = nested
+
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
self.disabled = disable or (not isGuiThread)
if self.disabled:
@@ -42,20 +60,34 @@ class ProgressDialog(QtGui.QProgressDialog):
noCancel = True
self.busyCursor = busyCursor
-
+
QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent)
- self.setMinimumDuration(wait)
+
+ # If this will be a nested dialog, then we ignore the wait time
+ if nested is True and len(ProgressDialog.allDialogs) > 0:
+ self.setMinimumDuration(2**30)
+ else:
+ self.setMinimumDuration(wait)
+
self.setWindowModality(QtCore.Qt.WindowModal)
self.setValue(self.minimum())
if noCancel:
self.setCancelButton(None)
-
def __enter__(self):
if self.disabled:
return self
if self.busyCursor:
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
+
+ if self.nested and len(ProgressDialog.allDialogs) > 0:
+ topDialog = ProgressDialog.allDialogs[0]
+ topDialog._addSubDialog(self)
+ self._topDialog = topDialog
+ topDialog.canceled.connect(self.cancel)
+
+ ProgressDialog.allDialogs.append(self)
+
return self
def __exit__(self, exType, exValue, exTrace):
@@ -63,6 +95,12 @@ class ProgressDialog(QtGui.QProgressDialog):
return
if self.busyCursor:
QtGui.QApplication.restoreOverrideCursor()
+
+ if self._topDialog is not None:
+ self._topDialog._removeSubDialog(self)
+
+ ProgressDialog.allDialogs.pop(-1)
+
self.setValue(self.maximum())
def __iadd__(self, val):
@@ -72,6 +110,88 @@ class ProgressDialog(QtGui.QProgressDialog):
self.setValue(self.value()+val)
return self
+ def _addSubDialog(self, dlg):
+ # insert widgets from another dialog into this one.
+
+ # set a new layout and arrange children into it (if needed).
+ self._prepareNesting()
+
+ bar, btn = dlg._extractWidgets()
+
+ # where should we insert this widget? Find the first slot with a
+ # "removed" widget (that was left as a placeholder)
+ inserted = False
+ for i,bar2 in enumerate(self._subBars):
+ if bar2.hidden:
+ self._subBars.pop(i)
+ bar2.hide()
+ bar2.setParent(None)
+ self._subBars.insert(i, bar)
+ inserted = True
+ break
+ if not inserted:
+ self._subBars.append(bar)
+
+ # reset the layout
+ while self.nestedLayout.count() > 0:
+ self.nestedLayout.takeAt(0)
+ for b in self._subBars:
+ self.nestedLayout.addWidget(b)
+
+ def _removeSubDialog(self, dlg):
+ # don't remove the widget just yet; instead we hide it and leave it in
+ # as a placeholder.
+ bar, btn = dlg._extractWidgets()
+ bar.hide()
+
+ def _prepareNesting(self):
+ # extract all child widgets and place into a new layout that we can add to
+ if self._nestingReady is False:
+ # top layout contains progress bars + cancel button at the bottom
+ self._topLayout = QtGui.QGridLayout()
+ self.setLayout(self._topLayout)
+ self._topLayout.setContentsMargins(0, 0, 0, 0)
+
+ # A vbox to contain all progress bars
+ self.nestedVBox = QtGui.QWidget()
+ self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2)
+ self.nestedLayout = QtGui.QVBoxLayout()
+ self.nestedVBox.setLayout(self.nestedLayout)
+
+ # re-insert all widgets
+ bar, btn = self._extractWidgets()
+ self.nestedLayout.addWidget(bar)
+ self._subBars.append(bar)
+ self._topLayout.addWidget(btn, 1, 1, 1, 1)
+ self._topLayout.setColumnStretch(0, 100)
+ self._topLayout.setColumnStretch(1, 1)
+ self._topLayout.setRowStretch(0, 100)
+ self._topLayout.setRowStretch(1, 1)
+
+ self._nestingReady = True
+
+ def _extractWidgets(self):
+ # return:
+ # 1. a single widget containing the label and progress bar
+ # 2. the cancel button
+
+ if self._nestableWidgets is None:
+ widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)]
+ label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0]
+ bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0]
+ btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0]
+
+ sw = ProgressWidget(label, bar)
+
+ self._nestableWidgets = (sw, btn)
+
+ return self._nestableWidgets
+
+ def resizeEvent(self, ev):
+ if self._nestingReady:
+ # don't let progress dialog manage widgets anymore.
+ return
+ return QtGui.QProgressDialog.resizeEvent(self, ev)
## wrap all other functions to make sure they aren't being called from non-gui threads
@@ -80,6 +200,11 @@ class ProgressDialog(QtGui.QProgressDialog):
return
QtGui.QProgressDialog.setValue(self, val)
+ # Qt docs say this should happen automatically, but that doesn't seem
+ # to be the case.
+ if self.windowModality() == QtCore.Qt.WindowModal:
+ QtGui.QApplication.processEvents()
+
def setLabelText(self, val):
if self.disabled:
return
@@ -109,4 +234,29 @@ class ProgressDialog(QtGui.QProgressDialog):
if self.disabled:
return 0
return QtGui.QProgressDialog.minimum(self)
+
+
+class ProgressWidget(QtGui.QWidget):
+ """Container for a label + progress bar that also allows its child widgets
+ to be hidden without changing size.
+ """
+ def __init__(self, label, bar):
+ QtGui.QWidget.__init__(self)
+ self.hidden = False
+ self.layout = QtGui.QVBoxLayout()
+ self.setLayout(self.layout)
+ self.label = label
+ self.bar = bar
+ self.layout.addWidget(label)
+ self.layout.addWidget(bar)
+
+ def eventFilter(self, obj, ev):
+ return ev.type() == QtCore.QEvent.Paint
+
+ def hide(self):
+ # hide label and bar, but continue occupying the same space in the layout
+ for widget in (self.label, self.bar):
+ widget.installEventFilter(self)
+ widget.update()
+ self.hidden = True
diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py
index 657701f9..ef1d7a38 100644
--- a/pyqtgraph/widgets/RawImageWidget.py
+++ b/pyqtgraph/widgets/RawImageWidget.py
@@ -122,7 +122,7 @@ if HAVE_OPENGL:
if not self.uploaded:
self.uploadTexture()
- glViewport(0, 0, self.width(), self.height())
+ glViewport(0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio())
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture)
glColor4f(1,1,1,1)
diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py
index cca40e65..bd0eb908 100644
--- a/pyqtgraph/widgets/ScatterPlotWidget.py
+++ b/pyqtgraph/widgets/ScatterPlotWidget.py
@@ -33,6 +33,8 @@ class ScatterPlotWidget(QtGui.QSplitter):
specifying multiple criteria.
4) A PlotWidget for displaying the data.
"""
+ sigScatterPlotClicked = QtCore.Signal(object, object)
+
def __init__(self, parent=None):
QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal)
self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical)
@@ -81,7 +83,19 @@ class ScatterPlotWidget(QtGui.QSplitter):
item = self.fieldList.addItem(item)
self.filter.setFields(fields)
self.colorMap.setFields(fields)
-
+
+ def setSelectedFields(self, *fields):
+ self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged)
+ try:
+ self.fieldList.clearSelection()
+ for f in fields:
+ i = self.fields.keys().index(f)
+ item = self.fieldList.item(i)
+ item.setSelected(True)
+ finally:
+ self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
+ self.fieldSelectionChanged()
+
def setData(self, data):
"""
Set the data to be processed and displayed.
@@ -112,7 +126,6 @@ class ScatterPlotWidget(QtGui.QSplitter):
else:
self.filterText.setText('\n'.join(desc))
self.filterText.setVisible(True)
-
def updatePlot(self):
self.plot.clear()
@@ -175,9 +188,9 @@ class ScatterPlotWidget(QtGui.QSplitter):
## mask out any nan values
mask = np.ones(len(xy[0]), dtype=bool)
if xy[0].dtype.kind == 'f':
- mask &= ~np.isnan(xy[0])
+ mask &= np.isfinite(xy[0])
if xy[1] is not None and xy[1].dtype.kind == 'f':
- mask &= ~np.isnan(xy[1])
+ mask &= np.isfinite(xy[1])
xy[0] = xy[0][mask]
style['symbolBrush'] = colors[mask]
@@ -211,8 +224,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
-
def plotClicked(self, plot, points):
- pass
+ self.sigScatterPlotClicked.emit(self, points)
diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py
index b8066cd7..ea59bf31 100644
--- a/pyqtgraph/widgets/SpinBox.py
+++ b/pyqtgraph/widgets/SpinBox.py
@@ -106,11 +106,11 @@ class SpinBox(QtGui.QAbstractSpinBox):
self.skipValidate = False
self.setCorrectionMode(self.CorrectToPreviousValue)
self.setKeyboardTracking(False)
+ self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay'])
self.setOpts(**kwargs)
self._updateHeight()
self.editingFinished.connect(self.editingFinishedEvent)
- self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay'])
def event(self, ev):
ret = QtGui.QAbstractSpinBox.event(self, ev)
@@ -518,7 +518,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
# tokenize into numerical value, si prefix, and suffix
try:
- val, siprefix, suffix = fn.siParse(strn, self.opts['regex'])
+ val, siprefix, suffix = fn.siParse(strn, self.opts['regex'], suffix=self.opts['suffix'])
except Exception:
return False
diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py
index b98da6fa..b0ec54c1 100644
--- a/pyqtgraph/widgets/TreeWidget.py
+++ b/pyqtgraph/widgets/TreeWidget.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
-from weakref import *
from ..Qt import QtGui, QtCore
-from ..python2_3 import xrange
-
+from weakref import *
__all__ = ['TreeWidget', 'TreeWidgetItem']
@@ -13,15 +11,23 @@ class TreeWidget(QtGui.QTreeWidget):
This class demonstrates the absurd lengths one must go to to make drag/drop work."""
sigItemMoved = QtCore.Signal(object, object, object) # (item, parent, index)
+ sigItemCheckStateChanged = QtCore.Signal(object, object)
+ sigItemTextChanged = QtCore.Signal(object, object)
+ sigColumnCountChanged = QtCore.Signal(object, object) # self, count
def __init__(self, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
- #self.itemWidgets = WeakKeyDictionary()
+
+ # wrap this item so that we can propagate tree change information
+ # to children.
+ self._invRootItem = InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self))
+
self.setAcceptDrops(True)
self.setDragEnabled(True)
self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked)
self.placeholders = []
self.childNestingLimit = None
+ self.itemClicked.connect(self._itemClicked)
def setItemWidget(self, item, col, wid):
"""
@@ -42,7 +48,7 @@ class TreeWidget(QtGui.QTreeWidget):
def itemWidget(self, item, col):
w = QtGui.QTreeWidget.itemWidget(self, item, col)
- if w is not None:
+ if w is not None and hasattr(w, 'realChild'):
w = w.realChild
return w
@@ -141,7 +147,6 @@ class TreeWidget(QtGui.QTreeWidget):
QtGui.QTreeWidget.dropEvent(self, ev)
self.updateDropFlags()
-
def updateDropFlags(self):
### intended to put a limit on how deep nests of children can go.
### self.childNestingLimit is upheld when moving items without children, but if the item being moved has children/grandchildren, the children/grandchildren
@@ -165,10 +170,8 @@ class TreeWidget(QtGui.QTreeWidget):
def informTreeWidgetChange(item):
if hasattr(item, 'treeWidgetChanged'):
item.treeWidgetChanged()
- else:
- for i in xrange(item.childCount()):
- TreeWidget.informTreeWidgetChange(item.child(i))
-
+ for i in range(item.childCount()):
+ TreeWidget.informTreeWidgetChange(item.child(i))
def addTopLevelItem(self, item):
QtGui.QTreeWidget.addTopLevelItem(self, item)
@@ -209,21 +212,59 @@ class TreeWidget(QtGui.QTreeWidget):
## Why do we want to do this? It causes RuntimeErrors.
#for item in items:
#self.informTreeWidgetChange(item)
+
+ def invisibleRootItem(self):
+ return self._invRootItem
-
+ def itemFromIndex(self, index):
+ """Return the item and column corresponding to a QModelIndex.
+ """
+ col = index.column()
+ rows = []
+ while index.row() >= 0:
+ rows.insert(0, index.row())
+ index = index.parent()
+ item = self.topLevelItem(rows[0])
+ for row in rows[1:]:
+ item = item.child(row)
+ return item, col
+
+ def setColumnCount(self, c):
+ QtGui.QTreeWidget.setColumnCount(self, c)
+ self.sigColumnCountChanged.emit(self, c)
+
+ def _itemClicked(self, item, col):
+ if hasattr(item, 'itemClicked'):
+ item.itemClicked(col)
+
+
class TreeWidgetItem(QtGui.QTreeWidgetItem):
"""
- TreeWidgetItem that keeps track of its own widgets.
- Widgets may be added to columns before the item is added to a tree.
+ TreeWidgetItem that keeps track of its own widgets and expansion state.
+
+ * Widgets may be added to columns before the item is added to a tree.
+ * Expanded state may be set before item is added to a tree.
+ * Adds setCheked and isChecked methods.
+ * Adds addChildren, insertChildren, and takeChildren methods.
"""
def __init__(self, *args):
QtGui.QTreeWidgetItem.__init__(self, *args)
self._widgets = {} # col: widget
self._tree = None
-
+ self._expanded = False
def setChecked(self, column, checked):
self.setCheckState(column, QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked)
+
+ def isChecked(self, col):
+ return self.checkState(col) == QtCore.Qt.Checked
+
+ def setExpanded(self, exp):
+ self._expanded = exp
+ QtGui.QTreeWidgetItem.setExpanded(self, exp)
+
+ def isExpanded(self):
+ return self._expanded
def setWidget(self, column, widget):
if column in self._widgets:
@@ -251,7 +292,11 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem):
return
for col, widget in self._widgets.items():
tree.setItemWidget(self, col, widget)
-
+ QtGui.QTreeWidgetItem.setExpanded(self, self._expanded)
+
+ def childItems(self):
+ return [self.child(i) for i in range(self.childCount())]
+
def addChild(self, child):
QtGui.QTreeWidgetItem.addChild(self, child)
TreeWidget.informTreeWidgetChange(child)
@@ -285,4 +330,67 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem):
TreeWidget.informTreeWidgetChange(child)
return childs
+ def setData(self, column, role, value):
+ # credit: ekhumoro
+ # http://stackoverflow.com/questions/13662020/how-to-implement-itemchecked-and-itemunchecked-signals-for-qtreewidget-in-pyqt4
+ checkstate = self.checkState(column)
+ text = self.text(column)
+ QtGui.QTreeWidgetItem.setData(self, column, role, value)
+ treewidget = self.treeWidget()
+ if treewidget is None:
+ return
+ if (role == QtCore.Qt.CheckStateRole and checkstate != self.checkState(column)):
+ treewidget.sigItemCheckStateChanged.emit(self, column)
+ elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)):
+ treewidget.sigItemTextChanged.emit(self, column)
+
+ def itemClicked(self, col):
+ """Called when this item is clicked on.
+
+ Override this method to react to user clicks.
+ """
+
+
+class InvisibleRootItem(object):
+ """Wrapper around a TreeWidget's invisible root item that calls
+ TreeWidget.informTreeWidgetChange when child items are added/removed.
+ """
+ def __init__(self, item):
+ self._real_item = item
+
+ def addChild(self, child):
+ self._real_item.addChild(child)
+ TreeWidget.informTreeWidgetChange(child)
+
+ def addChildren(self, childs):
+ self._real_item.addChildren(childs)
+ for child in childs:
+ TreeWidget.informTreeWidgetChange(child)
+
+ def insertChild(self, index, child):
+ self._real_item.insertChild(index, child)
+ TreeWidget.informTreeWidgetChange(child)
+
+ def insertChildren(self, index, childs):
+ self._real_item.addChildren(index, childs)
+ for child in childs:
+ TreeWidget.informTreeWidgetChange(child)
+
+ def removeChild(self, child):
+ self._real_item.removeChild(child)
+ TreeWidget.informTreeWidgetChange(child)
+
+ def takeChild(self, index):
+ child = self._real_item.takeChild(index)
+ TreeWidget.informTreeWidgetChange(child)
+ return child
+
+ def takeChildren(self):
+ childs = self._real_item.takeChildren()
+ for child in childs:
+ TreeWidget.informTreeWidgetChange(child)
+ return childs
+
+ def __getattr__(self, attr):
+ return getattr(self._real_item, attr)
diff --git a/pyqtgraph/widgets/__init__.py b/pyqtgraph/widgets/__init__.py
index a81fe391..e69de29b 100644
--- a/pyqtgraph/widgets/__init__.py
+++ b/pyqtgraph/widgets/__init__.py
@@ -1,21 +0,0 @@
-## just import everything from sub-modules
-
-#import os
-
-#d = os.path.split(__file__)[0]
-#files = []
-#for f in os.listdir(d):
- #if os.path.isdir(os.path.join(d, f)):
- #files.append(f)
- #elif f[-3:] == '.py' and f != '__init__.py':
- #files.append(f[:-3])
-
-#for modName in files:
- #mod = __import__(modName, globals(), locals(), fromlist=['*'])
- #if hasattr(mod, '__all__'):
- #names = mod.__all__
- #else:
- #names = [n for n in dir(mod) if n[0] != '_']
- #for k in names:
- #print modName, k
- #globals()[k] = getattr(mod, k)
diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py
index 10087881..cff97da7 100644
--- a/pyqtgraph/widgets/tests/test_spinbox.py
+++ b/pyqtgraph/widgets/tests/test_spinbox.py
@@ -18,6 +18,8 @@ def test_spinbox_formatting():
(12345678955, '12345678955', dict(int=True, decimals=100)),
(1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)),
(1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)),
+ (1.45, '1.45 PSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)),
+ (1.45e-3, '1.45 mPSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)),
(-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')),
]
@@ -26,3 +28,14 @@ def test_spinbox_formatting():
sb.setValue(value)
assert sb.value() == value
assert pg.asUnicode(sb.text()) == text
+
+ # test setting value
+ if not opts.get('int', False):
+ suf = sb.opts['suffix']
+ sb.lineEdit().setText('0.1' + suf)
+ sb.editingFinishedEvent()
+ assert sb.value() == 0.1
+ if suf != '':
+ sb.lineEdit().setText('0.1 m' + suf)
+ sb.editingFinishedEvent()
+ assert sb.value() == 0.1e-3
diff --git a/test.py b/test.py
new file mode 100644
index 00000000..b07fb1cf
--- /dev/null
+++ b/test.py
@@ -0,0 +1,24 @@
+"""
+Script for invoking pytest with options to select Qt library
+"""
+
+import sys
+import pytest
+
+args = sys.argv[1:]
+if '--pyside' in args:
+ args.remove('--pyside')
+ import PySide
+elif '--pyqt4' in args:
+ args.remove('--pyqt4')
+ import PyQt4
+elif '--pyqt5' in args:
+ args.remove('--pyqt5')
+ import PyQt5
+
+import pyqtgraph as pg
+pg.systemInfo()
+
+pytest.main(args)
+
+
\ No newline at end of file