Commit f9c85dae authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Merge remote-tracking branch 'pyqtgraph/develop' into core

parents c7f4a8fd 55a07b0b
......@@ -11,6 +11,8 @@ This module exists to smooth out some of the differences between PySide and PyQt
import sys, re
from .python2_3 import asUnicode
## Automatically determine whether to use PyQt or PySide.
## This is done by first checking to see whether one of the libraries
## is already imported. If not, then attempt to import PyQt4, then PySide.
......@@ -31,6 +33,10 @@ else:
if USE_PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
try:
from PySide import QtTest
except ImportError:
pass
import PySide
try:
from PySide import shiboken
......@@ -56,18 +62,29 @@ if USE_PYSIDE:
# Credit:
# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313
class StringIO(object):
"""Alternative to built-in StringIO needed to circumvent unicode/ascii issues"""
def __init__(self):
self.data = []
def write(self, data):
self.data.append(data)
def getvalue(self):
return ''.join(map(asUnicode, self.data)).encode('utf8')
def loadUiType(uiFile):
"""
Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class.
"""
import pysideuic
import xml.etree.ElementTree as xml
from io import StringIO
#from io import StringIO
parsed = xml.parse(uiFile)
widget_class = parsed.find('widget').get('class')
form_class = parsed.find('class').text
with open(uiFile, 'r') as f:
o = StringIO()
frame = {}
......@@ -93,6 +110,10 @@ else:
from PyQt4 import QtOpenGL
except ImportError:
pass
try:
from PyQt4 import QtTest
except ImportError:
pass
import sip
......
......@@ -2,6 +2,7 @@
from .Qt import QtCore
from .ptime import time
from . import ThreadsafeTimer
import weakref
__all__ = ['SignalProxy']
......@@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject):
self.timer = ThreadsafeTimer.ThreadsafeTimer()
self.timer.timeout.connect(self.flush)
self.block = False
self.slot = slot
self.slot = weakref.ref(slot)
self.lastFlushTime = None
if slot is not None:
self.sigDelayed.connect(slot)
......@@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject):
except:
pass
try:
self.sigDelayed.disconnect(self.slot)
self.sigDelayed.disconnect(self.slot())
except:
pass
......
......@@ -257,7 +257,7 @@ from .graphicsWindows import *
from .SignalProxy import *
from .colormap import *
from .ptime import time
from pyqtgraph.Qt import isQObjectAlive
from .Qt import isQObjectAlive
##############################################################
......
......@@ -22,6 +22,9 @@ class Container(object):
return None
def insert(self, new, pos=None, neighbor=None):
# remove from existing parent first
new.setParent(None)
if not isinstance(new, list):
new = [new]
if neighbor is None:
......
......@@ -8,11 +8,13 @@ class Dock(QtGui.QWidget, DockDrop):
sigStretchChanged = QtCore.Signal()
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True):
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False):
QtGui.QWidget.__init__(self)
DockDrop.__init__(self)
self.area = area
self.label = DockLabel(name, self)
self.label = DockLabel(name, self, closable)
if closable:
self.label.sigCloseClicked.connect(self.close)
self.labelHidden = False
self.moveLabel = True ## If false, the dock is no longer allowed to move the label.
self.autoOrient = autoOrientation
......@@ -35,30 +37,30 @@ class Dock(QtGui.QWidget, DockDrop):
#self.titlePos = 'top'
self.raiseOverlay()
self.hStyle = """
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-top-width: 0px;
}"""
self.vStyle = """
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-left-width: 0px;
}"""
self.nStyle = """
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
Dock > QWidget {
border: 1px solid #000;
border-radius: 5px;
}"""
self.dragStyle = """
Dock > QWidget {
border: 4px solid #00F;
border-radius: 5px;
Dock > QWidget {
border: 4px solid #00F;
border-radius: 5px;
}"""
self.setAutoFillBackground(False)
self.widgetArea.setStyleSheet(self.hStyle)
......@@ -79,7 +81,7 @@ class Dock(QtGui.QWidget, DockDrop):
def setStretch(self, x=None, y=None):
"""
Set the 'target' size for this Dock.
Set the 'target' size for this Dock.
The actual size will be determined by comparing this Dock's
stretch value to the rest of the docks it shares space with.
"""
......@@ -130,7 +132,7 @@ class Dock(QtGui.QWidget, DockDrop):
Sets the orientation of the title bar for this Dock.
Must be one of 'auto', 'horizontal', or 'vertical'.
By default ('auto'), the orientation is determined
based on the aspect ratio of the Dock.
based on the aspect ratio of the Dock.
"""
#print self.name(), "setOrientation", o, force
if o == 'auto' and self.autoOrient:
......@@ -175,7 +177,7 @@ class Dock(QtGui.QWidget, DockDrop):
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
"""
Add a new widget to the interior of this Dock.
Add a new widget to the interior of this Dock.
Each Dock uses a QGridLayout to arrange widgets within.
"""
if row is None:
......@@ -239,11 +241,13 @@ class Dock(QtGui.QWidget, DockDrop):
def dropEvent(self, *args):
DockDrop.dropEvent(self, *args)
class DockLabel(VerticalLabel):
sigClicked = QtCore.Signal(object, object)
sigCloseClicked = QtCore.Signal()
def __init__(self, text, dock):
def __init__(self, text, dock, showCloseButton):
self.dim = False
self.fixedWidth = False
VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False)
......@@ -251,10 +255,13 @@ class DockLabel(VerticalLabel):
self.dock = dock
self.updateStyle()
self.setAutoFillBackground(False)
self.startedDrag = False
#def minimumSizeHint(self):
##sh = QtGui.QWidget.minimumSizeHint(self)
#return QtCore.QSize(20, 20)
self.closeButton = None
if showCloseButton:
self.closeButton = QtGui.QToolButton(self)
self.closeButton.clicked.connect(self.sigCloseClicked)
self.closeButton.setIcon(QtGui.QApplication.style().standardIcon(QtGui.QStyle.SP_TitleBarCloseButton))
def updateStyle(self):
r = '3px'
......@@ -268,28 +275,28 @@ class DockLabel(VerticalLabel):
border = '#55B'
if self.orientation == 'vertical':
self.vStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: 0px;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: %s;
border-width: 0px;
self.vStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: 0px;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: %s;
border-width: 0px;
border-right: 2px solid %s;
padding-top: 3px;
padding-bottom: 3px;
}""" % (bg, fg, r, r, border)
self.setStyleSheet(self.vStyle)
else:
self.hStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: %s;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-width: 0px;
self.hStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: %s;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-width: 0px;
border-bottom: 2px solid %s;
padding-left: 3px;
padding-right: 3px;
......@@ -315,11 +322,9 @@ class DockLabel(VerticalLabel):
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
self.dock.startDrag()
ev.accept()
#print ev.pos()
def mouseReleaseEvent(self, ev):
if not self.startedDrag:
#self.emit(QtCore.SIGNAL('clicked'), self, ev)
self.sigClicked.emit(self, ev)
ev.accept()
......@@ -327,13 +332,14 @@ class DockLabel(VerticalLabel):
if ev.button() == QtCore.Qt.LeftButton:
self.dock.float()
#def paintEvent(self, ev):
#p = QtGui.QPainter(self)
##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200)))
#p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100)))
#p.drawRect(self.rect().adjusted(0, 0, -1, -1))
#VerticalLabel.paintEvent(self, ev)
def resizeEvent (self, ev):
if self.closeButton:
if self.orientation == 'vertical':
size = ev.size().width()
pos = QtCore.QPoint(0, 0)
else:
size = ev.size().height()
pos = QtCore.QPoint(ev.size().width() - size, 0)
self.closeButton.setFixedSize(QtCore.QSize(size, size))
self.closeButton.move(pos)
super(DockLabel,self).resizeEvent(ev)
......@@ -52,10 +52,11 @@ class CSVExporter(Exporter):
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for d in data:
if i < len(d[0]):
fd.write(numFormat % d[0][i] + sep + numFormat % d[1][i] + sep)
else:
fd.write(' %s %s' % (sep, sep))
for j in [0, 1]:
if i < len(d[j]):
fd.write(numFormat % d[j][i] + sep)
else:
fd.write(' %s' % sep)
fd.write('\n')
fd.close()
......
"""
SVG export test
"""
import pyqtgraph as pg
import pyqtgraph.exporters
import csv
app = pg.mkQApp()
def approxeq(a, b):
return (a-b) <= ((a + b) * 1e-6)
def test_CSVExporter():
plt = pg.plot()
y1 = [1,3,2,3,1,6,9,8,4,2]
plt.plot(y=y1, name='myPlot')
y2 = [3,4,6,1,2,4,2,3,5,3,5,1,3]
x2 = pg.np.linspace(0, 1.0, len(y2))
plt.plot(x=x2, y=y2)
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
x3 = pg.np.linspace(0, 1.0, len(y3)+1)
plt.plot(x=x3, y=y3, stepMode=True)
ex = pg.exporters.CSVExporter(plt.plotItem)
ex.export(fileName='test.csv')
r = csv.reader(open('test.csv', 'r'))
lines = [line for line in r]
header = lines.pop(0)
assert header == ['myPlot_x', 'myPlot_y', 'x', 'y', 'x', 'y']
i = 0
for vals in lines:
vals = list(map(str.strip, vals))
assert (i >= len(y1) and vals[0] == '') or approxeq(float(vals[0]), i)
assert (i >= len(y1) and vals[1] == '') or approxeq(float(vals[1]), y1[i])
assert (i >= len(x2) and vals[2] == '') or approxeq(float(vals[2]), x2[i])
assert (i >= len(y2) and vals[3] == '') or approxeq(float(vals[3]), y2[i])
assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i])
assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i])
i += 1
if __name__ == '__main__':
test_CSVExporter()
\ No newline at end of file
......@@ -538,7 +538,6 @@ def interpolateArray(data, x, default=0.0):
prof = debug.Profiler()
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
nd = data.ndim
md = x.shape[-1]
......
......@@ -55,6 +55,8 @@ class AxisItem(GraphicsWidget):
],
'showValues': showValues,
'tickLength': maxTickLength,
'maxTickLevel': 2,
'maxTextLevel': 2,
}
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
......@@ -68,6 +70,7 @@ class AxisItem(GraphicsWidget):
self.tickFont = None
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self._tickSpacing = None # used to override default tickSpacing method
self.scale = 1.0
self.autoSIPrefix = True
self.autoSIPrefixScale = 1.0
......@@ -161,7 +164,11 @@ class AxisItem(GraphicsWidget):
self.scene().removeItem(self)
def setGrid(self, grid):
"""Set the alpha value for the grid, or False to disable."""
"""Set the alpha value (0-255) for the grid, or False to disable.
When grid lines are enabled, the axis tick lines are extended to cover
the extent of the linked ViewBox, if any.
"""
self.grid = grid
self.picture = None
self.prepareGeometryChange()
......@@ -229,7 +236,7 @@ class AxisItem(GraphicsWidget):
without any scaling prefix (eg, 'V' instead of 'mV'). The
scaling prefix will be automatically prepended based on the
range of data displayed.
**args All extra keyword arguments become CSS style options for
**args All extra keyword arguments become CSS style options for
the <span> tag which will surround the axis label and units.
============== =============================================================
......@@ -454,7 +461,10 @@ class AxisItem(GraphicsWidget):
else:
if newRange is None:
newRange = view.viewRange()[0]
self.setRange(*newRange)
if view.xInverted():
self.setRange(*newRange[::-1])
else:
self.setRange(*newRange)
def boundingRect(self):
linkedView = self.linkedView()
......@@ -510,6 +520,37 @@ class AxisItem(GraphicsWidget):
self.picture = None
self.update()
def setTickSpacing(self, major=None, minor=None, levels=None):
"""
Explicitly determine the spacing of major and minor ticks. This
overrides the default behavior of the tickSpacing method, and disables
the effect of setTicks(). Arguments may be either *major* and *minor*,
or *levels* which is a list of (spacing, offset) tuples for each
tick level desired.
If no arguments are given, then the default behavior of tickSpacing
is enabled.
Examples::
# two levels, all offsets = 0
axis.setTickSpacing(5, 1)
# three levels, all offsets = 0
axis.setTickSpacing([(3, 0), (1, 0), (0.25, 0)])
# reset to default
axis.setTickSpacing()
"""
if levels is None:
if major is None:
levels = None
else:
levels = [(major, 0), (minor, 0)]
self._tickSpacing = levels
self.picture = None
self.update()
def tickSpacing(self, minVal, maxVal, size):
"""Return values describing the desired spacing and offset of ticks.
......@@ -525,6 +566,10 @@ class AxisItem(GraphicsWidget):
...
]
"""
# First check for override tick spacing
if self._tickSpacing is not None:
return self._tickSpacing
dif = abs(maxVal - minVal)
if dif == 0:
return []
......@@ -550,12 +595,13 @@ class AxisItem(GraphicsWidget):
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
]
## decide whether to include the last level of ticks
minSpacing = min(size / 20., 30.)
maxTickCount = size / minSpacing
if dif / intervals[minorIndex] <= maxTickCount:
levels.append((intervals[minorIndex], 0))
return levels
if self.style['maxTickLevel'] >= 2:
## decide whether to include the last level of ticks
minSpacing = min(size / 20., 30.)
maxTickCount = size / minSpacing
if dif / intervals[minorIndex] <= maxTickCount:
levels.append((intervals[minorIndex], 0))
return levels
......@@ -581,8 +627,6 @@ class AxisItem(GraphicsWidget):
#(intervals[intIndexes[0]], 0)
#]
def tickValues(self, minVal, maxVal, size):
"""
Return the values and spacing of ticks to draw::
......@@ -756,8 +800,6 @@ class AxisItem(GraphicsWidget):
values.append(val)
strings.append(strn)
textLevel = 1 ## draw text at this scale level
## determine mapping between tick values and local coordinates
dif = self.range[1] - self.range[0]
if dif == 0:
......@@ -846,7 +888,7 @@ class AxisItem(GraphicsWidget):
if not self.style['showValues']:
return (axisSpec, tickSpecs, textSpecs)
for i in range(len(tickLevels)):
for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
## Get the list of strings to display for this level
if tickStrings is None:
spacing, values = tickLevels[i]
......
......@@ -8,15 +8,7 @@ __all__ = ['ErrorBarItem']
class ErrorBarItem(GraphicsObject):
def __init__(self, **opts):
"""
Valid keyword options are:
x, y, height, width, top, bottom, left, right, beam, pen
x and y must be numpy arrays specifying the coordinates of data points.
height, width, top, bottom, left, right, and beam may be numpy arrays,
single values, or None to disable. All values should be positive.
If height is specified, it overrides top and bottom.
If width is specified, it overrides left and right.
All keyword arguments are passed to setData().
"""
GraphicsObject.__init__(self)
self.opts = dict(
......@@ -31,14 +23,37 @@ class ErrorBarItem(GraphicsObject):
beam=None,
pen=None
)
self.setOpts(**opts)
self.setData(**opts)
def setData(self, **opts):
"""
Update the data in the item. All arguments are optional.
def setOpts(self, **opts):
Valid keyword options are:
x, y, height, width, top, bottom, left, right, beam, pen
* x and y must be numpy arrays specifying the coordinates of data points.
* height, width, top, bottom, left, right, and beam may be numpy arrays,
single values, or None to disable. All values should be positive.
* top, bottom, left, and right specify the lengths of bars extending
in each direction.
* If height is specified, it overrides top and bottom.
* If width is specified, it overrides left and right.
* beam specifies the width of the beam at the end of each bar.
* pen may be any single argument accepted by pg.mkPen().
This method was added in version 0.9.9. For prior versions, use setOpts.
"""
self.opts.update(opts)
self.path = None
self.update()
self.prepareGeometryChange()
self.informViewBoundsChanged()
def setOpts(self, **opts):
# for backward compatibility
self.setData(**opts)
def drawPath(self):
p = QtGui.QPainterPath()
......
......@@ -102,7 +102,7 @@ class GraphicsItem(object):
Extends deviceTransform to automatically determine the viewportTransform.
"""
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
return self._exportOpts['painter'].deviceTransform()
return self._exportOpts['painter'].deviceTransform() * self.sceneTransform()
if viewportTransform is None:
view = self.getViewWidget()
......@@ -318,6 +318,8 @@ class GraphicsItem(object):
vt = self.deviceTransform()
if vt is None:
return None
if isinstance(obj, QtCore.QPoint):
obj = QtCore.QPointF(obj)
vt = fn.invertQTransform(vt)
return vt.map(obj)
......
......@@ -17,6 +17,7 @@ from .. import functions as fn
import numpy as np
from .. import debug as debug
import weakref
__all__ = ['HistogramLUTItem']
......@@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget):
"""
GraphicsWidget.__init__(self)
self.lut = None
self.imageItem = None
self.imageItem = lambda: None # fake a dead weakref
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
......@@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget):
#self.region.setBounds([vr.top(), vr.bottom()])
def setImageItem(self, img):
self.imageItem = img
self.imageItem = weakref.ref(img)