Merge branch 'develop' into pyside2-uic
This commit is contained in:
commit
47f06e78be
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -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"},
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -17,7 +17,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
|
@ -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))
|
||||
|
@ -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:"))
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'):
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="verticalSpacing">
|
||||
|
@ -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))
|
||||
|
@ -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.."))
|
||||
|
@ -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))
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="averageGroup">
|
||||
<property name="geometry">
|
||||
|
@ -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))
|
||||
|
@ -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."))
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -17,7 +17,7 @@
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="margin">
|
||||
|
@ -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))
|
||||
|
@ -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>"))
|
||||
|
@ -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))
|
||||
|
200
pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py
Normal file
200
pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBoxZoom.py
Normal 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()
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
@ -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("""
|
||||
|
@ -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">
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user