Merge branch 'develop' into pyside2-uic

This commit is contained in:
Ogi Moore 2020-06-03 21:48:16 -07:00 committed by GitHub
commit 47f06e78be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 771 additions and 281 deletions

View File

@ -128,16 +128,19 @@ jobs:
displayName: 'Install Wheel'
- bash: |
sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm
sudo apt-get install -y libxkbcommon-x11-dev
# workaround for QTBUG-84489
sudo apt-get install -y libxcb-xfixes0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
if [ $(install.method) == "conda" ]
then
source activate test-environment-$(python.version)
fi
pip install pytest-xvfb
pip install PyVirtualDisplay==0.2.5 pytest-xvfb
displayName: "Virtual Display Setup"
condition: eq(variables['agent.os'], 'Linux' )
- bash: |
export QT_DEBUG_PLUGINS=1
if [ $(install.method) == "conda" ]
then
source activate test-environment-$(python.version)

View File

@ -11,7 +11,9 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import sys
import os
from datetime import datetime
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -19,6 +21,7 @@ import sys, os
path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(path, '..', '..'))
sys.path.insert(0, os.path.join(path, '..', 'extensions'))
import pyqtgraph
# -- General configuration -----------------------------------------------------
@ -43,16 +46,16 @@ master_doc = 'index'
# General information about the project.
project = 'pyqtgraph'
copyright = '2011, Luke Campagnola'
copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year)
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.10.0'
version = pyqtgraph.__version__
# The full version, including alpha/beta/rc tags.
release = '0.10.0'
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">

View File

@ -41,7 +41,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8))
self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8))
self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -36,7 +36,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8))
self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8))
self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">

View File

@ -89,7 +89,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.graphicsSystemCombo.setItemText(0, _translate("Form", "default", None))
self.graphicsSystemCombo.setItemText(1, _translate("Form", "native", None))
self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster", None))

View File

@ -78,7 +78,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.graphicsSystemCombo.setItemText(0, _translate("Form", "default"))
self.graphicsSystemCombo.setItemText(1, _translate("Form", "native"))
self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster"))

View File

@ -78,7 +78,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(0, QtGui.QApplication.translate("Form", "default", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(1, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(2, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -96,6 +96,16 @@ params = [
{'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True},
{'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True},
]},
{'name': 'Custom context menu', 'type': 'group', 'children': [
{'name': 'List contextMenu', 'type': 'float', 'value': 0, 'context': [
'menu1',
'menu2'
]},
{'name': 'Dict contextMenu', 'type': 'float', 'value': 0, 'context': {
'changeName': 'Title',
'internal': 'What the user sees',
}},
]},
ComplexParameter(name='Custom parameter group (reciprocal values)'),
ScalableGroup(name="Expandable Parameter Group", children=[
{'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"},

View File

@ -23,8 +23,6 @@ class ExportDialog(QtGui.QWidget):
self.currentExporter = None
self.scene = scene
self.exporterParameters = {}
self.selectBox = QtGui.QGraphicsRectItem()
self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine))
self.selectBox.hide()
@ -124,16 +122,7 @@ class ExportDialog(QtGui.QWidget):
expClass = self.exporterClasses[str(item.text())]
exp = expClass(item=self.ui.itemTree.currentItem().gitem)
if prev:
oldtext = str(prev.text())
self.exporterParameters[oldtext] = self.currentExporter.parameters()
newtext = str(item.text())
if newtext in self.exporterParameters.keys():
params = self.exporterParameters[newtext]
exp.params = params
else:
params = exp.parameters()
self.exporterParameters[newtext] = params
if params is None:
self.ui.paramTree.clear()

View File

@ -1,3 +1,4 @@
import numpy as np
class PlotData(object):
@ -50,7 +51,3 @@ class PlotData(object):
mn = np.min(self[field])
self.minVals[field] = mn
return mn

View File

@ -413,12 +413,20 @@ def plot(*args, **kargs):
dataArgs[k] = kargs[k]
w = PlotWindow(**pwArgs)
w.sigClosed.connect(_plotWindowClosed)
if len(args) > 0 or len(dataArgs) > 0:
w.plot(*args, **dataArgs)
plots.append(w)
w.show()
return w
def _plotWindowClosed(w):
w.close()
try:
plots.remove(w)
except ValueError:
pass
def image(*args, **kargs):
"""
Create and return an :class:`ImageWindow <pyqtgraph.ImageWindow>`
@ -429,11 +437,19 @@ def image(*args, **kargs):
"""
mkQApp()
w = ImageWindow(*args, **kargs)
w.sigClosed.connect(_imageWindowClosed)
images.append(w)
w.show()
return w
show = image ## for backward compatibility
def _imageWindowClosed(w):
w.close()
try:
images.remove(w)
except ValueError:
pass
def dbg(*args, **kwds):
"""
Create a console window and begin watching for exceptions.

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="margin">

View File

@ -91,7 +91,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.autoRangeBtn.setText(_translate("Form", "Auto Range", None))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None))
self.redirectCheck.setText(_translate("Form", "Redirect", None))

View File

@ -79,7 +79,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.autoRangeBtn.setText(_translate("Form", "Auto Range"))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas."))
self.redirectCheck.setText(_translate("Form", "Redirect"))

View File

@ -80,7 +80,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -17,7 +17,7 @@
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">

View File

@ -59,7 +59,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.translateLabel.setText(_translate("Form", "Translate:", None))
self.rotateLabel.setText(_translate("Form", "Rotate:", None))
self.scaleLabel.setText(_translate("Form", "Scale:", None))

View File

@ -46,7 +46,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.translateLabel.setText(_translate("Form", "Translate:"))
self.rotateLabel.setText(_translate("Form", "Rotate:"))
self.scaleLabel.setText(_translate("Form", "Scale:"))

View File

@ -46,7 +46,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8))
self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8))
self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -223,6 +223,7 @@ class Dock(QtGui.QWidget, DockDrop):
def close(self):
"""Remove this dock from the DockArea it lives inside."""
self.setParent(None)
QtGui.QLabel.close(self.label)
self.label.setParent(None)
self._container.apoptose()
self._container = None

View File

@ -3,6 +3,7 @@ from ..Qt import QtGui, QtCore
from .Exporter import Exporter
from ..parametertree import Parameter
from .. import PlotItem
from ..python2_3 import asUnicode
__all__ = ['CSVExporter']
@ -57,7 +58,7 @@ class CSVExporter(Exporter):
sep = '\t'
with open(fileName, 'w') as fd:
fd.write(sep.join(header) + '\n')
fd.write(sep.join(map(asUnicode, header)) + '\n')
i = 0
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])

View File

@ -45,14 +45,19 @@ class ImageExporter(Exporter):
def parameters(self):
return self.params
def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy:
@staticmethod
def getSupportedImageFormats():
filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
if p in filter:
filter.remove(p)
filter.insert(0, p)
return filter
def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy:
filter = self.getSupportedImageFormats()
self.fileSaveDialog(filter=filter)
return

View File

@ -763,6 +763,9 @@ class FlowchartCtrlWidget(QtGui.QWidget):
item = self.items[node]
self.ui.ctrlList.setCurrentItem(item)
def clearSelection(self):
self.ui.ctrlList.selectionModel().clearSelection()
class FlowchartWidget(dockarea.DockArea):
"""Includes the actual graphical flowchart and debugging interface"""
@ -890,7 +893,10 @@ class FlowchartWidget(dockarea.DockArea):
item = items[0]
if hasattr(item, 'node') and isinstance(item.node, Node):
n = item.node
if n in self.ctrl.items:
self.ctrl.select(n)
else:
self.ctrl.clearSelection()
data = {'outputs': n.outputValues(), 'inputs': n.inputValues()}
self.selNameLabel.setText(n.name())
if hasattr(n, 'nodeName'):

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">

View File

@ -69,7 +69,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.loadBtn.setText(_translate("Form", "Load..", None))
self.saveBtn.setText(_translate("Form", "Save", None))
self.saveAsBtn.setText(_translate("Form", "As..", None))

View File

@ -56,7 +56,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.loadBtn.setText(_translate("Form", "Load.."))
self.saveBtn.setText(_translate("Form", "Save"))
self.saveAsBtn.setText(_translate("Form", "As.."))

View File

@ -55,7 +55,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8))
self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8))
self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<widget class="QWidget" name="selInfoWidget" native="true">
<property name="geometry">

View File

@ -62,7 +62,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
from ..widgets.DataTreeWidget import DataTreeWidget

View File

@ -49,7 +49,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
from ..widgets.DataTreeWidget import DataTreeWidget
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView

View File

@ -48,7 +48,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
from ..widgets.DataTreeWidget import DataTreeWidget

View File

@ -2,6 +2,7 @@
from ..Node import Node
from ...Qt import QtGui, QtCore
import numpy as np
import sys
from .common import *
from ...SRTTransform import SRTTransform
from ...Point import Point
@ -238,7 +239,12 @@ class EvalNode(Node):
fn = "def fn(**args):\n"
run = "\noutput=fn(**args)\n"
text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run
if sys.version_info.major == 2:
exec(text)
elif sys.version_info.major == 3:
ldict = locals()
exec(text, globals(), ldict)
output = ldict['output']
except:
print("Error processing node: %s" % self.name())
raise

View File

@ -4,6 +4,7 @@ from ..python2_3 import asUnicode
import numpy as np
from ..Point import Point
from .. import debug as debug
import sys
import weakref
from .. import functions as fn
from .. import getConfigOption
@ -44,11 +45,8 @@ class AxisItem(GraphicsWidget):
GraphicsWidget.__init__(self, parent)
self.label = QtGui.QGraphicsTextItem(self)
self.picture = None
self.orientation = orientation
if orientation not in ['left', 'right', 'top', 'bottom']:
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
if orientation in ['left', 'right']:
self.label.rotate(-90)
self.orientation = None
self.setOrientation(orientation)
self.style = {
'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis
@ -110,6 +108,27 @@ class AxisItem(GraphicsWidget):
self.grid = False
#self.setCacheMode(self.DeviceCoordinateCache)
def setOrientation(self, orientation):
"""
orientation = 'left', 'right', 'top', 'bottom'
"""
if orientation != self.orientation:
if orientation not in ['left', 'right', 'top', 'bottom']:
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
#rotate absolute allows to change orientation multiple times:
if orientation in ['left', 'right']:
self.label.setRotation(-90)
if self.orientation:
self._updateWidth()
self.setMaximumHeight(16777215)
else:
self.label.setRotation(0)
if self.orientation:
self._updateHeight()
self.setMaximumWidth(16777215)
self.orientation = orientation
def setStyle(self, **kwds):
"""
Set various style options.
@ -513,6 +532,7 @@ class AxisItem(GraphicsWidget):
self.unlinkFromView()
self._linkedView = weakref.ref(view)
if self.orientation in ['right', 'left']:
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
@ -813,7 +833,37 @@ class AxisItem(GraphicsWidget):
return strings
def logTickStrings(self, values, scale, spacing):
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)]
estrings = ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)]
if sys.version_info < (3, 0):
# python 2 does not support unicode strings like that
return estrings
else: # python 3+
convdict = {"0": "",
"1": "¹",
"2": "²",
"3": "³",
"4": "",
"5": "",
"6": "",
"7": "",
"8": "",
"9": "",
}
dstrings = []
for e in estrings:
if e.count("e"):
v, p = e.split("e")
sign = "" if p[0] == "-" else ""
pot = "".join([convdict[pp] for pp in p[1:].lstrip("0")])
if v == "1":
v = ""
else:
v = v + "·"
dstrings.append(v + "10" + sign + pot)
else:
dstrings.append(e)
return dstrings
def generateDrawSpecs(self, p):
"""
@ -1110,23 +1160,26 @@ class AxisItem(GraphicsWidget):
self._updateHeight()
def wheelEvent(self, ev):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
if self.orientation in ['left', 'right']:
self.linkedView().wheelEvent(ev, axis=1)
lv.wheelEvent(ev, axis=1)
else:
self.linkedView().wheelEvent(ev, axis=0)
lv.wheelEvent(ev, axis=0)
ev.accept()
def mouseDragEvent(self, event):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
if self.orientation in ['left', 'right']:
return self.linkedView().mouseDragEvent(event, axis=1)
return lv.mouseDragEvent(event, axis=1)
else:
return self.linkedView().mouseDragEvent(event, axis=0)
return lv.mouseDragEvent(event, axis=0)
def mouseClickEvent(self, event):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
return self.linkedView().mouseClickEvent(event)
return lv.mouseClickEvent(event)

View File

@ -461,6 +461,19 @@ class GradientEditorItem(TickSliderItem):
self.addTick(1, QtGui.QColor(255,0,0), True)
self.setColorMode('rgb')
self.updateGradient()
self.linkedGradients = {}
def showTicks(self, show=True):
for tick in self.ticks.keys():
if show:
tick.show()
orig = getattr(self, '_allowAdd_backup', None)
if orig:
self.allowAdd = orig
else:
self._allowAdd_backup = self.allowAdd
self.allowAdd = False #block tick creation
tick.hide()
def setOrientation(self, orientation):
## public
@ -764,7 +777,9 @@ class GradientEditorItem(TickSliderItem):
for t in self.ticks:
c = t.color
ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha())))
state = {'mode': self.colorMode, 'ticks': ticks}
state = {'mode': self.colorMode,
'ticks': ticks,
'ticksVisible': next(iter(self.ticks)).isVisible()}
return state
def restoreState(self, state):
@ -789,6 +804,8 @@ class GradientEditorItem(TickSliderItem):
for t in state['ticks']:
c = QtGui.QColor(*t[1])
self.addTick(t[0], c, finish=False)
self.showTicks( state.get('ticksVisible',
next(iter(self.ticks)).isVisible()) )
self.updateGradient()
self.sigGradientChangeFinished.emit(self)
@ -804,6 +821,18 @@ class GradientEditorItem(TickSliderItem):
self.updateGradient()
self.sigGradientChangeFinished.emit(self)
def linkGradient(self, slaveGradient, connect=True):
if connect:
fn = lambda g, slave=slaveGradient:slave.restoreState(
g.saveState())
self.linkedGradients[id(slaveGradient)] = fn
self.sigGradientChanged.connect(fn)
self.sigGradientChanged.emit(self)
else:
fn = self.linkedGradients.get(id(slaveGradient), None)
if fn:
self.sigGradientChanged.disconnect(fn)
class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsObject instead results in
## activating this bug: https://bugreports.qt-project.org/browse/PYSIDE-86
@ -874,8 +903,8 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO
self.view().tickMoveFinished(self)
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton and self.moving:
ev.accept()
if ev.button() == QtCore.Qt.RightButton and self.moving:
self.setPos(self.startPosition)
self.view().tickMoved(self, self.startPosition)
self.moving = False
@ -883,7 +912,6 @@ class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsO
self.sigMoved.emit(self)
else:
self.view().tickClicked(self, ev)
##remove
def hoverEvent(self, ev):
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):

View File

@ -19,6 +19,7 @@ class GraphicsItem(object):
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
"""
_pixelVectorGlobalCache = LRUCache(100, 70)
_mapRectFromViewGlobalCache = LRUCache(100, 70)
def __init__(self, register=None):
if not hasattr(self, '_qtBaseClass'):
@ -188,24 +189,23 @@ class GraphicsItem(object):
## (such as when looking at unix timestamps), we can get floating-point errors.
dt.setMatrix(dt.m11(), dt.m12(), 0, dt.m21(), dt.m22(), 0, 0, 0, 1)
if direction is None:
direction = QtCore.QPointF(1, 0)
elif direction.manhattanLength() == 0:
raise Exception("Cannot compute pixel length for 0-length vector.")
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22(), direction.x(), direction.y())
## check local cache
if direction is None and dt == self._pixelVectorCache[0]:
if key == self._pixelVectorCache[0]:
return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy*
## check global cache
#key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22())
pv = self._pixelVectorGlobalCache.get(key, None)
if direction is None and pv is not None:
self._pixelVectorCache = [dt, pv]
if pv is not None:
self._pixelVectorCache = [key, pv]
return tuple(map(Point,pv)) ## return a *copy*
if direction is None:
direction = QtCore.QPointF(1, 0)
if direction.manhattanLength() == 0:
raise Exception("Cannot compute pixel length for 0-length vector.")
## attempt to re-scale direction vector to fit within the precision of the coordinate system
## Here's the problem: we need to map the vector 'direction' from the item to the device, via transform 'dt'.
## In some extreme cases, this mapping can fail unless the length of 'direction' is cleverly chosen.
@ -368,8 +368,21 @@ class GraphicsItem(object):
vt = self.viewTransform()
if vt is None:
return None
vt = fn.invertQTransform(vt)
return vt.mapRect(obj)
cache = self._mapRectFromViewGlobalCache
k = (
vt.m11(), vt.m12(), vt.m13(),
vt.m21(), vt.m22(), vt.m23(),
vt.m31(), vt.m32(), vt.m33(),
)
try:
inv_vt = cache[k]
except KeyError:
inv_vt = fn.invertQTransform(vt)
cache[k] = inv_vt
return inv_vt.mapRect(obj)
def pos(self):
return Point(self._qtBaseClass.pos(self))

View File

@ -134,6 +134,9 @@ class GraphicsLayout(GraphicsWidget):
item.geometryChanged.connect(self._updateItemBorder)
self.layout.addItem(item, row, col, rowspan, colspan)
self.layout.activate() # Update layout, recalculating bounds.
# Allows some PyQtGraph features to also work without Qt event loop.
self.nextColumn()
def getItem(self, row, col):

View File

@ -51,6 +51,7 @@ class ImageItem(GraphicsObject):
self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None
self.autoDownsample = False
self._lastDownsample = (1, 1)
self.axisOrder = getConfigOption('imageAxisOrder')
@ -551,6 +552,17 @@ class ImageItem(GraphicsObject):
def viewTransformChanged(self):
if self.autoDownsample:
o = self.mapToDevice(QtCore.QPointF(0,0))
x = self.mapToDevice(QtCore.QPointF(1,0))
y = self.mapToDevice(QtCore.QPointF(0,1))
w = Point(x-o).length()
h = Point(y-o).length()
if w == 0 or h == 0:
self.qimage = None
return
xds = max(1, int(1.0 / w))
yds = max(1, int(1.0 / h))
if (xds, yds) != self._lastDownsample:
self.qimage = None
self.update()

View File

@ -153,9 +153,9 @@ class PlotItem(GraphicsWidget):
self.legend = None
# Initialize axis items
## Create and place axis items
self.axes = {}
self.setAxisItems(axisItems)
self.setAxes(axisItems)
self.titleLabel = LabelItem('', size='11pt', parent=self)
self.layout.addItem(self.titleLabel, 0, 1)
@ -260,6 +260,43 @@ class PlotItem(GraphicsWidget):
if len(kargs) > 0:
self.plot(**kargs)
def setAxes(self, axisItems):
"""
Create and place axis items
For valid values for axisItems see __init__
"""
for v in self.axes.values():
item = v['item']
self.layout.removeItem(item)
self.vb.removeItem(item)
self.axes = {}
if axisItems is None:
axisItems = {}
for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))):
axis = axisItems.get(k, None)
if axis:
axis.setOrientation(k)
else:
axis = AxisItem(orientation=k)
axis.linkToView(self.vb)
self.axes[k] = {'item': axis, 'pos': pos}
self.layout.addItem(axis, *pos)
axis.setZValue(-1000)
axis.setFlag(axis.ItemNegativeZStacksBehindParent)
#show/hide axes:
all_dir = ['left', 'bottom', 'right', 'top']
if axisItems:
to_show = list(axisItems.keys())
to_hide = [a for a in all_dir if a not in to_show]
else:
to_show = all_dir[:2]
to_hide = all_dir[2:]
for a in to_hide:
self.hideAxis(a)
for a in to_show:
self.showAxis(a)
def implements(self, interface=None):
return interface in ['ViewBoxWrapper']
@ -1123,8 +1160,8 @@ class PlotItem(GraphicsWidget):
Show or hide one of the plot's axes.
axis must be one of 'left', 'bottom', 'right', or 'top'
"""
s = self.getScale(axis)
p = self.axes[axis]['pos']
s = self.getAxis(axis)
#p = self.axes[axis]['pos']
if show:
s.show()
else:

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<widget class="QGroupBox" name="averageGroup">
<property name="geometry">

View File

@ -148,7 +148,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.averageGroup.setToolTip(_translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None))
self.averageGroup.setTitle(_translate("Form", "Average", None))
self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None))

View File

@ -135,7 +135,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.averageGroup.setToolTip(_translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available)."))
self.averageGroup.setTitle(_translate("Form", "Average"))
self.clipToViewCheck.setToolTip(_translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced."))

View File

@ -134,7 +134,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8))
self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -15,6 +15,7 @@ from ..pgcollections import OrderedDict
from .. import debug
from ..python2_3 import basestring
__all__ = ['ScatterPlotItem', 'SpotItem']
@ -128,8 +129,12 @@ class SymbolAtlas(object):
sourceRecti = None
symbol_map = self.symbolMap
for i, rec in enumerate(opts.tolist()):
size, symbol, pen, brush = rec[2: 6]
symbols = opts['symbol'].tolist()
sizes = opts['size'].tolist()
pens = opts['pen'].tolist()
brushes = opts['brush'].tolist()
for symbol, size, pen, brush in zip(symbols, sizes, pens, brushes):
key = id(symbol), size, id(pen), id(brush)
if key == keyi:
@ -560,6 +565,7 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate()
def updateSpots(self, dataSet=None):
if dataSet is None:
dataSet = self.data
@ -610,8 +616,6 @@ class ScatterPlotItem(GraphicsObject):
recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush'])
return recs
def measureSpotSizes(self, dataSet):
for rec in dataSet:
## keep track of the maximum spot size and pixel size
@ -630,7 +634,6 @@ class ScatterPlotItem(GraphicsObject):
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None]
def clear(self):
"""Remove all spots from the scatter plot"""
#self.clearItems()
@ -757,8 +760,10 @@ class ScatterPlotItem(GraphicsObject):
if self.opts['pxMode'] is True:
p.resetTransform()
data = self.data
# Map point coordinates to device
pts = np.vstack([self.data['x'], self.data['y']])
pts = np.vstack([data['x'], data['y']])
pts = self.mapPointsToDevice(pts)
if pts is None:
return
@ -770,25 +775,31 @@ class ScatterPlotItem(GraphicsObject):
# Draw symbols from pre-rendered atlas
atlas = self.fragmentAtlas.getAtlas()
target_rect = data['targetRect']
source_rect = data['sourceRect']
widths = data['width']
# Update targetRects if necessary
updateMask = viewMask & np.equal(self.data['targetRect'], None)
updateMask = viewMask & np.equal(target_rect, None)
if np.any(updateMask):
updatePts = pts[:,updateMask]
width = self.data[updateMask]['width']*2
self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
width = widths[updateMask] * 2
target_rect[updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
data = self.data[viewMask]
if QT_LIB == 'PyQt4':
p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas)
p.drawPixmapFragments(
target_rect[viewMask].tolist(),
source_rect[viewMask].tolist(),
atlas
)
else:
list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect']))
list(imap(p.drawPixmap, target_rect[viewMask].tolist(), repeat(atlas), source_rect[viewMask].tolist()))
else:
# render each symbol individually
p.setRenderHint(p.Antialiasing, aa)
data = self.data[viewMask]
pts = pts[:,viewMask]
for i, rec in enumerate(data):
for i, rec in enumerate(data[viewMask]):
p.resetTransform()
p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2)
drawSymbol(p, *self.getSpotOpts(rec, scale))

View File

@ -233,6 +233,17 @@ class ViewBox(GraphicsWidget):
if name is None:
self.updateViewLists()
def getAspectRatio(self):
'''return the current aspect ratio'''
rect = self.rect()
vr = self.viewRect()
if rect.height() == 0 or vr.width() == 0 or vr.height() == 0:
currentRatio = 1.0
else:
currentRatio = (rect.width()/float(rect.height())) / (
vr.width()/vr.height())
return currentRatio
def register(self, name):
"""
Add this ViewBox to the registered list of views.
@ -537,6 +548,10 @@ class ViewBox(GraphicsWidget):
self.enableAutoRange(x=xOff, y=yOff)
changed.append(True)
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]]
maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]]
for ax, range in changes.items():
mn = min(range)
mx = max(range)
@ -564,6 +579,39 @@ class ViewBox(GraphicsWidget):
mn -= p
mx += p
# max range cannot be larger than bounds, if they are given
if limits[ax][0] is not None and limits[ax][1] is not None:
if maxRng[ax] is not None:
maxRng[ax] = min(maxRng[ax], limits[ax][1] - limits[ax][0])
else:
maxRng[ax] = limits[ax][1] - limits[ax][0]
# If we have limits, we will have at least a max range as well
if maxRng[ax] is not None or minRng[ax] is not None:
diff = mx - mn
if maxRng[ax] is not None and diff > maxRng[ax]:
delta = maxRng[ax] - diff
elif minRng[ax] is not None and diff < minRng[ax]:
delta = minRng[ax] - diff
else:
delta = 0
mn -= delta / 2.
mx += delta / 2.
# Make sure our requested area is within limits, if any
if limits[ax][0] is not None or limits[ax][1] is not None:
lmn, lmx = limits[ax]
if lmn is not None and mn < lmn:
delta = lmn - mn # Shift the requested view to match our lower limit
mn = lmn
mx += delta
elif lmx is not None and mx > lmx:
delta = lmx - mx
mx = lmx
mn += delta
# Set target range
if self.state['targetRange'][ax] != [mn, mx]:
self.state['targetRange'][ax] = [mn, mx]
@ -1097,12 +1145,7 @@ class ViewBox(GraphicsWidget):
return
self.state['aspectLocked'] = False
else:
rect = self.rect()
vr = self.viewRect()
if rect.height() == 0 or vr.width() == 0 or vr.height() == 0:
currentRatio = 1.0
else:
currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height())
currentRatio = self.getAspectRatio()
if ratio is None:
ratio = currentRatio
if self.state['aspectLocked'] == ratio: # nothing to change
@ -1443,40 +1486,6 @@ class ViewBox(GraphicsWidget):
aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect()
bounds = self.rect()
if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]:
## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
## This is the view range aspect ratio we need to obey aspect constraint
viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect
viewRatio = 1 if viewRatio == 0 else viewRatio
# Decide which range to keep unchanged
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
if forceX:
ax = 0
elif forceY:
ax = 1
else:
# if we are not required to keep a particular axis unchanged,
# then make the entire target range visible
ax = 0 if targetRatio > viewRatio else 1
if ax == 0:
## view range needs to be taller than target
dy = 0.5 * (tr.width() / viewRatio - tr.height())
if dy != 0:
changed[1] = True
viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
else:
## view range needs to be wider than target
dx = 0.5 * (tr.height() * viewRatio - tr.width())
if dx != 0:
changed[0] = True
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
# ----------- Make corrections for view limits -----------
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]]
@ -1493,39 +1502,54 @@ class ViewBox(GraphicsWidget):
else:
maxRng[axis] = limits[axis][1] - limits[axis][0]
#print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis])
#print "Starting range:", viewRange[axis]
if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]:
# Apply xRange, yRange
diff = viewRange[axis][1] - viewRange[axis][0]
if maxRng[axis] is not None and diff > maxRng[axis]:
delta = maxRng[axis] - diff
changed[axis] = True
elif minRng[axis] is not None and diff < minRng[axis]:
delta = minRng[axis] - diff
changed[axis] = True
## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
## This is the view range aspect ratio we need to obey aspect constraint
viewRatio = (bounds.width() / bounds.height() if bounds.height() != 0 else 1) / aspect
viewRatio = 1 if viewRatio == 0 else viewRatio
# Calculate both the x and y ranges that would be needed to obtain the desired aspect ratio
dy = 0.5 * (tr.width() / viewRatio - tr.height())
dx = 0.5 * (tr.height() * viewRatio - tr.width())
rangeY = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
rangeX = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
canidateRange = [rangeX, rangeY]
# Decide which range to try to keep unchanged
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
if forceX:
ax = 0
elif forceY:
ax = 1
else:
delta = 0
# if we are not required to keep a particular axis unchanged,
# then try to make the entire target range visible
ax = 0 if targetRatio > viewRatio else 1
target = 0 if ax == 1 else 1
# See if this choice would cause out-of-range issues
if maxRng is not None or minRng is not None:
diff = canidateRange[target][1] - canidateRange[target][0]
if maxRng[target] is not None and diff > maxRng[target] or \
minRng[target] is not None and diff < minRng[target]:
# tweak the target range down so we can still pan properly
self.state['targetRange'][ax] = canidateRange[ax]
ax = target # Switch the "fixed" axes
viewRange[axis][0] -= delta/2.
viewRange[axis][1] += delta/2.
if ax == 0:
## view range needs to be taller than target
if dy != 0:
changed[1] = True
viewRange[1] = rangeY
else:
## view range needs to be wider than target
if dx != 0:
changed[0] = True
viewRange[0] = rangeX
#print "after applying min/max:", viewRange[axis]
# Apply xLimits, yLimits
mn, mx = limits[axis]
if mn is not None and viewRange[axis][0] < mn:
delta = mn - viewRange[axis][0]
viewRange[axis][0] += delta
viewRange[axis][1] += delta
changed[axis] = True
elif mx is not None and viewRange[axis][1] > mx:
delta = mx - viewRange[axis][1]
viewRange[axis][0] += delta
viewRange[axis][1] += delta
changed[axis] = True
#print "after applying edge limits:", viewRange[axis]
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
self.state['viewRange'] = viewRange

View File

@ -17,7 +17,7 @@
</size>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="margin">

View File

@ -78,7 +78,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.label.setText(_translate("Form", "Link Axis:", None))
self.linkCombo.setToolTip(_translate("Form", "<html><head/><body><p>Links this axis with another view. When linked, both views will display the same data range.</p></body></html>", None))
self.autoPercentSpin.setToolTip(_translate("Form", "<html><head/><body><p>Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.</p></body></html>", None))

View File

@ -65,7 +65,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.label.setText(_translate("Form", "Link Axis:"))
self.linkCombo.setToolTip(_translate("Form", "<html><head/><body><p>Links this axis with another view. When linked, both views will display the same data range.</p></body></html>"))
self.autoPercentSpin.setToolTip(_translate("Form", "<html><head/><body><p>Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.</p></body></html>"))

View File

@ -64,7 +64,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8))
self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "<html><head/><body><p>Links this axis with another view. When linked, both views will display the same data range.</p></body></html>", None, QtGui.QApplication.UnicodeUTF8))
self.autoPercentSpin.setToolTip(QtGui.QApplication.translate("Form", "<html><head/><body><p>Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.</p></body></html>", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -0,0 +1,200 @@
# -*- coding: utf-8 -*-
import pyqtgraph as pg
import pytest
app = pg.mkQApp()
def test_zoom_normal():
vb = pg.ViewBox()
testRange = pg.QtCore.QRect(0, 0, 10, 20)
vb.setRange(testRange, padding=0)
vbViewRange = vb.getState()['viewRange']
assert vbViewRange == [[testRange.left(), testRange.right()],
[testRange.top(), testRange.bottom()]]
def test_zoom_limit():
"""Test zooming with X and Y limits set"""
vb = pg.ViewBox()
vb.setLimits(xMin=0, xMax=10, yMin=0, yMax=10)
# Try zooming within limits. Should return unmodified
testRange = pg.QtCore.QRect(0, 0, 9, 9)
vb.setRange(testRange, padding=0)
vbViewRange = vb.getState()['viewRange']
assert vbViewRange == [[testRange.left(), testRange.right()],
[testRange.top(), testRange.bottom()]]
# And outside limits. both view range and targetRange should be set to limits
testRange = pg.QtCore.QRect(-5, -5, 16, 20)
vb.setRange(testRange, padding=0)
expected = [[0, 10], [0, 10]]
vbState = vb.getState()
assert vbState['targetRange'] == expected
assert vbState['viewRange'] == expected
def test_zoom_range_limit():
"""Test zooming with XRange and YRange limits set, but no X and Y limits"""
vb = pg.ViewBox()
vb.setLimits(minXRange=5, maxXRange=10, minYRange=5, maxYRange=10)
# Try something within limits
testRange = pg.QtCore.QRect(-15, -15, 7, 7)
vb.setRange(testRange, padding=0)
expected = [[testRange.left(), testRange.right()],
[testRange.top(), testRange.bottom()]]
vbViewRange = vb.getState()['viewRange']
assert vbViewRange == expected
# and outside limits
testRange = pg.QtCore.QRect(-15, -15, 17, 17)
# Code should center the required width reduction, so move each side by 3
expected = [[testRange.left() + 3, testRange.right() - 3],
[testRange.top() + 3, testRange.bottom() - 3]]
vb.setRange(testRange, padding=0)
vbViewRange = vb.getState()['viewRange']
vbTargetRange = vb.getState()['targetRange']
assert vbViewRange == expected
assert vbTargetRange == expected
def test_zoom_ratio():
"""Test zooming with a fixed aspect ratio set"""
vb = pg.ViewBox(lockAspect=1)
# Give the viewbox a size of the proper aspect ratio to keep things easy
vb.setFixedHeight(10)
vb.setFixedWidth(10)
# request a range with a good ratio
testRange = pg.QtCore.QRect(0, 0, 10, 10)
vb.setRange(testRange, padding=0)
expected = [[testRange.left(), testRange.right()],
[testRange.top(), testRange.bottom()]]
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# Assert that the width and height are equal, since we locked the aspect ratio at 1
assert viewWidth == viewHeight
# and for good measure, that it is the same as the test range
assert viewRange == expected
# Now try to set to something with a different aspect ratio
testRange = pg.QtCore.QRect(0, 0, 10, 20)
vb.setRange(testRange, padding=0)
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# Don't really care what we got here, as long as the width and height are the same
assert viewWidth == viewHeight
def test_zoom_ratio2():
"""Slightly more complicated zoom ratio test, where the view box shape does not match the ratio"""
vb = pg.ViewBox(lockAspect=1)
# twice as wide as tall
vb.setFixedHeight(10)
vb.setFixedWidth(20)
# more or less random requested range
testRange = pg.QtCore.QRect(0, 0, 10, 15)
vb.setRange(testRange, padding=0)
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# View width should be twice as wide as the height,
# since the viewbox is twice as wide as it is tall.
assert viewWidth == 2 * viewHeight
def test_zoom_ratio_with_limits1():
"""Test zoom with both ratio and limits set"""
vb = pg.ViewBox(lockAspect=1)
# twice as wide as tall
vb.setFixedHeight(10)
vb.setFixedWidth(20)
# set some limits
vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5)
# Try to zoom too tall
testRange = pg.QtCore.QRect(0, 0, 6, 10)
vb.setRange(testRange, padding=0)
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# Make sure our view is within limits and the proper aspect ratio
assert viewRange[0][0] >= -5
assert viewRange[0][1] <= 5
assert viewRange[1][0] >= -5
assert viewRange[1][1] <= 5
assert viewWidth == 2 * viewHeight
def test_zoom_ratio_with_limits2():
vb = pg.ViewBox(lockAspect=1)
# twice as wide as tall
vb.setFixedHeight(10)
vb.setFixedWidth(20)
# set some limits
vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5)
# Same thing, but out-of-range the other way
testRange = pg.QtCore.QRect(0, 0, 16, 6)
vb.setRange(testRange, padding=0)
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# Make sure our view is within limits and the proper aspect ratio
assert viewRange[0][0] >= -5
assert viewRange[0][1] <= 5
assert viewRange[1][0] >= -5
assert viewRange[1][1] <= 5
assert viewWidth == 2 * viewHeight
def test_zoom_ratio_with_limits_out_of_range():
vb = pg.ViewBox(lockAspect=1)
# twice as wide as tall
vb.setFixedHeight(10)
vb.setFixedWidth(20)
# set some limits
vb.setLimits(xMin=-5, xMax=5, yMin=-5, yMax=5)
# Request something completely out-of-range and out-of-aspect
testRange = pg.QtCore.QRect(10, 10, 25, 100)
vb.setRange(testRange, padding=0)
viewRange = vb.getState()['viewRange']
viewWidth = viewRange[0][1] - viewRange[0][0]
viewHeight = viewRange[1][1] - viewRange[1][0]
# Make sure our view is within limits and the proper aspect ratio
assert viewRange[0][0] >= -5
assert viewRange[0][1] <= 5
assert viewRange[1][0] >= -5
assert viewRange[1][1] <= 5
assert viewWidth == 2 * viewHeight
if __name__ == "__main__":
setup_module(None)
test_zoom_ratio()

View File

@ -48,38 +48,39 @@ class TabWindow(QtGui.QMainWindow):
class PlotWindow(PlotWidget):
sigClosed = QtCore.Signal(object)
"""
(deprecated; use :class:`~pyqtgraph.PlotWidget` instead)
"""
def __init__(self, title=None, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
PlotWidget.__init__(self, **kargs)
self.win.setCentralWidget(self)
for m in ['resize']:
setattr(self, m, getattr(self.win, m))
if title is not None:
self.win.setWindowTitle(title)
self.win.show()
self.setWindowTitle(title)
self.show()
def closeEvent(self, event):
PlotWidget.closeEvent(self, event)
self.sigClosed.emit(self)
class ImageWindow(ImageView):
sigClosed = QtCore.Signal(object)
"""
(deprecated; use :class:`~pyqtgraph.ImageView` instead)
"""
def __init__(self, *args, **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
self.win.resize(800,600)
ImageView.__init__(self)
if 'title' in kargs:
self.win.setWindowTitle(kargs['title'])
self.setWindowTitle(kargs['title'])
del kargs['title']
ImageView.__init__(self, self.win)
if len(args) > 0 or len(kargs) > 0:
self.setImage(*args, **kargs)
self.win.setCentralWidget(self)
for m in ['resize']:
setattr(self, m, getattr(self.win, m))
#for m in ['setImage', 'autoRange', 'addItem', 'removeItem', 'blackLevel', 'whiteLevel', 'imageItem']:
#setattr(self, m, getattr(self.cw, m))
self.win.show()
self.show()
def closeEvent(self, event):
ImageView.closeEvent(self, event)
self.sigClosed.emit(self)

View File

@ -411,11 +411,9 @@ class ImageView(QtGui.QWidget):
def close(self):
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
self.ui.roiPlot.close()
self.ui.graphicsView.close()
self.scene.clear()
del self.image
del self.imageDisp
self.clear()
self.imageDisp = None
self.imageItem.setParent(None)
super(ImageView, self).close()
self.setParent(None)

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<property name="margin">

View File

@ -146,7 +146,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.roiBtn.setText(_translate("Form", "ROI", None))
self.menuBtn.setText(_translate("Form", "Menu", None))
self.normGroup.setTitle(_translate("Form", "Normalization", None))

View File

@ -134,7 +134,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.roiBtn.setText(_translate("Form", "ROI"))
self.menuBtn.setText(_translate("Form", "Norm"))
self.normGroup.setTitle(_translate("Form", "Normalization"))

View File

@ -132,7 +132,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8))
self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8))
self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -55,6 +55,7 @@ class Parameter(QtCore.QObject):
sigDefaultChanged(self, default) Emitted when this parameter's default value has changed
sigNameChanged(self, name) Emitted when this parameter's name has changed
sigOptionsChanged(self, opts) Emitted when any of this parameter's options have changed
sigContextMenu(self, name) Emitted when a context menu was clicked
=================================== =========================================================
"""
## name, type, limits, etc.
@ -81,6 +82,7 @@ class Parameter(QtCore.QObject):
## (but only if monitorChildren() is called)
sigTreeStateChanged = QtCore.Signal(object, object) # self, changes
# changes = [(param, change, info), ...]
sigContextMenu = QtCore.Signal(object, object) # self, name
# bad planning.
#def __new__(cls, *args, **opts):
@ -135,9 +137,12 @@ class Parameter(QtCore.QObject):
(default=False)
removable If True, the user may remove this Parameter.
(default=False)
expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be
visible). (default=True)
expanded If True, the Parameter will initially be expanded in
ParameterTrees: Its children will be visible.
(default=True)
syncExpanded If True, the `expanded` state of this Parameter is
synchronized with all ParameterTrees it is displayed in.
(default=False)
title (str or None) If specified, then the parameter will be
displayed to the user using this string as its name.
However, the parameter will still be referred to
@ -159,6 +164,7 @@ class Parameter(QtCore.QObject):
'removable': False,
'strictNaming': False, # forces name to be usable as a python variable
'expanded': True,
'syncExpanded': False,
'title': None,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
}
@ -199,6 +205,8 @@ class Parameter(QtCore.QObject):
self.sigDefaultChanged.connect(lambda param, data: self.emitStateChanged('default', data))
self.sigNameChanged.connect(lambda param, data: self.emitStateChanged('name', data))
self.sigOptionsChanged.connect(lambda param, data: self.emitStateChanged('options', data))
self.sigContextMenu.connect(lambda param, data: self.emitStateChanged('contextMenu', data))
#self.watchParam(self) ## emit treechange signals if our own state changes
@ -206,6 +214,10 @@ class Parameter(QtCore.QObject):
"""Return the name of this Parameter."""
return self.opts['name']
def contextMenu(self, name):
""""A context menu entry was clicked"""
self.sigContextMenu.emit(self, name)
def setName(self, name):
"""Attempt to change the name of this parameter; return the actual name.
(The parameter may reject the name change or automatically pick a different name)"""
@ -453,7 +465,7 @@ class Parameter(QtCore.QObject):
Set any arbitrary options on this parameter.
The exact behavior of this function will depend on the parameter type, but
most parameters will accept a common set of options: value, name, limits,
default, readonly, removable, renamable, visible, enabled, and expanded.
default, readonly, removable, renamable, visible, enabled, expanded and syncExpanded.
See :func:`Parameter.__init__ <pyqtgraph.parametertree.Parameter.__init__>`
for more information on default options.

View File

@ -34,19 +34,20 @@ class ParameterItem(QtGui.QTreeWidgetItem):
param.sigOptionsChanged.connect(self.optsChanged)
param.sigParentChanged.connect(self.parentChanged)
opts = param.opts
self.updateFlags()
## flag used internally during name editing
self.ignoreNameColumnChange = False
def updateFlags(self):
## called when Parameter opts changed
opts = self.param.opts
## Generate context menu for renaming/removing parameter
self.contextMenu = QtGui.QMenu()
self.contextMenu.addSeparator()
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
if opts.get('renamable', False):
if param.opts.get('title', None) is not None:
if opts.get('title', None) is not None:
raise Exception("Cannot make parameter with both title != None and renamable == True.")
flags |= QtCore.Qt.ItemIsEditable
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if opts.get('removable', False):
self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove)
## handle movable / dropEnabled options
if opts.get('movable', False):
@ -55,9 +56,6 @@ class ParameterItem(QtGui.QTreeWidgetItem):
flags |= QtCore.Qt.ItemIsDropEnabled
self.setFlags(flags)
## flag used internally during name editing
self.ignoreNameColumnChange = False
def valueChanged(self, param, val):
## called when the parameter's value has changed
@ -106,9 +104,29 @@ class ParameterItem(QtGui.QTreeWidgetItem):
pass
def contextMenuEvent(self, ev):
if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False):
if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False)\
and "context" not in self.param.opts:
return
## Generate context menu for renaming/removing parameter
self.contextMenu = QtGui.QMenu() # Put in global name space to prevent garbage collection
self.contextMenu.addSeparator()
if self.param.opts.get('renamable', False):
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if self.param.opts.get('removable', False):
self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove)
# context menu
context = opts.get('context', None)
if isinstance(context, list):
for name in context:
self.contextMenu.addAction(name).triggered.connect(
self.contextMenuTriggered(name))
elif isinstance(context, dict):
for name, title in context.items():
self.contextMenu.addAction(title).triggered.connect(
self.contextMenuTriggered(name))
self.contextMenu.popup(ev.globalPos())
def columnChangedEvent(self, col):
@ -130,6 +148,10 @@ class ParameterItem(QtGui.QTreeWidgetItem):
finally:
self.ignoreNameColumnChange = False
def expandedChangedEvent(self, expanded):
if self.param.opts['syncExpanded']:
self.param.setOpts(expanded=expanded)
def nameChanged(self, param, name):
## called when the parameter's name has changed.
if self.param.opts.get('title', None) is None:
@ -146,10 +168,27 @@ class ParameterItem(QtGui.QTreeWidgetItem):
def optsChanged(self, param, opts):
"""Called when any options are changed that are not
name, value, default, or limits"""
#print opts
if 'visible' in opts:
self.setHidden(not opts['visible'])
if 'expanded' in opts:
if self.param.opts['syncExpanded']:
if self.isExpanded() != opts['expanded']:
self.setExpanded(opts['expanded'])
if 'syncExpanded' in opts:
if opts['syncExpanded']:
if self.isExpanded() != self.param.opts['expanded']:
self.setExpanded(self.param.opts['expanded'])
self.updateFlags()
def contextMenuTriggered(self, name):
def trigger():
self.param.contextMenu(name)
return trigger
def editName(self):
self.treeWidget().editItem(self, 0)

View File

@ -28,6 +28,8 @@ class ParameterTree(TreeWidget):
self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
self.setHeaderHidden(not showHeader)
self.itemChanged.connect(self.itemChangedEvent)
self.itemExpanded.connect(self.itemExpandedEvent)
self.itemCollapsed.connect(self.itemCollapsedEvent)
self.lastSel = None
self.setRootIsDecorated(False)
@ -135,6 +137,14 @@ class ParameterTree(TreeWidget):
if hasattr(item, 'columnChangedEvent'):
item.columnChangedEvent(col)
def itemExpandedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(True)
def itemCollapsedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(False)
def selectionChanged(self, *args):
sel = self.selectedItems()
if len(sel) != 1:

View File

@ -44,10 +44,6 @@ class WidgetParameterItem(ParameterItem):
self.widget = w
self.eventProxy = EventProxy(w, self.widgetEventFilter)
opts = self.param.opts
if 'tip' in opts:
w.setToolTip(opts['tip'])
self.defaultBtn = QtGui.QPushButton()
self.defaultBtn.setFixedWidth(20)
self.defaultBtn.setFixedHeight(20)
@ -73,6 +69,7 @@ class WidgetParameterItem(ParameterItem):
w.sigChanging.connect(self.widgetValueChanging)
## update value shown in widget.
opts = self.param.opts
if opts.get('value', None) is not None:
self.valueChanged(self, opts['value'], force=True)
else:
@ -81,6 +78,8 @@ class WidgetParameterItem(ParameterItem):
self.updateDefaultBtn()
self.optsChanged(self.param, self.param.opts)
def makeWidget(self):
"""
Return a single widget that should be placed in the second tree column.
@ -280,6 +279,9 @@ class WidgetParameterItem(ParameterItem):
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
self.widget.setEnabled(not opts['readonly'])
if 'tip' in opts:
self.widget.setToolTip(opts['tip'])
## If widget is a SpinBox, pass options straight through
if isinstance(self.widget, SpinBox):
# send only options supported by spinbox
@ -426,10 +428,13 @@ class GroupParameterItem(ParameterItem):
def treeWidgetChanged(self):
ParameterItem.treeWidgetChanged(self)
self.treeWidget().setFirstItemColumnSpanned(self, True)
tw = self.treeWidget()
if tw is None:
return
tw.setFirstItemColumnSpanned(self, True)
if self.addItem is not None:
self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox)
self.treeWidget().setFirstItemColumnSpanned(self.addItem, True)
tw.setItemWidget(self.addItem, 0, self.addWidgetBox)
tw.setFirstItemColumnSpanned(self.addItem, True)
def addChild(self, child): ## make sure added childs are actually inserted before add btn
if self.addItem is not None:
@ -664,8 +669,12 @@ class TextParameterItem(WidgetParameterItem):
## TODO: fix so that superclass method can be called
## (WidgetParameter should just natively support this style)
#WidgetParameterItem.treeWidgetChanged(self)
self.treeWidget().setFirstItemColumnSpanned(self.subItem, True)
self.treeWidget().setItemWidget(self.subItem, 0, self.textBox)
tw = self.treeWidget()
if tw is None:
return
tw.setFirstItemColumnSpanned(self.subItem, True)
tw.setItemWidget(self.subItem, 0, self.textBox)
# for now, these are copied from ParameterItem.treeWidgetChanged
self.setHidden(not self.param.opts.get('visible', True))

View File

@ -67,7 +67,7 @@ def test_exit_crash():
os.remove(tmp)
@pytest.mark.skipif(pg.Qt.QtVersion.startswith("5.9"), reason="Functionality not well supported, failing only on this config")
def test_pg_exit():
# test the pg.exit() function
code = textwrap.dedent("""

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<widget class="PlotWidget" name="widget" native="true">
<property name="geometry">

View File

@ -44,7 +44,7 @@ class PlotWidget(GraphicsView):
For all
other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
"""
def __init__(self, parent=None, background='default', **kargs):
def __init__(self, parent=None, background='default', plotItem=None, **kargs):
"""When initializing PlotWidget, *parent* and *background* are passed to
:func:`GraphicsWidget.__init__() <pyqtgraph.GraphicsWidget.__init__>`
and all others are passed
@ -52,7 +52,10 @@ class PlotWidget(GraphicsView):
GraphicsView.__init__(self, parent, background=background)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.enableMouse(False)
if plotItem is None:
self.plotItem = PlotItem(**kargs)
else:
self.plotItem = plotItem
self.setCentralItem(self.plotItem)
## Explicitly wrap methods from plotItem
## NOTE: If you change this list, update the documentation above as well.

View File

@ -96,7 +96,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
try:
self.fieldList.clearSelection()
for f in fields:
i = self.fields.keys().index(f)
i = list(self.fields.keys()).index(f)
item = self.fieldList.item(i)
item.setSelected(True)
finally: