diff --git a/GradientWidget.py b/GradientWidget.py deleted file mode 100644 index 033c62db..00000000 --- a/GradientWidget.py +++ /dev/null @@ -1,452 +0,0 @@ -# -*- coding: utf-8 -*- -from PyQt4 import QtGui, QtCore -import weakref - -class TickSlider(QtGui.QGraphicsView): - def __init__(self, parent=None, orientation='bottom', allowAdd=True, **kargs): - QtGui.QGraphicsView.__init__(self, parent) - #self.orientation = orientation - self.allowAdd = allowAdd - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) - self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) - self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) - self.length = 100 - self.tickSize = 15 - self.orientations = { - 'left': (270, 1, -1), - 'right': (270, 1, 1), - 'top': (0, 1, -1), - 'bottom': (0, 1, 1) - } - - self.scene = QtGui.QGraphicsScene() - self.setScene(self.scene) - - self.ticks = {} - self.maxDim = 20 - self.setOrientation(orientation) - self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) - self.setBackgroundRole(QtGui.QPalette.NoRole) - self.setMouseTracking(True) - - def keyPressEvent(self, ev): - ev.ignore() - - def setMaxDim(self, mx=None): - if mx is None: - mx = self.maxDim - else: - self.maxDim = mx - - if self.orientation in ['bottom', 'top']: - self.setFixedHeight(mx) - self.setMaximumWidth(16777215) - else: - self.setFixedWidth(mx) - self.setMaximumHeight(16777215) - - def setOrientation(self, ort): - self.orientation = ort - self.resetTransform() - self.rotate(self.orientations[ort][0]) - self.scale(*self.orientations[ort][1:]) - self.setMaxDim() - - def addTick(self, x, color=None, movable=True): - if color is None: - color = QtGui.QColor(255,255,255) - tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize) - self.ticks[tick] = x - self.scene.addItem(tick) - return tick - - def removeTick(self, tick): - del self.ticks[tick] - self.scene.removeItem(tick) - - def tickMoved(self, tick, pos): - #print "tick changed" - ## Correct position of tick if it has left bounds. - newX = min(max(0, pos.x()), self.length) - pos.setX(newX) - tick.setPos(pos) - self.ticks[tick] = float(newX) / self.length - - def tickClicked(self, tick, ev): - if ev.button() == QtCore.Qt.RightButton: - self.removeTick(tick) - - def widgetLength(self): - if self.orientation in ['bottom', 'top']: - return self.width() - else: - return self.height() - - def resizeEvent(self, ev): - wlen = max(40, self.widgetLength()) - self.setLength(wlen-self.tickSize) - bounds = self.scene.itemsBoundingRect() - bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) - bounds.setRight(max(self.length + self.tickSize, bounds.right())) - #bounds.setTop(min(bounds.top(), self.tickSize)) - #bounds.setBottom(max(0, bounds.bottom())) - self.setSceneRect(bounds) - self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) - - def setLength(self, newLen): - for t, x in self.ticks.items(): - t.setPos(x * newLen, t.pos().y()) - self.length = float(newLen) - - def mousePressEvent(self, ev): - QtGui.QGraphicsView.mousePressEvent(self, ev) - self.ignoreRelease = False - if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks - self.ignoreRelease = True - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsView.mouseReleaseEvent(self, ev) - if self.ignoreRelease: - return - - pos = self.mapToScene(ev.pos()) - if pos.x() < 0 or pos.x() > self.length: - return - if pos.y() < 0 or pos.y() > self.tickSize: - return - - if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: - pos.setX(min(max(pos.x(), 0), self.length)) - self.addTick(pos.x()/self.length) - elif ev.button() == QtCore.Qt.RightButton: - self.showMenu(ev) - - - def showMenu(self, ev): - pass - - def setTickColor(self, tick, color): - tick = self.getTick(tick) - tick.color = color - tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) - - def setTickValue(self, tick, val): - tick = self.getTick(tick) - val = min(max(0.0, val), 1.0) - x = val * self.length - pos = tick.pos() - pos.setX(x) - tick.setPos(pos) - self.ticks[tick] = val - - def tickValue(self, tick): - tick = self.getTick(tick) - return self.ticks[tick] - - def getTick(self, tick): - if type(tick) is int: - tick = self.listTicks()[tick][0] - return tick - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsView.mouseMoveEvent(self, ev) - #print ev.pos(), ev.buttons() - - def listTicks(self): - ticks = self.ticks.items() - ticks.sort(lambda a,b: cmp(a[1], b[1])) - return ticks - - -class GradientWidget(TickSlider): - - sigGradientChanged = QtCore.Signal(object) - - def __init__(self, *args, **kargs): - TickSlider.__init__(self, *args, **kargs) - self.currentTick = None - self.currentTickColor = None - self.rectSize = 15 - self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) - self.colorMode = 'rgb' - self.colorDialog = QtGui.QColorDialog() - self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) - self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) - #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('currentColorChanged(const QColor&)'), self.currentColorChanged) - self.colorDialog.currentColorChanged.connect(self.currentColorChanged) - #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('rejected()'), self.currentColorRejected) - self.colorDialog.rejected.connect(self.currentColorRejected) - - #self.gradient = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(100,0)) - self.scene.addItem(self.gradRect) - self.addTick(0, QtGui.QColor(0,0,0), True) - self.addTick(1, QtGui.QColor(255,0,0), True) - - self.setMaxDim(self.rectSize + self.tickSize) - self.updateGradient() - - #self.btn = QtGui.QPushButton('RGB') - #self.btnProxy = self.scene.addWidget(self.btn) - #self.btnProxy.setFlag(self.btnProxy.ItemIgnoresTransformations) - #self.btnProxy.scale(0.7, 0.7) - #self.btnProxy.translate(-self.btnProxy.sceneBoundingRect().width()+self.tickSize/2., 0) - #if self.orientation == 'bottom': - #self.btnProxy.translate(0, -self.rectSize) - - def setColorMode(self, cm): - if cm not in ['rgb', 'hsv']: - raise Exception("Unknown color mode %s" % str(cm)) - self.colorMode = cm - self.updateGradient() - - def updateGradient(self): - self.gradient = self.getGradient() - self.gradRect.setBrush(QtGui.QBrush(self.gradient)) - #self.emit(QtCore.SIGNAL('gradientChanged'), self) - self.sigGradientChanged.emit(self) - - def setLength(self, newLen): - TickSlider.setLength(self, newLen) - self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) - self.updateGradient() - - def currentColorChanged(self, color): - if color.isValid() and self.currentTick is not None: - self.setTickColor(self.currentTick, color) - self.updateGradient() - - def currentColorRejected(self): - self.setTickColor(self.currentTick, self.currentTickColor) - self.updateGradient() - - def tickClicked(self, tick, ev): - if ev.button() == QtCore.Qt.LeftButton: - if not tick.colorChangeAllowed: - return - self.currentTick = tick - self.currentTickColor = tick.color - self.colorDialog.setCurrentColor(tick.color) - self.colorDialog.open() - #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - #if color.isValid(): - #self.setTickColor(tick, color) - #self.updateGradient() - elif ev.button() == QtCore.Qt.RightButton: - if not tick.removeAllowed: - return - if len(self.ticks) > 2: - self.removeTick(tick) - self.updateGradient() - - def tickMoved(self, tick, pos): - TickSlider.tickMoved(self, tick, pos) - self.updateGradient() - - - def getGradient(self): - g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) - if self.colorMode == 'rgb': - ticks = self.listTicks() - g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) - elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop - ticks = self.listTicks() - stops = [] - stops.append((ticks[0][1], ticks[0][0].color)) - for i in range(1,len(ticks)): - x1 = ticks[i-1][1] - x2 = ticks[i][1] - dx = (x2-x1) / 10. - for j in range(1,10): - x = x1 + dx*j - stops.append((x, self.getColor(x))) - stops.append((x2, self.getColor(x2))) - g.setStops(stops) - return g - - def getColor(self, x): - ticks = self.listTicks() - if x <= ticks[0][1]: - return QtGui.QColor(ticks[0][0].color) # always copy colors before handing them out - if x >= ticks[-1][1]: - return QtGui.QColor(ticks[-1][0].color) - - x2 = ticks[0][1] - for i in range(1,len(ticks)): - x1 = x2 - x2 = ticks[i][1] - if x1 <= x and x2 >= x: - break - - dx = (x2-x1) - if dx == 0: - f = 0. - else: - f = (x-x1) / dx - c1 = ticks[i-1][0].color - c2 = ticks[i][0].color - if self.colorMode == 'rgb': - r = c1.red() * (1.-f) + c2.red() * f - g = c1.green() * (1.-f) + c2.green() * f - b = c1.blue() * (1.-f) + c2.blue() * f - a = c1.alpha() * (1.-f) + c2.alpha() * f - return QtGui.QColor(r, g, b,a) - elif self.colorMode == 'hsv': - h1,s1,v1,_ = c1.getHsv() - h2,s2,v2,_ = c2.getHsv() - h = h1 * (1.-f) + h2 * f - s = s1 * (1.-f) + s2 * f - v = v1 * (1.-f) + v2 * f - c = QtGui.QColor() - c.setHsv(h,s,v) - return c - - - - def mouseReleaseEvent(self, ev): - TickSlider.mouseReleaseEvent(self, ev) - self.updateGradient() - - def addTick(self, x, color=None, movable=True): - if color is None: - color = self.getColor(x) - t = TickSlider.addTick(self, x, color=color, movable=movable) - t.colorChangeAllowed = True - t.removeAllowed = True - return t - - def saveState(self): - ticks = [] - 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} - return state - - def restoreState(self, state): - self.setColorMode(state['mode']) - for t in self.ticks.keys(): - self.removeTick(t) - for t in state['ticks']: - c = QtGui.QColor(*t[1]) - self.addTick(t[0], c) - self.updateGradient() - - - -class BlackWhiteSlider(GradientWidget): - def __init__(self, parent): - GradientWidget.__init__(self, parent) - self.getTick(0).colorChangeAllowed = False - self.getTick(1).colorChangeAllowed = False - self.allowAdd = False - self.setTickColor(self.getTick(1), QtGui.QColor(255,255,255)) - self.setOrientation('right') - - def getLevels(self): - return (self.tickValue(0), self.tickValue(1)) - - def setLevels(self, black, white): - self.setTickValue(0, black) - self.setTickValue(1, white) - - - - -class GammaWidget(TickSlider): - pass - - -class Tick(QtGui.QGraphicsPolygonItem): - def __init__(self, view, pos, color, movable=True, scale=10): - #QObjectWorkaround.__init__(self) - self.movable = movable - self.view = weakref.ref(view) - self.scale = scale - self.color = color - #self.endTick = endTick - self.pg = QtGui.QPolygonF([QtCore.QPointF(0,0), QtCore.QPointF(-scale/3**0.5,scale), QtCore.QPointF(scale/3**0.5,scale)]) - QtGui.QGraphicsPolygonItem.__init__(self, self.pg) - self.setPos(pos[0], pos[1]) - self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsSelectable) - self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) - if self.movable: - self.setZValue(1) - else: - self.setZValue(0) - - #def x(self): - #return self.pos().x()/100. - - def mouseMoveEvent(self, ev): - #print self, "move", ev.scenePos() - if not self.movable: - return - if not ev.buttons() & QtCore.Qt.LeftButton: - return - - - newPos = ev.scenePos() + self.mouseOffset - newPos.setY(self.pos().y()) - #newPos.setX(min(max(newPos.x(), 0), 100)) - self.setPos(newPos) - self.view().tickMoved(self, newPos) - self.movedSincePress = True - #self.emit(QtCore.SIGNAL('tickChanged'), self) - ev.accept() - - def mousePressEvent(self, ev): - self.movedSincePress = False - if ev.button() == QtCore.Qt.LeftButton: - ev.accept() - self.mouseOffset = self.pos() - ev.scenePos() - self.pressPos = ev.scenePos() - elif ev.button() == QtCore.Qt.RightButton: - ev.accept() - #if self.endTick: - #return - #self.view.tickChanged(self, delete=True) - - def mouseReleaseEvent(self, ev): - #print self, "release", ev.scenePos() - if not self.movedSincePress: - self.view().tickClicked(self, ev) - - #if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: - #color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - #if color.isValid(): - #self.color = color - #self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) - ##self.emit(QtCore.SIGNAL('tickChanged'), self) - #self.view.tickChanged(self) - - - - - -if __name__ == '__main__': - app = QtGui.QApplication([]) - w = QtGui.QMainWindow() - w.show() - w.resize(400,400) - cw = QtGui.QWidget() - w.setCentralWidget(cw) - - l = QtGui.QGridLayout() - l.setSpacing(0) - cw.setLayout(l) - - w1 = GradientWidget(orientation='top') - w2 = GradientWidget(orientation='right', allowAdd=False) - w2.setTickColor(1, QtGui.QColor(255,255,255)) - w3 = GradientWidget(orientation='bottom') - w4 = TickSlider(orientation='left') - - l.addWidget(w1, 0, 1) - l.addWidget(w2, 1, 2) - l.addWidget(w3, 2, 1) - l.addWidget(w4, 1, 0) - - - \ No newline at end of file diff --git a/GraphicsView.py b/GraphicsView.py deleted file mode 100644 index 9846d0c4..00000000 --- a/GraphicsView.py +++ /dev/null @@ -1,521 +0,0 @@ -# -*- coding: utf-8 -*- -""" -GraphicsView.py - Extension of QGraphicsView -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. -""" - -from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg -#from numpy import vstack -#import time -from Point import * -#from vector import * -import sys, os -import debug - -class GraphicsView(QtGui.QGraphicsView): - - sigRangeChanged = QtCore.Signal(object, object) - sigMouseReleased = QtCore.Signal(object) - sigSceneMouseMoved = QtCore.Signal(object) - #sigRegionChanged = QtCore.Signal(object) - lastFileDir = None - - def __init__(self, parent=None, useOpenGL=False): - """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the - viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget - that is automatically scaled to the full view geometry. - - By default, the view coordinate system matches the widget's pixel coordinates and - automatically updates when the view is resized. This can be overridden by setting - autoPixelRange=False. The exact visible range can be set with setRange(). - - The view can be panned using the middle mouse button and scaled using the right mouse button if - enabled via enableMouse().""" - self.closed = False - - QtGui.QGraphicsView.__init__(self, parent) - if 'linux' in sys.platform: ## linux has bugs in opengl implementation - useOpenGL = False - self.useOpenGL(useOpenGL) - - self.setCacheMode(self.CacheBackground) - - brush = QtGui.QBrush(QtGui.QColor(0,0,0)) - self.setBackgroundBrush(brush) - - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setFrameShape(QtGui.QFrame.NoFrame) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) - self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) - self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) - - - #self.setSceneRect(QtCore.QRectF(-1e10, -1e10, 2e10, 2e10)) - - self.lockedViewports = [] - self.lastMousePos = None - #self.setMouseTracking(False) - self.aspectLocked = False - #self.yInverted = True - self.range = QtCore.QRectF(0, 0, 1, 1) - self.autoPixelRange = True - self.currentItem = None - self.clearMouse() - self.updateMatrix() - self.sceneObj = QtGui.QGraphicsScene() - self.setScene(self.sceneObj) - - ## by default we set up a central widget with a grid layout. - ## this can be replaced if needed. - self.centralWidget = None - self.setCentralItem(QtGui.QGraphicsWidget()) - self.centralLayout = QtGui.QGraphicsGridLayout() - self.centralWidget.setLayout(self.centralLayout) - - self.mouseEnabled = False - self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) - self.clickAccepted = False - - #def paintEvent(self, *args): - #prof = debug.Profiler('GraphicsView.paintEvent '+str(id(self)), disabled=False) - #QtGui.QGraphicsView.paintEvent(self, *args) - #prof.finish() - - def close(self): - self.centralWidget = None - self.scene().clear() - #print " ", self.scene().itemCount() - self.currentItem = None - self.sceneObj = None - self.closed = True - self.setViewport(None) - - def useOpenGL(self, b=True): - if b: - v = QtOpenGL.QGLWidget() - else: - v = QtGui.QWidget() - - #v.setStyleSheet("background-color: #000000;") - self.setViewport(v) - - def keyPressEvent(self, ev): - ev.ignore() - - def setCentralItem(self, item): - """Sets a QGraphicsWidget to automatically fill the entire view.""" - if self.centralWidget is not None: - self.scene().removeItem(self.centralWidget) - self.centralWidget = item - self.sceneObj.addItem(item) - self.resizeEvent(None) - - def addItem(self, *args): - return self.scene().addItem(*args) - - def removeItem(self, *args): - return self.scene().removeItem(*args) - - def enableMouse(self, b=True): - self.mouseEnabled = b - self.autoPixelRange = (not b) - - def clearMouse(self): - self.mouseTrail = [] - self.lastButtonReleased = None - - def resizeEvent(self, ev): - if self.closed: - return - if self.autoPixelRange: - self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) - self.setRange(self.range, padding=0, disableAutoPixel=False) - self.updateMatrix() - - def updateMatrix(self, propagate=True): - self.setSceneRect(self.range) - if self.aspectLocked: - self.fitInView(self.range, QtCore.Qt.KeepAspectRatio) - else: - self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) - - ##print "udpateMatrix:" - #translate = Point(self.range.center()) - #if self.range.width() == 0 or self.range.height() == 0: - #return - #scale = Point(self.size().width()/self.range.width(), self.size().height()/self.range.height()) - - #m = QtGui.QTransform() - - ### First center the viewport at 0 - #self.resetMatrix() - #center = self.viewportTransform().inverted()[0].map(Point(self.width()/2., self.height()/2.)) - #if self.yInverted: - #m.translate(center.x(), center.y()) - ##print " inverted; translate", center.x(), center.y() - #else: - #m.translate(center.x(), -center.y()) - ##print " not inverted; translate", center.x(), -center.y() - - ### Now scale and translate properly - #if self.aspectLocked: - #scale = Point(scale.min()) - #if not self.yInverted: - #scale = scale * Point(1, -1) - #m.scale(scale[0], scale[1]) - ##print " scale:", scale - #st = translate - #m.translate(-st[0], -st[1]) - ##print " translate:", st - #self.setTransform(m) - #self.currentScale = scale - ##self.emit(QtCore.SIGNAL('viewChanged'), self.range) - self.sigRangeChanged.emit(self, self.range) - - if propagate: - for v in self.lockedViewports: - v.setXRange(self.range, padding=0) - - def visibleRange(self): - """Return the boundaries of the view in scene coordinates""" - ## easier to just return self.range ? - r = QtCore.QRectF(self.rect()) - return self.viewportTransform().inverted()[0].mapRect(r) - - def translate(self, dx, dy): - self.range.adjust(dx, dy, dx, dy) - self.updateMatrix() - - def scale(self, sx, sy, center=None): - scale = [sx, sy] - if self.aspectLocked: - scale[0] = scale[1] - #adj = (self.range.width()*0.5*(1.0-(1.0/scale[0])), self.range.height()*0.5*(1.0-(1.0/scale[1]))) - #print "======\n", scale, adj - #print self.range - #self.range.adjust(adj[0], adj[1], -adj[0], -adj[1]) - #print self.range - - if self.scaleCenter: - center = None - if center is None: - center = self.range.center() - - w = self.range.width() / scale[0] - h = self.range.height() / scale[1] - self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) - - - self.updateMatrix() - - def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True): - if disableAutoPixel: - self.autoPixelRange=False - if newRect is None: - newRect = self.visibleRange() - padding = 0 - padding = Point(padding) - newRect = QtCore.QRectF(newRect) - pw = newRect.width() * padding[0] - ph = newRect.height() * padding[1] - self.range = newRect.adjusted(-pw, -ph, pw, ph) - #print "New Range:", self.range - self.centralWidget.setGeometry(self.range) - self.updateMatrix(propagate) - - def scaleToImage(self, image): - """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.""" - pxSize = image.pixelSize() - tl = image.sceneBoundingRect().topLeft() - w = self.size().width() * pxSize[0] - h = self.size().height() * pxSize[1] - range = QtCore.QRectF(tl.x(), tl.y(), w, h) - self.setRange(range, padding=0) - - - - def lockXRange(self, v1): - if not v1 in self.lockedViewports: - self.lockedViewports.append(v1) - - def setXRange(self, r, padding=0.05): - r1 = QtCore.QRectF(self.range) - r1.setLeft(r.left()) - r1.setRight(r.right()) - self.setRange(r1, padding=[padding, 0], propagate=False) - - def setYRange(self, r, padding=0.05): - r1 = QtCore.QRectF(self.range) - r1.setTop(r.top()) - r1.setBottom(r.bottom()) - self.setRange(r1, padding=[0, padding], propagate=False) - - #def invertY(self, invert=True): - ##if self.yInverted != invert: - ##self.scale[1] *= -1. - #self.yInverted = invert - #self.updateMatrix() - - - def wheelEvent(self, ev): - QtGui.QGraphicsView.wheelEvent(self, ev) - if not self.mouseEnabled: - return - sc = 1.001 ** ev.delta() - #self.scale *= sc - #self.updateMatrix() - self.scale(sc, sc) - - def setAspectLocked(self, s): - self.aspectLocked = s - - #def mouseDoubleClickEvent(self, ev): - #QtGui.QGraphicsView.mouseDoubleClickEvent(self, ev) - #pass - - ### This function is here because interactive mode is disabled due to bugs. - #def graphicsSceneEvent(self, ev, pev=None, fev=None): - #ev1 = GraphicsSceneMouseEvent() - #ev1.setPos(QtCore.QPointF(ev.pos().x(), ev.pos().y())) - #ev1.setButtons(ev.buttons()) - #ev1.setButton(ev.button()) - #ev1.setModifiers(ev.modifiers()) - #ev1.setScenePos(self.mapToScene(QtCore.QPoint(ev.pos()))) - #if pev is not None: - #ev1.setLastPos(pev.pos()) - #ev1.setLastScenePos(pev.scenePos()) - #ev1.setLastScreenPos(pev.screenPos()) - #if fev is not None: - #ev1.setButtonDownPos(fev.pos()) - #ev1.setButtonDownScenePos(fev.scenePos()) - #ev1.setButtonDownScreenPos(fev.screenPos()) - #return ev1 - - def mousePressEvent(self, ev): - QtGui.QGraphicsView.mousePressEvent(self, ev) - - #print "Press over:" - #for i in self.items(ev.pos()): - # print i.zValue(), int(i.acceptedMouseButtons()), i, i.scenePos() - #print "Event accepted:", ev.isAccepted() - #print "Grabber:", self.scene().mouseGrabberItem() - - - if not self.mouseEnabled: - return - self.lastMousePos = Point(ev.pos()) - self.mousePressPos = ev.pos() - self.clickAccepted = ev.isAccepted() - if not self.clickAccepted: - self.scene().clearSelection() - return ## Everything below disabled for now.. - - #self.currentItem = None - #maxZ = None - #for i in self.items(ev.pos()): - #if maxZ is None or maxZ < i.zValue(): - #self.currentItem = i - #maxZ = i.zValue() - #print "make event" - #self.pev = self.graphicsSceneEvent(ev) - #self.fev = self.pev - #if self.currentItem is not None: - #self.currentItem.mousePressEvent(self.pev) - ##self.clearMouse() - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mousePressed(PyQt_PyObject)"), self.mouseTrail) - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsView.mouseReleaseEvent(self, ev) - if not self.mouseEnabled: - return - #self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mouseReleased"), ev) - self.sigMouseReleased.emit(ev) - self.lastButtonReleased = ev.button() - return ## Everything below disabled for now.. - - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #self.emit(QtCore.SIGNAL("mouseReleased(PyQt_PyObject)"), self.mouseTrail) - #if self.currentItem is not None: - #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) - #self.pev = pev - #self.currentItem.mouseReleaseEvent(pev) - #self.currentItem = None - - def mouseMoveEvent(self, ev): - if self.lastMousePos is None: - self.lastMousePos = Point(ev.pos()) - delta = Point(ev.pos() - self.lastMousePos) - self.lastMousePos = Point(ev.pos()) - - QtGui.QGraphicsView.mouseMoveEvent(self, ev) - if not self.mouseEnabled: - return - #self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos())) - self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos())) - #print "moved. Grabber:", self.scene().mouseGrabberItem() - - - if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. - return - - if ev.buttons() == QtCore.Qt.RightButton: - delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) - scale = 1.01 ** delta - #if self.yInverted: - #scale[0] = 1. / scale[0] - self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) - #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) - self.sigRangeChanged.emit(self, self.range) - - elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. - px = self.pixelSize() - tr = -delta * px - - self.translate(tr[0], tr[1]) - #self.emit(QtCore.SIGNAL('regionChanged(QRectF)'), self.range) - self.sigRangeChanged.emit(self, self.range) - - #return ## Everything below disabled for now.. - - ##self.mouseTrail.append(Point(self.mapToScene(ev.pos()))) - #if self.currentItem is not None: - #pev = self.graphicsSceneEvent(ev, self.pev, self.fev) - #self.pev = pev - #self.currentItem.mouseMoveEvent(pev) - - - def pixelSize(self): - """Return vector with the length and width of one view pixel in scene coordinates""" - p0 = Point(0,0) - p1 = Point(1,1) - tr = self.transform().inverted()[0] - p01 = tr.map(p0) - p11 = tr.map(p1) - return Point(p11 - p01) - - - def writeSvg(self, fileName=None): - if fileName is None: - self.fileDialog = QtGui.QFileDialog() - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - if GraphicsView.lastFileDir is not None: - self.fileDialog.setDirectory(GraphicsView.lastFileDir) - self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writeSvg) - return - fileName = str(fileName) - GraphicsView.lastFileDir = os.path.split(fileName)[0] - self.svg = QtSvg.QSvgGenerator() - self.svg.setFileName(fileName) - self.svg.setSize(self.size()) - self.svg.setResolution(600) - painter = QtGui.QPainter(self.svg) - self.render(painter) - - def writeImage(self, fileName=None): - if fileName is None: - self.fileDialog = QtGui.QFileDialog() - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - if GraphicsView.lastFileDir is not None: - self.fileDialog.setDirectory(GraphicsView.lastFileDir) - self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writePng) - return - fileName = str(fileName) - GraphicsView.lastFileDir = os.path.split(fileName)[0] - self.png = QtGui.QImage(self.size(), QtGui.QImage.Format_ARGB32) - painter = QtGui.QPainter(self.png) - rh = self.renderHints() - self.setRenderHints(QtGui.QPainter.Antialiasing) - self.render(painter) - self.setRenderHints(rh) - self.png.save(fileName) - - def writePs(self, fileName=None): - if fileName is None: - fileName = str(QtGui.QFileDialog.getSaveFileName()) - printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) - printer.setOutputFileName(fileName) - painter = QtGui.QPainter(printer) - self.render(painter) - painter.end() - - def dragEnterEvent(self, ev): - ev.ignore() ## not sure why, but for some reason this class likes to consume drag events - - - - #def getFreehandLine(self): - - ## Wait for click - #self.clearMouse() - #while self.lastButtonReleased != QtCore.Qt.LeftButton: - #QtGui.qApp.sendPostedEvents() - #QtGui.qApp.processEvents() - #time.sleep(0.01) - #fl = vstack(self.mouseTrail) - #return fl - - #def getClick(self): - #fl = self.getFreehandLine() - #return fl[-1] - - -#class GraphicsSceneMouseEvent(QtGui.QGraphicsSceneMouseEvent): - #"""Stand-in class for QGraphicsSceneMouseEvent""" - #def __init__(self): - #QtGui.QGraphicsSceneMouseEvent.__init__(self) - - #def setPos(self, p): - #self.vpos = p - #def setButtons(self, p): - #self.vbuttons = p - #def setButton(self, p): - #self.vbutton = p - #def setModifiers(self, p): - #self.vmodifiers = p - #def setScenePos(self, p): - #self.vscenePos = p - #def setLastPos(self, p): - #self.vlastPos = p - #def setLastScenePos(self, p): - #self.vlastScenePos = p - #def setLastScreenPos(self, p): - #self.vlastScreenPos = p - #def setButtonDownPos(self, p): - #self.vbuttonDownPos = p - #def setButtonDownScenePos(self, p): - #self.vbuttonDownScenePos = p - #def setButtonDownScreenPos(self, p): - #self.vbuttonDownScreenPos = p - - #def pos(self): - #return self.vpos - #def buttons(self): - #return self.vbuttons - #def button(self): - #return self.vbutton - #def modifiers(self): - #return self.vmodifiers - #def scenePos(self): - #return self.vscenePos - #def lastPos(self): - #return self.vlastPos - #def lastScenePos(self): - #return self.vlastScenePos - #def lastScreenPos(self): - #return self.vlastScreenPos - #def buttonDownPos(self): - #return self.vbuttonDownPos - #def buttonDownScenePos(self): - #return self.vbuttonDownScenePos - #def buttonDownScreenPos(self): - #return self.vbuttonDownScreenPos - diff --git a/ImageViewTemplate.ui b/ImageViewTemplate.ui deleted file mode 100644 index 98d58a1f..00000000 --- a/ImageViewTemplate.ui +++ /dev/null @@ -1,283 +0,0 @@ - - - Form - - - - 0 - 0 - 726 - 588 - - - - Form - - - - 0 - - - 0 - - - - - Qt::Vertical - - - - - 0 - - - 0 - - - - - - 10 - 10 - - - - - - - - - 0 - 1 - - - - - 30 - 16777215 - - - - R - - - true - - - - - - - - 0 - 100 - - - - - - - - - 0 - 1 - - - - - 30 - 16777215 - - - - N - - - true - - - - - - - Normalization - - - - 0 - - - 0 - - - - - Subtract - - - - - - - Divide - - - false - - - - - - - - 75 - true - - - - Operation: - - - - - - - - 75 - true - - - - Mean: - - - - - - - - 75 - true - - - - Blur: - - - - - - - ROI - - - - - - - - - - X - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Y - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - T - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Off - - - true - - - - - - - Time range - - - - - - - Frame - - - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 40 - - - - - - - - - - GradientWidget - QWidget -
pyqtgraph.GradientWidget
- 1 -
- - GraphicsView - QWidget -
GraphicsView
- 1 -
- - PlotWidget - QWidget -
PlotWidget
- 1 -
-
- - -
diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..22791ae3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,10 @@ +Copyright (c) 2012 University of North Carolina at Chapel Hill +Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + +The MIT License +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/PlotItem.py b/PlotItem.py deleted file mode 100644 index d53d2bfc..00000000 --- a/PlotItem.py +++ /dev/null @@ -1,1284 +0,0 @@ -# -*- coding: utf-8 -*- -""" -PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -This class is one of the workhorses of pyqtgraph. It implements a graphics item with -plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want -a widget that can be added to your GUI, see PlotWidget instead. - -This class is very heavily featured: - - Automatically creates and manages PlotCurveItems - - Fast display and update of plots - - Manages zoom/pan ViewBox, scale, and label elements - - Automatic scaling when data changes - - Control panel with a huge feature set including averaging, decimation, - display, power spectrum, svg/png export, plot linking, and more. -""" - -from graphicsItems import * -from plotConfigTemplate import * -from PyQt4 import QtGui, QtCore, QtSvg -from functions import * -#from ObjectWorkaround import * -#tryWorkaround(QtCore, QtGui) -import weakref -import numpy as np -#import debug - -try: - from WidgetGroup import * - HAVE_WIDGETGROUP = True -except: - HAVE_WIDGETGROUP = False - -try: - from metaarray import * - HAVE_METAARRAY = True -except: - HAVE_METAARRAY = False - - -class PlotItem(QtGui.QGraphicsWidget): - - sigYRangeChanged = QtCore.Signal(object, object) - sigXRangeChanged = QtCore.Signal(object, object) - sigRangeChanged = QtCore.Signal(object, object) - - """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" - lastFileDir = None - managers = {} - - def __init__(self, parent=None, name=None, labels=None, **kargs): - QtGui.QGraphicsWidget.__init__(self, parent) - - ## Set up control buttons - - self.ctrlBtn = QtGui.QToolButton() - self.ctrlBtn.setText('?') - self.autoBtn = QtGui.QToolButton() - self.autoBtn.setText('A') - self.autoBtn.hide() - self.proxies = [] - for b in [self.ctrlBtn, self.autoBtn]: - proxy = QtGui.QGraphicsProxyWidget(self) - proxy.setWidget(b) - proxy.setAcceptHoverEvents(False) - b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") - self.proxies.append(proxy) - #QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) - self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) - #QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) - self.autoBtn.clicked.connect(self.enableAutoScale) - - - self.layout = QtGui.QGraphicsGridLayout() - self.layout.setContentsMargins(1,1,1,1) - self.setLayout(self.layout) - self.layout.setHorizontalSpacing(0) - self.layout.setVerticalSpacing(0) - - self.vb = ViewBox() - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('xRangeChanged'), self.xRangeChanged) - self.vb.sigXRangeChanged.connect(self.xRangeChanged) - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('yRangeChanged'), self.yRangeChanged) - self.vb.sigYRangeChanged.connect(self.yRangeChanged) - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('rangeChangedManually'), self.enableManualScale) - self.vb.sigRangeChangedManually.connect(self.enableManualScale) - - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('viewChanged'), self.viewChanged) - self.vb.sigRangeChanged.connect(self.viewRangeChanged) - - self.layout.addItem(self.vb, 2, 1) - self.alpha = 1.0 - self.autoAlpha = True - self.spectrumMode = False - - self.autoScale = [True, True] - - ## Create and place scale items - self.scales = { - 'top': {'item': ScaleItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, - 'bottom': {'item': ScaleItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, - 'left': {'item': ScaleItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, - 'right': {'item': ScaleItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} - } - for k in self.scales: - self.layout.addItem(self.scales[k]['item'], *self.scales[k]['pos']) - - ## Create and place label items - #self.labels = { - #'title': {'item': LabelItem('title', size='11pt'), 'pos': (0, 2), 'text': ''}, - #'top': {'item': LabelItem('top'), 'pos': (1, 2), 'text': '', 'units': '', 'unitPrefix': ''}, - #'bottom': {'item': LabelItem('bottom'), 'pos': (5, 2), 'text': '', 'units': '', 'unitPrefix': ''}, - #'left': {'item': LabelItem('left'), 'pos': (3, 0), 'text': '', 'units': '', 'unitPrefix': ''}, - #'right': {'item': LabelItem('right'), 'pos': (3, 4), 'text': '', 'units': '', 'unitPrefix': ''} - #} - #self.labels['left']['item'].setAngle(-90) - #self.labels['right']['item'].setAngle(-90) - #for k in self.labels: - #self.layout.addItem(self.labels[k]['item'], *self.labels[k]['pos']) - self.titleLabel = LabelItem('', size='11pt') - self.layout.addItem(self.titleLabel, 0, 1) - self.setTitle(None) ## hide - - - for i in range(4): - self.layout.setRowPreferredHeight(i, 0) - self.layout.setRowMinimumHeight(i, 0) - self.layout.setRowSpacing(i, 0) - self.layout.setRowStretchFactor(i, 1) - - for i in range(3): - self.layout.setColumnPreferredWidth(i, 0) - self.layout.setColumnMinimumWidth(i, 0) - self.layout.setColumnSpacing(i, 0) - self.layout.setColumnStretchFactor(i, 1) - self.layout.setRowStretchFactor(2, 100) - self.layout.setColumnStretchFactor(1, 100) - - - ## Wrap a few methods from viewBox - for m in ['setXRange', 'setYRange', 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled']: - setattr(self, m, getattr(self.vb, m)) - - self.items = [] - self.curves = [] - self.dataItems = [] - self.paramList = {} - self.avgCurves = {} - - ### Set up context menu - - w = QtGui.QWidget() - self.ctrl = c = Ui_Form() - c.setupUi(w) - dv = QtGui.QDoubleValidator(self) - self.ctrlMenu = QtGui.QMenu() - self.menuAction = QtGui.QWidgetAction(self) - self.menuAction.setDefaultWidget(w) - self.ctrlMenu.addAction(self.menuAction) - - if HAVE_WIDGETGROUP: - self.stateGroup = WidgetGroup(self.ctrlMenu) - - self.fileDialog = None - - self.xLinkPlot = None - self.yLinkPlot = None - self.linksBlocked = False - - - #self.ctrlBtn.setFixedWidth(60) - self.setAcceptHoverEvents(True) - - ## Connect control widgets - #QtCore.QObject.connect(c.xMinText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - c.xMinText.editingFinished.connect(self.setManualXScale) - #QtCore.QObject.connect(c.xMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - c.xMaxText.editingFinished.connect(self.setManualXScale) - #QtCore.QObject.connect(c.yMinText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) - c.yMinText.editingFinished.connect(self.setManualYScale) - #QtCore.QObject.connect(c.yMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) - c.yMaxText.editingFinished.connect(self.setManualYScale) - - #QtCore.QObject.connect(c.xManualRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - c.xManualRadio.clicked.connect(lambda: self.updateXScale()) - #QtCore.QObject.connect(c.yManualRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) - c.yManualRadio.clicked.connect(lambda: self.updateYScale()) - - #QtCore.QObject.connect(c.xAutoRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - c.xAutoRadio.clicked.connect(self.updateXScale) - #QtCore.QObject.connect(c.yAutoRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) - c.yAutoRadio.clicked.connect(self.updateYScale) - - #QtCore.QObject.connect(c.xAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) - c.xAutoPercentSpin.valueChanged.connect(self.replot) - #QtCore.QObject.connect(c.yAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) - c.yAutoPercentSpin.valueChanged.connect(self.replot) - - #QtCore.QObject.connect(c.xLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setXLog) - #QtCore.QObject.connect(c.yLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setYLog) - - #QtCore.QObject.connect(c.alphaGroup, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) - c.alphaGroup.toggled.connect(self.updateAlpha) - #QtCore.QObject.connect(c.alphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateAlpha) - c.alphaSlider.valueChanged.connect(self.updateAlpha) - #QtCore.QObject.connect(c.autoAlphaCheck, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) - c.autoAlphaCheck.toggled.connect(self.updateAlpha) - - #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) - c.gridGroup.toggled.connect(self.updateGrid) - #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) - c.gridAlphaSlider.valueChanged.connect(self.updateGrid) - - #QtCore.QObject.connect(c.powerSpectrumGroup, QtCore.SIGNAL('toggled(bool)'), self.updateSpectrumMode) - c.powerSpectrumGroup.toggled.connect(self.updateSpectrumMode) - #QtCore.QObject.connect(c.saveSvgBtn, QtCore.SIGNAL('clicked()'), self.saveSvgClicked) - c.saveSvgBtn.clicked.connect(self.saveSvgClicked) - #QtCore.QObject.connect(c.saveImgBtn, QtCore.SIGNAL('clicked()'), self.saveImgClicked) - c.saveImgBtn.clicked.connect(self.saveImgClicked) - #QtCore.QObject.connect(c.saveCsvBtn, QtCore.SIGNAL('clicked()'), self.saveCsvClicked) - c.saveCsvBtn.clicked.connect(self.saveCsvClicked) - - #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) - #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) - - #QtCore.QObject.connect(self.ctrl.xLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.xLinkComboChanged) - self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) - #QtCore.QObject.connect(self.ctrl.yLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.yLinkComboChanged) - self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) - - #QtCore.QObject.connect(c.downsampleSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDownsampling) - c.downsampleSpin.valueChanged.connect(self.updateDownsampling) - - #QtCore.QObject.connect(self.ctrl.avgParamList, QtCore.SIGNAL('itemClicked(QListWidgetItem*)'), self.avgParamListClicked) - self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) - #QtCore.QObject.connect(self.ctrl.averageGroup, QtCore.SIGNAL('toggled(bool)'), self.avgToggled) - self.ctrl.averageGroup.toggled.connect(self.avgToggled) - - #QtCore.QObject.connect(self.ctrl.pointsGroup, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) - #QtCore.QObject.connect(self.ctrl.autoPointsCheck, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) - - #QtCore.QObject.connect(self.ctrl.maxTracesCheck, QtCore.SIGNAL('toggled(bool)'), self.updateDecimation) - self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) - #QtCore.QObject.connect(self.ctrl.maxTracesSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDecimation) - self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - #QtCore.QObject.connect(c.xMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) - c.xMouseCheck.toggled.connect(self.mouseCheckChanged) - #QtCore.QObject.connect(c.yMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) - c.yMouseCheck.toggled.connect(self.mouseCheckChanged) - - self.xLinkPlot = None - self.yLinkPlot = None - self.linksBlocked = False - self.manager = None - - #self.showLabel('right', False) - #self.showLabel('top', False) - #self.showLabel('title', False) - #self.showLabel('left', False) - #self.showLabel('bottom', False) - self.showScale('right', False) - self.showScale('top', False) - self.showScale('left', True) - self.showScale('bottom', True) - - if name is not None: - self.registerPlot(name) - - if labels is not None: - for k in labels: - if isinstance(labels[k], basestring): - labels[k] = (labels[k],) - self.setLabel(k, *labels[k]) - - if len(kargs) > 0: - self.plot(**kargs) - - #def paint(self, *args): - #prof = debug.Profiler('PlotItem.paint', disabled=True) - #QtGui.QGraphicsWidget.paint(self, *args) - #prof.finish() - - - def close(self): - #print "delete", self - ## Most of this crap is needed to avoid PySide trouble. - ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) - ## the solution is to manually remove all widgets before scene.clear() is called - if self.ctrlMenu is None: ## already shut down - return - self.ctrlMenu.setParent(None) - self.ctrlMenu = None - - self.ctrlBtn.setParent(None) - self.ctrlBtn = None - self.autoBtn.setParent(None) - self.autoBtn = None - - for k in self.scales: - i = self.scales[k]['item'] - i.close() - - self.scales = None - self.scene().removeItem(self.vb) - self.vb = None - - ## causes invalid index errors: - #for i in range(self.layout.count()): - #self.layout.removeAt(i) - - for p in self.proxies: - try: - p.setWidget(None) - except RuntimeError: - break - self.scene().removeItem(p) - self.proxies = [] - - self.menuAction.releaseWidget(self.menuAction.defaultWidget()) - self.menuAction.setParent(None) - self.menuAction = None - - if self.manager is not None: - self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) - self.manager.removeWidget(self.name) - #else: - #print "no manager" - - def registerPlot(self, name): - self.name = name - win = str(self.window()) - #print "register", name, win - if win not in PlotItem.managers: - PlotItem.managers[win] = PlotWidgetManager() - self.manager = PlotItem.managers[win] - self.manager.addWidget(self, name) - #QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) - self.manager.sigWidgetListChanged.connect(self.updatePlotList) - self.updatePlotList() - - def updatePlotList(self): - """Update the list of all plotWidgets in the "link" combos""" - #print "update plot list", self - try: - for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: - current = str(sc.currentText()) - sc.clear() - sc.addItem("") - if self.manager is not None: - for w in self.manager.listWidgets(): - #print w - if w == self.name: - continue - sc.addItem(w) - except: - import gc - refs= gc.get_referrers(self) - print " error during update of", self - print " Referrers are:", refs - raise - - def updateGrid(self, *args): - g = self.ctrl.gridGroup.isChecked() - if g: - g = self.ctrl.gridAlphaSlider.value() - for k in self.scales: - self.scales[k]['item'].setGrid(g) - - def viewGeometry(self): - """return the screen geometry of the viewbox""" - v = self.scene().views()[0] - b = self.vb.mapRectToScene(self.vb.boundingRect()) - wr = v.mapFromScene(b).boundingRect() - pos = v.mapToGlobal(v.pos()) - wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) - return wr - - - - - def viewRangeChanged(self, vb, range): - #self.emit(QtCore.SIGNAL('viewChanged'), *args) - self.sigRangeChanged.emit(self, range) - - def blockLink(self, b): - self.linksBlocked = b - - def xLinkComboChanged(self): - self.setXLink(str(self.ctrl.xLinkCombo.currentText())) - - def yLinkComboChanged(self): - self.setYLink(str(self.ctrl.yLinkCombo.currentText())) - - def setXLink(self, plot=None): - """Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - if isinstance(plot, basestring): - if self.manager is None: - return - if self.xLinkPlot is not None: - self.manager.unlinkX(self, self.xLinkPlot) - plot = self.manager.getWidget(plot) - if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - plot = plot.getPlotItem() - self.xLinkPlot = plot - if plot is not None: - self.setManualXScale() - self.manager.linkX(self, plot) - - def setYLink(self, plot=None): - """Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - if isinstance(plot, basestring): - if self.manager is None: - return - if self.yLinkPlot is not None: - self.manager.unlinkY(self, self.yLinkPlot) - plot = self.manager.getWidget(plot) - if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - plot = plot.getPlotItem() - self.yLinkPlot = plot - if plot is not None: - self.setManualYScale() - self.manager.linkY(self, plot) - - def linkXChanged(self, plot): - """Called when a linked plot has changed its X scale""" - #print "update from", plot - if self.linksBlocked: - return - pr = plot.vb.viewRect() - pg = plot.viewGeometry() - if pg is None: - #print " return early" - return - sg = self.viewGeometry() - upp = float(pr.width()) / pg.width() - x1 = pr.left() + (sg.x()-pg.x()) * upp - x2 = x1 + sg.width() * upp - plot.blockLink(True) - self.setManualXScale() - self.setXRange(x1, x2, padding=0) - plot.blockLink(False) - self.replot() - - def linkYChanged(self, plot): - """Called when a linked plot has changed its Y scale""" - if self.linksBlocked: - return - pr = plot.vb.viewRect() - pg = plot.vb.boundingRect() - sg = self.vb.boundingRect() - upp = float(pr.height()) / pg.height() - y1 = pr.bottom() + (sg.y()-pg.y()) * upp - y2 = y1 + sg.height() * upp - plot.blockLink(True) - self.setManualYScale() - self.setYRange(y1, y2, padding=0) - plot.blockLink(False) - self.replot() - - def avgToggled(self, b): - if b: - self.recomputeAverages() - for k in self.avgCurves: - self.avgCurves[k][1].setVisible(b) - - def avgParamListClicked(self, item): - name = str(item.text()) - self.paramList[name] = (item.checkState() == QtCore.Qt.Checked) - self.recomputeAverages() - - def recomputeAverages(self): - if not self.ctrl.averageGroup.isChecked(): - return - for k in self.avgCurves: - self.removeItem(self.avgCurves[k][1]) - #Qwt.QwtPlotCurve.detach(self.avgCurves[k][1]) - self.avgCurves = {} - for c in self.curves: - self.addAvgCurve(c) - self.replot() - - def addAvgCurve(self, curve): - """Add a single curve into the pool of curves averaged together""" - - ## If there are plot parameters, then we need to determine which to average together. - remKeys = [] - addKeys = [] - if self.ctrl.avgParamList.count() > 0: - - ### First determine the key of the curve to which this new data should be averaged - for i in range(self.ctrl.avgParamList.count()): - item = self.ctrl.avgParamList.item(i) - if item.checkState() == QtCore.Qt.Checked: - remKeys.append(str(item.text())) - else: - addKeys.append(str(item.text())) - - if len(remKeys) < 1: ## In this case, there would be 1 average plot for each data plot; not useful. - return - - p = curve.meta().copy() - for k in p: - if type(k) is tuple: - p['.'.join(k)] = p[k] - del p[k] - for rk in remKeys: - if rk in p: - del p[rk] - for ak in addKeys: - if ak not in p: - p[ak] = None - key = tuple(p.items()) - - ### Create a new curve if needed - if key not in self.avgCurves: - plot = PlotCurveItem() - plot.setPen(mkPen([0, 200, 0])) - plot.setShadowPen(mkPen([0, 0, 0, 100], 3)) - plot.setAlpha(1.0, False) - plot.setZValue(100) - self.addItem(plot) - #Qwt.QwtPlotCurve.attach(plot, self) - self.avgCurves[key] = [0, plot] - self.avgCurves[key][0] += 1 - (n, plot) = self.avgCurves[key] - - ### Average data together - (x, y) = curve.getData() - if plot.yData is not None: - newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) - plot.setData(plot.xData, newData) - else: - plot.setData(x, y) - - - def mouseCheckChanged(self): - state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] - self.vb.setMouseEnabled(*state) - - def xRangeChanged(self, _, range): - if any(np.isnan(range)) or any(np.isinf(range)): - raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - self.ctrl.xMinText.setText('%0.5g' % range[0]) - self.ctrl.xMaxText.setText('%0.5g' % range[1]) - - ## automatically change unit scale - maxVal = max(abs(range[0]), abs(range[1])) - (scale, prefix) = siScale(maxVal) - #for l in ['top', 'bottom']: - #if self.getLabel(l).isVisible(): - #self.setLabel(l, unitPrefix=prefix) - #self.getScale(l).setScale(scale) - #else: - #self.setLabel(l, unitPrefix='') - #self.getScale(l).setScale(1.0) - - #self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) - self.sigXRangeChanged.emit(self, range) - - def yRangeChanged(self, _, range): - if any(np.isnan(range)) or any(np.isinf(range)): - raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - self.ctrl.yMinText.setText('%0.5g' % range[0]) - self.ctrl.yMaxText.setText('%0.5g' % range[1]) - - ## automatically change unit scale - maxVal = max(abs(range[0]), abs(range[1])) - (scale, prefix) = siScale(maxVal) - #for l in ['left', 'right']: - #if self.getLabel(l).isVisible(): - #self.setLabel(l, unitPrefix=prefix) - #self.getScale(l).setScale(scale) - #else: - #self.setLabel(l, unitPrefix='') - #self.getScale(l).setScale(1.0) - #self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) - self.sigYRangeChanged.emit(self, range) - - - def enableAutoScale(self): - self.ctrl.xAutoRadio.setChecked(True) - self.ctrl.yAutoRadio.setChecked(True) - self.autoBtn.hide() - self.updateXScale() - self.updateYScale() - self.replot() - - def updateXScale(self): - """Set plot to autoscale or not depending on state of radio buttons""" - if self.ctrl.xManualRadio.isChecked(): - self.setManualXScale() - else: - self.setAutoXScale() - self.replot() - - def updateYScale(self, b=False): - """Set plot to autoscale or not depending on state of radio buttons""" - if self.ctrl.yManualRadio.isChecked(): - self.setManualYScale() - else: - self.setAutoYScale() - self.replot() - - def enableManualScale(self, v=[True, True]): - if v[0]: - self.autoScale[0] = False - self.ctrl.xManualRadio.setChecked(True) - #self.setManualXScale() - if v[1]: - self.autoScale[1] = False - self.ctrl.yManualRadio.setChecked(True) - #self.setManualYScale() - self.autoBtn.show() - #self.replot() - - def setManualXScale(self): - self.autoScale[0] = False - x1 = float(self.ctrl.xMinText.text()) - x2 = float(self.ctrl.xMaxText.text()) - self.ctrl.xManualRadio.setChecked(True) - self.setXRange(x1, x2, padding=0) - self.autoBtn.show() - #self.replot() - - def setManualYScale(self): - self.autoScale[1] = False - y1 = float(self.ctrl.yMinText.text()) - y2 = float(self.ctrl.yMaxText.text()) - self.ctrl.yManualRadio.setChecked(True) - self.setYRange(y1, y2, padding=0) - self.autoBtn.show() - #self.replot() - - def setAutoXScale(self): - self.autoScale[0] = True - self.ctrl.xAutoRadio.setChecked(True) - #self.replot() - - def setAutoYScale(self): - self.autoScale[1] = True - self.ctrl.yAutoRadio.setChecked(True) - #self.replot() - - def addItem(self, item, *args): - self.items.append(item) - self.vb.addItem(item, *args) - - def removeItem(self, item): - if not item in self.items: - return - self.items.remove(item) - if item in self.dataItems: - self.dataItems.remove(item) - - if item.scene() is not None: - self.vb.removeItem(item) - if item in self.curves: - self.curves.remove(item) - self.updateDecimation() - self.updateParamList() - #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) - item.sigPlotChanged.connect(self.plotChanged) - - def clear(self): - for i in self.items[:]: - self.removeItem(i) - self.avgCurves = {} - - def clearPlots(self): - for i in self.curves[:]: - self.removeItem(i) - self.avgCurves = {} - - - def plot(self, data=None, data2=None, x=None, y=None, clear=False, params=None, pen=None): - """Add a new plot curve. Data may be specified a few ways: - plot(yVals) # x vals will be integers - plot(xVals, yVals) - plot(y=yVals, x=xVals) - """ - if y is not None: - data = y - if data2 is not None: - x = data - data = data2 - - if clear: - self.clear() - if params is None: - params = {} - if HAVE_METAARRAY and isinstance(data, MetaArray): - curve = self._plotMetaArray(data, x=x) - elif isinstance(data, np.ndarray): - curve = self._plotArray(data, x=x) - elif isinstance(data, list): - if x is not None: - x = np.array(x) - curve = self._plotArray(np.array(data), x=x) - elif data is None: - curve = PlotCurveItem() - else: - raise Exception('Not sure how to plot object of type %s' % type(data)) - - #print data, curve - self.addCurve(curve, params) - if pen is not None: - curve.setPen(mkPen(pen)) - - return curve - - def scatterPlot(self, *args, **kargs): - sp = ScatterPlotItem(*args, **kargs) - self.addDataItem(sp) - return sp - - def addDataItem(self, item): - self.addItem(item) - self.dataItems.append(item) - - def addCurve(self, c, params=None): - if params is None: - params = {} - c.setMeta(params) - self.curves.append(c) - #Qwt.QwtPlotCurve.attach(c, self) - self.addItem(c) - - ## configure curve for this plot - (alpha, auto) = self.alphaState() - c.setAlpha(alpha, auto) - c.setSpectrumMode(self.ctrl.powerSpectrumGroup.isChecked()) - c.setDownsampling(self.downsampleMode()) - c.setPointMode(self.pointMode()) - - ## Hide older plots if needed - self.updateDecimation() - - ## Add to average if needed - self.updateParamList() - if self.ctrl.averageGroup.isChecked(): - self.addAvgCurve(c) - - #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) - c.sigPlotChanged.connect(self.plotChanged) - self.plotChanged() - - def plotChanged(self, curve=None): - ## Recompute auto range if needed - for ax in [0, 1]: - if self.autoScale[ax]: - percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 - mn = None - mx = None - for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems: - if not c.isVisible(): - continue - cmn, cmx = c.getRange(ax, percentScale) - if mn is None or cmn < mn: - mn = cmn - if mx is None or cmx > mx: - mx = cmx - if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): - continue - if mn == mx: - mn -= 1 - mx += 1 - self.setRange(ax, mn, mx) - #print "Auto range:", ax, mn, mx - - def replot(self): - self.plotChanged() - self.update() - - def updateParamList(self): - self.ctrl.avgParamList.clear() - ## Check to see that each parameter for each curve is present in the list - #print "\nUpdate param list", self - #print "paramList:", self.paramList - for c in self.curves: - #print " curve:", c - for p in c.meta().keys(): - #print " param:", p - if type(p) is tuple: - p = '.'.join(p) - - ## If the parameter is not in the list, add it. - matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) - #print " matches:", matches - if len(matches) == 0: - i = QtGui.QListWidgetItem(p) - if p in self.paramList and self.paramList[p] is True: - #print " set checked" - i.setCheckState(QtCore.Qt.Checked) - else: - #print " set unchecked" - i.setCheckState(QtCore.Qt.Unchecked) - self.ctrl.avgParamList.addItem(i) - else: - i = matches[0] - - self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) - #print "paramList:", self.paramList - - - ## This is bullshit. - def writeSvg(self, fileName=None): - if fileName is None: - fileName = QtGui.QFileDialog.getSaveFileName() - if isinstance(fileName, tuple): - raise Exception("Not implemented yet..") - fileName = str(fileName) - PlotItem.lastFileDir = os.path.dirname(fileName) - - rect = self.vb.viewRect() - xRange = rect.left(), rect.right() - - svg = "" - fh = open(fileName, 'w') - - dx = max(rect.right(),0) - min(rect.left(),0) - ymn = min(rect.top(), rect.bottom()) - ymx = max(rect.top(), rect.bottom()) - dy = max(ymx,0) - min(ymn,0) - sx = 1. - sy = 1. - while dx*sx < 10: - sx *= 1000 - while dy*sy < 10: - sy *= 1000 - sy *= -1 - - #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) - fh.write('\n') - fh.write('\n' % (rect.left()*sx, rect.right()*sx)) - fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) - - - for item in self.curves: - if isinstance(item, PlotCurveItem): - color = colorStr(item.pen.color()) - opacity = item.pen.color().alpha() / 255. - color = color[:6] - x, y = item.getData() - mask = (x > xRange[0]) * (x < xRange[1]) - mask[:-1] += mask[1:] - m2 = mask.copy() - mask[1:] += m2[:-1] - x = x[mask] - y = y[mask] - - x *= sx - y *= sy - - #fh.write('\n' % color) - fh.write('') - #fh.write("") - for item in self.dataItems: - if isinstance(item, ScatterPlotItem): - - pRect = item.boundingRect() - vRect = pRect.intersected(rect) - - for point in item.points(): - pos = point.pos() - if not rect.contains(pos): - continue - color = colorStr(point.brush.color()) - opacity = point.brush.color().alpha() / 255. - color = color[:6] - x = pos.x() * sx - y = pos.y() * sy - - fh.write('\n' % (x, y, color, opacity)) - #fh.write('') - - ## get list of curves, scatter plots - - - fh.write("\n") - - - - #def writeSvg(self, fileName=None): - #if fileName is None: - #fileName = QtGui.QFileDialog.getSaveFileName() - #fileName = str(fileName) - #PlotItem.lastFileDir = os.path.dirname(fileName) - - #self.svg = QtSvg.QSvgGenerator() - #self.svg.setFileName(fileName) - #res = 120. - #view = self.scene().views()[0] - #bounds = view.viewport().rect() - #bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) - - #self.svg.setResolution(res) - #self.svg.setViewBox(bounds) - - #self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) - - #painter = QtGui.QPainter(self.svg) - #view.render(painter, bounds) - - #painter.end() - - ### Workaround to set pen widths correctly - #import re - #data = open(fileName).readlines() - #for i in range(len(data)): - #line = data[i] - #m = re.match(r'(= split: - curves[i].show() - else: - if self.ctrl.forgetTracesCheck.isChecked(): - curves[i].free() - self.removeItem(curves[i]) - else: - curves[i].hide() - - - def updateAlpha(self, *args): - (alpha, auto) = self.alphaState() - for c in self.curves: - c.setAlpha(alpha**2, auto) - - #self.replot(autoRange=False) - - def alphaState(self): - enabled = self.ctrl.alphaGroup.isChecked() - auto = self.ctrl.autoAlphaCheck.isChecked() - alpha = float(self.ctrl.alphaSlider.value()) / self.ctrl.alphaSlider.maximum() - if auto: - alpha = 1.0 ## should be 1/number of overlapping plots - if not enabled: - auto = False - alpha = 1.0 - return (alpha, auto) - - def pointMode(self): - if self.ctrl.pointsGroup.isChecked(): - if self.ctrl.autoPointsCheck.isChecked(): - mode = None - else: - mode = True - else: - mode = False - return mode - - def wheelEvent(self, ev): - # disables default panning the whole scene by mousewheel - ev.accept() - - def resizeEvent(self, ev): - if self.ctrlBtn is None: ## already closed down - return - self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height()) - self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height()) - - def hoverMoveEvent(self, ev): - self.mousePos = ev.pos() - self.mouseScreenPos = ev.screenPos() - - def ctrlBtnClicked(self): - self.ctrlMenu.popup(self.mouseScreenPos) - - def getLabel(self, key): - pass - - def _checkScaleKey(self, key): - if key not in self.scales: - raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(self.scales.keys()))) - - def getScale(self, key): - self._checkScaleKey(key) - return self.scales[key]['item'] - - def setLabel(self, key, text=None, units=None, unitPrefix=None, **args): - self.getScale(key).setLabel(text=text, units=units, unitPrefix=unitPrefix, **args) - - def showLabel(self, key, show=True): - self.getScale(key).showLabel(show) - - def setTitle(self, title=None, **args): - if title is None: - self.titleLabel.setVisible(False) - self.layout.setRowFixedHeight(0, 0) - self.titleLabel.setMaximumHeight(0) - else: - self.titleLabel.setMaximumHeight(30) - self.layout.setRowFixedHeight(0, 30) - self.titleLabel.setVisible(True) - self.titleLabel.setText(title, **args) - - def showScale(self, key, show=True): - s = self.getScale(key) - p = self.scales[key]['pos'] - if show: - s.show() - else: - s.hide() - - def _plotArray(self, arr, x=None): - if arr.ndim != 1: - raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) - if x is None: - x = np.arange(arr.shape[0]) - if x.ndim != 1: - raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) - c = PlotCurveItem(arr, x=x) - return c - - - - def _plotMetaArray(self, arr, x=None, autoLabel=True): - inf = arr.infoCopy() - if arr.ndim != 1: - raise Exception('can only automatically plot 1 dimensional arrays.') - ## create curve - try: - xv = arr.xvals(0) - #print 'xvals:', xv - except: - if x is None: - xv = arange(arr.shape[0]) - else: - xv = x - c = PlotCurveItem() - c.setData(x=xv, y=arr.view(np.ndarray)) - - if autoLabel: - name = arr._info[0].get('name', None) - units = arr._info[0].get('units', None) - self.setLabel('bottom', text=name, units=units) - - name = arr._info[1].get('name', None) - units = arr._info[1].get('units', None) - self.setLabel('left', text=name, units=units) - - return c - - def saveSvgClicked(self): - fileName = QtGui.QFileDialog.getSaveFileName() - self.writeSvg(fileName) - - ## QFileDialog seems to be broken under OSX - #self.fileDialog = QtGui.QFileDialog() - ##if PlotItem.lastFileDir is not None: - ##self.fileDialog.setDirectory(PlotItem.lastFileDir) - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - - #self.fileDialog.show() - ##QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeSvg) - #self.fileDialog.fileSelected.connect(self.writeSvg) - - #def svgFileSelected(self, fileName): - ##PlotWidget.lastFileDir = os.path.split(fileName)[0] - #self.writeSvg(str(fileName)) - - def saveImgClicked(self): - self.fileDialog = QtGui.QFileDialog() - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - #QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeImage) - self.fileDialog.fileSelected.connect(self.writeImage) - - def saveCsvClicked(self): - self.fileDialog = QtGui.QFileDialog() - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - #QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeCsv) - self.fileDialog.fileSelected.connect(self.writeCsv) - #def imgFileSelected(self, fileName): - ##PlotWidget.lastFileDir = os.path.split(fileName)[0] - #self.writeImage(str(fileName)) - - -class PlotWidgetManager(QtCore.QObject): - - sigWidgetListChanged = QtCore.Signal(object) - - """Used for managing communication between PlotWidgets""" - def __init__(self): - QtCore.QObject.__init__(self) - self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here - - def addWidget(self, w, name): - self.widgets[name] = w - #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - self.sigWidgetListChanged.emit(self.widgets.keys()) - - def removeWidget(self, name): - if name in self.widgets: - del self.widgets[name] - #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - self.sigWidgetListChanged.emit(self.widgets.keys()) - else: - print "plot %s not managed" % name - - - def listWidgets(self): - return self.widgets.keys() - - def getWidget(self, name): - if name not in self.widgets: - return None - else: - return self.widgets[name] - - def linkX(self, p1, p2): - #QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - p1.sigXRangeChanged.connect(p2.linkXChanged) - #QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - p2.sigXRangeChanged.connect(p1.linkXChanged) - p1.linkXChanged(p2) - #p2.setManualXScale() - - def unlinkX(self, p1, p2): - #QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - p1.sigXRangeChanged.disconnect(p2.linkXChanged) - #QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - p2.sigXRangeChanged.disconnect(p1.linkXChanged) - - def linkY(self, p1, p2): - #QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - p1.sigYRangeChanged.connect(p2.linkYChanged) - #QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - p2.sigYRangeChanged.connect(p1.linkYChanged) - p1.linkYChanged(p2) - #p2.setManualYScale() - - def unlinkY(self, p1, p2): - #QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - p1.sigYRangeChanged.disconnect(p2.linkYChanged) - #QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - p2.sigYRangeChanged.disconnect(p1.linkYChanged) diff --git a/README b/README deleted file mode 100644 index 0072a82f..00000000 --- a/README +++ /dev/null @@ -1,18 +0,0 @@ -PyQtGraph - A pure-Python graphics library for PyQt/PySide -Copyright 2011 University of North Carolina at Chapel Hill - -Authors: - Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') - Megan Kratz - Ingo Breßler - -Requirements: - PyQt 4.5+ or (coming soon) PySide - python 2.6+ - numpy, scipy - - Known to run on Windows, Linux, and Mac. - -Documentation: - None. - You can look around in the examples directory or pester Luke to write some. diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..b51b9aa3 --- /dev/null +++ b/README.txt @@ -0,0 +1,39 @@ +PyQtGraph - A pure-Python graphics library for PyQt/PySide +Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill +http://www.pyqtgraph.org + +Authors: + Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') + Megan Kratz + Ingo Breßler + +Requirements: + PyQt 4.7+ or PySide + python 2.6, 2.7, or 3.x + numpy, scipy + For 3D graphics: pyopengl + Known to run on Windows, Linux, and Mac. + +Support: + Post at the mailing list / forum: + https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph + +Installation Methods: + - To use with a specific project, simply copy the pyqtgraph subdirectory + anywhere that is importable from your project + - To install system-wide from source distribution: + $ python setup.py install + - For instalation packages, see the website (pyqtgraph.org) + +Documentation: + There are many examples; run "python -m pyqtgraph.examples" for a menu. + Some (incomplete) documentation exists at this time. + - Easiest place to get documentation is at + http://www.pyqtgraph.org/documentation + - If you acquired this code as a .tar.gz file from the website, then you can also look in + doc/html. + - If you acquired this code via BZR, then you can build the documentation using sphinx. + From the documentation directory, run: + $ make html + Please feel free to pester Luke or post to the forum if you need a specific + section of documentation. diff --git a/SignalProxy.py b/SignalProxy.py deleted file mode 100644 index 6ac25193..00000000 --- a/SignalProxy.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -from PyQt4 import QtCore -from ptime import time - -class SignalProxy(QtCore.QObject): - """Object which collects rapid-fire signals and condenses them - into a single signal. Used, for example, to prevent a SpinBox - from generating multiple signals when the mouse wheel is rolled - over it.""" - - def __init__(self, source, signal, delay=0.3): - """Initialization arguments: - source - Any QObject that will emit signal, or None if signal is new style - signal - Output of QtCore.SIGNAL(...), or obj.signal for new style - delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s)""" - - QtCore.QObject.__init__(self) - if source is None: - signal.connect(self.signalReceived) - self.signal = QtCore.SIGNAL('signal') - else: - source.connect(source, signal, self.signalReceived) - self.signal = signal - self.delay = delay - self.args = None - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.flush) - self.block = False - - def setDelay(self, delay): - self.delay = delay - - def signalReceived(self, *args): - """Received signal. Cancel previous timer and store args to be forwarded later.""" - if self.block: - return - self.args = args - self.timer.stop() - self.timer.start((self.delay*1000)+1) - - def flush(self): - """If there is a signal queued up, send it now.""" - if self.args is None or self.block: - return False - self.emit(self.signal, *self.args) - self.args = None - return True - - def disconnect(self): - self.block = True - - -def proxyConnect(source, signal, slot, delay=0.3): - """Connect a signal to a slot with delay. Returns the SignalProxy - object that was created. Be sure to store this object so it is not - garbage-collected immediately.""" - sp = SignalProxy(source, signal, delay) - if source is None: - sp.connect(sp, QtCore.SIGNAL('signal'), slot) - else: - sp.connect(sp, signal, slot) - return sp - - -if __name__ == '__main__': - from PyQt4 import QtGui - app = QtGui.QApplication([]) - win = QtGui.QMainWindow() - spin = QtGui.QSpinBox() - win.setCentralWidget(spin) - win.show() - - def fn(*args): - print "Got signal:", args - - proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn) - - \ No newline at end of file diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 618436ac..00000000 --- a/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -### import all the goodies and add some helper functions for easy CLI use - -from functions import * -from graphicsItems import * -from graphicsWindows import * -#import PlotWidget -#import ImageView -from PyQt4 import QtGui -from Point import Point -from Transform import Transform - -plots = [] -images = [] -QAPP = None - -def plot(*args, **kargs): - mkQApp() - if 'title' in kargs: - w = PlotWindow(title=kargs['title']) - del kargs['title'] - else: - w = PlotWindow() - w.plot(*args, **kargs) - plots.append(w) - w.show() - return w - -def show(*args, **kargs): - mkQApp() - w = ImageWindow(*args, **kargs) - images.append(w) - w.show() - return w - -def mkQApp(): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) \ No newline at end of file diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..15b77d38 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyqtgraph.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyqtgraph.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pyqtgraph" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyqtgraph" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/listmissing.py b/doc/listmissing.py new file mode 100644 index 00000000..28fcbcf2 --- /dev/null +++ b/doc/listmissing.py @@ -0,0 +1,14 @@ +import os +dirs = [ + ('graphicsItems', 'graphicsItems'), + ('3dgraphics', 'opengl/items'), + ('widgets', 'widgets'), +] + +path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') +for a, b in dirs: + rst = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, 'documentation', 'source', a))] + py = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, b))] + print a + for x in set(py) - set(rst): + print " ", x diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 00000000..1d76823d --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyqtgraph.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyqtgraph.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/doc/source/3dgraphics.rst b/doc/source/3dgraphics.rst new file mode 100644 index 00000000..effa288d --- /dev/null +++ b/doc/source/3dgraphics.rst @@ -0,0 +1,48 @@ +3D Graphics +=========== + +Pyqtgraph uses OpenGL to provide a 3D scenegraph system. This system is functional but still early in development. +Current capabilities include: + +* 3D view widget with zoom/rotate controls (mouse drag and wheel) +* Scenegraph allowing items to be added/removed from scene with per-item transformations and parent/child relationships. +* Triangular meshes +* Basic mesh computation functions: isosurfaces, per-vertex normals +* Volumetric rendering item +* Grid/axis items + +See the :doc:`API Reference ` and the Volumetric (GLVolumeItem.py) and Isosurface (GLMeshItem.py) examples for more information. + +Basic usage example:: + + ## build a QApplication before building other widgets + import pyqtgraph as pg + pg.mkQApp() + + ## make a widget for displaying 3D objects + import pyqtgraph.opengl as gl + view = gl.GLViewWidget() + view.show() + + ## create three grids, add each to the view + xgrid = gl.GLGridItem() + ygrid = gl.GLGridItem() + zgrid = gl.GLGridItem() + view.addItem(xgrid) + view.addItem(ygrid) + view.addItem(zgrid) + + ## rotate x and y grids to face the correct direction + xgrid.rotate(90, 0, 1, 0) + ygrid.rotate(90, 1, 0, 0) + + ## scale each grid differently + xgrid.scale(0.2, 0.1, 0.1) + ygrid.scale(0.2, 0.1, 0.1) + zgrid.scale(0.1, 0.2, 0.1) + + + + + + diff --git a/doc/source/3dgraphics/glaxisitem.rst b/doc/source/3dgraphics/glaxisitem.rst new file mode 100644 index 00000000..4f6d02d9 --- /dev/null +++ b/doc/source/3dgraphics/glaxisitem.rst @@ -0,0 +1,8 @@ +GLAxisItem +========== + +.. autoclass:: pyqtgraph.opengl.GLAxisItem + :members: + + .. automethod:: pyqtgraph.opengl.GLAxisItem.__init__ + diff --git a/doc/source/3dgraphics/glgraphicsitem.rst b/doc/source/3dgraphics/glgraphicsitem.rst new file mode 100644 index 00000000..4ff3d175 --- /dev/null +++ b/doc/source/3dgraphics/glgraphicsitem.rst @@ -0,0 +1,8 @@ +GLGraphicsItem +============== + +.. autoclass:: pyqtgraph.opengl.GLGraphicsItem + :members: + + .. automethod:: pyqtgraph.GLGraphicsItem.__init__ + diff --git a/doc/source/3dgraphics/glgriditem.rst b/doc/source/3dgraphics/glgriditem.rst new file mode 100644 index 00000000..11c185c5 --- /dev/null +++ b/doc/source/3dgraphics/glgriditem.rst @@ -0,0 +1,8 @@ +GLGridItem +========== + +.. autoclass:: pyqtgraph.opengl.GLGridItem + :members: + + .. automethod:: pyqtgraph.opengl.GLGridItem.__init__ + diff --git a/doc/source/3dgraphics/glimageitem.rst b/doc/source/3dgraphics/glimageitem.rst new file mode 100644 index 00000000..ca40ff41 --- /dev/null +++ b/doc/source/3dgraphics/glimageitem.rst @@ -0,0 +1,8 @@ +GLImageItem +=========== + +.. autoclass:: pyqtgraph.opengl.GLImageItem + :members: + + .. automethod:: pyqtgraph.opengl.GLImageItem.__init__ + diff --git a/doc/source/3dgraphics/glmeshitem.rst b/doc/source/3dgraphics/glmeshitem.rst new file mode 100644 index 00000000..4f23e12e --- /dev/null +++ b/doc/source/3dgraphics/glmeshitem.rst @@ -0,0 +1,8 @@ +GLMeshItem +========== + +.. autoclass:: pyqtgraph.opengl.GLMeshItem + :members: + + .. automethod:: pyqtgraph.opengl.GLMeshItem.__init__ + diff --git a/doc/source/3dgraphics/glscatterplotitem.rst b/doc/source/3dgraphics/glscatterplotitem.rst new file mode 100644 index 00000000..4fa337c6 --- /dev/null +++ b/doc/source/3dgraphics/glscatterplotitem.rst @@ -0,0 +1,8 @@ +GLScatterPlotItem +================= + +.. autoclass:: pyqtgraph.opengl.GLScatterPlotItem + :members: + + .. automethod:: pyqtgraph.opengl.GLScatterPlotItem.__init__ + diff --git a/doc/source/3dgraphics/glsurfaceplotitem.rst b/doc/source/3dgraphics/glsurfaceplotitem.rst new file mode 100644 index 00000000..b6f4881e --- /dev/null +++ b/doc/source/3dgraphics/glsurfaceplotitem.rst @@ -0,0 +1,8 @@ +GLSurfacePlotItem +================= + +.. autoclass:: pyqtgraph.opengl.GLSurfacePlotItem + :members: + + .. automethod:: pyqtgraph.opengl.GLSurfacePlotItem.__init__ + diff --git a/doc/source/3dgraphics/glviewwidget.rst b/doc/source/3dgraphics/glviewwidget.rst new file mode 100644 index 00000000..7ac39949 --- /dev/null +++ b/doc/source/3dgraphics/glviewwidget.rst @@ -0,0 +1,8 @@ +GLViewWidget +============ + +.. autoclass:: pyqtgraph.opengl.GLViewWidget + :members: + + .. automethod:: pyqtgraph.opengl.GLViewWidget.__init__ + diff --git a/doc/source/3dgraphics/glvolumeitem.rst b/doc/source/3dgraphics/glvolumeitem.rst new file mode 100644 index 00000000..951d78d2 --- /dev/null +++ b/doc/source/3dgraphics/glvolumeitem.rst @@ -0,0 +1,8 @@ +GLVolumeItem +============ + +.. autoclass:: pyqtgraph.opengl.GLVolumeItem + :members: + + .. automethod:: pyqtgraph.opengl.GLVolumeItem.__init__ + diff --git a/doc/source/3dgraphics/index.rst b/doc/source/3dgraphics/index.rst new file mode 100644 index 00000000..255f550b --- /dev/null +++ b/doc/source/3dgraphics/index.rst @@ -0,0 +1,27 @@ +Pyqtgraph's 3D Graphics System +============================== + +The 3D graphics system in pyqtgraph is composed of a :class:`view widget ` and +several graphics items (all subclasses of :class:`GLGraphicsItem `) which +can be added to a view widget. + +**Note:** use of this system requires python-opengl bindings. Linux users should install the python-opengl +packages from their distribution. Windows/OSX users can download from ``_. + +Contents: + +.. toctree:: + :maxdepth: 2 + + glviewwidget + + glgriditem + glsurfaceplotitem + glvolumeitem + glimageitem + glmeshitem + glaxisitem + glgraphicsitem + glscatterplotitem + meshdata + diff --git a/doc/source/3dgraphics/meshdata.rst b/doc/source/3dgraphics/meshdata.rst new file mode 100644 index 00000000..2c49c0bf --- /dev/null +++ b/doc/source/3dgraphics/meshdata.rst @@ -0,0 +1,8 @@ +MeshData +======== + +.. autoclass:: pyqtgraph.opengl.MeshData + :members: + + .. automethod:: pyqtgraph.opengl.MeshData.__init__ + diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst new file mode 100644 index 00000000..777e6ad4 --- /dev/null +++ b/doc/source/apireference.rst @@ -0,0 +1,14 @@ +API Reference +============= + +Contents: + +.. toctree:: + :maxdepth: 2 + + functions + graphicsItems/index + widgets/index + 3dgraphics/index + parametertree/index + graphicsscene/index diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..2fd718e4 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# +# pyqtgraph documentation build configuration file, created by +# sphinx-quickstart on Fri Nov 18 19:33:12 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# 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 +# documentation root, use os.path.abspath to make it absolute, like shown here. +path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(path, '..', '..', '..')) +print sys.path + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'pyqtgraph' +copyright = '2011, Luke Campagnola' + +# 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 = '1.8' +# The full version, including alpha/beta/rc tags. +release = '1.8' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyqtgraphdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pyqtgraph.tex', 'pyqtgraph Documentation', + 'Luke Campagnola', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pyqtgraph', 'pyqtgraph Documentation', + ['Luke Campagnola'], 1) +] diff --git a/doc/source/functions.rst b/doc/source/functions.rst new file mode 100644 index 00000000..65f2c202 --- /dev/null +++ b/doc/source/functions.rst @@ -0,0 +1,100 @@ +Pyqtgraph's Helper Functions +============================ + +Simple Data Display Functions +----------------------------- + +.. autofunction:: pyqtgraph.plot + +.. autofunction:: pyqtgraph.image + +.. autofunction:: pyqtgraph.dbg + +Color, Pen, and Brush Functions +------------------------------- + +Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: + + pg.plot(xdata, ydata, pen='r') + pg.plot(xdata, ydata, pen=pg.mkPen('r')) + pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0))) + + +.. autofunction:: pyqtgraph.mkColor + +.. autofunction:: pyqtgraph.mkPen + +.. autofunction:: pyqtgraph.mkBrush + +.. autofunction:: pyqtgraph.hsvColor + +.. autofunction:: pyqtgraph.intColor + +.. autofunction:: pyqtgraph.colorTuple + +.. autofunction:: pyqtgraph.colorStr + +.. autofunction:: pyqtgraph.glColor + + +Data Slicing +------------ + +.. autofunction:: pyqtgraph.affineSlice + + +Coordinate Transformation +------------------------- + +.. autofunction:: pyqtgraph.transformToArray + +.. autofunction:: pyqtgraph.transformCoordinates + +.. autofunction:: pyqtgraph.solve3DTransform + +.. autofunction:: pyqtgraph.solveBilinearTransform + + + +SI Unit Conversion Functions +---------------------------- + +.. autofunction:: pyqtgraph.siFormat + +.. autofunction:: pyqtgraph.siScale + +.. autofunction:: pyqtgraph.siEval + + +Image Preparation Functions +--------------------------- + +.. autofunction:: pyqtgraph.makeARGB + +.. autofunction:: pyqtgraph.makeQImage + +.. autofunction:: pyqtgraph.applyLookupTable + +.. autofunction:: pyqtgraph.rescaleData + +.. autofunction:: pyqtgraph.imageToArray + + +Mesh Generation Functions +------------------------- + +.. autofunction:: pyqtgraph.isocurve + +.. autofunction:: pyqtgraph.isosurface + + +Miscellaneous Functions +----------------------- + +.. autofunction:: pyqtgraph.pseudoScatter + +.. autofunction:: pyqtgraph.systemInfo + + + + diff --git a/doc/source/graphicsItems/arrowitem.rst b/doc/source/graphicsItems/arrowitem.rst new file mode 100644 index 00000000..250957a5 --- /dev/null +++ b/doc/source/graphicsItems/arrowitem.rst @@ -0,0 +1,8 @@ +ArrowItem +========= + +.. autoclass:: pyqtgraph.ArrowItem + :members: + + .. automethod:: pyqtgraph.ArrowItem.__init__ + diff --git a/doc/source/graphicsItems/axisitem.rst b/doc/source/graphicsItems/axisitem.rst new file mode 100644 index 00000000..8f76d130 --- /dev/null +++ b/doc/source/graphicsItems/axisitem.rst @@ -0,0 +1,8 @@ +AxisItem +======== + +.. autoclass:: pyqtgraph.AxisItem + :members: + + .. automethod:: pyqtgraph.AxisItem.__init__ + diff --git a/doc/source/graphicsItems/buttonitem.rst b/doc/source/graphicsItems/buttonitem.rst new file mode 100644 index 00000000..44469db6 --- /dev/null +++ b/doc/source/graphicsItems/buttonitem.rst @@ -0,0 +1,8 @@ +ButtonItem +========== + +.. autoclass:: pyqtgraph.ButtonItem + :members: + + .. automethod:: pyqtgraph.ButtonItem.__init__ + diff --git a/doc/source/graphicsItems/curvearrow.rst b/doc/source/graphicsItems/curvearrow.rst new file mode 100644 index 00000000..4c7f11ab --- /dev/null +++ b/doc/source/graphicsItems/curvearrow.rst @@ -0,0 +1,8 @@ +CurveArrow +========== + +.. autoclass:: pyqtgraph.CurveArrow + :members: + + .. automethod:: pyqtgraph.CurveArrow.__init__ + diff --git a/doc/source/graphicsItems/curvepoint.rst b/doc/source/graphicsItems/curvepoint.rst new file mode 100644 index 00000000..f19791f7 --- /dev/null +++ b/doc/source/graphicsItems/curvepoint.rst @@ -0,0 +1,8 @@ +CurvePoint +========== + +.. autoclass:: pyqtgraph.CurvePoint + :members: + + .. automethod:: pyqtgraph.CurvePoint.__init__ + diff --git a/doc/source/graphicsItems/fillbetweenitem.rst b/doc/source/graphicsItems/fillbetweenitem.rst new file mode 100644 index 00000000..680f29cc --- /dev/null +++ b/doc/source/graphicsItems/fillbetweenitem.rst @@ -0,0 +1,8 @@ +FillBetweenItem +=============== + +.. autoclass:: pyqtgraph.FillBetweenItem + :members: + + .. automethod:: pyqtgraph.FillBetweenItem.__init__ + diff --git a/doc/source/graphicsItems/gradienteditoritem.rst b/doc/source/graphicsItems/gradienteditoritem.rst new file mode 100644 index 00000000..cd277869 --- /dev/null +++ b/doc/source/graphicsItems/gradienteditoritem.rst @@ -0,0 +1,19 @@ +GradientEditorItem +================== + +.. autoclass:: pyqtgraph.GradientEditorItem + :members: + + .. automethod:: pyqtgraph.GradientEditorItem.__init__ + + +TickSliderItem +================== + +.. autoclass:: pyqtgraph.TickSliderItem + :members: + + .. automethod:: pyqtgraph.TickSliderItem.__init__ + + + diff --git a/doc/source/graphicsItems/gradientlegend.rst b/doc/source/graphicsItems/gradientlegend.rst new file mode 100644 index 00000000..f47031c0 --- /dev/null +++ b/doc/source/graphicsItems/gradientlegend.rst @@ -0,0 +1,8 @@ +GradientLegend +============== + +.. autoclass:: pyqtgraph.GradientLegend + :members: + + .. automethod:: pyqtgraph.GradientLegend.__init__ + diff --git a/doc/source/graphicsItems/graphicsitem.rst b/doc/source/graphicsItems/graphicsitem.rst new file mode 100644 index 00000000..b9573aea --- /dev/null +++ b/doc/source/graphicsItems/graphicsitem.rst @@ -0,0 +1,6 @@ +GraphicsItem +============ + +.. autoclass:: pyqtgraph.GraphicsItem + :members: + diff --git a/doc/source/graphicsItems/graphicslayout.rst b/doc/source/graphicsItems/graphicslayout.rst new file mode 100644 index 00000000..f45dfd87 --- /dev/null +++ b/doc/source/graphicsItems/graphicslayout.rst @@ -0,0 +1,8 @@ +GraphicsLayout +============== + +.. autoclass:: pyqtgraph.GraphicsLayout + :members: + + .. automethod:: pyqtgraph.GraphicsLayout.__init__ + diff --git a/doc/source/graphicsItems/graphicsobject.rst b/doc/source/graphicsItems/graphicsobject.rst new file mode 100644 index 00000000..736d941e --- /dev/null +++ b/doc/source/graphicsItems/graphicsobject.rst @@ -0,0 +1,8 @@ +GraphicsObject +============== + +.. autoclass:: pyqtgraph.GraphicsObject + :members: + + .. automethod:: pyqtgraph.GraphicsObject.__init__ + diff --git a/doc/source/graphicsItems/graphicswidget.rst b/doc/source/graphicsItems/graphicswidget.rst new file mode 100644 index 00000000..7cf23bbe --- /dev/null +++ b/doc/source/graphicsItems/graphicswidget.rst @@ -0,0 +1,8 @@ +GraphicsWidget +============== + +.. autoclass:: pyqtgraph.GraphicsWidget + :members: + + .. automethod:: pyqtgraph.GraphicsWidget.__init__ + diff --git a/doc/source/graphicsItems/graphicswidgetanchor.rst b/doc/source/graphicsItems/graphicswidgetanchor.rst new file mode 100644 index 00000000..e0f9d6a7 --- /dev/null +++ b/doc/source/graphicsItems/graphicswidgetanchor.rst @@ -0,0 +1,8 @@ +GraphicsWidgetAnchor +==================== + +.. autoclass:: pyqtgraph.GraphicsWidgetAnchor + :members: + + .. automethod:: pyqtgraph.GraphicsWidgetAnchor.__init__ + diff --git a/doc/source/graphicsItems/griditem.rst b/doc/source/graphicsItems/griditem.rst new file mode 100644 index 00000000..aa932766 --- /dev/null +++ b/doc/source/graphicsItems/griditem.rst @@ -0,0 +1,8 @@ +GridItem +======== + +.. autoclass:: pyqtgraph.GridItem + :members: + + .. automethod:: pyqtgraph.GridItem.__init__ + diff --git a/doc/source/graphicsItems/histogramlutitem.rst b/doc/source/graphicsItems/histogramlutitem.rst new file mode 100644 index 00000000..db0e18cb --- /dev/null +++ b/doc/source/graphicsItems/histogramlutitem.rst @@ -0,0 +1,8 @@ +HistogramLUTItem +================ + +.. autoclass:: pyqtgraph.HistogramLUTItem + :members: + + .. automethod:: pyqtgraph.HistogramLUTItem.__init__ + diff --git a/doc/source/graphicsItems/imageitem.rst b/doc/source/graphicsItems/imageitem.rst new file mode 100644 index 00000000..49a981dc --- /dev/null +++ b/doc/source/graphicsItems/imageitem.rst @@ -0,0 +1,8 @@ +ImageItem +========= + +.. autoclass:: pyqtgraph.ImageItem + :members: + + .. automethod:: pyqtgraph.ImageItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst new file mode 100644 index 00000000..70786d20 --- /dev/null +++ b/doc/source/graphicsItems/index.rst @@ -0,0 +1,43 @@ +Pyqtgraph's Graphics Items +========================== + +Since pyqtgraph relies on Qt's GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph's GraphicsItem classes can be used in any normal QGraphicsScene. + + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotdataitem + plotitem + imageitem + viewbox + linearregionitem + infiniteline + roi + graphicslayout + plotcurveitem + scatterplotitem + isocurveitem + axisitem + textitem + arrowitem + fillbetweenitem + curvepoint + curvearrow + griditem + scalebar + labelitem + vtickgroup + legenditem + gradienteditoritem + histogramlutitem + gradientlegend + buttonitem + graphicsobject + graphicswidget + graphicsitem + uigraphicsitem + graphicswidgetanchor + diff --git a/doc/source/graphicsItems/infiniteline.rst b/doc/source/graphicsItems/infiniteline.rst new file mode 100644 index 00000000..e95987bc --- /dev/null +++ b/doc/source/graphicsItems/infiniteline.rst @@ -0,0 +1,8 @@ +InfiniteLine +============ + +.. autoclass:: pyqtgraph.InfiniteLine + :members: + + .. automethod:: pyqtgraph.InfiniteLine.__init__ + diff --git a/doc/source/graphicsItems/isocurveitem.rst b/doc/source/graphicsItems/isocurveitem.rst new file mode 100644 index 00000000..01b8ee69 --- /dev/null +++ b/doc/source/graphicsItems/isocurveitem.rst @@ -0,0 +1,7 @@ +IsocurveItem +============ + +.. autoclass:: pyqtgraph.IsocurveItem + :members: + + .. automethod:: pyqtgraph.IsocurveItem.__init__ diff --git a/doc/source/graphicsItems/labelitem.rst b/doc/source/graphicsItems/labelitem.rst new file mode 100644 index 00000000..ca420d76 --- /dev/null +++ b/doc/source/graphicsItems/labelitem.rst @@ -0,0 +1,8 @@ +LabelItem +========= + +.. autoclass:: pyqtgraph.LabelItem + :members: + + .. automethod:: pyqtgraph.LabelItem.__init__ + diff --git a/doc/source/graphicsItems/legenditem.rst b/doc/source/graphicsItems/legenditem.rst new file mode 100644 index 00000000..e94b0995 --- /dev/null +++ b/doc/source/graphicsItems/legenditem.rst @@ -0,0 +1,8 @@ +LegendItem +========== + +.. autoclass:: pyqtgraph.LegendItem + :members: + + .. automethod:: pyqtgraph.LegendItem.__init__ + diff --git a/doc/source/graphicsItems/linearregionitem.rst b/doc/source/graphicsItems/linearregionitem.rst new file mode 100644 index 00000000..9bcb534c --- /dev/null +++ b/doc/source/graphicsItems/linearregionitem.rst @@ -0,0 +1,8 @@ +LinearRegionItem +================ + +.. autoclass:: pyqtgraph.LinearRegionItem + :members: + + .. automethod:: pyqtgraph.LinearRegionItem.__init__ + diff --git a/doc/source/graphicsItems/make b/doc/source/graphicsItems/make new file mode 100644 index 00000000..2a990405 --- /dev/null +++ b/doc/source/graphicsItems/make @@ -0,0 +1,37 @@ +files = """ArrowItem +AxisItem +ButtonItem +CurvePoint +GradientEditorItem +GradientLegend +GraphicsLayout +GraphicsObject +GraphicsWidget +GridItem +HistogramLUTItem +ImageItem +InfiniteLine +LabelItem +LinearRegionItem +PlotCurveItem +PlotDataItem +ROI +ScaleBar +ScatterPlotItem +UIGraphicsItem +ViewBox +VTickGroup""".split('\n') + +for f in files: + print f + fh = open(f.lower()+'.rst', 'w') + fh.write( +"""%s +%s + +.. autoclass:: pyqtgraph.%s + :members: + + .. automethod:: pyqtgraph.%s.__init__ + +""" % (f, '='*len(f), f, f)) diff --git a/doc/source/graphicsItems/plotcurveitem.rst b/doc/source/graphicsItems/plotcurveitem.rst new file mode 100644 index 00000000..f0b2171d --- /dev/null +++ b/doc/source/graphicsItems/plotcurveitem.rst @@ -0,0 +1,8 @@ +PlotCurveItem +============= + +.. autoclass:: pyqtgraph.PlotCurveItem + :members: + + .. automethod:: pyqtgraph.PlotCurveItem.__init__ + diff --git a/doc/source/graphicsItems/plotdataitem.rst b/doc/source/graphicsItems/plotdataitem.rst new file mode 100644 index 00000000..275084e9 --- /dev/null +++ b/doc/source/graphicsItems/plotdataitem.rst @@ -0,0 +1,8 @@ +PlotDataItem +============ + +.. autoclass:: pyqtgraph.PlotDataItem + :members: + + .. automethod:: pyqtgraph.PlotDataItem.__init__ + diff --git a/doc/source/graphicsItems/plotitem.rst b/doc/source/graphicsItems/plotitem.rst new file mode 100644 index 00000000..60cedf60 --- /dev/null +++ b/doc/source/graphicsItems/plotitem.rst @@ -0,0 +1,7 @@ +PlotItem +======== + +.. autoclass:: pyqtgraph.PlotItem() + :members: + + .. automethod:: pyqtgraph.PlotItem.__init__ diff --git a/doc/source/graphicsItems/roi.rst b/doc/source/graphicsItems/roi.rst new file mode 100644 index 00000000..22945ade --- /dev/null +++ b/doc/source/graphicsItems/roi.rst @@ -0,0 +1,8 @@ +ROI +=== + +.. autoclass:: pyqtgraph.ROI + :members: + + .. automethod:: pyqtgraph.ROI.__init__ + diff --git a/doc/source/graphicsItems/scalebar.rst b/doc/source/graphicsItems/scalebar.rst new file mode 100644 index 00000000..2ab33967 --- /dev/null +++ b/doc/source/graphicsItems/scalebar.rst @@ -0,0 +1,8 @@ +ScaleBar +======== + +.. autoclass:: pyqtgraph.ScaleBar + :members: + + .. automethod:: pyqtgraph.ScaleBar.__init__ + diff --git a/doc/source/graphicsItems/scatterplotitem.rst b/doc/source/graphicsItems/scatterplotitem.rst new file mode 100644 index 00000000..be2c874b --- /dev/null +++ b/doc/source/graphicsItems/scatterplotitem.rst @@ -0,0 +1,8 @@ +ScatterPlotItem +=============== + +.. autoclass:: pyqtgraph.ScatterPlotItem + :members: + + .. automethod:: pyqtgraph.ScatterPlotItem.__init__ + diff --git a/doc/source/graphicsItems/textitem.rst b/doc/source/graphicsItems/textitem.rst new file mode 100644 index 00000000..143e539d --- /dev/null +++ b/doc/source/graphicsItems/textitem.rst @@ -0,0 +1,8 @@ +TextItem +======== + +.. autoclass:: pyqtgraph.TextItem + :members: + + .. automethod:: pyqtgraph.TextItem.__init__ + diff --git a/doc/source/graphicsItems/uigraphicsitem.rst b/doc/source/graphicsItems/uigraphicsitem.rst new file mode 100644 index 00000000..4f0b9933 --- /dev/null +++ b/doc/source/graphicsItems/uigraphicsitem.rst @@ -0,0 +1,8 @@ +UIGraphicsItem +============== + +.. autoclass:: pyqtgraph.UIGraphicsItem + :members: + + .. automethod:: pyqtgraph.UIGraphicsItem.__init__ + diff --git a/doc/source/graphicsItems/viewbox.rst b/doc/source/graphicsItems/viewbox.rst new file mode 100644 index 00000000..3593d295 --- /dev/null +++ b/doc/source/graphicsItems/viewbox.rst @@ -0,0 +1,8 @@ +ViewBox +======= + +.. autoclass:: pyqtgraph.ViewBox + :members: + + .. automethod:: pyqtgraph.ViewBox.__init__ + diff --git a/doc/source/graphicsItems/vtickgroup.rst b/doc/source/graphicsItems/vtickgroup.rst new file mode 100644 index 00000000..342705de --- /dev/null +++ b/doc/source/graphicsItems/vtickgroup.rst @@ -0,0 +1,8 @@ +VTickGroup +========== + +.. autoclass:: pyqtgraph.VTickGroup + :members: + + .. automethod:: pyqtgraph.VTickGroup.__init__ + diff --git a/doc/source/graphicsscene/graphicsscene.rst b/doc/source/graphicsscene/graphicsscene.rst new file mode 100644 index 00000000..334a282b --- /dev/null +++ b/doc/source/graphicsscene/graphicsscene.rst @@ -0,0 +1,8 @@ +GraphicsScene +============= + +.. autoclass:: pyqtgraph.GraphicsScene + :members: + + .. automethod:: pyqtgraph.GraphicsScene.__init__ + diff --git a/doc/source/graphicsscene/hoverevent.rst b/doc/source/graphicsscene/hoverevent.rst new file mode 100644 index 00000000..46007f91 --- /dev/null +++ b/doc/source/graphicsscene/hoverevent.rst @@ -0,0 +1,5 @@ +HoverEvent +========== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.HoverEvent + :members: diff --git a/doc/source/graphicsscene/index.rst b/doc/source/graphicsscene/index.rst new file mode 100644 index 00000000..189bde6c --- /dev/null +++ b/doc/source/graphicsscene/index.rst @@ -0,0 +1,12 @@ +GraphicsScene and Mouse Events +============================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + graphicsscene + hoverevent + mouseclickevent + mousedragevent diff --git a/doc/source/graphicsscene/mouseclickevent.rst b/doc/source/graphicsscene/mouseclickevent.rst new file mode 100644 index 00000000..f0c94e16 --- /dev/null +++ b/doc/source/graphicsscene/mouseclickevent.rst @@ -0,0 +1,5 @@ +MouseClickEvent +=============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent + :members: diff --git a/doc/source/graphicsscene/mousedragevent.rst b/doc/source/graphicsscene/mousedragevent.rst new file mode 100644 index 00000000..05c3aa6c --- /dev/null +++ b/doc/source/graphicsscene/mousedragevent.rst @@ -0,0 +1,5 @@ +MouseDragEvent +============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseDragEvent + :members: diff --git a/doc/source/graphicswindow.rst b/doc/source/graphicswindow.rst new file mode 100644 index 00000000..3d5641c3 --- /dev/null +++ b/doc/source/graphicswindow.rst @@ -0,0 +1,8 @@ +Basic display widgets +===================== + + - GraphicsWindow + - GraphicsView + - GraphicsLayoutItem + - ViewBox + diff --git a/doc/source/how_to_use.rst b/doc/source/how_to_use.rst new file mode 100644 index 00000000..53a3d2b0 --- /dev/null +++ b/doc/source/how_to_use.rst @@ -0,0 +1,73 @@ +How to use pyqtgraph +==================== + +There are a few suggested ways to use pyqtgraph: + +* From the interactive shell (python -i, ipython, etc) +* Displaying pop-up windows from an application +* Embedding widgets in a PyQt application + + + +Command-line use +---------------- + +Pyqtgraph makes it very easy to visualize data from the command line. Observe:: + + import pyqtgraph as pg + pg.plot(data) # data can be a list of values or a numpy array + +The example above would open a window displaying a line plot of the data given. The call to :func:`pg.plot ` returns a handle to the :class:`plot widget ` that is created, allowing more data to be added to the same window. + +Further examples:: + + pw = pg.plot(xVals, yVals, pen='r') # plot x vs y in red + pw.plot(xVals, yVals2, pen='b') + + win = pg.GraphicsWindow() # Automatically generates grids with multiple items + win.addPlot(data1, row=0, col=0) + win.addPlot(data2, row=0, col=1) + win.addPlot(data3, row=1, col=0, colspan=2) + + pg.show(imageData) # imageData must be a numpy array with 2 to 4 dimensions + +We're only scratching the surface here--these functions accept many different data formats and options for customizing the appearance of your data. + + +Displaying windows from within an application +--------------------------------------------- + +While I consider this approach somewhat lazy, it is often the case that 'lazy' is indistinguishable from 'highly efficient'. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it. + + +Embedding widgets inside PyQt applications +------------------------------------------ + +For the serious application developer, all of the functionality in pyqtgraph is available via :ref:`widgets ` that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality: + +#. In Designer, create a QGraphicsView widget ("Graphics View" under the "Display Widgets" category). +#. Right-click on the QGraphicsView and select "Promote To...". +#. Under "Promoted class name", enter the class name you wish to use ("PlotWidget", "GraphicsLayoutWidget", etc). +#. Under "Header file", enter "pyqtgraph". +#. Click "Add", then click "Promote". + +See the designer documentation for more information on promoting widgets. + + +PyQt and PySide +--------------- + +Pyqtgraph supports two popular python wrappers for the Qt library: PyQt and PySide. Both packages provide nearly identical +APIs and functionality, but for various reasons (discussed elsewhere) you may prefer to use one package or the other. When +pyqtgraph is first imported, it automatically determines which library to use by making the fillowing checks: + +#. If PyQt4 is already imported, use that +#. Else, if PySide is already imported, use that +#. Else, attempt to import PyQt4 +#. If that import fails, attempt to import PySide. + +If you have both libraries installed on your system and you wish to force pyqtgraph to use one or the other, simply +make sure it is imported before pyqtgraph:: + + import PySide ## this will force pyqtgraph to use PySide instead of PyQt4 + import pyqtgraph as pg diff --git a/doc/source/images.rst b/doc/source/images.rst new file mode 100644 index 00000000..00d45650 --- /dev/null +++ b/doc/source/images.rst @@ -0,0 +1,26 @@ +Displaying images and video +=========================== + +Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). + +The easiest way to display 2D or 3D data is using the :func:`pyqtgraph.image` function:: + + import pyqtgraph as pg + pg.image(imageData) + +This function will accept any floating-point or integer data types and displays a single :class:`~pyqtgraph.ImageView` widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional): + +1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram) +2. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor) + +If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them) + +There are a few other methods for displaying images as well: + +* The :class:`~pyqtgraph.ImageView` class can also be instantiated directly and embedded in Qt applications. +* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a :class:`ViewBox ` or :class:`GraphicsView `. +* For higher performance, use :class:`~pyqtgraph.RawImageWidget`. + +Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead. + +For more information, see the classes listed above and the 'VideoSpeedTest', 'ImageItem', 'ImageView', and 'HistogramLUT' :ref:`examples`. \ No newline at end of file diff --git a/doc/source/images/plottingClasses.png b/doc/source/images/plottingClasses.png new file mode 100644 index 00000000..7c8325a5 Binary files /dev/null and b/doc/source/images/plottingClasses.png differ diff --git a/doc/source/images/plottingClasses.svg b/doc/source/images/plottingClasses.svg new file mode 100644 index 00000000..393d16d7 --- /dev/null +++ b/doc/source/images/plottingClasses.svg @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + PlotWidget(GraphicsView) + + + + PlotItem(GraphicsItem) + + + + ViewBox(GraphicsItem) + + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + Title - LabelItem(GraphicsItem) + + PlotDataItem(GraphicsItem) + + + + + + GraphicsLayoutWidget(GraphicsView) + + + + GraphicsLayoutItem(GraphicsItem) + + + + PlotItem + + + + + + + + + + + + + + + + + + + + + + + + + ViewBox + + + + + PlotItem + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..5d606061 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,35 @@ +.. pyqtgraph documentation master file, created by + sphinx-quickstart on Fri Nov 18 19:33:12 2011. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to the documentation for pyqtgraph 1.8 +============================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + introduction + mouse_interaction + how_to_use + installation + plotting + images + 3dgraphics + style + region_of_interest + prototyping + parametertree/index + internals + apireference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 00000000..2d48fead --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,9 @@ +Installation +============ + +Pyqtgraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: + +* **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. +* **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) +* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. +* **Everybody (including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph web page, extract its contents, and run "python setup.py install" from within the extracted directory. diff --git a/doc/source/internals.rst b/doc/source/internals.rst new file mode 100644 index 00000000..8c1d246e --- /dev/null +++ b/doc/source/internals.rst @@ -0,0 +1,9 @@ +Internals - Extensions to Qt's GraphicsView +=========================================== + +* GraphicsView +* GraphicsScene (mouse events) +* GraphicsObject +* GraphicsWidget +* ViewBox + diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst new file mode 100644 index 00000000..44a498bc --- /dev/null +++ b/doc/source/introduction.rst @@ -0,0 +1,51 @@ +Introduction +============ + + + +What is pyqtgraph? +------------------ + +Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). + +Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. + +It is known to run on Linux, Windows, and OSX + + +What can it do? +--------------- + +Amongst the core features of pyqtgraph are: + +* Basic data visualization primitives: Images, line and scatter plots +* Fast enough for realtime update of video/plot data +* Interactive scaling/panning, averaging, FFTs, SVG/PNG export +* Widgets for marking/selecting plot regions +* Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data +* Framework for building customized image region-of-interest widgets +* Docking system that replaces/complements Qt's dock system to allow more complex (and more predictable) docking arrangements +* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications) + + +.. _examples: + +Examples +-------- + +Pyqtgraph includes an extensive set of examples that can be accessed by running:: + + import pyqtgraph.examples + pyqtgraph.examples.run() + +This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example. + + +How does it compare to... +------------------------- + +* matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as matplotlib, but runs much faster. Matplotlib is more aimed toward making publication-quality graphics, whereas pyqtgraph is intended for use in data acquisition and analysis applications. Matplotlib is more intuitive for matlab programmers; pyqtgraph is more intuitive for python/qt programmers. Matplotlib (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. + +* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting functionality. Image handling in pyqtgraph is much more complete (again, no ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is more portable than pyqwt, which often lags behind pyqt in development (I originally used pyqwt, but decided it was too much trouble to rely on it as a dependency in my projects). Like matplotlib, pyqwt (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. + +(My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst new file mode 100644 index 00000000..0e149f0c --- /dev/null +++ b/doc/source/mouse_interaction.rst @@ -0,0 +1,49 @@ +Mouse Interaction +================= + +Most applications that use pyqtgraph's data visualization will generate widgets that can be interactively scaled, panned, and otherwise configured using the mouse. This section describes mouse interaction with these widgets. + + +2D Graphics +----------- + +In pyqtgraph, most 2D visualizations follow the following mouse interaction: + +* Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. +* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. +* Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). +* Wheel spin: Zooms the scene in and out. + +For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling:: + + pyqtgraph.setConfigOption('leftButtonPan', False) + + +Context Menu +------------ + +Right-clicking on most scenes will show a context menu with various options for changing the behavior of the scene. Some of the options available in this menu are: + +* Enable/disable automatic scaling when the data range changes +* Link the axes of multiple views together +* Enable disable mouse interaction per axis +* Explicitly set the visible range values + +The exact set of items available in the menu depends on the contents of the scene and the object clicked on. + + +3D Graphics +----------- + +3D visualizations use the following mouse interaction: + +* Left button drag: Rotates the scene around a central point +* Middle button drag: Pan the scene by moving the central "look-at" point within the x-y plane +* Middle button drag + CTRL: Pan the scene by moving the central "look-at" point along the z axis +* Wheel spin: zoom in/out +* Wheel + CTRL: change field-of-view angle + +And keyboard controls: + +* Arrow keys rotate around central point, just like dragging the left mouse button diff --git a/doc/source/parametertree/apiref.rst b/doc/source/parametertree/apiref.rst new file mode 100644 index 00000000..876253e0 --- /dev/null +++ b/doc/source/parametertree/apiref.rst @@ -0,0 +1,15 @@ +ParameterTree API Reference +=========================== + +Also see the 'parametertree' example included with pyqtgraph + +Contents: + +.. toctree:: + :maxdepth: 2 + + parameter + parametertree + parametertypes + parameteritem + diff --git a/doc/source/parametertree/index.rst b/doc/source/parametertree/index.rst new file mode 100644 index 00000000..ee33b19c --- /dev/null +++ b/doc/source/parametertree/index.rst @@ -0,0 +1,21 @@ +.. _parametertree: + +Parameter Trees +=============== + +Parameter trees are a system for handling hierarchies of parameters while automatically generating one or more GUIs to display and interact with the parameters. +This feature is commonly seen, for example, in user interface design applications which display a list of editable properties for each widget. +Parameters generally have a name, a data type (int, float, string, color, etc), and a value matching the data type. Parameters may be grouped and nested +to form hierarchies and may be subclassed to provide custom behavior and display widgets. + +Pyqtgraph's parameter tree system works similarly to the model-view architecture used by some components of Qt: Parameters are purely data-handling classes +that exist independent of any graphical interface. A ParameterTree is a widget that automatically generates a graphical interface which represents +the state of a haierarchy of Parameter objects and allows the user to edit the values within that hierarchy. This separation of data (model) and graphical +interface (view) allows the same data to be represented multiple times and in a variety of different ways. + +For more information, see the 'parametertree' example included with pyqtgraph and the API reference + +.. toctree:: + :maxdepth: 2 + + apiref diff --git a/doc/source/parametertree/parameter.rst b/doc/source/parametertree/parameter.rst new file mode 100644 index 00000000..b5326b91 --- /dev/null +++ b/doc/source/parametertree/parameter.rst @@ -0,0 +1,8 @@ +Parameter +========= + +.. autoclass:: pyqtgraph.parametertree.Parameter + :members: + + .. automethod:: pyqtgraph.parametertree.Parameter.__init__ + diff --git a/doc/source/parametertree/parameteritem.rst b/doc/source/parametertree/parameteritem.rst new file mode 100644 index 00000000..27f18a8a --- /dev/null +++ b/doc/source/parametertree/parameteritem.rst @@ -0,0 +1,8 @@ +ParameterItem +============= + +.. autoclass:: pyqtgraph.parametertree.ParameterItem + :members: + + .. automethod:: pyqtgraph.parametertree.ParameterItem.__init__ + diff --git a/doc/source/parametertree/parametertree.rst b/doc/source/parametertree/parametertree.rst new file mode 100644 index 00000000..b66bfa3b --- /dev/null +++ b/doc/source/parametertree/parametertree.rst @@ -0,0 +1,8 @@ +ParameterTree +============= + +.. autoclass:: pyqtgraph.parametertree.ParameterTree + :members: + + .. automethod:: pyqtgraph.parametertree.ParameterTree.__init__ + diff --git a/doc/source/parametertree/parametertypes.rst b/doc/source/parametertree/parametertypes.rst new file mode 100644 index 00000000..4344cee3 --- /dev/null +++ b/doc/source/parametertree/parametertypes.rst @@ -0,0 +1,6 @@ +Built-in Parameter Types +======================== + +.. automodule:: pyqtgraph.parametertree.parameterTypes + :members: + diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst new file mode 100644 index 00000000..ee9ed6dc --- /dev/null +++ b/doc/source/plotting.rst @@ -0,0 +1,73 @@ +Plotting in pyqtgraph +===================== + +There are a few basic ways to plot data in pyqtgraph: + +================================================================ ================================================== +:func:`pyqtgraph.plot` Create a new plot window showing your data +:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget +:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget +:func:`GraphicsWindow.addPlot() ` Add a new plot to a grid of plots +================================================================ ================================================== + +All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: + +* x - Optional X data; if not specified, then a range of integers will be generated automatically. +* y - Y data. +* pen - The pen to use when drawing plot lines, or None to disable lines. +* symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point. +* symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline. +* symbolBrush - The brush (or sequence of brushes) to use when filling the symbol. +* fillLevel - Fills the area under the plot curve to this Y-value. +* brush - The brush to use when filling under the curve. + +See the 'plotting' :ref:`example ` for a demonstration of these arguments. + +All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified. + +Organization of Plotting Classes +-------------------------------- + +There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. + +* Data Classes (all subclasses of QGraphicsItem) + * PlotCurveItem - Displays a plot line given x,y data + * ScatterPlotItem - Displays points given x,y data + * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. +* Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView) + * PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView + * GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. + * ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. + * AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem. +* Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) + * PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. + * GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + +.. image:: images/plottingClasses.png + + +Examples +-------- + +See the 'plotting' and 'PlotWidget' :ref:`examples included with pyqtgraph ` for more information. + +Show x,y data as scatter plot:: + + import pyqtgraph as pg + import numpy as np + x = np.random.normal(size=1000) + y = np.random.normal(size=1000) + pg.plot(x, y, pen=None, symbol='o') ## setting pen=None disables line drawing + +Create/show a plot widget, display three data curves:: + + import pyqtgraph as pg + import numpy as np + x = np.arange(1000) + y = np.random.normal(size=(3, 1000)) + plotWidget = pg.plot(title="Three plot curves") + for i in range(3): + plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens + + + diff --git a/doc/source/prototyping.rst b/doc/source/prototyping.rst new file mode 100644 index 00000000..63815a08 --- /dev/null +++ b/doc/source/prototyping.rst @@ -0,0 +1,41 @@ +Rapid GUI prototyping +===================== + +[Just an overview; documentation is not complete yet] + +Pyqtgraph offers several powerful features which are commonly used in engineering and scientific applications. + +Parameter Trees +--------------- + +The parameter tree system provides a widget displaying a tree of modifiable values similar to those used in most GUI editor applications. This allows a large number of variables to be controlled by the user with relatively little programming effort. The system also provides separation between the data being controlled and the user interface controlling it (model/view architecture). Parameters may be grouped/nested to any depth and custom parameter types can be built by subclassing from Parameter and ParameterItem. + +See the `parametertree documentation `_ for more information. + + +Visual Programming Flowcharts +----------------------------- + +Pyqtgraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a Python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. + +One major limitation of flowcharts is that there is no mechanism for looping within a flowchart. (however individual Nodes may contain loops (they may contain any Python code at all), and an entire flowchart may be executed from within a loop). + +There are two distinct modes of executing the code in a flowchart: + +1. Provide data to the input terminals of the flowchart. This method is slower and will provide a graphical representation of the data as it passes through the flowchart. This is useful for debugging as it allows the user to inspect the data at each terminal and see where exceptions occurred within the flowchart. +2. Call Flowchart.process. This method does not update the displayed state of the flowchart and only retains the state of each terminal as long as it is needed. Additionally, Nodes which do not contribute to the output values of the flowchart (such as plotting nodes) are ignored. This mode allows for faster processing of large data sets and avoids memory issues which can occur if doo much data is present in the flowchart at once (e.g., when processing image data through several stages). + +See the flowchart example for more information. + +Graphical Canvas +---------------- + +The Canvas is a system designed to allow the user to add/remove items to a 2D canvas similar to most vector graphics applications. Items can be translated/scaled/rotated and each item may define its own custom control interface. + + +Dockable Widgets +---------------- + +The dockarea system allows the design of user interfaces which can be rearranged by the user at runtime. Docks can be moved, resized, stacked, and torn out of the main window. This is similar in principle to the docking system built into Qt, but offers a more deterministic dock placement API (in Qt it is very difficult to programatically generate complex dock arrangements). Additionally, Qt's docks are designed to be used as small panels around the outer edge of a window. Pyqtgraph's docks were created with the notion that the entire window (or any portion of it) would consist of dockable components. + + diff --git a/doc/source/region_of_interest.rst b/doc/source/region_of_interest.rst new file mode 100644 index 00000000..eda9cacc --- /dev/null +++ b/doc/source/region_of_interest.rst @@ -0,0 +1,23 @@ +Interactive Data Selection Controls +=================================== + +Pyqtgraph includes graphics items which allow the user to select and mark regions of data. + +Linear Selection and Marking +---------------------------- + +Two classes allow marking and selecting 1-dimensional data: :class:`LinearRegionItem ` and :class:`InfiniteLine `. The first class, :class:`LinearRegionItem `, may be added to any ViewBox or PlotItem to mark either a horizontal or vertical region. The region can be dragged and its bounding edges can be moved independently. The second class, :class:`InfiniteLine `, is usually used to mark a specific position along the x or y axis. These may be dragged by the user. + + +2D Selection and Marking +------------------------ + +To select a 2D region from an image, pyqtgraph uses the :class:`ROI ` class or any of its subclasses. By default, :class:`ROI ` simply displays a rectangle which can be moved by the user to mark a specific region (most often this will be a region of an image, but this is not required). To allow the ROI to be resized or rotated, there are several methods for adding handles (:func:`addScaleHandle `, :func:`addRotateHandle `, etc.) which can be dragged by the user. These handles may be placed at any location relative to the ROI and may scale/rotate the ROI around any arbitrary center point. There are several ROI subclasses with a variety of shapes and modes of interaction. + +To automatically extract a region of image data using an ROI and an ImageItem, use :func:`ROI.getArrayRegion `. ROI classes use the :func:`affineSlice ` function to perform this extraction. + +ROI can also be used as a control for moving/rotating/scaling items in a scene similar to most vetctor graphics editing applications. + +See the ROITypes example for more information. + + diff --git a/doc/source/style.rst b/doc/source/style.rst new file mode 100644 index 00000000..593e6a32 --- /dev/null +++ b/doc/source/style.rst @@ -0,0 +1,47 @@ +Line, Fill, and Color +===================== + +Qt relies on its QColor, QPen and QBrush classes for specifying line and fill styles for all of its drawing. +Internally, pyqtgraph uses the same system but also allows many shorthand methods of specifying +the same style options. + +Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. +For most of these function arguments, the following values may be used: + +* single-character string representing color (b, g, r, c, m, y, k, w) +* (r, g, b) or (r, g, b, a) tuple +* single greyscale value (0.0 - 1.0) +* (index, maximum) tuple for automatically iterating through colors (see :func:`intColor `) +* QColor +* QPen / QBrush where appropriate + +Notably, more complex pens and brushes can be easily built using the +:func:`mkPen() ` / :func:`mkBrush() ` functions or with Qt's QPen and QBrush classes:: + + mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide + mkPen(0.5) ## solid grey line 1px wide + mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line + +See the Qt documentation for 'QPen' and 'PenStyle' for more line-style options and 'QBrush' for more fill options. +Colors can also be built using :func:`mkColor() `, +:func:`intColor() `, :func:`hsvColor() `, or Qt's QColor class. + + +Default Background and Foreground Colors +---------------------------------------- + +By default, pyqtgraph uses a black background for its plots and grey for axes, text, and plot lines. +These defaults can be changed using pyqtgraph.setConfigOption():: + + import pyqtgraph as pg + + ## Switch to using white background and black foreground + pg.setConfigOption('background', 'w') + pg.setConfigOption('foreground', 'k') + + ## The following plot has inverted colors + pg.plot([1,4,2,3,5]) + +(Note that this must be set *before* creating any widgets) + + diff --git a/doc/source/widgets/busycursor.rst b/doc/source/widgets/busycursor.rst new file mode 100644 index 00000000..32e1977d --- /dev/null +++ b/doc/source/widgets/busycursor.rst @@ -0,0 +1,8 @@ +BusyCursor +========== + +.. autoclass:: pyqtgraph.BusyCursor + :members: + + .. automethod:: pyqtgraph.BusyCursor.__init__ + diff --git a/doc/source/widgets/checktable.rst b/doc/source/widgets/checktable.rst new file mode 100644 index 00000000..5301a4e9 --- /dev/null +++ b/doc/source/widgets/checktable.rst @@ -0,0 +1,8 @@ +CheckTable +========== + +.. autoclass:: pyqtgraph.CheckTable + :members: + + .. automethod:: pyqtgraph.CheckTable.__init__ + diff --git a/doc/source/widgets/colorbutton.rst b/doc/source/widgets/colorbutton.rst new file mode 100644 index 00000000..690239d8 --- /dev/null +++ b/doc/source/widgets/colorbutton.rst @@ -0,0 +1,8 @@ +ColorButton +=========== + +.. autoclass:: pyqtgraph.ColorButton + :members: + + .. automethod:: pyqtgraph.ColorButton.__init__ + diff --git a/doc/source/widgets/combobox.rst b/doc/source/widgets/combobox.rst new file mode 100644 index 00000000..9fbfd828 --- /dev/null +++ b/doc/source/widgets/combobox.rst @@ -0,0 +1,8 @@ +ComboBox +======== + +.. autoclass:: pyqtgraph.ComboBox + :members: + + .. automethod:: pyqtgraph.ComboBox.__init__ + diff --git a/doc/source/widgets/datatreewidget.rst b/doc/source/widgets/datatreewidget.rst new file mode 100644 index 00000000..f6bbdbaf --- /dev/null +++ b/doc/source/widgets/datatreewidget.rst @@ -0,0 +1,8 @@ +DataTreeWidget +============== + +.. autoclass:: pyqtgraph.DataTreeWidget + :members: + + .. automethod:: pyqtgraph.DataTreeWidget.__init__ + diff --git a/doc/source/widgets/dockarea.rst b/doc/source/widgets/dockarea.rst new file mode 100644 index 00000000..09a6acca --- /dev/null +++ b/doc/source/widgets/dockarea.rst @@ -0,0 +1,5 @@ +dockarea module +=============== + +.. automodule:: pyqtgraph.dockarea + :members: diff --git a/doc/source/widgets/feedbackbutton.rst b/doc/source/widgets/feedbackbutton.rst new file mode 100644 index 00000000..67102603 --- /dev/null +++ b/doc/source/widgets/feedbackbutton.rst @@ -0,0 +1,8 @@ +FeedbackButton +============== + +.. autoclass:: pyqtgraph.FeedbackButton + :members: + + .. automethod:: pyqtgraph.FeedbackButton.__init__ + diff --git a/doc/source/widgets/filedialog.rst b/doc/source/widgets/filedialog.rst new file mode 100644 index 00000000..bf2f9c07 --- /dev/null +++ b/doc/source/widgets/filedialog.rst @@ -0,0 +1,8 @@ +FileDialog +========== + +.. autoclass:: pyqtgraph.FileDialog + :members: + + .. automethod:: pyqtgraph.FileDialog.__init__ + diff --git a/doc/source/widgets/gradientwidget.rst b/doc/source/widgets/gradientwidget.rst new file mode 100644 index 00000000..a2587503 --- /dev/null +++ b/doc/source/widgets/gradientwidget.rst @@ -0,0 +1,8 @@ +GradientWidget +============== + +.. autoclass:: pyqtgraph.GradientWidget + :members: + + .. automethod:: pyqtgraph.GradientWidget.__init__ + diff --git a/doc/source/widgets/graphicslayoutwidget.rst b/doc/source/widgets/graphicslayoutwidget.rst new file mode 100644 index 00000000..5f885f07 --- /dev/null +++ b/doc/source/widgets/graphicslayoutwidget.rst @@ -0,0 +1,8 @@ +GraphicsLayoutWidget +==================== + +.. autoclass:: pyqtgraph.GraphicsLayoutWidget + :members: + + .. automethod:: pyqtgraph.GraphicsLayoutWidget.__init__ + diff --git a/doc/source/widgets/graphicsview.rst b/doc/source/widgets/graphicsview.rst new file mode 100644 index 00000000..ac7ae3bf --- /dev/null +++ b/doc/source/widgets/graphicsview.rst @@ -0,0 +1,8 @@ +GraphicsView +============ + +.. autoclass:: pyqtgraph.GraphicsView + :members: + + .. automethod:: pyqtgraph.GraphicsView.__init__ + diff --git a/doc/source/widgets/histogramlutwidget.rst b/doc/source/widgets/histogramlutwidget.rst new file mode 100644 index 00000000..9d8f3b20 --- /dev/null +++ b/doc/source/widgets/histogramlutwidget.rst @@ -0,0 +1,8 @@ +HistogramLUTWidget +================== + +.. autoclass:: pyqtgraph.HistogramLUTWidget + :members: + + .. automethod:: pyqtgraph.HistogramLUTWidget.__init__ + diff --git a/doc/source/widgets/imageview.rst b/doc/source/widgets/imageview.rst new file mode 100644 index 00000000..1eadabbf --- /dev/null +++ b/doc/source/widgets/imageview.rst @@ -0,0 +1,8 @@ +ImageView +========= + +.. autoclass:: pyqtgraph.ImageView + :members: + + .. automethod:: pyqtgraph.ImageView.__init__ + diff --git a/doc/source/widgets/index.rst b/doc/source/widgets/index.rst new file mode 100644 index 00000000..913557b7 --- /dev/null +++ b/doc/source/widgets/index.rst @@ -0,0 +1,40 @@ +.. _api_widgets: + +Pyqtgraph's Widgets +=================== + +Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications. + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotwidget + imageview + dockarea + spinbox + gradientwidget + histogramlutwidget + parametertree + graphicsview + rawimagewidget + datatreewidget + tablewidget + treewidget + checktable + colorbutton + graphicslayoutwidget + progressdialog + filedialog + joystickbutton + multiplotwidget + verticallabel + remotegraphicsview + matplotlibwidget + feedbackbutton + combobox + layoutwidget + pathbutton + valuelabel + busycursor diff --git a/doc/source/widgets/joystickbutton.rst b/doc/source/widgets/joystickbutton.rst new file mode 100644 index 00000000..4d21e16f --- /dev/null +++ b/doc/source/widgets/joystickbutton.rst @@ -0,0 +1,8 @@ +JoystickButton +============== + +.. autoclass:: pyqtgraph.JoystickButton + :members: + + .. automethod:: pyqtgraph.JoystickButton.__init__ + diff --git a/doc/source/widgets/layoutwidget.rst b/doc/source/widgets/layoutwidget.rst new file mode 100644 index 00000000..98090a6a --- /dev/null +++ b/doc/source/widgets/layoutwidget.rst @@ -0,0 +1,8 @@ +LayoutWidget +============ + +.. autoclass:: pyqtgraph.LayoutWidget + :members: + + .. automethod:: pyqtgraph.LayoutWidget.__init__ + diff --git a/doc/source/widgets/make b/doc/source/widgets/make new file mode 100644 index 00000000..40d0e126 --- /dev/null +++ b/doc/source/widgets/make @@ -0,0 +1,31 @@ +files = """CheckTable +ColorButton +DataTreeWidget +FileDialog +GradientWidget +GraphicsLayoutWidget +GraphicsView +HistogramLUTWidget +JoystickButton +MultiPlotWidget +PlotWidget +ProgressDialog +RawImageWidget +SpinBox +TableWidget +TreeWidget +VerticalLabel""".split('\n') + +for f in files: + print f + fh = open(f.lower()+'.rst', 'w') + fh.write( +"""%s +%s + +.. autoclass:: pyqtgraph.%s + :members: + + .. automethod:: pyqtgraph.%s.__init__ + +""" % (f, '='*len(f), f, f)) diff --git a/doc/source/widgets/matplotlibwidget.rst b/doc/source/widgets/matplotlibwidget.rst new file mode 100644 index 00000000..1823495d --- /dev/null +++ b/doc/source/widgets/matplotlibwidget.rst @@ -0,0 +1,8 @@ +MatplotlibWidget +================ + +.. autoclass:: pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget + :members: + + .. automethod:: pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget.__init__ + diff --git a/doc/source/widgets/multiplotwidget.rst b/doc/source/widgets/multiplotwidget.rst new file mode 100644 index 00000000..46986db0 --- /dev/null +++ b/doc/source/widgets/multiplotwidget.rst @@ -0,0 +1,8 @@ +MultiPlotWidget +=============== + +.. autoclass:: pyqtgraph.MultiPlotWidget + :members: + + .. automethod:: pyqtgraph.MultiPlotWidget.__init__ + diff --git a/doc/source/widgets/parametertree.rst b/doc/source/widgets/parametertree.rst new file mode 100644 index 00000000..565b930b --- /dev/null +++ b/doc/source/widgets/parametertree.rst @@ -0,0 +1,5 @@ +parametertree module +==================== + +.. automodule:: pyqtgraph.parametertree + :members: diff --git a/doc/source/widgets/pathbutton.rst b/doc/source/widgets/pathbutton.rst new file mode 100644 index 00000000..13298f47 --- /dev/null +++ b/doc/source/widgets/pathbutton.rst @@ -0,0 +1,8 @@ +PathButton +========== + +.. autoclass:: pyqtgraph.PathButton + :members: + + .. automethod:: pyqtgraph.PathButton.__init__ + diff --git a/doc/source/widgets/plotwidget.rst b/doc/source/widgets/plotwidget.rst new file mode 100644 index 00000000..cbded80d --- /dev/null +++ b/doc/source/widgets/plotwidget.rst @@ -0,0 +1,8 @@ +PlotWidget +========== + +.. autoclass:: pyqtgraph.PlotWidget + :members: + + .. automethod:: pyqtgraph.PlotWidget.__init__ + diff --git a/doc/source/widgets/progressdialog.rst b/doc/source/widgets/progressdialog.rst new file mode 100644 index 00000000..fff04cb3 --- /dev/null +++ b/doc/source/widgets/progressdialog.rst @@ -0,0 +1,8 @@ +ProgressDialog +============== + +.. autoclass:: pyqtgraph.ProgressDialog + :members: + + .. automethod:: pyqtgraph.ProgressDialog.__init__ + diff --git a/doc/source/widgets/rawimagewidget.rst b/doc/source/widgets/rawimagewidget.rst new file mode 100644 index 00000000..29fda791 --- /dev/null +++ b/doc/source/widgets/rawimagewidget.rst @@ -0,0 +1,8 @@ +RawImageWidget +============== + +.. autoclass:: pyqtgraph.RawImageWidget + :members: + + .. automethod:: pyqtgraph.RawImageWidget.__init__ + diff --git a/doc/source/widgets/remotegraphicsview.rst b/doc/source/widgets/remotegraphicsview.rst new file mode 100644 index 00000000..c6c48c25 --- /dev/null +++ b/doc/source/widgets/remotegraphicsview.rst @@ -0,0 +1,8 @@ +RemoteGraphicsView +================== + +.. autoclass:: pyqtgraph.widgets.RemoteGraphicsView.RemoteGraphicsView + :members: + + .. automethod:: pyqtgraph.widgets.RemoteGraphicsView.RemoteGraphicsView.__init__ + diff --git a/doc/source/widgets/spinbox.rst b/doc/source/widgets/spinbox.rst new file mode 100644 index 00000000..33da1f4c --- /dev/null +++ b/doc/source/widgets/spinbox.rst @@ -0,0 +1,8 @@ +SpinBox +======= + +.. autoclass:: pyqtgraph.SpinBox + :members: + + .. automethod:: pyqtgraph.SpinBox.__init__ + diff --git a/doc/source/widgets/tablewidget.rst b/doc/source/widgets/tablewidget.rst new file mode 100644 index 00000000..283b540b --- /dev/null +++ b/doc/source/widgets/tablewidget.rst @@ -0,0 +1,8 @@ +TableWidget +=========== + +.. autoclass:: pyqtgraph.TableWidget + :members: + + .. automethod:: pyqtgraph.TableWidget.__init__ + diff --git a/doc/source/widgets/treewidget.rst b/doc/source/widgets/treewidget.rst new file mode 100644 index 00000000..00f9fa28 --- /dev/null +++ b/doc/source/widgets/treewidget.rst @@ -0,0 +1,8 @@ +TreeWidget +========== + +.. autoclass:: pyqtgraph.TreeWidget + :members: + + .. automethod:: pyqtgraph.TreeWidget.__init__ + diff --git a/doc/source/widgets/valuelabel.rst b/doc/source/widgets/valuelabel.rst new file mode 100644 index 00000000..f87a6b14 --- /dev/null +++ b/doc/source/widgets/valuelabel.rst @@ -0,0 +1,8 @@ +ValueLabel +========== + +.. autoclass:: pyqtgraph.ValueLabel + :members: + + .. automethod:: pyqtgraph.ValueLabel.__init__ + diff --git a/doc/source/widgets/verticallabel.rst b/doc/source/widgets/verticallabel.rst new file mode 100644 index 00000000..4f627437 --- /dev/null +++ b/doc/source/widgets/verticallabel.rst @@ -0,0 +1,8 @@ +VerticalLabel +============= + +.. autoclass:: pyqtgraph.VerticalLabel + :members: + + .. automethod:: pyqtgraph.VerticalLabel.__init__ + diff --git a/examples/Arrow.py b/examples/Arrow.py new file mode 100644 index 00000000..665bbab1 --- /dev/null +++ b/examples/Arrow.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +## Display an animated arrowhead following a curve. +## This example uses the CurveArrow class, which is a combination +## of ArrowItem and CurvePoint. +## +## To place a static arrow anywhere in a scene, use ArrowItem. +## To attach other types of item to a curve, use CurvePoint. + +import initExample ## Add path to library (just for examples; you do not need this) + +import numpy as np +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) + +w = QtGui.QMainWindow() +cw = pg.GraphicsLayoutWidget() +w.show() +w.resize(400,600) +w.setCentralWidget(cw) + +p = cw.addPlot(row=0, col=0) +p2 = cw.addPlot(row=1, col=0) + +## variety of arrow shapes +a1 = pg.ArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3}) +a2 = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, headLen=40, tailLen=40, tailWidth=8, pen=None, brush='y') +a3 = pg.ArrowItem(angle=-60, tipAngle=30, baseAngle=20, headLen=40, tailLen=None, brush=None) +a4 = pg.ArrowItem(angle=-20, tipAngle=30, baseAngle=-30, headLen=40, tailLen=None) +a2.setPos(10,0) +a3.setPos(20,0) +a4.setPos(30,0) +p.addItem(a1) +p.addItem(a2) +p.addItem(a3) +p.addItem(a4) +p.setRange(QtCore.QRectF(-20, -10, 60, 20)) + + +## Animated arrow following curve +c = p2.plot(x=np.sin(np.linspace(0, 2*np.pi, 1000)), y=np.cos(np.linspace(0, 6*np.pi, 1000))) +a = pg.CurveArrow(c) +p2.addItem(a) +anim = a.makeAnimation(loop=-1) +anim.start() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/CLIexample.py b/examples/CLIexample.py new file mode 100644 index 00000000..a412698f --- /dev/null +++ b/examples/CLIexample.py @@ -0,0 +1,21 @@ +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +app = QtGui.QApplication([]) + + +data = np.random.normal(size=1000) +pg.plot(data, title="Simplest possible plotting example") + +data = np.random.normal(size=(500,500)) +pg.show(data, title="Simplest possible image example") + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/ColorButton.py b/examples/ColorButton.py new file mode 100644 index 00000000..4199c8bc --- /dev/null +++ b/examples/ColorButton.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +""" +Simple example demonstrating a button which displays a colored rectangle +and allows the user to select a new color by clicking on the button. +""" + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +btn = pg.ColorButton() +win.setCentralWidget(btn) +win.show() + +def change(btn): + print("change", btn.color()) +def done(btn): + print("done", btn.color()) + +btn.sigColorChanging.connect(change) +btn.sigColorChanged.connect(done) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ConsoleWidget.py b/examples/ConsoleWidget.py new file mode 100644 index 00000000..52fc022e --- /dev/null +++ b/examples/ConsoleWidget.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import pyqtgraph.console + +app = pg.mkQApp() + +## build an initial namespace for console commands to be executed in (this is optional; +## the user can always import these modules manually) +namespace = {'pg': pg, 'np': np} + +## initial text to display in the console +text = """ +This is an interactive python console. The numpy and pyqtgraph modules have already been imported +as 'np' and 'pg'. + +Go, play. +""" +c = pyqtgraph.console.ConsoleWidget(namespace=namespace, text=text) +c.show() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/DataSlicing.py b/examples/DataSlicing.py new file mode 100644 index 00000000..6b83a592 --- /dev/null +++ b/examples/DataSlicing.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +import numpy as np +import scipy +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg + +app = QtGui.QApplication([]) + +## Create window with two ImageView widgets +win = QtGui.QMainWindow() +win.resize(800,800) +cw = QtGui.QWidget() +win.setCentralWidget(cw) +l = QtGui.QGridLayout() +cw.setLayout(l) +imv1 = pg.ImageView() +imv2 = pg.ImageView() +l.addWidget(imv1, 0, 0) +l.addWidget(imv2, 1, 0) +win.show() + +roi = pg.LineSegmentROI([[10, 64], [120,64]], pen='r') +imv1.addItem(roi) + +x1 = np.linspace(-30, 10, 128)[:, np.newaxis, np.newaxis] +x2 = np.linspace(-20, 20, 128)[:, np.newaxis, np.newaxis] +y = np.linspace(-30, 10, 128)[np.newaxis, :, np.newaxis] +z = np.linspace(-20, 20, 128)[np.newaxis, np.newaxis, :] +d1 = np.sqrt(x1**2 + y**2 + z**2) +d2 = 2*np.sqrt(x1[::-1]**2 + y**2 + z**2) +d3 = 4*np.sqrt(x2**2 + y[:,::-1]**2 + z**2) +data = (np.sin(d1) / d1**2) + (np.sin(d2) / d2**2) + (np.sin(d3) / d3**2) + +def update(): + global data, imv1, imv2 + d2 = roi.getArrayRegion(data, imv1.imageItem, axes=(1,2)) + imv2.setImage(d2) + +roi.sigRegionChanged.connect(update) + + +## Display the data +imv1.setImage(data) +imv1.setHistogramRange(-0.01, 0.01) +imv1.setLevels(-0.003, 0.003) + +update() + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py new file mode 100644 index 00000000..01c66b2a --- /dev/null +++ b/examples/DataTreeWidget.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- + +""" +Simple use of DataTreeWidget to display a structure of nested dicts, lists, and arrays +""" + + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +app = QtGui.QApplication([]) +d = { + 'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'dict1': { + 'x': 1, + 'y': 2, + 'z': 'three' + }, + 'array1 (20x20)': np.ones((10,10)) +} + +tree = pg.DataTreeWidget(data=d) +tree.show() +tree.resize(600,600) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() \ No newline at end of file diff --git a/examples/Draw.py b/examples/Draw.py new file mode 100644 index 00000000..2abf0280 --- /dev/null +++ b/examples/Draw.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import pyqtgraph as pg + +app = QtGui.QApplication([]) + +## Create window with GraphicsView widget +w = pg.GraphicsView() +w.show() +w.resize(800,800) + +view = pg.ViewBox() +w.setCentralItem(view) + +## lock the aspect ratio +view.setAspectLocked(True) + +## Create image item +img = pg.ImageItem(np.zeros((200,200))) +view.addItem(img) + +## Set initial view bounds +view.setRange(QtCore.QRectF(0, 0, 200, 200)) + +## start drawing with 3x3 brush +kern = np.array([ + [0.0, 0.5, 0.0], + [0.5, 1.0, 0.5], + [0.0, 0.5, 0.0] +]) +img.setDrawKernel(kern, mask=kern, center=(1,1), mode='add') +img.setLevels([0, 10]) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/Flowchart.py b/examples/Flowchart.py new file mode 100644 index 00000000..8de6016a --- /dev/null +++ b/examples/Flowchart.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates a very basic use of flowcharts: filter data, +displaying both the input and output of the filter. The behavior of +he filter can be reprogrammed by the user. + +Basic steps are: + - create a flowchart and two plots + - input noisy data to the flowchart + - flowchart connects data to the first plot, where it is displayed + - add a gaussian filter to lowpass the data, then display it in the second plot. +""" +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.flowchart import Flowchart +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np +import pyqtgraph.metaarray as metaarray + +app = QtGui.QApplication([]) + + +win = QtGui.QMainWindow() +cw = QtGui.QWidget() +win.setCentralWidget(cw) +layout = QtGui.QGridLayout() +cw.setLayout(layout) + +fc = Flowchart(terminals={ + 'dataIn': {'io': 'in'}, + 'dataOut': {'io': 'out'} +}) +w = fc.widget() + +layout.addWidget(fc.widget(), 0, 0, 2, 1) + +pw1 = pg.PlotWidget() +pw2 = pg.PlotWidget() +layout.addWidget(pw1, 0, 1) +layout.addWidget(pw2, 1, 1) + +win.show() + + +data = np.random.normal(size=1000) +data[200:300] += 1 +data += np.sin(np.linspace(0, 100, 1000)) +data = metaarray.MetaArray(data, info=[{'name': 'Time', 'values': np.linspace(0, 1.0, len(data))}, {}]) + +fc.setInput(dataIn=data) + +pw1Node = fc.createNode('PlotWidget', pos=(0, -150)) +pw1Node.setPlot(pw1) + +pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) +pw2Node.setPlot(pw2) + +fNode = fc.createNode('GaussianFilter', pos=(0, 0)) +fNode.ctrls['sigma'].setValue(5) +fc.connectTerminals(fc.dataIn, fNode.In) +fc.connectTerminals(fc.dataIn, pw1Node.In) +fc.connectTerminals(fNode.Out, pw2Node.In) +fc.connectTerminals(fNode.Out, fc.dataOut) + + +#n1 = fc.createNode('Add', pos=(0,-80)) +#n2 = fc.createNode('Subtract', pos=(140,-10)) +#n3 = fc.createNode('Abs', pos=(0, 80)) +#n4 = fc.createNode('Add', pos=(140,100)) + +#fc.connectTerminals(fc.dataIn, n1.A) +#fc.connectTerminals(fc.dataIn, n1.B) +#fc.connectTerminals(fc.dataIn, n2.A) +#fc.connectTerminals(n1.Out, n4.A) +#fc.connectTerminals(n1.Out, n2.B) +#fc.connectTerminals(n2.Out, n3.In) +#fc.connectTerminals(n3.Out, n4.B) +#fc.connectTerminals(n4.Out, fc.dataOut) + + +#def process(**kargs): + #return fc.process(**kargs) + + +#print process(dataIn=7) + +#fc.setInput(dataIn=3) + +#s = fc.saveState() +#fc.clear() + +#fc.restoreState(s) + +#fc.setInput(dataIn=3) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py new file mode 100644 index 00000000..1d8faa3f --- /dev/null +++ b/examples/GLImageItem.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl +import pyqtgraph as pg +import numpy as np +import scipy.ndimage as ndi + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 200 +w.show() + +## create volume data set to slice three images from +shape = (100,100,70) +data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4)) +data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15 + +## slice out three planes, convert to ARGB for OpenGL texture +levels = (-0.08, 0.08) +tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane +tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane +tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane + +## Create three image items from textures, add to view +v1 = gl.GLImageItem(tex1) +v1.translate(-shape[1]/2, -shape[2]/2, 0) +v1.rotate(90, 0,0,1) +v1.rotate(-90, 0,1,0) +w.addItem(v1) +v2 = gl.GLImageItem(tex2) +v2.translate(-shape[0]/2, -shape[2]/2, 0) +v2.rotate(-90, 1,0,0) +w.addItem(v2) +v3 = gl.GLImageItem(tex3) +v3.translate(-shape[0]/2, -shape[1]/2, 0) +w.addItem(v3) + +ax = gl.GLAxisItem() +w.addItem(ax) + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLIsosurface.py b/examples/GLIsosurface.py new file mode 100644 index 00000000..97fc4874 --- /dev/null +++ b/examples/GLIsosurface.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +## This example uses the isosurface function to convert a scalar field +## (a hydrogen orbital) into a mesh for 3D display. + +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() + +w.setCameraPosition(distance=40) + +g = gl.GLGridItem() +g.scale(2,2,1) +w.addItem(g) + +import numpy as np + +## Define a scalar field from which we will generate an isosurface +def psi(i, j, k, offset=(25, 25, 50)): + x = i-offset[0] + y = j-offset[1] + z = k-offset[2] + th = np.arctan2(z, (x**2+y**2)**0.5) + phi = np.arctan2(y, x) + r = (x**2 + y**2 + z **2)**0.5 + a0 = 1 + #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + return ps + + #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +print("Generating scalar field..") +data = np.abs(np.fromfunction(psi, (50,50,100))) + + +print("Generating isosurface..") +verts, faces = pg.isosurface(data, data.max()/4.) + +md = gl.MeshData(vertexes=verts, faces=faces) + +colors = np.ones((md.faceCount(), 4), dtype=float) +colors[:,3] = 0.2 +colors[:,2] = np.linspace(0, 1, colors.shape[0]) +md.setFaceColors(colors) +m1 = gl.GLMeshItem(meshdata=md, smooth=False, shader='balloon') +m1.setGLOptions('additive') + +#w.addItem(m1) +m1.translate(-25, -25, -20) + +m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='balloon') +m2.setGLOptions('additive') + +w.addItem(m2) +m2.translate(-25, -25, -50) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLLinePlotItem.py b/examples/GLLinePlotItem.py new file mode 100644 index 00000000..2194a51f --- /dev/null +++ b/examples/GLLinePlotItem.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl +import pyqtgraph as pg +import numpy as np + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 40 +w.show() + +gx = gl.GLGridItem() +gx.rotate(90, 0, 1, 0) +gx.translate(-10, 0, 0) +w.addItem(gx) +gy = gl.GLGridItem() +gy.rotate(90, 1, 0, 0) +gy.translate(0, -10, 0) +w.addItem(gy) +gz = gl.GLGridItem() +gz.translate(0, 0, -10) +w.addItem(gz) + +def fn(x, y): + return np.cos((x**2 + y**2)**0.5) + +n = 51 +y = np.linspace(-10,10,n) +x = np.linspace(-10,10,100) +for i in range(n): + yi = np.array([y[i]]*100) + d = (x**2 + yi**2)**0.5 + z = 10 * np.cos(d) / (d+1) + pts = np.vstack([x,yi,z]).transpose() + plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3))) + w.addItem(plt) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py new file mode 100644 index 00000000..49923913 --- /dev/null +++ b/examples/GLMeshItem.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +Simple examples demonstrating the use of GLMeshItem. + +""" + +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() + +w.setCameraPosition(distance=40) + +g = gl.GLGridItem() +g.scale(2,2,1) +w.addItem(g) + +import numpy as np + + +## Example 1: +## Array of vertex positions and array of vertex indexes defining faces +## Colors are specified per-face + +verts = np.array([ + [0, 0, 0], + [2, 0, 0], + [1, 2, 0], + [1, 1, 1], +]) +faces = np.array([ + [0, 1, 2], + [0, 1, 3], + [0, 2, 3], + [1, 2, 3] +]) +colors = np.array([ + [1, 0, 0, 0.3], + [0, 1, 0, 0.3], + [0, 0, 1, 0.3], + [1, 1, 0, 0.3] +]) + +## Mesh item will automatically compute face normals. +m1 = gl.GLMeshItem(vertexes=verts, faces=faces, faceColors=colors, smooth=False) +m1.translate(5, 5, 0) +m1.setGLOptions('additive') +w.addItem(m1) + +## Example 2: +## Array of vertex positions, three per face +## Colors are specified per-vertex + +verts = verts[faces] ## Same mesh geometry as example 2, but now we are passing in 12 vertexes +colors = np.random.random(size=(verts.shape[0], 3, 4)) +#colors[...,3] = 1.0 + +m2 = gl.GLMeshItem(vertexes=verts, vertexColors=colors, smooth=False, shader='balloon') +m2.translate(-5, 5, 0) +w.addItem(m2) + + +## Example 3: +## icosahedron + +md = gl.MeshData.sphere(rows=10, cols=20) +#colors = np.random.random(size=(md.faceCount(), 4)) +#colors[:,3] = 0.3 +#colors[100:] = 0.0 +colors = np.ones((md.faceCount(), 4), dtype=float) +colors[::2,0] = 0 +colors[:,1] = np.linspace(0, 1, colors.shape[0]) +md.setFaceColors(colors) +m3 = gl.GLMeshItem(meshdata=md, smooth=False)#, shader='balloon') + +#m3.translate(-5, -5, 0) +w.addItem(m3) + + + + + +#def psi(i, j, k, offset=(25, 25, 50)): + #x = i-offset[0] + #y = j-offset[1] + #z = k-offset[2] + #th = np.arctan2(z, (x**2+y**2)**0.5) + #phi = np.arctan2(y, x) + #r = (x**2 + y**2 + z **2)**0.5 + #a0 = 1 + ##ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + #ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + #return ps + + ##return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +#print("Generating scalar field..") +#data = np.abs(np.fromfunction(psi, (50,50,100))) + + +##data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); +#print("Generating isosurface..") +#verts = pg.isosurface(data, data.max()/4.) + +#md = gl.MeshData.MeshData(vertexes=verts) + +#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float) +#colors[:,3] = 0.3 +#colors[:,2] = np.linspace(0, 1, colors.shape[0]) +#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False) + +#w.addItem(m1) +#m1.translate(-25, -25, -20) + +#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True) + +#w.addItem(m2) +#m2.translate(-25, -25, -50) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLScatterPlotItem.py b/examples/GLScatterPlotItem.py new file mode 100644 index 00000000..2d25ae12 --- /dev/null +++ b/examples/GLScatterPlotItem.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl +import numpy as np + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 20 +w.show() + +g = gl.GLGridItem() +w.addItem(g) + + +## +## First example is a set of points with pxMode=False +## These demonstrate the ability to have points with real size down to a very small scale +## +pos = np.empty((53, 3)) +size = np.empty((53)) +color = np.empty((53, 4)) +pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5) +pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5) +pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5) + +z = 0.5 +d = 6.0 +for i in range(3,53): + pos[i] = (0,0,z) + size[i] = 2./d + color[i] = (0.0, 1.0, 0.0, 0.5) + z *= 0.5 + d *= 2.0 + +sp1 = gl.GLScatterPlotItem(pos=pos, size=size, color=color, pxMode=False) +sp1.translate(5,5,0) +w.addItem(sp1) + + +## +## Second example shows a volume of points with rapidly updating color +## and pxMode=True +## + +pos = np.random.random(size=(100000,3)) +pos *= [10,-10,10] +pos[0] = (0,0,0) +color = np.ones((pos.shape[0], 4)) +d2 = (pos**2).sum(axis=1)**0.5 +size = np.random.random(size=pos.shape[0])*10 +sp2 = gl.GLScatterPlotItem(pos=pos, color=(1,1,1,1), size=size) +phase = 0. + +w.addItem(sp2) + + +## +## Third example shows a grid of points with rapidly updating position +## and pxMode = False +## + +pos3 = np.zeros((100,100,3)) +pos3[:,:,:2] = np.mgrid[:100, :100].transpose(1,2,0) * [-0.1,0.1] +pos3 = pos3.reshape(10000,3) +d3 = (pos3**2).sum(axis=1)**0.5 + +sp3 = gl.GLScatterPlotItem(pos=pos3, color=(1,1,1,.3), size=0.1, pxMode=False) + +w.addItem(sp3) + + +def update(): + ## update volume colors + global phase, sp2, d2 + s = -np.cos(d2*2+phase) + color = np.empty((len(d2),4), dtype=np.float32) + color[:,3] = np.clip(s * 0.1, 0, 1) + color[:,0] = np.clip(s * 3.0, 0, 1) + color[:,1] = np.clip(s * 1.0, 0, 1) + color[:,2] = np.clip(s ** 3, 0, 1) + sp2.setData(color=color) + phase -= 0.1 + + ## update surface positions and colors + global sp3, d3, pos3 + z = -np.cos(d3*2+phase) + pos3[:,2] = z + color = np.empty((len(d3),4), dtype=np.float32) + color[:,3] = 0.3 + color[:,0] = np.clip(z * 3.0, 0, 1) + color[:,1] = np.clip(z * 1.0, 0, 1) + color[:,2] = np.clip(z ** 3, 0, 1) + sp3.setData(pos=pos3, color=color) + +t = QtCore.QTimer() +t.timeout.connect(update) +t.start(50) + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLSurfacePlot.py b/examples/GLSurfacePlot.py new file mode 100644 index 00000000..c901d51e --- /dev/null +++ b/examples/GLSurfacePlot.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of GLSurfacePlotItem. +""" + + +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.opengl as gl +import scipy.ndimage as ndi +import numpy as np + +## Create a GL View widget to display data +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() +w.setCameraPosition(distance=50) + +## Add a grid to the view +g = gl.GLGridItem() +g.scale(2,2,1) +g.setDepthValue(10) # draw grid after surfaces since they may be translucent +w.addItem(g) + + +## Simple surface plot example +## x, y values are not specified, so assumed to be 0:50 +z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1)) +p1 = gl.GLSurfacePlotItem(z=z, shader='shaded', color=(0.5, 0.5, 1, 1)) +p1.scale(16./49., 16./49., 1.0) +p1.translate(-18, 2, 0) +w.addItem(p1) + + +## Saddle example with x and y specified +x = np.linspace(-8, 8, 50) +y = np.linspace(-8, 8, 50) +z = 0.1 * ((x.reshape(50,1) ** 2) - (y.reshape(1,50) ** 2)) +p2 = gl.GLSurfacePlotItem(x=x, y=y, z=z, shader='normalColor') +p2.translate(-10,-10,0) +w.addItem(p2) + + +## Manually specified colors +z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1)) +x = np.linspace(-12, 12, 50) +y = np.linspace(-12, 12, 50) +colors = np.ones((50,50,4), dtype=float) +colors[...,0] = np.clip(np.cos(((x.reshape(50,1) ** 2) + (y.reshape(1,50) ** 2)) ** 0.5), 0, 1) +colors[...,1] = colors[...,0] + +p3 = gl.GLSurfacePlotItem(z=z, colors=colors.reshape(50*50,4), shader='shaded', smooth=False) +p3.scale(16./49., 16./49., 1.0) +p3.translate(2, -18, 0) +w.addItem(p3) + + + + +## Animated example +## compute surface vertex data +cols = 100 +rows = 100 +x = np.linspace(-8, 8, cols+1).reshape(cols+1,1) +y = np.linspace(-8, 8, rows+1).reshape(1,rows+1) +d = (x**2 + y**2) * 0.1 +d2 = d ** 0.5 + 0.1 + +## precompute height values for all frames +phi = np.arange(0, np.pi*2, np.pi/20.) +z = np.sin(d[np.newaxis,...] + phi.reshape(phi.shape[0], 1, 1)) / d2[np.newaxis,...] + + +## create a surface plot, tell it to use the 'heightColor' shader +## since this does not require normal vectors to render (thus we +## can set computeNormals=False to save time when the mesh updates) +p4 = gl.GLSurfacePlotItem(x=x[:,0], y = y[0,:], shader='heightColor', computeNormals=False, smooth=False) +p4.shader()['colorMap'] = np.array([0.2, 2, 0.5, 0.2, 1, 1, 0.2, 0, 2]) +p4.translate(10, 10, 0) +w.addItem(p4) + +index = 0 +def update(): + global p4, z, index + index -= 1 + p4.setData(z=z[index%z.shape[0]]) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(30) + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLViewWidget.py b/examples/GLViewWidget.py new file mode 100644 index 00000000..4989e8a2 --- /dev/null +++ b/examples/GLViewWidget.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 20 +w.show() + +ax = gl.GLAxisItem() +ax.setSize(5,5,5) +w.addItem(ax) + +b = gl.GLBoxItem() +w.addItem(b) + +ax2 = gl.GLAxisItem() +ax2.setParentItem(b) + +b.translate(1,1,1) + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLVolumeItem.py b/examples/GLVolumeItem.py new file mode 100644 index 00000000..d11730a5 --- /dev/null +++ b/examples/GLVolumeItem.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 200 +w.show() + + +#b = gl.GLBoxItem() +#w.addItem(b) +g = gl.GLGridItem() +g.scale(10, 10, 1) +w.addItem(g) + +import numpy as np +## Hydrogen electron probability density +def psi(i, j, k, offset=(50,50,100)): + x = i-offset[0] + y = j-offset[1] + z = k-offset[2] + th = np.arctan2(z, (x**2+y**2)**0.5) + phi = np.arctan2(y, x) + r = (x**2 + y**2 + z **2)**0.5 + a0 = 2 + #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + return ps + + #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +data = np.fromfunction(psi, (100,100,200)) +positive = np.log(np.clip(data, 0, data.max())**2) +negative = np.log(np.clip(-data, 0, -data.min())**2) + +d2 = np.empty(data.shape + (4,), dtype=np.ubyte) +d2[..., 0] = positive * (255./positive.max()) +d2[..., 1] = negative * (255./negative.max()) +d2[..., 2] = d2[...,1] +d2[..., 3] = d2[..., 0]*0.3 + d2[..., 1]*0.3 +d2[..., 3] = (d2[..., 3].astype(float) / 255.) **2 * 255 + +d2[:, 0, 0] = [255,0,0,100] +d2[0, :, 0] = [0,255,0,100] +d2[0, 0, :] = [0,0,255,100] + +v = gl.GLVolumeItem(d2) +v.translate(-50,-50,-100) +w.addItem(v) + +ax = gl.GLAxisItem() +w.addItem(ax) + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GLshaders.py b/examples/GLshaders.py new file mode 100644 index 00000000..d9d6f00c --- /dev/null +++ b/examples/GLshaders.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" +Demonstration of some of the shader programs included with pyqtgraph. +""" + + + +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() + +w.setCameraPosition(distance=15, azimuth=-90) + +g = gl.GLGridItem() +g.scale(2,2,1) +w.addItem(g) + +import numpy as np + + +md = gl.MeshData.sphere(rows=10, cols=20) +x = np.linspace(-8, 8, 6) + +m1 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 0.2), shader='balloon', glOptions='additive') +m1.translate(x[0], 0, 0) +m1.scale(1, 1, 2) +w.addItem(m1) + +m2 = gl.GLMeshItem(meshdata=md, smooth=True, shader='normalColor', glOptions='opaque') +m2.translate(x[1], 0, 0) +m2.scale(1, 1, 2) +w.addItem(m2) + +m3 = gl.GLMeshItem(meshdata=md, smooth=True, shader='viewNormalColor', glOptions='opaque') +m3.translate(x[2], 0, 0) +m3.scale(1, 1, 2) +w.addItem(m3) + +m4 = gl.GLMeshItem(meshdata=md, smooth=True, shader='shaded', glOptions='opaque') +m4.translate(x[3], 0, 0) +m4.scale(1, 1, 2) +w.addItem(m4) + +m5 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='edgeHilight', glOptions='opaque') +m5.translate(x[4], 0, 0) +m5.scale(1, 1, 2) +w.addItem(m5) + +m6 = gl.GLMeshItem(meshdata=md, smooth=True, color=(1, 0, 0, 1), shader='heightColor', glOptions='opaque') +m6.translate(x[5], 0, 0) +m6.scale(1, 1, 2) +w.addItem(m6) + + + + +#def psi(i, j, k, offset=(25, 25, 50)): + #x = i-offset[0] + #y = j-offset[1] + #z = k-offset[2] + #th = np.arctan2(z, (x**2+y**2)**0.5) + #phi = np.arctan2(y, x) + #r = (x**2 + y**2 + z **2)**0.5 + #a0 = 1 + ##ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + #ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + #return ps + + ##return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +#print("Generating scalar field..") +#data = np.abs(np.fromfunction(psi, (50,50,100))) + + +##data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); +#print("Generating isosurface..") +#verts = pg.isosurface(data, data.max()/4.) + +#md = gl.MeshData.MeshData(vertexes=verts) + +#colors = np.ones((md.vertexes(indexed='faces').shape[0], 4), dtype=float) +#colors[:,3] = 0.3 +#colors[:,2] = np.linspace(0, 1, colors.shape[0]) +#m1 = gl.GLMeshItem(meshdata=md, color=colors, smooth=False) + +#w.addItem(m1) +#m1.translate(-25, -25, -20) + +#m2 = gl.GLMeshItem(vertexes=verts, color=colors, smooth=True) + +#w.addItem(m2) +#m2.translate(-25, -25, -50) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GradientEditor.py b/examples/GradientEditor.py new file mode 100644 index 00000000..1bba4af1 --- /dev/null +++ b/examples/GradientEditor.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +import numpy as np +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +mw = pg.GraphicsView() +mw.resize(800,800) +mw.show() + +#ts = pg.TickSliderItem() +#mw.setCentralItem(ts) +#ts.addTick(0.5, 'r') +#ts.addTick(0.9, 'b') + +ge = pg.GradientEditorItem() +mw.setCentralItem(ge) + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GradientWidget.py b/examples/GradientWidget.py new file mode 100644 index 00000000..1a6458ce --- /dev/null +++ b/examples/GradientWidget.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + + +app = QtGui.QApplication([]) +w = QtGui.QMainWindow() +w.show() +w.resize(400,400) +cw = QtGui.QWidget() +w.setCentralWidget(cw) + +l = QtGui.QGridLayout() +l.setSpacing(0) +cw.setLayout(l) + +w1 = pg.GradientWidget(orientation='top') +w2 = pg.GradientWidget(orientation='right', allowAdd=False) +#w2.setTickColor(1, QtGui.QColor(255,255,255)) +w3 = pg.GradientWidget(orientation='bottom') +w4 = pg.GradientWidget(orientation='left') +w4.loadPreset('spectrum') +label = QtGui.QLabel(""" +- Click a triangle to change its color +- Drag triangles to move +- Click in an empty area to add a new color + (adding is disabled for the right-side widget) +- Right click a triangle to remove +""") + +l.addWidget(w1, 0, 1) +l.addWidget(w2, 1, 2) +l.addWidget(w3, 2, 1) +l.addWidget(w4, 1, 0) +l.addWidget(label, 1, 1) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + diff --git a/examples/GraphicsLayout.py b/examples/GraphicsLayout.py new file mode 100644 index 00000000..90e773c7 --- /dev/null +++ b/examples/GraphicsLayout.py @@ -0,0 +1,79 @@ +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +app = QtGui.QApplication([]) +view = pg.GraphicsView() +l = pg.GraphicsLayout(border=(100,100,100)) +view.setCentralItem(l) +view.show() +view.resize(800,600) + +## Title at top +text = """ +This example demonstrates the use of GraphicsLayout to arrange items in a grid.
+The items added to the layout must be subclasses of QGraphicsWidget (this includes
+PlotItem, ViewBox, LabelItem, and GrphicsLayout itself). +""" +l.addLabel(text, col=1, colspan=4) +l.nextRow() + +## Put vertical label on left side +l.addLabel('Long Vertical Label', angle=-90, rowspan=3) + +## Add 3 plots into the first row (automatic position) +p1 = l.addPlot(title="Plot 1") +p2 = l.addPlot(title="Plot 2") +vb = l.addViewBox(lockAspect=True) +img = pg.ImageItem(np.random.normal(size=(100,100))) +vb.addItem(img) +vb.autoRange() + + +## Add a sub-layout into the second row (automatic position) +## The added item should avoid the first column, which is already filled +l.nextRow() +l2 = l.addLayout(colspan=3, border=(50,0,0)) +l2.setContentsMargins(10, 10, 10, 10) +l2.addLabel("Sub-layout: this layout demonstrates the use of shared axes and axis labels", colspan=3) +l2.nextRow() +l2.addLabel('Vertical Axis Label', angle=-90, rowspan=2) +p21 = l2.addPlot() +p22 = l2.addPlot() +l2.nextRow() +p23 = l2.addPlot() +p24 = l2.addPlot() +l2.nextRow() +l2.addLabel("HorizontalAxisLabel", col=1, colspan=2) + +## hide axes on some plots +p21.hideAxis('bottom') +p22.hideAxis('bottom') +p22.hideAxis('left') +p24.hideAxis('left') +p21.hideButtons() +p22.hideButtons() +p23.hideButtons() +p24.hideButtons() + + +## Add 2 more plots into the third row (manual position) +p4 = l.addPlot(row=3, col=1) +p5 = l.addPlot(row=3, col=2, colspan=2) + +## show some content in the plots +p1.plot([1,3,2,4,3,5]) +p2.plot([1,3,2,4,3,5]) +p4.plot([1,3,2,4,3,5]) +p5.plot([1,3,2,4,3,5]) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/GraphicsScene.py b/examples/GraphicsScene.py new file mode 100644 index 00000000..c8931444 --- /dev/null +++ b/examples/GraphicsScene.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +from pyqtgraph.GraphicsScene import GraphicsScene + +app = QtGui.QApplication([]) +win = pg.GraphicsView() +win.show() + + +class Obj(QtGui.QGraphicsObject): + def __init__(self): + QtGui.QGraphicsObject.__init__(self) + GraphicsScene.registerObject(self) + + def paint(self, p, *args): + p.setPen(pg.mkPen(200,200,200)) + p.drawRect(self.boundingRect()) + + def boundingRect(self): + return QtCore.QRectF(0, 0, 20, 20) + + def mouseClickEvent(self, ev): + if ev.double(): + print("double click") + else: + print("click") + ev.accept() + + #def mouseDragEvent(self, ev): + #print "drag" + #ev.accept() + #self.setPos(self.pos() + ev.pos()-ev.lastPos()) + + + +vb = pg.ViewBox() +win.setCentralItem(vb) + +obj = Obj() +vb.addItem(obj) + +obj2 = Obj() +win.addItem(obj2) + +def clicked(): + print("button click") +btn = QtGui.QPushButton("BTN") +btn.clicked.connect(clicked) +prox = QtGui.QGraphicsProxyWidget() +prox.setWidget(btn) +prox.setPos(100,0) +vb.addItem(prox) + +g = pg.GridItem() +vb.addItem(g) + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py new file mode 100644 index 00000000..9f606457 --- /dev/null +++ b/examples/HistogramLUT.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +import numpy as np +import scipy.ndimage as ndi +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +win.resize(800,600) +win.show() + +cw = QtGui.QWidget() +win.setCentralWidget(cw) + +l = QtGui.QGridLayout() +cw.setLayout(l) +l.setSpacing(0) + +v = pg.GraphicsView() +vb = pg.ViewBox() +vb.setAspectLocked() +v.setCentralItem(vb) +l.addWidget(v, 0, 0) + +w = pg.HistogramLUTWidget() +l.addWidget(w, 0, 1) + +data = ndi.gaussian_filter(np.random.normal(size=(256, 256)), (20, 20)) +for i in range(32): + for j in range(32): + data[i*8, j*8] += .1 +img = pg.ImageItem(data) +#data2 = np.zeros((2,) + data.shape + (2,)) +#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes +#img = pg.ImageItem(data2[0,:,:,0]) +vb.addItem(img) +vb.autoRange() + +w.setImageItem(img) + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ImageItem.py b/examples/ImageItem.py new file mode 100644 index 00000000..4e40f56e --- /dev/null +++ b/examples/ImageItem.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import pyqtgraph as pg +import pyqtgraph.ptime as ptime + +app = QtGui.QApplication([]) + +## Create window with GraphicsView widget +view = pg.GraphicsView() +view.show() ## show view alone in its own window + +## Allow mouse scale/pan. Normally we use a ViewBox for this, but +## for simple examples this is easier. +view.enableMouse() + +## lock the aspect ratio so pixels are always square +view.setAspectLocked(True) + +## Create image item +img = pg.ImageItem(border='w') +view.scene().addItem(img) + +## Set initial view bounds +view.setRange(QtCore.QRectF(0, 0, 600, 600)) + +## Create random image +data = np.random.normal(size=(15, 600, 600), loc=1024, scale=64).astype(np.uint16) +i = 0 + +updateTime = ptime.time() +fps = 0 + +def updateData(): + global img, data, i, updateTime, fps + + ## Display the data + img.setImage(data[i]) + i = (i+1) % data.shape[0] + + QtCore.QTimer.singleShot(1, updateData) + now = ptime.time() + fps2 = 1.0 / (now-updateTime) + updateTime = now + fps = fps * 0.9 + fps2 * 0.1 + + #print "%0.1f fps" % fps + + +updateData() + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/test_ImageView.py b/examples/ImageView.py old mode 100755 new mode 100644 similarity index 83% rename from examples/test_ImageView.py rename to examples/ImageView.py index fd1fd8fe..5edae00b --- a/examples/test_ImageView.py +++ b/examples/ImageView.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- ## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - +import initExample import numpy as np import scipy -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg app = QtGui.QApplication([]) @@ -43,5 +41,7 @@ data[:,50:60,50:60] += sig imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0])) ## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/JoystickButton.py b/examples/JoystickButton.py new file mode 100644 index 00000000..49d310a0 --- /dev/null +++ b/examples/JoystickButton.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +JoystickButton is a button with x/y values. When the button is depressed and the mouse dragged, the x/y values change to follow the mouse. +When the mouse button is released, the x/y values change to 0,0 (rather like litting go of the joystick). +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +mw.resize(300,50) +cw = QtGui.QWidget() +mw.setCentralWidget(cw) +layout = QtGui.QGridLayout() +cw.setLayout(layout) +mw.show() + +l1 = pg.ValueLabel(siPrefix=True, suffix='m') +l2 = pg.ValueLabel(siPrefix=True, suffix='m') +jb = pg.JoystickButton() +jb.setFixedWidth(30) +jb.setFixedHeight(30) + + +layout.addWidget(l1, 0, 0) +layout.addWidget(l2, 0, 1) +layout.addWidget(jb, 0, 2) + +x = 0 +y = 0 +def update(): + global x, y, l1, l2, jb + dx, dy = jb.getState() + x += dx * 1e-3 + y += dy * 1e-3 + l1.setValue(x) + l2.setValue(y) +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(30) + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/Legend.py b/examples/Legend.py new file mode 100644 index 00000000..3d6d5730 --- /dev/null +++ b/examples/Legend.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + +plt = pg.plot() + +plt.addLegend() +#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) +#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case + +c1 = plt.plot([1,3,2,4], pen='r', name='red plot') +c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot') +#l.addItem(c1, 'red plot') +#l.addItem(c2, 'green plot') + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/test_MultiPlotWidget.py b/examples/MultiPlotWidget.py old mode 100755 new mode 100644 similarity index 63% rename from examples/test_MultiPlotWidget.py rename to examples/MultiPlotWidget.py index 4c72275b..28492f64 --- a/examples/test_MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -1,19 +1,18 @@ #!/usr/bin/python # -*- coding: utf-8 -*- ## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample from scipy import random from numpy import linspace -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg -from pyqtgraph.MultiPlotWidget import MultiPlotWidget +from pyqtgraph import MultiPlotWidget try: from metaarray import * except: - print "MultiPlot is only used with MetaArray for now (and you do not have the metaarray package)" + print("MultiPlot is only used with MetaArray for now (and you do not have the metaarray package)") exit() app = QtGui.QApplication([]) @@ -27,6 +26,8 @@ ma = MetaArray(random.random((3, 1000)), info=[{'name': 'Signal', 'cols': [{'nam pw.plot(ma) ## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/PlotAutoRange.py b/examples/PlotAutoRange.py new file mode 100644 index 00000000..3c25b193 --- /dev/null +++ b/examples/PlotAutoRange.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +## This example demonstrates the different auto-ranging capabilities of ViewBoxes + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = pg.GraphicsWindow(title="Plot auto-range examples") +win.resize(800,600) + + +d = np.random.normal(size=100) +d[50:54] += 10 +p1 = win.addPlot(title="95th percentile range", y=d) +p1.enableAutoRange('y', 0.95) + + +p2 = win.addPlot(title="Auto Pan Only") +p2.setAutoPan(y=True) +curve = p2.plot() +def update(): + t = pg.time() + + data = np.ones(100) * np.sin(t) + data[50:60] += np.sin(t) + global curve + curve.setData(data) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py new file mode 100644 index 00000000..cb200429 --- /dev/null +++ b/examples/PlotSpeedTest.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +from pyqtgraph.ptime import time +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +p = pg.plot() +p.setRange(QtCore.QRectF(0, -10, 5000, 20)) +p.setLabel('bottom', 'Index', units='B') +curve = p.plot() + +#curve.setFillBrush((0, 0, 100, 100)) +#curve.setFillLevel(0) + +#lr = pg.LinearRegionItem([100, 4900]) +#p.addItem(lr) + +data = np.random.normal(size=(50,5000)) +ptr = 0 +lastTime = time() +fps = None +def update(): + global curve, data, ptr, p, lastTime, fps + curve.setData(data[ptr%10]) + ptr += 1 + now = time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + p.setTitle('%0.2f fps' % fps) + app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/test_PlotWidget.py b/examples/PlotWidget.py old mode 100755 new mode 100644 similarity index 61% rename from examples/test_PlotWidget.py rename to examples/PlotWidget.py index 2b2ef496..3cca8f7a --- a/examples/test_PlotWidget.py +++ b/examples/PlotWidget.py @@ -1,11 +1,8 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample ## Add path to library (just for examples; you do not need this) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg @@ -32,10 +29,14 @@ p1 = pw.plot() p1.setPen((200,200,100)) ## Add in some extra graphics -rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 1)) +rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 5e-11)) rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) pw.addItem(rect) +pw.setLabel('left', 'Value', units='V') +pw.setLabel('bottom', 'Time', units='s') +pw.setXRange(0, 2) +pw.setYRange(0, 1e-10) def rand(n): data = np.random.random(n) @@ -49,12 +50,13 @@ def rand(n): def updateData(): yd, xd = rand(10000) - p1.updateData(yd, x=xd) + p1.setData(y=yd, x=xd) ## Start a timer to rapidly update the plot in pw t = QtCore.QTimer() t.timeout.connect(updateData) t.start(50) +#updateData() ## Multiple parameterized plots--we can autogenerate averages for these. for i in range(0, 5): @@ -63,11 +65,22 @@ for i in range(0, 5): pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j}) ## Test large numbers -curve = pw3.plot(np.random.normal(size=100)*1e6) +curve = pw3.plot(np.random.normal(size=100)*1e0, clickable=True) curve.setPen('w') ## white pen curve.setShadowPen(pg.mkPen((70,70,30), width=6, cosmetic=True)) +def clicked(): + print("curve clicked") +curve.sigClicked.connect(clicked) -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() +lr = pg.LinearRegionItem([1, 30], bounds=[0,100], movable=True) +pw3.addItem(lr) +line = pg.InfiniteLine(angle=90, movable=True) +pw3.addItem(line) +line.setBounds([0,200]) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/Plotting.py b/examples/Plotting.py new file mode 100644 index 00000000..a41fcd1e --- /dev/null +++ b/examples/Plotting.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +## This example demonstrates many of the 2D plotting capabilities +## in pyqtgraph. All of the plots may be panned/scaled by dragging with +## the left/right mouse buttons. Right click on any plot to show a context menu. + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = pg.GraphicsWindow(title="Basic plotting examples") +win.resize(1000,600) + + + +p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) + +p2 = win.addPlot(title="Multiple curves") +p2.plot(np.random.normal(size=100), pen=(255,0,0)) +p2.plot(np.random.normal(size=100)+5, pen=(0,255,0)) +p2.plot(np.random.normal(size=100)+10, pen=(0,0,255)) + +p3 = win.addPlot(title="Drawing with points") +p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), symbolPen='w') + + +win.nextRow() + +p4 = win.addPlot(title="Parametric, grid enabled") +x = np.cos(np.linspace(0, 2*np.pi, 1000)) +y = np.sin(np.linspace(0, 4*np.pi, 1000)) +p4.plot(x, y) +p4.showGrid(x=True, y=True) + +p5 = win.addPlot(title="Scatter plot, axis labels, log scale") +x = np.random.normal(size=1000) * 1e-5 +y = x*1000 + 0.005 * np.random.normal(size=1000) +y -= y.min()-1.0 +p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) +p5.setLabel('left', "Y Axis", units='A') +p5.setLabel('bottom', "Y Axis", units='s') +p5.setLogMode(x=True, y=False) + +p6 = win.addPlot(title="Updating plot") +curve = p6.plot(pen='y') +data = np.random.normal(size=(10,1000)) +ptr = 0 +def update(): + global curve, data, ptr, p6 + curve.setData(data[ptr%10]) + if ptr == 0: + p6.enableAutoRange('xy', False) ## stop auto-scaling after the first data set is plotted + ptr += 1 +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + + +win.nextRow() + +p7 = win.addPlot(title="Filled plot, axis disabled") +y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1) +p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100)) +p7.showAxis('bottom', False) + + +x2 = np.linspace(-100, 100, 1000) +data2 = np.sin(x2) / x2 +p8 = win.addPlot(title="Region Selection") +p8.plot(data2, pen=(255,255,255,200)) +lr = pg.LinearRegionItem([400,700]) +lr.setZValue(-10) +p8.addItem(lr) + +p9 = win.addPlot(title="Zoom on selected region") +p9.plot(data2) +def updatePlot(): + p9.setXRange(*lr.getRegion(), padding=0) +def updateRegion(): + lr.setRegion(p9.getViewBox().viewRange()[0]) +lr.sigRegionChanged.connect(updatePlot) +p9.sigXRangeChanged.connect(updateRegion) +updatePlot() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py new file mode 100644 index 00000000..d8ad3dd0 --- /dev/null +++ b/examples/ROIExamples.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +## Create image to display +arr = np.ones((100, 100), dtype=float) +arr[45:55, 45:55] = 0 +arr[25, :] = 5 +arr[:, 25] = 5 +arr[75, :] = 5 +arr[:, 75] = 5 +arr[50, :] = 10 +arr[:, 50] = 10 +arr += np.sin(np.linspace(0, 20, 100)).reshape(1, 100) +arr += np.random.normal(size=(100,100)) + + +## create GUI +app = QtGui.QApplication([]) +w = pg.GraphicsWindow(size=(800,800), border=True) + +text = """Data Selection From Image.
\n +Drag an ROI or its handles to update the selected image.
+Hold CTRL while dragging to snap to pixel boundaries
+and 15-degree rotation angles. +""" +w1 = w.addLayout(row=0, col=0) +label1 = w1.addLabel(text, row=0, col=0) +v1a = w1.addViewBox(row=1, col=0, lockAspect=True) +v1b = w1.addViewBox(row=2, col=0, lockAspect=True) +img1a = pg.ImageItem(arr) +v1a.addItem(img1a) +img1b = pg.ImageItem() +v1b.addItem(img1b) +v1a.disableAutoRange('xy') +v1b.disableAutoRange('xy') +v1a.autoRange() +v1b.autoRange() + +rois = [] +rois.append(pg.RectROI([20, 20], [20, 20], pen=(0,9))) +rois[-1].addRotateHandle([1,0], [0.5, 0.5]) +rois.append(pg.LineROI([0, 60], [20, 80], width=5, pen=(1,9))) +rois.append(pg.MultiRectROI([[20, 90], [50, 60], [60, 90]], width=5, pen=(2,9))) +rois.append(pg.EllipseROI([60, 10], [30, 20], pen=(3,9))) +rois.append(pg.CircleROI([80, 50], [20, 20], pen=(4,9))) +#rois.append(pg.LineSegmentROI([[110, 50], [20, 20]], pen=(5,9))) +#rois.append(pg.PolyLineROI([[110, 60], [20, 30], [50, 10]], pen=(6,9))) + +def update(roi): + img1b.setImage(roi.getArrayRegion(arr, img1a), levels=(0, arr.max())) + v1b.autoRange() + +for roi in rois: + roi.sigRegionChanged.connect(update) + v1a.addItem(roi) + +update(rois[-1]) + + + +text = """User-Modifiable ROIs
+Click on a line segment to add a new handle. +Right click on a handle to remove. +""" +w2 = w.addLayout(row=0, col=1) +label2 = w2.addLabel(text, row=0, col=0) +v2a = w2.addViewBox(row=1, col=0, lockAspect=True) +r2a = pg.PolyLineROI([[0,0], [10,10], [10,30], [30,10]], closed=True) +v2a.addItem(r2a) +r2b = pg.PolyLineROI([[0,-20], [10,-10], [10,-30]], closed=False) +v2a.addItem(r2b) +v2a.disableAutoRange('xy') +#v2b.disableAutoRange('xy') +v2a.autoRange() +#v2b.autoRange() + +text = """Building custom ROI types
+ROIs can be built with a variety of different handle types
+that scale and rotate the roi around an arbitrary center location +""" +w3 = w.addLayout(row=1, col=0) +label3 = w3.addLabel(text, row=0, col=0) +v3 = w3.addViewBox(row=1, col=0, lockAspect=True) + +r3a = pg.ROI([0,0], [10,10]) +v3.addItem(r3a) +## handles scaling horizontally around center +r3a.addScaleHandle([1, 0.5], [0.5, 0.5]) +r3a.addScaleHandle([0, 0.5], [0.5, 0.5]) + +## handles scaling vertically from opposite edge +r3a.addScaleHandle([0.5, 0], [0.5, 1]) +r3a.addScaleHandle([0.5, 1], [0.5, 0]) + +## handles scaling both vertically and horizontally +r3a.addScaleHandle([1, 1], [0, 0]) +r3a.addScaleHandle([0, 0], [1, 1]) + +r3b = pg.ROI([20,0], [10,10]) +v3.addItem(r3b) +## handles rotating around center +r3b.addRotateHandle([1, 1], [0.5, 0.5]) +r3b.addRotateHandle([0, 0], [0.5, 0.5]) + +## handles rotating around opposite corner +r3b.addRotateHandle([1, 0], [0, 1]) +r3b.addRotateHandle([0, 1], [1, 0]) + +## handles rotating/scaling around center +r3b.addScaleRotateHandle([0, 0.5], [0.5, 0.5]) +r3b.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) + +v3.disableAutoRange('xy') +v3.autoRange() + + +text = """Transforming objects with ROI""" +w4 = w.addLayout(row=1, col=1) +label4 = w4.addLabel(text, row=0, col=0) +v4 = w4.addViewBox(row=1, col=0, lockAspect=True) +g = pg.GridItem() +v4.addItem(g) +r4 = pg.ROI([0,0], [100,100]) +r4.addRotateHandle([1,0], [0.5, 0.5]) +r4.addRotateHandle([0,1], [0.5, 0.5]) +img4 = pg.ImageItem(arr) +v4.addItem(r4) +img4.setParentItem(r4) + +v4.disableAutoRange('xy') +v4.autoRange() + + + + + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/test_ROItypes.py b/examples/ROItypes.py old mode 100755 new mode 100644 similarity index 52% rename from examples/test_ROItypes.py rename to examples/ROItypes.py index f080e0b4..95b938cd --- a/examples/test_ROItypes.py +++ b/examples/ROItypes.py @@ -1,27 +1,31 @@ #!/usr/bin/python -i # -*- coding: utf-8 -*- ## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg ## create GUI app = QtGui.QApplication([]) -w = QtGui.QMainWindow() -w.resize(800,800) -v = pg.GraphicsView() -#v.invertY(True) ## Images usually have their Y-axis pointing downward + +w = pg.GraphicsWindow(size=(800,800), border=True) + +v = w.addViewBox(colspan=2) + +#w = QtGui.QMainWindow() +#w.resize(800,800) +#v = pg.GraphicsView() +v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) -v.enableMouse(True) -v.autoPixelScale = False -w.setCentralWidget(v) -s = v.scene() -v.setRange(QtCore.QRect(-2, -2, 220, 220)) -w.show() +#v.enableMouse(True) +#v.autoPixelScale = False +#w.setCentralWidget(v) +#s = v.scene() +#v.setRange(QtCore.QRectF(-2, -2, 220, 220)) + ## Create image to display arr = np.ones((100, 100), dtype=float) @@ -36,23 +40,35 @@ arr[:, 50] = 10 ## Create image items, add to scene and set position im1 = pg.ImageItem(arr) im2 = pg.ImageItem(arr) -s.addItem(im1) -s.addItem(im2) +v.addItem(im1) +v.addItem(im2) im2.moveBy(110, 20) +v.setRange(QtCore.QRectF(0, 0, 200, 120)) + im3 = pg.ImageItem() -s.addItem(im3) -im3.moveBy(0, 130) +v2 = w.addViewBox(1,0) +v2.addItem(im3) +v2.setRange(QtCore.QRectF(0, 0, 60, 60)) +v2.invertY(True) +v2.setAspectLocked(True) +#im3.moveBy(0, 130) im3.setZValue(10) + im4 = pg.ImageItem() -s.addItem(im4) -im4.moveBy(110, 130) +v3 = w.addViewBox(1,1) +v3.addItem(im4) +v3.setRange(QtCore.QRectF(0, 0, 60, 60)) +v3.invertY(True) +v3.setAspectLocked(True) +#im4.moveBy(110, 130) im4.setZValue(10) ## create the plot -pi1 = pg.PlotItem() -s.addItem(pi1) -pi1.scale(0.5, 0.5) -pi1.setGeometry(0, 170, 300, 100) +pi1 = w.addPlot(2,0, colspan=2) +#pi1 = pg.PlotItem() +#s.addItem(pi1) +#pi1.scale(0.5, 0.5) +#pi1.setGeometry(0, 170, 300, 100) lastRoi = None @@ -62,31 +78,31 @@ def updateRoi(roi): return lastRoi = roi arr1 = roi.getArrayRegion(im1.image, img=im1) - im3.updateImage(arr1, autoRange=True) + im3.setImage(arr1) arr2 = roi.getArrayRegion(im2.image, img=im2) - im4.updateImage(arr2, autoRange=True) + im4.setImage(arr2) updateRoiPlot(roi, arr1) def updateRoiPlot(roi, data=None): if data is None: data = roi.getArrayRegion(im1.image, img=im1) if data is not None: - roi.curve.updateData(data.mean(axis=1)) + roi.curve.setData(data.mean(axis=1)) ## Create a variety of different ROI types rois = [] -rois.append(pg.widgets.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) -rois.append(pg.widgets.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) -rois.append(pg.widgets.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) -rois.append(pg.widgets.EllipseROI([110, 10], [30, 20], pen=(3,9))) -rois.append(pg.widgets.CircleROI([110, 50], [20, 20], pen=(4,9))) -rois.append(pg.widgets.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) +rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) +rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) +rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) +rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9))) +rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9))) +rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) #rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) ## Add each ROI to the scene and link its data to a plot curve with the same color for r in rois: - s.addItem(r) + v.addItem(r) c = pi1.plot(pen=r.pen) r.curve = c r.sigRegionChanged.connect(updateRoi) @@ -107,5 +123,7 @@ t.start(50) ## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/RemoteGraphicsView.py b/examples/RemoteGraphicsView.py new file mode 100644 index 00000000..137b5e87 --- /dev/null +++ b/examples/RemoteGraphicsView.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +app = pg.mkQApp() + +v = pg.RemoteGraphicsView() +v.show() + +plt = v.pg.PlotItem() +v.setCentralItem(plt) +plt.plot([1,4,2,3,6,2,3,4,2,3], pen='g') + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py new file mode 100644 index 00000000..9baf5cd3 --- /dev/null +++ b/examples/ScatterPlot.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +import sys, os +## Add path to library (just for examples; you do not need this) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +mw.resize(800,800) +view = pg.GraphicsLayoutWidget() ## GraphicsView with GraphicsLayout inserted by default +mw.setCentralWidget(view) +mw.show() + +## create four areas to add plots +w1 = view.addPlot() +w2 = view.addViewBox() +w2.setAspectLocked(True) +view.nextRow() +w3 = view.addPlot() +w4 = view.addPlot() +print("Generating data, this takes a few seconds...") + +## There are a few different ways we can draw scatter plots; each is optimized for different types of data: + + +## 1) All spots identical and transform-invariant (top-left plot). +## In this case we can get a huge performance boost by pre-rendering the spot +## image and just drawing that image repeatedly. + +n = 300 +s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120)) +pos = np.random.normal(size=(2,n), scale=1e-5) +spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] +s1.addPoints(spots) +w1.addItem(s1) + +## Make all plots clickable +lastClicked = [] +def clicked(plot, points): + global lastClicked + for p in lastClicked: + p.resetPen() + print("clicked points", points) + for p in points: + p.setPen('b', width=2) + lastClicked = points +s1.sigClicked.connect(clicked) + + + +## 2) Spots are transform-invariant, but not identical (top-right plot). +## In this case, drawing is as fast as 1), but there is more startup overhead +## and memory usage since each spot generates its own pre-rendered image. + +s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) +pos = np.random.normal(size=(2,n), scale=1e-5) +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] +s2.addPoints(spots) +w2.addItem(s2) +w2.setRange(s2.boundingRect()) +s2.sigClicked.connect(clicked) + + +## 3) Spots are not transform-invariant, not identical (bottom-left). +## This is the slowest case, since all spots must be completely re-drawn +## every time because their apparent transformation may have changed. + +s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view +spots3 = [] +for i in range(10): + for j in range(10): + spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) +s3.addPoints(spots3) +w3.addItem(s3) +s3.sigClicked.connect(clicked) + + +## Test performance of large scatterplots + +s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20)) +pos = np.random.normal(size=(2,10000), scale=1e-9) +s4.addPoints(x=pos[0], y=pos[1]) +w4.addItem(s4) +s4.sigClicked.connect(clicked) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py new file mode 100644 index 00000000..545071b1 --- /dev/null +++ b/examples/ScatterPlotSpeedTest.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import initExample + + +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +import numpy as np +import pyqtgraph as pg +from pyqtgraph.ptime import time +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) +if USE_PYSIDE: + from ScatterPlotSpeedTestTemplate_pyside import Ui_Form +else: + from ScatterPlotSpeedTestTemplate_pyqt import Ui_Form + +win = QtGui.QWidget() +ui = Ui_Form() +ui.setupUi(win) +win.show() + +p = ui.plot + +data = np.random.normal(size=(50,500), scale=100) +sizeArray = (np.random.random(500) * 20.).astype(int) +ptr = 0 +lastTime = time() +fps = None +def update(): + global curve, data, ptr, p, lastTime, fps + p.clear() + if ui.randCheck.isChecked(): + size = sizeArray + else: + size = ui.sizeSpin.value() + curve = pg.ScatterPlotItem(x=data[ptr%50], y=data[(ptr+1)%50], pen='w', brush='b', size=size, pxMode=ui.pixelModeCheck.isChecked()) + p.addItem(curve) + ptr += 1 + now = time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + p.setTitle('%0.2f fps' % fps) + p.repaint() + #app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/ScatterPlotSpeedTestTemplate.ui b/examples/ScatterPlotSpeedTestTemplate.ui new file mode 100644 index 00000000..6b87e85d --- /dev/null +++ b/examples/ScatterPlotSpeedTestTemplate.ui @@ -0,0 +1,59 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + 10 + + + + + + + pixel mode + + + + + + + Size + + + + + + + + + + Randomize + + + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/examples/ScatterPlotSpeedTestTemplate_pyqt.py b/examples/ScatterPlotSpeedTestTemplate_pyqt.py new file mode 100644 index 00000000..22136690 --- /dev/null +++ b/examples/ScatterPlotSpeedTestTemplate_pyqt.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/ScatterPlotSpeedTestTemplate.ui' +# +# Created: Fri Sep 21 15:39:09 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(400, 300) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.sizeSpin = QtGui.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName(_fromUtf8("sizeSpin")) + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtGui.QCheckBox(Form) + self.pixelModeCheck.setObjectName(_fromUtf8("pixelModeCheck")) + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtGui.QLabel(Form) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName(_fromUtf8("plot")) + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtGui.QCheckBox(Form) + self.randCheck.setObjectName(_fromUtf8("randCheck")) + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + +from pyqtgraph import PlotWidget diff --git a/examples/ScatterPlotSpeedTestTemplate_pyside.py b/examples/ScatterPlotSpeedTestTemplate_pyside.py new file mode 100644 index 00000000..690b0990 --- /dev/null +++ b/examples/ScatterPlotSpeedTestTemplate_pyside.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/ScatterPlotSpeedTestTemplate.ui' +# +# Created: Fri Sep 21 15:39:09 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.sizeSpin = QtGui.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName("sizeSpin") + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtGui.QCheckBox(Form) + self.pixelModeCheck.setObjectName("pixelModeCheck") + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtGui.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtGui.QCheckBox(Form) + self.randCheck.setObjectName("randCheck") + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + +from pyqtgraph import PlotWidget diff --git a/examples/SpinBox.py b/examples/SpinBox.py new file mode 100644 index 00000000..488e995b --- /dev/null +++ b/examples/SpinBox.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +app = QtGui.QApplication([]) + + +spins = [ + ("Floating-point spin box, min=0, no maximum.", pg.SpinBox(value=5.0, bounds=[0, None])), + ("Integer spin box, dec stepping
(1-9, 10-90, 100-900, etc)", pg.SpinBox(value=10, int=True, dec=True, minStep=1, step=1)), + ("Float with SI-prefixed units
(n, u, m, k, M, etc)", pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), + ("Float with SI-prefixed units,
dec step=0.1, minStep=0.1", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), + ("Float with SI-prefixed units,
dec step=0.5, minStep=0.01", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), + ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), +] + + +win = QtGui.QMainWindow() +cw = QtGui.QWidget() +layout = QtGui.QGridLayout() +cw.setLayout(layout) +win.setCentralWidget(cw) +win.show() +#win.resize(300, 600) +changingLabel = QtGui.QLabel() ## updated immediately +changedLabel = QtGui.QLabel() ## updated only when editing is finished or mouse wheel has stopped for 0.3sec +changingLabel.setMinimumWidth(200) +font = changingLabel.font() +font.setBold(True) +font.setPointSize(14) +changingLabel.setFont(font) +changedLabel.setFont(font) +labels = [] + + +def valueChanged(sb): + changedLabel.setText("Final value: %s" % str(sb.value())) + +def valueChanging(sb, value): + changingLabel.setText("Value changing: %s" % str(sb.value())) + + +for text, spin in spins: + label = QtGui.QLabel(text) + labels.append(label) + layout.addWidget(label) + layout.addWidget(spin) + spin.sigValueChanged.connect(valueChanged) + spin.sigValueChanging.connect(valueChanging) + +layout.addWidget(changingLabel, 0, 1) +layout.addWidget(changedLabel, 2, 1) + + +#def mkWin(): + #win = QtGui.QMainWindow() + #g = QtGui.QFormLayout() + #w = QtGui.QWidget() + #w.setLayout(g) + #win.setCentralWidget(w) + #s1 = SpinBox(value=5, step=0.1, bounds=[-1.5, None], suffix='units') + #t1 = QtGui.QLineEdit() + #g.addRow(s1, t1) + #s2 = SpinBox(value=10e-6, dec=True, step=0.1, minStep=1e-6, suffix='A', siPrefix=True) + #t2 = QtGui.QLineEdit() + #g.addRow(s2, t2) + #s3 = SpinBox(value=1000, dec=True, step=0.5, minStep=1e-6, bounds=[1, 1e9], suffix='Hz', siPrefix=True) + #t3 = QtGui.QLineEdit() + #g.addRow(s3, t3) + #s4 = SpinBox(int=True, dec=True, step=1, minStep=1, bounds=[-10, 1000]) + #t4 = QtGui.QLineEdit() + #g.addRow(s4, t4) + + #win.show() + + #import sys + #for sb in [s1, s2, s3,s4]: + + ##QtCore.QObject.connect(sb, QtCore.SIGNAL('valueChanged(double)'), lambda v: sys.stdout.write(str(sb) + " valueChanged\n")) + ##QtCore.QObject.connect(sb, QtCore.SIGNAL('editingFinished()'), lambda: sys.stdout.write(str(sb) + " editingFinished\n")) + #sb.sigValueChanged.connect(valueChanged) + #sb.sigValueChanging.connect(valueChanging) + #sb.editingFinished.connect(lambda: sys.stdout.write(str(sb) + " editingFinished\n")) + #return win, w, [s1, s2, s3, s4] +#a = mkWin() + + +#def test(n=100): + #for i in range(n): + #win, w, sb = mkWin() + #for s in sb: + #w.setParent(None) + #s.setParent(None) + #s.valueChanged.disconnect() + #s.editingFinished.disconnect() + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/TreeWidget.py b/examples/TreeWidget.py new file mode 100644 index 00000000..80e9bd24 --- /dev/null +++ b/examples/TreeWidget.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +app = QtGui.QApplication([]) + +w = pg.TreeWidget() +w.setColumnCount(2) +w.show() + +i1 = QtGui.QTreeWidgetItem(["Item 1"]) +i11 = QtGui.QTreeWidgetItem(["Item 1.1"]) +i12 = QtGui.QTreeWidgetItem(["Item 1.2"]) +i2 = QtGui.QTreeWidgetItem(["Item 2"]) +i21 = QtGui.QTreeWidgetItem(["Item 2.1"]) +i211 = pg.TreeWidgetItem(["Item 2.1.1"]) +i212 = pg.TreeWidgetItem(["Item 2.1.2"]) +i22 = pg.TreeWidgetItem(["Item 2.2"]) +i3 = pg.TreeWidgetItem(["Item 3"]) +i4 = pg.TreeWidgetItem(["Item 4"]) +i5 = pg.TreeWidgetItem(["Item 5"]) +b5 = QtGui.QPushButton('Button') +i5.setWidget(1, b5) + + + +w.addTopLevelItem(i1) +w.addTopLevelItem(i2) +w.addTopLevelItem(i3) +w.addTopLevelItem(i4) +w.addTopLevelItem(i5) +i1.addChild(i11) +i1.addChild(i12) +i2.addChild(i21) +i21.addChild(i211) +i21.addChild(i212) +i2.addChild(i22) + +b1 = QtGui.QPushButton("Button") +w.setItemWidget(i1, 1, b1) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py new file mode 100644 index 00000000..1d0fa58c --- /dev/null +++ b/examples/VideoSpeedTest.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +Tests the speed of image updates for an ImageItem and RawImageWidget. +The speed will generally depend on the type of data being shown, whether +it is being scaled and/or converted by lookup table, and whether OpenGL +is used by the view widget +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +import numpy as np +import pyqtgraph as pg +from pyqtgraph import RawImageWidget +import scipy.ndimage as ndi +import pyqtgraph.ptime as ptime + +if USE_PYSIDE: + import VideoTemplate_pyside as VideoTemplate +else: + import VideoTemplate_pyqt as VideoTemplate + + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = QtGui.QMainWindow() +ui = VideoTemplate.Ui_MainWindow() +ui.setupUi(win) +win.show() +ui.maxSpin1.setOpts(value=255, step=1) +ui.minSpin1.setOpts(value=0, step=1) + +#ui.graphicsView.useOpenGL() ## buggy, but you can try it if you need extra speed. + +vb = pg.ViewBox() +ui.graphicsView.setCentralItem(vb) +vb.setAspectLocked() +img = pg.ImageItem() +vb.addItem(img) +vb.setRange(QtCore.QRectF(0, 0, 512, 512)) + +LUT = None +def updateLUT(): + global LUT, ui + dtype = ui.dtypeCombo.currentText() + if dtype == 'uint8': + n = 256 + else: + n = 4096 + LUT = ui.gradient.getLookupTable(n, alpha=ui.alphaCheck.isChecked()) +ui.gradient.sigGradientChanged.connect(updateLUT) +updateLUT() + +ui.alphaCheck.toggled.connect(updateLUT) + +def updateScale(): + global ui + spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3] + if ui.rgbLevelsCheck.isChecked(): + for s in spins[2:]: + s.setEnabled(True) + else: + for s in spins[2:]: + s.setEnabled(False) +ui.rgbLevelsCheck.toggled.connect(updateScale) + +cache = {} +def mkData(): + global data, cache, ui + dtype = (ui.dtypeCombo.currentText(), ui.rgbCheck.isChecked()) + if dtype not in cache: + if dtype[0] == 'uint8': + dt = np.uint8 + loc = 128 + scale = 64 + mx = 255 + elif dtype[0] == 'uint16': + dt = np.uint16 + loc = 4096 + scale = 1024 + mx = 2**16 + elif dtype[0] == 'float': + dt = np.float + loc = 1.0 + scale = 0.1 + + if ui.rgbCheck.isChecked(): + data = np.random.normal(size=(20,512,512,3), loc=loc, scale=scale) + data = ndi.gaussian_filter(data, (0, 6, 6, 0)) + else: + data = np.random.normal(size=(20,512,512), loc=loc, scale=scale) + data = ndi.gaussian_filter(data, (0, 6, 6)) + if dtype[0] != 'float': + data = np.clip(data, 0, mx) + data = data.astype(dt) + cache[dtype] = data + + data = cache[dtype] + updateLUT() +mkData() +ui.dtypeCombo.currentIndexChanged.connect(mkData) +ui.rgbCheck.toggled.connect(mkData) + +ptr = 0 +lastTime = ptime.time() +fps = None +def update(): + global ui, ptr, lastTime, fps, LUT, img + if ui.lutCheck.isChecked(): + useLut = LUT + else: + useLut = None + + if ui.scaleCheck.isChecked(): + if ui.rgbLevelsCheck.isChecked(): + useScale = [ + [ui.minSpin1.value(), ui.maxSpin1.value()], + [ui.minSpin2.value(), ui.maxSpin2.value()], + [ui.minSpin3.value(), ui.maxSpin3.value()]] + else: + useScale = [ui.minSpin1.value(), ui.maxSpin1.value()] + else: + useScale = None + + if ui.rawRadio.isChecked(): + ui.rawImg.setImage(data[ptr%data.shape[0]], lut=useLut, levels=useScale) + else: + img.setImage(data[ptr%data.shape[0]], autoLevels=False, levels=useScale, lut=useLut) + #img.setImage(data[ptr%data.shape[0]], autoRange=False) + + ptr += 1 + now = ptime.time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + ui.fpsLabel.setText('%0.2f fps' % fps) + app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui new file mode 100644 index 00000000..3dddb928 --- /dev/null +++ b/examples/VideoTemplate.ui @@ -0,0 +1,256 @@ + + + MainWindow + + + + 0 + 0 + 985 + 674 + + + + MainWindow + + + + + + + + + + 0 + 0 + + + + + + + + + + + RawImageWidget (unscaled; faster) + + + true + + + + + + + GraphicsView + ImageItem (scaled; slower) + + + + + + + + + Data type + + + + + + + + uint8 + + + + + uint16 + + + + + float + + + + + + + + Scale Data + + + + + + + RGB + + + + + + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + + + + + + + + false + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + false + + + + + + + + + + + false + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + false + + + + + + + + + Use Lookup Table + + + + + + + alpha + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 12 + + + + FPS + + + Qt::AlignCenter + + + + + + + RGB + + + + + + + + + GraphicsView + QGraphicsView +
pyqtgraph
+
+ + RawImageWidget + QWidget +
pyqtgraph
+ 1 +
+ + GradientWidget + QWidget +
pyqtgraph
+ 1 +
+ + SpinBox + QDoubleSpinBox +
pyqtgraph
+
+
+ + +
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py new file mode 100644 index 00000000..c3430e2d --- /dev/null +++ b/examples/VideoTemplate_pyqt.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# +# Created: Sun Nov 4 18:24:20 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + MainWindow.resize(985, 674) + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) + self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.gridLayout = QtGui.QGridLayout() + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.rawImg = RawImageWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName(_fromUtf8("rawImg")) + self.gridLayout.addWidget(self.rawImg, 0, 0, 1, 1) + self.graphicsView = GraphicsView(self.centralwidget) + self.graphicsView.setObjectName(_fromUtf8("graphicsView")) + self.gridLayout.addWidget(self.graphicsView, 0, 1, 1, 1) + self.rawRadio = QtGui.QRadioButton(self.centralwidget) + self.rawRadio.setChecked(True) + self.rawRadio.setObjectName(_fromUtf8("rawRadio")) + self.gridLayout.addWidget(self.rawRadio, 1, 0, 1, 1) + self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setObjectName(_fromUtf8("gfxRadio")) + self.gridLayout.addWidget(self.gfxRadio, 1, 1, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.label = QtGui.QLabel(self.centralwidget) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.dtypeCombo = QtGui.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName(_fromUtf8("dtypeCombo")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.gridLayout_2.addWidget(self.dtypeCombo, 2, 2, 1, 1) + self.scaleCheck = QtGui.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName(_fromUtf8("scaleCheck")) + self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) + self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName(_fromUtf8("rgbLevelsCheck")) + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName(_fromUtf8("minSpin1")) + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtGui.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName(_fromUtf8("maxSpin1")) + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 3, 2, 1, 1) + self.horizontalLayout_2 = QtGui.QHBoxLayout() + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName(_fromUtf8("minSpin2")) + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtGui.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName(_fromUtf8("maxSpin2")) + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 4, 2, 1, 1) + self.horizontalLayout_3 = QtGui.QHBoxLayout() + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName(_fromUtf8("minSpin3")) + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtGui.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName(_fromUtf8("label_4")) + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName(_fromUtf8("maxSpin3")) + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 5, 2, 1, 1) + self.lutCheck = QtGui.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName(_fromUtf8("lutCheck")) + self.gridLayout_2.addWidget(self.lutCheck, 6, 0, 1, 1) + self.alphaCheck = QtGui.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName(_fromUtf8("alphaCheck")) + self.gridLayout_2.addWidget(self.alphaCheck, 6, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName(_fromUtf8("gradient")) + self.gridLayout_2.addWidget(self.gradient, 6, 2, 1, 2) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 2, 3, 1, 1) + self.fpsLabel = QtGui.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName(_fromUtf8("fpsLabel")) + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName(_fromUtf8("rgbCheck")) + self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) + self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget (unscaled; faster)", None, QtGui.QApplication.UnicodeUTF8)) + self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem (scaled; slower)", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) + self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py new file mode 100644 index 00000000..d19e0f23 --- /dev/null +++ b/examples/VideoTemplate_pyside.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# +# Created: Sun Nov 4 18:24:21 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(985, 674) + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.gridLayout = QtGui.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.rawImg = RawImageWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName("rawImg") + self.gridLayout.addWidget(self.rawImg, 0, 0, 1, 1) + self.graphicsView = GraphicsView(self.centralwidget) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 0, 1, 1, 1) + self.rawRadio = QtGui.QRadioButton(self.centralwidget) + self.rawRadio.setChecked(True) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 1, 0, 1, 1) + self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 1, 1, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.label = QtGui.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.dtypeCombo = QtGui.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName("dtypeCombo") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.gridLayout_2.addWidget(self.dtypeCombo, 2, 2, 1, 1) + self.scaleCheck = QtGui.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName("scaleCheck") + self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) + self.rgbLevelsCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 3, 1, 1, 1) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName("minSpin1") + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtGui.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName("maxSpin1") + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 3, 2, 1, 1) + self.horizontalLayout_2 = QtGui.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName("minSpin2") + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtGui.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName("maxSpin2") + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 4, 2, 1, 1) + self.horizontalLayout_3 = QtGui.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName("minSpin3") + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtGui.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName("maxSpin3") + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 5, 2, 1, 1) + self.lutCheck = QtGui.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName("lutCheck") + self.gridLayout_2.addWidget(self.lutCheck, 6, 0, 1, 1) + self.alphaCheck = QtGui.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName("alphaCheck") + self.gridLayout_2.addWidget(self.alphaCheck, 6, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName("gradient") + self.gridLayout_2.addWidget(self.gradient, 6, 2, 1, 2) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 2, 3, 1, 1) + self.fpsLabel = QtGui.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName("fpsLabel") + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 2, 1, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) + self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget (unscaled; faster)", None, QtGui.QApplication.UnicodeUTF8)) + self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem (scaled; slower)", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) + self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbLevelsCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget diff --git a/examples/test_viewBox.py b/examples/ViewBox.py old mode 100755 new mode 100644 similarity index 72% rename from examples/test_viewBox.py rename to examples/ViewBox.py index 0b8db232..f2269176 --- a/examples/test_viewBox.py +++ b/examples/ViewBox.py @@ -1,37 +1,39 @@ #!/usr/bin/python # -*- coding: utf-8 -*- ## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample ## This example uses a ViewBox to create a PlotWidget-like interface #from scipy import random import numpy as np -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) mw = QtGui.QMainWindow() -cw = QtGui.QWidget() -vl = QtGui.QVBoxLayout() -cw.setLayout(vl) -mw.setCentralWidget(cw) +#cw = QtGui.QWidget() +#vl = QtGui.QVBoxLayout() +#cw.setLayout(vl) +#mw.setCentralWidget(cw) mw.show() mw.resize(800, 600) - -gv = pg.GraphicsView(cw) -gv.enableMouse(False) ## Mouse interaction will be handled by the ViewBox +gv = pg.GraphicsView() +mw.setCentralWidget(gv) +#gv.enableMouse(False) ## Mouse interaction will be handled by the ViewBox l = QtGui.QGraphicsGridLayout() l.setHorizontalSpacing(0) l.setVerticalSpacing(0) +#vl.addWidget(gv) vb = pg.ViewBox() -p1 = pg.PlotCurveItem() +#grid = pg.GridItem() +#vb.addItem(grid) + +p1 = pg.PlotDataItem() vb.addItem(p1) -vl.addWidget(gv) class movableRect(QtGui.QGraphicsRectItem): def __init__(self, *args): @@ -63,12 +65,12 @@ l.addItem(vb, 0, 1) gv.centralWidget.setLayout(l) -xScale = pg.ScaleItem(orientation='bottom', linkView=vb) +xScale = pg.AxisItem(orientation='bottom', linkView=vb) l.addItem(xScale, 1, 1) -yScale = pg.ScaleItem(orientation='left', linkView=vb) +yScale = pg.AxisItem(orientation='left', linkView=vb) l.addItem(yScale, 0, 0) -xScale.setLabel(text=u"X Axis", units="s") +xScale.setLabel(text="X Axis", units="s") yScale.setLabel('Y Axis', units='V') def rand(n): @@ -82,7 +84,7 @@ def rand(n): def updateData(): yd, xd = rand(10000) - p1.updateData(yd, x=xd) + p1.setData(y=yd, x=xd) yd, xd = rand(10000) updateData() @@ -93,5 +95,7 @@ t.timeout.connect(updateData) t.start(50) ## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..23b7cd58 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +from __main__ import run diff --git a/examples/__main__.py b/examples/__main__.py new file mode 100644 index 00000000..e234a9da --- /dev/null +++ b/examples/__main__.py @@ -0,0 +1,244 @@ +import sys, os, subprocess, time + +import initExample +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE + +if USE_PYSIDE: + from exampleLoaderTemplate_pyside import Ui_Form +else: + from exampleLoaderTemplate_pyqt import Ui_Form + +import os, sys +from pyqtgraph.pgcollections import OrderedDict + +examples = OrderedDict([ + ('Command-line usage', 'CLIexample.py'), + ('Basic Plotting', 'Plotting.py'), + ('ImageView', 'ImageView.py'), + ('ParameterTree', 'parametertree.py'), + ('Crosshair / Mouse interaction', 'crosshair.py'), + ('Data Slicing', 'DataSlicing.py'), + ('Plot Customization', 'customPlot.py'), + ('Dock widgets', 'dockarea.py'), + ('Console', 'ConsoleWidget.py'), + ('Histograms', 'histogram.py'), + ('GraphicsItems', OrderedDict([ + ('Scatter Plot', 'ScatterPlot.py'), + #('PlotItem', 'PlotItem.py'), + ('IsocurveItem', 'isocurve.py'), + ('ImageItem - video', 'ImageItem.py'), + ('ImageItem - draw', 'Draw.py'), + ('Region-of-Interest', 'ROIExamples.py'), + ('GraphicsLayout', 'GraphicsLayout.py'), + ('LegendItem', 'Legend.py'), + ('Text Item', 'text.py'), + ('Linked Views', 'linkedViews.py'), + ('Arrow', 'Arrow.py'), + ('ViewBox', 'ViewBox.py'), + ])), + ('Benchmarks', OrderedDict([ + ('Video speed test', 'VideoSpeedTest.py'), + ('Line Plot update', 'PlotSpeedTest.py'), + ('Scatter Plot update', 'ScatterPlotSpeedTest.py'), + ])), + ('3D Graphics', OrderedDict([ + ('Volumetric', 'GLVolumeItem.py'), + ('Isosurface', 'GLIsosurface.py'), + ('Surface Plot', 'GLSurfacePlot.py'), + ('Scatter Plot', 'GLScatterPlotItem.py'), + ('Shaders', 'GLshaders.py'), + ('Line Plot', 'GLLinePlotItem.py'), + ('Mesh', 'GLMeshItem.py'), + ('Image', 'GLImageItem.py'), + ])), + ('Widgets', OrderedDict([ + ('PlotWidget', 'PlotWidget.py'), + ('SpinBox', 'SpinBox.py'), + ('ConsoleWidget', 'ConsoleWidget.py'), + ('TreeWidget', 'TreeWidget.py'), + ('DataTreeWidget', 'DataTreeWidget.py'), + ('GradientWidget', 'GradientWidget.py'), + #('TableWidget', '../widgets/TableWidget.py'), + ('ColorButton', 'ColorButton.py'), + #('CheckTable', '../widgets/CheckTable.py'), + #('VerticalLabel', '../widgets/VerticalLabel.py'), + ('JoystickButton', 'JoystickButton.py'), + ])), + + ('GraphicsScene', 'GraphicsScene.py'), + ('Flowcharts', 'Flowchart.py'), + #('Canvas', '../canvas'), + #('MultiPlotWidget', 'MultiPlotWidget.py'), +]) + +path = os.path.abspath(os.path.dirname(__file__)) + +class ExampleLoader(QtGui.QMainWindow): + def __init__(self): + QtGui.QMainWindow.__init__(self) + self.ui = Ui_Form() + self.cw = QtGui.QWidget() + self.setCentralWidget(self.cw) + self.ui.setupUi(self.cw) + + global examples + self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) + self.ui.exampleTree.expandAll() + + self.resize(1000,500) + self.show() + self.ui.splitter.setSizes([250,750]) + self.ui.loadBtn.clicked.connect(self.loadFile) + self.ui.exampleTree.currentItemChanged.connect(self.showFile) + self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) + self.ui.pyqtCheck.toggled.connect(self.pyqtToggled) + self.ui.pysideCheck.toggled.connect(self.pysideToggled) + + def pyqtToggled(self, b): + if b: + self.ui.pysideCheck.setChecked(False) + + def pysideToggled(self, b): + if b: + self.ui.pyqtCheck.setChecked(False) + + + def populateTree(self, root, examples): + for key, val in examples.items(): + item = QtGui.QTreeWidgetItem([key]) + if isinstance(val, basestring): + item.file = val + else: + self.populateTree(item, val) + root.addChild(item) + + + def currentFile(self): + item = self.ui.exampleTree.currentItem() + if hasattr(item, 'file'): + global path + return os.path.join(path, item.file) + return None + + def loadFile(self): + fn = self.currentFile() + extra = [] + if self.ui.pyqtCheck.isChecked(): + extra.append('pyqt') + elif self.ui.pysideCheck.isChecked(): + extra.append('pyside') + + if self.ui.forceGraphicsCheck.isChecked(): + extra.append(str(self.ui.forceGraphicsCombo.currentText())) + + if fn is None: + return + if sys.platform.startswith('win'): + os.spawnl(os.P_NOWAIT, sys.executable, '"'+sys.executable+'"', '"' + fn + '"', *extra) + else: + os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra) + + + def showFile(self): + fn = self.currentFile() + if fn is None: + self.ui.codeView.clear() + return + if os.path.isdir(fn): + fn = os.path.join(fn, '__main__.py') + text = open(fn).read() + self.ui.codeView.setPlainText(text) + +def run(): + app = QtGui.QApplication([]) + loader = ExampleLoader() + + app.exec_() + +def buildFileList(examples, files=None): + if files == None: + files = [] + for key, val in examples.items(): + #item = QtGui.QTreeWidgetItem([key]) + if isinstance(val, basestring): + #item.file = val + files.append((key,val)) + else: + buildFileList(val, files) + return files + +def testFile(name, f, exe, lib, graphicsSystem=None): + global path + fn = os.path.join(path,f) + #print "starting process: ", fn + os.chdir(path) + sys.stdout.write(name) + sys.stdout.flush() + + import1 = "import %s" % lib if lib != '' else '' + import2 = os.path.splitext(os.path.split(fn)[1])[0] + graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + code = """ +try: + %s + import pyqtgraph as pg + %s + import %s + import sys + print("test complete") + sys.stdout.flush() + import time + while True: ## run a little event loop + pg.QtGui.QApplication.processEvents() + time.sleep(0.01) +except: + print("test failed") + raise + +""" % (import1, graphicsSystem, import2) + + process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + #process.stdin.close() + output = '' + fail = False + while True: + c = process.stdout.read(1).decode() + output += c + #sys.stdout.write(c) + #sys.stdout.flush() + if output.endswith('test complete'): + break + if output.endswith('test failed'): + fail = True + break + time.sleep(1) + process.kill() + #process.wait() + res = process.communicate() + + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + print('.' * (50-len(name)) + 'FAILED') + print(res[0].decode()) + print(res[1].decode()) + else: + print('.' * (50-len(name)) + 'passed') + + + +if __name__ == '__main__': + if '--test' in sys.argv[1:]: + files = buildFileList(examples) + if '--pyside' in sys.argv[1:]: + lib = 'PySide' + elif '--pyqt' in sys.argv[1:]: + lib = 'PyQt4' + else: + lib = '' + + exe = sys.executable + print("Running tests:", lib, sys.executable) + for f in files: + testFile(f[0], f[1], exe, lib) + else: + run() diff --git a/examples/crosshair.py b/examples/crosshair.py new file mode 100644 index 00000000..a99f097b --- /dev/null +++ b/examples/crosshair.py @@ -0,0 +1,78 @@ +import initExample ## Add path to library (just for examples; you do not need this) +import numpy as np +import scipy.ndimage as ndi +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Point import Point + +#genearte layout +app = QtGui.QApplication([]) +win = pg.GraphicsWindow() +label = pg.LabelItem(justify='right') +win.addItem(label) +p1 = win.addPlot(row=1, col=0) +p2 = win.addPlot(row=2, col=0) + +region = pg.LinearRegionItem() +region.setZValue(10) +p2.addItem(region) + +#pg.dbg() +p1.setAutoVisible(y=True) + + +#create numpy arrays +#make the numbers large to show that the xrange shows data from 10000 to all the way 0 +data1 = 10000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) +data2 = 15000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000) + +p1.plot(data1, pen="r") +p1.plot(data2, pen="g") + +p2.plot(data1, pen="w") + +def update(): + region.setZValue(10) + minX, maxX = region.getRegion() + p1.setXRange(minX, maxX, padding=0) + +region.sigRegionChanged.connect(update) + +def updateRegion(window, viewRange): + rgn = viewRange[0] + region.setRegion(rgn) + +p1.sigRangeChanged.connect(updateRegion) + +region.setRegion([1000, 2000]) + +#cross hair +vLine = pg.InfiniteLine(angle=90, movable=False) +hLine = pg.InfiniteLine(angle=0, movable=False) +p1.addItem(vLine, ignoreBounds=True) +p1.addItem(hLine, ignoreBounds=True) + + +vb = p1.vb + +def mouseMoved(evt): + pos = evt[0] ## using signal proxy turns original arguments into a tuple + if p1.sceneBoundingRect().contains(pos): + mousePoint = vb.mapSceneToView(pos) + index = int(mousePoint.x()) + if index > 0 and index < len(data1): + label.setText("x=%0.1f, y1=%0.1f, y2=%0.1f" % (mousePoint.x(), data1[index], data2[index])) + vLine.setPos(mousePoint.x()) + hLine.setPos(mousePoint.y()) + + + +proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved) +#p1.scene().sigMouseMoved.connect(mouseMoved) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/customGraphicsItem.py b/examples/customGraphicsItem.py new file mode 100644 index 00000000..263ce0c5 --- /dev/null +++ b/examples/customGraphicsItem.py @@ -0,0 +1,42 @@ +import pyqtgraph as pg +from pyqtgraph import QtCore, QtGui + +class CandlestickItem(pg.GraphicsObject): + def __init__(self, data): + pg.GraphicsObject.__init__(self) + self.data = data ## data must have fields: time, open, close, min, max + self.generatePicture() + + def generatePicture(self): + self.picture = QtGui.QPicture() + p = QtGui.QPainter(self.picture) + p.setPen(pg.mkPen('w')) + w = (self.data[1][0] - self.data[0][0]) / 3. + for (t, open, close, min, max) in self.data: + p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max)) + if open > close: + p.setBrush(pg.mkBrush('r')) + else: + p.setBrush(pg.mkBrush('g')) + p.drawRect(QtCore.QRectF(t-w, open, w*2, close-open)) + p.end() + + def paint(self, p, *args): + p.drawPicture(0, 0, self.picture) + + def boundingRect(self): + return QtCore.QRectF(self.picture.boundingRect()) + +data = [ ## fields are (time, open, close, min, max). + (1., 10, 13, 5, 15), + (2., 13, 17, 9, 20), + (3., 17, 14, 11, 23), + (4., 14, 15, 5, 19), + (5., 15, 9, 8, 22), + (6., 9, 15, 8, 16), +] +item = CandlestickItem(data) +plt = pg.plot() +plt.addItem(item) + +QtGui.QApplication.exec_() \ No newline at end of file diff --git a/examples/customPlot.py b/examples/customPlot.py new file mode 100644 index 00000000..1c3b2489 --- /dev/null +++ b/examples/customPlot.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +## +## This example demonstrates the creation of a plot with a customized +## AxisItem and ViewBox. +## + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import time + +class DateAxis(pg.AxisItem): + def tickStrings(self, values, scale, spacing): + strns = [] + rng = max(values)-min(values) + #if rng < 120: + # return pg.AxisItem.tickStrings(self, values, scale, spacing) + if rng < 3600*24: + string = '%H:%M:%S' + label1 = '%b %d -' + label2 = ' %b %d, %Y' + elif rng >= 3600*24 and rng < 3600*24*30: + string = '%d' + label1 = '%b - ' + label2 = '%b, %Y' + elif rng >= 3600*24*30 and rng < 3600*24*30*24: + string = '%b' + label1 = '%Y -' + label2 = ' %Y' + elif rng >=3600*24*30*24: + string = '%Y' + label1 = '' + label2 = '' + for x in values: + try: + strns.append(time.strftime(string, time.localtime(x))) + except ValueError: ## Windows can't handle dates before 1970 + strns.append('') + try: + label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values))) + except ValueError: + label = '' + #self.setLabel(text=label) + return strns + +class CustomViewBox(pg.ViewBox): + def __init__(self, *args, **kwds): + pg.ViewBox.__init__(self, *args, **kwds) + self.setMouseMode(self.RectMode) + + ## reimplement right-click to zoom out + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + self.autoRange() + + def mouseDragEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + ev.ignore() + else: + pg.ViewBox.mouseDragEvent(self, ev) + + +app = pg.mkQApp() + +axis = DateAxis(orientation='bottom') +vb = CustomViewBox() + +pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox
Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") +dates = np.arange(8) * (3600*24*356) +pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o') +pw.show() + +r = pg.PolyLineROI([(0,0), (10, 10)]) +pw.addItem(r) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/dockarea.py b/examples/dockarea.py new file mode 100644 index 00000000..8b668cf1 --- /dev/null +++ b/examples/dockarea.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of pyqtgraph's dock widget system. + +The dockarea system allows the design of user interfaces which can be rearranged by +the user at runtime. Docks can be moved, resized, stacked, and torn out of the main +window. This is similar in principle to the docking system built into Qt, but +offers a more deterministic dock placement API (in Qt it is very difficult to +programatically generate complex dock arrangements). Additionally, Qt's docks are +designed to be used as small panels around the outer edge of a window. Pyqtgraph's +docks were created with the notion that the entire window (or any portion of it) +would consist of dockable components. + +""" + + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.console +import numpy as np + +from pyqtgraph.dockarea import * + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +area = DockArea() +win.setCentralWidget(area) +win.resize(1000,500) + +## Create docks, place them into the window one at a time. +## Note that size arguments are only a suggestion; docks will still have to +## fill the entire dock area and obey the limits of their internal widgets. +d1 = Dock("Dock1", size=(1, 1)) ## give this dock the minimum possible size +d2 = Dock("Dock2 - Console", size=(500,300)) +d3 = Dock("Dock3", size=(500,400)) +d4 = Dock("Dock4 (tabbed) - Plot", size=(500,200)) +d5 = Dock("Dock5 - Image", size=(500,200)) +d6 = Dock("Dock6 (tabbed) - Plot", size=(500,200)) +area.addDock(d1, 'left') ## place d1 at left edge of dock area (it will fill the whole space since there are no other docks yet) +area.addDock(d2, 'right') ## place d2 at right edge of dock area +area.addDock(d3, 'bottom', d1)## place d3 at bottom edge of d1 +area.addDock(d4, 'right') ## place d4 at right edge of dock area +area.addDock(d5, 'left', d1) ## place d5 at left edge of d1 +area.addDock(d6, 'top', d4) ## place d5 at top edge of d4 + +## Test ability to move docks programatically after they have been placed +area.moveDock(d4, 'top', d2) ## move d4 to top edge of d2 +area.moveDock(d6, 'above', d4) ## move d6 to stack on top of d4 +area.moveDock(d5, 'top', d2) ## move d5 to top edge of d2 + + +## Add widgets into each dock + +## first dock gets save/restore buttons +w1 = pg.LayoutWidget() +label = QtGui.QLabel(""" -- DockArea Example -- +This window has 6 Dock widgets in it. Each dock can be dragged +by its title bar to occupy a different space within the window +but note that one dock has its title bar hidden). Additionally, +the borders between docks may be dragged to resize. Docks that are dragged on top +of one another are stacked in a tabbed layout. Double-click a dock title +bar to place it in its own window. +""") +saveBtn = QtGui.QPushButton('Save dock state') +restoreBtn = QtGui.QPushButton('Restore dock state') +restoreBtn.setEnabled(False) +w1.addWidget(label, row=0, col=0) +w1.addWidget(saveBtn, row=1, col=0) +w1.addWidget(restoreBtn, row=2, col=0) +d1.addWidget(w1) +state = None +def save(): + global state + state = area.saveState() + restoreBtn.setEnabled(True) +def load(): + global state + area.restoreState(state) +saveBtn.clicked.connect(save) +restoreBtn.clicked.connect(load) + + +w2 = pg.console.ConsoleWidget() +d2.addWidget(w2) + +## Hide title bar on dock 3 +d3.hideTitleBar() +w3 = pg.PlotWidget(title="Plot inside dock with no title bar") +w3.plot(np.random.normal(size=100)) +d3.addWidget(w3) + +w4 = pg.PlotWidget(title="Dock 4 plot") +w4.plot(np.random.normal(size=100)) +d4.addWidget(w4) + +w5 = pg.ImageView() +w5.setImage(np.random.normal(size=(100,100))) +d5.addWidget(w5) + +w6 = pg.PlotWidget(title="Dock 6 plot") +w6.plot(np.random.normal(size=100)) +d6.addWidget(w6) + + + +win.show() + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui new file mode 100644 index 00000000..cd5ce921 --- /dev/null +++ b/examples/exampleLoaderTemplate.ui @@ -0,0 +1,113 @@ + + + Form + + + + 0 + 0 + 762 + 302 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + false + + + + 1 + + + + + + + + + + Force PyQt + + + + + + + Force PySide + + + + + + + + + + + Force Graphics System: + + + + + + + + native + + + + + raster + + + + + opengl + + + + + + + + + + Load Example + + + + + + + + + Monospace + 10 + + + + + + + + + + diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py new file mode 100644 index 00000000..f359cc32 --- /dev/null +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui' +# +# Created: Mon Dec 24 00:33:38 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(762, 302) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) + self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.exampleTree = QtGui.QTreeWidget(self.layoutWidget) + self.exampleTree.setObjectName(_fromUtf8("exampleTree")) + self.exampleTree.headerItem().setText(0, _fromUtf8("1")) + self.exampleTree.header().setVisible(False) + self.verticalLayout.addWidget(self.exampleTree) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.pyqtCheck = QtGui.QCheckBox(self.layoutWidget) + self.pyqtCheck.setObjectName(_fromUtf8("pyqtCheck")) + self.horizontalLayout.addWidget(self.pyqtCheck) + self.pysideCheck = QtGui.QCheckBox(self.layoutWidget) + self.pysideCheck.setObjectName(_fromUtf8("pysideCheck")) + self.horizontalLayout.addWidget(self.pysideCheck) + self.verticalLayout.addLayout(self.horizontalLayout) + self.horizontalLayout_2 = QtGui.QHBoxLayout() + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.forceGraphicsCheck = QtGui.QCheckBox(self.layoutWidget) + self.forceGraphicsCheck.setObjectName(_fromUtf8("forceGraphicsCheck")) + self.horizontalLayout_2.addWidget(self.forceGraphicsCheck) + self.forceGraphicsCombo = QtGui.QComboBox(self.layoutWidget) + self.forceGraphicsCombo.setObjectName(_fromUtf8("forceGraphicsCombo")) + self.forceGraphicsCombo.addItem(_fromUtf8("")) + self.forceGraphicsCombo.addItem(_fromUtf8("")) + self.forceGraphicsCombo.addItem(_fromUtf8("")) + self.horizontalLayout_2.addWidget(self.forceGraphicsCombo) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.loadBtn = QtGui.QPushButton(self.layoutWidget) + self.loadBtn.setObjectName(_fromUtf8("loadBtn")) + self.verticalLayout.addWidget(self.loadBtn) + self.codeView = QtGui.QTextBrowser(self.splitter) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Monospace")) + font.setPointSize(10) + self.codeView.setFont(font) + self.codeView.setObjectName(_fromUtf8("codeView")) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8)) + self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8)) + self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/examples/exampleLoaderTemplate_pyside.py b/examples/exampleLoaderTemplate_pyside.py new file mode 100644 index 00000000..113c1654 --- /dev/null +++ b/examples/exampleLoaderTemplate_pyside.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui' +# +# Created: Mon Dec 24 00:33:39 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.2 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(762, 302) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.exampleTree = QtGui.QTreeWidget(self.layoutWidget) + self.exampleTree.setObjectName("exampleTree") + self.exampleTree.headerItem().setText(0, "1") + self.exampleTree.header().setVisible(False) + self.verticalLayout.addWidget(self.exampleTree) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.pyqtCheck = QtGui.QCheckBox(self.layoutWidget) + self.pyqtCheck.setObjectName("pyqtCheck") + self.horizontalLayout.addWidget(self.pyqtCheck) + self.pysideCheck = QtGui.QCheckBox(self.layoutWidget) + self.pysideCheck.setObjectName("pysideCheck") + self.horizontalLayout.addWidget(self.pysideCheck) + self.verticalLayout.addLayout(self.horizontalLayout) + self.horizontalLayout_2 = QtGui.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.forceGraphicsCheck = QtGui.QCheckBox(self.layoutWidget) + self.forceGraphicsCheck.setObjectName("forceGraphicsCheck") + self.horizontalLayout_2.addWidget(self.forceGraphicsCheck) + self.forceGraphicsCombo = QtGui.QComboBox(self.layoutWidget) + self.forceGraphicsCombo.setObjectName("forceGraphicsCombo") + self.forceGraphicsCombo.addItem("") + self.forceGraphicsCombo.addItem("") + self.forceGraphicsCombo.addItem("") + self.horizontalLayout_2.addWidget(self.forceGraphicsCombo) + self.verticalLayout.addLayout(self.horizontalLayout_2) + self.loadBtn = QtGui.QPushButton(self.layoutWidget) + self.loadBtn.setObjectName("loadBtn") + self.verticalLayout.addWidget(self.loadBtn) + self.codeView = QtGui.QTextBrowser(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + font.setPointSize(10) + self.codeView.setFont(font) + self.codeView.setObjectName("codeView") + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8)) + self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) + self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8)) + self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/examples/histogram.py b/examples/histogram.py new file mode 100644 index 00000000..fdde7da1 --- /dev/null +++ b/examples/histogram.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +In this example we draw two different kinds of histogram. +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +win = pg.GraphicsWindow() +win.resize(800,350) +plt1 = win.addPlot() +plt2 = win.addPlot() + +## make interesting distribution of values +vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)]) + +## draw standard histogram +y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) + +## notice that len(x) == len(y)+1 +## We are required to use stepMode=True so that PlotCurveItem will interpret this data correctly. +curve = pg.PlotCurveItem(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 80)) +plt1.addItem(curve) + + +## Now draw all points as a nicely-spaced scatter plot +y = pg.pseudoScatter(vals, spacing=0.15) +#plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5) +plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150)) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/initExample.py b/examples/initExample.py new file mode 100644 index 00000000..f95a0cb0 --- /dev/null +++ b/examples/initExample.py @@ -0,0 +1,23 @@ +## make this version of pyqtgraph importable before any others +import sys, os +path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +path.rstrip(os.path.sep) +if path.endswith('pyqtgraph'): + sys.path.insert(0, os.path.join(path, '..')) ## examples installed inside pyqtgraph package +elif 'pyqtgraph' in os.listdir(path): + sys.path.insert(0, path) ## examples adjacent to pyqtgraph (as in source) + +## should force example to use PySide instead of PyQt +if 'pyside' in sys.argv: + from PySide import QtGui +elif 'pyqt' in sys.argv: + from PyQt4 import QtGui +else: + from pyqtgraph.Qt import QtGui + +## Force use of a specific graphics system +for gs in ['raster', 'native', 'opengl']: + if gs in sys.argv: + QtGui.QApplication.setGraphicsSystem(gs) + break + diff --git a/examples/isocurve.py b/examples/isocurve.py new file mode 100644 index 00000000..14a3e56a --- /dev/null +++ b/examples/isocurve.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +""" +Tests use of IsoCurve item displayed with image +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +import scipy.ndimage as ndi + +app = QtGui.QApplication([]) + +## make pretty looping data +frames = 200 +data = np.random.normal(size=(frames,30,30), loc=0, scale=100) +data = np.concatenate([data, data], axis=0) +data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2] +data[:, 15:16, 15:17] += 1 + +win = pg.GraphicsWindow() +vb = win.addViewBox() +img = pg.ImageItem(data[0]) +vb.addItem(img) +vb.setAspectLocked() + +## generate empty curves +curves = [] +levels = np.linspace(data.min(), data.max(), 10) +for i in range(len(levels)): + v = levels[i] + ## generate isocurve with automatic color selection + c = pg.IsocurveItem(level=v, pen=(i, len(levels)*1.5)) + c.setParentItem(img) ## make sure isocurve is always correctly displayed over image + c.setZValue(10) + curves.append(c) + +## animate! +ptr = 0 +imgLevels = (data.min(), data.max() * 2) +def update(): + global data, curves, img, ptr, imgLevels + ptr = (ptr + 1) % data.shape[0] + data[ptr] + img.setImage(data[ptr], levels=imgLevels) + for c in curves: + c.setData(data[ptr]) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/linkedViews.py b/examples/linkedViews.py new file mode 100644 index 00000000..9d9cd7f5 --- /dev/null +++ b/examples/linkedViews.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +## This example demonstrates the ability to link the axes of views together +## Views can be linked manually using the context menu, but only if they are given names. + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +x = np.linspace(-50, 50, 1000) +y = np.sin(x) / x + +win = pg.GraphicsWindow(title="View Linking Examples") +win.resize(800,600) + +win.addLabel("Linked Views", colspan=2) +win.nextRow() + +p1 = win.addPlot(x=x, y=y, name="Plot1", title="Plot1") +p2 = win.addPlot(x=x, y=y, name="Plot2", title="Plot2: Y linked with Plot1") +p2.setLabel('bottom', "Label to test offset") +p2.setYLink('Plot1') ## test linking by name + + +## create plots 3 and 4 out of order +p4 = win.addPlot(x=x, y=y, name="Plot4", title="Plot4: X -> Plot3 (deferred), Y -> Plot1", row=2, col=1) +p4.setXLink('Plot3') ## Plot3 has not been created yet, but this should still work anyway. +p4.setYLink(p1) +p3 = win.addPlot(x=x, y=y, name="Plot3", title="Plot3: X linked with Plot1", row=2, col=0) +p3.setXLink(p1) +p3.setLabel('left', "Label to test offset") +#QtGui.QApplication.processEvents() + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + diff --git a/examples/logAxis.py b/examples/logAxis.py new file mode 100644 index 00000000..77ee66e5 --- /dev/null +++ b/examples/logAxis.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import initExample ## Add path to library (just for examples; you do not need this) + +import numpy as np +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) + +w = pg.GraphicsWindow() +p1 = w.addPlot(0,0, title="X Semilog") +p2 = w.addPlot(1,0, title="Y Semilog") +p3 = w.addPlot(2,0, title="XY Log") +p1.showGrid(True, True) +p2.showGrid(True, True) +p3.showGrid(True, True) +p1.setLogMode(True, False) +p2.setLogMode(False, True) +p3.setLogMode(True, True) +w.show() + +y = np.random.normal(size=1000) +x = np.linspace(0, 1, 1000) +p1.plot(x, y) +p2.plot(x, y) +p3.plot(x, y) + + + +#p.getAxis('bottom').setLogMode(True) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/multiprocess.py b/examples/multiprocess.py new file mode 100644 index 00000000..0b2d7ed8 --- /dev/null +++ b/examples/multiprocess.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) +import numpy as np +import pyqtgraph.multiprocess as mp +import pyqtgraph as pg +import time + + + + +print "\n=================\nStart Process" +proc = mp.Process() +import os +print "parent:", os.getpid(), "child:", proc.proc.pid +print "started" +rnp = proc._import('numpy') +arr = rnp.array([1,2,3,4]) +print repr(arr) +print str(arr) +print "return value:", repr(arr.mean(_returnType='value')) +print "return proxy:", repr(arr.mean(_returnType='proxy')) +print "return auto: ", repr(arr.mean(_returnType='auto')) +proc.join() +print "process finished" + + + +print "\n=================\nStart ForkedProcess" +proc = mp.ForkedProcess() +rnp = proc._import('numpy') +arr = rnp.array([1,2,3,4]) +print repr(arr) +print str(arr) +print repr(arr.mean()) +proc.join() +print "process finished" + + + + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +app = pg.QtGui.QApplication([]) + +print "\n=================\nStart QtProcess" +proc = mp.QtProcess() +d1 = proc.transfer(np.random.normal(size=1000)) +d2 = proc.transfer(np.random.normal(size=1000)) +rpg = proc._import('pyqtgraph') +plt = rpg.plot(d1+d2) + + +## Start Qt event loop unless running in interactive mode or using pyside. +#import sys +#if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + #QtGui.QApplication.instance().exec_() diff --git a/examples/parallelize.py b/examples/parallelize.py new file mode 100644 index 00000000..d2ba0ce0 --- /dev/null +++ b/examples/parallelize.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) +import numpy as np +import pyqtgraph.multiprocess as mp +import pyqtgraph as pg +import time + +print "\n=================\nParallelize" + +## Do a simple task: +## for x in range(N): +## sum([x*i for i in range(M)]) +## +## We'll do this three times +## - once without Parallelize +## - once with Parallelize, but forced to use a single worker +## - once with Parallelize automatically determining how many workers to use +## + +tasks = range(10) +results = [None] * len(tasks) +results2 = results[:] +results3 = results[:] +size = 2000000 + +pg.mkQApp() + +### Purely serial processing +start = time.time() +with pg.ProgressDialog('processing serially..', maximum=len(tasks)) as dlg: + for i, x in enumerate(tasks): + tot = 0 + for j in xrange(size): + tot += j * x + results[i] = tot + dlg += 1 + if dlg.wasCanceled(): + raise Exception('processing canceled') +print "Serial time: %0.2f" % (time.time() - start) + +### Use parallelize, but force a single worker +### (this simulates the behavior seen on windows, which lacks os.fork) +start = time.time() +with mp.Parallelize(enumerate(tasks), results=results2, workers=1, progressDialog='processing serially (using Parallelizer)..') as tasker: + for i, x in tasker: + tot = 0 + for j in xrange(size): + tot += j * x + tasker.results[i] = tot +print "\nParallel time, 1 worker: %0.2f" % (time.time() - start) +print "Results match serial: ", results2 == results + +### Use parallelize with multiple workers +start = time.time() +with mp.Parallelize(enumerate(tasks), results=results3, progressDialog='processing in parallel..') as tasker: + for i, x in tasker: + tot = 0 + for j in xrange(size): + tot += j * x + tasker.results[i] = tot +print "\nParallel time, %d workers: %0.2f" % (mp.Parallelize.suggestedWorkerCount(), time.time() - start) +print "Results match serial: ", results3 == results + diff --git a/examples/parametertree.py b/examples/parametertree.py new file mode 100644 index 00000000..243fd0fe --- /dev/null +++ b/examples/parametertree.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates the use of pyqtgraph's parametertree system. This provides +a simple way to generate user interfaces that control sets of parameters. The example +demonstrates a variety of different parameter types (int, float, list, etc.) +as well as some customized parameter types + +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + + +app = QtGui.QApplication([]) +import pyqtgraph.parametertree.parameterTypes as pTypes +from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType + + +## test subclassing parameters +## This parameter automatically generates two child parameters which are always reciprocals of each other +class ComplexParameter(pTypes.GroupParameter): + def __init__(self, **opts): + opts['type'] = 'bool' + opts['value'] = True + pTypes.GroupParameter.__init__(self, **opts) + + self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True}) + self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True}) + self.a = self.param('A = 1/B') + self.b = self.param('B = 1/A') + self.a.sigValueChanged.connect(self.aChanged) + self.b.sigValueChanged.connect(self.bChanged) + + def aChanged(self): + self.b.setValue(1.0 / self.a.value(), blockSignal=self.bChanged) + + def bChanged(self): + self.a.setValue(1.0 / self.b.value(), blockSignal=self.aChanged) + + +## test add/remove +## this group includes a menu allowing the user to add new parameters into its child list +class ScalableGroup(pTypes.GroupParameter): + def __init__(self, **opts): + opts['type'] = 'group' + opts['addText'] = "Add" + opts['addList'] = ['str', 'float', 'int'] + pTypes.GroupParameter.__init__(self, **opts) + + def addNew(self, typ): + val = { + 'str': '', + 'float': 0.0, + 'int': 0 + }[typ] + self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True)) + + + + +params = [ + {'name': 'Basic parameter data types', 'type': 'group', 'children': [ + {'name': 'Integer', 'type': 'int', 'value': 10}, + {'name': 'Float', 'type': 'float', 'value': 10.5, 'step': 0.1}, + {'name': 'String', 'type': 'str', 'value': "hi"}, + {'name': 'List', 'type': 'list', 'values': [1,2,3], 'value': 2}, + {'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, + {'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, + {'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"}, + {'name': 'Subgroup', 'type': 'group', 'children': [ + {'name': 'Sub-param 1', 'type': 'int', 'value': 10}, + {'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6}, + ]}, + {'name': 'Text Parameter', 'type': 'text', 'value': 'Some text...'}, + {'name': 'Action Parameter', 'type': 'action'}, + ]}, + {'name': 'Numerical Parameter Options', 'type': 'group', 'children': [ + {'name': 'Units + SI prefix', 'type': 'float', 'value': 1.2e-6, 'step': 1e-6, 'siPrefix': True, 'suffix': 'V'}, + {'name': 'Limits (min=7;max=15)', 'type': 'int', 'value': 11, 'limits': (7, 15), 'default': -6}, + {'name': 'DEC stepping', 'type': 'float', 'value': 1.2e6, 'dec': True, 'step': 1, 'siPrefix': True, 'suffix': 'Hz'}, + + ]}, + {'name': 'Save/Restore functionality', 'type': 'group', 'children': [ + {'name': 'Save State', 'type': 'action'}, + {'name': 'Restore State', 'type': 'action', 'children': [ + {'name': 'Add missing items', 'type': 'bool', 'value': True}, + {'name': 'Remove extra items', 'type': 'bool', 'value': True}, + ]}, + ]}, + {'name': 'Extra Parameter Options', 'type': 'group', 'children': [ + {'name': 'Read-only', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True}, + {'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}, + ]}, + ComplexParameter(name='Custom parameter group (reciprocal values)'), + ScalableGroup(name="Expandable Parameter Group", children=[ + {'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"}, + {'name': 'ScalableParam 2', 'type': 'str', 'value': "default param 2"}, + ]), +] + +## Create tree of Parameter objects +p = Parameter.create(name='params', type='group', children=params) + +## If anything changes in the tree, print a message +def change(param, changes): + print("tree changes:") + for param, change, data in changes: + path = p.childPath(param) + if path is not None: + childName = '.'.join(path) + else: + childName = param.name() + print(' parameter: %s'% childName) + print(' change: %s'% change) + print(' data: %s'% str(data)) + print(' ----------') + +p.sigTreeStateChanged.connect(change) + + +def save(): + global state + state = p.saveState() + +def restore(): + global state + add = p['Save/Restore functionality', 'Restore State', 'Add missing items'] + rem = p['Save/Restore functionality', 'Restore State', 'Remove extra items'] + p.restoreState(state, addChildren=add, removeChildren=rem) +p.param('Save/Restore functionality', 'Save State').sigActivated.connect(save) +p.param('Save/Restore functionality', 'Restore State').sigActivated.connect(restore) + + +## Create two ParameterTree widgets, both accessing the same data +t = ParameterTree() +t.setParameters(p, showTop=False) +t.show() +t.resize(400,800) +t2 = ParameterTree() +t2.setParameters(p, showTop=False) +t2.show() +t2.resize(400,800) + +## test save/restore +s = p.saveState() +p.restoreState(s) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/template.py b/examples/template.py new file mode 100644 index 00000000..76b14361 --- /dev/null +++ b/examples/template.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/test_Arrow.py b/examples/test_Arrow.py deleted file mode 100755 index 7d0c4aad..00000000 --- a/examples/test_Arrow.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - - -import numpy as np -from PyQt4 import QtGui, QtCore -import pyqtgraph as pg - - -app = QtGui.QApplication([]) -mw = QtGui.QMainWindow() -mw.resize(800,800) - -p = pg.PlotWidget() -mw.setCentralWidget(p) -c = p.plot(x=np.sin(np.linspace(0, 2*np.pi, 100)), y=np.cos(np.linspace(0, 2*np.pi, 100))) -a = pg.CurveArrow(c) -p.addItem(a) - -mw.show() - -anim = a.makeAnimation(loop=-1) -anim.start() - -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() diff --git a/examples/test_ImageItem.py b/examples/test_ImageItem.py deleted file mode 100755 index f48f0f51..00000000 --- a/examples/test_ImageItem.py +++ /dev/null @@ -1,68 +0,0 @@ -# -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - - -from PyQt4 import QtCore, QtGui -import numpy as np -import pyqtgraph as pg - -app = QtGui.QApplication([]) - -## Create window with GraphicsView widget -win = QtGui.QMainWindow() -win.resize(800,800) -view = pg.GraphicsView() -#view.useOpenGL(True) -win.setCentralWidget(view) -win.show() - -## Allow mouse scale/pan -view.enableMouse() - -## ..But lock the aspect ratio -view.setAspectLocked(True) - -## Create image item -img = pg.ImageItem() -view.scene().addItem(img) - -## Set initial view bounds -view.setRange(QtCore.QRectF(0, 0, 200, 200)) - -## Create random image -data = np.random.normal(size=(50, 200, 200)) -i = 0 - -def updateData(): - global img, data, i - - ## Display the data - img.updateImage(data[i]) - i = (i+1) % data.shape[0] - - QtCore.QTimer.singleShot(20, updateData) - - -# update image data every 20ms (or so) -#t = QtCore.QTimer() -#t.timeout.connect(updateData) -#t.start(20) -updateData() - - -def doWork(): - while True: - x = '.'.join(['%f'%i for i in range(100)]) ## some work for the thread to do - if time is None: ## main thread has started cleaning up, bail out now - break - time.sleep(1e-3) - -import thread -thread.start_new_thread(doWork, ()) - - -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() diff --git a/examples/test_draw.py b/examples/test_draw.py deleted file mode 100755 index b40932ba..00000000 --- a/examples/test_draw.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - - -from PyQt4 import QtCore, QtGui -import numpy as np -import pyqtgraph as pg - -app = QtGui.QApplication([]) - -## Create window with GraphicsView widget -win = QtGui.QMainWindow() -win.resize(800,800) -view = pg.GraphicsView() -#view.useOpenGL(True) -win.setCentralWidget(view) -win.show() - -## Allow mouse scale/pan -view.enableMouse() - -## ..But lock the aspect ratio -view.setAspectLocked(True) - -## Create image item -img = pg.ImageItem(np.zeros((200,200))) -view.scene().addItem(img) - -## Set initial view bounds -view.setRange(QtCore.QRectF(0, 0, 200, 200)) - -img.setDrawKernel(1) -img.setLevels(10,0) - -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() diff --git a/examples/test_scatterPlot.py b/examples/test_scatterPlot.py deleted file mode 100755 index e8d91eea..00000000 --- a/examples/test_scatterPlot.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -import sys, os -## Add path to library (just for examples; you do not need this) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -from PyQt4 import QtGui, QtCore -import pyqtgraph as pg -import numpy as np - -#QtGui.QApplication.setGraphicsSystem('raster') -app = QtGui.QApplication([]) - -mw = QtGui.QMainWindow() -mw.resize(800,800) -cw = QtGui.QWidget() -layout = QtGui.QGridLayout() -cw.setLayout(layout) -mw.setCentralWidget(cw) - -w1 = pg.PlotWidget() -layout.addWidget(w1, 0,0) - -w2 = pg.PlotWidget() -layout.addWidget(w2, 1,0) - -w3 = pg.GraphicsView() -w3.enableMouse() -w3.aspectLocked = True -layout.addWidget(w3, 0,1) - -w4 = pg.PlotWidget() -#vb = pg.ViewBox() -#w4.setCentralItem(vb) -layout.addWidget(w4, 1,1) - -mw.show() - - -n = 3000 -s1 = pg.ScatterPlotItem(size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) -pos = np.random.normal(size=(2,n), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] -s1.addPoints(spots) -w1.addDataItem(s1) - -def clicked(plot, points): - print "clicked points", points - -s1.sigClicked.connect(clicked) - - -s2 = pg.ScatterPlotItem(pxMode=False) -spots2 = [] -for i in range(10): - for j in range(10): - spots2.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) -s2.addPoints(spots2) -w2.addDataItem(s2) - -s2.sigClicked.connect(clicked) - - -s3 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) -pos = np.random.normal(size=(2,3000), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, 3000)} for i in range(3000)] -s3.addPoints(spots) -w3.addItem(s3) -w3.setRange(s3.boundingRect()) -s3.sigClicked.connect(clicked) - - -s4 = pg.ScatterPlotItem(identical=True, size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) -#pos = np.random.normal(size=(2,n), scale=1e-5) -#spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] -s4.addPoints(spots) -w4.addDataItem(s4) - - -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() - diff --git a/examples/text.py b/examples/text.py new file mode 100644 index 00000000..f9300064 --- /dev/null +++ b/examples/text.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +## This example shows how to insert text into a scene using QTextItem + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +x = np.linspace(-20, 20, 1000) +y = np.sin(x) / x +plot = pg.plot() ## create an empty plot widget +plot.setYRange(-1, 2) +curve = plot.plot(x,y) ## add a single curve + +## Create text object, use HTML tags to specify color/size +text = pg.TextItem(html='
This is the
PEAK
', anchor=(-0.3,1.3), border='w', fill=(0, 0, 255, 100)) +plot.addItem(text) +text.setPos(0, y.max()) + +## Draw an arrowhead next to the text box +arrow = pg.ArrowItem(pos=(0, y.max()), angle=-45) +plot.addItem(arrow) + + +## Set up an animated arrow and text that track the curve +curvePoint = pg.CurvePoint(curve) +plot.addItem(curvePoint) +text2 = pg.TextItem("test", anchor=(0.5, -1.0)) +text2.setParentItem(curvePoint) +arrow2 = pg.ArrowItem(angle=90) +arrow2.setParentItem(curvePoint) + +## update position every 10ms +index = 0 +def update(): + global curvePoint, index + index = (index + 1) % len(x) + curvePoint.setPos(float(index)/(len(x)-1)) + #text2.viewRangeChanged() + text2.setText('[%0.1f, %0.1f]' % (x[index], y[index])) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(10) + + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/functions.py b/functions.py deleted file mode 100644 index e5b6a41b..00000000 --- a/functions.py +++ /dev/null @@ -1,259 +0,0 @@ -# -*- coding: utf-8 -*- -""" -functions.py - Miscellaneous functions with no other home -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. -""" - -colorAbbrev = { - 'b': (0,0,255,255), - 'g': (0,255,0,255), - 'r': (255,0,0,255), - 'c': (0,255,255,255), - 'm': (255,0,255,255), - 'y': (255,255,0,255), - 'k': (0,0,0,255), - 'w': (255,255,255,255), -} - - -from PyQt4 import QtGui, QtCore -import numpy as np -import scipy.ndimage - -## Copied from acq4/lib/util/functions -SI_PREFIXES = u'yzafpnµm kMGTPEZY' -def siScale(x, minVal=1e-25): - """Return the recommended scale factor and SI prefix string for x.""" - if abs(x) < minVal: - m = 0 - x = 0 - else: - m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) - if m == 0: - pref = '' - elif m < -8 or m > 8: - pref = 'e%d' % (m*3) - else: - pref = SI_PREFIXES[m+8] - p = .001**m - return (p, pref) - -def mkBrush(color): - if isinstance(color, QtGui.QBrush): - return color - return QtGui.QBrush(mkColor(color)) - -def mkPen(arg='default', color=None, width=1, style=None, cosmetic=True, hsv=None, ): - """Convenience function for making pens. Examples: - mkPen(color) - mkPen(color, width=2) - mkPen(cosmetic=False, width=4.5, color='r') - mkPen({'color': "FF0", width: 2}) - mkPen(None) (no pen) - """ - if isinstance(arg, dict): - return mkPen(**arg) - elif arg != 'default': - if isinstance(arg, QtGui.QPen): - return arg - elif arg is None: - style = QtCore.Qt.NoPen - else: - color = arg - - if color is None: - color = mkColor(200, 200, 200) - if hsv is not None: - color = hsvColor(*hsv) - else: - color = mkColor(color) - - pen = QtGui.QPen(QtGui.QBrush(color), width) - pen.setCosmetic(cosmetic) - if style is not None: - pen.setStyle(style) - return pen - -def hsvColor(h, s=1.0, v=1.0, a=1.0): - c = QtGui.QColor() - c.setHsvF(h, s, v, a) - return c - -def mkColor(*args): - """make a QColor from a variety of argument types - accepted types are: - r, g, b, [a] - (r, g, b, [a]) - float (greyscale, 0.0-1.0) - int (uses intColor) - (int, hues) (uses intColor) - QColor - "c" (see colorAbbrev dictionary) - "RGB" (strings may optionally begin with "#") - "RGBA" - "RRGGBB" - "RRGGBBAA" - """ - err = 'Not sure how to make a color from "%s"' % str(args) - if len(args) == 1: - if isinstance(args[0], QtGui.QColor): - return QtGui.QColor(args[0]) - elif isinstance(args[0], float): - r = g = b = int(args[0] * 255) - a = 255 - elif isinstance(args[0], basestring): - c = args[0] - if c[0] == '#': - c = c[1:] - if len(c) == 1: - (r, g, b, a) = colorAbbrev[c] - if len(c) == 3: - r = int(c[0]*2, 16) - g = int(c[1]*2, 16) - b = int(c[2]*2, 16) - a = 255 - elif len(c) == 4: - r = int(c[0]*2, 16) - g = int(c[1]*2, 16) - b = int(c[2]*2, 16) - a = int(c[3]*2, 16) - elif len(c) == 6: - r = int(c[0:2], 16) - g = int(c[2:4], 16) - b = int(c[4:6], 16) - a = 255 - elif len(c) == 8: - r = int(c[0:2], 16) - g = int(c[2:4], 16) - b = int(c[4:6], 16) - a = int(c[6:8], 16) - elif hasattr(args[0], '__len__'): - if len(args[0]) == 3: - (r, g, b) = args[0] - a = 255 - elif len(args[0]) == 4: - (r, g, b, a) = args[0] - elif len(args[0]) == 2: - return intColor(*args[0]) - else: - raise Exception(err) - elif type(args[0]) == int: - return intColor(args[0]) - else: - raise Exception(err) - elif len(args) == 3: - (r, g, b) = args - a = 255 - elif len(args) == 4: - (r, g, b, a) = args - else: - raise Exception(err) - return QtGui.QColor(r, g, b, a) - -def colorTuple(c): - return (c.red(), c.green(), c.blue(), c.alpha()) - -def colorStr(c): - """Generate a hex string code from a QColor""" - return ('%02x'*4) % colorTuple(c) - -def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): - """Creates a QColor from a single index. Useful for stepping through a predefined list of colors. - - The argument "index" determines which color from the set will be returned - - All other arguments determine what the set of predefined colors will be - - Colors are chosen by cycling across hues while varying the value (brightness). By default, there - are 9 hues and 3 values for a total of 27 different colors. """ - hues = int(hues) - values = int(values) - ind = int(index) % (hues * values) - indh = ind % hues - indv = ind / hues - if values > 1: - v = minValue + indv * ((maxValue-minValue) / (values-1)) - else: - v = maxValue - h = minHue + (indh * (maxHue-minHue)) / hues - - c = QtGui.QColor() - c.setHsv(h, sat, v) - c.setAlpha(alpha) - return c - - -def affineSlice(data, shape, origin, vectors, axes, **kargs): - """Take an arbitrary slice through an array. - Parameters: - data: the original dataset - shape: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) - origin: the location in the original dataset that will become the origin in the sliced data. - vectors: list of unit vectors which point in the direction of the slice axes - each vector must be the same length as axes - If the vectors are not unit length, the result will be scaled. - If the vectors are not orthogonal, the result will be sheared. - axes: the axes in the original dataset which correspond to the slice vectors - - Example: start with a 4D data set, take a diagonal-planar slice out of the last 3 axes - - data = array with dims (time, x, y, z) = (100, 40, 40, 40) - - The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) - - The origin of the slice will be at (x,y,z) = (40, 0, 0) - - The we will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) - affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) - - Note the following: - len(shape) == len(vectors) - len(origin) == len(axes) == len(vectors[0]) - """ - - # sanity check - if len(shape) != len(vectors): - raise Exception("shape and vectors must have same length.") - if len(origin) != len(axes): - raise Exception("origin and axes must have same length.") - for v in vectors: - if len(v) != len(axes): - raise Exception("each vector must be same length as axes.") - shape = (np.ceil(shape[0]), np.ceil(shape[1])) - - ## transpose data so slice axes come first - trAx = range(data.ndim) - for x in axes: - trAx.remove(x) - tr1 = tuple(axes) + tuple(trAx) - data = data.transpose(tr1) - #print "tr1:", tr1 - ## dims are now [(slice axes), (other axes)] - - - ## make sure vectors are arrays - vectors = np.array(vectors) - origin = np.array(origin) - origin.shape = (len(axes),) + (1,)*len(shape) - - ## Build array of sample locations. - grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes - #print shape, grid.shape - x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic - x += origin - #print "X values:" - #print x - ## iterate manually over unused axes since map_coordinates won't do it for us - extraShape = data.shape[len(axes):] - output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) - for inds in np.ndindex(*extraShape): - ind = (Ellipsis,) + inds - #print data[ind].shape, x.shape, output[ind].shape, output.shape - output[ind] = scipy.ndimage.map_coordinates(data[ind], x, **kargs) - - tr = range(output.ndim) - trb = [] - for i in range(min(axes)): - ind = tr1.index(i) + (len(shape)-len(axes)) - tr.remove(ind) - trb.append(ind) - tr2 = tuple(trb+tr) - - ## Untranspose array before returning - return output.transpose(tr2) - diff --git a/graphicsItems.py b/graphicsItems.py deleted file mode 100644 index 63de57e0..00000000 --- a/graphicsItems.py +++ /dev/null @@ -1,2997 +0,0 @@ -# -*- coding: utf-8 -*- -""" -graphicsItems.py - Defines several graphics item classes for use in Qt graphics/view framework -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -Provides ImageItem, PlotCurveItem, and ViewBox, amongst others. -""" - - -from PyQt4 import QtGui, QtCore -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal -#from ObjectWorkaround import * -#tryWorkaround(QtCore, QtGui) -#from numpy import * -import numpy as np -try: - import scipy.weave as weave - from scipy.weave import converters -except: - pass -from scipy.fftpack import fft -#from scipy.signal import resample -import scipy.stats -#from metaarray import MetaArray -from Point import * -from functions import * -import types, sys, struct -import weakref -import debug -#from debug import * - -## QGraphicsObject didn't appear until 4.6; this is for compatibility with 4.5 -if not hasattr(QtGui, 'QGraphicsObject'): - class QGraphicsObject(QtGui.QGraphicsWidget): - def shape(self): - return QtGui.QGraphicsItem.shape(self) - QtGui.QGraphicsObject = QGraphicsObject - - -## Should probably just use QGraphicsGroupItem and instruct it to pass events on to children.. -class ItemGroup(QtGui.QGraphicsItem): - def __init__(self, *args): - QtGui.QGraphicsItem.__init__(self, *args) - if hasattr(self, "ItemHasNoContents"): - self.setFlag(self.ItemHasNoContents) - - def boundingRect(self): - return QtCore.QRectF() - - def paint(self, *args): - pass - - def addItem(self, item): - item.setParentItem(self) - - -#if hasattr(QtGui, "QGraphicsObject"): - #QGraphicsObject = QtGui.QGraphicsObject -#else: - #class QObjectWorkaround: - #def __init__(self): - #self._qObj_ = QtCore.QObject() - #def connect(self, *args): - #return QtCore.QObject.connect(self._qObj_, *args) - #def disconnect(self, *args): - #return QtCore.QObject.disconnect(self._qObj_, *args) - #def emit(self, *args): - #return QtCore.QObject.emit(self._qObj_, *args) - - #class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): - #def __init__(self, *args): - #QtGui.QGraphicsItem.__init__(self, *args) - #QObjectWorkaround.__init__(self) - - - -class GraphicsObject(QtGui.QGraphicsObject): - """Extends QGraphicsObject with a few important functions. - (Most of these assume that the object is in a scene with a single view)""" - - def __init__(self, *args): - QtGui.QGraphicsObject.__init__(self, *args) - self._view = None - - def getViewWidget(self): - """Return the view widget for this item. If the scene has multiple views, only the first view is returned. - the view is remembered for the lifetime of the object, so expect trouble if the object is moved to another view.""" - if self._view is None: - scene = self.scene() - if scene is None: - return None - views = scene.views() - if len(views) < 1: - return None - self._view = weakref.ref(self.scene().views()[0]) - return self._view() - - def getBoundingParents(self): - """Return a list of parents to this item that have child clipping enabled.""" - p = self - parents = [] - while True: - p = p.parentItem() - if p is None: - break - if p.flags() & self.ItemClipsChildrenToShape: - parents.append(p) - return parents - - def viewBounds(self): - """Return the allowed visible boundaries for this item. Takes into account the viewport as well as any parents that clip.""" - bounds = QtCore.QRectF(0, 0, 1, 1) - view = self.getViewWidget() - if view is None: - return None - bounds = self.mapRectFromScene(view.visibleRange()) - - for p in self.getBoundingParents(): - bounds &= self.mapRectFromScene(p.sceneBoundingRect()) - - return bounds - - def viewTransform(self): - """Return the transform that maps from local coordinates to the item's view coordinates""" - view = self.getViewWidget() - if view is None: - return None - return self.deviceTransform(view.viewportTransform()) - - def pixelVectors(self): - """Return vectors in local coordinates representing the width and height of a view pixel.""" - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - orig = vt.map(QtCore.QPointF(0, 0)) - return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig - - def pixelWidth(self): - vt = self.viewTransform() - if vt is None: - return 0 - vt = vt.inverted()[0] - return abs((vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).x()) - - def pixelHeight(self): - vt = self.viewTransform() - if vt is None: - return 0 - vt = vt.inverted()[0] - return abs((vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).y()) - - def mapToView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - return vt.map(obj) - - def mapRectToView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - return vt.mapRect(obj) - - def mapFromView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - return vt.map(obj) - - def mapRectFromView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - return vt.mapRect(obj) - - - - - -class ImageItem(QtGui.QGraphicsObject): - - sigImageChanged = QtCore.Signal() - - if 'linux' not in sys.platform: ## disable weave optimization on linux--broken there. - useWeave = True - else: - useWeave = False - - def __init__(self, image=None, copy=True, parent=None, border=None, mode=None, *args): - #QObjectWorkaround.__init__(self) - QtGui.QGraphicsObject.__init__(self) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - self.qimage = QtGui.QImage() - self.pixmap = None - self.paintMode = mode - #self.useWeave = True - self.blackLevel = None - self.whiteLevel = None - self.alpha = 1.0 - self.image = None - self.clipLevel = None - self.drawKernel = None - if border is not None: - border = mkPen(border) - self.border = border - - #QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - if image is not None: - self.updateImage(image, copy, autoRange=True) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - def setCompositionMode(self, mode): - self.paintMode = mode - self.update() - - def setAlpha(self, alpha): - self.alpha = alpha - self.updateImage() - - #def boundingRect(self): - #return self.pixmapItem.boundingRect() - #return QtCore.QRectF(0, 0, self.qimage.width(), self.qimage.height()) - - def width(self): - if self.pixmap is None: - return None - return self.pixmap.width() - - def height(self): - if self.pixmap is None: - return None - return self.pixmap.height() - - def boundingRect(self): - if self.pixmap is None: - return QtCore.QRectF(0., 0., 0., 0.) - return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) - - def setClipLevel(self, level=None): - self.clipLevel = level - - #def paint(self, p, opt, widget): - #pass - #if self.pixmap is not None: - #p.drawPixmap(0, 0, self.pixmap) - #print "paint" - - def setLevels(self, white=None, black=None): - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - self.updateImage() - - def getLevels(self): - return self.whiteLevel, self.blackLevel - - def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None, axes=None): - prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self), disabled=True) - #debug.printTrace() - if axes is None: - axh = {'x': 0, 'y': 1, 'c': 2} - else: - axh = axes - #print "Update image", black, white - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - - gotNewData = False - if image is None: - if self.image is None: - return - else: - gotNewData = True - if self.image is None or image.shape != self.image.shape: - self.prepareGeometryChange() - if copy: - self.image = image.view(np.ndarray).copy() - else: - self.image = image.view(np.ndarray) - #print " image max:", self.image.max(), "min:", self.image.min() - prof.mark('1') - - # Determine scale factors - if autoRange or self.blackLevel is None: - if self.image.dtype is np.ubyte: - self.blackLevel = 0 - self.whiteLevel = 255 - else: - self.blackLevel = self.image.min() - self.whiteLevel = self.image.max() - #print "Image item using", self.blackLevel, self.whiteLevel - - if self.blackLevel != self.whiteLevel: - scale = 255. / (self.whiteLevel - self.blackLevel) - else: - scale = 0. - - prof.mark('2') - - ## Recolor and convert to 8 bit per channel - # Try using weave, then fall back to python - shape = self.image.shape - black = float(self.blackLevel) - white = float(self.whiteLevel) - - if black == 0 and white == 255 and self.image.dtype == np.ubyte: - im = self.image - - else: - try: - if not ImageItem.useWeave: - raise Exception('Skipping weave compile') - sim = np.ascontiguousarray(self.image) - sim.shape = sim.size - im = np.empty(sim.shape, dtype=np.ubyte) - n = im.size - - code = """ - for( int i=0; i 255.0 ) - a = 255.0; - else if( a < 0.0 ) - a = 0.0; - im(i) = a; - } - """ - - weave.inline(code, ['sim', 'im', 'n', 'black', 'scale'], type_converters=converters.blitz, compiler = 'gcc') - sim.shape = shape - im.shape = shape - except: - if ImageItem.useWeave: - ImageItem.useWeave = False - #sys.excepthook(*sys.exc_info()) - #print "==============================================================================" - print "Weave compile failed, falling back to slower version." - self.image.shape = shape - im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte) - prof.mark('3') - - try: - im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) - except: - print im.shape, axh - raise - alpha = np.clip(int(255 * self.alpha), 0, 255) - prof.mark('4') - # Fill image - if im.ndim == 2: - im2 = im.transpose(axh['y'], axh['x']) - im1[..., 0] = im2 - im1[..., 1] = im2 - im1[..., 2] = im2 - im1[..., 3] = alpha - elif im.ndim == 3: #color image - im2 = im.transpose(axh['y'], axh['x'], axh['c']) - ## [B G R A] Reorder colors - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - - for i in range(0, im.shape[axh['c']]): - im1[..., order[i]] = im2[..., i] - - ## fill in unused channels with 0 or alpha - for i in range(im.shape[axh['c']], 3): - im1[..., i] = 0 - if im.shape[axh['c']] < 4: - im1[..., 3] = alpha - - else: - raise Exception("Image must be 2 or 3 dimensions") - #self.im1 = im1 - # Display image - prof.mark('5') - if self.clipLevel is not None or clipMask is not None: - if clipMask is not None: - mask = clipMask.transpose() - else: - mask = (self.image < self.clipLevel).transpose() - im1[..., 0][mask] *= 0.5 - im1[..., 1][mask] *= 0.5 - im1[..., 2][mask] = 255 - prof.mark('6') - #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape - self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( - prof.mark('7') - qimage = QtGui.QImage(buffer(self.ims), im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) - prof.mark('8') - self.pixmap = QtGui.QPixmap.fromImage(qimage) - prof.mark('9') - ##del self.ims - #self.pixmapItem.setPixmap(self.pixmap) - - self.update() - prof.mark('10') - - if gotNewData: - #self.emit(QtCore.SIGNAL('imageChanged')) - self.sigImageChanged.emit() - - prof.finish() - - def getPixmap(self): - return self.pixmap.copy() - - def getHistogram(self, bins=500, step=3): - """returns x and y arrays containing the histogram values for the current image. - The step argument causes pixels to be skipped when computing the histogram to save time.""" - stepData = self.image[::step, ::step] - hist = np.histogram(stepData, bins=bins) - return hist[1][:-1], hist[0] - - def mousePressEvent(self, ev): - if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - self.drawAt(ev.pos(), ev) - ev.accept() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.drawKernel is not None: - self.drawAt(ev.pos(), ev) - - def mouseReleaseEvent(self, ev): - pass - - def tabletEvent(self, ev): - print ev.device() - print ev.pointerType() - print ev.pressure() - - def drawAt(self, pos, ev=None): - pos = [int(pos.x()), int(pos.y())] - dk = self.drawKernel - kc = self.drawKernelCenter - sx = [0,dk.shape[0]] - sy = [0,dk.shape[1]] - tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] - ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] - - for i in [0,1]: - dx1 = -min(0, tx[i]) - dx2 = min(0, self.image.shape[0]-tx[i]) - tx[i] += dx1+dx2 - sx[i] += dx1+dx2 - - dy1 = -min(0, ty[i]) - dy2 = min(0, self.image.shape[1]-ty[i]) - ty[i] += dy1+dy2 - sy[i] += dy1+dy2 - - #print sx - #print sy - #print tx - #print ty - #print self.image.shape - #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape - #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape - ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) - ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) - #src = dk[sx[0]:sx[1], sy[0]:sy[1]] - #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] - mask = self.drawMask - src = dk - #print self.image[ts].shape, src.shape - - if callable(self.drawMode): - self.drawMode(dk, self.image, mask, ss, ts, ev) - else: - mask = mask[ss] - src = src[ss] - if self.drawMode == 'set': - if mask is not None: - self.image[ts] = self.image[ts] * (1-mask) + src * mask - else: - self.image[ts] = src - elif self.drawMode == 'add': - self.image[ts] += src - else: - raise Exception("Unknown draw mode '%s'" % self.drawMode) - self.updateImage() - - def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): - self.drawKernel = kernel - self.drawKernelCenter = center - self.drawMode = mode - self.drawMask = mask - - def paint(self, p, *args): - - #QtGui.QGraphicsPixmapItem.paint(self, p, *args) - if self.pixmap is None: - return - if self.paintMode is not None: - p.setCompositionMode(self.paintMode) - p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height())) - if self.border is not None: - p.setPen(self.border) - p.drawRect(self.boundingRect()) - - def pixelSize(self): - """return size of a single pixel in the image""" - br = self.sceneBoundingRect() - return br.width()/self.pixmap.width(), br.height()/self.pixmap.height() - -class PlotCurveItem(GraphicsObject): - - sigPlotChanged = QtCore.Signal(object) - - """Class representing a single plot curve.""" - - sigClicked = QtCore.Signal(object) - - def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None, clickable=False): - GraphicsObject.__init__(self, parent) - #GraphicsWidget.__init__(self, parent) - self.free() - #self.dispPath = None - - if pen is None: - if color is None: - self.setPen((200,200,200)) - else: - self.setPen(color) - else: - self.setPen(pen) - - self.shadow = shadow - if y is not None: - self.updateData(y, x, copy) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - self.metaData = {} - self.opts = { - 'spectrumMode': False, - 'logMode': [False, False], - 'pointMode': False, - 'pointStyle': None, - 'downsample': False, - 'alphaHint': 1.0, - 'alphaMode': False - } - - self.setClickable(clickable) - #self.fps = None - - def setClickable(self, s): - self.clickable = s - - - def getData(self): - if self.xData is None: - return (None, None) - if self.xDisp is None: - nanMask = np.isnan(self.xData) | np.isnan(self.yData) - if any(nanMask): - x = self.xData[~nanMask] - y = self.yData[~nanMask] - else: - x = self.xData - y = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] - if self.opts['spectrumMode']: - f = fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) - self.xDisp = x - self.yDisp = y - #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() - #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() - return self.xDisp, self.yDisp - - #def generateSpecData(self): - #f = fft(self.yData) / len(self.yData) - #self.ySpec = abs(f[1:len(f)/2]) - #dt = self.xData[-1] - self.xData[0] - #self.xSpec = linspace(0, 0.5*len(self.xData)/dt, len(self.ySpec)) - - def getRange(self, ax, frac=1.0): - #print "getRange", ax, frac - (x, y) = self.getData() - if x is None or len(x) == 0: - return (0, 1) - - if ax == 0: - d = x - elif ax == 1: - d = y - - if frac >= 1.0: - return (d.min(), d.max()) - elif frac <= 0.0: - raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) - else: - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - #bins = 1000 - #h = histogram(d, bins) - #s = len(d) * (1.0-frac) - #mnTot = mxTot = 0 - #mnInd = mxInd = 0 - #for i in range(bins): - #mnTot += h[0][i] - #if mnTot > s: - #mnInd = i - #break - #for i in range(bins): - #mxTot += h[0][-i-1] - #if mxTot > s: - #mxInd = -i-1 - #break - ##print mnInd, mxInd, h[1][mnInd], h[1][mxInd] - #return(h[1][mnInd], h[1][mxInd]) - - - - - def setMeta(self, data): - self.metaData = data - - def meta(self): - return self.metaData - - def setPen(self, pen): - self.pen = mkPen(pen) - self.update() - - def setColor(self, color): - self.pen.setColor(color) - self.update() - - def setAlpha(self, alpha, auto): - self.opts['alphaHint'] = alpha - self.opts['alphaMode'] = auto - self.update() - - def setSpectrumMode(self, mode): - self.opts['spectrumMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setLogMode(self, mode): - self.opts['logMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setPointMode(self, mode): - self.opts['pointMode'] = mode - self.update() - - def setShadowPen(self, pen): - self.shadow = pen - self.update() - - def setDownsampling(self, ds): - if self.opts['downsample'] != ds: - self.opts['downsample'] = ds - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setData(self, x, y, copy=False): - """For Qwt compatibility""" - self.updateData(y, x, copy) - - def updateData(self, data, x=None, copy=False): - prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) - if isinstance(data, list): - data = np.array(data) - if isinstance(x, list): - x = np.array(x) - if not isinstance(data, np.ndarray) or data.ndim > 2: - raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) - if x == None: - if 'complex' in str(data.dtype): - raise Exception("Can not plot complex data types.") - else: - if 'complex' in str(data.dtype)+str(x.dtype): - raise Exception("Can not plot complex data types.") - - if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. - if x is not None: - raise Exception("Plot data may be 2D only if no x argument is supplied.") - ax = 0 - if data.shape[0] > 2 and data.shape[1] == 2: - ax = 1 - ind = [slice(None), slice(None)] - ind[ax] = 0 - y = data[tuple(ind)] - ind[ax] = 1 - x = data[tuple(ind)] - elif data.ndim == 1: - y = data - prof.mark("data checks") - - self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly - ## Test this bug with test_PlotWidget and zoom in on the animated plot - - self.prepareGeometryChange() - if copy: - self.yData = y.copy() - else: - self.yData = y - - if copy and x is not None: - self.xData = x.copy() - else: - self.xData = x - prof.mark('copy') - - if x is None: - self.xData = np.arange(0, self.yData.shape[0]) - - if self.xData.shape != self.yData.shape: - raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) - - self.path = None - self.xDisp = self.yDisp = None - - prof.mark('set') - self.update() - prof.mark('update') - #self.emit(QtCore.SIGNAL('plotChanged'), self) - self.sigPlotChanged.emit(self) - prof.mark('emit') - #prof.finish() - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - prof.mark('set cache mode') - prof.finish() - - def generatePath(self, x, y): - prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) - path = QtGui.QPainterPath() - - ## Create all vertices in path. The method used below creates a binary format so that all - ## vertices can be read in at once. This binary format may change in future versions of Qt, - ## so the original (slower) method is left here for emergencies: - #path.moveTo(x[0], y[0]) - #for i in range(1, y.shape[0]): - # path.lineTo(x[i], y[i]) - - ## Speed this up using >> operator - ## Format is: - ## numVerts(i4) 0(i4) - ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect - ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex - ## ... - ## 0(i4) - ## - ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') - - n = x.shape[0] - # create empty array, pad with extra space on either end - arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) - # write first two integers - prof.mark('allocate empty') - arr.data[12:20] = struct.pack('>ii', n, 0) - prof.mark('pack header') - # Fill array with vertex values - arr[1:-1]['x'] = x - arr[1:-1]['y'] = y - arr[1:-1]['c'] = 1 - prof.mark('fill array') - # write last 0 - lastInd = 20*(n+1) - arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) - prof.mark('footer') - # create datastream object and stream into path - buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here - prof.mark('create buffer') - ds = QtCore.QDataStream(buf) - prof.mark('create datastream') - ds >> path - prof.mark('load') - - prof.finish() - return path - - def boundingRect(self): - (x, y) = self.getData() - if x is None or y is None or len(x) == 0 or len(y) == 0: - return QtCore.QRectF() - - - if self.shadow is not None: - lineWidth = (max(self.pen.width(), self.shadow.width()) + 1) - else: - lineWidth = (self.pen.width()+1) - - - pixels = self.pixelVectors() - xmin = x.min() - pixels[0].x() * lineWidth - xmax = x.max() + pixels[0].x() * lineWidth - ymin = y.min() - abs(pixels[1].y()) * lineWidth - ymax = y.max() + abs(pixels[1].y()) * lineWidth - - - return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) - - def paint(self, p, opt, widget): - prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) - if self.xData is None: - return - #if self.opts['spectrumMode']: - #if self.specPath is None: - - #self.specPath = self.generatePath(*self.getData()) - #path = self.specPath - #else: - if self.path is None: - self.path = self.generatePath(*self.getData()) - path = self.path - prof.mark('generate path') - - if self.shadow is not None: - sp = QtGui.QPen(self.shadow) - else: - sp = None - - ## Copy pens and apply alpha adjustment - cp = QtGui.QPen(self.pen) - for pen in [sp, cp]: - if pen is None: - continue - c = pen.color() - c.setAlpha(c.alpha() * self.opts['alphaHint']) - pen.setColor(c) - #pen.setCosmetic(True) - - if self.shadow is not None: - p.setPen(sp) - p.drawPath(path) - p.setPen(cp) - p.drawPath(path) - prof.mark('drawPath') - - prof.finish() - #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - #p.drawRect(self.boundingRect()) - - - def free(self): - self.xData = None ## raw values - self.yData = None - self.xDisp = None ## display values (after log / fft) - self.yDisp = None - self.path = None - #del self.xData, self.yData, self.xDisp, self.yDisp, self.path - - def mousePressEvent(self, ev): - #GraphicsObject.mousePressEvent(self, ev) - if not self.clickable: - ev.ignore() - if ev.button() != QtCore.Qt.LeftButton: - ev.ignore() - self.mousePressPos = ev.pos() - self.mouseMoved = False - - def mouseMoveEvent(self, ev): - #GraphicsObject.mouseMoveEvent(self, ev) - self.mouseMoved = True - #print "move" - - def mouseReleaseEvent(self, ev): - #GraphicsObject.mouseReleaseEvent(self, ev) - if not self.mouseMoved: - self.sigClicked.emit(self) - - -class CurvePoint(QtGui.QGraphicsObject): - """A GraphicsItem that sets its location to a point on a PlotCurveItem. - The position along the curve is a property, and thus can be easily animated.""" - - def __init__(self, curve, index=0, pos=None): - """Position can be set either as an index referring to the sample number or - the position 0.0 - 1.0""" - - QtGui.QGraphicsObject.__init__(self) - #QObjectWorkaround.__init__(self) - self.curve = weakref.ref(curve) - self.setParentItem(curve) - self.setProperty('position', 0.0) - self.setProperty('index', 0) - - if hasattr(self, 'ItemHasNoContents'): - self.setFlags(self.flags() | self.ItemHasNoContents) - - if pos is not None: - self.setPos(pos) - else: - self.setIndex(index) - - def setPos(self, pos): - self.setProperty('position', float(pos))## cannot use numpy types here, MUST be python float. - - def setIndex(self, index): - self.setProperty('index', int(index)) ## cannot use numpy types here, MUST be python int. - - def event(self, ev): - if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: - return False - - if ev.propertyName() == 'index': - index = self.property('index').toInt()[0] - elif ev.propertyName() == 'position': - index = None - else: - return False - - (x, y) = self.curve().getData() - if index is None: - #print ev.propertyName(), self.property('position').toDouble()[0], self.property('position').typeName() - index = (len(x)-1) * clip(self.property('position').toDouble()[0], 0.0, 1.0) - - if index != int(index): ## interpolate floating-point values - i1 = int(index) - i2 = clip(i1+1, 0, len(x)-1) - s2 = index-i1 - s1 = 1.0-s2 - newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2) - else: - index = int(index) - i1 = clip(index-1, 0, len(x)-1) - i2 = clip(index+1, 0, len(x)-1) - newPos = (x[index], y[index]) - - p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) - p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) - ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians - self.resetTransform() - self.rotate(180+ ang * 180 / np.pi) ## takes degrees - QtGui.QGraphicsItem.setPos(self, *newPos) - return True - - def boundingRect(self): - return QtCore.QRectF() - - def paint(self, *args): - pass - - def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): - anim = QtCore.QPropertyAnimation(self, prop) - anim.setDuration(duration) - anim.setStartValue(start) - anim.setEndValue(end) - anim.setLoopCount(loop) - return anim - - - -class ArrowItem(QtGui.QGraphicsPolygonItem): - def __init__(self, **opts): - QtGui.QGraphicsPolygonItem.__init__(self) - defOpts = { - 'style': 'tri', - 'pxMode': True, - 'size': 20, - 'angle': -150, - 'pos': (0,0), - 'width': 8, - 'tipAngle': 25, - 'baseAngle': 90, - 'pen': (200,200,200), - 'brush': (50,50,200), - } - defOpts.update(opts) - - self.setStyle(**defOpts) - - self.setPen(mkPen(defOpts['pen'])) - self.setBrush(mkBrush(defOpts['brush'])) - - self.rotate(self.opts['angle']) - self.moveBy(*self.opts['pos']) - - def setStyle(self, **opts): - self.opts = opts - - if opts['style'] == 'tri': - points = [ - QtCore.QPointF(0,0), - QtCore.QPointF(opts['size'],-opts['width']/2.), - QtCore.QPointF(opts['size'],opts['width']/2.), - ] - poly = QtGui.QPolygonF(points) - - else: - raise Exception("Unrecognized arrow style '%s'" % opts['style']) - - self.setPolygon(poly) - - if opts['pxMode']: - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - else: - self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - QtGui.QGraphicsPolygonItem.paint(self, p, *args) - -class CurveArrow(CurvePoint): - """Provides an arrow that points to any specific sample on a PlotCurveItem. - Provides properties that can be animated.""" - - def __init__(self, curve, index=0, pos=None, **opts): - CurvePoint.__init__(self, curve, index=index, pos=pos) - if opts.get('pxMode', True): - opts['pxMode'] = False - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - opts['angle'] = 0 - self.arrow = ArrowItem(**opts) - self.arrow.setParentItem(self) - - def setStyle(**opts): - return self.arrow.setStyle(**opts) - - - -class ScatterPlotItem(GraphicsObject): - - #sigPointClicked = QtCore.Signal(object, object) - sigClicked = QtCore.Signal(object, object) ## self, points - - def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=5, identical=False, data=None): - """ - Arguments: - spots: list of dicts. Each dict specifies parameters for a single spot. - x,y: array of x,y values. Alternatively, specify spots['pos'] = (x,y) - pxMode: If True, spots are always the same size regardless of scaling - identical: If True, all spots are forced to look identical. - This can result in performance enhancement.""" - GraphicsObject.__init__(self) - self.spots = [] - self.range = [[0,0], [0,0]] - self.identical = identical - self._spotPixmap = None - - if brush == 'default': - self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150)) - else: - self.brush = mkBrush(brush) - - if pen == 'default': - self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) - else: - self.pen = mkPen(pen) - - self.size = size - - self.pxMode = pxMode - if spots is not None or x is not None: - self.setPoints(spots, x, y, data) - - #self.optimize = optimize - #if optimize: - #self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied) - #self.spotImage.fill(0) - #p = QtGui.QPainter(self.spotImage) - #p.setRenderHint(p.Antialiasing) - #p.setBrush(brush) - #p.setPen(pen) - #p.drawEllipse(0, 0, size, size) - #p.end() - #self.optimizePixmap = QtGui.QPixmap(self.spotImage) - #self.optimizeFragments = [] - #self.setFlags(self.flags() | self.ItemIgnoresTransformations) - - def setPxMode(self, mode): - self.pxMode = mode - - def clear(self): - for i in self.spots: - i.setParentItem(None) - s = i.scene() - if s is not None: - s.removeItem(i) - self.spots = [] - - - def getRange(self, ax, percent): - return self.range[ax] - - def setPoints(self, spots=None, x=None, y=None, data=None): - self.clear() - self.range = [[0,0],[0,0]] - self.addPoints(spots, x, y, data) - - def addPoints(self, spots=None, x=None, y=None, data=None): - xmn = ymn = xmx = ymx = None - if spots is not None: - n = len(spots) - else: - n = len(x) - - for i in range(n): - if spots is not None: - s = spots[i] - pos = Point(s['pos']) - else: - s = {} - pos = Point(x[i], y[i]) - if data is not None: - s['data'] = data[i] - - size = s.get('size', self.size) - if self.pxMode: - psize = 0 - else: - psize = size - if xmn is None: - xmn = pos[0]-psize - xmx = pos[0]+psize - ymn = pos[1]-psize - ymx = pos[1]+psize - else: - xmn = min(xmn, pos[0]-psize) - xmx = max(xmx, pos[0]+psize) - ymn = min(ymn, pos[1]-psize) - ymx = max(ymx, pos[1]+psize) - #print pos, xmn, xmx, ymn, ymx - brush = s.get('brush', self.brush) - pen = s.get('pen', self.pen) - pen.setCosmetic(True) - data2 = s.get('data', None) - item = self.mkSpot(pos, size, self.pxMode, brush, pen, data2, index=len(self.spots)) - self.spots.append(item) - #if self.optimize: - #item.hide() - #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size)) - #self.optimizeFragments.append(frag) - self.range = [[xmn, xmx], [ymn, ymx]] - - #def paint(self, p, *args): - #if not self.optimize: - #return - ##p.setClipRegion(self.boundingRect()) - #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap) - - def paint(self, *args): - pass - - def spotPixmap(self): - if not self.identical: - return None - if self._spotPixmap is None: - self._spotPixmap = PixmapSpotItem.makeSpotImage(self.size, self.pen, self.brush) - return self._spotPixmap - - def mkSpot(self, pos, size, pxMode, brush, pen, data, index=None): - if pxMode: - img = self.spotPixmap() - item = PixmapSpotItem(size, brush, pen, data, image=img, index=index) - else: - item = SpotItem(size, pxMode, brush, pen, data, index=index) - item.setParentItem(self) - item.setPos(pos) - #item.sigClicked.connect(self.pointClicked) - return item - - def boundingRect(self): - ((xmn, xmx), (ymn, ymx)) = self.range - if xmn is None or xmx is None or ymn is None or ymx is None: - return QtCore.QRectF() - return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) - return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2) - - #def pointClicked(self, point): - #self.sigPointClicked.emit(self, point) - - def points(self): - return self.spots[:] - - def pointsAt(self, pos): - x = pos.x() - y = pos.y() - pw = self.pixelWidth() - ph = self.pixelHeight() - pts = [] - for s in self.spots: - sp = s.pos() - ss = s.size - sx = sp.x() - sy = sp.y() - s2x = s2y = ss * 0.5 - if self.pxMode: - s2x *= pw - s2y *= ph - if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y: - pts.append(s) - #print "HIT:", x, y, sx, sy, s2x, s2y - #else: - #print "No hit:", (x, y), (sx, sy) - #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) - pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) - return pts - - - def mousePressEvent(self, ev): - QtGui.QGraphicsItem.mousePressEvent(self, ev) - if ev.button() == QtCore.Qt.LeftButton: - pts = self.pointsAt(ev.pos()) - if len(pts) > 0: - self.mouseMoved = False - self.ptsClicked = pts - ev.accept() - else: - #print "no spots" - ev.ignore() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsItem.mouseMoveEvent(self, ev) - self.mouseMoved = True - pass - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) - if not self.mouseMoved: - self.sigClicked.emit(self, self.ptsClicked) - - -class SpotItem(QtGui.QGraphicsWidget): - #sigClicked = QtCore.Signal(object) - - def __init__(self, size, pxMode, brush, pen, data, index=None): - QtGui.QGraphicsWidget.__init__(self) - self.pxMode = pxMode - - self.pen = pen - self.brush = brush - self.size = size - self.index = index - #s2 = size/2. - self.path = QtGui.QPainterPath() - self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) - if pxMode: - #self.setCacheMode(self.DeviceCoordinateCache) ## broken. - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied) - self.spotImage.fill(0) - p = QtGui.QPainter(self.spotImage) - p.setRenderHint(p.Antialiasing) - p.setBrush(brush) - p.setPen(pen) - p.drawEllipse(0, 0, size, size) - p.end() - self.pixmap = QtGui.QPixmap(self.spotImage) - else: - self.scale(size, size) - self.data = data - - def setBrush(self, brush): - self.brush = mkBrush(brush) - self.update() - - def setPen(self, pen): - self.pen = mkPen(pen) - self.update() - - def boundingRect(self): - return self.path.boundingRect() - - def shape(self): - return self.path - - def paint(self, p, *opts): - if self.pxMode: - p.drawPixmap(QtCore.QPoint(int(-0.5*self.size), int(-0.5*self.size)), self.pixmap) - else: - p.setPen(self.pen) - p.setBrush(self.brush) - p.drawPath(self.path) - - #def mousePressEvent(self, ev): - #QtGui.QGraphicsItem.mousePressEvent(self, ev) - #if ev.button() == QtCore.Qt.LeftButton: - #self.mouseMoved = False - #ev.accept() - #else: - #ev.ignore() - - - - #def mouseMoveEvent(self, ev): - #QtGui.QGraphicsItem.mouseMoveEvent(self, ev) - #self.mouseMoved = True - #pass - - #def mouseReleaseEvent(self, ev): - #QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) - #if not self.mouseMoved: - #self.sigClicked.emit(self) - -class PixmapSpotItem(QtGui.QGraphicsItem): - #sigClicked = QtCore.Signal(object) - - def __init__(self, size, brush, pen, data, image=None, index=None): - """This class draws a scale-invariant image centered at 0,0. - If no image is specified, then an antialiased circle is constructed instead. - It should be quite fast, but large spots will use a lot of memory.""" - - QtGui.QGraphicsItem.__init__(self) - self.pen = pen - self.brush = brush - self.size = size - self.index = index - self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemHasNoContents) - if image is None: - self.image = self.makeSpotImage(self.size, self.pen, self.brush) - else: - self.image = image - self.pixmap = QtGui.QPixmap(self.image) - #self.setPixmap(self.pixmap) - self.data = data - self.pi = QtGui.QGraphicsPixmapItem(self.pixmap, self) - self.pi.setPos(-0.5*size, -0.5*size) - - #self.translate(-0.5, -0.5) - def boundingRect(self): - return self.pi.boundingRect() - - @staticmethod - def makeSpotImage(size, pen, brush): - img = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied) - img.fill(0) - p = QtGui.QPainter(img) - try: - p.setRenderHint(p.Antialiasing) - p.setBrush(brush) - p.setPen(pen) - p.drawEllipse(1, 1, size, size) - finally: - p.end() ## failure to end a painter properly causes crash. - return img - - - - #def paint(self, p, *args): - #p.setCompositionMode(p.CompositionMode_Plus) - #QtGui.QGraphicsPixmapItem.paint(self, p, *args) - - #def setBrush(self, brush): - #self.brush = mkBrush(brush) - #self.update() - - #def setPen(self, pen): - #self.pen = mkPen(pen) - #self.update() - - #def boundingRect(self): - #return self.path.boundingRect() - - #def shape(self): - #return self.path - - #def paint(self, p, *opts): - #if self.pxMode: - #p.drawPixmap(QtCore.QPoint(int(-0.5*self.size), int(-0.5*self.size)), self.pixmap) - #else: - #p.setPen(self.pen) - #p.setBrush(self.brush) - #p.drawPath(self.path) - - - -class ROIPlotItem(PlotCurveItem): - """Plot curve that monitors an ROI and image for changes to automatically replot.""" - def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): - self.roi = roi - self.roiData = data - self.roiImg = img - self.axes = axes - self.xVals = xVals - PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) - #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) - roi.sigRegionChanged.connect(self.roiChangedEvent) - #self.roiChangedEvent() - - def getRoiData(self): - d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) - if d is None: - return - while d.ndim > 1: - d = d.mean(axis=1) - return d - - def roiChangedEvent(self): - d = self.getRoiData() - self.updateData(d, self.xVals) - - - - -class UIGraphicsItem(GraphicsObject): - """Base class for graphics items with boundaries relative to a GraphicsView widget""" - def __init__(self, view, bounds=None): - GraphicsObject.__init__(self) - self._view = weakref.ref(view) - if bounds is None: - self._bounds = QtCore.QRectF(0, 0, 1, 1) - else: - self._bounds = bounds - self._viewRect = self._view().rect() - self._viewTransform = self.viewTransform() - self.setNewBounds() - #QtCore.QObject.connect(view, QtCore.SIGNAL('viewChanged'), self.viewChangedEvent) - view.sigRangeChanged.connect(self.viewRangeChanged) - - def viewRect(self): - """Return the viewport widget rect""" - return self._view().rect() - - def viewTransform(self): - """Returns a matrix that maps viewport coordinates onto scene coordinates""" - if self._view() is None: - return QtGui.QTransform() - else: - return self._view().viewportTransform() - - def boundingRect(self): - if self._view() is None: - self.bounds = self._bounds - else: - vr = self._view().rect() - tr = self.viewTransform() - if vr != self._viewRect or tr != self._viewTransform: - #self.viewChangedEvent(vr, self._viewRect) - self._viewRect = vr - self._viewTransform = tr - self.setNewBounds() - #print "viewRect", self._viewRect.x(), self._viewRect.y(), self._viewRect.width(), self._viewRect.height() - #print "bounds", self.bounds.x(), self.bounds.y(), self.bounds.width(), self.bounds.height() - return self.bounds - - def setNewBounds(self): - bounds = QtCore.QRectF( - QtCore.QPointF(self._bounds.left()*self._viewRect.width(), self._bounds.top()*self._viewRect.height()), - QtCore.QPointF(self._bounds.right()*self._viewRect.width(), self._bounds.bottom()*self._viewRect.height()) - ) - bounds.adjust(0.5, 0.5, 0.5, 0.5) - self.bounds = self.viewTransform().inverted()[0].mapRect(bounds) - self.prepareGeometryChange() - - def viewRangeChanged(self): - """Called when the view widget is resized""" - self.boundingRect() - self.update() - - def unitRect(self): - return self.viewTransform().inverted()[0].mapRect(QtCore.QRectF(0, 0, 1, 1)) - - def paint(self, *args): - pass - - -class DebugText(QtGui.QGraphicsTextItem): - def paint(self, *args): - p = debug.Profiler("DebugText.paint", disabled=True) - QtGui.QGraphicsTextItem.paint(self, *args) - p.finish() - -class LabelItem(QtGui.QGraphicsWidget): - def __init__(self, text, parent=None, **args): - QtGui.QGraphicsWidget.__init__(self, parent) - self.item = DebugText(self) - self.opts = args - if 'color' not in args: - self.opts['color'] = 'CCC' - else: - if isinstance(args['color'], QtGui.QColor): - self.opts['color'] = colorStr(args['color'])[:6] - self.sizeHint = {} - self.setText(text) - - - def setAttr(self, attr, value): - """Set default text properties. See setText() for accepted parameters.""" - self.opts[attr] = value - - def setText(self, text, **args): - """Set the text and text properties in the label. Accepts optional arguments for auto-generating - a CSS style string: - color: string (example: 'CCFF00') - size: string (example: '8pt') - bold: boolean - italic: boolean - """ - self.text = text - opts = self.opts.copy() - for k in args: - opts[k] = args[k] - - optlist = [] - if 'color' in opts: - optlist.append('color: #' + opts['color']) - if 'size' in opts: - optlist.append('font-size: ' + opts['size']) - if 'bold' in opts and opts['bold'] in [True, False]: - optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) - if 'italic' in opts and opts['italic'] in [True, False]: - optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) - full = "%s" % ('; '.join(optlist), text) - #print full - self.item.setHtml(full) - self.updateMin() - - def resizeEvent(self, ev): - c1 = self.boundingRect().center() - c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() - dif = c1 - c2 - self.item.moveBy(dif.x(), dif.y()) - #print c1, c2, dif, self.item.pos() - - def setAngle(self, angle): - self.angle = angle - self.item.resetTransform() - self.item.rotate(angle) - self.updateMin() - - def updateMin(self): - bounds = self.item.mapRectToParent(self.item.boundingRect()) - self.setMinimumWidth(bounds.width()) - self.setMinimumHeight(bounds.height()) - #print self.text, bounds.width(), bounds.height() - - #self.sizeHint = { - #QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), - #QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), - #QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), - #QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? - #} - - - #def sizeHint(self, hint, constraint): - #return self.sizeHint[hint] - - - - - -class ScaleItem(QtGui.QGraphicsWidget): - def __init__(self, orientation, pen=None, linkView=None, parent=None): - """GraphicsItem showing a single plot axis with ticks, values, and label. - Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. - Ticks can be extended to make a grid.""" - QtGui.QGraphicsWidget.__init__(self, parent) - self.label = QtGui.QGraphicsTextItem(self) - 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.setMinimumWidth(25) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Minimum, - #QtGui.QSizePolicy.Expanding - #)) - self.label.rotate(-90) - #else: - #self.setMinimumHeight(50) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Expanding, - #QtGui.QSizePolicy.Minimum - #)) - #self.drawLabel = False - - self.labelText = '' - self.labelUnits = '' - self.labelUnitPrefix='' - self.labelStyle = {'color': '#CCC'} - - self.textHeight = 18 - self.tickLength = 10 - self.scale = 1.0 - self.autoScale = True - - self.setRange(0, 1) - - if pen is None: - pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) - self.setPen(pen) - - self.linkedView = None - if linkView is not None: - self.linkToView(linkView) - - self.showLabel(False) - - self.grid = False - self.setCacheMode(self.DeviceCoordinateCache) - - def close(self): - self.scene().removeItem(self.label) - self.label = None - self.scene().removeItem(self) - - def setGrid(self, grid): - """Set the alpha value for the grid, or False to disable.""" - self.grid = grid - self.update() - - - def resizeEvent(self, ev=None): - #s = self.size() - - ## Set the position of the label - nudge = 5 - br = self.label.boundingRect() - p = QtCore.QPointF(0, 0) - if self.orientation == 'left': - p.setY(int(self.size().height()/2 + br.width()/2)) - p.setX(-nudge) - #s.setWidth(10) - elif self.orientation == 'right': - #s.setWidth(10) - p.setY(int(self.size().height()/2 + br.width()/2)) - p.setX(int(self.size().width()-br.height()+nudge)) - elif self.orientation == 'top': - #s.setHeight(10) - p.setY(-nudge) - p.setX(int(self.size().width()/2. - br.width()/2.)) - elif self.orientation == 'bottom': - p.setX(int(self.size().width()/2. - br.width()/2.)) - #s.setHeight(10) - p.setY(int(self.size().height()-br.height()+nudge)) - #self.label.resize(s) - self.label.setPos(p) - - def showLabel(self, show=True): - #self.drawLabel = show - self.label.setVisible(show) - if self.orientation in ['left', 'right']: - self.setWidth() - else: - self.setHeight() - if self.autoScale: - self.setScale() - - def setLabel(self, text=None, units=None, unitPrefix=None, **args): - if text is not None: - self.labelText = text - self.showLabel() - if units is not None: - self.labelUnits = units - self.showLabel() - if unitPrefix is not None: - self.labelUnitPrefix = unitPrefix - if len(args) > 0: - self.labelStyle = args - self.label.setHtml(self.labelString()) - self.resizeEvent() - self.update() - - def labelString(self): - if self.labelUnits == '': - if self.scale == 1.0: - units = '' - else: - units = u'(x%g)' % (1.0/self.scale) - else: - #print repr(self.labelUnitPrefix), repr(self.labelUnits) - units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits) - - s = u'%s %s' % (self.labelText, units) - - style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle]) - - return u"%s" % (style, s) - - def setHeight(self, h=None): - if h is None: - h = self.textHeight + self.tickLength - if self.label.isVisible(): - h += self.textHeight - self.setMaximumHeight(h) - self.setMinimumHeight(h) - - - def setWidth(self, w=None): - if w is None: - w = self.tickLength + 40 - if self.label.isVisible(): - w += self.textHeight - self.setMaximumWidth(w) - self.setMinimumWidth(w) - - def setPen(self, pen): - self.pen = pen - self.update() - - def setScale(self, scale=None): - if scale is None: - #if self.drawLabel: ## If there is a label, then we are free to rescale the values - if self.label.isVisible(): - d = self.range[1] - self.range[0] - #pl = 1-int(log10(d)) - #scale = 10 ** pl - (scale, prefix) = siScale(d / 2.) - if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. - scale = 1.0 - prefix = '' - self.setLabel(unitPrefix=prefix) - else: - scale = 1.0 - - - if scale != self.scale: - self.scale = scale - self.setLabel() - self.update() - - def setRange(self, mn, mx): - if mn in [np.nan, np.inf, -np.inf] or mx in [np.nan, np.inf, -np.inf]: - raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) - self.range = [mn, mx] - if self.autoScale: - self.setScale() - self.update() - - def linkToView(self, view): - if self.orientation in ['right', 'left']: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigYRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) - view.sigYRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('yRangeChanged') - else: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigXRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) - view.sigXRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('xRangeChanged') - - - def linkedViewChanged(self, view, newRange): - self.setRange(*newRange) - - def boundingRect(self): - if self.linkedView is None or self.linkedView() is None or self.grid is False: - return self.mapRectFromParent(self.geometry()) - else: - return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) - - def paint(self, p, opt, widget): - prof = debug.Profiler("ScaleItem.paint", disabled=True) - p.setPen(self.pen) - - #bounds = self.boundingRect() - bounds = self.mapRectFromParent(self.geometry()) - - if self.linkedView is None or self.linkedView() is None or self.grid is False: - tbounds = bounds - else: - tbounds = self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) - - if self.orientation == 'left': - p.drawLine(bounds.topRight(), bounds.bottomRight()) - tickStart = tbounds.right() - tickStop = bounds.right() - tickDir = -1 - axis = 0 - elif self.orientation == 'right': - p.drawLine(bounds.topLeft(), bounds.bottomLeft()) - tickStart = tbounds.left() - tickStop = bounds.left() - tickDir = 1 - axis = 0 - elif self.orientation == 'top': - p.drawLine(bounds.bottomLeft(), bounds.bottomRight()) - tickStart = tbounds.bottom() - tickStop = bounds.bottom() - tickDir = -1 - axis = 1 - elif self.orientation == 'bottom': - p.drawLine(bounds.topLeft(), bounds.topRight()) - tickStart = tbounds.top() - tickStop = bounds.top() - tickDir = 1 - axis = 1 - - ## Determine optimal tick spacing - #intervals = [1., 2., 5., 10., 20., 50.] - #intervals = [1., 2.5, 5., 10., 25., 50.] - intervals = [1., 2., 10., 20., 100.] - dif = abs(self.range[1] - self.range[0]) - if dif == 0.0: - return - #print "dif:", dif - pw = 10 ** (np.floor(np.log10(dif))-1) - for i in range(len(intervals)): - i1 = i - if dif / (pw*intervals[i]) < 10: - break - - textLevel = i1 ## draw text at this scale level - - #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) - - #print " start at %f, %d ticks" % (start, num) - - - if axis == 0: - xs = -bounds.height() / dif - else: - xs = bounds.width() / dif - - prof.mark('init') - - tickPositions = set() # remembers positions of previously drawn ticks - ## draw ticks and generate list of texts to draw - ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) - ## draw three different intervals, long ticks first - texts = [] - for i in reversed([i1, i1+1, i1+2]): - if i > len(intervals): - continue - ## spacing for this interval - sp = pw*intervals[i] - - ## determine starting tick - start = np.ceil(self.range[0] / sp) * sp - - ## determine number of ticks - num = int(dif / sp) + 1 - - ## last tick value - last = start + sp * num - - ## Number of decimal places to print - maxVal = max(abs(start), abs(last)) - places = max(0, 1-int(np.log10(sp*self.scale))) - - ## length of tick - h = min(self.tickLength, (self.tickLength*3 / num) - 1.) - - ## alpha - a = min(255, (765. / num) - 1.) - - if axis == 0: - offset = self.range[0] * xs - bounds.height() - else: - offset = self.range[0] * xs - - for j in range(num): - v = start + sp * j - x = (v * xs) - offset - p1 = [0, 0] - p2 = [0, 0] - p1[axis] = tickStart - p2[axis] = tickStop + h*tickDir - p1[1-axis] = p2[1-axis] = x - - if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: - continue - if p1[1-axis] < 0: - continue - p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, a))) - # draw tick only if there is none - tickPos = p1[1-axis] - if tickPos not in tickPositions: - p.drawLine(Point(p1), Point(p2)) - tickPositions.add(tickPos) - if i == textLevel: - if abs(v) < .001 or abs(v) >= 10000: - vstr = "%g" % (v * self.scale) - else: - vstr = ("%%0.%df" % places) % (v * self.scale) - - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - height = textRect.height() - self.textHeight = height - if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 100-self.tickLength, height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+self.tickLength, x-(height/2), 100-self.tickLength, height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-self.tickLength-height, 200, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+self.tickLength, 200, height) - - p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) - #p.drawText(rect, textFlags, vstr) - texts.append((rect, textFlags, vstr)) - - prof.mark('draw ticks') - for args in texts: - p.drawText(*args) - prof.mark('draw text') - prof.finish() - - def show(self): - - if self.orientation in ['left', 'right']: - self.setWidth() - else: - self.setHeight() - QtGui.QGraphicsWidget.show(self) - - def hide(self): - if self.orientation in ['left', 'right']: - self.setWidth(0) - else: - self.setHeight(0) - QtGui.QGraphicsWidget.hide(self) - - def wheelEvent(self, ev): - if self.linkedView is None or self.linkedView() is None: return - if self.orientation in ['left', 'right']: - self.linkedView().wheelEvent(ev, axis=1) - else: - self.linkedView().wheelEvent(ev, axis=0) - ev.accept() - - - -class ViewBox(QtGui.QGraphicsWidget): - - sigYRangeChanged = QtCore.Signal(object, object) - sigXRangeChanged = QtCore.Signal(object, object) - sigRangeChangedManually = QtCore.Signal(object) - sigRangeChanged = QtCore.Signal(object, object) - - """Box that allows internal scaling/panning of children by mouse drag. Not compatible with GraphicsView having the same functionality.""" - def __init__(self, parent=None, border=None): - QtGui.QGraphicsWidget.__init__(self, parent) - #self.gView = view - #self.showGrid = showGrid - - ## separating targetRange and viewRange allows the view to be resized - ## while keeping all previously viewed contents visible - self.targetRange = [[0,1], [0,1]] ## child coord. range visible [[xmin, xmax], [ymin, ymax]] - self.viewRange = [[0,1], [0,1]] ## actual range viewed - - self.wheelScaleFactor = -1.0 / 8.0 - self.aspectLocked = False - self.setFlag(QtGui.QGraphicsItem.ItemClipsChildrenToShape) - #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - #self.childGroup = QtGui.QGraphicsItemGroup(self) - self.childGroup = ItemGroup(self) - self.currentScale = Point(1, 1) - - self.yInverted = False - #self.invertY() - self.setZValue(-100) - #self.picture = None - self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - - self.border = border - - self.mouseEnabled = [True, True] - - def setMouseEnabled(self, x, y): - self.mouseEnabled = [x, y] - - def addItem(self, item): - if item.zValue() < self.zValue(): - item.setZValue(self.zValue()+1) - item.setParentItem(self.childGroup) - #print "addItem:", item, item.boundingRect() - - def removeItem(self, item): - self.scene().removeItem(item) - - def resizeEvent(self, ev): - #self.setRange(self.range, padding=0) - self.updateMatrix() - - - def viewRect(self): - try: - vr0 = self.viewRange[0] - vr1 = self.viewRange[1] - return QtCore.QRectF(vr0[0], vr1[0], vr0[1]-vr0[0], vr1[1] - vr1[0]) - except: - print "make qrectf failed:", self.viewRange - raise - - def targetRect(self): - """Return the region which has been requested to be visible. - (this is not necessarily the same as the region that is *actually* visible)""" - try: - tr0 = self.targetRange[0] - tr1 = self.targetRange[1] - return QtCore.QRectF(tr0[0], tr1[0], tr0[1]-tr0[0], tr1[1] - tr1[0]) - except: - print "make qrectf failed:", self.targetRange - raise - - def invertY(self, b=True): - self.yInverted = b - self.updateMatrix() - - def setAspectLocked(self, lock=True, ratio=1): - """If the aspect ratio is locked, view scaling is always forced to be isotropic. - By default, the ratio is set to 1; x and y both have the same scaling. - This ratio can be overridden (width/height), or use None to lock in the current ratio. - """ - if not lock: - self.aspectLocked = False - else: - vr = self.viewRect() - currentRatio = vr.width() / vr.height() - if ratio is None: - ratio = currentRatio - self.aspectLocked = ratio - if ratio != currentRatio: ## If this would change the current range, do that now - #self.setRange(0, self.viewRange[0][0], self.viewRange[0][1]) - self.updateMatrix() - - def childTransform(self): - m = self.childGroup.transform() - m1 = QtGui.QTransform() - m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) - return m*m1 - - - def viewScale(self): - vr = self.viewRect() - #print "viewScale:", self.range - xd = vr.width() - yd = vr.height() - if xd == 0 or yd == 0: - print "Warning: 0 range in view:", xd, yd - return np.array([1,1]) - - #cs = self.canvas().size() - cs = self.boundingRect() - scale = np.array([cs.width() / xd, cs.height() / yd]) - #print "view scale:", scale - return scale - - def scaleBy(self, s, center=None): - """Scale by s around given center point (or center of view)""" - #print "scaleBy", s, center - #if self.aspectLocked: - #s[0] = s[1] - scale = Point(s) - if self.aspectLocked is not False: - scale[0] = self.aspectLocked * scale[1] - - - #xr, yr = self.range - vr = self.viewRect() - if center is None: - center = Point(vr.center()) - #xc = (xr[1] + xr[0]) * 0.5 - #yc = (yr[1] + yr[0]) * 0.5 - else: - center = Point(center) - #(xc, yc) = center - - #x1 = xc + (xr[0]-xc) * s[0] - #x2 = xc + (xr[1]-xc) * s[0] - #y1 = yc + (yr[0]-yc) * s[1] - #y2 = yc + (yr[1]-yc) * s[1] - tl = center + (vr.topLeft()-center) * scale - br = center + (vr.bottomRight()-center) * scale - - #print xr, xc, s, (xr[0]-xc) * s[0], (xr[1]-xc) * s[0] - #print [[x1, x2], [y1, y2]] - - #if not self.aspectLocked: - #self.setXRange(x1, x2, update=False, padding=0) - #self.setYRange(y1, y2, padding=0) - #print self.range - - self.setRange(QtCore.QRectF(tl, br), padding=0) - - def translateBy(self, t, viewCoords=False): - t = t.astype(np.float) - #print "translate:", t, self.viewScale() - if viewCoords: ## scale from pixels - t /= self.viewScale() - #xr, yr = self.range - - vr = self.viewRect() - #print xr, yr, t - #self.setXRange(xr[0] + t[0], xr[1] + t[0], update=False, padding=0) - #self.setYRange(yr[0] + t[1], yr[1] + t[1], padding=0) - self.setRange(vr.translated(Point(t)), padding=0) - - def wheelEvent(self, ev, axis=None): - mask = np.array(self.mouseEnabled, dtype=np.float) - if axis is not None and axis >= 0 and axis < len(mask): - mv = mask[axis] - mask[:] = 0 - mask[axis] = mv - s = ((mask * 0.02) + 1) ** (ev.delta() * self.wheelScaleFactor) # actual scaling factor - # scale 'around' mouse cursor position - center = Point(self.childGroup.transform().inverted()[0].map(ev.pos())) - self.scaleBy(s, center) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsWidget.mouseMoveEvent(self, ev) - pos = np.array([ev.pos().x(), ev.pos().y()]) - dif = pos - self.mousePos - dif *= -1 - self.mousePos = pos - - ## Ignore axes if mouse is disabled - mask = np.array(self.mouseEnabled, dtype=np.float) - - ## Scale or translate based on mouse button - if ev.buttons() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): - if not self.yInverted: - mask *= np.array([1, -1]) - tr = dif*mask - self.translateBy(tr, viewCoords=True) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - elif ev.buttons() & QtCore.Qt.RightButton: - if self.aspectLocked is not False: - mask[0] = 0 - dif = ev.screenPos() - ev.lastScreenPos() - dif = np.array([dif.x(), dif.y()]) - dif[0] *= -1 - s = ((mask * 0.02) + 1) ** dif - #print mask, dif, s - center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) - self.scaleBy(s, center) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - else: - ev.ignore() - - def mousePressEvent(self, ev): - QtGui.QGraphicsWidget.mousePressEvent(self, ev) - - self.mousePos = np.array([ev.pos().x(), ev.pos().y()]) - self.pressPos = self.mousePos.copy() - ev.accept() - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev) - pos = np.array([ev.pos().x(), ev.pos().y()]) - #if sum(abs(self.pressPos - pos)) < 3: ## Detect click - #if ev.button() == QtCore.Qt.RightButton: - #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) - self.mousePos = pos - ev.accept() - - def setRange(self, ax, min=None, max=None, padding=0.02, update=True): - if isinstance(ax, QtCore.QRectF): - changes = {0: [ax.left(), ax.right()], 1: [ax.top(), ax.bottom()]} - #if self.aspectLocked is not False: - #sbr = self.boundingRect() - #if sbr.width() == 0 or (ax.height()/ax.width()) > (sbr.height()/sbr.width()): - #chax = 0 - #else: - #chax = 1 - - - - - elif ax in [1,0]: - changes = {ax: [min,max]} - #if self.aspectLocked is not False: - #ax2 = 1 - ax - #ratio = self.aspectLocked - #r2 = self.range[ax2] - #d = ratio * (max-min) * 0.5 - #c = (self.range[ax2][1] + self.range[ax2][0]) * 0.5 - #changes[ax2] = [c-d, c+d] - - else: - print ax - raise Exception("argument 'ax' must be 0, 1, or QRectF.") - - - changed = [False, False] - for ax, range in changes.iteritems(): - min, max = range - if min == max: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale. - dy = self.viewRange[ax][1] - self.viewRange[ax][0] - if dy == 0: - dy = 1 - min -= dy*0.5 - max += dy*0.5 - padding = 0.0 - if any(np.isnan([min, max])) or any(np.isinf([min, max])): - raise Exception("Not setting range [%s, %s]" % (str(min), str(max))) - - p = (max-min) * padding - min -= p - max += p - - if self.targetRange[ax] != [min, max]: - self.targetRange[ax] = [min, max] - changed[ax] = True - - if update: - self.updateMatrix(changed) - - - - - def setYRange(self, min, max, update=True, padding=0.02): - self.setRange(1, min, max, update=update, padding=padding) - - def setXRange(self, min, max, update=True, padding=0.02): - self.setRange(0, min, max, update=update, padding=padding) - - def autoRange(self, padding=0.02): - br = self.childGroup.childrenBoundingRect() - self.setRange(br, padding=padding) - - - def updateMatrix(self, changed=None): - if changed is None: - changed = [False, False] - #print "udpateMatrix:" - #print " range:", self.range - tr = self.targetRect() - bounds = self.boundingRect() - - ## set viewRect, given targetRect and possibly aspect ratio constraint - if self.aspectLocked is False or bounds.height() == 0: - self.viewRange = [self.targetRange[0][:], self.targetRange[1][:]] - else: - viewRatio = bounds.width() / bounds.height() - targetRatio = self.aspectLocked * tr.width() / tr.height() - if targetRatio > viewRatio: - ## target is wider than view - dy = 0.5 * (tr.width() / (self.aspectLocked * viewRatio) - tr.height()) - if dy != 0: - changed[1] = True - self.viewRange = [self.targetRange[0][:], [self.targetRange[1][0] - dy, self.targetRange[1][1] + dy]] - else: - dx = 0.5 * (tr.height() * viewRatio * self.aspectLocked - tr.width()) - if dx != 0: - changed[0] = True - self.viewRange = [[self.targetRange[0][0] - dx, self.targetRange[0][1] + dx], self.targetRange[1][:]] - - - vr = self.viewRect() - translate = Point(vr.center()) - #print " bounds:", bounds - if vr.height() == 0 or vr.width() == 0: - return - scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) - #print " scale:", scale - m = QtGui.QTransform() - - ## First center the viewport at 0 - self.childGroup.resetTransform() - center = self.transform().inverted()[0].map(bounds.center()) - #print " transform to center:", center - if self.yInverted: - m.translate(center.x(), -center.y()) - #print " inverted; translate", center.x(), center.y() - else: - m.translate(center.x(), center.y()) - #print " not inverted; translate", center.x(), -center.y() - - ## Now scale and translate properly - if not self.yInverted: - scale = scale * Point(1, -1) - m.scale(scale[0], scale[1]) - st = translate - m.translate(-st[0], -st[1]) - self.childGroup.setTransform(m) - self.currentScale = scale - - - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.viewRange[0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.viewRange[1])) - if any(changed): - self.sigRangeChanged.emit(self, self.viewRange) - - - - def boundingRect(self): - return QtCore.QRectF(0, 0, self.size().width(), self.size().height()) - - def paint(self, p, opt, widget): - if self.border is not None: - bounds = self.boundingRect() - p.setPen(self.border) - #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) - p.drawRect(bounds) - - -class InfiniteLine(GraphicsObject): - - sigDragged = QtCore.Signal(object) - sigPositionChangeFinished = QtCore.Signal(object) - sigPositionChanged = QtCore.Signal(object) - - def __init__(self, view, pos=0, angle=90, pen=None, movable=False, bounds=None): - GraphicsObject.__init__(self) - self.bounds = QtCore.QRectF() ## graphicsitem boundary - - if bounds is None: ## allowed value boundaries for orthogonal lines - self.maxRange = [None, None] - else: - self.maxRange = bounds - self.setMovable(movable) - self.view = weakref.ref(view) - self.p = [0, 0] - self.setAngle(angle) - self.setPos(pos) - - - self.hasMoved = False - - - if pen is None: - pen = QtGui.QPen(QtGui.QColor(200, 200, 100)) - self.setPen(pen) - self.currentPen = self.pen - #self.setFlag(self.ItemSendsScenePositionChanges) - #for p in self.getBoundingParents(): - #QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine) - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine) - self.view().sigRangeChanged.connect(self.updateLine) - - def setMovable(self, m): - self.movable = m - self.setAcceptHoverEvents(m) - - - def setBounds(self, bounds): - self.maxRange = bounds - self.setValue(self.value()) - - def hoverEnterEvent(self, ev): - self.currentPen = QtGui.QPen(QtGui.QColor(255, 0,0)) - self.update() - ev.ignore() - - def hoverLeaveEvent(self, ev): - self.currentPen = self.pen - self.update() - ev.ignore() - - def setPen(self, pen): - self.pen = pen - self.currentPen = self.pen - - def setAngle(self, angle): - """Takes angle argument in degrees.""" - self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 - self.updateLine() - - def setPos(self, pos): - if type(pos) in [list, tuple]: - newPos = pos - elif isinstance(pos, QtCore.QPointF): - newPos = [pos.x(), pos.y()] - else: - if self.angle == 90: - newPos = [pos, 0] - elif self.angle == 0: - newPos = [0, pos] - else: - raise Exception("Must specify 2D coordinate for non-orthogonal lines.") - - ## check bounds (only works for orthogonal lines) - if self.angle == 90: - if self.maxRange[0] is not None: - newPos[0] = max(newPos[0], self.maxRange[0]) - if self.maxRange[1] is not None: - newPos[0] = min(newPos[0], self.maxRange[1]) - elif self.angle == 0: - if self.maxRange[0] is not None: - newPos[1] = max(newPos[1], self.maxRange[0]) - if self.maxRange[1] is not None: - newPos[1] = min(newPos[1], self.maxRange[1]) - - - if self.p != newPos: - self.p = newPos - self.updateLine() - #self.emit(QtCore.SIGNAL('positionChanged'), self) - self.sigPositionChanged.emit(self) - - def getXPos(self): - return self.p[0] - - def getYPos(self): - return self.p[1] - - def getPos(self): - return self.p - - def value(self): - if self.angle%180 == 0: - return self.getYPos() - elif self.angle%180 == 90: - return self.getXPos() - else: - return self.getPos() - - def setValue(self, v): - self.setPos(v) - - ## broken in 4.7 - #def itemChange(self, change, val): - #if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]: - #self.updateLine() - #print "update", change - #print self.getBoundingParents() - #else: - #print "ignore", change - #return GraphicsObject.itemChange(self, change, val) - - def updateLine(self): - - #unit = QtCore.QRect(0, 0, 10, 10) - #if self.scene() is not None: - #gv = self.scene().views()[0] - #unit = gv.mapToScene(unit).boundingRect() - ##print unit - #unit = self.mapRectFromScene(unit) - ##print unit - - vr = self.view().viewRect() - #vr = self.viewBounds() - if vr is None: - return - #print 'before', self.bounds - - if self.angle > 45: - m = np.tan((90-self.angle) * np.pi / 180.) - y2 = vr.bottom() - y1 = vr.top() - x1 = self.p[0] + (y1 - self.p[1]) * m - x2 = self.p[0] + (y2 - self.p[1]) * m - else: - m = np.tan(self.angle * np.pi / 180.) - x1 = vr.left() - x2 = vr.right() - y2 = self.p[1] + (x1 - self.p[0]) * m - y1 = self.p[1] + (x2 - self.p[0]) * m - #print vr, x1, y1, x2, y2 - self.prepareGeometryChange() - self.line = (QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)) - self.bounds = QtCore.QRectF(self.line[0], self.line[1]) - ## Stupid bug causes lines to disappear: - if self.angle % 180 == 90: - px = self.pixelWidth() - #self.bounds.setWidth(1e-9) - self.bounds.setX(x1 + px*-5) - self.bounds.setWidth(px*10) - if self.angle % 180 == 0: - px = self.pixelHeight() - #self.bounds.setHeight(1e-9) - self.bounds.setY(y1 + px*-5) - self.bounds.setHeight(px*10) - - #QtGui.QGraphicsLineItem.setLine(self, x1, y1, x2, y2) - #self.update() - - def boundingRect(self): - #self.updateLine() - #return QtGui.QGraphicsLineItem.boundingRect(self) - #print "bounds", self.bounds - return self.bounds - - def paint(self, p, *args): - w,h = self.pixelWidth()*5, self.pixelHeight()*5*1.1547 - #self.updateLine() - l = self.line - - p.setPen(self.currentPen) - #print "paint", self.line - p.drawLine(l[0], l[1]) - - p.setBrush(QtGui.QBrush(self.currentPen.color())) - p.drawConvexPolygon(QtGui.QPolygonF([ - l[0] + QtCore.QPointF(-w, 0), - l[0] + QtCore.QPointF(0, h), - l[0] + QtCore.QPointF(w, 0), - ])) - - #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - #p.drawRect(self.boundingRect()) - - def mousePressEvent(self, ev): - if self.movable and ev.button() == QtCore.Qt.LeftButton: - ev.accept() - self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - #self.emit(QtCore.SIGNAL('dragged'), self) - self.sigDragged.emit(self) - self.hasMoved = True - - def mouseReleaseEvent(self, ev): - if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: - self.hasMoved = False - #self.emit(QtCore.SIGNAL('positionChangeFinished'), self) - self.sigPositionChangeFinished.emit(self) - - - -class LinearRegionItem(GraphicsObject): - - sigRegionChangeFinished = QtCore.Signal(object) - sigRegionChanged = QtCore.Signal(object) - - """Used for marking a horizontal or vertical region in plots.""" - def __init__(self, view, orientation="vertical", vals=[0,1], brush=None, movable=True, bounds=None): - GraphicsObject.__init__(self) - self.orientation = orientation - if hasattr(self, "ItemHasNoContents"): - self.setFlag(self.ItemHasNoContents) - self.rect = QtGui.QGraphicsRectItem(self) - self.rect.setParentItem(self) - self.bounds = QtCore.QRectF() - self.view = weakref.ref(view) - self.setBrush = self.rect.setBrush - self.brush = self.rect.brush - - if orientation[0] == 'h': - self.lines = [ - InfiniteLine(view, QtCore.QPointF(0, vals[0]), 0, movable=movable, bounds=bounds), - InfiniteLine(view, QtCore.QPointF(0, vals[1]), 0, movable=movable, bounds=bounds)] - else: - self.lines = [ - InfiniteLine(view, QtCore.QPointF(vals[0], 0), 90, movable=movable, bounds=bounds), - InfiniteLine(view, QtCore.QPointF(vals[1], 0), 90, movable=movable, bounds=bounds)] - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateBounds) - self.view().sigRangeChanged.connect(self.updateBounds) - - for l in self.lines: - l.setParentItem(self) - #l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) - l.sigPositionChangeFinished.connect(self.lineMoveFinished) - #l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved) - l.sigPositionChanged.connect(self.lineMoved) - - if brush is None: - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) - self.setBrush(brush) - self.setMovable(movable) - - def setBounds(self, bounds): - for l in self.lines: - l.setBounds(bounds) - - def setMovable(self, m): - for l in self.lines: - l.setMovable(m) - self.movable = m - - def boundingRect(self): - return self.rect.boundingRect() - - def lineMoved(self): - self.updateBounds() - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) - - def lineMoveFinished(self): - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) - - - def updateBounds(self): - vb = self.view().viewRect() - vals = [self.lines[0].value(), self.lines[1].value()] - if self.orientation[0] == 'h': - vb.setTop(min(vals)) - vb.setBottom(max(vals)) - else: - vb.setLeft(min(vals)) - vb.setRight(max(vals)) - if vb != self.bounds: - self.bounds = vb - self.rect.setRect(vb) - - def mousePressEvent(self, ev): - if not self.movable: - ev.ignore() - return - for l in self.lines: - l.mousePressEvent(ev) ## pass event to both lines so they move together - #if self.movable and ev.button() == QtCore.Qt.LeftButton: - #ev.accept() - #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - #else: - #ev.ignore() - - def mouseReleaseEvent(self, ev): - for l in self.lines: - l.mouseReleaseEvent(ev) - - def mouseMoveEvent(self, ev): - #print "move", ev.pos() - if not self.movable: - return - self.lines[0].blockSignals(True) # only want to update once - for l in self.lines: - l.mouseMoveEvent(ev) - self.lines[0].blockSignals(False) - #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - #self.emit(QtCore.SIGNAL('dragged'), self) - - def getRegion(self): - if self.orientation[0] == 'h': - r = (self.bounds.top(), self.bounds.bottom()) - else: - r = (self.bounds.left(), self.bounds.right()) - return (min(r), max(r)) - - def setRegion(self, rgn): - self.lines[0].setValue(rgn[0]) - self.lines[1].setValue(rgn[1]) - - -class VTickGroup(QtGui.QGraphicsPathItem): - def __init__(self, xvals=None, yrange=None, pen=None, relative=False, view=None): - QtGui.QGraphicsPathItem.__init__(self) - if yrange is None: - yrange = [0, 1] - if xvals is None: - xvals = [] - if pen is None: - pen = (200, 200, 200) - self.ticks = [] - self.xvals = [] - if view is None: - self.view = None - else: - self.view = weakref.ref(view) - self.yrange = [0,1] - self.setPen(pen) - self.setYRange(yrange, relative) - self.setXVals(xvals) - self.valid = False - - def setPen(self, pen): - pen = mkPen(pen) - QtGui.QGraphicsPathItem.setPen(self, pen) - - def setXVals(self, vals): - self.xvals = vals - self.rebuildTicks() - self.valid = False - - def setYRange(self, vals, relative=False): - self.yrange = vals - self.relative = relative - if self.view is not None: - if relative: - #QtCore.QObject.connect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) - self.view().sigRangeChanged.connect(self.rescale) - else: - try: - #QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - #QtCore.QObject.disconnect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) - self.view().sigRangeChanged.disconnect(self.rescale) - except: - pass - self.rebuildTicks() - self.valid = False - - def rescale(self): - #print "RESCALE:" - self.resetTransform() - #height = self.view.size().height() - #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - #yr = [p1.y(), p2.y()] - vb = self.view().viewRect() - p1 = vb.bottom() - vb.height() * self.yrange[0] - p2 = vb.bottom() - vb.height() * self.yrange[1] - yr = [p1, p2] - - #print " ", vb, yr - self.translate(0.0, yr[0]) - self.scale(1.0, (yr[1]-yr[0])) - #print " ", self.mapRectToScene(self.boundingRect()) - self.boundingRect() - self.update() - - def boundingRect(self): - #print "--request bounds:" - b = QtGui.QGraphicsPathItem.boundingRect(self) - #print " ", self.mapRectToScene(b) - return b - - def yRange(self): - #if self.relative: - #height = self.view.size().height() - #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - #return [p1.y(), p2.y()] - #else: - #return self.yrange - - return self.yrange - - def rebuildTicks(self): - self.path = QtGui.QPainterPath() - yrange = self.yRange() - #print "rebuild ticks:", yrange - for x in self.xvals: - #path.moveTo(x, yrange[0]) - #path.lineTo(x, yrange[1]) - self.path.moveTo(x, 0.) - self.path.lineTo(x, 1.) - self.setPath(self.path) - self.valid = True - self.rescale() - #print " done..", self.boundingRect() - - def paint(self, *args): - if not self.valid: - self.rebuildTicks() - #print "Paint", self.boundingRect() - QtGui.QGraphicsPathItem.paint(self, *args) - - -class GridItem(UIGraphicsItem): - """Class used to make square grids in plots. NOT the grid used for running scanner sequences.""" - - def __init__(self, view, bounds=None, *args): - UIGraphicsItem.__init__(self, view, bounds) - #QtGui.QGraphicsItem.__init__(self, *args) - self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - self.picture = None - - - def viewRangeChanged(self): - self.picture = None - UIGraphicsItem.viewRangeChanged(self) - #self.update() - - def paint(self, p, opt, widget): - #p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) - #p.drawRect(self.boundingRect()) - - ## draw picture - if self.picture is None: - #print "no pic, draw.." - self.generatePicture() - p.drawPicture(0, 0, self.picture) - #print "drawing Grid." - - - def generatePicture(self): - self.picture = QtGui.QPicture() - p = QtGui.QPainter() - p.begin(self.picture) - - dt = self.viewTransform().inverted()[0] - vr = self.viewRect() - unit = self.unitRect() - dim = [vr.width(), vr.height()] - lvr = self.boundingRect() - ul = np.array([lvr.left(), lvr.top()]) - br = np.array([lvr.right(), lvr.bottom()]) - - texts = [] - - if ul[1] > br[1]: - x = ul[1] - ul[1] = br[1] - br[1] = x - for i in range(2, -1, -1): ## Draw three different scales of grid - - dist = br-ul - nlTarget = 10.**i - d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) - ul1 = np.floor(ul / d) * d - br1 = np.ceil(br / d) * d - dist = br1-ul1 - nl = (dist / d) + 0.5 - for ax in range(0,2): ## Draw grid for both axes - ppl = dim[ax] / nl[ax] - c = np.clip(3.*(ppl-3), 0., 30.) - linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) - textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) - #linePen.setCosmetic(True) - #linePen.setWidth(1) - bx = (ax+1) % 2 - for x in range(0, int(nl[ax])): - linePen.setCosmetic(False) - if ax == 0: - linePen.setWidthF(self.pixelHeight()) - else: - linePen.setWidthF(self.pixelWidth()) - p.setPen(linePen) - p1 = np.array([0.,0.]) - p2 = np.array([0.,0.]) - p1[ax] = ul1[ax] + x * d[ax] - p2[ax] = p1[ax] - p1[bx] = ul[bx] - p2[bx] = br[bx] - p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) - if i < 2: - p.setPen(textPen) - if ax == 0: - x = p1[0] + unit.width() - y = ul[1] + unit.height() * 8. - else: - x = ul[0] + unit.width()*3 - y = p1[1] + unit.height() - texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) - tr = self.viewTransform() - tr.scale(1.5, 1.5) - p.setWorldTransform(tr.inverted()[0]) - for t in texts: - x = tr.map(t[0]) - p.drawText(x, t[1]) - p.end() - -class ScaleBar(UIGraphicsItem): - def __init__(self, view, size, width=5, color=(100, 100, 255)): - self.size = size - UIGraphicsItem.__init__(self, view) - self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - #self.pen = QtGui.QPen(QtGui.QColor(*color)) - #self.pen.setWidth(width) - #self.pen.setCosmetic(True) - #self.pen2 = QtGui.QPen(QtGui.QColor(0,0,0)) - #self.pen2.setWidth(width+2) - #self.pen2.setCosmetic(True) - self.brush = QtGui.QBrush(QtGui.QColor(*color)) - self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) - self.width = width - - def paint(self, p, opt, widget): - rect = self.boundingRect() - unit = self.unitRect() - y = rect.bottom() + (rect.top()-rect.bottom()) * 0.02 - y1 = y + unit.height()*self.width - x = rect.right() + (rect.left()-rect.right()) * 0.02 - x1 = x - self.size - - - p.setPen(self.pen) - p.setBrush(self.brush) - rect = QtCore.QRectF( - QtCore.QPointF(x1, y1), - QtCore.QPointF(x, y) - ) - p.translate(x1, y1) - p.scale(rect.width(), rect.height()) - p.drawRect(0, 0, 1, 1) - - alpha = np.clip(((self.size/unit.width()) - 40.) * 255. / 80., 0, 255) - p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) - for i in range(1, 10): - #x2 = x + (x1-x) * 0.1 * i - x2 = 0.1 * i - p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) - - - def setSize(self, s): - self.size = s - -class ColorScaleBar(UIGraphicsItem): - def __init__(self, view, size, offset): - self.size = size - self.offset = offset - UIGraphicsItem.__init__(self, view) - self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - self.brush = QtGui.QBrush(QtGui.QColor(200,0,0)) - self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) - self.labels = {'max': 1, 'min': 0} - self.gradient = QtGui.QLinearGradient() - self.gradient.setColorAt(0, QtGui.QColor(0,0,0)) - self.gradient.setColorAt(1, QtGui.QColor(255,0,0)) - - def setGradient(self, g): - self.gradient = g - self.update() - - def setIntColorScale(self, minVal, maxVal, *args, **kargs): - colors = [intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)] - g = QtGui.QLinearGradient() - for i in range(len(colors)): - x = float(i)/len(colors) - g.setColorAt(x, colors[i]) - self.setGradient(g) - if 'labels' not in kargs: - self.setLabels({str(minVal/10.): 0, str(maxVal): 1}) - else: - self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1}) - - def setLabels(self, l): - """Defines labels to appear next to the color scale""" - self.labels = l - self.update() - - def paint(self, p, opt, widget): - rect = self.boundingRect() ## Boundaries of visible area in scene coords. - unit = self.unitRect() ## Size of one view pixel in scene coords. - - ## determine max width of all labels - labelWidth = 0 - labelHeight = 0 - for k in self.labels: - b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) - labelWidth = max(labelWidth, b.width()) - labelHeight = max(labelHeight, b.height()) - - labelWidth *= unit.width() - labelHeight *= unit.height() - - textPadding = 2 # in px - - if self.offset[0] < 0: - x3 = rect.right() + unit.width() * self.offset[0] - x2 = x3 - labelWidth - unit.width()*textPadding*2 - x1 = x2 - unit.width() * self.size[0] - else: - x1 = rect.left() + unit.width() * self.offset[0] - x2 = x1 + unit.width() * self.size[0] - x3 = x2 + labelWidth + unit.width()*textPadding*2 - if self.offset[1] < 0: - y2 = rect.top() - unit.height() * self.offset[1] - y1 = y2 + unit.height() * self.size[1] - else: - y1 = rect.bottom() - unit.height() * self.offset[1] - y2 = y1 - unit.height() * self.size[1] - self.b = [x1,x2,x3,y1,y2,labelWidth] - - ## Draw background - p.setPen(self.pen) - p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100))) - rect = QtCore.QRectF( - QtCore.QPointF(x1 - unit.width()*textPadding, y1 + labelHeight/2 + unit.height()*textPadding), - QtCore.QPointF(x3, y2 - labelHeight/2 - unit.height()*textPadding) - ) - p.drawRect(rect) - - - ## Have to scale painter so that text and gradients are correct size. Bleh. - p.scale(unit.width(), unit.height()) - - ## Draw color bar - self.gradient.setStart(0, y1/unit.height()) - self.gradient.setFinalStop(0, y2/unit.height()) - p.setBrush(self.gradient) - rect = QtCore.QRectF( - QtCore.QPointF(x1/unit.width(), y1/unit.height()), - QtCore.QPointF(x2/unit.width(), y2/unit.height()) - ) - p.drawRect(rect) - - - ## draw labels - p.setPen(QtGui.QPen(QtGui.QColor(0,0,0))) - tx = x2 + unit.width()*textPadding - lh = labelHeight/unit.height() - for k in self.labels: - y = y1 + self.labels[k] * (y2-y1) - p.drawText(QtCore.QRectF(tx/unit.width(), y/unit.height() - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) - - diff --git a/plotConfigTemplate.py b/plotConfigTemplate.py deleted file mode 100644 index e0063b14..00000000 --- a/plotConfigTemplate.py +++ /dev/null @@ -1,295 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './lib/util/pyqtgraph/plotConfigTemplate.ui' -# -# Created: Wed May 18 20:44:20 2011 -# by: PyQt4 UI code generator 4.8.3 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName(_fromUtf8("Form")) - Form.resize(250, 340) - Form.setMaximumSize(QtCore.QSize(250, 350)) - self.gridLayout_3 = QtGui.QGridLayout(Form) - self.gridLayout_3.setMargin(0) - self.gridLayout_3.setSpacing(0) - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) - self.tabWidget = QtGui.QTabWidget(Form) - self.tabWidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.tabWidget.setObjectName(_fromUtf8("tabWidget")) - self.tab = QtGui.QWidget() - self.tab.setObjectName(_fromUtf8("tab")) - self.verticalLayout = QtGui.QVBoxLayout(self.tab) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) - self.groupBox = QtGui.QGroupBox(self.tab) - self.groupBox.setObjectName(_fromUtf8("groupBox")) - self.gridLayout = QtGui.QGridLayout(self.groupBox) - self.gridLayout.setMargin(0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - self.xManualRadio = QtGui.QRadioButton(self.groupBox) - self.xManualRadio.setObjectName(_fromUtf8("xManualRadio")) - self.gridLayout.addWidget(self.xManualRadio, 0, 0, 1, 1) - self.xMinText = QtGui.QLineEdit(self.groupBox) - self.xMinText.setObjectName(_fromUtf8("xMinText")) - self.gridLayout.addWidget(self.xMinText, 0, 1, 1, 1) - self.xMaxText = QtGui.QLineEdit(self.groupBox) - self.xMaxText.setObjectName(_fromUtf8("xMaxText")) - self.gridLayout.addWidget(self.xMaxText, 0, 2, 1, 1) - self.xAutoRadio = QtGui.QRadioButton(self.groupBox) - self.xAutoRadio.setChecked(True) - self.xAutoRadio.setObjectName(_fromUtf8("xAutoRadio")) - self.gridLayout.addWidget(self.xAutoRadio, 1, 0, 1, 1) - self.xAutoPercentSpin = QtGui.QSpinBox(self.groupBox) - self.xAutoPercentSpin.setEnabled(True) - self.xAutoPercentSpin.setMinimum(1) - self.xAutoPercentSpin.setMaximum(100) - self.xAutoPercentSpin.setSingleStep(1) - self.xAutoPercentSpin.setProperty(_fromUtf8("value"), 100) - self.xAutoPercentSpin.setObjectName(_fromUtf8("xAutoPercentSpin")) - self.gridLayout.addWidget(self.xAutoPercentSpin, 1, 1, 1, 2) - self.xLinkCombo = QtGui.QComboBox(self.groupBox) - self.xLinkCombo.setObjectName(_fromUtf8("xLinkCombo")) - self.gridLayout.addWidget(self.xLinkCombo, 2, 1, 1, 2) - self.xMouseCheck = QtGui.QCheckBox(self.groupBox) - self.xMouseCheck.setChecked(True) - self.xMouseCheck.setObjectName(_fromUtf8("xMouseCheck")) - self.gridLayout.addWidget(self.xMouseCheck, 3, 1, 1, 1) - self.xLogCheck = QtGui.QCheckBox(self.groupBox) - self.xLogCheck.setObjectName(_fromUtf8("xLogCheck")) - self.gridLayout.addWidget(self.xLogCheck, 3, 0, 1, 1) - self.label = QtGui.QLabel(self.groupBox) - self.label.setObjectName(_fromUtf8("label")) - self.gridLayout.addWidget(self.label, 2, 0, 1, 1) - self.verticalLayout.addWidget(self.groupBox) - self.groupBox_2 = QtGui.QGroupBox(self.tab) - self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) - self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) - self.gridLayout_2.setMargin(0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.yManualRadio = QtGui.QRadioButton(self.groupBox_2) - self.yManualRadio.setObjectName(_fromUtf8("yManualRadio")) - self.gridLayout_2.addWidget(self.yManualRadio, 0, 0, 1, 1) - self.yMinText = QtGui.QLineEdit(self.groupBox_2) - self.yMinText.setObjectName(_fromUtf8("yMinText")) - self.gridLayout_2.addWidget(self.yMinText, 0, 1, 1, 1) - self.yMaxText = QtGui.QLineEdit(self.groupBox_2) - self.yMaxText.setObjectName(_fromUtf8("yMaxText")) - self.gridLayout_2.addWidget(self.yMaxText, 0, 2, 1, 1) - self.yAutoRadio = QtGui.QRadioButton(self.groupBox_2) - self.yAutoRadio.setChecked(True) - self.yAutoRadio.setObjectName(_fromUtf8("yAutoRadio")) - self.gridLayout_2.addWidget(self.yAutoRadio, 1, 0, 1, 1) - self.yAutoPercentSpin = QtGui.QSpinBox(self.groupBox_2) - self.yAutoPercentSpin.setEnabled(True) - self.yAutoPercentSpin.setMinimum(1) - self.yAutoPercentSpin.setMaximum(100) - self.yAutoPercentSpin.setSingleStep(1) - self.yAutoPercentSpin.setProperty(_fromUtf8("value"), 100) - self.yAutoPercentSpin.setObjectName(_fromUtf8("yAutoPercentSpin")) - self.gridLayout_2.addWidget(self.yAutoPercentSpin, 1, 1, 1, 2) - self.yLinkCombo = QtGui.QComboBox(self.groupBox_2) - self.yLinkCombo.setObjectName(_fromUtf8("yLinkCombo")) - self.gridLayout_2.addWidget(self.yLinkCombo, 2, 1, 1, 2) - self.yMouseCheck = QtGui.QCheckBox(self.groupBox_2) - self.yMouseCheck.setChecked(True) - self.yMouseCheck.setObjectName(_fromUtf8("yMouseCheck")) - self.gridLayout_2.addWidget(self.yMouseCheck, 3, 1, 1, 1) - self.yLogCheck = QtGui.QCheckBox(self.groupBox_2) - self.yLogCheck.setObjectName(_fromUtf8("yLogCheck")) - self.gridLayout_2.addWidget(self.yLogCheck, 3, 0, 1, 1) - self.label_2 = QtGui.QLabel(self.groupBox_2) - self.label_2.setObjectName(_fromUtf8("label_2")) - self.gridLayout_2.addWidget(self.label_2, 2, 0, 1, 1) - self.verticalLayout.addWidget(self.groupBox_2) - self.tabWidget.addTab(self.tab, _fromUtf8("")) - self.tab_2 = QtGui.QWidget() - self.tab_2.setObjectName(_fromUtf8("tab_2")) - self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab_2) - self.verticalLayout_2.setSpacing(0) - self.verticalLayout_2.setMargin(0) - self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) - self.powerSpectrumGroup = QtGui.QGroupBox(self.tab_2) - self.powerSpectrumGroup.setCheckable(True) - self.powerSpectrumGroup.setChecked(False) - self.powerSpectrumGroup.setObjectName(_fromUtf8("powerSpectrumGroup")) - self.verticalLayout_2.addWidget(self.powerSpectrumGroup) - self.decimateGroup = QtGui.QGroupBox(self.tab_2) - self.decimateGroup.setCheckable(True) - self.decimateGroup.setObjectName(_fromUtf8("decimateGroup")) - self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) - self.gridLayout_4.setMargin(0) - self.gridLayout_4.setSpacing(0) - self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio")) - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) - self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) - self.downsampleSpin.setMinimum(1) - self.downsampleSpin.setMaximum(100000) - self.downsampleSpin.setProperty(_fromUtf8("value"), 1) - self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio")) - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) - self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) - self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) - self.verticalLayout_2.addWidget(self.decimateGroup) - self.averageGroup = QtGui.QGroupBox(self.tab_2) - self.averageGroup.setCheckable(True) - self.averageGroup.setChecked(False) - self.averageGroup.setObjectName(_fromUtf8("averageGroup")) - self.gridLayout_5 = QtGui.QGridLayout(self.averageGroup) - self.gridLayout_5.setMargin(0) - self.gridLayout_5.setSpacing(0) - self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) - self.avgParamList = QtGui.QListWidget(self.averageGroup) - self.avgParamList.setObjectName(_fromUtf8("avgParamList")) - self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.verticalLayout_2.addWidget(self.averageGroup) - self.tabWidget.addTab(self.tab_2, _fromUtf8("")) - self.tab_3 = QtGui.QWidget() - self.tab_3.setObjectName(_fromUtf8("tab_3")) - self.verticalLayout_3 = QtGui.QVBoxLayout(self.tab_3) - self.verticalLayout_3.setObjectName(_fromUtf8("verticalLayout_3")) - self.alphaGroup = QtGui.QGroupBox(self.tab_3) - self.alphaGroup.setCheckable(True) - self.alphaGroup.setObjectName(_fromUtf8("alphaGroup")) - self.horizontalLayout = QtGui.QHBoxLayout(self.alphaGroup) - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.autoAlphaCheck = QtGui.QCheckBox(self.alphaGroup) - self.autoAlphaCheck.setChecked(False) - self.autoAlphaCheck.setObjectName(_fromUtf8("autoAlphaCheck")) - self.horizontalLayout.addWidget(self.autoAlphaCheck) - self.alphaSlider = QtGui.QSlider(self.alphaGroup) - self.alphaSlider.setMaximum(1000) - self.alphaSlider.setProperty(_fromUtf8("value"), 1000) - self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) - self.alphaSlider.setObjectName(_fromUtf8("alphaSlider")) - self.horizontalLayout.addWidget(self.alphaSlider) - self.verticalLayout_3.addWidget(self.alphaGroup) - self.gridGroup = QtGui.QGroupBox(self.tab_3) - self.gridGroup.setCheckable(True) - self.gridGroup.setChecked(False) - self.gridGroup.setObjectName(_fromUtf8("gridGroup")) - self.verticalLayout_4 = QtGui.QVBoxLayout(self.gridGroup) - self.verticalLayout_4.setObjectName(_fromUtf8("verticalLayout_4")) - self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) - self.gridAlphaSlider.setMaximum(255) - self.gridAlphaSlider.setProperty(_fromUtf8("value"), 70) - self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) - self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider")) - self.verticalLayout_4.addWidget(self.gridAlphaSlider) - self.verticalLayout_3.addWidget(self.gridGroup) - self.pointsGroup = QtGui.QGroupBox(self.tab_3) - self.pointsGroup.setCheckable(True) - self.pointsGroup.setObjectName(_fromUtf8("pointsGroup")) - self.verticalLayout_5 = QtGui.QVBoxLayout(self.pointsGroup) - self.verticalLayout_5.setObjectName(_fromUtf8("verticalLayout_5")) - self.autoPointsCheck = QtGui.QCheckBox(self.pointsGroup) - self.autoPointsCheck.setChecked(True) - self.autoPointsCheck.setObjectName(_fromUtf8("autoPointsCheck")) - self.verticalLayout_5.addWidget(self.autoPointsCheck) - self.verticalLayout_3.addWidget(self.pointsGroup) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.verticalLayout_3.addItem(spacerItem) - self.tabWidget.addTab(self.tab_3, _fromUtf8("")) - self.tab_4 = QtGui.QWidget() - self.tab_4.setObjectName(_fromUtf8("tab_4")) - self.gridLayout_7 = QtGui.QGridLayout(self.tab_4) - self.gridLayout_7.setObjectName(_fromUtf8("gridLayout_7")) - spacerItem1 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem1, 0, 0, 1, 1) - self.gridLayout_6 = QtGui.QGridLayout() - self.gridLayout_6.setObjectName(_fromUtf8("gridLayout_6")) - self.saveSvgBtn = QtGui.QPushButton(self.tab_4) - self.saveSvgBtn.setObjectName(_fromUtf8("saveSvgBtn")) - self.gridLayout_6.addWidget(self.saveSvgBtn, 0, 0, 1, 1) - self.saveImgBtn = QtGui.QPushButton(self.tab_4) - self.saveImgBtn.setObjectName(_fromUtf8("saveImgBtn")) - self.gridLayout_6.addWidget(self.saveImgBtn, 1, 0, 1, 1) - self.saveMaBtn = QtGui.QPushButton(self.tab_4) - self.saveMaBtn.setObjectName(_fromUtf8("saveMaBtn")) - self.gridLayout_6.addWidget(self.saveMaBtn, 2, 0, 1, 1) - self.saveCsvBtn = QtGui.QPushButton(self.tab_4) - self.saveCsvBtn.setObjectName(_fromUtf8("saveCsvBtn")) - self.gridLayout_6.addWidget(self.saveCsvBtn, 3, 0, 1, 1) - self.gridLayout_7.addLayout(self.gridLayout_6, 0, 1, 1, 1) - spacerItem2 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem2, 0, 2, 1, 1) - spacerItem3 = QtGui.QSpacerItem(20, 211, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_7.addItem(spacerItem3, 1, 1, 1, 1) - self.tabWidget.addTab(self.tab_4, _fromUtf8("")) - self.gridLayout_3.addWidget(self.tabWidget, 0, 0, 1, 1) - - self.retranslateUi(Form) - self.tabWidget.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox.setTitle(QtGui.QApplication.translate("Form", "X Axis", None, QtGui.QApplication.UnicodeUTF8)) - self.xManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.xMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.xMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.xAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.xAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.xMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) - self.xLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_2.setTitle(QtGui.QApplication.translate("Form", "Y Axis", None, QtGui.QApplication.UnicodeUTF8)) - self.yManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.yMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.yMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.yAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.yAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.yMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) - self.yLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtGui.QApplication.translate("Form", "Scale", None, QtGui.QApplication.UnicodeUTF8)) - self.powerSpectrumGroup.setTitle(QtGui.QApplication.translate("Form", "Power Spectrum", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", 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.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("Form", "Data", None, QtGui.QApplication.UnicodeUTF8)) - self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) - self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.gridGroup.setTitle(QtGui.QApplication.translate("Form", "Grid", None, QtGui.QApplication.UnicodeUTF8)) - self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QtGui.QApplication.translate("Form", "Display", None, QtGui.QApplication.UnicodeUTF8)) - self.saveSvgBtn.setText(QtGui.QApplication.translate("Form", "SVG", None, QtGui.QApplication.UnicodeUTF8)) - self.saveImgBtn.setText(QtGui.QApplication.translate("Form", "Image", None, QtGui.QApplication.UnicodeUTF8)) - self.saveMaBtn.setText(QtGui.QApplication.translate("Form", "MetaArray", None, QtGui.QApplication.UnicodeUTF8)) - self.saveCsvBtn.setText(QtGui.QApplication.translate("Form", "CSV", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/plotConfigTemplate.ui b/plotConfigTemplate.ui deleted file mode 100644 index 7baeb337..00000000 --- a/plotConfigTemplate.ui +++ /dev/null @@ -1,563 +0,0 @@ - - - Form - - - - 0 - 0 - 250 - 340 - - - - - 250 - 350 - - - - Form - - - - 0 - - - 0 - - - - - - 16777215 - 16777215 - - - - 0 - - - - Scale - - - - 0 - - - 0 - - - - - X Axis - - - - 0 - - - 0 - - - - - Manual - - - - - - - 0 - - - - - - - 0 - - - - - - - Auto - - - true - - - - - - - true - - - % - - - 1 - - - 100 - - - 1 - - - 100 - - - - - - - - - - Mouse - - - true - - - - - - - Log - - - - - - - Link with: - - - - - - - - - - Y Axis - - - - 0 - - - 0 - - - - - Manual - - - - - - - 0 - - - - - - - 0 - - - - - - - Auto - - - true - - - - - - - true - - - % - - - 1 - - - 100 - - - 1 - - - 100 - - - - - - - - - - Mouse - - - true - - - - - - - Log - - - - - - - Link with: - - - - - - - - - - - Data - - - - 0 - - - 0 - - - - - Power Spectrum - - - true - - - false - - - - - - - Downsample - - - true - - - - 0 - - - 0 - - - - - Manual - - - true - - - - - - - 1 - - - 100000 - - - 1 - - - - - - - Auto - - - false - - - - - - - If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. - - - Max Traces: - - - - - - - If multiple curves are displayed in this plot, check "Max Traces" and set this value to limit the number of traces that are displayed. - - - - - - - If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden). - - - Forget hidden traces - - - - - - - - - - Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available). - - - Average - - - true - - - false - - - - 0 - - - 0 - - - - - - - - - - - - Display - - - - - - Alpha - - - true - - - - - - Auto - - - false - - - - - - - 1000 - - - 1000 - - - Qt::Horizontal - - - - - - - - - - Grid - - - true - - - false - - - - - - 255 - - - 70 - - - Qt::Horizontal - - - - - - - - - - Points - - - true - - - - - - Auto - - - true - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Save - - - - - - Qt::Horizontal - - - - 59 - 20 - - - - - - - - - - SVG - - - - - - - Image - - - - - - - MetaArray - - - - - - - CSV - - - - - - - - - Qt::Horizontal - - - - 59 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 211 - - - - - - - - - - - - - diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py new file mode 100644 index 00000000..a5f60059 --- /dev/null +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -0,0 +1,562 @@ +from pyqtgraph.Qt import QtCore, QtGui + +from pyqtgraph.python2_3 import sortList +#try: + #from PyQt4 import QtOpenGL + #HAVE_OPENGL = True +#except ImportError: + #HAVE_OPENGL = False + +import weakref +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +import pyqtgraph.ptime as ptime +from .mouseEvents import * +import pyqtgraph.debug as debug +from . import exportDialog + +if hasattr(QtCore, 'PYQT_VERSION'): + try: + import sip + HAVE_SIP = True + except ImportError: + HAVE_SIP = False +else: + HAVE_SIP = False + + +__all__ = ['GraphicsScene'] + +class GraphicsScene(QtGui.QGraphicsScene): + """ + Extension of QGraphicsScene that implements a complete, parallel mouse event system. + (It would have been preferred to just alter the way QGraphicsScene creates and delivers + events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent + is private) + + * Generates MouseClicked events in addition to the usual press/move/release events. + (This works around a problem where it is impossible to have one item respond to a + drag if another is watching for a click.) + * Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects + * Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. + * Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. + This lets us indicate unambiguously to the user which item they are about to click/drag on + * Eats mouseMove events that occur too soon after a mouse press. + * Reimplements items() and itemAt() to circumvent PyQt bug + + Mouse interaction is as follows: + + 1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents + as well as custom HoverEvents. + 2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button), + acceptDrags(button) or both. If this method call returns True, this informs the item that _if_ + the user clicks/drags the specified mouse button, the item is guaranteed to be the + recipient of click/drag events (the item may wish to change its appearance to indicate this). + If the call to acceptClicks/Drags returns False, then the item is guaranteed to *not* receive + the requested event (because another item has already accepted it). + 3) If the mouse is clicked, a mousePressEvent is generated as usual. If any items accept this press event, then + No click/drag events will be generated and mouse interaction proceeds as defined by Qt. This allows + items to function properly if they are expecting the usual press/move/release sequence of events. + (It is recommended that items do NOT accept press events, and instead use click/drag events) + Note: The default implementation of QGraphicsItem.mousePressEvent will *accept* the event if the + item is has its Selectable or Movable flags enabled. You may need to override this behavior. + 4) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. + If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released, + then a mouseDragEvent is generated. + If no drag events are generated before the button is released, then a mouseClickEvent is generated. + 5) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent + in step 1. If no such items exist, then the scene attempts to deliver the events to items near the event. + ClickEvents may be delivered in this way even if no + item originally claimed it could accept the click. DragEvents may only be delivered this way if it is the initial + move in a drag. + """ + + sigMouseHover = QtCore.Signal(object) ## emits a list of objects hovered over + sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move + sigMouseClicked = QtCore.Signal(object) ## emitted when mouse is clicked. Check for event.isAccepted() to see whether the event has already been acted on. + + _addressCache = weakref.WeakValueDictionary() + + ExportDirectory = None + + @classmethod + def registerObject(cls, obj): + """ + Workaround for PyQt bug in qgraphicsscene.items() + All subclasses of QGraphicsObject must register themselves with this function. + (otherwise, mouse interaction with those objects will likely fail) + """ + if HAVE_SIP and isinstance(obj, sip.wrapper): + cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj + + + def __init__(self, clickRadius=2, moveDistance=5): + QtGui.QGraphicsScene.__init__(self) + self.setClickRadius(clickRadius) + self.setMoveDistance(moveDistance) + self.exportDirectory = None + + self.clickEvents = [] + self.dragButtons = [] + self.mouseGrabber = None + self.dragItem = None + self.lastDrag = None + self.hoverItems = weakref.WeakKeyDictionary() + self.lastHoverEvent = None + #self.searchRect = QtGui.QGraphicsRectItem() + #self.searchRect.setPen(fn.mkPen(200,0,0)) + #self.addItem(self.searchRect) + + self.contextMenu = [QtGui.QAction("Export...", self)] + self.contextMenu[0].triggered.connect(self.showExportDialog) + + self.exportDialog = None + + + def setClickRadius(self, r): + """ + Set the distance away from mouse clicks to search for interacting items. + When clicking, the scene searches first for items that directly intersect the click position + followed by any other items that are within a rectangle that extends r pixels away from the + click position. + """ + self._clickRadius = r + + def setMoveDistance(self, d): + """ + Set the distance the mouse must move after a press before mouseMoveEvents will be delivered. + This ensures that clicks with a small amount of movement are recognized as clicks instead of + drags. + """ + self._moveDistance = d + + def mousePressEvent(self, ev): + #print 'scenePress' + QtGui.QGraphicsScene.mousePressEvent(self, ev) + #print "mouseGrabberItem: ", self.mouseGrabberItem() + if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + self.clickEvents.append(MouseClickEvent(ev)) + + ## set focus on the topmost focusable item under this click + items = self.items(ev.scenePos()) + for i in items: + if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0: + i.setFocus(QtCore.Qt.MouseFocusReason) + break + #else: + #addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem)) + #item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem()) + #print "click grabbed by:", item + + def mouseMoveEvent(self, ev): + self.sigMouseMoved.emit(ev.scenePos()) + + ## First allow QGraphicsScene to deliver hoverEnter/Move/ExitEvents + QtGui.QGraphicsScene.mouseMoveEvent(self, ev) + + ## Next deliver our own HoverEvents + self.sendHoverEvents(ev) + + if int(ev.buttons()) != 0: ## button is pressed; send mouseMoveEvents and mouseDragEvents + QtGui.QGraphicsScene.mouseMoveEvent(self, ev) + if self.mouseGrabberItem() is None: + now = ptime.time() + init = False + ## keep track of which buttons are involved in dragging + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: + if int(ev.buttons() & btn) == 0: + continue + if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet + cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] + dist = Point(ev.screenPos() - cev.screenPos()) + if dist.length() < self._moveDistance and now - cev.time() < 0.5: + continue + init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True + self.dragButtons.append(int(btn)) + + ## If we have dragged buttons, deliver a drag event + if len(self.dragButtons) > 0: + if self.sendDragEvent(ev, init=init): + ev.accept() + + def leaveEvent(self, ev): ## inform items that mouse is gone + if len(self.dragButtons) == 0: + self.sendHoverEvents(ev, exitOnly=True) + + + def mouseReleaseEvent(self, ev): + #print 'sceneRelease' + if self.mouseGrabberItem() is None: + #print "sending click/drag event" + if ev.button() in self.dragButtons: + if self.sendDragEvent(ev, final=True): + #print "sent drag event" + ev.accept() + self.dragButtons.remove(ev.button()) + else: + cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())] + if self.sendClickEvent(cev[0]): + #print "sent click event" + ev.accept() + self.clickEvents.remove(cev[0]) + + if int(ev.buttons()) == 0: + self.dragItem = None + self.dragButtons = [] + self.clickEvents = [] + self.lastDrag = None + QtGui.QGraphicsScene.mouseReleaseEvent(self, ev) + + self.sendHoverEvents(ev) ## let items prepare for next click/drag + + def mouseDoubleClickEvent(self, ev): + QtGui.QGraphicsScene.mouseDoubleClickEvent(self, ev) + if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + self.clickEvents.append(MouseClickEvent(ev, double=True)) + + def sendHoverEvents(self, ev, exitOnly=False): + ## if exitOnly, then just inform all previously hovered items that the mouse has left. + + if exitOnly: + acceptable=False + items = [] + event = HoverEvent(None, acceptable) + else: + acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event. + event = HoverEvent(ev, acceptable) + items = self.itemsNearEvent(event) + self.sigMouseHover.emit(items) + + prevItems = list(self.hoverItems.keys()) + + for item in items: + if hasattr(item, 'hoverEvent'): + event.currentItem = item + if item not in self.hoverItems: + self.hoverItems[item] = None + event.enter = True + else: + prevItems.remove(item) + event.enter = False + + try: + item.hoverEvent(event) + except: + debug.printExc("Error sending hover event:") + + event.enter = False + event.exit = True + for item in prevItems: + event.currentItem = item + try: + item.hoverEvent(event) + except: + debug.printExc("Error sending hover exit event:") + finally: + del self.hoverItems[item] + + if hasattr(ev, 'buttons') and int(ev.buttons()) == 0: + self.lastHoverEvent = event ## save this so we can ask about accepted events later. + + + def sendDragEvent(self, ev, init=False, final=False): + ## Send a MouseDragEvent to the current dragItem or to + ## items near the beginning of the drag + event = MouseDragEvent(ev, self.clickEvents[0], self.lastDrag, start=init, finish=final) + #print "dragEvent: init=", init, 'final=', final, 'self.dragItem=', self.dragItem + if init and self.dragItem is None: + if self.lastHoverEvent is not None: + acceptedItem = self.lastHoverEvent.dragItems().get(event.button(), None) + else: + acceptedItem = None + + if acceptedItem is not None: + #print "Drag -> pre-selected item:", acceptedItem + self.dragItem = acceptedItem + event.currentItem = self.dragItem + try: + self.dragItem.mouseDragEvent(event) + except: + debug.printExc("Error sending drag event:") + + else: + #print "drag -> new item" + for item in self.itemsNearEvent(event): + #print "check item:", item + if not item.isVisible() or not item.isEnabled(): + continue + if hasattr(item, 'mouseDragEvent'): + event.currentItem = item + try: + item.mouseDragEvent(event) + except: + debug.printExc("Error sending drag event:") + if event.isAccepted(): + #print " --> accepted" + self.dragItem = item + if int(item.flags() & item.ItemIsFocusable) > 0: + item.setFocus(QtCore.Qt.MouseFocusReason) + break + elif self.dragItem is not None: + event.currentItem = self.dragItem + try: + self.dragItem.mouseDragEvent(event) + except: + debug.printExc("Error sending hover exit event:") + + self.lastDrag = event + + return event.isAccepted() + + + def sendClickEvent(self, ev): + ## if we are in mid-drag, click events may only go to the dragged item. + if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'): + ev.currentItem = self.dragItem + self.dragItem.mouseClickEvent(ev) + + ## otherwise, search near the cursor + else: + if self.lastHoverEvent is not None: + acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None) + else: + acceptedItem = None + + if acceptedItem is not None: + ev.currentItem = acceptedItem + try: + acceptedItem.mouseClickEvent(ev) + except: + debug.printExc("Error sending click event:") + else: + for item in self.itemsNearEvent(ev): + if not item.isVisible() or not item.isEnabled(): + continue + if hasattr(item, 'mouseClickEvent'): + ev.currentItem = item + try: + item.mouseClickEvent(ev) + except: + debug.printExc("Error sending click event:") + + if ev.isAccepted(): + if int(item.flags() & item.ItemIsFocusable) > 0: + item.setFocus(QtCore.Qt.MouseFocusReason) + break + #if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton: + #print "GraphicsScene emitting sigSceneContextMenu" + #self.sigMouseClicked.emit(ev) + #ev.accept() + self.sigMouseClicked.emit(ev) + return ev.isAccepted() + + #def claimEvent(self, item, button, eventType): + #key = (button, eventType) + #if key in self.claimedEvents: + #return False + #self.claimedEvents[key] = item + #print "event", key, "claimed by", item + #return True + + + def items(self, *args): + #print 'args:', args + items = QtGui.QGraphicsScene.items(self, *args) + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + items2 = list(map(self.translateGraphicsItem, items)) + #if HAVE_SIP and isinstance(self, sip.wrapper): + #items2 = [] + #for i in items: + #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) + #i2 = GraphicsScene._addressCache.get(addr, i) + ##print i, "==>", i2 + #items2.append(i2) + #print 'items:', items + return items2 + + def selectedItems(self, *args): + items = QtGui.QGraphicsScene.selectedItems(self, *args) + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + #if HAVE_SIP and isinstance(self, sip.wrapper): + #items2 = [] + #for i in items: + #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) + #i2 = GraphicsScene._addressCache.get(addr, i) + ##print i, "==>", i2 + #items2.append(i2) + items2 = list(map(self.translateGraphicsItem, items)) + + #print 'items:', items + return items2 + + def itemAt(self, *args): + item = QtGui.QGraphicsScene.itemAt(self, *args) + + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + #if HAVE_SIP and isinstance(self, sip.wrapper): + #addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) + #item = GraphicsScene._addressCache.get(addr, item) + #return item + return self.translateGraphicsItem(item) + + def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder): + """ + Return an iterator that iterates first through the items that directly intersect point (in Z order) + followed by any other items that are within the scene's click radius. + """ + #tr = self.getViewWidget(event.widget()).transform() + view = self.views()[0] + tr = view.viewportTransform() + r = self._clickRadius + rect = view.mapToScene(QtCore.QRect(0, 0, 2*r, 2*r)).boundingRect() + + seen = set() + if hasattr(event, 'buttonDownScenePos'): + point = event.buttonDownScenePos() + else: + point = event.scenePos() + w = rect.width() + h = rect.height() + rgn = QtCore.QRectF(point.x()-w, point.y()-h, 2*w, 2*h) + #self.searchRect.setRect(rgn) + + + items = self.items(point, selMode, sortOrder, tr) + + ## remove items whose shape does not contain point (scene.items() apparently sucks at this) + items2 = [] + for item in items: + shape = item.shape() + if shape is None: + continue + if item.mapToScene(shape).contains(point): + items2.append(item) + + ## Sort by descending Z-order (don't trust scene.itms() to do this either) + ## use 'absolute' z value, which is the sum of all item/parent ZValues + def absZValue(item): + if item is None: + return 0 + return item.zValue() + absZValue(item.parentItem()) + + sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a))) + + return items2 + + #for item in items: + ##seen.add(item) + + #shape = item.mapToScene(item.shape()) + #if not shape.contains(point): + #continue + #yield item + #for item in self.items(rgn, selMode, sortOrder, tr): + ##if item not in seen: + #yield item + + def getViewWidget(self): + return self.views()[0] + + #def getViewWidget(self, widget): + ### same pyqt bug -- mouseEvent.widget() doesn't give us the original python object. + ### [[doesn't seem to work correctly]] + #if HAVE_SIP and isinstance(self, sip.wrapper): + #addr = sip.unwrapinstance(sip.cast(widget, QtGui.QWidget)) + ##print "convert", widget, addr + #for v in self.views(): + #addr2 = sip.unwrapinstance(sip.cast(v, QtGui.QWidget)) + ##print " check:", v, addr2 + #if addr2 == addr: + #return v + #else: + #return widget + + def addParentContextMenus(self, item, menu, event): + """ + Can be called by any item in the scene to expand its context menu to include parent context menus. + Parents may implement getContextMenus to add new menus / actions to the existing menu. + getContextMenus must accept 1 argument (the event that generated the original menu) and + return a single QMenu or a list of QMenus. + + The final menu will look like: + + | Original Item 1 + | Original Item 2 + | ... + | Original Item N + | ------------------ + | Parent Item 1 + | Parent Item 2 + | ... + | Grandparent Item 1 + | ... + + + ============== ================================================== + **Arguments:** + item The item that initially created the context menu + (This is probably the item making the call to this function) + menu The context menu being shown by the item + event The original event that triggered the menu to appear. + ============== ================================================== + """ + + #items = self.itemsNearEvent(ev) + menusToAdd = [] + while item is not self: + item = item.parentItem() + + if item is None: + item = self + + if not hasattr(item, "getContextMenus"): + continue + + subMenus = item.getContextMenus(event) + if subMenus is None: + continue + if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus + subMenus = [subMenus] + + for sm in subMenus: + menusToAdd.append(sm) + + if len(menusToAdd) > 0: + menu.addSeparator() + + for m in menusToAdd: + if isinstance(m, QtGui.QMenu): + menu.addMenu(m) + elif isinstance(m, QtGui.QAction): + menu.addAction(m) + else: + raise Exception("Cannot add object %s (type=%s) to QMenu." % (str(m), str(type(m)))) + + return menu + + def getContextMenus(self, event): + self.contextMenuItem = event.acceptedItem + return self.contextMenu + + def showExportDialog(self): + if self.exportDialog is None: + self.exportDialog = exportDialog.ExportDialog(self) + self.exportDialog.show(self.contextMenuItem) + + @staticmethod + def translateGraphicsItem(item): + ## for fixing pyqt bugs where the wrong item is returned + if HAVE_SIP and isinstance(item, sip.wrapper): + addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) + item = GraphicsScene._addressCache.get(addr, item) + return item + + @staticmethod + def translateGraphicsItems(items): + return list(map(GraphicsScene.translateGraphicsItem, items)) + + + diff --git a/pyqtgraph/GraphicsScene/__init__.py b/pyqtgraph/GraphicsScene/__init__.py new file mode 100644 index 00000000..abe42c6f --- /dev/null +++ b/pyqtgraph/GraphicsScene/__init__.py @@ -0,0 +1 @@ +from .GraphicsScene import * diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py new file mode 100644 index 00000000..dafcd501 --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -0,0 +1,129 @@ +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +import pyqtgraph as pg +import pyqtgraph.exporters as exporters + +if USE_PYSIDE: + from . import exportDialogTemplate_pyside as exportDialogTemplate +else: + from . import exportDialogTemplate_pyqt as exportDialogTemplate + + +class ExportDialog(QtGui.QWidget): + def __init__(self, scene): + QtGui.QWidget.__init__(self) + self.setVisible(False) + self.setWindowTitle("Export") + self.shown = False + self.currentExporter = None + self.scene = scene + + self.selectBox = QtGui.QGraphicsRectItem() + self.selectBox.setPen(pg.mkPen('y', width=3, style=QtCore.Qt.DashLine)) + self.selectBox.hide() + self.scene.addItem(self.selectBox) + + self.ui = exportDialogTemplate.Ui_Form() + self.ui.setupUi(self) + + self.ui.closeBtn.clicked.connect(self.close) + self.ui.exportBtn.clicked.connect(self.exportClicked) + self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged) + self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged) + + + def show(self, item=None): + if item is not None: + while not isinstance(item, pg.ViewBox) and not isinstance(item, pg.PlotItem) and item is not None: + item = item.parentItem() + self.updateItemList(select=item) + self.setVisible(True) + self.activateWindow() + self.raise_() + self.selectBox.setVisible(True) + + if not self.shown: + self.shown = True + vcenter = self.scene.getViewWidget().geometry().center() + self.setGeometry(vcenter.x()-self.width()/2, vcenter.y()-self.height()/2, self.width(), self.height()) + + def updateItemList(self, select=None): + self.ui.itemTree.clear() + si = QtGui.QTreeWidgetItem(["Entire Scene"]) + si.gitem = self.scene + self.ui.itemTree.addTopLevelItem(si) + self.ui.itemTree.setCurrentItem(si) + si.setExpanded(True) + for child in self.scene.items(): + if child.parentItem() is None: + self.updateItemTree(child, si, select=select) + + def updateItemTree(self, item, treeItem, select=None): + si = None + if isinstance(item, pg.ViewBox): + si = QtGui.QTreeWidgetItem(['ViewBox']) + elif isinstance(item, pg.PlotItem): + si = QtGui.QTreeWidgetItem(['Plot']) + + if si is not None: + si.gitem = item + treeItem.addChild(si) + treeItem = si + if si.gitem is select: + self.ui.itemTree.setCurrentItem(si) + + for ch in item.childItems(): + self.updateItemTree(ch, treeItem, select=select) + + + def exportItemChanged(self, item, prev): + if item is None: + return + if item.gitem is self.scene: + newBounds = self.scene.views()[0].viewRect() + else: + newBounds = item.gitem.sceneBoundingRect() + self.selectBox.setRect(newBounds) + self.selectBox.show() + self.updateFormatList() + + def updateFormatList(self): + current = self.ui.formatList.currentItem() + if current is not None: + current = str(current.text()) + self.ui.formatList.clear() + self.exporterClasses = {} + gotCurrent = False + for exp in exporters.listExporters(): + self.ui.formatList.addItem(exp.Name) + self.exporterClasses[exp.Name] = exp + if exp.Name == current: + self.ui.formatList.setCurrentRow(self.ui.formatList.count()-1) + gotCurrent = True + + if not gotCurrent: + self.ui.formatList.setCurrentRow(0) + + def exportFormatChanged(self, item, prev): + if item is None: + self.currentExporter = None + self.ui.paramTree.clear() + return + expClass = self.exporterClasses[str(item.text())] + exp = expClass(item=self.ui.itemTree.currentItem().gitem) + params = exp.parameters() + if params is None: + self.ui.paramTree.clear() + else: + self.ui.paramTree.setParameters(params) + self.currentExporter = exp + + def exportClicked(self): + self.selectBox.hide() + self.currentExporter.export() + + def close(self): + self.selectBox.setVisible(False) + self.setVisible(False) + + + diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate.ui b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui new file mode 100644 index 00000000..c81c8831 --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate.ui @@ -0,0 +1,93 @@ + + + Form + + + + 0 + 0 + 241 + 367 + + + + Export + + + + 0 + + + + + Item to export: + + + + + + + false + + + + 1 + + + + + + + + Export format + + + + + + + + + + Export + + + + + + + Close + + + + + + + false + + + + 1 + + + + + + + + Export options + + + + + + + + ParameterTree + QTreeWidget +
pyqtgraph.parametertree
+
+
+ + +
diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py new file mode 100644 index 00000000..20609b51 --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' +# +# Created: Sun Sep 9 14:41:31 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(241, 367) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.label = QtGui.QLabel(Form) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtGui.QTreeWidget(Form) + self.itemTree.setObjectName(_fromUtf8("itemTree")) + self.itemTree.headerItem().setText(0, _fromUtf8("1")) + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtGui.QLabel(Form) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtGui.QListWidget(Form) + self.formatList.setObjectName(_fromUtf8("formatList")) + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtGui.QPushButton(Form) + self.exportBtn.setObjectName(_fromUtf8("exportBtn")) + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtGui.QPushButton(Form) + self.closeBtn.setObjectName(_fromUtf8("closeBtn")) + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName(_fromUtf8("paramTree")) + self.paramTree.headerItem().setText(0, _fromUtf8("1")) + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtGui.QLabel(Form) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8)) + self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) + self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py new file mode 100644 index 00000000..4ffc0b9a --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui' +# +# Created: Sun Sep 9 14:41:31 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(241, 367) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtGui.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtGui.QTreeWidget(Form) + self.itemTree.setObjectName("itemTree") + self.itemTree.headerItem().setText(0, "1") + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtGui.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtGui.QListWidget(Form) + self.formatList.setObjectName("formatList") + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtGui.QPushButton(Form) + self.exportBtn.setObjectName("exportBtn") + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtGui.QPushButton(Form) + self.closeBtn.setObjectName("closeBtn") + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName("paramTree") + self.paramTree.headerItem().setText(0, "1") + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtGui.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8)) + self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) + self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py new file mode 100644 index 00000000..0b71ac6f --- /dev/null +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -0,0 +1,365 @@ +from pyqtgraph.Point import Point +from pyqtgraph.Qt import QtCore, QtGui +import weakref +import pyqtgraph.ptime as ptime + +class MouseDragEvent(object): + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseDragEvent() method when the item is being mouse-dragged. + + """ + + + + def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): + self.start = start + self.finish = finish + self.accepted = False + self.currentItem = None + self._buttonDownScenePos = {} + self._buttonDownScreenPos = {} + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: + self._buttonDownScenePos[int(btn)] = moveEvent.buttonDownScenePos(btn) + self._buttonDownScreenPos[int(btn)] = moveEvent.buttonDownScreenPos(btn) + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + if lastEvent is None: + self._lastScenePos = pressEvent.scenePos() + self._lastScreenPos = pressEvent.screenPos() + else: + self._lastScenePos = lastEvent.scenePos() + self._lastScreenPos = lastEvent.screenPos() + self._buttons = moveEvent.buttons() + self._button = pressEvent.button() + self._modifiers = moveEvent.modifiers() + self.acceptedItem = None + + def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + """Return the current scene position of the mouse.""" + return Point(self._scenePos) + + def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" + return Point(self._screenPos) + + def buttonDownScenePos(self, btn=None): + """ + Return the scene position of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ + if btn is None: + btn = self.button() + return Point(self._buttonDownScenePos[int(btn)]) + + def buttonDownScreenPos(self, btn=None): + """ + Return the screen position (pixels relative to widget) of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ + if btn is None: + btn = self.button() + return Point(self._buttonDownScreenPos[int(btn)]) + + def lastScenePos(self): + """ + Return the scene position of the mouse immediately prior to this event. + """ + return Point(self._lastScenePos) + + def lastScreenPos(self): + """ + Return the screen position of the mouse immediately prior to this event. + """ + return Point(self._lastScreenPos) + + def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ + return self._buttons + + def button(self): + """Return the button that initiated the drag (may be different from the buttons currently pressed) + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + + """ + return self._button + + def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def buttonDownPos(self, btn=None): + """ + Return the position of the mouse at the time the drag was initiated + in the coordinate system of the item that the event was delivered to. + """ + if btn is None: + btn = self.button() + return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) + + def isStart(self): + """Returns True if this event is the first since a drag was initiated.""" + return self.start + + def isFinish(self): + """Returns False if this is the last event in a drag. Note that this + event will have the same position as the previous one.""" + return self.finish + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) + + def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + + """ + return self._modifiers + + + +class MouseClickEvent(object): + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseClickEvent() method when the item is clicked. + + + """ + + def __init__(self, pressEvent, double=False): + self.accepted = False + self.currentItem = None + self._double = double + self._scenePos = pressEvent.scenePos() + self._screenPos = pressEvent.screenPos() + self._button = pressEvent.button() + self._buttons = pressEvent.buttons() + self._modifiers = pressEvent.modifiers() + self._time = ptime.time() + self.acceptedItem = None + + def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + """Return the current scene position of the mouse.""" + return Point(self._scenePos) + + def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" + return Point(self._screenPos) + + def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ + return self._buttons + + def button(self): + """Return the mouse button that generated the click event. + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + """ + return self._button + + def double(self): + """Return True if this is a double-click.""" + return self._double + + def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ + return self._modifiers + + def __repr__(self): + p = self.pos() + return "" % (p.x(), p.y(), int(self.button())) + + def time(self): + return self._time + + + +class HoverEvent(object): + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their hoverEvent() method when the mouse is hovering over the item. + This event class both informs items that the mouse cursor is nearby and allows items to + communicate with one another about whether each item will accept *potential* mouse events. + + It is common for multiple overlapping items to receive hover events and respond by changing + their appearance. This can be misleading to the user since, in general, only one item will + respond to mouse events. To avoid this, items make calls to event.acceptClicks(button) + and/or acceptDrags(button). + + Each item may make multiple calls to acceptClicks/Drags, each time for a different button. + If the method returns True, then the item is guaranteed to be + the recipient of the claimed event IF the user presses the specified mouse button before + moving. If claimEvent returns False, then this item is guaranteed NOT to get the specified + event (because another has already claimed it) and the item should change its appearance + accordingly. + + event.isEnter() returns True if the mouse has just entered the item's shape; + event.isExit() returns True if the mouse has just left. + """ + def __init__(self, moveEvent, acceptable): + self.enter = False + self.acceptable = acceptable + self.exit = False + self.__clickItems = weakref.WeakValueDictionary() + self.__dragItems = weakref.WeakValueDictionary() + self.currentItem = None + if moveEvent is not None: + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + self._lastScenePos = moveEvent.lastScenePos() + self._lastScreenPos = moveEvent.lastScreenPos() + self._buttons = moveEvent.buttons() + self._modifiers = moveEvent.modifiers() + else: + self.exit = True + + + + def isEnter(self): + """Returns True if the mouse has just entered the item's shape""" + return self.enter + + def isExit(self): + """Returns True if the mouse has just exited the item's shape""" + return self.exit + + def acceptClicks(self, button): + """Inform the scene that the item (that the event was delivered to) + would accept a mouse click event if the user were to click before + moving the mouse again. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming click). + """ + if not self.acceptable: + return False + if button not in self.__clickItems: + self.__clickItems[button] = self.currentItem + return True + return False + + def acceptDrags(self, button): + """Inform the scene that the item (that the event was delivered to) + would accept a mouse drag event if the user were to drag before + the next hover event. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming drag event). + """ + if not self.acceptable: + return False + if button not in self.__dragItems: + self.__dragItems[button] = self.currentItem + return True + return False + + def scenePos(self): + """Return the current scene position of the mouse.""" + return Point(self._scenePos) + + def screenPos(self): + """Return the current screen position of the mouse.""" + return Point(self._screenPos) + + def lastScenePos(self): + """Return the previous scene position of the mouse.""" + return Point(self._lastScenePos) + + def lastScreenPos(self): + """Return the previous screen position of the mouse.""" + return Point(self._lastScreenPos) + + def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ + return self._buttons + + def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) + + def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ + return self._modifiers + + def clickItems(self): + return self.__clickItems + + def dragItems(self): + return self.__dragItems + + + \ No newline at end of file diff --git a/PIL_Fix/Image.py-1.6 b/pyqtgraph/PIL_Fix/Image.py-1.6 similarity index 100% rename from PIL_Fix/Image.py-1.6 rename to pyqtgraph/PIL_Fix/Image.py-1.6 diff --git a/PIL_Fix/Image.py-1.7 b/pyqtgraph/PIL_Fix/Image.py-1.7 similarity index 100% rename from PIL_Fix/Image.py-1.7 rename to pyqtgraph/PIL_Fix/Image.py-1.7 diff --git a/PIL_Fix/README b/pyqtgraph/PIL_Fix/README similarity index 100% rename from PIL_Fix/README rename to pyqtgraph/PIL_Fix/README diff --git a/Point.py b/pyqtgraph/Point.py similarity index 89% rename from Point.py rename to pyqtgraph/Point.py index b98dfad0..ea35d119 100644 --- a/Point.py +++ b/pyqtgraph/Point.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from PyQt4 import QtCore +from .Qt import QtCore import numpy as np def clip(x, mn, mx): @@ -23,12 +23,12 @@ class Point(QtCore.QPointF): if isinstance(args[0], QtCore.QSizeF): QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height())) return + elif isinstance(args[0], float) or isinstance(args[0], int): + QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) + return elif hasattr(args[0], '__getitem__'): QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1])) return - elif type(args[0]) in [float, int]: - QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) - return elif len(args) == 2: QtCore.QPointF.__init__(self, args[0], args[1]) return @@ -46,7 +46,7 @@ class Point(QtCore.QPointF): elif i == 1: return self.y() else: - raise IndexError("Point has no index %d" % i) + raise IndexError("Point has no index %s" % str(i)) def __setitem__(self, i, x): if i == 0: @@ -54,7 +54,7 @@ class Point(QtCore.QPointF): elif i == 1: return self.setY(x) else: - raise IndexError("Point has no index %d" % i) + raise IndexError("Point has no index %s" % str(i)) def __radd__(self, a): return self._math_('__radd__', a) @@ -101,6 +101,10 @@ class Point(QtCore.QPointF): """Returns the vector length of this Point.""" return (self[0]**2 + self[1]**2) ** 0.5 + def norm(self): + """Returns a vector in the same direction with unit length.""" + return self / self.length() + def angle(self, a): """Returns the angle in degrees between this vector and the vector a.""" n1 = self.length() @@ -139,4 +143,7 @@ class Point(QtCore.QPointF): return max(self[0], self[1]) def copy(self): - return Point(self) \ No newline at end of file + return Point(self) + + def toQPoint(self): + return QtCore.QPoint(*self) \ No newline at end of file diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py new file mode 100644 index 00000000..e584a381 --- /dev/null +++ b/pyqtgraph/Qt.py @@ -0,0 +1,48 @@ +## Do all Qt imports from here to allow easier PyQt / PySide compatibility +import sys, re + +## 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. +if 'PyQt4' in sys.modules: + USE_PYSIDE = False +elif 'PySide' in sys.modules: + USE_PYSIDE = True +else: + try: + import PyQt4 + USE_PYSIDE = False + except ImportError: + try: + import PySide + USE_PYSIDE = True + except ImportError: + raise Exception("PyQtGraph requires either PyQt4 or PySide; neither package could be imported.") + +if USE_PYSIDE: + from PySide import QtGui, QtCore, QtOpenGL, QtSvg + import PySide + VERSION_INFO = 'PySide ' + PySide.__version__ +else: + from PyQt4 import QtGui, QtCore + try: + from PyQt4 import QtSvg + except ImportError: + pass + try: + from PyQt4 import QtOpenGL + except ImportError: + pass + + QtCore.Signal = QtCore.pyqtSignal + VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR + + +## Make sure we have Qt >= 4.7 +versionReq = [4, 7] +QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR +m = re.match(r'(\d+)\.(\d+).*', QtVersion) +if m is not None and list(map(int, m.groups())) < versionReq: + print(map(int, m.groups())) + raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) + diff --git a/Transform.py b/pyqtgraph/SRTTransform.py similarity index 76% rename from Transform.py rename to pyqtgraph/SRTTransform.py index 9938a190..a861f940 100644 --- a/Transform.py +++ b/pyqtgraph/SRTTransform.py @@ -1,20 +1,22 @@ # -*- coding: utf-8 -*- -from PyQt4 import QtCore, QtGui -from Point import Point +from .Qt import QtCore, QtGui +from .Point import Point import numpy as np +import pyqtgraph as pg -class Transform(QtGui.QTransform): +class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate - - This transform always has 0 shear. + This transform has no shear; angles are always preserved. """ def __init__(self, init=None): QtGui.QTransform.__init__(self) self.reset() - if isinstance(init, dict): + if init is None: + return + elif isinstance(init, dict): self.restoreState(init) - elif isinstance(init, Transform): + elif isinstance(init, SRTTransform): self._state = { 'pos': Point(init._state['pos']), 'scale': Point(init._state['scale']), @@ -23,7 +25,25 @@ class Transform(QtGui.QTransform): self.update() elif isinstance(init, QtGui.QTransform): self.setFromQTransform(init) + elif isinstance(init, QtGui.QMatrix4x4): + self.setFromMatrix4x4(init) + else: + raise Exception("Cannot create SRTTransform from input type: %s" % str(type(init))) + + def getScale(self): + return self._state['scale'] + + def getAngle(self): + ## deprecated; for backward compatibility + return self.getRotation() + + def getRotation(self): + return self._state['angle'] + + def getTranslation(self): + return self._state['pos'] + def reset(self): self._state = { 'pos': Point(0,0), @@ -56,6 +76,19 @@ class Transform(QtGui.QTransform): } self.update() + def setFromMatrix4x4(self, m): + m = pg.SRTTransform3D(m) + angle, axis = m.getRotation() + if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): + print("angle: %s axis: %s" % (str(angle), str(axis))) + raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.") + self._state = { + 'pos': Point(m.getTranslation()), + 'scale': Point(m.getScale()), + 'angle': angle + } + self.update() + def translate(self, *args): """Acceptable arguments are: x, y @@ -100,16 +133,16 @@ class Transform(QtGui.QTransform): def __div__(self, t): """A / B == B^-1 * A""" dt = t.inverted()[0] * self - return Transform(dt) + return SRTTransform(dt) def __mul__(self, t): - return Transform(QtGui.QTransform.__mul__(self, t)) + return SRTTransform(QtGui.QTransform.__mul__(self, t)) def saveState(self): p = self._state['pos'] s = self._state['scale'] - if s[0] == 0: - raise Exception('Invalid scale') + #if s[0] == 0: + #raise Exception('Invalid scale: %s' % str(s)) return {'pos': (p[0], p[1]), 'scale': (s[0], s[1]), 'angle': self._state['angle']} def restoreState(self, state): @@ -132,9 +165,9 @@ class Transform(QtGui.QTransform): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) if __name__ == '__main__': - import widgets + from . import widgets import GraphicsView - from functions import * + from .functions import * app = QtGui.QApplication([]) win = QtGui.QMainWindow() win.show() @@ -175,28 +208,28 @@ if __name__ == '__main__': s.addItem(l1) s.addItem(l2) - tr1 = Transform() - tr2 = Transform() + tr1 = SRTTransform() + tr2 = SRTTransform() tr3 = QtGui.QTransform() tr3.translate(20, 0) tr3.rotate(45) - print "QTransform -> Transform:", Transform(tr3) + print("QTransform -> Transform:", SRTTransform(tr3)) - print "tr1:", tr1 + print("tr1:", tr1) tr2.translate(20, 0) tr2.rotate(45) - print "tr2:", tr2 + print("tr2:", tr2) dt = tr2/tr1 - print "tr2 / tr1 = ", dt + print("tr2 / tr1 = ", dt) - print "tr2 * tr1 = ", tr2*tr1 + print("tr2 * tr1 = ", tr2*tr1) - tr4 = Transform() + tr4 = SRTTransform() tr4.scale(-1, 1) tr4.rotate(30) - print "tr1 * tr4 = ", tr1*tr4 + print("tr1 * tr4 = ", tr1*tr4) w1 = widgets.TestROI((19,19), (22, 22), invertible=True) #w2 = widgets.TestROI((0,0), (150, 150)) diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py new file mode 100644 index 00000000..77583b5a --- /dev/null +++ b/pyqtgraph/SRTTransform3D.py @@ -0,0 +1,303 @@ +# -*- coding: utf-8 -*- +from .Qt import QtCore, QtGui +from .Vector import Vector +from .SRTTransform import SRTTransform +import pyqtgraph as pg +import numpy as np +import scipy.linalg + +class SRTTransform3D(pg.Transform3D): + """4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate + This transform has no shear; angles are always preserved. + """ + def __init__(self, init=None): + pg.Transform3D.__init__(self) + self.reset() + if init is None: + return + if init.__class__ is QtGui.QTransform: + init = SRTTransform(init) + + if isinstance(init, dict): + self.restoreState(init) + elif isinstance(init, SRTTransform3D): + self._state = { + 'pos': Vector(init._state['pos']), + 'scale': Vector(init._state['scale']), + 'angle': init._state['angle'], + 'axis': Vector(init._state['axis']), + } + self.update() + elif isinstance(init, SRTTransform): + self._state = { + 'pos': Vector(init._state['pos']), + 'scale': Vector(init._state['scale']), + 'angle': init._state['angle'], + 'axis': Vector(0, 0, 1), + } + self._state['scale'][2] = 1.0 + self.update() + elif isinstance(init, QtGui.QMatrix4x4): + self.setFromMatrix(init) + else: + raise Exception("Cannot build SRTTransform3D from argument type:", type(init)) + + + def getScale(self): + return pg.Vector(self._state['scale']) + + def getRotation(self): + """Return (angle, axis) of rotation""" + return self._state['angle'], pg.Vector(self._state['axis']) + + def getTranslation(self): + return pg.Vector(self._state['pos']) + + def reset(self): + self._state = { + 'pos': Vector(0,0,0), + 'scale': Vector(1,1,1), + 'angle': 0.0, ## in degrees + 'axis': (0, 0, 1) + } + self.update() + + def translate(self, *args): + """Adjust the translation of this transform""" + t = Vector(*args) + self.setTranslate(self._state['pos']+t) + + def setTranslate(self, *args): + """Set the translation of this transform""" + self._state['pos'] = Vector(*args) + self.update() + + def scale(self, *args): + """adjust the scale of this transform""" + ## try to prevent accidentally setting 0 scale on z axis + if len(args) == 1 and hasattr(args[0], '__len__'): + args = args[0] + if len(args) == 2: + args = args + (1,) + + s = Vector(*args) + self.setScale(self._state['scale'] * s) + + def setScale(self, *args): + """Set the scale of this transform""" + if len(args) == 1 and hasattr(args[0], '__len__'): + args = args[0] + if len(args) == 2: + args = args + (1,) + self._state['scale'] = Vector(*args) + self.update() + + def rotate(self, angle, axis=(0,0,1)): + """Adjust the rotation of this transform""" + origAxis = self._state['axis'] + if axis[0] == origAxis[0] and axis[1] == origAxis[1] and axis[2] == origAxis[2]: + self.setRotate(self._state['angle'] + angle) + else: + m = QtGui.QMatrix4x4() + m.translate(*self._state['pos']) + m.rotate(self._state['angle'], *self._state['axis']) + m.rotate(angle, *axis) + m.scale(*self._state['scale']) + self.setFromMatrix(m) + + def setRotate(self, angle, axis=(0,0,1)): + """Set the transformation rotation to angle (in degrees)""" + + self._state['angle'] = angle + self._state['axis'] = Vector(axis) + self.update() + + def setFromMatrix(self, m): + """ + Set this transform mased on the elements of *m* + The input matrix must be affine AND have no shear, + otherwise the conversion will most likely fail. + """ + for i in range(4): + self.setRow(i, m.row(i)) + m = self.matrix().reshape(4,4) + ## translation is 4th column + self._state['pos'] = m[:3,3] + + ## scale is vector-length of first three columns + scale = (m[:3,:3]**2).sum(axis=0)**0.5 + ## see whether there is an inversion + z = np.cross(m[0, :3], m[1, :3]) + if np.dot(z, m[2, :3]) < 0: + scale[1] *= -1 ## doesn't really matter which axis we invert + self._state['scale'] = scale + + ## rotation axis is the eigenvector with eigenvalue=1 + r = m[:3, :3] / scale[:, np.newaxis] + try: + evals, evecs = scipy.linalg.eig(r) + except: + print("Rotation matrix: %s" % str(r)) + print("Scale: %s" % str(scale)) + print("Original matrix: %s" % str(m)) + raise + eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) + if len(eigIndex) < 1: + print("eigenvalues: %s" % str(evals)) + print("eigenvectors: %s" % str(evecs)) + print("index: %s, %s" % (str(eigIndex), str(evals-1))) + raise Exception("Could not determine rotation axis.") + axis = evecs[eigIndex[0,0]].real + axis /= ((axis**2).sum())**0.5 + self._state['axis'] = axis + + ## trace(r) == 2 cos(angle) + 1, so: + self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi + if self._state['angle'] == 0: + self._state['axis'] = (0,0,1) + + def as2D(self): + """Return a QTransform representing the x,y portion of this transform (if possible)""" + return pg.SRTTransform(self) + + #def __div__(self, t): + #"""A / B == B^-1 * A""" + #dt = t.inverted()[0] * self + #return SRTTransform(dt) + + #def __mul__(self, t): + #return SRTTransform(QtGui.QTransform.__mul__(self, t)) + + def saveState(self): + p = self._state['pos'] + s = self._state['scale'] + ax = self._state['axis'] + #if s[0] == 0: + #raise Exception('Invalid scale: %s' % str(s)) + return { + 'pos': (p[0], p[1], p[2]), + 'scale': (s[0], s[1], s[2]), + 'angle': self._state['angle'], + 'axis': (ax[0], ax[1], ax[2]) + } + + def restoreState(self, state): + self._state['pos'] = Vector(state.get('pos', (0.,0.,0.))) + scale = state.get('scale', (1.,1.,1.)) + scale = tuple(scale) + (1.,) * (3-len(scale)) + self._state['scale'] = Vector(scale) + self._state['angle'] = state.get('angle', 0.) + self._state['axis'] = state.get('axis', (0, 0, 1)) + self.update() + + def update(self): + pg.Transform3D.setToIdentity(self) + ## modifications to the transform are multiplied on the right, so we need to reverse order here. + pg.Transform3D.translate(self, *self._state['pos']) + pg.Transform3D.rotate(self, self._state['angle'], *self._state['axis']) + pg.Transform3D.scale(self, *self._state['scale']) + + def __repr__(self): + return str(self.saveState()) + + def matrix(self, nd=3): + if nd == 3: + return np.array(self.copyDataTo()).reshape(4,4) + elif nd == 2: + m = np.array(self.copyDataTo()).reshape(4,4) + m[2] = m[3] + m[:,2] = m[:,3] + return m[:3,:3] + else: + raise Exception("Argument 'nd' must be 2 or 3") + +if __name__ == '__main__': + import widgets + import GraphicsView + from functions import * + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + win.show() + cw = GraphicsView.GraphicsView() + #cw.enableMouse() + win.setCentralWidget(cw) + s = QtGui.QGraphicsScene() + cw.setScene(s) + win.resize(600,600) + cw.enableMouse() + cw.setRange(QtCore.QRectF(-100., -100., 200., 200.)) + + class Item(QtGui.QGraphicsItem): + def __init__(self): + QtGui.QGraphicsItem.__init__(self) + self.b = QtGui.QGraphicsRectItem(20, 20, 20, 20, self) + self.b.setPen(QtGui.QPen(mkPen('y'))) + self.t1 = QtGui.QGraphicsTextItem(self) + self.t1.setHtml('R') + self.t1.translate(20, 20) + self.l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0, self) + self.l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10, self) + self.l1.setPen(QtGui.QPen(mkPen('y'))) + self.l2.setPen(QtGui.QPen(mkPen('y'))) + def boundingRect(self): + return QtCore.QRectF() + def paint(self, *args): + pass + + #s.addItem(b) + #s.addItem(t1) + item = Item() + s.addItem(item) + l1 = QtGui.QGraphicsLineItem(10, 0, -10, 0) + l2 = QtGui.QGraphicsLineItem(0, 10, 0, -10) + l1.setPen(QtGui.QPen(mkPen('r'))) + l2.setPen(QtGui.QPen(mkPen('r'))) + s.addItem(l1) + s.addItem(l2) + + tr1 = SRTTransform() + tr2 = SRTTransform() + tr3 = QtGui.QTransform() + tr3.translate(20, 0) + tr3.rotate(45) + print("QTransform -> Transform: %s" % str(SRTTransform(tr3))) + + print("tr1: %s" % str(tr1)) + + tr2.translate(20, 0) + tr2.rotate(45) + print("tr2: %s" % str(tr2)) + + dt = tr2/tr1 + print("tr2 / tr1 = %s" % str(dt)) + + print("tr2 * tr1 = %s" % str(tr2*tr1)) + + tr4 = SRTTransform() + tr4.scale(-1, 1) + tr4.rotate(30) + print("tr1 * tr4 = %s" % str(tr1*tr4)) + + w1 = widgets.TestROI((19,19), (22, 22), invertible=True) + #w2 = widgets.TestROI((0,0), (150, 150)) + w1.setZValue(10) + s.addItem(w1) + #s.addItem(w2) + w1Base = w1.getState() + #w2Base = w2.getState() + def update(): + tr1 = w1.getGlobalTransform(w1Base) + #tr2 = w2.getGlobalTransform(w2Base) + item.setTransform(tr1) + + #def update2(): + #tr1 = w1.getGlobalTransform(w1Base) + #tr2 = w2.getGlobalTransform(w2Base) + #t1.setTransform(tr1) + #w1.setState(w1Base) + #w1.applyGlobalTransform(tr2) + + w1.sigRegionChanged.connect(update) + #w2.sigRegionChanged.connect(update2) + + \ No newline at end of file diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py new file mode 100644 index 00000000..6f9b9112 --- /dev/null +++ b/pyqtgraph/SignalProxy.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +from .Qt import QtCore +from .ptime import time +from . import ThreadsafeTimer + +__all__ = ['SignalProxy'] + +class SignalProxy(QtCore.QObject): + """Object which collects rapid-fire signals and condenses them + into a single signal or a rate-limited stream of signals. + Used, for example, to prevent a SpinBox from generating multiple + signals when the mouse wheel is rolled over it. + + Emits sigDelayed after input signals have stopped for a certain period of time. + """ + + sigDelayed = QtCore.Signal(object) + + def __init__(self, signal, delay=0.3, rateLimit=0, slot=None): + """Initialization arguments: + signal - a bound Signal or pyqtSignal instance + delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s) + slot - Optional function to connect sigDelayed to. + rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a + steady rate while they are being received. + """ + + QtCore.QObject.__init__(self) + signal.connect(self.signalReceived) + self.signal = signal + self.delay = delay + self.rateLimit = rateLimit + self.args = None + self.timer = ThreadsafeTimer.ThreadsafeTimer() + self.timer.timeout.connect(self.flush) + self.block = False + self.slot = slot + self.lastFlushTime = None + if slot is not None: + self.sigDelayed.connect(slot) + + def setDelay(self, delay): + self.delay = delay + + def signalReceived(self, *args): + """Received signal. Cancel previous timer and store args to be forwarded later.""" + if self.block: + return + self.args = args + if self.rateLimit == 0: + self.timer.stop() + self.timer.start((self.delay*1000)+1) + else: + now = time() + if self.lastFlushTime is None: + leakTime = 0 + else: + lastFlush = self.lastFlushTime + leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now) + + self.timer.stop() + self.timer.start((min(leakTime, self.delay)*1000)+1) + + + def flush(self): + """If there is a signal queued up, send it now.""" + if self.args is None or self.block: + return False + #self.emit(self.signal, *self.args) + self.sigDelayed.emit(self.args) + self.args = None + self.timer.stop() + self.lastFlushTime = time() + return True + + def disconnect(self): + self.block = True + try: + self.signal.disconnect(self.signalReceived) + except: + pass + try: + self.sigDelayed.disconnect(self.slot) + except: + pass + + + +#def proxyConnect(source, signal, slot, delay=0.3): + #"""Connect a signal to a slot with delay. Returns the SignalProxy + #object that was created. Be sure to store this object so it is not + #garbage-collected immediately.""" + #sp = SignalProxy(source, signal, delay) + #if source is None: + #sp.connect(sp, QtCore.SIGNAL('signal'), slot) + #else: + #sp.connect(sp, signal, slot) + #return sp + + +if __name__ == '__main__': + from .Qt import QtGui + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + spin = QtGui.QSpinBox() + win.setCentralWidget(spin) + win.show() + + def fn(*args): + print("Raw signal:", args) + def fn2(*args): + print("Delayed signal:", args) + + + spin.valueChanged.connect(fn) + #proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn) + proxy = SignalProxy(spin.valueChanged, delay=0.5, slot=fn2) + \ No newline at end of file diff --git a/pyqtgraph/ThreadsafeTimer.py b/pyqtgraph/ThreadsafeTimer.py new file mode 100644 index 00000000..f2de9791 --- /dev/null +++ b/pyqtgraph/ThreadsafeTimer.py @@ -0,0 +1,41 @@ +from pyqtgraph.Qt import QtCore, QtGui + +class ThreadsafeTimer(QtCore.QObject): + """ + Thread-safe replacement for QTimer. + """ + + timeout = QtCore.Signal() + sigTimerStopRequested = QtCore.Signal() + sigTimerStartRequested = QtCore.Signal(object) + + def __init__(self): + QtCore.QObject.__init__(self) + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timerFinished) + self.timer.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.sigTimerStopRequested.connect(self.stop, QtCore.Qt.QueuedConnection) + self.sigTimerStartRequested.connect(self.start, QtCore.Qt.QueuedConnection) + + + def start(self, timeout): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + #print "start timer", self, "from gui thread" + self.timer.start(timeout) + else: + #print "start timer", self, "from remote thread" + self.sigTimerStartRequested.emit(timeout) + + def stop(self): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + #print "stop timer", self, "from gui thread" + self.timer.stop() + else: + #print "stop timer", self, "from remote thread" + self.sigTimerStopRequested.emit() + + def timerFinished(self): + self.timeout.emit() \ No newline at end of file diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py new file mode 100644 index 00000000..aa948e28 --- /dev/null +++ b/pyqtgraph/Transform3D.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from .Qt import QtCore, QtGui +import pyqtgraph as pg +import numpy as np + +class Transform3D(QtGui.QMatrix4x4): + """ + Extension of QMatrix4x4 with some helpful methods added. + """ + def __init__(self, *args): + QtGui.QMatrix4x4.__init__(self, *args) + + def matrix(self, nd=3): + if nd == 3: + return np.array(self.copyDataTo()).reshape(4,4) + elif nd == 2: + m = np.array(self.copyDataTo()).reshape(4,4) + m[2] = m[3] + m[:,2] = m[:,3] + return m[:3,:3] + else: + raise Exception("Argument 'nd' must be 2 or 3") + + def map(self, obj): + """ + Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates + """ + if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): + return pg.transformCoordinates(self, obj) + else: + return QtGui.QMatrix4x4.map(self, obj) + + def inverted(self): + inv, b = QtGui.QMatrix4x4.inverted(self) + return Transform3D(inv), b \ No newline at end of file diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py new file mode 100644 index 00000000..e9c109d8 --- /dev/null +++ b/pyqtgraph/Vector.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" +Vector.py - Extension of QVector3D which adds a few missing methods. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from .Qt import QtGui, QtCore +import numpy as np + +class Vector(QtGui.QVector3D): + """Extension of QVector3D which adds a few helpful methods.""" + + def __init__(self, *args): + if len(args) == 1: + if isinstance(args[0], QtCore.QSizeF): + QtGui.QVector3D.__init__(self, float(args[0].width()), float(args[0].height()), 0) + return + elif isinstance(args[0], QtCore.QPoint) or isinstance(args[0], QtCore.QPointF): + QtGui.QVector3D.__init__(self, float(args[0].x()), float(args[0].y()), 0) + elif hasattr(args[0], '__getitem__'): + vals = list(args[0]) + if len(vals) == 2: + vals.append(0) + if len(vals) != 3: + raise Exception('Cannot init Vector with sequence of length %d' % len(args[0])) + QtGui.QVector3D.__init__(self, *vals) + return + elif len(args) == 2: + QtGui.QVector3D.__init__(self, args[0], args[1], 0) + return + QtGui.QVector3D.__init__(self, *args) + + def __len__(self): + return 3 + + #def __reduce__(self): + #return (Point, (self.x(), self.y())) + + def __getitem__(self, i): + if i == 0: + return self.x() + elif i == 1: + return self.y() + elif i == 2: + return self.z() + else: + raise IndexError("Point has no index %s" % str(i)) + + def __setitem__(self, i, x): + if i == 0: + return self.setX(x) + elif i == 1: + return self.setY(x) + elif i == 2: + return self.setZ(x) + else: + raise IndexError("Point has no index %s" % str(i)) + + def __iter__(self): + yield(self.x()) + yield(self.y()) + yield(self.z()) + \ No newline at end of file diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py new file mode 100644 index 00000000..29541454 --- /dev/null +++ b/pyqtgraph/WidgetGroup.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +""" +WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +This class addresses the problem of having to save and restore the state +of a large group of widgets. +""" + +from .Qt import QtCore, QtGui +import weakref, inspect +from .python2_3 import asUnicode + + +__all__ = ['WidgetGroup'] + +def splitterState(w): + s = str(w.saveState().toPercentEncoding()) + return s + +def restoreSplitter(w, s): + if type(s) is list: + w.setSizes(s) + elif type(s) is str: + w.restoreState(QtCore.QByteArray.fromPercentEncoding(s)) + else: + print("Can't configure QSplitter using object of type", type(s)) + if w.count() > 0: ## make sure at least one item is not collapsed + for i in w.sizes(): + if i > 0: + return + w.setSizes([50] * w.count()) + +def comboState(w): + ind = w.currentIndex() + data = w.itemData(ind) + #if not data.isValid(): + if data is not None: + try: + if not data.isValid(): + data = None + else: + data = data.toInt()[0] + except AttributeError: + pass + if data is None: + return asUnicode(w.itemText(ind)) + else: + return data + +def setComboState(w, v): + if type(v) is int: + #ind = w.findData(QtCore.QVariant(v)) + ind = w.findData(v) + if ind > -1: + w.setCurrentIndex(ind) + return + w.setCurrentIndex(w.findText(str(v))) + + +class WidgetGroup(QtCore.QObject): + """This class takes a list of widgets and keeps an internal record of their state which is always up to date. Allows reading and writing from groups of widgets simultaneously.""" + + ## List of widget types which can be handled by WidgetGroup. + ## The value for each type is a tuple (change signal function, get function, set function, [auto-add children]) + ## The change signal function that takes an object and returns a signal that is emitted any time the state of the widget changes, not just + ## when it is changed by user interaction. (for example, 'clicked' is not a valid signal here) + ## If the change signal is None, the value of the widget is not cached. + ## Custom widgets not in this list can be made to work with WidgetGroup by giving them a 'widgetGroupInterface' method + ## which returns the tuple. + classes = { + QtGui.QSpinBox: + (lambda w: w.valueChanged, + QtGui.QSpinBox.value, + QtGui.QSpinBox.setValue), + QtGui.QDoubleSpinBox: + (lambda w: w.valueChanged, + QtGui.QDoubleSpinBox.value, + QtGui.QDoubleSpinBox.setValue), + QtGui.QSplitter: + (None, + splitterState, + restoreSplitter, + True), + QtGui.QCheckBox: + (lambda w: w.stateChanged, + QtGui.QCheckBox.isChecked, + QtGui.QCheckBox.setChecked), + QtGui.QComboBox: + (lambda w: w.currentIndexChanged, + comboState, + setComboState), + QtGui.QGroupBox: + (lambda w: w.toggled, + QtGui.QGroupBox.isChecked, + QtGui.QGroupBox.setChecked, + True), + QtGui.QLineEdit: + (lambda w: w.editingFinished, + lambda w: str(w.text()), + QtGui.QLineEdit.setText), + QtGui.QRadioButton: + (lambda w: w.toggled, + QtGui.QRadioButton.isChecked, + QtGui.QRadioButton.setChecked), + QtGui.QSlider: + (lambda w: w.valueChanged, + QtGui.QSlider.value, + QtGui.QSlider.setValue), + } + + sigChanged = QtCore.Signal(str, object) + + + def __init__(self, widgetList=None): + """Initialize WidgetGroup, adding specified widgets into this group. + widgetList can be: + - a list of widget specifications (widget, [name], [scale]) + - a dict of name: widget pairs + - any QObject, and all compatible child widgets will be added recursively. + + The 'scale' parameter for each widget allows QSpinBox to display a different value than the value recorded + in the group state (for example, the program may set a spin box value to 100e-6 and have it displayed as 100 to the user) + """ + QtCore.QObject.__init__(self) + self.widgetList = weakref.WeakKeyDictionary() # Make sure widgets don't stick around just because they are listed here + self.scales = weakref.WeakKeyDictionary() + self.cache = {} ## name:value pairs + self.uncachedWidgets = weakref.WeakKeyDictionary() + if isinstance(widgetList, QtCore.QObject): + self.autoAdd(widgetList) + elif isinstance(widgetList, list): + for w in widgetList: + self.addWidget(*w) + elif isinstance(widgetList, dict): + for name, w in widgetList.items(): + self.addWidget(w, name) + elif widgetList is None: + return + else: + raise Exception("Wrong argument type %s" % type(widgetList)) + + def addWidget(self, w, name=None, scale=None): + if not self.acceptsType(w): + raise Exception("Widget type %s not supported by WidgetGroup" % type(w)) + if name is None: + name = str(w.objectName()) + if name == '': + raise Exception("Cannot add widget '%s' without a name." % str(w)) + self.widgetList[w] = name + self.scales[w] = scale + self.readWidget(w) + + if type(w) in WidgetGroup.classes: + signal = WidgetGroup.classes[type(w)][0] + else: + signal = w.widgetGroupInterface()[0] + + if signal is not None: + if inspect.isfunction(signal) or inspect.ismethod(signal): + signal = signal(w) + signal.connect(self.mkChangeCallback(w)) + else: + self.uncachedWidgets[w] = None + + def findWidget(self, name): + for w in self.widgetList: + if self.widgetList[w] == name: + return w + return None + + def interface(self, obj): + t = type(obj) + if t in WidgetGroup.classes: + return WidgetGroup.classes[t] + else: + return obj.widgetGroupInterface() + + def checkForChildren(self, obj): + """Return true if we should automatically search the children of this object for more.""" + iface = self.interface(obj) + return (len(iface) > 3 and iface[3]) + + def autoAdd(self, obj): + ## Find all children of this object and add them if possible. + accepted = self.acceptsType(obj) + if accepted: + #print "%s auto add %s" % (self.objectName(), obj.objectName()) + self.addWidget(obj) + + if not accepted or self.checkForChildren(obj): + for c in obj.children(): + self.autoAdd(c) + + def acceptsType(self, obj): + for c in WidgetGroup.classes: + if isinstance(obj, c): + return True + if hasattr(obj, 'widgetGroupInterface'): + return True + return False + #return (type(obj) in WidgetGroup.classes) + + def setScale(self, widget, scale): + val = self.readWidget(widget) + self.scales[widget] = scale + self.setWidget(widget, val) + #print "scaling %f to %f" % (val, self.readWidget(widget)) + + + def mkChangeCallback(self, w): + return lambda *args: self.widgetChanged(w, *args) + + def widgetChanged(self, w, *args): + #print "widget changed" + n = self.widgetList[w] + v1 = self.cache[n] + v2 = self.readWidget(w) + if v1 != v2: + #print "widget", n, " = ", v2 + self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) + self.sigChanged.emit(self.widgetList[w], v2) + + def state(self): + for w in self.uncachedWidgets: + self.readWidget(w) + + #cc = self.cache.copy() + #if 'averageGroup' in cc: + #val = cc['averageGroup'] + #w = self.findWidget('averageGroup') + #self.readWidget(w) + #if val != self.cache['averageGroup']: + #print " AverageGroup did not match cached value!" + #else: + #print " AverageGroup OK" + return self.cache.copy() + + def setState(self, s): + #print "SET STATE", self, s + for w in self.widgetList: + n = self.widgetList[w] + #print " restore %s?" % n + if n not in s: + continue + #print " restore state", w, n, s[n] + self.setWidget(w, s[n]) + + def readWidget(self, w): + if type(w) in WidgetGroup.classes: + getFunc = WidgetGroup.classes[type(w)][1] + else: + getFunc = w.widgetGroupInterface()[1] + + if getFunc is None: + return None + + ## if the getter function provided in the interface is a bound method, + ## then just call the method directly. Otherwise, pass in the widget as the first arg + ## to the function. + if inspect.ismethod(getFunc) and getFunc.__self__ is not None: + val = getFunc() + else: + val = getFunc(w) + + if self.scales[w] is not None: + val /= self.scales[w] + #if isinstance(val, QtCore.QString): + #val = str(val) + n = self.widgetList[w] + self.cache[n] = val + return val + + def setWidget(self, w, v): + v1 = v + if self.scales[w] is not None: + v *= self.scales[w] + + if type(w) in WidgetGroup.classes: + setFunc = WidgetGroup.classes[type(w)][2] + else: + setFunc = w.widgetGroupInterface()[2] + + ## if the setter function provided in the interface is a bound method, + ## then just call the method directly. Otherwise, pass in the widget as the first arg + ## to the function. + if inspect.ismethod(setFunc) and setFunc.__self__ is not None: + setFunc(v) + else: + setFunc(w, v) + + #name = self.widgetList[w] + #if name in self.cache and (self.cache[name] != v1): + #print "%s: Cached value %s != set value %s" % (name, str(self.cache[name]), str(v1)) + + + \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py new file mode 100644 index 00000000..6e950770 --- /dev/null +++ b/pyqtgraph/__init__.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +REVISION = None + +### import all the goodies and add some helper functions for easy CLI use + +## 'Qt' is a local module; it is intended mainly to cover up the differences +## between PyQt4 and PySide. +from .Qt import QtGui + +## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) +#if QtGui.QApplication.instance() is None: + #app = QtGui.QApplication([]) + +import os, sys + +## check python version +## Allow anything >= 2.7 +if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] < 6): + raise Exception("Pyqtgraph requires Python version 2.6 or greater (this is %d.%d)" % (sys.version_info[0], sys.version_info[1])) + +## helpers for 2/3 compatibility +from . import python2_3 + +## install workarounds for numpy bugs +from . import numpy_fix + +## in general openGL is poorly supported with Qt+GraphicsView. +## we only enable it where the performance benefit is critical. +## Note this only applies to 2D graphics; 3D graphics always use OpenGL. +if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation + useOpenGL = False +elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but also has serious bugs + useOpenGL = False + if QtGui.QApplication.instance() is not None: + print('Warning: QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).') + QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system +else: + useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. + +CONFIG_OPTIONS = { + 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. + 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox + 'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc. + 'background': (0, 0, 0), ## default background for GraphicsWidget + 'antialias': False, + 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets +} + + +def setConfigOption(opt, value): + CONFIG_OPTIONS[opt] = value + +def setConfigOptions(**opts): + CONFIG_OPTIONS.update(opts) + +def getConfigOption(opt): + return CONFIG_OPTIONS[opt] + + +def systemInfo(): + print("sys.platform: %s" % sys.platform) + print("sys.version: %s" % sys.version) + from .Qt import VERSION_INFO + print("qt bindings: %s" % VERSION_INFO) + + global REVISION + if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file + lastRevFile = os.path.join(os.path.dirname(__file__), '.bzr', 'branch', 'last-revision') + if os.path.exists(lastRevFile): + REVISION = open(lastRevFile, 'r').read().strip() + + print("pyqtgraph: %s" % REVISION) + print("config:") + import pprint + pprint.pprint(CONFIG_OPTIONS) + +## Rename orphaned .pyc files. This is *probably* safe :) + +def renamePyc(startDir): + ### Used to rename orphaned .pyc files + ### When a python file changes its location in the repository, usually the .pyc file + ### is left behind, possibly causing mysterious and difficult to track bugs. + + ### Note that this is no longer necessary for python 3.2; from PEP 3147: + ### "If the py source file is missing, the pyc file inside __pycache__ will be ignored. + ### This eliminates the problem of accidental stale pyc file imports." + + printed = False + startDir = os.path.abspath(startDir) + for path, dirs, files in os.walk(startDir): + if '__pycache__' in path: + continue + for f in files: + fileName = os.path.join(path, f) + base, ext = os.path.splitext(fileName) + py = base + ".py" + if ext == '.pyc' and not os.path.isfile(py): + if not printed: + print("NOTE: Renaming orphaned .pyc files:") + printed = True + n = 1 + while True: + name2 = fileName + ".renamed%d" % n + if not os.path.exists(name2): + break + n += 1 + print(" " + fileName + " ==>") + print(" " + name2) + os.rename(fileName, name2) + +import os +path = os.path.split(__file__)[0] +if not hasattr(sys, 'frozen'): ## If we are frozen, there's a good chance we don't have the original .py files anymore. + renamePyc(path) + + +## Import almost everything to make it available from a single namespace +## don't import the more complex systems--canvas, parametertree, flowchart, dockarea +## these must be imported separately. +from . import frozenSupport +def importModules(path, globals, locals, excludes=()): + """Import all modules residing within *path*, return a dict of name: module pairs. + + Note that *path* MUST be relative to the module doing the import. + """ + d = os.path.join(os.path.split(globals['__file__'])[0], path) + files = set() + for f in frozenSupport.listdir(d): + if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__': + files.add(f) + elif f[-3:] == '.py' and f != '__init__.py': + files.add(f[:-3]) + elif f[-4:] == '.pyc' and f != '__init__.pyc': + files.add(f[:-4]) + + mods = {} + path = path.replace(os.sep, '.') + for modName in files: + if modName in excludes: + continue + try: + if len(path) > 0: + modName = path + '.' + modName + mod = __import__(modName, globals, locals, fromlist=['*']) + mods[modName] = mod + except: + import traceback + traceback.print_stack() + sys.excepthook(*sys.exc_info()) + print("[Error importing module: %s]" % modName) + + return mods + +def importAll(path, globals, locals, excludes=()): + """Given a list of modules, import all names from each module into the global namespace.""" + mods = importModules(path, globals, locals, excludes) + for mod in mods.values(): + if hasattr(mod, '__all__'): + names = mod.__all__ + else: + names = [n for n in dir(mod) if n[0] != '_'] + for k in names: + if hasattr(mod, k): + globals[k] = getattr(mod, k) + +importAll('graphicsItems', globals(), locals()) +importAll('widgets', globals(), locals(), excludes=['MatplotlibWidget', 'RemoteGraphicsView']) + +from .imageview import * +from .WidgetGroup import * +from .Point import Point +from .Vector import Vector +from .SRTTransform import SRTTransform +from .Transform3D import Transform3D +from .SRTTransform3D import SRTTransform3D +from .functions import * +from .graphicsWindows import * +from .SignalProxy import * +from .ptime import time + + +import atexit +def cleanup(): + ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore. + + ## Workaround for Qt exit crash: + ## ALL QGraphicsItems must have a scene before they are deleted. + ## This is potentially very expensive, but preferred over crashing. + ## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer.. + if QtGui.QApplication.instance() is None: + return + import gc + s = QtGui.QGraphicsScene() + for o in gc.get_objects(): + try: + if isinstance(o, QtGui.QGraphicsItem) and o.scene() is None: + s.addItem(o) + except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object + continue +atexit.register(cleanup) + + + +## Convenience functions for command-line use + +plots = [] +images = [] +QAPP = None + +def plot(*args, **kargs): + """ + Create and return a :class:`PlotWindow ` + (this is just a window with :class:`PlotWidget ` inside), plot data in it. + Accepts a *title* argument to set the title of the window. + All other arguments are used to plot data. (see :func:`PlotItem.plot() `) + """ + mkQApp() + #if 'title' in kargs: + #w = PlotWindow(title=kargs['title']) + #del kargs['title'] + #else: + #w = PlotWindow() + #if len(args)+len(kargs) > 0: + #w.plot(*args, **kargs) + + pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom'] + pwArgs = {} + dataArgs = {} + for k in kargs: + if k in pwArgList: + pwArgs[k] = kargs[k] + else: + dataArgs[k] = kargs[k] + + w = PlotWindow(**pwArgs) + w.plot(*args, **dataArgs) + plots.append(w) + w.show() + return w + +def image(*args, **kargs): + """ + Create and return an :class:`ImageWindow ` + (this is just a window with :class:`ImageView ` widget inside), show image data inside. + Will show 2D or 3D image data. + Accepts a *title* argument to set the title of the window. + All other arguments are used to show data. (see :func:`ImageView.setImage() `) + """ + mkQApp() + w = ImageWindow(*args, **kargs) + images.append(w) + w.show() + return w +show = image ## for backward compatibility + +def dbg(): + """ + Create a console window and begin watching for exceptions. + """ + mkQApp() + import console + c = console.ConsoleWidget() + c.catchAllExceptions() + c.show() + global consoles + try: + consoles.append(c) + except NameError: + consoles = [c] + + +def mkQApp(): + global QAPP + inst = QtGui.QApplication.instance() + if inst is None: + QAPP = QtGui.QApplication([]) + else: + QAPP = inst + return QAPP + diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py new file mode 100644 index 00000000..17a39c2b --- /dev/null +++ b/pyqtgraph/canvas/Canvas.py @@ -0,0 +1,608 @@ +# -*- coding: utf-8 -*- +if __name__ == '__main__': + import sys, os + md = os.path.dirname(os.path.abspath(__file__)) + sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path + #print md + + +#from pyqtgraph.GraphicsView import GraphicsView +#import pyqtgraph.graphicsItems as graphicsItems +#from pyqtgraph.PlotWidget import PlotWidget +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from pyqtgraph.graphicsItems.ROI import ROI +from pyqtgraph.graphicsItems.ViewBox import ViewBox +from pyqtgraph.graphicsItems.GridItem import GridItem + +if USE_PYSIDE: + from .CanvasTemplate_pyside import * +else: + from .CanvasTemplate_pyqt import * + +#import DataManager +import numpy as np +from pyqtgraph import debug +#import pyqtgraph as pg +import weakref +from .CanvasManager import CanvasManager +#import items +from .CanvasItem import CanvasItem, GroupCanvasItem + +class Canvas(QtGui.QWidget): + + sigSelectionChanged = QtCore.Signal(object, object) + sigItemTransformChanged = QtCore.Signal(object, object) + sigItemTransformChangeFinished = QtCore.Signal(object, object) + + def __init__(self, parent=None, allowTransforms=True, hideCtrl=False, name=None): + QtGui.QWidget.__init__(self, parent) + self.ui = Ui_Form() + self.ui.setupUi(self) + #self.view = self.ui.view + self.view = ViewBox() + self.ui.view.setCentralItem(self.view) + self.itemList = self.ui.itemList + self.itemList.setSelectionMode(self.itemList.ExtendedSelection) + self.allowTransforms = allowTransforms + self.multiSelectBox = SelectBox() + self.view.addItem(self.multiSelectBox) + self.multiSelectBox.hide() + self.multiSelectBox.setZValue(1e6) + self.ui.mirrorSelectionBtn.hide() + self.ui.reflectSelectionBtn.hide() + self.ui.resetTransformsBtn.hide() + + self.redirect = None ## which canvas to redirect items to + self.items = [] + + #self.view.enableMouse() + self.view.setAspectLocked(True) + #self.view.invertY() + + grid = GridItem() + self.grid = CanvasItem(grid, name='Grid', movable=False) + self.addItem(self.grid) + + self.hideBtn = QtGui.QPushButton('>', self) + self.hideBtn.setFixedWidth(20) + self.hideBtn.setFixedHeight(20) + self.ctrlSize = 200 + self.sizeApplied = False + self.hideBtn.clicked.connect(self.hideBtnClicked) + self.ui.splitter.splitterMoved.connect(self.splitterMoved) + + self.ui.itemList.itemChanged.connect(self.treeItemChanged) + self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) + self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) + self.ui.autoRangeBtn.clicked.connect(self.autoRange) + self.ui.storeSvgBtn.clicked.connect(self.storeSvg) + self.ui.storePngBtn.clicked.connect(self.storePng) + self.ui.redirectCheck.toggled.connect(self.updateRedirect) + self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) + self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) + self.multiSelectBox.sigRegionChangeFinished.connect(self.multiSelectBoxChangeFinished) + self.ui.mirrorSelectionBtn.clicked.connect(self.mirrorSelectionClicked) + self.ui.reflectSelectionBtn.clicked.connect(self.reflectSelectionClicked) + self.ui.resetTransformsBtn.clicked.connect(self.resetTransformsClicked) + + self.resizeEvent() + if hideCtrl: + self.hideBtnClicked() + + if name is not None: + self.registeredName = CanvasManager.instance().registerCanvas(self, name) + self.ui.redirectCombo.setHostName(self.registeredName) + + self.menu = QtGui.QMenu() + #self.menu.setTitle("Image") + remAct = QtGui.QAction("Remove item", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent + + + def storeSvg(self): + self.ui.view.writeSvg() + + def storePng(self): + self.ui.view.writeImage() + + def splitterMoved(self): + self.resizeEvent() + + def hideBtnClicked(self): + ctrlSize = self.ui.splitter.sizes()[1] + if ctrlSize == 0: + cs = self.ctrlSize + w = self.ui.splitter.size().width() + if cs > w: + cs = w - 20 + self.ui.splitter.setSizes([w-cs, cs]) + self.hideBtn.setText('>') + else: + self.ctrlSize = ctrlSize + self.ui.splitter.setSizes([100, 0]) + self.hideBtn.setText('<') + self.resizeEvent() + + def autoRange(self): + self.view.autoRange() + + def resizeEvent(self, ev=None): + if ev is not None: + QtGui.QWidget.resizeEvent(self, ev) + self.hideBtn.move(self.ui.view.size().width() - self.hideBtn.width(), 0) + + if not self.sizeApplied: + self.sizeApplied = True + s = min(self.width(), max(100, min(200, self.width()*0.25))) + s2 = self.width()-s + self.ui.splitter.setSizes([s2, s]) + + + def updateRedirect(self, *args): + ### Decide whether/where to redirect items and make it so + cname = str(self.ui.redirectCombo.currentText()) + man = CanvasManager.instance() + if self.ui.redirectCheck.isChecked() and cname != '': + redirect = man.getCanvas(cname) + else: + redirect = None + + if self.redirect is redirect: + return + + self.redirect = redirect + if redirect is None: + self.reclaimItems() + else: + self.redirectItems(redirect) + + + def redirectItems(self, canvas): + for i in self.items: + if i is self.grid: + continue + li = i.listItem + parent = li.parent() + if parent is None: + tree = li.treeWidget() + if tree is None: + print("Skipping item", i, i.name) + continue + tree.removeTopLevelItem(li) + else: + parent.removeChild(li) + canvas.addItem(i) + + + def reclaimItems(self): + items = self.items + #self.items = {'Grid': items['Grid']} + #del items['Grid'] + self.items = [self.grid] + items.remove(self.grid) + + for i in items: + i.canvas.removeItem(i) + self.addItem(i) + + def treeItemChanged(self, item, col): + #gi = self.items.get(item.name, None) + #if gi is None: + #return + try: + citem = item.canvasItem() + except AttributeError: + return + if item.checkState(0) == QtCore.Qt.Checked: + for i in range(item.childCount()): + item.child(i).setCheckState(0, QtCore.Qt.Checked) + citem.show() + else: + for i in range(item.childCount()): + item.child(i).setCheckState(0, QtCore.Qt.Unchecked) + citem.hide() + + def treeItemSelected(self): + sel = self.selectedItems() + #sel = [] + #for listItem in self.itemList.selectedItems(): + #if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None: + #sel.append(listItem.canvasItem) + #sel = [self.items[item.name] for item in sel] + + if len(sel) == 0: + #self.selectWidget.hide() + return + + multi = len(sel) > 1 + for i in self.items: + #i.ctrlWidget().hide() + ## updated the selected state of every item + i.selectionChanged(i in sel, multi) + + if len(sel)==1: + #item = sel[0] + #item.ctrlWidget().show() + self.multiSelectBox.hide() + self.ui.mirrorSelectionBtn.hide() + self.ui.reflectSelectionBtn.hide() + self.ui.resetTransformsBtn.hide() + elif len(sel) > 1: + self.showMultiSelectBox() + + #if item.isMovable(): + #self.selectBox.setPos(item.item.pos()) + #self.selectBox.setSize(item.item.sceneBoundingRect().size()) + #self.selectBox.show() + #else: + #self.selectBox.hide() + + #self.emit(QtCore.SIGNAL('itemSelected'), self, item) + self.sigSelectionChanged.emit(self, sel) + + def selectedItems(self): + """ + Return list of all selected canvasItems + """ + return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None] + + #def selectedItem(self): + #sel = self.itemList.selectedItems() + #if sel is None or len(sel) < 1: + #return + #return self.items.get(sel[0].name, None) + + def selectItem(self, item): + li = item.listItem + #li = self.getListItem(item.name()) + #print "select", li + self.itemList.setCurrentItem(li) + + + + def showMultiSelectBox(self): + ## Get list of selected canvas items + items = self.selectedItems() + + rect = self.view.itemBoundingRect(items[0].graphicsItem()) + for i in items: + if not i.isMovable(): ## all items in selection must be movable + return + br = self.view.itemBoundingRect(i.graphicsItem()) + rect = rect|br + + self.multiSelectBox.blockSignals(True) + self.multiSelectBox.setPos([rect.x(), rect.y()]) + self.multiSelectBox.setSize(rect.size()) + self.multiSelectBox.setAngle(0) + self.multiSelectBox.blockSignals(False) + + self.multiSelectBox.show() + + self.ui.mirrorSelectionBtn.show() + self.ui.reflectSelectionBtn.show() + self.ui.resetTransformsBtn.show() + #self.multiSelectBoxBase = self.multiSelectBox.getState().copy() + + def mirrorSelectionClicked(self): + for ci in self.selectedItems(): + ci.mirrorY() + self.showMultiSelectBox() + + def reflectSelectionClicked(self): + for ci in self.selectedItems(): + ci.mirrorXY() + self.showMultiSelectBox() + + def resetTransformsClicked(self): + for i in self.selectedItems(): + i.resetTransformClicked() + self.showMultiSelectBox() + + def multiSelectBoxChanged(self): + self.multiSelectBoxMoved() + + def multiSelectBoxChangeFinished(self): + for ci in self.selectedItems(): + ci.applyTemporaryTransform() + ci.sigTransformChangeFinished.emit(ci) + + def multiSelectBoxMoved(self): + transform = self.multiSelectBox.getGlobalTransform() + for ci in self.selectedItems(): + ci.setTemporaryTransform(transform) + ci.sigTransformChanged.emit(ci) + + + def addGraphicsItem(self, item, **opts): + """Add a new GraphicsItem to the scene at pos. + Common options are name, pos, scale, and z + """ + citem = CanvasItem(item, **opts) + item._canvasItem = citem + self.addItem(citem) + return citem + + + def addGroup(self, name, **kargs): + group = GroupCanvasItem(name=name) + self.addItem(group, **kargs) + return group + + + def addItem(self, citem): + """ + Add an item to the canvas. + """ + + ## Check for redirections + if self.redirect is not None: + name = self.redirect.addItem(citem) + self.items.append(citem) + return name + + if not self.allowTransforms: + citem.setMovable(False) + + citem.sigTransformChanged.connect(self.itemTransformChanged) + citem.sigTransformChangeFinished.connect(self.itemTransformChangeFinished) + citem.sigVisibilityChanged.connect(self.itemVisibilityChanged) + + + ## Determine name to use in the item list + name = citem.opts['name'] + if name is None: + name = 'item' + newname = name + + ## If name already exists, append a number to the end + ## NAH. Let items have the same name if they really want. + #c=0 + #while newname in self.items: + #c += 1 + #newname = name + '_%03d' %c + #name = newname + + ## find parent and add item to tree + #currentNode = self.itemList.invisibleRootItem() + insertLocation = 0 + #print "Inserting node:", name + + + ## determine parent list item where this item should be inserted + parent = citem.parentItem() + if parent in (None, self.view.childGroup): + parent = self.itemList.invisibleRootItem() + else: + parent = parent.listItem + + ## set Z value above all other siblings if none was specified + siblings = [parent.child(i).canvasItem() for i in range(parent.childCount())] + z = citem.zValue() + if z is None: + zvals = [i.zValue() for i in siblings] + if parent == self.itemList.invisibleRootItem(): + if len(zvals) == 0: + z = 0 + else: + z = max(zvals)+10 + else: + if len(zvals) == 0: + z = parent.canvasItem().zValue() + else: + z = max(zvals)+1 + citem.setZValue(z) + + ## determine location to insert item relative to its siblings + for i in range(parent.childCount()): + ch = parent.child(i) + zval = ch.canvasItem().graphicsItem().zValue() ## should we use CanvasItem.zValue here? + if zval < z: + insertLocation = i + break + else: + insertLocation = i+1 + + node = QtGui.QTreeWidgetItem([name]) + flags = node.flags() | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsDragEnabled + if not isinstance(citem, GroupCanvasItem): + flags = flags & ~QtCore.Qt.ItemIsDropEnabled + node.setFlags(flags) + if citem.opts['visible']: + node.setCheckState(0, QtCore.Qt.Checked) + else: + node.setCheckState(0, QtCore.Qt.Unchecked) + + node.name = name + #if citem.opts['parent'] != None: + ## insertLocation is incorrect in this case + parent.insertChild(insertLocation, node) + #else: + #root.insertChild(insertLocation, node) + + citem.name = name + citem.listItem = node + node.canvasItem = weakref.ref(citem) + self.items.append(citem) + + ctrl = citem.ctrlWidget() + ctrl.hide() + self.ui.ctrlLayout.addWidget(ctrl) + + ## inform the canvasItem that its parent canvas has changed + citem.setCanvas(self) + + ## Autoscale to fit the first item added (not including the grid). + if len(self.items) == 2: + self.autoRange() + + + #for n in name: + #nextnode = None + #for x in range(currentNode.childCount()): + #ch = currentNode.child(x) + #if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location + #zval = ch.canvasItem.zValue() + #if zval > z: + ###print " ->", x + #insertLocation = x+1 + #if n == ch.text(0): + #nextnode = ch + #break + #if nextnode is None: ## If name doesn't exist, create it + #nextnode = QtGui.QTreeWidgetItem([n]) + #nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled) + #nextnode.setCheckState(0, QtCore.Qt.Checked) + ### Add node to correct position in list by Z-value + ###print " ==>", insertLocation + #currentNode.insertChild(insertLocation, nextnode) + + #if n == name[-1]: ## This is the leaf; add some extra properties. + #nextnode.name = name + + #if n == name[0]: ## This is the root; make the item movable + #nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled) + #else: + #nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled) + + #currentNode = nextnode + return citem + + def treeItemMoved(self, item, parent, index): + ##Item moved in tree; update Z values + if parent is self.itemList.invisibleRootItem(): + item.canvasItem().setParentItem(self.view.childGroup) + else: + item.canvasItem().setParentItem(parent.canvasItem()) + siblings = [parent.child(i).canvasItem() for i in range(parent.childCount())] + + zvals = [i.zValue() for i in siblings] + zvals.sort(reverse=True) + + for i in range(len(siblings)): + item = siblings[i] + item.setZValue(zvals[i]) + #item = self.itemList.topLevelItem(i) + + ##ci = self.items[item.name] + #ci = item.canvasItem + #if ci is None: + #continue + #if ci.zValue() != zvals[i]: + #ci.setZValue(zvals[i]) + + #if self.itemList.topLevelItemCount() < 2: + #return + #name = item.name + #gi = self.items[name] + #if index == 0: + #next = self.itemList.topLevelItem(1) + #z = self.items[next.name].zValue()+1 + #else: + #prev = self.itemList.topLevelItem(index-1) + #z = self.items[prev.name].zValue()-1 + #gi.setZValue(z) + + + + + + + def itemVisibilityChanged(self, item): + listItem = item.listItem + checked = listItem.checkState(0) == QtCore.Qt.Checked + vis = item.isVisible() + if vis != checked: + if vis: + listItem.setCheckState(0, QtCore.Qt.Checked) + else: + listItem.setCheckState(0, QtCore.Qt.Unchecked) + + def removeItem(self, item): + if isinstance(item, QtGui.QTreeWidgetItem): + item = item.canvasItem() + + + if isinstance(item, CanvasItem): + item.setCanvas(None) + listItem = item.listItem + listItem.canvasItem = None + item.listItem = None + self.itemList.removeTopLevelItem(listItem) + self.items.remove(item) + ctrl = item.ctrlWidget() + ctrl.hide() + self.ui.ctrlLayout.removeWidget(ctrl) + else: + if hasattr(item, '_canvasItem'): + self.removeItem(item._canvasItem) + else: + self.view.removeItem(item) + + ## disconnect signals, remove from list, etc.. + + def clear(self): + while len(self.items) > 0: + self.removeItem(self.items[0]) + + + def addToScene(self, item): + self.view.addItem(item) + + def removeFromScene(self, item): + self.view.removeItem(item) + + + def listItems(self): + """Return a dictionary of name:item pairs""" + return self.items + + def getListItem(self, name): + return self.items[name] + + #def scene(self): + #return self.view.scene() + + def itemTransformChanged(self, item): + #self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item) + self.sigItemTransformChanged.emit(self, item) + + def itemTransformChangeFinished(self, item): + #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) + self.sigItemTransformChangeFinished.emit(self, item) + + def itemListContextMenuEvent(self, ev): + self.menuItem = self.itemList.itemAt(ev.pos()) + self.menu.popup(ev.globalPos()) + + def removeClicked(self): + self.removeItem(self.menuItem) + self.menuItem = None + import gc + gc.collect() + +class SelectBox(ROI): + def __init__(self, scalable=False): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, [0,0], [1,1]) + center = [0.5, 0.5] + + if scalable: + self.addScaleHandle([1, 1], center, lockAspect=True) + self.addScaleHandle([0, 0], center, lockAspect=True) + self.addRotateHandle([0, 1], center) + self.addRotateHandle([1, 0], center) + + + + + + + + + + + \ No newline at end of file diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py new file mode 100644 index 00000000..81388cb6 --- /dev/null +++ b/pyqtgraph/canvas/CanvasItem.py @@ -0,0 +1,509 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from pyqtgraph.graphicsItems.ROI import ROI +import pyqtgraph as pg +if USE_PYSIDE: + from . import TransformGuiTemplate_pyside as TransformGuiTemplate +else: + from . import TransformGuiTemplate_pyqt as TransformGuiTemplate + +from pyqtgraph import debug + +class SelectBox(ROI): + def __init__(self, scalable=False, rotatable=True): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, [0,0], [1,1], invertible=True) + center = [0.5, 0.5] + + if scalable: + self.addScaleHandle([1, 1], center, lockAspect=True) + self.addScaleHandle([0, 0], center, lockAspect=True) + if rotatable: + self.addRotateHandle([0, 1], center) + self.addRotateHandle([1, 0], center) + +class CanvasItem(QtCore.QObject): + + sigResetUserTransform = QtCore.Signal(object) + sigTransformChangeFinished = QtCore.Signal(object) + sigTransformChanged = QtCore.Signal(object) + + """CanvasItem takes care of managing an item's state--alpha, visibility, z-value, transformations, etc. and + provides a control widget""" + + sigVisibilityChanged = QtCore.Signal(object) + transformCopyBuffer = None + + def __init__(self, item, **opts): + defOpts = {'name': None, 'z': None, 'movable': True, 'scalable': False, 'rotatable': True, 'visible': True, 'parent':None} #'pos': [0,0], 'scale': [1,1], 'angle':0, + defOpts.update(opts) + self.opts = defOpts + self.selectedAlone = False ## whether this item is the only one selected + + QtCore.QObject.__init__(self) + self.canvas = None + self._graphicsItem = item + + parent = self.opts['parent'] + if parent is not None: + self._graphicsItem.setParentItem(parent.graphicsItem()) + self._parentItem = parent + else: + self._parentItem = None + + z = self.opts['z'] + if z is not None: + item.setZValue(z) + + self.ctrl = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + self.layout.setSpacing(0) + self.layout.setContentsMargins(0,0,0,0) + self.ctrl.setLayout(self.layout) + + self.alphaLabel = QtGui.QLabel("Alpha") + self.alphaSlider = QtGui.QSlider() + self.alphaSlider.setMaximum(1023) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setValue(1023) + self.layout.addWidget(self.alphaLabel, 0, 0) + self.layout.addWidget(self.alphaSlider, 0, 1) + self.resetTransformBtn = QtGui.QPushButton('Reset Transform') + self.copyBtn = QtGui.QPushButton('Copy') + self.pasteBtn = QtGui.QPushButton('Paste') + + self.transformWidget = QtGui.QWidget() + self.transformGui = TransformGuiTemplate.Ui_Form() + self.transformGui.setupUi(self.transformWidget) + self.layout.addWidget(self.transformWidget, 3, 0, 1, 2) + self.transformGui.mirrorImageBtn.clicked.connect(self.mirrorY) + self.transformGui.reflectImageBtn.clicked.connect(self.mirrorXY) + + self.layout.addWidget(self.resetTransformBtn, 1, 0, 1, 2) + self.layout.addWidget(self.copyBtn, 2, 0, 1, 1) + self.layout.addWidget(self.pasteBtn, 2, 1, 1, 1) + self.alphaSlider.valueChanged.connect(self.alphaChanged) + self.alphaSlider.sliderPressed.connect(self.alphaPressed) + self.alphaSlider.sliderReleased.connect(self.alphaReleased) + #self.canvas.sigSelectionChanged.connect(self.selectionChanged) + self.resetTransformBtn.clicked.connect(self.resetTransformClicked) + self.copyBtn.clicked.connect(self.copyClicked) + self.pasteBtn.clicked.connect(self.pasteClicked) + + self.setMovable(self.opts['movable']) ## update gui to reflect this option + + + if 'transform' in self.opts: + self.baseTransform = self.opts['transform'] + else: + self.baseTransform = pg.SRTTransform() + if 'pos' in self.opts and self.opts['pos'] is not None: + self.baseTransform.translate(self.opts['pos']) + if 'angle' in self.opts and self.opts['angle'] is not None: + self.baseTransform.rotate(self.opts['angle']) + if 'scale' in self.opts and self.opts['scale'] is not None: + self.baseTransform.scale(self.opts['scale']) + + ## create selection box (only visible when selected) + tr = self.baseTransform.saveState() + if 'scalable' not in opts and tr['scale'] == (1,1): + self.opts['scalable'] = True + + ## every CanvasItem implements its own individual selection box + ## so that subclasses are free to make their own. + self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable']) + #self.canvas.scene().addItem(self.selectBox) + self.selectBox.hide() + self.selectBox.setZValue(1e6) + self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved + self.selectBox.sigRegionChangeFinished.connect(self.selectBoxChangeFinished) + + ## set up the transformations that will be applied to the item + ## (It is not safe to use item.setTransform, since the item might count on that not changing) + self.itemRotation = QtGui.QGraphicsRotation() + self.itemScale = QtGui.QGraphicsScale() + self._graphicsItem.setTransformations([self.itemRotation, self.itemScale]) + + self.tempTransform = pg.SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. + self.userTransform = pg.SRTTransform() ## stores the total transform of the object + self.resetUserTransform() + + ## now happens inside resetUserTransform -> selectBoxToItem + # self.selectBoxBase = self.selectBox.getState().copy() + + + #print "Created canvas item", self + #print " base:", self.baseTransform + #print " user:", self.userTransform + #print " temp:", self.tempTransform + #print " bounds:", self.item.sceneBoundingRect() + + def setMovable(self, m): + self.opts['movable'] = m + + if m: + self.resetTransformBtn.show() + self.copyBtn.show() + self.pasteBtn.show() + else: + self.resetTransformBtn.hide() + self.copyBtn.hide() + self.pasteBtn.hide() + + def setCanvas(self, canvas): + ## Called by canvas whenever the item is added. + ## It is our responsibility to add all graphicsItems to the canvas's scene + ## The canvas will automatically add our graphicsitem, + ## so we just need to take care of the selectbox. + if canvas is self.canvas: + return + + if canvas is None: + self.canvas.removeFromScene(self._graphicsItem) + self.canvas.removeFromScene(self.selectBox) + else: + canvas.addToScene(self._graphicsItem) + canvas.addToScene(self.selectBox) + self.canvas = canvas + + def graphicsItem(self): + """Return the graphicsItem for this canvasItem.""" + return self._graphicsItem + + def parentItem(self): + return self._parentItem + + def setParentItem(self, parent): + self._parentItem = parent + if parent is not None: + if isinstance(parent, CanvasItem): + parent = parent.graphicsItem() + self.graphicsItem().setParentItem(parent) + + #def name(self): + #return self.opts['name'] + + def copyClicked(self): + CanvasItem.transformCopyBuffer = self.saveTransform() + + def pasteClicked(self): + t = CanvasItem.transformCopyBuffer + if t is None: + return + else: + self.restoreTransform(t) + + def mirrorY(self): + if not self.isMovable(): + return + + #flip = self.transformGui.mirrorImageCheck.isChecked() + #tr = self.userTransform.saveState() + + inv = pg.SRTTransform() + inv.scale(-1, 1) + self.userTransform = self.userTransform * inv + self.updateTransform() + self.selectBoxFromUser() + self.sigTransformChangeFinished.emit(self) + #if flip: + #if tr['scale'][0] < 0 xor tr['scale'][1] < 0: + #return + #else: + #self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]]) + #self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]]) + #self.userTransform.setRotate(-tr['angle']) + #self.updateTransform() + #self.selectBoxFromUser() + #return + #elif not flip: + #if tr['scale'][0] > 0 and tr['scale'][1] > 0: + #return + #else: + #self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]]) + #self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]]) + #self.userTransform.setRotate(-tr['angle']) + #self.updateTransform() + #self.selectBoxFromUser() + #return + + def mirrorXY(self): + if not self.isMovable(): + return + self.rotate(180.) + # inv = pg.SRTTransform() + # inv.scale(-1, -1) + # self.userTransform = self.userTransform * inv #flip lr/ud + # s=self.updateTransform() + # self.setTranslate(-2*s['pos'][0], -2*s['pos'][1]) + # self.selectBoxFromUser() + + + def hasUserTransform(self): + #print self.userRotate, self.userTranslate + return not self.userTransform.isIdentity() + + def ctrlWidget(self): + return self.ctrl + + def alphaChanged(self, val): + alpha = val / 1023. + self._graphicsItem.setOpacity(alpha) + + def isMovable(self): + return self.opts['movable'] + + + def selectBoxMoved(self): + """The selection box has moved; get its transformation information and pass to the graphics item""" + self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase) + self.updateTransform() + + def scale(self, x, y): + self.userTransform.scale(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def rotate(self, ang): + self.userTransform.rotate(ang) + self.selectBoxFromUser() + self.updateTransform() + + def translate(self, x, y): + self.userTransform.translate(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def setTranslate(self, x, y): + self.userTransform.setTranslate(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def setRotate(self, angle): + self.userTransform.setRotate(angle) + self.selectBoxFromUser() + self.updateTransform() + + def setScale(self, x, y): + self.userTransform.setScale(x, y) + self.selectBoxFromUser() + self.updateTransform() + + + def setTemporaryTransform(self, transform): + self.tempTransform = transform + self.updateTransform() + + def applyTemporaryTransform(self): + """Collapses tempTransform into UserTransform, resets tempTransform""" + self.userTransform = self.userTransform * self.tempTransform ## order is important! + self.resetTemporaryTransform() + self.selectBoxFromUser() ## update the selection box to match the new userTransform + + #st = self.userTransform.saveState() + + #self.userTransform = self.userTransform * self.tempTransform ## order is important! + + #### matrix multiplication affects the scale factors, need to reset + #if st['scale'][0] < 0 or st['scale'][1] < 0: + #nst = self.userTransform.saveState() + #self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]]) + + #self.resetTemporaryTransform() + #self.selectBoxFromUser() + #self.selectBoxChangeFinished() + + + + def resetTemporaryTransform(self): + self.tempTransform = pg.SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. + self.updateTransform() + + def transform(self): + return self._graphicsItem.transform() + + def updateTransform(self): + """Regenerate the item position from the base, user, and temp transforms""" + transform = self.baseTransform * self.userTransform * self.tempTransform ## order is important + s = transform.saveState() + self._graphicsItem.setPos(*s['pos']) + + self.itemRotation.setAngle(s['angle']) + self.itemScale.setXScale(s['scale'][0]) + self.itemScale.setYScale(s['scale'][1]) + + self.displayTransform(transform) + return(s) # return the transform state + + def displayTransform(self, transform): + """Updates transform numbers in the ctrl widget.""" + + tr = transform.saveState() + + self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1])) + self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle']) + self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1])) + #self.transformGui.mirrorImageCheck.setChecked(False) + #if tr['scale'][0] < 0: + # self.transformGui.mirrorImageCheck.setChecked(True) + + + def resetUserTransform(self): + #self.userRotate = 0 + #self.userTranslate = pg.Point(0,0) + self.userTransform.reset() + self.updateTransform() + + self.selectBox.blockSignals(True) + self.selectBoxToItem() + self.selectBox.blockSignals(False) + self.sigTransformChanged.emit(self) + self.sigTransformChangeFinished.emit(self) + + def resetTransformClicked(self): + self.resetUserTransform() + self.sigResetUserTransform.emit(self) + + def restoreTransform(self, tr): + try: + #self.userTranslate = pg.Point(tr['trans']) + #self.userRotate = tr['rot'] + self.userTransform = pg.SRTTransform(tr) + self.updateTransform() + + self.selectBoxFromUser() ## move select box to match + self.sigTransformChanged.emit(self) + self.sigTransformChangeFinished.emit(self) + except: + #self.userTranslate = pg.Point([0,0]) + #self.userRotate = 0 + self.userTransform = pg.SRTTransform() + debug.printExc("Failed to load transform:") + #print "set transform", self, self.userTranslate + + def saveTransform(self): + """Return a dict containing the current user transform""" + #print "save transform", self, self.userTranslate + #return {'trans': list(self.userTranslate), 'rot': self.userRotate} + return self.userTransform.saveState() + + def selectBoxFromUser(self): + """Move the selection box to match the current userTransform""" + ## user transform + #trans = QtGui.QTransform() + #trans.translate(*self.userTranslate) + #trans.rotate(-self.userRotate) + + #x2, y2 = trans.map(*self.selectBoxBase['pos']) + + self.selectBox.blockSignals(True) + self.selectBox.setState(self.selectBoxBase) + self.selectBox.applyGlobalTransform(self.userTransform) + #self.selectBox.setAngle(self.userRotate) + #self.selectBox.setPos([x2, y2]) + self.selectBox.blockSignals(False) + + + def selectBoxToItem(self): + """Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)""" + self.itemRect = self._graphicsItem.boundingRect() + rect = self._graphicsItem.mapRectToParent(self.itemRect) + self.selectBox.blockSignals(True) + self.selectBox.setPos([rect.x(), rect.y()]) + self.selectBox.setSize(rect.size()) + self.selectBox.setAngle(0) + self.selectBoxBase = self.selectBox.getState().copy() + self.selectBox.blockSignals(False) + + def zValue(self): + return self.opts['z'] + + def setZValue(self, z): + self.opts['z'] = z + if z is not None: + self._graphicsItem.setZValue(z) + + #def selectionChanged(self, canvas, items): + #self.selected = len(items) == 1 and (items[0] is self) + #self.showSelectBox() + + + def selectionChanged(self, sel, multi): + """ + Inform the item that its selection state has changed. + Arguments: + sel: bool, whether the item is currently selected + multi: bool, whether there are multiple items currently selected + """ + self.selectedAlone = sel and not multi + self.showSelectBox() + if self.selectedAlone: + self.ctrlWidget().show() + else: + self.ctrlWidget().hide() + + def showSelectBox(self): + """Display the selection box around this item if it is selected and movable""" + if self.selectedAlone and self.isMovable() and self.isVisible(): #and len(self.canvas.itemList.selectedItems())==1: + self.selectBox.show() + else: + self.selectBox.hide() + + def hideSelectBox(self): + self.selectBox.hide() + + + def selectBoxChanged(self): + self.selectBoxMoved() + #self.updateTransform(self.selectBox) + #self.emit(QtCore.SIGNAL('transformChanged'), self) + self.sigTransformChanged.emit(self) + + def selectBoxChangeFinished(self): + #self.emit(QtCore.SIGNAL('transformChangeFinished'), self) + self.sigTransformChangeFinished.emit(self) + + def alphaPressed(self): + """Hide selection box while slider is moving""" + self.hideSelectBox() + + def alphaReleased(self): + self.showSelectBox() + + def show(self): + if self.opts['visible']: + return + self.opts['visible'] = True + self._graphicsItem.show() + self.showSelectBox() + self.sigVisibilityChanged.emit(self) + + def hide(self): + if not self.opts['visible']: + return + self.opts['visible'] = False + self._graphicsItem.hide() + self.hideSelectBox() + self.sigVisibilityChanged.emit(self) + + def setVisible(self, vis): + if vis: + self.show() + else: + self.hide() + + def isVisible(self): + return self.opts['visible'] + + +class GroupCanvasItem(CanvasItem): + """ + Canvas item used for grouping others + """ + + def __init__(self, **opts): + defOpts = {'movable': False, 'scalable': False} + defOpts.update(opts) + item = pg.ItemGroup() + CanvasItem.__init__(self, item, **defOpts) + diff --git a/pyqtgraph/canvas/CanvasManager.py b/pyqtgraph/canvas/CanvasManager.py new file mode 100644 index 00000000..e89ec00f --- /dev/null +++ b/pyqtgraph/canvas/CanvasManager.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal +import weakref + +class CanvasManager(QtCore.QObject): + SINGLETON = None + + sigCanvasListChanged = QtCore.Signal() + + def __init__(self): + if CanvasManager.SINGLETON is not None: + raise Exception("Can only create one canvas manager.") + CanvasManager.SINGLETON = self + QtCore.QObject.__init__(self) + self.canvases = weakref.WeakValueDictionary() + + @classmethod + def instance(cls): + return CanvasManager.SINGLETON + + def registerCanvas(self, canvas, name): + n2 = name + i = 0 + while n2 in self.canvases: + n2 = "%s_%03d" % (name, i) + i += 1 + self.canvases[n2] = canvas + self.sigCanvasListChanged.emit() + return n2 + + def unregisterCanvas(self, name): + c = self.canvases[name] + del self.canvases[name] + self.sigCanvasListChanged.emit() + + def listCanvases(self): + return list(self.canvases.keys()) + + def getCanvas(self, name): + return self.canvases[name] + + +manager = CanvasManager() + + +class CanvasCombo(QtGui.QComboBox): + def __init__(self, parent=None): + QtGui.QComboBox.__init__(self, parent) + man = CanvasManager.instance() + man.sigCanvasListChanged.connect(self.updateCanvasList) + self.hostName = None + self.updateCanvasList() + + def updateCanvasList(self): + canvases = CanvasManager.instance().listCanvases() + canvases.insert(0, "") + if self.hostName in canvases: + canvases.remove(self.hostName) + + sel = self.currentText() + if sel in canvases: + self.blockSignals(True) ## change does not affect current selection; block signals during update + self.clear() + for i in canvases: + self.addItem(i) + if i == sel: + self.setCurrentIndex(self.count()) + + self.blockSignals(False) + + def setHostName(self, name): + self.hostName = name + self.updateCanvasList() + diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui new file mode 100644 index 00000000..da032906 --- /dev/null +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -0,0 +1,149 @@ + + + Form + + + + 0 + 0 + 490 + 414 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + + Store SVG + + + + + + + Store PNG + + + + + + + + 0 + 1 + + + + Auto Range + + + + + + + 0 + + + + + Check to display all local items in a remote canvas. + + + Redirect + + + + + + + + + + + + + 0 + 100 + + + + true + + + + 1 + + + + + + + + 0 + + + + + + + Reset Transforms + + + + + + + Mirror Selection + + + + + + + MirrorXY + + + + + + + + + + + + TreeWidget + QTreeWidget +
pyqtgraph.widgets.TreeWidget
+
+ + GraphicsView + QGraphicsView +
pyqtgraph.widgets.GraphicsView
+
+ + CanvasCombo + QComboBox +
CanvasManager
+
+
+ + +
diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py new file mode 100644 index 00000000..4d1d8208 --- /dev/null +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './canvas/CanvasTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(490, 414) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + self.view = GraphicsView(self.splitter) + self.view.setObjectName(_fromUtf8("view")) + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) + self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) + self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn")) + self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) + self.storePngBtn = QtGui.QPushButton(self.layoutWidget) + self.storePngBtn.setObjectName(_fromUtf8("storePngBtn")) + self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) + self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) + self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName(_fromUtf8("itemList")) + self.itemList.headerItem().setText(0, _fromUtf8("1")) + self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.ctrlLayout = QtGui.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) + self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) + self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", 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)) + self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.GraphicsView import GraphicsView +from CanvasManager import CanvasCombo +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py new file mode 100644 index 00000000..12afdf25 --- /dev/null +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './canvas/CanvasTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(490, 414) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.view = GraphicsView(self.splitter) + self.view.setObjectName("view") + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) + self.storeSvgBtn.setObjectName("storeSvgBtn") + self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) + self.storePngBtn = QtGui.QPushButton(self.layoutWidget) + self.storePngBtn.setObjectName("storePngBtn") + self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) + self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName("autoRangeBtn") + self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName("redirectCheck") + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName("redirectCombo") + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName("itemList") + self.itemList.headerItem().setText(0, "1") + self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.ctrlLayout = QtGui.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) + self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", 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)) + self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.GraphicsView import GraphicsView +from CanvasManager import CanvasCombo +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/canvas/TransformGuiTemplate.ui b/pyqtgraph/canvas/TransformGuiTemplate.ui new file mode 100644 index 00000000..d8312388 --- /dev/null +++ b/pyqtgraph/canvas/TransformGuiTemplate.ui @@ -0,0 +1,75 @@ + + + Form + + + + 0 + 0 + 224 + 117 + + + + + 0 + 0 + + + + Form + + + + 1 + + + 0 + + + + + Translate: + + + + + + + Rotate: + + + + + + + Scale: + + + + + + + + + + + + Mirror + + + + + + + Reflect + + + + + + + + + + diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py new file mode 100644 index 00000000..1fb86d24 --- /dev/null +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './canvas/TransformGuiTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(224, 117) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtGui.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.translateLabel = QtGui.QLabel(Form) + self.translateLabel.setObjectName(_fromUtf8("translateLabel")) + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtGui.QLabel(Form) + self.rotateLabel.setObjectName(_fromUtf8("rotateLabel")) + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtGui.QLabel(Form) + self.scaleLabel.setObjectName(_fromUtf8("scaleLabel")) + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.mirrorImageBtn = QtGui.QPushButton(Form) + self.mirrorImageBtn.setToolTip(_fromUtf8("")) + self.mirrorImageBtn.setObjectName(_fromUtf8("mirrorImageBtn")) + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtGui.QPushButton(Form) + self.reflectImageBtn.setObjectName(_fromUtf8("reflectImageBtn")) + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectImageBtn.setText(QtGui.QApplication.translate("Form", "Reflect", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py new file mode 100644 index 00000000..47b23faa --- /dev/null +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './canvas/TransformGuiTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(224, 117) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtGui.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.translateLabel = QtGui.QLabel(Form) + self.translateLabel.setObjectName("translateLabel") + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtGui.QLabel(Form) + self.rotateLabel.setObjectName("rotateLabel") + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtGui.QLabel(Form) + self.scaleLabel.setObjectName("scaleLabel") + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.mirrorImageBtn = QtGui.QPushButton(Form) + self.mirrorImageBtn.setToolTip("") + self.mirrorImageBtn.setObjectName("mirrorImageBtn") + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtGui.QPushButton(Form) + self.reflectImageBtn.setObjectName("reflectImageBtn") + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectImageBtn.setText(QtGui.QApplication.translate("Form", "Reflect", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/canvas/__init__.py b/pyqtgraph/canvas/__init__.py new file mode 100644 index 00000000..f649d0a1 --- /dev/null +++ b/pyqtgraph/canvas/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from .Canvas import * +from .CanvasItem import * \ No newline at end of file diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py new file mode 100644 index 00000000..db7dc732 --- /dev/null +++ b/pyqtgraph/configfile.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +""" +configfile.py - Human-readable text configuration file library +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Used for reading and writing dictionary objects to a python-like configuration +file format. Data structures may be nested and contain any data type as long +as it can be converted to/from a string using repr and eval. +""" + +import re, os, sys +from pgcollections import OrderedDict +GLOBAL_PATH = None # so not thread safe. +from . import units +from .python2_3 import asUnicode + +class ParseError(Exception): + def __init__(self, message, lineNum, line, fileName=None): + self.lineNum = lineNum + self.line = line + #self.message = message + self.fileName = fileName + Exception.__init__(self, message) + + def __str__(self): + if self.fileName is None: + msg = "Error parsing string at line %d:\n" % self.lineNum + else: + msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum) + msg += "%s\n%s" % (self.line, self.message) + return msg + #raise Exception() + + +def writeConfigFile(data, fname): + s = genString(data) + fd = open(fname, 'w') + fd.write(s) + fd.close() + +def readConfigFile(fname): + #cwd = os.getcwd() + global GLOBAL_PATH + if GLOBAL_PATH is not None: + fname2 = os.path.join(GLOBAL_PATH, fname) + if os.path.exists(fname2): + fname = fname2 + + GLOBAL_PATH = os.path.dirname(os.path.abspath(fname)) + + try: + #os.chdir(newDir) ## bad. + fd = open(fname) + s = asUnicode(fd.read()) + fd.close() + s = s.replace("\r\n", "\n") + s = s.replace("\r", "\n") + data = parseString(s)[1] + except ParseError: + sys.exc_info()[1].fileName = fname + raise + except: + print("Error while reading config file %s:"% fname) + raise + #finally: + #os.chdir(cwd) + return data + +def appendConfigFile(data, fname): + s = genString(data) + fd = open(fname, 'a') + fd.write(s) + fd.close() + + +def genString(data, indent=''): + s = '' + for k in data: + sk = str(k) + if len(sk) == 0: + print(data) + raise Exception('blank dict keys not allowed (see data above)') + if sk[0] == ' ' or ':' in sk: + print(data) + raise Exception('dict keys must not contain ":" or start with spaces [offending key is "%s"]' % sk) + if isinstance(data[k], dict): + s += indent + sk + ':\n' + s += genString(data[k], indent + ' ') + else: + s += indent + sk + ': ' + repr(data[k]) + '\n' + return s + +def parseString(lines, start=0): + + data = OrderedDict() + if isinstance(lines, basestring): + lines = lines.split('\n') + lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines + + indent = measureIndent(lines[start]) + ln = start - 1 + + try: + while True: + ln += 1 + #print ln + if ln >= len(lines): + break + + l = lines[ln] + + ## Skip blank lines or lines starting with # + if re.match(r'\s*#', l) or not re.search(r'\S', l): + continue + + ## Measure line indentation, make sure it is correct for this level + lineInd = measureIndent(l) + if lineInd < indent: + ln -= 1 + break + if lineInd > indent: + #print lineInd, indent + raise ParseError('Indentation is incorrect. Expected %d, got %d' % (indent, lineInd), ln+1, l) + + + if ':' not in l: + raise ParseError('Missing colon', ln+1, l) + + (k, p, v) = l.partition(':') + k = k.strip() + v = v.strip() + + ## set up local variables to use for eval + local = units.allUnits.copy() + local['OrderedDict'] = OrderedDict + local['readConfigFile'] = readConfigFile + if len(k) < 1: + raise ParseError('Missing name preceding colon', ln+1, l) + if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it. + try: + k1 = eval(k, local) + if type(k1) is tuple: + k = k1 + except: + pass + if re.search(r'\S', v) and v[0] != '#': ## eval the value + try: + val = eval(v, local) + except: + ex = sys.exc_info()[1] + raise ParseError("Error evaluating expression '%s': [%s: %s]" % (v, ex.__class__.__name__, str(ex)), (ln+1), l) + else: + if ln+1 >= len(lines) or measureIndent(lines[ln+1]) <= indent: + #print "blank dict" + val = {} + else: + #print "Going deeper..", ln+1 + (ln, val) = parseString(lines, start=ln+1) + data[k] = val + #print k, repr(val) + except ParseError: + raise + except: + ex = sys.exc_info()[1] + raise ParseError("%s: %s" % (ex.__class__.__name__, str(ex)), ln+1, l) + #print "Returning shallower..", ln+1 + return (ln, data) + +def measureIndent(s): + n = 0 + while n < len(s) and s[n] == ' ': + n += 1 + return n + + + +if __name__ == '__main__': + import tempfile + fn = tempfile.mktemp() + tf = open(fn, 'w') + cf = """ +key: 'value' +key2: ##comment + ##comment + key21: 'value' ## comment + ##comment + key22: [1,2,3] + key23: 234 #comment + """ + tf.write(cf) + tf.close() + print("=== Test:===") + num = 1 + for line in cf.split('\n'): + print("%02d %s" % (num, line)) + num += 1 + print(cf) + print("============") + data = readConfigFile(fn) + print(data) + os.remove(fn) \ No newline at end of file diff --git a/pyqtgraph/console/CmdInput.py b/pyqtgraph/console/CmdInput.py new file mode 100644 index 00000000..3e9730d6 --- /dev/null +++ b/pyqtgraph/console/CmdInput.py @@ -0,0 +1,62 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import asUnicode + +class CmdInput(QtGui.QLineEdit): + + sigExecuteCmd = QtCore.Signal(object) + + def __init__(self, parent): + QtGui.QLineEdit.__init__(self, parent) + self.history = [""] + self.ptr = 0 + #self.lastCmd = None + #self.setMultiline(False) + + def keyPressEvent(self, ev): + #print "press:", ev.key(), QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Enter + if ev.key() == QtCore.Qt.Key_Up and self.ptr < len(self.history) - 1: + self.setHistory(self.ptr+1) + ev.accept() + return + elif ev.key() == QtCore.Qt.Key_Down and self.ptr > 0: + self.setHistory(self.ptr-1) + ev.accept() + return + elif ev.key() == QtCore.Qt.Key_Return: + self.execCmd() + else: + QtGui.QLineEdit.keyPressEvent(self, ev) + self.history[0] = asUnicode(self.text()) + + def execCmd(self): + cmd = asUnicode(self.text()) + if len(self.history) == 1 or cmd != self.history[1]: + self.history.insert(1, cmd) + #self.lastCmd = cmd + self.history[0] = "" + self.setHistory(0) + self.sigExecuteCmd.emit(cmd) + + def setHistory(self, num): + self.ptr = num + self.setText(self.history[self.ptr]) + + #def setMultiline(self, m): + #height = QtGui.QFontMetrics(self.font()).lineSpacing() + #if m: + #self.setFixedHeight(height*5) + #else: + #self.setFixedHeight(height+15) + #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + + #def sizeHint(self): + #hint = QtGui.QPlainTextEdit.sizeHint(self) + #height = QtGui.QFontMetrics(self.font()).lineSpacing() + #hint.setHeight(height) + #return hint + + + + \ No newline at end of file diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py new file mode 100644 index 00000000..6fbe44a7 --- /dev/null +++ b/pyqtgraph/console/Console.py @@ -0,0 +1,375 @@ + +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +import sys, re, os, time, traceback, subprocess +import pyqtgraph as pg +if USE_PYSIDE: + from . import template_pyside as template +else: + from . import template_pyqt as template + +import pyqtgraph.exceptionHandling as exceptionHandling +import pickle + +class ConsoleWidget(QtGui.QWidget): + """ + Widget displaying console output and accepting command input. + Implements: + + - eval python expressions / exec python statements + - storable history of commands + - exception handling allowing commands to be interpreted in the context of any level in the exception stack frame + + Why not just use python in an interactive shell (or ipython) ? There are a few reasons: + + - pyside does not yet allow Qt event processing and interactive shell at the same time + - on some systems, typing in the console _blocks_ the qt event loop until the user presses enter. This can + be baffling and frustrating to users since it would appear the program has frozen. + - some terminals (eg windows cmd.exe) have notoriously unfriendly interfaces + - ability to add extra features like exception stack introspection + - ability to have multiple interactive prompts, including for spawned sub-processes + """ + + def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): + """ + ============ ============================================================================ + Arguments: + namespace dictionary containing the initial variables present in the default namespace + historyFile optional file for storing command history + text initial text to display in the console window + editor optional string for invoking code editor (called when stack trace entries are + double-clicked). May contain {fileName} and {lineNum} format keys. Example:: + + editorCommand --loadfile {fileName} --gotoline {lineNum} + ============ ============================================================================= + """ + QtGui.QWidget.__init__(self, parent) + if namespace is None: + namespace = {} + self.localNamespace = namespace + self.editor = editor + self.multiline = None + self.inCmd = False + + self.ui = template.Ui_Form() + self.ui.setupUi(self) + self.output = self.ui.output + self.input = self.ui.input + self.input.setFocus() + + if text is not None: + self.output.setPlainText(text) + + self.historyFile = historyFile + + history = self.loadHistory() + if history is not None: + self.input.history = [""] + history + self.ui.historyList.addItems(history[::-1]) + self.ui.historyList.hide() + self.ui.exceptionGroup.hide() + + self.input.sigExecuteCmd.connect(self.runCmd) + self.ui.historyBtn.toggled.connect(self.ui.historyList.setVisible) + self.ui.historyList.itemClicked.connect(self.cmdSelected) + self.ui.historyList.itemDoubleClicked.connect(self.cmdDblClicked) + self.ui.exceptionBtn.toggled.connect(self.ui.exceptionGroup.setVisible) + + self.ui.catchAllExceptionsBtn.toggled.connect(self.catchAllExceptions) + self.ui.catchNextExceptionBtn.toggled.connect(self.catchNextException) + self.ui.clearExceptionBtn.clicked.connect(self.clearExceptionClicked) + self.ui.exceptionStackList.itemClicked.connect(self.stackItemClicked) + self.ui.exceptionStackList.itemDoubleClicked.connect(self.stackItemDblClicked) + self.ui.onlyUncaughtCheck.toggled.connect(self.updateSysTrace) + + self.currentTraceback = None + + def loadHistory(self): + """Return the list of previously-invoked command strings (or None).""" + if self.historyFile is not None: + return pickle.load(open(self.historyFile, 'rb')) + + def saveHistory(self, history): + """Store the list of previously-invoked command strings.""" + if self.historyFile is not None: + pickle.dump(open(self.historyFile, 'wb'), history) + + def runCmd(self, cmd): + #cmd = str(self.input.lastCmd) + self.stdout = sys.stdout + self.stderr = sys.stderr + encCmd = re.sub(r'>', '>', re.sub(r'<', '<', cmd)) + encCmd = re.sub(r' ', ' ', encCmd) + + self.ui.historyList.addItem(cmd) + self.saveHistory(self.input.history[1:100]) + + try: + sys.stdout = self + sys.stderr = self + if self.multiline is not None: + self.write("
%s\n"%encCmd, html=True) + self.execMulti(cmd) + else: + self.write("
%s\n"%encCmd, html=True) + self.inCmd = True + self.execSingle(cmd) + + if not self.inCmd: + self.write("
\n", html=True) + + finally: + sys.stdout = self.stdout + sys.stderr = self.stderr + + sb = self.output.verticalScrollBar() + sb.setValue(sb.maximum()) + sb = self.ui.historyList.verticalScrollBar() + sb.setValue(sb.maximum()) + + def globals(self): + frame = self.currentFrame() + if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): + return self.currentFrame().tb_frame.f_globals + else: + return globals() + + def locals(self): + frame = self.currentFrame() + if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): + return self.currentFrame().tb_frame.f_locals + else: + return self.localNamespace + + def currentFrame(self): + ## Return the currently selected exception stack frame (or None if there is no exception) + if self.currentTraceback is None: + return None + index = self.ui.exceptionStackList.currentRow() + tb = self.currentTraceback + for i in range(index): + tb = tb.tb_next + return tb + + def execSingle(self, cmd): + try: + output = eval(cmd, self.globals(), self.locals()) + self.write(repr(output) + '\n') + except SyntaxError: + try: + exec(cmd, self.globals(), self.locals()) + except SyntaxError as exc: + if 'unexpected EOF' in exc.msg: + self.multiline = cmd + else: + self.displayException() + except: + self.displayException() + except: + self.displayException() + + + def execMulti(self, nextLine): + self.stdout.write(nextLine+"\n") + if nextLine.strip() != '': + self.multiline += "\n" + nextLine + return + else: + cmd = self.multiline + + try: + output = eval(cmd, self.globals(), self.locals()) + self.write(str(output) + '\n') + self.multiline = None + except SyntaxError: + try: + exec(cmd, self.globals(), self.locals()) + self.multiline = None + except SyntaxError as exc: + if 'unexpected EOF' in exc.msg: + self.multiline = cmd + else: + self.displayException() + self.multiline = None + except: + self.displayException() + self.multiline = None + except: + self.displayException() + self.multiline = None + + def write(self, strn, html=False): + self.output.moveCursor(QtGui.QTextCursor.End) + if html: + self.output.textCursor().insertHtml(strn) + else: + if self.inCmd: + self.inCmd = False + self.output.textCursor().insertHtml("
") + #self.stdout.write("

") + self.output.insertPlainText(strn) + #self.stdout.write(strn) + + def displayException(self): + """ + Display the current exception and stack. + """ + tb = traceback.format_exc() + lines = [] + indent = 4 + prefix = '' + for l in tb.split('\n'): + lines.append(" "*indent + prefix + l) + self.write('\n'.join(lines)) + self.exceptionHandler(*sys.exc_info()) + + def cmdSelected(self, item): + index = -(self.ui.historyList.row(item)+1) + self.input.setHistory(index) + self.input.setFocus() + + def cmdDblClicked(self, item): + index = -(self.ui.historyList.row(item)+1) + self.input.setHistory(index) + self.input.execCmd() + + def flush(self): + pass + + def catchAllExceptions(self, catch=True): + """ + If True, the console will catch all unhandled exceptions and display the stack + trace. Each exception caught clears the last. + """ + self.ui.catchAllExceptionsBtn.setChecked(catch) + if catch: + self.ui.catchNextExceptionBtn.setChecked(False) + self.enableExceptionHandling() + self.ui.exceptionBtn.setChecked(True) + else: + self.disableExceptionHandling() + + def catchNextException(self, catch=True): + """ + If True, the console will catch the next unhandled exception and display the stack + trace. + """ + self.ui.catchNextExceptionBtn.setChecked(catch) + if catch: + self.ui.catchAllExceptionsBtn.setChecked(False) + self.enableExceptionHandling() + self.ui.exceptionBtn.setChecked(True) + else: + self.disableExceptionHandling() + + def enableExceptionHandling(self): + exceptionHandling.register(self.exceptionHandler) + self.updateSysTrace() + + def disableExceptionHandling(self): + exceptionHandling.unregister(self.exceptionHandler) + self.updateSysTrace() + + def clearExceptionClicked(self): + self.currentTraceback = None + self.ui.exceptionInfoLabel.setText("[No current exception]") + self.ui.exceptionStackList.clear() + self.ui.clearExceptionBtn.setEnabled(False) + + def stackItemClicked(self, item): + pass + + def stackItemDblClicked(self, item): + editor = self.editor + if editor is None: + editor = pg.getConfigOption('editorCommand') + if editor is None: + return + tb = self.currentFrame() + lineNum = tb.tb_lineno + fileName = tb.tb_frame.f_code.co_filename + subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) + + + #def allExceptionsHandler(self, *args): + #self.exceptionHandler(*args) + + #def nextExceptionHandler(self, *args): + #self.ui.catchNextExceptionBtn.setChecked(False) + #self.exceptionHandler(*args) + + def updateSysTrace(self): + ## Install or uninstall sys.settrace handler + + if not self.ui.catchNextExceptionBtn.isChecked() and not self.ui.catchAllExceptionsBtn.isChecked(): + if sys.gettrace() == self.systrace: + sys.settrace(None) + return + + if self.ui.onlyUncaughtCheck.isChecked(): + if sys.gettrace() == self.systrace: + sys.settrace(None) + else: + if sys.gettrace() is not None and sys.gettrace() != self.systrace: + self.ui.onlyUncaughtCheck.setChecked(False) + raise Exception("sys.settrace is in use; cannot monitor for caught exceptions.") + else: + sys.settrace(self.systrace) + + def exceptionHandler(self, excType, exc, tb): + if self.ui.catchNextExceptionBtn.isChecked(): + self.ui.catchNextExceptionBtn.setChecked(False) + elif not self.ui.catchAllExceptionsBtn.isChecked(): + return + + self.ui.clearExceptionBtn.setEnabled(True) + self.currentTraceback = tb + + excMessage = ''.join(traceback.format_exception_only(excType, exc)) + self.ui.exceptionInfoLabel.setText(excMessage) + self.ui.exceptionStackList.clear() + for index, line in enumerate(traceback.extract_tb(tb)): + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) + + def systrace(self, frame, event, arg): + if event == 'exception' and self.checkException(*arg): + self.exceptionHandler(*arg) + return self.systrace + + def checkException(self, excType, exc, tb): + ## Return True if the exception is interesting; False if it should be ignored. + + filename = tb.tb_frame.f_code.co_filename + function = tb.tb_frame.f_code.co_name + + ## Go through a list of common exception points we like to ignore: + if excType is GeneratorExit or excType is StopIteration: + return False + if excType is KeyError: + if filename.endswith('python2.7/weakref.py') and function in ('__contains__', 'get'): + return False + if filename.endswith('python2.7/copy.py') and function == '_keep_alive': + return False + if excType is AttributeError: + if filename.endswith('python2.7/collections.py') and function == '__init__': + return False + if filename.endswith('numpy/core/fromnumeric.py') and function in ('all', '_wrapit', 'transpose', 'sum'): + return False + if filename.endswith('numpy/core/arrayprint.py') and function in ('_array2string'): + return False + if filename.endswith('MetaArray.py') and function == '__getattr__': + for name in ('__array_interface__', '__array_struct__', '__array__'): ## numpy looks for these when converting objects to array + if name in exc: + return False + if filename.endswith('flowchart/eq.py'): + return False + if filename.endswith('pyqtgraph/functions.py') and function == 'makeQImage': + return False + if excType is TypeError: + if filename.endswith('numpy/lib/function_base.py') and function == 'iterable': + return False + if excType is ZeroDivisionError: + if filename.endswith('python2.7/traceback.py'): + return False + + return True + \ No newline at end of file diff --git a/pyqtgraph/console/__init__.py b/pyqtgraph/console/__init__.py new file mode 100644 index 00000000..16436abd --- /dev/null +++ b/pyqtgraph/console/__init__.py @@ -0,0 +1 @@ +from .Console import ConsoleWidget \ No newline at end of file diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui new file mode 100644 index 00000000..6e5c5be3 --- /dev/null +++ b/pyqtgraph/console/template.ui @@ -0,0 +1,184 @@ + + + Form + + + + 0 + 0 + 710 + 497 + + + + Console + + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + + + + Monospace + + + + true + + + + + + + + + + + + History.. + + + true + + + + + + + Exceptions.. + + + true + + + + + + + + + + + Monospace + + + + + + Exception Handling + + + + 0 + + + 0 + + + 0 + + + + + Show All Exceptions + + + true + + + + + + + Show Next Exception + + + true + + + + + + + Only Uncaught Exceptions + + + true + + + + + + + true + + + + + + + Run commands in selected stack frame + + + true + + + + + + + Exception Info + + + + + + + false + + + Clear Exception + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + CmdInput + QLineEdit +
.CmdInput
+
+
+ + +
diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py new file mode 100644 index 00000000..89ee6cff --- /dev/null +++ b/pyqtgraph/console/template_pyqt.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './console/template.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(710, 497) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName(_fromUtf8("splitter")) + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) + self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.output = QtGui.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Monospace")) + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName(_fromUtf8("output")) + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName(_fromUtf8("input")) + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtGui.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName(_fromUtf8("historyBtn")) + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtGui.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName(_fromUtf8("exceptionBtn")) + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtGui.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Monospace")) + self.historyList.setFont(font) + self.historyList.setObjectName(_fromUtf8("historyList")) + self.exceptionGroup = QtGui.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup")) + self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn")) + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName(_fromUtf8("catchNextExceptionBtn")) + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck")) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList")) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) + self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn")) + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Console", None, QtGui.QApplication.UnicodeUTF8)) + self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) + self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) + self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) + self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) + self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) + +from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py new file mode 100644 index 00000000..0493a0fe --- /dev/null +++ b/pyqtgraph/console/template_pyside.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './console/template.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(710, 497) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.output = QtGui.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName("output") + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName("input") + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtGui.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName("historyBtn") + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtGui.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName("exceptionBtn") + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtGui.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + self.historyList.setFont(font) + self.historyList.setObjectName("historyList") + self.exceptionGroup = QtGui.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName("exceptionGroup") + self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName("exceptionStackList") + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) + self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Console", None, QtGui.QApplication.UnicodeUTF8)) + self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) + self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) + self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) + self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) + self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) + +from .CmdInput import CmdInput diff --git a/debug.py b/pyqtgraph/debug.py similarity index 90% rename from debug.py rename to pyqtgraph/debug.py index 2a2157db..ea9157aa 100644 --- a/debug.py +++ b/pyqtgraph/debug.py @@ -6,9 +6,9 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile -import ptime +from . import ptime from numpy import ndarray -from PyQt4 import QtCore, QtGui +from .Qt import QtCore, QtGui __ftraceDepth = 0 def ftrace(func): @@ -18,13 +18,13 @@ def ftrace(func): def w(*args, **kargs): global __ftraceDepth pfx = " " * __ftraceDepth - print pfx + func.__name__ + " start" + print(pfx + func.__name__ + " start") __ftraceDepth += 1 try: rv = func(*args, **kargs) finally: __ftraceDepth -= 1 - print pfx + func.__name__ + " done" + print(pfx + func.__name__ + " done") return rv return w @@ -39,20 +39,20 @@ def printExc(msg='', indent=4, prefix='|'): """Print an error message followed by an indented exception backtrace (This function is intended to be called within except: blocks)""" exc = getExc(indent, prefix + ' ') - print "[%s] %s\n" % (time.strftime("%H:%M:%S"), msg) - print " "*indent + prefix + '='*30 + '>>' - print exc - print " "*indent + prefix + '='*30 + '<<' + print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg)) + print(" "*indent + prefix + '='*30 + '>>') + print(exc) + print(" "*indent + prefix + '='*30 + '<<') def printTrace(msg='', indent=4, prefix='|'): """Print an error message followed by an indented stack trace""" trace = backtrace(1) #exc = getExc(indent, prefix + ' ') - print "[%s] %s\n" % (time.strftime("%H:%M:%S"), msg) - print " "*indent + prefix + '='*30 + '>>' + print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg)) + print(" "*indent + prefix + '='*30 + '>>') for line in trace.split('\n'): - print " "*indent + prefix + " " + line - print " "*indent + prefix + '='*30 + '<<' + print(" "*indent + prefix + " " + line) + print(" "*indent + prefix + '='*30 + '<<') def backtrace(skip=0): @@ -107,12 +107,12 @@ def findRefPath(startObj, endObj, maxLen=8, restart=True, seen={}, path=None, ig #print prefix+" LOOP", objChainString([r]+path) continue except: - print r - print path + print(r) + print(path) raise if r is startObj: refs.append([r]) - print refPathString([startObj]+path) + print(refPathString([startObj]+path)) continue if maxLen == 0: #print prefix+" END:", objChainString([r]+path) @@ -125,7 +125,7 @@ def findRefPath(startObj, endObj, maxLen=8, restart=True, seen={}, path=None, ig if cache[0] >= maxLen: tree = cache[1] for p in tree: - print refPathString(p+path) + print(refPathString(p+path)) except KeyError: pass @@ -147,14 +147,14 @@ def findRefPath(startObj, endObj, maxLen=8, restart=True, seen={}, path=None, ig def objString(obj): """Return a short but descriptive string for any object""" try: - if type(obj) in [int, long, float]: + if type(obj) in [int, float]: return str(obj) elif isinstance(obj, dict): if len(obj) > 5: - return "" % (",".join(obj.keys()[:5])) + return "" % (",".join(list(obj.keys())[:5])) else: - return "" % (",".join(obj.keys())) - elif isinstance(obj, basestring): + return "" % (",".join(list(obj.keys()))) + elif isinstance(obj, str): if len(obj) > 50: return '"%s..."' % obj[:50] else: @@ -261,19 +261,19 @@ def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False): if recursive: if type(obj) in [list, tuple]: if verbose: - print indent+"list:" + print(indent+"list:") for o in obj: s = objectSize(o, ignore=ignore, verbose=verbose, depth=depth+1) if verbose: - print indent+' +', s + print(indent+' +', s) size += s elif isinstance(obj, dict): if verbose: - print indent+"list:" + print(indent+"list:") for k in obj: s = objectSize(obj[k], ignore=ignore, verbose=verbose, depth=depth+1) if verbose: - print indent+' +', k, s + print(indent+' +', k, s) size += s #elif isinstance(obj, QtCore.QObject): #try: @@ -291,7 +291,7 @@ def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False): #if isinstance(obj, types.InstanceType): gc.collect() if verbose: - print indent+'attrs:' + print(indent+'attrs:') for k in dir(obj): if k in ['__dict__']: continue @@ -311,13 +311,13 @@ def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False): s = objectSize(o, ignore=ignore, verbose=verbose, depth=depth+1) size += s if verbose: - print indent + " +", k, s + print(indent + " +", k, s) #else: #if verbose: #print indent + ' -', k, len(refs) return size -class GarbageWatcher: +class GarbageWatcher(object): """ Convenient dictionary for holding weak references to objects. Mainly used to check whether the objects have been collect yet or not. @@ -349,14 +349,14 @@ class GarbageWatcher: for k in self.objs: dead.remove(k) alive.append(k) - print "Deleted objects:", dead - print "Live objects:", alive + print("Deleted objects:", dead) + print("Live objects:", alive) def __getitem__(self, item): return self.objs[item] -class Profiler: +class Profiler(object): """Simple profiler allowing measurement of multiple time intervals. Example: @@ -379,20 +379,20 @@ class Profiler: self.t0 = ptime.time() self.t1 = self.t0 self.msg = " "*self.depth + msg - print self.msg, ">>> Started" + print(self.msg, ">>> Started") def mark(self, msg=''): if self.disabled: return t1 = ptime.time() - print " "+self.msg, msg, "%gms" % ((t1-self.t1)*1000) + print(" "+self.msg, msg, "%gms" % ((t1-self.t1)*1000)) self.t1 = t1 def finish(self): if self.disabled: return t1 = ptime.time() - print self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000) + print(self.msg, '<<< Finished, total time:', "%gms" % ((t1-self.t0)*1000)) def __del__(self): Profiler.depth -= 1 @@ -419,7 +419,7 @@ def _getr(slist, olist, first=True): oid = id(e) typ = type(e) - if oid in olist or typ is int or typ is long: ## or e in olist: ## since we're excluding all ints, there is no longer a need to check for olist keys + if oid in olist or typ is int: ## or e in olist: ## since we're excluding all ints, there is no longer a need to check for olist keys continue olist[oid] = e if first and (i%1000) == 0: @@ -451,7 +451,7 @@ def lookup(oid, objects=None): -class ObjTracker: +class ObjTracker(object): """ Tracks all objects under the sun, reporting the changes between snapshots: what objects are created, deleted, and persistent. This class is very useful for tracking memory leaks. The class goes to great (but not heroic) lengths to avoid tracking @@ -565,23 +565,23 @@ class ObjTracker: self.persistentRefs.update(persistentRefs) - print "----------- Count changes since start: ----------" + print("----------- Count changes since start: ----------") c1 = count.copy() for k in self.startCount: c1[k] = c1.get(k, 0) - self.startCount[k] - typs = c1.keys() + typs = list(c1.keys()) typs.sort(lambda a,b: cmp(c1[a], c1[b])) for t in typs: if c1[t] == 0: continue num = "%d" % c1[t] - print " " + num + " "*(10-len(num)) + str(t) + print(" " + num + " "*(10-len(num)) + str(t)) - print "----------- %d Deleted since last diff: ------------" % len(delRefs) + print("----------- %d Deleted since last diff: ------------" % len(delRefs)) self.report(delRefs, objs, **kargs) - print "----------- %d Created since last diff: ------------" % len(createRefs) + print("----------- %d Created since last diff: ------------" % len(createRefs)) self.report(createRefs, objs, **kargs) - print "----------- %d Created since start (persistent): ------------" % len(persistentRefs) + print("----------- %d Created since start (persistent): ------------" % len(persistentRefs)) self.report(persistentRefs, objs, **kargs) @@ -600,14 +600,14 @@ class ObjTracker: return type(o) is cls or id(o) in cls.allObjs def collect(self): - print "Collecting list of all objects..." + print("Collecting list of all objects...") gc.collect() objs = get_all_objects() frame = sys._getframe() del objs[id(frame)] ## ignore the current frame del objs[id(frame.f_code)] - ignoreTypes = [int, long] + ignoreTypes = [int] refs = {} count = {} for k in objs: @@ -628,7 +628,7 @@ class ObjTracker: ObjTracker.allObjs[id(typStr)] = None count[typ] = count.get(typ, 0) + 1 - print "All objects: %d Tracked objects: %d" % (len(objs), len(refs)) + print("All objects: %d Tracked objects: %d" % (len(objs), len(refs))) return refs, count, objs def forgetRef(self, ref): @@ -669,14 +669,14 @@ class ObjTracker: rev[typ].append(oid) c = count.get(typ, [0,0]) count[typ] = [c[0]+1, c[1]+objectSize(obj)] - typs = count.keys() + typs = list(count.keys()) typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) for t in typs: line = " %d\t%d\t%s" % (count[t][0], count[t][1], t) if showIDs: line += "\t"+",".join(map(str,rev[t])) - print line + print(line) def findTypes(self, refs, regex): allObjs = get_all_objects() @@ -709,21 +709,21 @@ def describeObj(obj, depth=4, path=None, ignore=None): for ref in refs: if id(ref) in ignore: continue - if id(ref) in map(id, path): - print "Cyclic reference: " + refPathString([ref]+path) + if id(ref) in list(map(id, path)): + print("Cyclic reference: " + refPathString([ref]+path)) printed = True continue newPath = [ref]+path if len(newPath) >= depth: refStr = refPathString(newPath) if '[_]' not in refStr: ## ignore '_' references generated by the interactive shell - print refStr + print(refStr) printed = True else: describeObj(ref, depth, newPath, ignore) printed = True if not printed: - print "Dead end: " + refPathString(path) + print("Dead end: " + refPathString(path)) @@ -773,18 +773,18 @@ def searchRefs(obj, *args): ignore[id(refs)] = None refs = [r for r in refs if id(r) not in ignore] elif a == 't': - print map(typeStr, refs) + print(list(map(typeStr, refs))) elif a == 'i': - print map(id, refs) + print(list(map(id, refs))) elif a == 'l': def slen(o): if hasattr(o, '__len__'): return len(o) else: return None - print map(slen, refs) + print(list(map(slen, refs))) elif a == 'o': - print obj + print(obj) elif a == 'ro': return obj elif a == 'rr': @@ -820,14 +820,14 @@ def findObj(regex): def listRedundantModules(): """List modules that have been imported more than once via different paths.""" mods = {} - for name, mod in sys.modules.iteritems(): + for name, mod in sys.modules.items(): if not hasattr(mod, '__file__'): continue mfile = os.path.abspath(mod.__file__) if mfile[-1] == 'c': mfile = mfile[:-1] if mfile in mods: - print "module at %s has 2 names: %s, %s" % (mfile, name, mods[mfile]) + print("module at %s has 2 names: %s, %s" % (mfile, name, mods[mfile])) else: mods[mfile] = name @@ -841,7 +841,7 @@ def walkQObjectTree(obj, counts=None, verbose=False, depth=0): """ if verbose: - print " "*depth + typeStr(obj) + print(" "*depth + typeStr(obj)) report = False if counts is None: counts = {} @@ -871,12 +871,12 @@ def qObjectReport(verbose=False): QObjCache[oid] += " " + obj.text() except: pass - print "check obj", oid, unicode(QObjCache[oid]) + print("check obj", oid, str(QObjCache[oid])) if obj.parent() is None: walkQObjectTree(obj, count, verbose) - typs = count.keys() + typs = list(count.keys()) typs.sort() for t in typs: - print count[t], "\t", t + print(count[t], "\t", t) diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py new file mode 100644 index 00000000..83610937 --- /dev/null +++ b/pyqtgraph/dockarea/Container.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +import weakref + +class Container(object): + #sigStretchChanged = QtCore.Signal() ## can't do this here; not a QObject. + + def __init__(self, area): + object.__init__(self) + self.area = area + self._container = None + self._stretch = (10, 10) + self.stretches = weakref.WeakKeyDictionary() + + def container(self): + return self._container + + def containerChanged(self, c): + self._container = c + + def type(self): + return None + + def insert(self, new, pos=None, neighbor=None): + if not isinstance(new, list): + new = [new] + if neighbor is None: + if pos == 'before': + index = 0 + else: + index = self.count() + else: + index = self.indexOf(neighbor) + if index == -1: + index = 0 + if pos == 'after': + index += 1 + + for n in new: + #print "change container", n, " -> ", self + n.containerChanged(self) + #print "insert", n, " -> ", self, index + self._insertItem(n, index) + index += 1 + n.sigStretchChanged.connect(self.childStretchChanged) + #print "child added", self + self.updateStretch() + + def apoptose(self, propagate=True): + ##if there is only one (or zero) item in this container, disappear. + cont = self._container + c = self.count() + if c > 1: + return + if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top) + if self is self.area.topContainer: + return + self.container().insert(self.widget(0), 'before', self) + #print "apoptose:", self + self.close() + if propagate and cont is not None: + cont.apoptose() + + def close(self): + self.area = None + self._container = None + self.setParent(None) + + def childEvent(self, ev): + ch = ev.child() + if ev.removed() and hasattr(ch, 'sigStretchChanged'): + #print "Child", ev.child(), "removed, updating", self + try: + ch.sigStretchChanged.disconnect(self.childStretchChanged) + except: + pass + self.updateStretch() + + def childStretchChanged(self): + #print "child", QtCore.QObject.sender(self), "changed shape, updating", self + self.updateStretch() + + def setStretch(self, x=None, y=None): + #print "setStretch", self, x, y + self._stretch = (x, y) + self.sigStretchChanged.emit() + + def updateStretch(self): + ###Set the stretch values for this container to reflect its contents + pass + + + def stretch(self): + """Return the stretch factors for this container""" + return self._stretch + + +class SplitContainer(Container, QtGui.QSplitter): + """Horizontal or vertical splitter with some changes: + - save/restore works correctly + """ + sigStretchChanged = QtCore.Signal() + + def __init__(self, area, orientation): + QtGui.QSplitter.__init__(self) + self.setOrientation(orientation) + Container.__init__(self, area) + #self.splitterMoved.connect(self.restretchChildren) + + def _insertItem(self, item, index): + self.insertWidget(index, item) + item.show() ## need to show since it may have been previously hidden by tab + + def saveState(self): + sizes = self.sizes() + if all([x == 0 for x in sizes]): + sizes = [10] * len(sizes) + return {'sizes': sizes} + + def restoreState(self, state): + sizes = state['sizes'] + self.setSizes(sizes) + for i in range(len(sizes)): + self.setStretchFactor(i, sizes[i]) + + def childEvent(self, ev): + QtGui.QSplitter.childEvent(self, ev) + Container.childEvent(self, ev) + + #def restretchChildren(self): + #sizes = self.sizes() + #tot = sum(sizes) + + + + +class HContainer(SplitContainer): + def __init__(self, area): + SplitContainer.__init__(self, area, QtCore.Qt.Horizontal) + + def type(self): + return 'horizontal' + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + #print "updateStretch", self + x = 0 + y = 0 + sizes = [] + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + x += wx + y = max(y, wy) + sizes.append(wx) + #print " child", self.widget(i), wx, wy + self.setStretch(x, y) + #print sizes + + tot = float(sum(sizes)) + if tot == 0: + scale = 1.0 + else: + scale = self.width() / tot + self.setSizes([int(s*scale) for s in sizes]) + + + +class VContainer(SplitContainer): + def __init__(self, area): + SplitContainer.__init__(self, area, QtCore.Qt.Vertical) + + def type(self): + return 'vertical' + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + #print "updateStretch", self + x = 0 + y = 0 + sizes = [] + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + y += wy + x = max(x, wx) + sizes.append(wy) + #print " child", self.widget(i), wx, wy + self.setStretch(x, y) + + #print sizes + tot = float(sum(sizes)) + if tot == 0: + scale = 1.0 + else: + scale = self.height() / tot + self.setSizes([int(s*scale) for s in sizes]) + + +class TContainer(Container, QtGui.QWidget): + sigStretchChanged = QtCore.Signal() + def __init__(self, area): + QtGui.QWidget.__init__(self) + Container.__init__(self, area) + self.layout = QtGui.QGridLayout() + self.layout.setSpacing(0) + self.layout.setContentsMargins(0,0,0,0) + self.setLayout(self.layout) + + self.hTabLayout = QtGui.QHBoxLayout() + self.hTabBox = QtGui.QWidget() + self.hTabBox.setLayout(self.hTabLayout) + self.hTabLayout.setSpacing(2) + self.hTabLayout.setContentsMargins(0,0,0,0) + self.layout.addWidget(self.hTabBox, 0, 1) + + self.stack = QtGui.QStackedWidget() + self.layout.addWidget(self.stack, 1, 1) + self.stack.childEvent = self.stackChildEvent + + + self.setLayout(self.layout) + for n in ['count', 'widget', 'indexOf']: + setattr(self, n, getattr(self.stack, n)) + + + def _insertItem(self, item, index): + if not isinstance(item, Dock.Dock): + raise Exception("Tab containers may hold only docks, not other containers.") + self.stack.insertWidget(index, item) + self.hTabLayout.insertWidget(index, item.label) + #QtCore.QObject.connect(item.label, QtCore.SIGNAL('clicked'), self.tabClicked) + item.label.sigClicked.connect(self.tabClicked) + self.tabClicked(item.label) + + def tabClicked(self, tab, ev=None): + if ev is None or ev.button() == QtCore.Qt.LeftButton: + for i in range(self.count()): + w = self.widget(i) + if w is tab.dock: + w.label.setDim(False) + self.stack.setCurrentIndex(i) + else: + w.label.setDim(True) + + def type(self): + return 'tab' + + def saveState(self): + return {'index': self.stack.currentIndex()} + + def restoreState(self, state): + self.stack.setCurrentIndex(state['index']) + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + x = 0 + y = 0 + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + x = max(x, wx) + y = max(y, wy) + self.setStretch(x, y) + + def stackChildEvent(self, ev): + QtGui.QStackedWidget.childEvent(self.stack, ev) + Container.childEvent(self, ev) + +from . import Dock diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py new file mode 100644 index 00000000..35781535 --- /dev/null +++ b/pyqtgraph/dockarea/Dock.py @@ -0,0 +1,313 @@ +from pyqtgraph.Qt import QtCore, QtGui + +from .DockDrop import * +from pyqtgraph.widgets.VerticalLabel import VerticalLabel + +class Dock(QtGui.QWidget, DockDrop): + + sigStretchChanged = QtCore.Signal() + + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True): + QtGui.QWidget.__init__(self) + DockDrop.__init__(self) + self.area = area + self.label = DockLabel(name, self) + self.labelHidden = False + self.moveLabel = True ## If false, the dock is no longer allowed to move the label. + self.autoOrient = autoOrientation + self.orientation = 'horizontal' + #self.label.setAlignment(QtCore.Qt.AlignHCenter) + self.topLayout = QtGui.QGridLayout() + self.topLayout.setContentsMargins(0, 0, 0, 0) + self.topLayout.setSpacing(0) + self.setLayout(self.topLayout) + self.topLayout.addWidget(self.label, 0, 1) + self.widgetArea = QtGui.QWidget() + self.topLayout.addWidget(self.widgetArea, 1, 1) + self.layout = QtGui.QGridLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + self.widgetArea.setLayout(self.layout) + self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.widgets = [] + self.currentRow = 0 + #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; + 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; + border-left-width: 0px; + }""" + self.nStyle = """ + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + }""" + self.dragStyle = """ + Dock > QWidget { + border: 4px solid #00F; + border-radius: 5px; + }""" + self.setAutoFillBackground(False) + self.widgetArea.setStyleSheet(self.hStyle) + + self.setStretch(*size) + + if widget is not None: + self.addWidget(widget) + + if hideTitle: + self.hideTitleBar() + + def implements(self, name=None): + if name is None: + return ['dock'] + else: + return name == 'dock' + + def setStretch(self, x=None, y=None): + """ + 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. + """ + #print "setStretch", self, x, y + #self._stretch = (x, y) + if x is None: + x = 0 + if y is None: + y = 0 + #policy = self.sizePolicy() + #policy.setHorizontalStretch(x) + #policy.setVerticalStretch(y) + #self.setSizePolicy(policy) + self._stretch = (x, y) + self.sigStretchChanged.emit() + #print "setStretch", self, x, y, self.stretch() + + def stretch(self): + #policy = self.sizePolicy() + #return policy.horizontalStretch(), policy.verticalStretch() + return self._stretch + + #def stretch(self): + #return self._stretch + + def hideTitleBar(self): + """ + Hide the title bar for this Dock. + This will prevent the Dock being moved by the user. + """ + self.label.hide() + self.labelHidden = True + if 'center' in self.allowedAreas: + self.allowedAreas.remove('center') + self.updateStyle() + + def showTitleBar(self): + """ + Show the title bar for this Dock. + """ + self.label.show() + self.labelHidden = False + self.allowedAreas.add('center') + self.updateStyle() + + def setOrientation(self, o='auto', force=False): + """ + 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. + """ + #print self.name(), "setOrientation", o, force + if o == 'auto' and self.autoOrient: + if self.container().type() == 'tab': + o = 'horizontal' + elif self.width() > self.height()*1.5: + o = 'vertical' + else: + o = 'horizontal' + if force or self.orientation != o: + self.orientation = o + self.label.setOrientation(o) + self.updateStyle() + + def updateStyle(self): + ## updates orientation and appearance of title bar + #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() + if self.labelHidden: + self.widgetArea.setStyleSheet(self.nStyle) + elif self.orientation == 'vertical': + self.label.setOrientation('vertical') + if self.moveLabel: + #print self.name(), "reclaim label" + self.topLayout.addWidget(self.label, 1, 0) + self.widgetArea.setStyleSheet(self.vStyle) + else: + self.label.setOrientation('horizontal') + if self.moveLabel: + #print self.name(), "reclaim label" + self.topLayout.addWidget(self.label, 0, 1) + self.widgetArea.setStyleSheet(self.hStyle) + + def resizeEvent(self, ev): + self.setOrientation() + self.resizeOverlay(self.size()) + + def name(self): + return str(self.label.text()) + + def container(self): + return self._container + + def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): + """ + Add a new widget to the interior of this Dock. + Each Dock uses a QGridLayout to arrange widgets within. + """ + if row is None: + row = self.currentRow + self.currentRow = max(row+1, self.currentRow) + self.widgets.append(widget) + self.layout.addWidget(widget, row, col, rowspan, colspan) + self.raiseOverlay() + + + def startDrag(self): + self.drag = QtGui.QDrag(self) + mime = QtCore.QMimeData() + #mime.setPlainText("asd") + self.drag.setMimeData(mime) + self.widgetArea.setStyleSheet(self.dragStyle) + self.update() + action = self.drag.exec_() + self.updateStyle() + + def float(self): + self.area.floatDock(self) + + def containerChanged(self, c): + #print self.name(), "container changed" + self._container = c + if c.type() != 'tab': + self.moveLabel = True + self.label.setDim(False) + else: + self.moveLabel = False + + self.setOrientation(force=True) + + def __repr__(self): + return "" % (self.name(), self.stretch()) + + +class DockLabel(VerticalLabel): + + sigClicked = QtCore.Signal(object, object) + + def __init__(self, text, dock): + self.dim = False + self.fixedWidth = False + VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) + self.setAlignment(QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter) + self.dock = dock + self.updateStyle() + self.setAutoFillBackground(False) + + #def minimumSizeHint(self): + ##sh = QtGui.QWidget.minimumSizeHint(self) + #return QtCore.QSize(20, 20) + + def updateStyle(self): + r = '3px' + if self.dim: + fg = '#aaa' + bg = '#44a' + border = '#339' + else: + fg = '#fff' + bg = '#66c' + 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; + 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; + border-bottom: 2px solid %s; + padding-left: 3px; + padding-right: 3px; + }""" % (bg, fg, r, r, border) + self.setStyleSheet(self.hStyle) + + def setDim(self, d): + if self.dim != d: + self.dim = d + self.updateStyle() + + def setOrientation(self, o): + VerticalLabel.setOrientation(self, o) + self.updateStyle() + + def mousePressEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + self.pressPos = ev.pos() + self.startedDrag = False + ev.accept() + + def mouseMoveEvent(self, ev): + if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): + self.dock.startDrag() + ev.accept() + #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() + + def mouseDoubleClickEvent(self, ev): + 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) + + + diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py new file mode 100644 index 00000000..78d512f3 --- /dev/null +++ b/pyqtgraph/dockarea/DockArea.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from .Container import * +from .DockDrop import * +from .Dock import Dock +import pyqtgraph.debug as debug +import weakref + +## TODO: +# - containers should be drop areas, not docks. (but every slot within a container must have its own drop areas?) +# - drop between tabs +# - nest splitters inside tab boxes, etc. + + + + +class DockArea(Container, QtGui.QWidget, DockDrop): + def __init__(self, temporary=False, home=None): + Container.__init__(self, self) + QtGui.QWidget.__init__(self) + DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom']) + self.layout = QtGui.QVBoxLayout() + self.layout.setContentsMargins(0,0,0,0) + self.layout.setSpacing(0) + self.setLayout(self.layout) + self.docks = weakref.WeakValueDictionary() + self.topContainer = None + self.raiseOverlay() + self.temporary = temporary + self.tempAreas = [] + self.home = home + + def type(self): + return "top" + + def addDock(self, dock, position='bottom', relativeTo=None): + """Adds a dock to this area. + + =========== ================================================================= + Arguments: + dock The new Dock object to add. + position 'bottom', 'top', 'left', 'right', 'over', or 'under' + relativeTo If relativeTo is None, then the new Dock is added to fill an + entire edge of the window. If relativeTo is another Dock, then + the new Dock is placed adjacent to it (or in a tabbed + configuration for 'over' and 'under'). + =========== ================================================================= + + """ + + ## Determine the container to insert this dock into. + ## If there is no neighbor, then the container is the top. + if relativeTo is None or relativeTo is self: + if self.topContainer is None: + container = self + neighbor = None + else: + container = self.topContainer + neighbor = None + else: + if isinstance(relativeTo, basestring): + relativeTo = self.docks[relativeTo] + container = self.getContainer(relativeTo) + neighbor = relativeTo + + ## what container type do we need? + neededContainer = { + 'bottom': 'vertical', + 'top': 'vertical', + 'left': 'horizontal', + 'right': 'horizontal', + 'above': 'tab', + 'below': 'tab' + }[position] + + ## Can't insert new containers into a tab container; insert outside instead. + if neededContainer != container.type() and container.type() == 'tab': + neighbor = container + container = container.container() + + ## Decide if the container we have is suitable. + ## If not, insert a new container inside. + if neededContainer != container.type(): + if neighbor is None: + container = self.addContainer(neededContainer, self.topContainer) + else: + container = self.addContainer(neededContainer, neighbor) + + ## Insert the new dock before/after its neighbor + insertPos = { + 'bottom': 'after', + 'top': 'before', + 'left': 'before', + 'right': 'after', + 'above': 'before', + 'below': 'after' + }[position] + #print "request insert", dock, insertPos, neighbor + container.insert(dock, insertPos, neighbor) + dock.area = self + self.docks[dock.name()] = dock + + def moveDock(self, dock, position, neighbor): + """ + Move an existing Dock to a new location. + """ + old = dock.container() + ## Moving to the edge of a tabbed dock causes a drop outside the tab box + if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': + neighbor = neighbor.container() + self.addDock(dock, position, neighbor) + old.apoptose() + + def getContainer(self, obj): + if obj is None: + return self + return obj.container() + + def makeContainer(self, typ): + if typ == 'vertical': + new = VContainer(self) + elif typ == 'horizontal': + new = HContainer(self) + elif typ == 'tab': + new = TContainer(self) + return new + + def addContainer(self, typ, obj): + """Add a new container around obj""" + new = self.makeContainer(typ) + + container = self.getContainer(obj) + container.insert(new, 'before', obj) + #print "Add container:", new, " -> ", container + if obj is not None: + new.insert(obj) + self.raiseOverlay() + return new + + def insert(self, new, pos=None, neighbor=None): + if self.topContainer is not None: + self.topContainer.containerChanged(None) + self.layout.addWidget(new) + self.topContainer = new + #print self, "set top:", new + new._container = self + self.raiseOverlay() + #print "Insert top:", new + + def count(self): + if self.topContainer is None: + return 0 + return 1 + + + #def paintEvent(self, ev): + #self.drawDockOverlay() + + def resizeEvent(self, ev): + self.resizeOverlay(self.size()) + + def addTempArea(self): + if self.home is None: + area = DockArea(temporary=True, home=self) + self.tempAreas.append(area) + win = QtGui.QMainWindow() + win.setCentralWidget(area) + area.win = win + win.show() + else: + area = self.home.addTempArea() + #print "added temp area", area, area.window() + return area + + def floatDock(self, dock): + """Removes *dock* from this DockArea and places it in a new window.""" + area = self.addTempArea() + area.win.resize(dock.size()) + area.moveDock(dock, 'top', None) + + + def removeTempArea(self, area): + self.tempAreas.remove(area) + #print "close window", area.window() + area.window().close() + + def saveState(self): + """ + Return a serialized (storable) representation of the state of + all Docks in this DockArea.""" + state = {'main': self.childState(self.topContainer), 'float': []} + for a in self.tempAreas: + geo = a.win.geometry() + geo = (geo.x(), geo.y(), geo.width(), geo.height()) + state['float'].append((a.saveState(), geo)) + return state + + def childState(self, obj): + if isinstance(obj, Dock): + return ('dock', obj.name(), {}) + else: + childs = [] + for i in range(obj.count()): + childs.append(self.childState(obj.widget(i))) + return (obj.type(), childs, obj.saveState()) + + + def restoreState(self, state): + """ + Restore Dock configuration as generated by saveState. + + Note that this function does not create any Docks--it will only + restore the arrangement of an existing set of Docks. + + """ + + ## 1) make dict of all docks and list of existing containers + containers, docks = self.findAll() + oldTemps = self.tempAreas[:] + #print "found docks:", docks + + ## 2) create container structure, move docks into new containers + self.buildFromState(state['main'], docks, self) + + ## 3) create floating areas, populate + for s in state['float']: + a = self.addTempArea() + a.buildFromState(s[0]['main'], docks, a) + a.win.setGeometry(*s[1]) + + ## 4) Add any remaining docks to the bottom + for d in docks.values(): + self.moveDock(d, 'below', None) + + #print "\nKill old containers:" + ## 5) kill old containers + for c in containers: + c.close() + for a in oldTemps: + a.apoptose() + + + def buildFromState(self, state, docks, root, depth=0): + typ, contents, state = state + pfx = " " * depth + if typ == 'dock': + try: + obj = docks[contents] + del docks[contents] + except KeyError: + raise Exception('Cannot restore dock state; no dock with name "%s"' % contents) + else: + obj = self.makeContainer(typ) + + root.insert(obj, 'after') + #print pfx+"Add:", obj, " -> ", root + + if typ != 'dock': + for o in contents: + self.buildFromState(o, docks, obj, depth+1) + obj.apoptose(propagate=False) + obj.restoreState(state) ## this has to be done later? + + + def findAll(self, obj=None, c=None, d=None): + if obj is None: + obj = self.topContainer + + ## check all temp areas first + if c is None: + c = [] + d = {} + for a in self.tempAreas: + c1, d1 = a.findAll() + c.extend(c1) + d.update(d1) + + if isinstance(obj, Dock): + d[obj.name()] = obj + elif obj is not None: + c.append(obj) + for i in range(obj.count()): + o2 = obj.widget(i) + c2, d2 = self.findAll(o2) + c.extend(c2) + d.update(d2) + return (c, d) + + def apoptose(self): + #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() + if self.temporary and self.topContainer.count() == 0: + self.topContainer = None + self.home.removeTempArea(self) + #self.close() + + + \ No newline at end of file diff --git a/pyqtgraph/dockarea/DockDrop.py b/pyqtgraph/dockarea/DockDrop.py new file mode 100644 index 00000000..acab28cd --- /dev/null +++ b/pyqtgraph/dockarea/DockDrop.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui + +class DockDrop(object): + """Provides dock-dropping methods""" + def __init__(self, allowedAreas=None): + object.__init__(self) + if allowedAreas is None: + allowedAreas = ['center', 'right', 'left', 'top', 'bottom'] + self.allowedAreas = set(allowedAreas) + self.setAcceptDrops(True) + self.dropArea = None + self.overlay = DropAreaOverlay(self) + self.overlay.raise_() + + def resizeOverlay(self, size): + self.overlay.resize(size) + + def raiseOverlay(self): + self.overlay.raise_() + + def dragEnterEvent(self, ev): + src = ev.source() + if hasattr(src, 'implements') and src.implements('dock'): + #print "drag enter accept" + ev.accept() + else: + #print "drag enter ignore" + ev.ignore() + + def dragMoveEvent(self, ev): + #print "drag move" + ld = ev.pos().x() + rd = self.width() - ld + td = ev.pos().y() + bd = self.height() - td + + mn = min(ld, rd, td, bd) + if mn > 30: + self.dropArea = "center" + elif (ld == mn or td == mn) and mn > self.height()/3.: + self.dropArea = "center" + elif (rd == mn or ld == mn) and mn > self.width()/3.: + self.dropArea = "center" + + elif rd == mn: + self.dropArea = "right" + elif ld == mn: + self.dropArea = "left" + elif td == mn: + self.dropArea = "top" + elif bd == mn: + self.dropArea = "bottom" + + if ev.source() is self and self.dropArea == 'center': + #print " no self-center" + self.dropArea = None + ev.ignore() + elif self.dropArea not in self.allowedAreas: + #print " not allowed" + self.dropArea = None + ev.ignore() + else: + #print " ok" + ev.accept() + self.overlay.setDropArea(self.dropArea) + + def dragLeaveEvent(self, ev): + self.dropArea = None + self.overlay.setDropArea(self.dropArea) + + def dropEvent(self, ev): + area = self.dropArea + if area is None: + return + if area == 'center': + area = 'above' + self.area.moveDock(ev.source(), area, self) + self.dropArea = None + self.overlay.setDropArea(self.dropArea) + + + +class DropAreaOverlay(QtGui.QWidget): + """Overlay widget that draws drop areas during a drag-drop operation""" + + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + self.dropArea = None + self.hide() + self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + + def setDropArea(self, area): + self.dropArea = area + if area is None: + self.hide() + else: + ## Resize overlay to just the region where drop area should be displayed. + ## This works around a Qt bug--can't display transparent widgets over QGLWidget + prgn = self.parent().rect() + rgn = QtCore.QRect(prgn) + w = min(30, prgn.width()/3.) + h = min(30, prgn.height()/3.) + + if self.dropArea == 'left': + rgn.setWidth(w) + elif self.dropArea == 'right': + rgn.setLeft(rgn.left() + prgn.width() - w) + elif self.dropArea == 'top': + rgn.setHeight(h) + elif self.dropArea == 'bottom': + rgn.setTop(rgn.top() + prgn.height() - h) + elif self.dropArea == 'center': + rgn.adjust(w, h, -w, -h) + self.setGeometry(rgn) + self.show() + + self.update() + + def paintEvent(self, ev): + if self.dropArea is None: + return + p = QtGui.QPainter(self) + rgn = self.rect() + + p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 255, 50))) + p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 150), 3)) + p.drawRect(rgn) diff --git a/pyqtgraph/dockarea/__init__.py b/pyqtgraph/dockarea/__init__.py new file mode 100644 index 00000000..f67c50c3 --- /dev/null +++ b/pyqtgraph/dockarea/__init__.py @@ -0,0 +1,2 @@ +from .DockArea import DockArea +from .Dock import Dock \ No newline at end of file diff --git a/pyqtgraph/exceptionHandling.py b/pyqtgraph/exceptionHandling.py new file mode 100644 index 00000000..daa821b7 --- /dev/null +++ b/pyqtgraph/exceptionHandling.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""This module installs a wrapper around sys.excepthook which allows multiple +new exception handlers to be registered. + +Optionally, the wrapper also stops exceptions from causing long-term storage +of local stack frames. This has two major effects: + - Unhandled exceptions will no longer cause memory leaks + (If an exception occurs while a lot of data is present on the stack, + such as when loading large files, the data would ordinarily be kept + until the next exception occurs. We would rather release this memory + as soon as possible.) + - Some debuggers may have a hard time handling uncaught exceptions + +The module also provides a callback mechanism allowing others to respond +to exceptions. +""" + +import sys, time +#from lib.Manager import logMsg +import traceback +#from log import * + +#logging = False + +callbacks = [] +clear_tracebacks = False + +def register(fn): + """ + Register a callable to be invoked when there is an unhandled exception. + The callback will be passed the output of sys.exc_info(): (exception type, exception, traceback) + Multiple callbacks will be invoked in the order they were registered. + """ + callbacks.append(fn) + +def unregister(fn): + """Unregister a previously registered callback.""" + callbacks.remove(fn) + +def setTracebackClearing(clear=True): + """ + Enable or disable traceback clearing. + By default, clearing is disabled and Python will indefinitely store unhandled exception stack traces. + This function is provided since Python's default behavior can cause unexpected retention of + large memory-consuming objects. + """ + global clear_tracebacks + clear_tracebacks = clear + +class ExceptionHandler(object): + def __call__(self, *args): + ## call original exception handler first (prints exception) + global original_excepthook, callbacks, clear_tracebacks + print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())))) + ret = original_excepthook(*args) + + for cb in callbacks: + try: + cb(*args) + except: + print(" --------------------------------------------------------------") + print(" Error occurred during exception callback %s" % str(cb)) + print(" --------------------------------------------------------------") + traceback.print_exception(*sys.exc_info()) + + + ## Clear long-term storage of last traceback to prevent memory-hogging. + ## (If an exception occurs while a lot of data is present on the stack, + ## such as when loading large files, the data would ordinarily be kept + ## until the next exception occurs. We would rather release this memory + ## as soon as possible.) + if clear_tracebacks is True: + sys.last_traceback = None + + def implements(self, interface=None): + ## this just makes it easy for us to detect whether an ExceptionHook is already installed. + if interface is None: + return ['ExceptionHandler'] + else: + return interface == 'ExceptionHandler' + + + +## replace built-in excepthook only if this has not already been done +if not (hasattr(sys.excepthook, 'implements') and sys.excepthook.implements('ExceptionHandler')): + original_excepthook = sys.excepthook + sys.excepthook = ExceptionHandler() + + + diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py new file mode 100644 index 00000000..629b2789 --- /dev/null +++ b/pyqtgraph/exporters/CSVExporter.py @@ -0,0 +1,61 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from .Exporter import Exporter +from pyqtgraph.parametertree import Parameter + + +__all__ = ['CSVExporter'] + + +class CSVExporter(Exporter): + Name = "CSV from plot data" + windows = [] + def __init__(self, item): + Exporter.__init__(self, item) + self.params = Parameter(name='params', type='group', children=[ + {'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']}, + ]) + + def parameters(self): + return self.params + + def export(self, fileName=None): + + if not isinstance(self.item, pg.PlotItem): + raise Exception("Must have a PlotItem selected for CSV export.") + + if fileName is None: + self.fileSaveDialog(filter=["*.csv", "*.tsv"]) + return + + fd = open(fileName, 'w') + data = [] + header = [] + for c in self.item.curves: + data.append(c.getData()) + header.extend(['x', 'y']) + + if self.params['separator'] == 'comma': + sep = ',' + else: + sep = '\t' + + fd.write(sep.join(header) + '\n') + i = 0 + while True: + done = True + for d in data: + if i < len(d[0]): + fd.write('%g%s%g%s'%(d[0][i], sep, d[1][i], sep)) + done = False + else: + fd.write(' %s %s' % (sep, sep)) + fd.write('\n') + if done: + break + i += 1 + fd.close() + + + + diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py new file mode 100644 index 00000000..b1a663bc --- /dev/null +++ b/pyqtgraph/exporters/Exporter.py @@ -0,0 +1,172 @@ +from pyqtgraph.widgets.FileDialog import FileDialog +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import os, re +LastExportDirectory = None + + +class Exporter(object): + """ + Abstract class used for exporting graphics to file / printer / whatever. + """ + + def __init__(self, item): + """ + Initialize with the item to be exported. + Can be an individual graphics item or a scene. + """ + object.__init__(self) + self.item = item + + #def item(self): + #return self.item + + def parameters(self): + """Return the parameters used to configure this exporter.""" + raise Exception("Abstract method must be overridden in subclass.") + + def export(self, fileName=None, toBytes=False): + """ + If *fileName* is None, pop-up a file dialog. + If *toString* is True, return a bytes object rather than writing to file. + """ + raise Exception("Abstract method must be overridden in subclass.") + + def fileSaveDialog(self, filter=None, opts=None): + ## Show a file dialog, call self.export(fileName) when finished. + if opts is None: + opts = {} + self.fileDialog = FileDialog() + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + if filter is not None: + if isinstance(filter, basestring): + self.fileDialog.setNameFilter(filter) + elif isinstance(filter, list): + self.fileDialog.setNameFilters(filter) + global LastExportDirectory + exportDir = LastExportDirectory + if exportDir is not None: + self.fileDialog.setDirectory(exportDir) + self.fileDialog.show() + self.fileDialog.opts = opts + self.fileDialog.fileSelected.connect(self.fileSaveFinished) + return + + def fileSaveFinished(self, fileName): + fileName = str(fileName) + global LastExportDirectory + LastExportDirectory = os.path.split(fileName)[0] + + ## If file name does not match selected extension, append it now + ext = os.path.splitext(fileName)[1].lower().lstrip('.') + selectedExt = re.search(r'\*\.(\w+)\b', str(self.fileDialog.selectedNameFilter())) + if selectedExt is not None: + selectedExt = selectedExt.groups()[0].lower() + if ext != selectedExt: + fileName = fileName + selectedExt + + self.export(fileName=fileName, **self.fileDialog.opts) + + def getScene(self): + if isinstance(self.item, pg.GraphicsScene): + return self.item + else: + return self.item.scene() + + def getSourceRect(self): + if isinstance(self.item, pg.GraphicsScene): + w = self.item.getViewWidget() + return w.viewportTransform().inverted()[0].mapRect(w.rect()) + else: + return self.item.sceneBoundingRect() + + def getTargetRect(self): + if isinstance(self.item, pg.GraphicsScene): + return self.item.getViewWidget().rect() + else: + return self.item.mapRectToDevice(self.item.boundingRect()) + + def setExportMode(self, export, opts=None): + """ + Call setExportMode(export, opts) on all items that will + be painted during the export. This informs the item + that it is about to be painted for export, allowing it to + alter its appearance temporarily + + + *export* - bool; must be True before exporting and False afterward + *opts* - dict; common parameters are 'antialias' and 'background' + """ + if opts is None: + opts = {} + for item in self.getPaintItems(): + if hasattr(item, 'setExportMode'): + item.setExportMode(export, opts) + + def getPaintItems(self, root=None): + """Return a list of all items that should be painted in the correct order.""" + if root is None: + root = self.item + preItems = [] + postItems = [] + if isinstance(root, QtGui.QGraphicsScene): + childs = [i for i in root.items() if i.parentItem() is None] + rootItem = [] + else: + childs = root.childItems() + rootItem = [root] + childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) + while len(childs) > 0: + ch = childs.pop(0) + tree = self.getPaintItems(ch) + if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0): + preItems.extend(tree) + else: + postItems.extend(tree) + + return preItems + rootItem + postItems + + def render(self, painter, targetRect, sourceRect, item=None): + + #if item is None: + #item = self.item + #preItems = [] + #postItems = [] + #if isinstance(item, QtGui.QGraphicsScene): + #childs = [i for i in item.items() if i.parentItem() is None] + #rootItem = [] + #else: + #childs = item.childItems() + #rootItem = [item] + #childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) + #while len(childs) > 0: + #ch = childs.pop(0) + #if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0): + #preItems.extend(tree) + #else: + #postItems.extend(tree) + + #for ch in preItems: + #self.render(painter, sourceRect, targetRect, item=ch) + ### paint root here + #for ch in postItems: + #self.render(painter, sourceRect, targetRect, item=ch) + + + self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect)) + + #def writePs(self, fileName=None, item=None): + #if fileName is None: + #self.fileSaveDialog(self.writeSvg, filter="PostScript (*.ps)") + #return + #if item is None: + #item = self + #printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) + #printer.setOutputFileName(fileName) + #painter = QtGui.QPainter(printer) + #self.render(painter) + #painter.end() + + #def writeToPrinter(self): + #pass diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py new file mode 100644 index 00000000..cb6cf396 --- /dev/null +++ b/pyqtgraph/exporters/ImageExporter.py @@ -0,0 +1,83 @@ +from .Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import pyqtgraph as pg +import numpy as np + +__all__ = ['ImageExporter'] + +class ImageExporter(Exporter): + Name = "Image File (PNG, TIF, JPG, ...)" + def __init__(self, item): + Exporter.__init__(self, item) + tr = self.getTargetRect() + if isinstance(item, QtGui.QGraphicsItem): + scene = item.scene() + else: + scene = item + bg = scene.views()[0].backgroundBrush().color() + self.params = Parameter(name='params', type='group', children=[ + {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)}, + {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)}, + {'name': 'antialias', 'type': 'bool', 'value': True}, + {'name': 'background', 'type': 'color', 'value': bg}, + ]) + self.params.param('width').sigValueChanged.connect(self.widthChanged) + self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = float(sr.height()) / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = float(sr.width()) / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None): + if fileName is None: + filter = ["*."+str(f) 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) + self.fileSaveDialog(filter=filter) + return + + targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) + sourceRect = self.getSourceRect() + + + #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) + #self.png.fill(pyqtgraph.mkColor(self.params['background'])) + bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte) + color = self.params['background'] + bg[:,:,0] = color.blue() + bg[:,:,1] = color.green() + bg[:,:,2] = color.red() + bg[:,:,3] = color.alpha() + self.png = pg.makeQImage(bg, alpha=True) + + ## set resolution of image: + origTargetRect = self.getTargetRect() + resolutionScale = targetRect.width() / origTargetRect.width() + #self.png.setDotsPerMeterX(self.png.dotsPerMeterX() * resolutionScale) + #self.png.setDotsPerMeterY(self.png.dotsPerMeterY() * resolutionScale) + + painter = QtGui.QPainter(self.png) + #dtr = painter.deviceTransform() + try: + self.setExportMode(True, {'antialias': self.params['antialias'], 'background': self.params['background'], 'painter': painter, 'resolutionScale': resolutionScale}) + painter.setRenderHint(QtGui.QPainter.Antialiasing, self.params['antialias']) + self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect)) + finally: + self.setExportMode(False) + painter.end() + self.png.save(fileName) + + \ No newline at end of file diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py new file mode 100644 index 00000000..76f878d2 --- /dev/null +++ b/pyqtgraph/exporters/Matplotlib.py @@ -0,0 +1,74 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from .Exporter import Exporter + + +__all__ = ['MatplotlibExporter'] + + +class MatplotlibExporter(Exporter): + Name = "Matplotlib Window" + windows = [] + def __init__(self, item): + Exporter.__init__(self, item) + + def parameters(self): + return None + + def export(self, fileName=None): + + if isinstance(self.item, pg.PlotItem): + mpw = MatplotlibWindow() + MatplotlibExporter.windows.append(mpw) + fig = mpw.getFigure() + + ax = fig.add_subplot(111) + ax.clear() + #ax.grid(True) + + for item in self.item.curves: + x, y = item.getData() + opts = item.opts + pen = pg.mkPen(opts['pen']) + if pen.style() == QtCore.Qt.NoPen: + linestyle = '' + else: + linestyle = '-' + color = tuple([c/255. for c in pg.colorTuple(pen.color())]) + symbol = opts['symbol'] + if symbol == 't': + symbol = '^' + symbolPen = pg.mkPen(opts['symbolPen']) + symbolBrush = pg.mkBrush(opts['symbolBrush']) + markeredgecolor = tuple([c/255. for c in pg.colorTuple(symbolPen.color())]) + markerfacecolor = tuple([c/255. for c in pg.colorTuple(symbolBrush.color())]) + + if opts['fillLevel'] is not None and opts['fillBrush'] is not None: + fillBrush = pg.mkBrush(opts['fillBrush']) + fillcolor = tuple([c/255. for c in pg.colorTuple(fillBrush.color())]) + ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) + + ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor) + + xr, yr = self.item.viewRange() + ax.set_xbound(*xr) + ax.set_ybound(*yr) + mpw.draw() + else: + raise Exception("Matplotlib export currently only works with plot items") + + + +class MatplotlibWindow(QtGui.QMainWindow): + def __init__(self): + import pyqtgraph.widgets.MatplotlibWidget + QtGui.QMainWindow.__init__(self) + self.mpl = pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget() + self.setCentralWidget(self.mpl) + self.show() + + def __getattr__(self, attr): + return getattr(self.mpl, attr) + + def closeEvent(self, ev): + MatplotlibExporter.windows.remove(self) diff --git a/pyqtgraph/exporters/PrintExporter.py b/pyqtgraph/exporters/PrintExporter.py new file mode 100644 index 00000000..5b31b45d --- /dev/null +++ b/pyqtgraph/exporters/PrintExporter.py @@ -0,0 +1,65 @@ +from .Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import re + +__all__ = ['PrintExporter'] +#__all__ = [] ## Printer is disabled for now--does not work very well. + +class PrintExporter(Exporter): + Name = "Printer" + def __init__(self, item): + Exporter.__init__(self, item) + tr = self.getTargetRect() + self.params = Parameter(name='params', type='group', children=[ + {'name': 'width', 'type': 'float', 'value': 0.1, 'limits': (0, None), 'suffix': 'm', 'siPrefix': True}, + {'name': 'height', 'type': 'float', 'value': (0.1 * tr.height()) / tr.width(), 'limits': (0, None), 'suffix': 'm', 'siPrefix': True}, + ]) + self.params.param('width').sigValueChanged.connect(self.widthChanged) + self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = sr.height() / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = sr.width() / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None): + printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) + dialog = QtGui.QPrintDialog(printer) + dialog.setWindowTitle("Print Document") + if dialog.exec_() != QtGui.QDialog.Accepted: + return; + + #dpi = QtGui.QDesktopWidget().physicalDpiX() + + #self.svg.setSize(QtCore.QSize(100,100)) + #self.svg.setResolution(600) + #res = printer.resolution() + sr = self.getSourceRect() + #res = sr.width() * .4 / (self.params['width'] * 100 / 2.54) + res = QtGui.QDesktopWidget().physicalDpiX() + printer.setResolution(res) + rect = printer.pageRect() + center = rect.center() + h = self.params['height'] * res * 100. / 2.54 + w = self.params['width'] * res * 100. / 2.54 + x = center.x() - w/2. + y = center.y() - h/2. + + targetRect = QtCore.QRect(x, y, w, h) + sourceRect = self.getSourceRect() + painter = QtGui.QPainter(printer) + try: + self.setExportMode(True, {'painter': painter}) + self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect)) + finally: + self.setExportMode(False) + painter.end() diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py new file mode 100644 index 00000000..ce05b82d --- /dev/null +++ b/pyqtgraph/exporters/SVGExporter.py @@ -0,0 +1,414 @@ +from .Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import pyqtgraph as pg +import re +import xml.dom.minidom as xml +import numpy as np + + +__all__ = ['SVGExporter'] + +class SVGExporter(Exporter): + Name = "Scalable Vector Graphics (SVG)" + def __init__(self, item): + Exporter.__init__(self, item) + #tr = self.getTargetRect() + self.params = Parameter(name='params', type='group', children=[ + #{'name': 'width', 'type': 'float', 'value': tr.width(), 'limits': (0, None)}, + #{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)}, + ]) + #self.params.param('width').sigValueChanged.connect(self.widthChanged) + #self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = sr.height() / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = sr.width() / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None, toBytes=False): + if toBytes is False and fileName is None: + self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") + return + #self.svg = QtSvg.QSvgGenerator() + #self.svg.setFileName(fileName) + #dpi = QtGui.QDesktopWidget().physicalDpiX() + ### not really sure why this works, but it seems to be important: + #self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) + #self.svg.setResolution(dpi) + ##self.svg.setViewBox() + #targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) + #sourceRect = self.getSourceRect() + + #painter = QtGui.QPainter(self.svg) + #try: + #self.setExportMode(True) + #self.render(painter, QtCore.QRectF(targetRect), sourceRect) + #finally: + #self.setExportMode(False) + #painter.end() + + ## Workaround to set pen widths correctly + #data = open(fileName).readlines() + #for i in range(len(data)): + #line = data[i] + #m = re.match(r'( + +pyqtgraph SVG export +Generated with Qt and pyqtgraph + + +""" + +def generateSvg(item): + global xmlHeader + try: + node = _generateItemSvg(item) + finally: + ## reset export mode for all items in the tree + if isinstance(item, QtGui.QGraphicsScene): + items = item.items() + else: + items = [item] + for i in items: + items.extend(i.childItems()) + for i in items: + if hasattr(i, 'setExportMode'): + i.setExportMode(False) + + cleanXml(node) + + return xmlHeader + node.toprettyxml(indent=' ') + "\n\n" + + +def _generateItemSvg(item, nodes=None, root=None): + ## This function is intended to work around some issues with Qt's SVG generator + ## and SVG in general. + ## 1) Qt SVG does not implement clipping paths. This is absurd. + ## The solution is to let Qt generate SVG for each item independently, + ## then glue them together manually with clipping. + ## + ## The format Qt generates for all items looks like this: + ## + ## + ## + ## one or more of: or or + ## + ## + ## one or more of: or or + ## + ## . . . + ## + ## + ## 2) There seems to be wide disagreement over whether path strokes + ## should be scaled anisotropically. + ## see: http://web.mit.edu/jonas/www/anisotropy/ + ## Given that both inkscape and illustrator seem to prefer isotropic + ## scaling, we will optimize for those cases. + ## + ## 3) Qt generates paths using non-scaling-stroke from SVG 1.2, but + ## inkscape only supports 1.1. + ## + ## Both 2 and 3 can be addressed by drawing all items in world coordinates. + + + + if nodes is None: ## nodes maps all node IDs to their XML element. + ## this allows us to ensure all elements receive unique names. + nodes = {} + + if root is None: + root = item + + ## Skip hidden items + if hasattr(item, 'isVisible') and not item.isVisible(): + return None + + ## If this item defines its own SVG generator, use that. + if hasattr(item, 'generateSvg'): + return item.generateSvg(nodes) + + + ## Generate SVG text for just this item (exclude its children; we'll handle them later) + tr = QtGui.QTransform() + if isinstance(item, QtGui.QGraphicsScene): + xmlStr = "\n\n" + childs = [i for i in item.items() if i.parentItem() is None] + doc = xml.parseString(xmlStr) + else: + childs = item.childItems() + tr = itemTransform(item, root) + + #print item, pg.SRTTransform(tr) + + #tr.translate(item.pos().x(), item.pos().y()) + #tr = tr * item.transform() + arr = QtCore.QByteArray() + buf = QtCore.QBuffer(arr) + svg = QtSvg.QSvgGenerator() + svg.setOutputDevice(buf) + dpi = QtGui.QDesktopWidget().physicalDpiX() + ### not really sure why this works, but it seems to be important: + #self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) + svg.setResolution(dpi) + + p = QtGui.QPainter() + p.begin(svg) + if hasattr(item, 'setExportMode'): + item.setExportMode(True, {'painter': p}) + try: + p.setTransform(tr) + item.paint(p, QtGui.QStyleOptionGraphicsItem(), None) + finally: + p.end() + ## Can't do this here--we need to wait until all children have painted as well. + ## this is taken care of in generateSvg instead. + #if hasattr(item, 'setExportMode'): + #item.setExportMode(False) + + xmlStr = str(arr) + doc = xml.parseString(xmlStr) + + try: + ## Get top-level group for this item + g1 = doc.getElementsByTagName('g')[0] + ## get list of sub-groups + g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g'] + except: + print doc.toxml() + raise + + + ## Get rid of group transformation matrices by applying + ## transformation to inner coordinates + correctCoordinates(g1, item) + + ## make sure g1 has the transformation matrix + #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) + #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) + + #print "=================",item,"=====================" + #print g1.toprettyxml(indent=" ", newl='') + + ## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1) + ## So we need to correct anything attempting to use this. + #correctStroke(g1, item, root) + + ## decide on a name for this item + baseName = item.__class__.__name__ + i = 1 + while True: + name = baseName + "_%d" % i + if name not in nodes: + break + i += 1 + nodes[name] = g1 + g1.setAttribute('id', name) + + ## If this item clips its children, we need to take car of that. + childGroup = g1 ## add children directly to this node unless we are clipping + if not isinstance(item, QtGui.QGraphicsScene): + ## See if this item clips its children + if int(item.flags() & item.ItemClipsChildrenToShape) > 0: + ## Generate svg for just the path + if isinstance(root, QtGui.QGraphicsScene): + path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) + else: + path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape()))) + pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] + ## and for the clipPath element + clip = name + '_clip' + clipNode = g1.ownerDocument.createElement('clipPath') + clipNode.setAttribute('id', clip) + clipNode.appendChild(pathNode) + g1.appendChild(clipNode) + + childGroup = g1.ownerDocument.createElement('g') + childGroup.setAttribute('clip-path', 'url(#%s)' % clip) + g1.appendChild(childGroup) + ## Add all child items as sub-elements. + childs.sort(key=lambda c: c.zValue()) + for ch in childs: + cg = _generateItemSvg(ch, nodes, root) + if cg is None: + continue + childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) + + return g1 + +def correctCoordinates(node, item): + ## Remove transformation matrices from tags by applying matrix to coordinates inside. + groups = node.getElementsByTagName('g') + for grp in groups: + matrix = grp.getAttribute('transform') + match = re.match(r'matrix\((.*)\)', matrix) + if match is None: + vals = [1,0,0,1,0,0] + else: + vals = map(float, match.groups()[0].split(',')) + tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]]) + + removeTransform = False + for ch in grp.childNodes: + if not isinstance(ch, xml.Element): + continue + if ch.tagName == 'polyline': + removeTransform = True + coords = np.array([map(float, c.split(',')) for c in ch.getAttribute('points').strip().split(' ')]) + coords = pg.transformCoordinates(tr, coords, transpose=True) + ch.setAttribute('points', ' '.join([','.join(map(str, c)) for c in coords])) + elif ch.tagName == 'path': + removeTransform = True + newCoords = '' + for c in ch.getAttribute('d').strip().split(' '): + x,y = c.split(',') + if x[0].isalpha(): + t = x[0] + x = x[1:] + else: + t = '' + nc = pg.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) + newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' + ch.setAttribute('d', newCoords) + elif ch.tagName == 'text': + removeTransform = False + ## leave text alone for now. Might need this later to correctly render text with outline. + #c = np.array([ + #[float(ch.getAttribute('x')), float(ch.getAttribute('y'))], + #[float(ch.getAttribute('font-size')), 0], + #[0,0]]) + #c = pg.transformCoordinates(tr, c, transpose=True) + #ch.setAttribute('x', str(c[0,0])) + #ch.setAttribute('y', str(c[0,1])) + #fs = c[1]-c[2] + #fs = (fs**2).sum()**0.5 + #ch.setAttribute('font-size', str(fs)) + else: + print('warning: export not implemented for SVG tag %s (from item %s)' % (ch.tagName, item)) + + ## correct line widths if needed + if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke': + w = float(grp.getAttribute('stroke-width')) + s = pg.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) + w = ((s[0]-s[1])**2).sum()**0.5 + ch.setAttribute('stroke-width', str(w)) + + if removeTransform: + grp.removeAttribute('transform') + + +def itemTransform(item, root): + ## Return the transformation mapping item to root + ## (actually to parent coordinate system of root) + + if item is root: + tr = QtGui.QTransform() + tr.translate(*item.pos()) + tr = tr * item.transform() + return tr + + + if int(item.flags() & item.ItemIgnoresTransformations) > 0: + pos = item.pos() + parent = item.parentItem() + if parent is not None: + pos = itemTransform(parent, root).map(pos) + tr = QtGui.QTransform() + tr.translate(pos.x(), pos.y()) + tr = item.transform() * tr + else: + ## find next parent that is either the root item or + ## an item that ignores its transformation + nextRoot = item + while True: + nextRoot = nextRoot.parentItem() + if nextRoot is None: + nextRoot = root + break + if nextRoot is root or int(nextRoot.flags() & nextRoot.ItemIgnoresTransformations) > 0: + break + + if isinstance(nextRoot, QtGui.QGraphicsScene): + tr = item.sceneTransform() + else: + tr = itemTransform(nextRoot, root) * item.itemTransform(nextRoot)[0] + #pos = QtGui.QTransform() + #pos.translate(root.pos().x(), root.pos().y()) + #tr = pos * root.transform() * item.itemTransform(root)[0] + + + return tr + + +#def correctStroke(node, item, root, width=1): + ##print "==============", item, node + #if node.hasAttribute('stroke-width'): + #width = float(node.getAttribute('stroke-width')) + #if node.getAttribute('vector-effect') == 'non-scaling-stroke': + #node.removeAttribute('vector-effect') + #if isinstance(root, QtGui.QGraphicsScene): + #w = item.mapFromScene(pg.Point(width,0)) + #o = item.mapFromScene(pg.Point(0,0)) + #else: + #w = item.mapFromItem(root, pg.Point(width,0)) + #o = item.mapFromItem(root, pg.Point(0,0)) + #w = w-o + ##print " ", w, o, w-o + #w = (w.x()**2 + w.y()**2) ** 0.5 + ##print " ", w + #node.setAttribute('stroke-width', str(w)) + + #for ch in node.childNodes: + #if isinstance(ch, xml.Element): + #correctStroke(ch, item, root, width) + +def cleanXml(node): + ## remove extraneous text; let the xml library do the formatting. + hasElement = False + nonElement = [] + for ch in node.childNodes: + if isinstance(ch, xml.Element): + hasElement = True + cleanXml(ch) + else: + nonElement.append(ch) + + if hasElement: + for ch in nonElement: + node.removeChild(ch) + elif node.tagName == 'g': ## remove childless groups + node.parentNode.removeChild(node) diff --git a/pyqtgraph/exporters/__init__.py b/pyqtgraph/exporters/__init__.py new file mode 100644 index 00000000..3f3c1f1d --- /dev/null +++ b/pyqtgraph/exporters/__init__.py @@ -0,0 +1,27 @@ +Exporters = [] +from pyqtgraph import importModules +#from .. import frozenSupport +import os +d = os.path.split(__file__)[0] +#files = [] +#for f in frozenSupport.listdir(d): + #if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__': + #files.append(f) + #elif f[-3:] == '.py' and f not in ['__init__.py', 'Exporter.py']: + #files.append(f[:-3]) + +#for modName in files: + #mod = __import__(modName, globals(), locals(), fromlist=['*']) +for mod in importModules('', globals(), locals(), excludes=['Exporter']).values(): + if hasattr(mod, '__all__'): + names = mod.__all__ + else: + names = [n for n in dir(mod) if n[0] != '_'] + for k in names: + if hasattr(mod, k): + Exporters.append(getattr(mod, k)) + + +def listExporters(): + return Exporters[:] + diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py new file mode 100644 index 00000000..6b1352d5 --- /dev/null +++ b/pyqtgraph/flowchart/Flowchart.py @@ -0,0 +1,951 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +from .Node import * +from pyqtgraph.pgcollections import OrderedDict +from pyqtgraph.widgets.TreeWidget import * + +## pyside and pyqt use incompatible ui files. +if USE_PYSIDE: + from . import FlowchartTemplate_pyside as FlowchartTemplate + from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate +else: + from . import FlowchartTemplate_pyqt as FlowchartTemplate + from . import FlowchartCtrlTemplate_pyqt as FlowchartCtrlTemplate + +from .Terminal import Terminal +from numpy import ndarray +from . import library +from pyqtgraph.debug import printExc +import pyqtgraph.configfile as configfile +import pyqtgraph.dockarea as dockarea +import pyqtgraph as pg +from . import FlowchartGraphicsView + +def strDict(d): + return dict([(str(k), v) for k, v in d.items()]) + + +def toposort(deps, nodes=None, seen=None, stack=None, depth=0): + """Topological sort. Arguments are: + deps dictionary describing dependencies where a:[b,c] means "a depends on b and c" + nodes optional, specifies list of starting nodes (these should be the nodes + which are not depended on by any other nodes) + """ + + if nodes is None: + ## run through deps to find nodes that are not depended upon + rem = set() + for dep in deps.values(): + rem |= set(dep) + nodes = set(deps.keys()) - rem + if seen is None: + seen = set() + stack = [] + sorted = [] + #print " "*depth, "Starting from", nodes + for n in nodes: + if n in stack: + raise Exception("Cyclic dependency detected", stack + [n]) + if n in seen: + continue + seen.add(n) + #print " "*depth, " descending into", n, deps[n] + sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) + #print " "*depth, " Added", n + sorted.append(n) + #print " "*depth, " ", sorted + return sorted + + +class Flowchart(Node): + + sigFileLoaded = QtCore.Signal(object) + sigFileSaved = QtCore.Signal(object) + + + #sigOutputChanged = QtCore.Signal() ## inherited from Node + sigChartLoaded = QtCore.Signal() + sigStateChanged = QtCore.Signal() + + def __init__(self, terminals=None, name=None, filePath=None): + if name is None: + name = "Flowchart" + if terminals is None: + terminals = {} + self.filePath = filePath + Node.__init__(self, name, allowAddInput=True, allowAddOutput=True) ## create node without terminals; we'll add these later + + + self.inputWasSet = False ## flag allows detection of changes in the absence of input change. + self._nodes = {} + self.nextZVal = 10 + #self.connects = [] + #self._chartGraphicsItem = FlowchartGraphicsItem(self) + self._widget = None + self._scene = None + self.processing = False ## flag that prevents recursive node updates + + self.widget() + + self.inputNode = Node('Input', allowRemove=False, allowAddOutput=True) + self.outputNode = Node('Output', allowRemove=False, allowAddInput=True) + self.addNode(self.inputNode, 'Input', [-150, 0]) + self.addNode(self.outputNode, 'Output', [300, 0]) + + self.outputNode.sigOutputChanged.connect(self.outputChanged) + self.outputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) + self.inputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) + self.outputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved) + self.inputNode.sigTerminalRemoved.connect(self.internalTerminalRemoved) + self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded) + self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded) + + self.viewBox.autoRange(padding = 0.04) + + for name, opts in terminals.items(): + self.addTerminal(name, **opts) + + def setInput(self, **args): + #print "setInput", args + #Node.setInput(self, **args) + #print " ....." + self.inputWasSet = True + self.inputNode.setOutput(**args) + + def outputChanged(self): + self.widget().outputChanged(self.outputNode.inputValues()) + self.sigOutputChanged.emit(self) + + def output(self): + return self.outputNode.inputValues() + + def nodes(self): + return self._nodes + + def addTerminal(self, name, **opts): + term = Node.addTerminal(self, name, **opts) + name = term.name() + if opts['io'] == 'in': ## inputs to the flowchart become outputs on the input node + opts['io'] = 'out' + opts['multi'] = False + self.inputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded) + try: + term2 = self.inputNode.addTerminal(name, **opts) + finally: + self.inputNode.sigTerminalAdded.connect(self.internalTerminalAdded) + + else: + opts['io'] = 'in' + #opts['multi'] = False + self.outputNode.sigTerminalAdded.disconnect(self.internalTerminalAdded) + try: + term2 = self.outputNode.addTerminal(name, **opts) + finally: + self.outputNode.sigTerminalAdded.connect(self.internalTerminalAdded) + return term + + def removeTerminal(self, name): + #print "remove:", name + term = self[name] + inTerm = self.internalTerminal(term) + Node.removeTerminal(self, name) + inTerm.node().removeTerminal(inTerm.name()) + + def internalTerminalRenamed(self, term, oldName): + self[oldName].rename(term.name()) + + def internalTerminalAdded(self, node, term): + if term._io == 'in': + io = 'out' + else: + io = 'in' + Node.addTerminal(self, term.name(), io=io, renamable=term.isRenamable(), removable=term.isRemovable(), multiable=term.isMultiable()) + + def internalTerminalRemoved(self, node, term): + try: + Node.removeTerminal(self, term.name()) + except KeyError: + pass + + def terminalRenamed(self, term, oldName): + newName = term.name() + #print "flowchart rename", newName, oldName + #print self.terminals + Node.terminalRenamed(self, self[oldName], oldName) + #print self.terminals + for n in [self.inputNode, self.outputNode]: + if oldName in n.terminals: + n[oldName].rename(newName) + + def createNode(self, nodeType, name=None, pos=None): + if name is None: + n = 0 + while True: + name = "%s.%d" % (nodeType, n) + if name not in self._nodes: + break + n += 1 + + node = library.getNodeType(nodeType)(name) + self.addNode(node, name, pos) + return node + + def addNode(self, node, name, pos=None): + if pos is None: + pos = [0, 0] + if type(pos) in [QtCore.QPoint, QtCore.QPointF]: + pos = [pos.x(), pos.y()] + item = node.graphicsItem() + item.setZValue(self.nextZVal*2) + self.nextZVal += 1 + #item.setParentItem(self.chartGraphicsItem()) + self.viewBox.addItem(item) + #item.setPos(pos2.x(), pos2.y()) + item.moveBy(*pos) + self._nodes[name] = node + self.widget().addNode(node) + #QtCore.QObject.connect(node, QtCore.SIGNAL('closed'), self.nodeClosed) + node.sigClosed.connect(self.nodeClosed) + #QtCore.QObject.connect(node, QtCore.SIGNAL('renamed'), self.nodeRenamed) + node.sigRenamed.connect(self.nodeRenamed) + #QtCore.QObject.connect(node, QtCore.SIGNAL('outputChanged'), self.nodeOutputChanged) + node.sigOutputChanged.connect(self.nodeOutputChanged) + + def removeNode(self, node): + node.close() + + def nodeClosed(self, node): + del self._nodes[node.name()] + self.widget().removeNode(node) + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('closed'), self.nodeClosed) + try: + node.sigClosed.disconnect(self.nodeClosed) + except TypeError: + pass + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('renamed'), self.nodeRenamed) + try: + node.sigRenamed.disconnect(self.nodeRenamed) + except TypeError: + pass + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('outputChanged'), self.nodeOutputChanged) + try: + node.sigOutputChanged.disconnect(self.nodeOutputChanged) + except TypeError: + pass + + def nodeRenamed(self, node, oldName): + del self._nodes[oldName] + self._nodes[node.name()] = node + self.widget().nodeRenamed(node, oldName) + + def arrangeNodes(self): + pass + + def internalTerminal(self, term): + """If the terminal belongs to the external Node, return the corresponding internal terminal""" + if term.node() is self: + if term.isInput(): + return self.inputNode[term.name()] + else: + return self.outputNode[term.name()] + else: + return term + + def connectTerminals(self, term1, term2): + """Connect two terminals together within this flowchart.""" + term1 = self.internalTerminal(term1) + term2 = self.internalTerminal(term2) + term1.connectTo(term2) + + + def process(self, **args): + """ + Process data through the flowchart, returning the output. + Keyword arguments must be the names of input terminals + + """ + data = {} ## Stores terminal:value pairs + + ## determine order of operations + ## order should look like [('p', node1), ('p', node2), ('d', terminal1), ...] + ## Each tuple specifies either (p)rocess this node or (d)elete the result from this terminal + order = self.processOrder() + #print "ORDER:", order + + ## Record inputs given to process() + for n, t in self.inputNode.outputs().items(): + if n not in args: + raise Exception("Parameter %s required to process this chart." % n) + data[t] = args[n] + + ret = {} + + ## process all in order + for c, arg in order: + + if c == 'p': ## Process a single node + #print "===> process:", arg + node = arg + if node is self.inputNode: + continue ## input node has already been processed. + + + ## get input and output terminals for this node + outs = list(node.outputs().values()) + ins = list(node.inputs().values()) + + ## construct input value dictionary + args = {} + for inp in ins: + inputs = inp.inputTerminals() + if len(inputs) == 0: + continue + if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs + args[inp.name()] = dict([(i, data[i]) for i in inputs]) + else: ## single-inputs terminals only need the single input value available + args[inp.name()] = data[inputs[0]] + + if node is self.outputNode: + ret = args ## we now have the return value, but must keep processing in case there are other endpoint nodes in the chart + else: + try: + if node.isBypassed(): + result = node.processBypassed(args) + else: + result = node.process(display=False, **args) + except: + print("Error processing node %s. Args are: %s" % (str(node), str(args))) + raise + for out in outs: + #print " Output:", out, out.name() + #print out.name() + try: + data[out] = result[out.name()] + except: + print(out, out.name()) + raise + elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory) + #print "===> delete", arg + if arg in data: + del data[arg] + + return ret + + def processOrder(self): + """Return the order of operations required to process this chart. + The order returned should look like [('p', node1), ('p', node2), ('d', terminal1), ...] + where each tuple specifies either (p)rocess this node or (d)elete the result from this terminal + """ + + ## first collect list of nodes/terminals and their dependencies + deps = {} + tdeps = {} ## {terminal: [nodes that depend on terminal]} + for name, node in self._nodes.items(): + deps[node] = node.dependentNodes() + for t in node.outputs().values(): + tdeps[t] = t.dependentNodes() + + #print "DEPS:", deps + ## determine correct node-processing order + #deps[self] = [] + order = toposort(deps) + #print "ORDER1:", order + + ## construct list of operations + ops = [('p', n) for n in order] + + ## determine when it is safe to delete terminal values + dels = [] + for t, nodes in tdeps.items(): + lastInd = 0 + lastNode = None + for n in nodes: ## determine which node is the last to be processed according to order + if n is self: + lastInd = None + break + else: + try: + ind = order.index(n) + except ValueError: + continue + if lastNode is None or ind > lastInd: + lastNode = n + lastInd = ind + #tdeps[t] = lastNode + if lastInd is not None: + dels.append((lastInd+1, t)) + dels.sort(lambda a,b: cmp(b[0], a[0])) + for i, t in dels: + ops.insert(i, ('d', t)) + + return ops + + + def nodeOutputChanged(self, startNode): + """Triggered when a node's output values have changed. (NOT called during process()) + Propagates new data forward through network.""" + ## first collect list of nodes/terminals and their dependencies + + if self.processing: + return + self.processing = True + try: + deps = {} + for name, node in self._nodes.items(): + deps[node] = [] + for t in node.outputs().values(): + deps[node].extend(t.dependentNodes()) + + ## determine order of updates + order = toposort(deps, nodes=[startNode]) + order.reverse() + + ## keep track of terminals that have been updated + terms = set(startNode.outputs().values()) + + #print "======= Updating", startNode + #print "Order:", order + for node in order[1:]: + #print "Processing node", node + for term in list(node.inputs().values()): + #print " checking terminal", term + deps = list(term.connections().keys()) + update = False + for d in deps: + if d in terms: + #print " ..input", d, "changed" + update = True + term.inputChanged(d, process=False) + if update: + #print " processing.." + node.update() + terms |= set(node.outputs().values()) + + finally: + self.processing = False + if self.inputWasSet: + self.inputWasSet = False + else: + self.sigStateChanged.emit() + + + + def chartGraphicsItem(self): + """Return the graphicsItem which displays the internals of this flowchart. + (graphicsItem() still returns the external-view item)""" + #return self._chartGraphicsItem + return self.viewBox + + def widget(self): + if self._widget is None: + self._widget = FlowchartCtrlWidget(self) + self.scene = self._widget.scene() + self.viewBox = self._widget.viewBox() + #self._scene = QtGui.QGraphicsScene() + #self._widget.setScene(self._scene) + #self.scene.addItem(self.chartGraphicsItem()) + + #ci = self.chartGraphicsItem() + #self.viewBox.addItem(ci) + #self.viewBox.autoRange() + return self._widget + + def listConnections(self): + conn = set() + for n in self._nodes.values(): + terms = n.outputs() + for n, t in terms.items(): + for c in t.connections(): + conn.add((t, c)) + return conn + + def saveState(self): + state = Node.saveState(self) + state['nodes'] = [] + state['connects'] = [] + #state['terminals'] = self.saveTerminals() + + for name, node in self._nodes.items(): + cls = type(node) + if hasattr(cls, 'nodeName'): + clsName = cls.nodeName + pos = node.graphicsItem().pos() + ns = {'class': clsName, 'name': name, 'pos': (pos.x(), pos.y()), 'state': node.saveState()} + state['nodes'].append(ns) + + conn = self.listConnections() + for a, b in conn: + state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name())) + + state['inputNode'] = self.inputNode.saveState() + state['outputNode'] = self.outputNode.saveState() + + return state + + def restoreState(self, state, clear=False): + self.blockSignals(True) + try: + if clear: + self.clear() + Node.restoreState(self, state) + nodes = state['nodes'] + nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) + for n in nodes: + if n['name'] in self._nodes: + #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) + self._nodes[n['name']].restoreState(n['state']) + continue + try: + node = self.createNode(n['class'], name=n['name']) + node.restoreState(n['state']) + except: + printExc("Error creating node %s: (continuing anyway)" % n['name']) + #node.graphicsItem().moveBy(*n['pos']) + + self.inputNode.restoreState(state.get('inputNode', {})) + self.outputNode.restoreState(state.get('outputNode', {})) + + #self.restoreTerminals(state['terminals']) + for n1, t1, n2, t2 in state['connects']: + try: + self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) + except: + print(self._nodes[n1].terminals) + print(self._nodes[n2].terminals) + printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) + + + finally: + self.blockSignals(False) + + self.sigChartLoaded.emit() + self.outputChanged() + self.sigStateChanged.emit() + #self.sigOutputChanged.emit() + + def loadFile(self, fileName=None, startDir=None): + if fileName is None: + if startDir is None: + startDir = self.filePath + if startDir is None: + startDir = '.' + self.fileDialog = pg.FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.loadFile) + return + ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. + #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + fileName = str(fileName) + state = configfile.readConfigFile(fileName) + self.restoreState(state, clear=True) + self.viewBox.autoRange() + #self.emit(QtCore.SIGNAL('fileLoaded'), fileName) + self.sigFileLoaded.emit(fileName) + + def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): + if fileName is None: + if startDir is None: + startDir = self.filePath + if startDir is None: + startDir = '.' + self.fileDialog = pg.FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + #self.fileDialog.setDirectory(startDir) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.saveFile) + return + #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + configfile.writeConfigFile(self.saveState(), fileName) + self.sigFileSaved.emit(fileName) + + def clear(self): + for n in list(self._nodes.values()): + if n is self.inputNode or n is self.outputNode: + continue + n.close() ## calls self.nodeClosed(n) by signal + #self.clearTerminals() + self.widget().clear() + + def clearTerminals(self): + Node.clearTerminals(self) + self.inputNode.clearTerminals() + self.outputNode.clearTerminals() + +#class FlowchartGraphicsItem(QtGui.QGraphicsItem): +class FlowchartGraphicsItem(GraphicsObject): + + def __init__(self, chart): + #print "FlowchartGraphicsItem.__init__" + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + self.chart = chart ## chart is an instance of Flowchart() + self.updateTerminals() + + def updateTerminals(self): + #print "FlowchartGraphicsItem.updateTerminals" + self.terminals = {} + bounds = self.boundingRect() + inp = self.chart.inputs() + dy = bounds.height() / (len(inp)+1) + y = dy + for n, t in inp.items(): + item = t.graphicsItem() + self.terminals[n] = item + item.setParentItem(self) + item.setAnchor(bounds.width(), y) + y += dy + out = self.chart.outputs() + dy = bounds.height() / (len(out)+1) + y = dy + for n, t in out.items(): + item = t.graphicsItem() + self.terminals[n] = item + item.setParentItem(self) + item.setAnchor(0, y) + y += dy + + def boundingRect(self): + #print "FlowchartGraphicsItem.boundingRect" + return QtCore.QRectF() + + def paint(self, p, *args): + #print "FlowchartGraphicsItem.paint" + pass + #p.drawRect(self.boundingRect()) + + +class FlowchartCtrlWidget(QtGui.QWidget): + """The widget that contains the list of all the nodes in a flowchart and their controls, as well as buttons for loading/saving flowcharts.""" + + def __init__(self, chart): + self.items = {} + #self.loadDir = loadDir ## where to look initially for chart files + self.currentFileName = None + QtGui.QWidget.__init__(self) + self.chart = chart + self.ui = FlowchartCtrlTemplate.Ui_Form() + self.ui.setupUi(self) + self.ui.ctrlList.setColumnCount(2) + #self.ui.ctrlList.setColumnWidth(0, 200) + self.ui.ctrlList.setColumnWidth(1, 20) + self.ui.ctrlList.setVerticalScrollMode(self.ui.ctrlList.ScrollPerPixel) + self.ui.ctrlList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + self.chartWidget = FlowchartWidget(chart, self) + #self.chartWidget.viewBox().autoRange() + self.cwWin = QtGui.QMainWindow() + self.cwWin.setWindowTitle('Flowchart') + self.cwWin.setCentralWidget(self.chartWidget) + self.cwWin.resize(1000,800) + + h = self.ui.ctrlList.header() + h.setResizeMode(0, h.Stretch) + + self.ui.ctrlList.itemChanged.connect(self.itemChanged) + self.ui.loadBtn.clicked.connect(self.loadClicked) + self.ui.saveBtn.clicked.connect(self.saveClicked) + self.ui.saveAsBtn.clicked.connect(self.saveAsClicked) + self.ui.showChartBtn.toggled.connect(self.chartToggled) + self.chart.sigFileLoaded.connect(self.setCurrentFile) + self.ui.reloadBtn.clicked.connect(self.reloadClicked) + self.chart.sigFileSaved.connect(self.fileSaved) + + + + #def resizeEvent(self, ev): + #QtGui.QWidget.resizeEvent(self, ev) + #self.ui.ctrlList.setColumnWidth(0, self.ui.ctrlList.viewport().width()-20) + + def chartToggled(self, b): + if b: + self.cwWin.show() + else: + self.cwWin.hide() + + def reloadClicked(self): + try: + self.chartWidget.reloadLibrary() + self.ui.reloadBtn.success("Reloaded.") + except: + self.ui.reloadBtn.success("Error.") + raise + + + def loadClicked(self): + newFile = self.chart.loadFile() + #self.setCurrentFile(newFile) + + def fileSaved(self, fileName): + self.setCurrentFile(fileName) + self.ui.saveBtn.success("Saved.") + + def saveClicked(self): + if self.currentFileName is None: + self.saveAsClicked() + else: + try: + self.chart.saveFile(self.currentFileName) + #self.ui.saveBtn.success("Saved.") + except: + self.ui.saveBtn.failure("Error") + raise + + def saveAsClicked(self): + try: + if self.currentFileName is None: + newFile = self.chart.saveFile() + else: + newFile = self.chart.saveFile(suggestedFileName=self.currentFileName) + #self.ui.saveAsBtn.success("Saved.") + #print "Back to saveAsClicked." + except: + self.ui.saveBtn.failure("Error") + raise + + #self.setCurrentFile(newFile) + + def setCurrentFile(self, fileName): + self.currentFileName = fileName + if fileName is None: + self.ui.fileNameLabel.setText("[ new ]") + else: + self.ui.fileNameLabel.setText("%s" % os.path.split(self.currentFileName)[1]) + self.resizeEvent(None) + + def itemChanged(self, *args): + pass + + def scene(self): + return self.chartWidget.scene() ## returns the GraphicsScene object + + def viewBox(self): + return self.chartWidget.viewBox() + + def nodeRenamed(self, node, oldName): + self.items[node].setText(0, node.name()) + + def addNode(self, node): + ctrl = node.ctrlWidget() + #if ctrl is None: + #return + item = QtGui.QTreeWidgetItem([node.name(), '', '']) + self.ui.ctrlList.addTopLevelItem(item) + byp = QtGui.QPushButton('X') + byp.setCheckable(True) + byp.setFixedWidth(20) + item.bypassBtn = byp + self.ui.ctrlList.setItemWidget(item, 1, byp) + byp.node = node + node.bypassButton = byp + byp.setChecked(node.isBypassed()) + byp.clicked.connect(self.bypassClicked) + + if ctrl is not None: + item2 = QtGui.QTreeWidgetItem() + item.addChild(item2) + self.ui.ctrlList.setItemWidget(item2, 0, ctrl) + + self.items[node] = item + + def removeNode(self, node): + if node in self.items: + item = self.items[node] + #self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked) + try: + item.bypassBtn.clicked.disconnect(self.bypassClicked) + except TypeError: + pass + self.ui.ctrlList.removeTopLevelItem(item) + + def bypassClicked(self): + btn = QtCore.QObject.sender(self) + btn.node.bypass(btn.isChecked()) + + def chartWidget(self): + return self.chartWidget + + def outputChanged(self, data): + pass + #self.ui.outputTree.setData(data, hideRoot=True) + + def clear(self): + self.chartWidget.clear() + + def select(self, node): + item = self.items[node] + self.ui.ctrlList.setCurrentItem(item) + +class FlowchartWidget(dockarea.DockArea): + """Includes the actual graphical flowchart and debugging interface""" + def __init__(self, chart, ctrl): + #QtGui.QWidget.__init__(self) + dockarea.DockArea.__init__(self) + self.chart = chart + self.ctrl = ctrl + self.hoverItem = None + #self.setMinimumWidth(250) + #self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)) + + #self.ui = FlowchartTemplate.Ui_Form() + #self.ui.setupUi(self) + + ## build user interface (it was easier to do it here than via developer) + self.view = FlowchartGraphicsView.FlowchartGraphicsView(self) + self.viewDock = dockarea.Dock('view', size=(1000,600)) + self.viewDock.addWidget(self.view) + self.viewDock.hideTitleBar() + self.addDock(self.viewDock) + + + self.hoverText = QtGui.QTextEdit() + self.hoverText.setReadOnly(True) + self.hoverDock = dockarea.Dock('Hover Info', size=(1000,20)) + self.hoverDock.addWidget(self.hoverText) + self.addDock(self.hoverDock, 'bottom') + + self.selInfo = QtGui.QWidget() + self.selInfoLayout = QtGui.QGridLayout() + self.selInfo.setLayout(self.selInfoLayout) + self.selDescLabel = QtGui.QLabel() + self.selNameLabel = QtGui.QLabel() + self.selDescLabel.setWordWrap(True) + self.selectedTree = pg.DataTreeWidget() + #self.selectedTree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + #self.selInfoLayout.addWidget(self.selNameLabel) + self.selInfoLayout.addWidget(self.selDescLabel) + self.selInfoLayout.addWidget(self.selectedTree) + self.selDock = dockarea.Dock('Selected Node', size=(1000,200)) + self.selDock.addWidget(self.selInfo) + self.addDock(self.selDock, 'bottom') + + self._scene = self.view.scene() + self._viewBox = self.view.viewBox() + #self._scene = QtGui.QGraphicsScene() + #self._scene = FlowchartGraphicsView.FlowchartGraphicsScene() + #self.view.setScene(self._scene) + + self.buildMenu() + #self.ui.addNodeBtn.mouseReleaseEvent = self.addNodeBtnReleased + + self._scene.selectionChanged.connect(self.selectionChanged) + self._scene.sigMouseHover.connect(self.hoverOver) + #self.view.sigClicked.connect(self.showViewMenu) + #self._scene.sigSceneContextMenu.connect(self.showViewMenu) + #self._viewBox.sigActionPositionChanged.connect(self.menuPosChanged) + + + def reloadLibrary(self): + #QtCore.QObject.disconnect(self.nodeMenu, QtCore.SIGNAL('triggered(QAction*)'), self.nodeMenuTriggered) + self.nodeMenu.triggered.disconnect(self.nodeMenuTriggered) + self.nodeMenu = None + self.subMenus = [] + library.loadLibrary(reloadLibs=True) + self.buildMenu() + + def buildMenu(self, pos=None): + self.nodeMenu = QtGui.QMenu() + self.subMenus = [] + for section, nodes in library.getNodeTree().items(): + menu = QtGui.QMenu(section) + self.nodeMenu.addMenu(menu) + for name in nodes: + act = menu.addAction(name) + act.nodeType = name + act.pos = pos + self.subMenus.append(menu) + self.nodeMenu.triggered.connect(self.nodeMenuTriggered) + return self.nodeMenu + + def menuPosChanged(self, pos): + self.menuPos = pos + + def showViewMenu(self, ev): + #QtGui.QPushButton.mouseReleaseEvent(self.ui.addNodeBtn, ev) + #if ev.button() == QtCore.Qt.RightButton: + #self.menuPos = self.view.mapToScene(ev.pos()) + #self.nodeMenu.popup(ev.globalPos()) + #print "Flowchart.showViewMenu called" + + #self.menuPos = ev.scenePos() + self.buildMenu(ev.scenePos()) + self.nodeMenu.popup(ev.screenPos()) + + def scene(self): + return self._scene ## the GraphicsScene item + + def viewBox(self): + return self._viewBox ## the viewBox that items should be added to + + def nodeMenuTriggered(self, action): + nodeType = action.nodeType + if action.pos is not None: + pos = action.pos + else: + pos = self.menuPos + pos = self.viewBox().mapSceneToView(pos) + + self.chart.createNode(nodeType, pos=pos) + + + def selectionChanged(self): + #print "FlowchartWidget.selectionChanged called." + items = self._scene.selectedItems() + #print " scene.selectedItems: ", items + if len(items) == 0: + data = None + else: + item = items[0] + if hasattr(item, 'node') and isinstance(item.node, Node): + n = item.node + self.ctrl.select(n) + data = {'outputs': n.outputValues(), 'inputs': n.inputValues()} + self.selNameLabel.setText(n.name()) + if hasattr(n, 'nodeName'): + self.selDescLabel.setText("%s: %s" % (n.nodeName, n.__class__.__doc__)) + else: + self.selDescLabel.setText("") + if n.exception is not None: + data['exception'] = n.exception + else: + data = None + self.selectedTree.setData(data, hideRoot=True) + + def hoverOver(self, items): + #print "FlowchartWidget.hoverOver called." + term = None + for item in items: + if item is self.hoverItem: + return + self.hoverItem = item + if hasattr(item, 'term') and isinstance(item.term, Terminal): + term = item.term + break + if term is None: + self.hoverText.setPlainText("") + else: + val = term.value() + if isinstance(val, ndarray): + val = "%s %s %s" % (type(val).__name__, str(val.shape), str(val.dtype)) + else: + val = str(val) + if len(val) > 400: + val = val[:400] + "..." + self.hoverText.setPlainText("%s.%s = %s" % (term.node().name(), term.name(), val)) + #self.hoverLabel.setCursorPosition(0) + + + + def clear(self): + #self.outputTree.setData(None) + self.selectedTree.setData(None) + self.hoverText.setPlainText('') + self.selNameLabel.setText('') + self.selDescLabel.setText('') + + +class FlowchartNode(Node): + pass + diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui new file mode 100644 index 00000000..610846b6 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate.ui @@ -0,0 +1,120 @@ + + + Form + + + + 0 + 0 + 217 + 499 + + + + Form + + + + 0 + + + 0 + + + + + Load.. + + + + + + + + + + + + + + + + Flowchart + + + true + + + + + + + false + + + false + + + false + + + false + + + + 1 + + + + + + + + + 75 + true + + + + + + + Qt::AlignCenter + + + + + + + + TreeWidget + QTreeWidget +
pyqtgraph.widgets.TreeWidget
+
+ + FeedbackButton + QPushButton +
pyqtgraph.widgets.FeedbackButton
+
+
+ + +
diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py new file mode 100644 index 00000000..0410cdf3 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './flowchart/FlowchartCtrlTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(217, 499) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.loadBtn = QtGui.QPushButton(Form) + self.loadBtn.setObjectName(_fromUtf8("loadBtn")) + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName(_fromUtf8("saveBtn")) + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName(_fromUtf8("saveAsBtn")) + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName(_fromUtf8("reloadBtn")) + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtGui.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName(_fromUtf8("showChartBtn")) + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName(_fromUtf8("ctrlList")) + self.ctrlList.headerItem().setText(0, _fromUtf8("1")) + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtGui.QLabel(Form) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText(_fromUtf8("")) + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName(_fromUtf8("fileNameLabel")) + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) + self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.FeedbackButton import FeedbackButton +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py new file mode 100644 index 00000000..f579c957 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './flowchart/FlowchartCtrlTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(217, 499) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtGui.QPushButton(Form) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName("saveBtn") + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName("saveAsBtn") + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtGui.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName("showChartBtn") + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName("ctrlList") + self.ctrlList.headerItem().setText(0, "1") + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtGui.QLabel(Form) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText("") + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName("fileNameLabel") + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) + self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) + self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.FeedbackButton import FeedbackButton +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartGraphicsView.py b/pyqtgraph/flowchart/FlowchartGraphicsView.py new file mode 100644 index 00000000..0ec4d5c8 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartGraphicsView.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.widgets.GraphicsView import GraphicsView +from pyqtgraph.GraphicsScene import GraphicsScene +from pyqtgraph.graphicsItems.ViewBox import ViewBox + +#class FlowchartGraphicsView(QtGui.QGraphicsView): +class FlowchartGraphicsView(GraphicsView): + + sigHoverOver = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + + def __init__(self, widget, *args): + #QtGui.QGraphicsView.__init__(self, *args) + GraphicsView.__init__(self, *args, useOpenGL=False) + #self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255))) + self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True) + self.setCentralItem(self._vb) + #self.scene().addItem(self.vb) + #self.setMouseTracking(True) + #self.lastPos = None + #self.setTransformationAnchor(self.AnchorViewCenter) + #self.setRenderHints(QtGui.QPainter.Antialiasing) + self.setRenderHint(QtGui.QPainter.Antialiasing, True) + #self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) + #self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect) + + def viewBox(self): + return self._vb + + + #def mousePressEvent(self, ev): + #self.moved = False + #self.lastPos = ev.pos() + #return QtGui.QGraphicsView.mousePressEvent(self, ev) + + #def mouseMoveEvent(self, ev): + #self.moved = True + #callSuper = False + #if ev.buttons() & QtCore.Qt.RightButton: + #if self.lastPos is not None: + #dif = ev.pos() - self.lastPos + #self.scale(1.01**-dif.y(), 1.01**-dif.y()) + #elif ev.buttons() & QtCore.Qt.MidButton: + #if self.lastPos is not None: + #dif = ev.pos() - self.lastPos + #self.translate(dif.x(), -dif.y()) + #else: + ##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos())) + #self.sigHoverOver.emit(self.items(ev.pos())) + #callSuper = True + #self.lastPos = ev.pos() + + #if callSuper: + #QtGui.QGraphicsView.mouseMoveEvent(self, ev) + + #def mouseReleaseEvent(self, ev): + #if not self.moved: + ##self.emit(QtCore.SIGNAL('clicked'), ev) + #self.sigClicked.emit(ev) + #return QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + +class FlowchartViewBox(ViewBox): + + def __init__(self, widget, *args, **kwargs): + ViewBox.__init__(self, *args, **kwargs) + self.widget = widget + #self.menu = None + #self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense)) + + + + + def getMenu(self, ev): + ## called by ViewBox to create a new context menu + self._fc_menu = QtGui.QMenu() + self._subMenus = self.getContextMenus(ev) + for menu in self._subMenus: + self._fc_menu.addMenu(menu) + return self._fc_menu + + def getContextMenus(self, ev): + ## called by scene to add menus on to someone else's context menu + menu = self.widget.buildMenu(ev.scenePos()) + menu.setTitle("Add node") + return [menu, ViewBox.getMenu(self, ev)] + + + + + + + + + + +##class FlowchartGraphicsScene(QtGui.QGraphicsScene): +#class FlowchartGraphicsScene(GraphicsScene): + + #sigContextMenuEvent = QtCore.Signal(object) + + #def __init__(self, *args): + ##QtGui.QGraphicsScene.__init__(self, *args) + #GraphicsScene.__init__(self, *args) + + #def mouseClickEvent(self, ev): + ##QtGui.QGraphicsScene.contextMenuEvent(self, ev) + #if not ev.button() in [QtCore.Qt.RightButton]: + #self.sigContextMenuEvent.emit(ev) \ No newline at end of file diff --git a/pyqtgraph/flowchart/FlowchartTemplate.ui b/pyqtgraph/flowchart/FlowchartTemplate.ui new file mode 100644 index 00000000..e4530800 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartTemplate.ui @@ -0,0 +1,98 @@ + + + Form + + + + 0 + 0 + 529 + 329 + + + + Form + + + + + 260 + 10 + 264 + 222 + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 75 + true + + + + + + + + + + + + 1 + + + + + + + + + + 0 + 240 + 521 + 81 + + + + + + + 0 + 0 + 256 + 192 + + + + + + + DataTreeWidget + QTreeWidget +
pyqtgraph.widgets.DataTreeWidget
+
+ + FlowchartGraphicsView + QGraphicsView +
FlowchartGraphicsView
+
+
+ + +
diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py new file mode 100644 index 00000000..2e9ea312 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './flowchart/FlowchartTemplate.ui' +# +# Created: Sun Sep 9 14:41:29 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(529, 329) + self.selInfoWidget = QtGui.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName(_fromUtf8("selInfoWidget")) + self.gridLayout = QtGui.QGridLayout(self.selInfoWidget) + self.gridLayout.setMargin(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.selDescLabel = QtGui.QLabel(self.selInfoWidget) + self.selDescLabel.setText(_fromUtf8("")) + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName(_fromUtf8("selDescLabel")) + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtGui.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.selNameLabel.setFont(font) + self.selNameLabel.setText(_fromUtf8("")) + self.selNameLabel.setObjectName(_fromUtf8("selNameLabel")) + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName(_fromUtf8("selectedTree")) + self.selectedTree.headerItem().setText(0, _fromUtf8("1")) + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtGui.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName(_fromUtf8("hoverText")) + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName(_fromUtf8("view")) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.DataTreeWidget import DataTreeWidget +from FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py new file mode 100644 index 00000000..d49d3083 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './flowchart/FlowchartTemplate.ui' +# +# Created: Sun Sep 9 14:41:30 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(529, 329) + self.selInfoWidget = QtGui.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName("selInfoWidget") + self.gridLayout = QtGui.QGridLayout(self.selInfoWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.selDescLabel = QtGui.QLabel(self.selInfoWidget) + self.selDescLabel.setText("") + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName("selDescLabel") + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtGui.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.selNameLabel.setFont(font) + self.selNameLabel.setText("") + self.selNameLabel.setObjectName("selNameLabel") + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName("selectedTree") + self.selectedTree.headerItem().setText(0, "1") + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtGui.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName("hoverText") + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName("view") + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.DataTreeWidget import DataTreeWidget +from FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py new file mode 100644 index 00000000..ed5c9714 --- /dev/null +++ b/pyqtgraph/flowchart/Node.py @@ -0,0 +1,540 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from .Terminal import * +from pyqtgraph.pgcollections import OrderedDict +from pyqtgraph.debug import * +import numpy as np +from .eq import * + + +def strDict(d): + return dict([(str(k), v) for k, v in d.items()]) + +class Node(QtCore.QObject): + + sigOutputChanged = QtCore.Signal(object) # self + sigClosed = QtCore.Signal(object) + sigRenamed = QtCore.Signal(object, object) + sigTerminalRenamed = QtCore.Signal(object, object) # term, oldName + sigTerminalAdded = QtCore.Signal(object, object) # self, term + sigTerminalRemoved = QtCore.Signal(object, object) # self, term + + + def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True): + QtCore.QObject.__init__(self) + self._name = name + self._bypass = False + self.bypassButton = None ## this will be set by the flowchart ctrl widget.. + self._graphicsItem = None + self.terminals = OrderedDict() + self._inputs = OrderedDict() + self._outputs = OrderedDict() + self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals + self._allowAddOutput = allowAddOutput + self._allowRemove = allowRemove + + self.exception = None + if terminals is None: + return + for name, opts in terminals.items(): + self.addTerminal(name, **opts) + + + def nextTerminalName(self, name): + """Return an unused terminal name""" + name2 = name + i = 1 + while name2 in self.terminals: + name2 = "%s.%d" % (name, i) + i += 1 + return name2 + + def addInput(self, name="Input", **args): + #print "Node.addInput called." + return self.addTerminal(name, io='in', **args) + + def addOutput(self, name="Output", **args): + return self.addTerminal(name, io='out', **args) + + def removeTerminal(self, term): + ## term may be a terminal or its name + + if isinstance(term, Terminal): + name = term.name() + else: + name = term + term = self.terminals[name] + + #print "remove", name + #term.disconnectAll() + term.close() + del self.terminals[name] + if name in self._inputs: + del self._inputs[name] + if name in self._outputs: + del self._outputs[name] + self.graphicsItem().updateTerminals() + self.sigTerminalRemoved.emit(self, term) + + + def terminalRenamed(self, term, oldName): + """Called after a terminal has been renamed""" + newName = term.name() + for d in [self.terminals, self._inputs, self._outputs]: + if oldName not in d: + continue + d[newName] = d[oldName] + del d[oldName] + + self.graphicsItem().updateTerminals() + self.sigTerminalRenamed.emit(term, oldName) + + def addTerminal(self, name, **opts): + name = self.nextTerminalName(name) + term = Terminal(self, name, **opts) + self.terminals[name] = term + if term.isInput(): + self._inputs[name] = term + elif term.isOutput(): + self._outputs[name] = term + self.graphicsItem().updateTerminals() + self.sigTerminalAdded.emit(self, term) + return term + + + def inputs(self): + return self._inputs + + def outputs(self): + return self._outputs + + def process(self, **kargs): + """Process data through this node. Each named argument supplies data to the corresponding terminal.""" + return {} + + def graphicsItem(self): + """Return a (the?) graphicsitem for this node""" + #print "Node.graphicsItem called." + if self._graphicsItem is None: + #print "Creating NodeGraphicsItem..." + self._graphicsItem = NodeGraphicsItem(self) + #print "Node.graphicsItem is returning ", self._graphicsItem + return self._graphicsItem + + def __getattr__(self, attr): + """Return the terminal with the given name""" + if attr not in self.terminals: + raise AttributeError(attr) + else: + return self.terminals[attr] + + def __getitem__(self, item): + return getattr(self, item) + + def name(self): + return self._name + + def rename(self, name): + oldName = self._name + self._name = name + #self.emit(QtCore.SIGNAL('renamed'), self, oldName) + self.sigRenamed.emit(self, oldName) + + def dependentNodes(self): + """Return the list of nodes which provide direct input to this node""" + nodes = set() + for t in self.inputs().values(): + nodes |= set([i.node() for i in t.inputTerminals()]) + return nodes + #return set([t.inputTerminals().node() for t in self.listInputs().itervalues()]) + + def __repr__(self): + return "" % (self.name(), id(self)) + + def ctrlWidget(self): + return None + + def bypass(self, byp): + self._bypass = byp + if self.bypassButton is not None: + self.bypassButton.setChecked(byp) + self.update() + + def isBypassed(self): + return self._bypass + + def setInput(self, **args): + """Set the values on input terminals. For most nodes, this will happen automatically through Terminal.inputChanged. + This is normally only used for nodes with no connected inputs.""" + changed = False + for k, v in args.items(): + term = self._inputs[k] + oldVal = term.value() + if not eq(oldVal, v): + changed = True + term.setValue(v, process=False) + if changed and '_updatesHandled_' not in args: + self.update() + + def inputValues(self): + vals = {} + for n, t in self.inputs().items(): + vals[n] = t.value() + return vals + + def outputValues(self): + vals = {} + for n, t in self.outputs().items(): + vals[n] = t.value() + return vals + + def connected(self, localTerm, remoteTerm): + """Called whenever one of this node's terminals is connected elsewhere.""" + pass + + def disconnected(self, localTerm, remoteTerm): + """Called whenever one of this node's terminals is connected elsewhere.""" + pass + + def update(self, signal=True): + """Collect all input values, attempt to process new output values, and propagate downstream.""" + vals = self.inputValues() + #print " inputs:", vals + try: + if self.isBypassed(): + out = self.processBypassed(vals) + else: + out = self.process(**strDict(vals)) + #print " output:", out + if out is not None: + if signal: + self.setOutput(**out) + else: + self.setOutputNoSignal(**out) + for n,t in self.inputs().items(): + t.setValueAcceptable(True) + self.clearException() + except: + #printExc( "Exception while processing %s:" % self.name()) + for n,t in self.outputs().items(): + t.setValue(None) + self.setException(sys.exc_info()) + + if signal: + #self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data + self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data + + def processBypassed(self, args): + result = {} + for term in list(self.outputs().values()): + byp = term.bypassValue() + if byp is None: + result[term.name()] = None + else: + result[term.name()] = args.get(byp, None) + return result + + def setOutput(self, **vals): + self.setOutputNoSignal(**vals) + #self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data + self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data + + def setOutputNoSignal(self, **vals): + for k, v in vals.items(): + term = self.outputs()[k] + term.setValue(v) + #targets = term.connections() + #for t in targets: ## propagate downstream + #if t is term: + #continue + #t.inputChanged(term) + term.setValueAcceptable(True) + + def setException(self, exc): + self.exception = exc + self.recolor() + + def clearException(self): + self.setException(None) + + def recolor(self): + if self.exception is None: + self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(0, 0, 0))) + else: + self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(150, 0, 0), 3)) + + def saveState(self): + pos = self.graphicsItem().pos() + state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} + termsEditable = self._allowAddInput | self._allowAddOutput + for term in self._inputs.values() + self._outputs.values(): + termsEditable |= term._renamable | term._removable | term._multiable + if termsEditable: + state['terminals'] = self.saveTerminals() + return state + + def restoreState(self, state): + pos = state.get('pos', (0,0)) + self.graphicsItem().setPos(*pos) + self.bypass(state.get('bypass', False)) + if 'terminals' in state: + self.restoreTerminals(state['terminals']) + + def saveTerminals(self): + terms = OrderedDict() + for n, t in self.terminals.items(): + terms[n] = (t.saveState()) + return terms + + def restoreTerminals(self, state): + for name in list(self.terminals.keys()): + if name not in state: + self.removeTerminal(name) + for name, opts in state.items(): + if name in self.terminals: + term = self[name] + term.setOpts(**opts) + continue + try: + opts = strDict(opts) + self.addTerminal(name, **opts) + except: + printExc("Error restoring terminal %s (%s):" % (str(name), str(opts))) + + + def clearTerminals(self): + for t in self.terminals.values(): + t.close() + self.terminals = OrderedDict() + self._inputs = OrderedDict() + self._outputs = OrderedDict() + + def close(self): + """Cleans up after the node--removes terminals, graphicsItem, widget""" + self.disconnectAll() + self.clearTerminals() + item = self.graphicsItem() + if item.scene() is not None: + item.scene().removeItem(item) + self._graphicsItem = None + w = self.ctrlWidget() + if w is not None: + w.setParent(None) + #self.emit(QtCore.SIGNAL('closed'), self) + self.sigClosed.emit(self) + + def disconnectAll(self): + for t in self.terminals.values(): + t.disconnectAll() + + +#class NodeGraphicsItem(QtGui.QGraphicsItem): +class NodeGraphicsItem(GraphicsObject): + def __init__(self, node): + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + #QObjectWorkaround.__init__(self) + + #self.shadow = QtGui.QGraphicsDropShadowEffect() + #self.shadow.setOffset(5,5) + #self.shadow.setBlurRadius(10) + #self.setGraphicsEffect(self.shadow) + + self.pen = fn.mkPen(0,0,0) + self.selectPen = fn.mkPen(200,200,200,width=2) + self.brush = fn.mkBrush(200, 200, 200, 150) + self.hoverBrush = fn.mkBrush(200, 200, 200, 200) + self.selectBrush = fn.mkBrush(200, 200, 255, 200) + self.hovered = False + + self.node = node + flags = self.ItemIsMovable | self.ItemIsSelectable | self.ItemIsFocusable |self.ItemSendsGeometryChanges + #flags = self.ItemIsFocusable |self.ItemSendsGeometryChanges + + self.setFlags(flags) + self.bounds = QtCore.QRectF(0, 0, 100, 100) + self.nameItem = QtGui.QGraphicsTextItem(self.node.name(), self) + self.nameItem.setDefaultTextColor(QtGui.QColor(50, 50, 50)) + self.nameItem.moveBy(self.bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) + self.nameItem.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction) + self.updateTerminals() + #self.setZValue(10) + + self.nameItem.focusOutEvent = self.labelFocusOut + self.nameItem.keyPressEvent = self.labelKeyPress + + self.menu = None + self.buildMenu() + + #self.node.sigTerminalRenamed.connect(self.updateActionMenu) + + #def setZValue(self, z): + #for t, item in self.terminals.itervalues(): + #item.setZValue(z+1) + #GraphicsObject.setZValue(self, z) + + def labelFocusOut(self, ev): + QtGui.QGraphicsTextItem.focusOutEvent(self.nameItem, ev) + self.labelChanged() + + def labelKeyPress(self, ev): + if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return: + self.labelChanged() + else: + QtGui.QGraphicsTextItem.keyPressEvent(self.nameItem, ev) + + def labelChanged(self): + newName = str(self.nameItem.toPlainText()) + if newName != self.node.name(): + self.node.rename(newName) + + ### re-center the label + bounds = self.boundingRect() + self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) + + def setPen(self, pen): + self.pen = pen + self.update() + + def setBrush(self, brush): + self.brush = brush + self.update() + + + def updateTerminals(self): + bounds = self.bounds + self.terminals = {} + inp = self.node.inputs() + dy = bounds.height() / (len(inp)+1) + y = dy + for i, t in inp.items(): + item = t.graphicsItem() + item.setParentItem(self) + #item.setZValue(self.zValue()+1) + br = self.bounds + item.setAnchor(0, y) + self.terminals[i] = (t, item) + y += dy + + out = self.node.outputs() + dy = bounds.height() / (len(out)+1) + y = dy + for i, t in out.items(): + item = t.graphicsItem() + item.setParentItem(self) + item.setZValue(self.zValue()) + br = self.bounds + item.setAnchor(bounds.width(), y) + self.terminals[i] = (t, item) + y += dy + + #self.buildMenu() + + + def boundingRect(self): + return self.bounds.adjusted(-5, -5, 5, 5) + + def paint(self, p, *args): + + p.setPen(self.pen) + if self.isSelected(): + p.setPen(self.selectPen) + p.setBrush(self.selectBrush) + else: + p.setPen(self.pen) + if self.hovered: + p.setBrush(self.hoverBrush) + else: + p.setBrush(self.brush) + + p.drawRect(self.bounds) + + + def mousePressEvent(self, ev): + ev.ignore() + + + def mouseClickEvent(self, ev): + #print "Node.mouseClickEvent called." + if int(ev.button()) == int(QtCore.Qt.LeftButton): + ev.accept() + #print " ev.button: left" + sel = self.isSelected() + #ret = QtGui.QGraphicsItem.mousePressEvent(self, ev) + self.setSelected(True) + if not sel and self.isSelected(): + #self.setBrush(QtGui.QBrush(QtGui.QColor(200, 200, 255))) + #self.emit(QtCore.SIGNAL('selected')) + #self.scene().selectionChanged.emit() ## for some reason this doesn't seem to be happening automatically + self.update() + #return ret + + elif int(ev.button()) == int(QtCore.Qt.RightButton): + #print " ev.button: right" + ev.accept() + #pos = ev.screenPos() + self.raiseContextMenu(ev) + #self.menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def mouseDragEvent(self, ev): + #print "Node.mouseDrag" + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.setPos(self.pos()+self.mapToParent(ev.pos())-self.mapToParent(ev.lastPos())) + + def hoverEvent(self, ev): + if not ev.isExit() and ev.acceptClicks(QtCore.Qt.LeftButton): + ev.acceptDrags(QtCore.Qt.LeftButton) + self.hovered = True + else: + self.hovered = False + self.update() + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: + ev.accept() + if not self.node._allowRemove: + return + self.node.close() + else: + ev.ignore() + + def itemChange(self, change, val): + if change == self.ItemPositionHasChanged: + for k, t in self.terminals.items(): + t[1].nodeMoved() + return GraphicsObject.itemChange(self, change, val) + + + def getMenu(self): + return self.menu + + def getContextMenus(self, event): + return [self.menu] + + def raiseContextMenu(self, ev): + menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def buildMenu(self): + self.menu = QtGui.QMenu() + self.menu.setTitle("Node") + a = self.menu.addAction("Add input", self.addInputFromMenu) + if not self.node._allowAddInput: + a.setEnabled(False) + a = self.menu.addAction("Add output", self.addOutputFromMenu) + if not self.node._allowAddOutput: + a.setEnabled(False) + a = self.menu.addAction("Remove node", self.node.close) + if not self.node._allowRemove: + a.setEnabled(False) + + def addInputFromMenu(self): ## called when add input is clicked in context menu + self.node.addInput(renamable=True, removable=True, multiable=True) + + def addOutputFromMenu(self): ## called when add output is clicked in context menu + self.node.addOutput(renamable=True, removable=True, multiable=False) + diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py new file mode 100644 index 00000000..18ff12c1 --- /dev/null +++ b/pyqtgraph/flowchart/Terminal.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +import weakref +from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from pyqtgraph.Point import Point +#from PySide import QtCore, QtGui +from .eq import * + +class Terminal(object): + def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None): + """ + Construct a new terminal. + + ============== ================================================================================= + **Arguments:** + node the node to which this terminal belongs + name string, the name of the terminal + io 'in' or 'out' + optional bool, whether the node may process without connection to this terminal + multi bool, for inputs: whether this terminal may make multiple connections + for outputs: whether this terminal creates a different value for each connection + pos [x, y], the position of the terminal within its node's boundaries + renamable (bool) Whether the terminal can be renamed by the user + removable (bool) Whether the terminal can be removed by the user + multiable (bool) Whether the user may toggle the *multi* option for this terminal + ============== ================================================================================= + """ + self._io = io + #self._isOutput = opts[0] in ['out', 'io'] + #self._isInput = opts[0]] in ['in', 'io'] + #self._isIO = opts[0]=='io' + self._optional = optional + self._multi = multi + self._node = weakref.ref(node) + self._name = name + self._renamable = renamable + self._removable = removable + self._multiable = multiable + self._connections = {} + self._graphicsItem = TerminalGraphicsItem(self, parent=self._node().graphicsItem()) + self._bypass = bypass + + if multi: + self._value = {} ## dictionary of terminal:value pairs. + else: + self._value = None + + self.valueOk = None + self.recolor() + + def value(self, term=None): + """Return the value this terminal provides for the connected terminal""" + if term is None: + return self._value + + if self.isMultiValue(): + return self._value.get(term, None) + else: + return self._value + + def bypassValue(self): + return self._bypass + + def setValue(self, val, process=True): + """If this is a single-value terminal, val should be a single value. + If this is a multi-value terminal, val should be a dict of terminal:value pairs""" + if not self.isMultiValue(): + if eq(val, self._value): + return + self._value = val + else: + if not isinstance(self._value, dict): + self._value = {} + if val is not None: + self._value.update(val) + + self.setValueAcceptable(None) ## by default, input values are 'unchecked' until Node.update(). + if self.isInput() and process: + self.node().update() + + ## Let the flowchart handle this. + #if self.isOutput(): + #for c in self.connections(): + #if c.isInput(): + #c.inputChanged(self) + self.recolor() + + def setOpts(self, **opts): + self._renamable = opts.get('renamable', self._renamable) + self._removable = opts.get('removable', self._removable) + self._multiable = opts.get('multiable', self._multiable) + if 'multi' in opts: + self.setMultiValue(opts['multi']) + + + def connected(self, term): + """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" + if self.isInput() and term.isOutput(): + self.inputChanged(term) + if self.isOutput() and self.isMultiValue(): + self.node().update() + self.node().connected(self, term) + + def disconnected(self, term): + """Called whenever this terminal has been disconnected from another. (note--this function is called on both terminals)""" + if self.isMultiValue() and term in self._value: + del self._value[term] + self.node().update() + #self.recolor() + else: + if self.isInput(): + self.setValue(None) + self.node().disconnected(self, term) + #self.node().update() + + def inputChanged(self, term, process=True): + """Called whenever there is a change to the input value to this terminal. + It may often be useful to override this function.""" + if self.isMultiValue(): + self.setValue({term: term.value(self)}, process=process) + else: + self.setValue(term.value(self), process=process) + + def valueIsAcceptable(self): + """Returns True->acceptable None->unknown False->Unacceptable""" + return self.valueOk + + def setValueAcceptable(self, v=True): + self.valueOk = v + self.recolor() + + def connections(self): + return self._connections + + def node(self): + return self._node() + + def isInput(self): + return self._io == 'in' + + def isMultiValue(self): + return self._multi + + def setMultiValue(self, multi): + """Set whether this is a multi-value terminal.""" + self._multi = multi + if not multi and len(self.inputTerminals()) > 1: + self.disconnectAll() + + for term in self.inputTerminals(): + self.inputChanged(term) + + def isOutput(self): + return self._io == 'out' + + def isRenamable(self): + return self._renamable + + def isRemovable(self): + return self._removable + + def isMultiable(self): + return self._multiable + + def name(self): + return self._name + + def graphicsItem(self): + return self._graphicsItem + + def isConnected(self): + return len(self.connections()) > 0 + + def connectedTo(self, term): + return term in self.connections() + + def hasInput(self): + #conn = self.extendedConnections() + for t in self.connections(): + if t.isOutput(): + return True + return False + + def inputTerminals(self): + """Return the terminal(s) that give input to this one.""" + #terms = self.extendedConnections() + #for t in terms: + #if t.isOutput(): + #return t + return [t for t in self.connections() if t.isOutput()] + + + def dependentNodes(self): + """Return the list of nodes which receive input from this terminal.""" + #conn = self.extendedConnections() + #del conn[self] + return set([t.node() for t in self.connections() if t.isInput()]) + + def connectTo(self, term, connectionItem=None): + try: + if self.connectedTo(term): + raise Exception('Already connected') + if term is self: + raise Exception('Not connecting terminal to self') + if term.node() is self.node(): + raise Exception("Can't connect to terminal on same node.") + for t in [self, term]: + if t.isInput() and not t._multi and len(t.connections()) > 0: + raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys()))) + #if self.hasInput() and term.hasInput(): + #raise Exception('Target terminal already has input') + + #if term in self.node().terminals.values(): + #if self.isOutput() or term.isOutput(): + #raise Exception('Can not connect an output back to the same node.') + except: + if connectionItem is not None: + connectionItem.close() + raise + + if connectionItem is None: + connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) + #self.graphicsItem().scene().addItem(connectionItem) + self.graphicsItem().getViewBox().addItem(connectionItem) + #connectionItem.setParentItem(self.graphicsItem().parent().parent()) + self._connections[term] = connectionItem + term._connections[self] = connectionItem + + self.recolor() + + #if self.isOutput() and term.isInput(): + #term.inputChanged(self) + #if term.isInput() and term.isOutput(): + #self.inputChanged(term) + self.connected(term) + term.connected(self) + + return connectionItem + + def disconnectFrom(self, term): + if not self.connectedTo(term): + return + item = self._connections[term] + #print "removing connection", item + #item.scene().removeItem(item) + item.close() + del self._connections[term] + del term._connections[self] + self.recolor() + term.recolor() + + self.disconnected(term) + term.disconnected(self) + #if self.isOutput() and term.isInput(): + #term.inputChanged(self) + #if term.isInput() and term.isOutput(): + #self.inputChanged(term) + + + def disconnectAll(self): + for t in list(self._connections.keys()): + self.disconnectFrom(t) + + def recolor(self, color=None, recurse=True): + if color is None: + if not self.isConnected(): ## disconnected terminals are black + color = QtGui.QColor(0,0,0) + elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals + color = QtGui.QColor(200,200,0) + elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) + color = QtGui.QColor(255,255,255) + elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok + color = QtGui.QColor(200, 200, 0) + elif self.valueIsAcceptable() is True: ## terminal has good input, all ok + color = QtGui.QColor(0, 200, 0) + else: ## terminal has bad input + color = QtGui.QColor(200, 0, 0) + self.graphicsItem().setBrush(QtGui.QBrush(color)) + + if recurse: + for t in self.connections(): + t.recolor(color, recurse=False) + + + def rename(self, name): + oldName = self._name + self._name = name + self.node().terminalRenamed(self, oldName) + self.graphicsItem().termRenamed(name) + + def __repr__(self): + return "" % (str(self.node().name()), str(self.name())) + + #def extendedConnections(self, terms=None): + #"""Return list of terminals (including this one) that are directly or indirectly wired to this.""" + #if terms is None: + #terms = {} + #terms[self] = None + #for t in self._connections: + #if t in terms: + #continue + #terms.update(t.extendedConnections(terms)) + #return terms + + def __hash__(self): + return id(self) + + def close(self): + self.disconnectAll() + item = self.graphicsItem() + if item.scene() is not None: + item.scene().removeItem(item) + + def saveState(self): + return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable} + + +#class TerminalGraphicsItem(QtGui.QGraphicsItem): +class TerminalGraphicsItem(GraphicsObject): + + def __init__(self, term, parent=None): + self.term = term + #QtGui.QGraphicsItem.__init__(self, parent) + GraphicsObject.__init__(self, parent) + self.brush = fn.mkBrush(0,0,0) + self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) + self.label = QtGui.QGraphicsTextItem(self.term.name(), self) + self.label.scale(0.7, 0.7) + #self.setAcceptHoverEvents(True) + self.newConnection = None + self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem + if self.term.isRenamable(): + self.label.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction) + self.label.focusOutEvent = self.labelFocusOut + self.label.keyPressEvent = self.labelKeyPress + self.setZValue(1) + self.menu = None + + + def labelFocusOut(self, ev): + QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) + self.labelChanged() + + def labelKeyPress(self, ev): + if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return: + self.labelChanged() + else: + QtGui.QGraphicsTextItem.keyPressEvent(self.label, ev) + + def labelChanged(self): + newName = str(self.label.toPlainText()) + if newName != self.term.name(): + self.term.rename(newName) + + def termRenamed(self, name): + self.label.setPlainText(name) + + def setBrush(self, brush): + self.brush = brush + self.box.setBrush(brush) + + def disconnect(self, target): + self.term.disconnectFrom(target.term) + + def boundingRect(self): + br = self.box.mapRectToParent(self.box.boundingRect()) + lr = self.label.mapRectToParent(self.label.boundingRect()) + return br | lr + + def paint(self, p, *args): + pass + + def setAnchor(self, x, y): + pos = QtCore.QPointF(x, y) + self.anchorPos = pos + br = self.box.mapRectToParent(self.box.boundingRect()) + lr = self.label.mapRectToParent(self.label.boundingRect()) + + + if self.term.isInput(): + self.box.setPos(pos.x(), pos.y()-br.height()/2.) + self.label.setPos(pos.x() + br.width(), pos.y() - lr.height()/2.) + else: + self.box.setPos(pos.x()-br.width(), pos.y()-br.height()/2.) + self.label.setPos(pos.x()-br.width()-lr.width(), pos.y()-lr.height()/2.) + self.updateConnections() + + def updateConnections(self): + for t, c in self.term.connections().items(): + c.updateLine() + + def mousePressEvent(self, ev): + #ev.accept() + ev.ignore() ## necessary to allow click/drag events to process correctly + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.label.setFocus(QtCore.Qt.MouseFocusReason) + elif ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.raiseContextMenu(ev) + + def raiseContextMenu(self, ev): + ## only raise menu if this terminal is removable + menu = self.getMenu() + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def getMenu(self): + if self.menu is None: + self.menu = QtGui.QMenu() + self.menu.setTitle("Terminal") + remAct = QtGui.QAction("Remove terminal", self.menu) + remAct.triggered.connect(self.removeSelf) + self.menu.addAction(remAct) + self.menu.remAct = remAct + if not self.term.isRemovable(): + remAct.setEnabled(False) + multiAct = QtGui.QAction("Multi-value", self.menu) + multiAct.setCheckable(True) + multiAct.setChecked(self.term.isMultiValue()) + multiAct.setEnabled(self.term.isMultiable()) + + multiAct.triggered.connect(self.toggleMulti) + self.menu.addAction(multiAct) + self.menu.multiAct = multiAct + if self.term.isMultiable(): + multiAct.setEnabled = False + return self.menu + + def toggleMulti(self): + multi = self.menu.multiAct.isChecked() + self.term.setMultiValue(multi) + + ## probably never need this + #def getContextMenus(self, ev): + #return [self.getMenu()] + + def removeSelf(self): + self.term.node().removeTerminal(self.term) + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + + ev.accept() + if ev.isStart(): + if self.newConnection is None: + self.newConnection = ConnectionItem(self) + #self.scene().addItem(self.newConnection) + self.getViewBox().addItem(self.newConnection) + #self.newConnection.setParentItem(self.parent().parent()) + + self.newConnection.setTarget(self.mapToView(ev.pos())) + elif ev.isFinish(): + if self.newConnection is not None: + items = self.scene().items(ev.scenePos()) + gotTarget = False + for i in items: + if isinstance(i, TerminalGraphicsItem): + self.newConnection.setTarget(i) + try: + self.term.connectTo(i.term, self.newConnection) + gotTarget = True + except: + self.scene().removeItem(self.newConnection) + self.newConnection = None + raise + break + + if not gotTarget: + #print "remove unused connection" + #self.scene().removeItem(self.newConnection) + self.newConnection.close() + self.newConnection = None + else: + if self.newConnection is not None: + self.newConnection.setTarget(self.mapToView(ev.pos())) + + def hoverEvent(self, ev): + if not ev.isExit() and ev.acceptDrags(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. + ev.acceptClicks(QtCore.Qt.RightButton) + self.box.setBrush(fn.mkBrush('w')) + else: + self.box.setBrush(self.brush) + self.update() + + #def hoverEnterEvent(self, ev): + #self.hover = True + + #def hoverLeaveEvent(self, ev): + #self.hover = False + + def connectPoint(self): + ## return the connect position of this terminal in view coords + return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) + + def nodeMoved(self): + for t, item in self.term.connections().items(): + item.updateLine() + + +#class ConnectionItem(QtGui.QGraphicsItem): +class ConnectionItem(GraphicsObject): + + def __init__(self, source, target=None): + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + self.setFlags( + self.ItemIsSelectable | + self.ItemIsFocusable + ) + self.source = source + self.target = target + self.length = 0 + self.hovered = False + #self.line = QtGui.QGraphicsLineItem(self) + self.source.getViewBox().addItem(self) + self.updateLine() + self.setZValue(0) + + def close(self): + if self.scene() is not None: + #self.scene().removeItem(self.line) + self.scene().removeItem(self) + + def setTarget(self, target): + self.target = target + self.updateLine() + + def updateLine(self): + start = Point(self.source.connectPoint()) + if isinstance(self.target, TerminalGraphicsItem): + stop = Point(self.target.connectPoint()) + elif isinstance(self.target, QtCore.QPointF): + stop = Point(self.target) + else: + return + self.prepareGeometryChange() + self.resetTransform() + ang = (stop-start).angle(Point(0, 1)) + if ang is None: + ang = 0 + self.rotate(ang) + self.setPos(start) + self.length = (start-stop).length() + self.update() + #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: + #if isinstance(self.target, TerminalGraphicsItem): + self.source.disconnect(self.target) + ev.accept() + else: + ev.ignore() + + def mousePressEvent(self, ev): + ev.ignore() + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + sel = self.isSelected() + self.setSelected(True) + if not sel and self.isSelected(): + self.update() + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): + self.hovered = True + else: + self.hovered = False + self.update() + + + def boundingRect(self): + #return self.line.boundingRect() + px = self.pixelWidth() + return QtCore.QRectF(-5*px, 0, 10*px, self.length) + + #def shape(self): + #return self.line.shape() + + def paint(self, p, *args): + if self.isSelected(): + p.setPen(fn.mkPen(200, 200, 0, width=3)) + else: + if self.hovered: + p.setPen(fn.mkPen(150, 150, 250, width=1)) + else: + p.setPen(fn.mkPen(100, 100, 250, width=1)) + + p.drawLine(0, 0, 0, self.length) diff --git a/pyqtgraph/flowchart/__init__.py b/pyqtgraph/flowchart/__init__.py new file mode 100644 index 00000000..46e04db0 --- /dev/null +++ b/pyqtgraph/flowchart/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from .Flowchart import * + +from .library import getNodeType, registerNodeType, getNodeTree \ No newline at end of file diff --git a/pyqtgraph/flowchart/eq.py b/pyqtgraph/flowchart/eq.py new file mode 100644 index 00000000..031ebce8 --- /dev/null +++ b/pyqtgraph/flowchart/eq.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from numpy import ndarray, bool_ +from pyqtgraph.metaarray import MetaArray + +def eq(a, b): + """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + if a is b: + return True + + try: + e = a==b + except ValueError: + return False + except AttributeError: + return False + except: + print("a:", str(type(a)), str(a)) + print("b:", str(type(b)), str(b)) + raise + t = type(e) + if t is bool: + return e + elif t is bool_: + return bool(e) + elif isinstance(e, ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')): + try: ## disaster: if a is an empty array and b is not, then e.all() is True + if a.shape != b.shape: + return False + except: + return False + if (hasattr(e, 'implements') and e.implements('MetaArray')): + return e.asarray().all() + else: + return e.all() + else: + raise Exception("== operator returned type %s" % str(type(e))) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py new file mode 100644 index 00000000..85ab6232 --- /dev/null +++ b/pyqtgraph/flowchart/library/Data.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +from ..Node import Node +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +from .common import * +from pyqtgraph.SRTTransform import SRTTransform +from pyqtgraph.Point import Point +from pyqtgraph.widgets.TreeWidget import TreeWidget +from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem + +from . import functions + +class ColumnSelectNode(Node): + """Select named columns from a record array or MetaArray.""" + nodeName = "ColumnSelect" + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in'}}) + self.columns = set() + self.columnList = QtGui.QListWidget() + self.axis = 0 + self.columnList.itemChanged.connect(self.itemChanged) + + def process(self, In, display=True): + if display: + self.updateList(In) + + out = {} + if hasattr(In, 'implements') and In.implements('MetaArray'): + for c in self.columns: + out[c] = In[self.axis:c] + elif isinstance(In, np.ndarray) and In.dtype.fields is not None: + for c in self.columns: + out[c] = In[c] + else: + self.In.setValueAcceptable(False) + raise Exception("Input must be MetaArray or ndarray with named fields") + + return out + + def ctrlWidget(self): + return self.columnList + + def updateList(self, data): + if hasattr(data, 'implements') and data.implements('MetaArray'): + cols = data.listColumns() + for ax in cols: ## find first axis with columns + if len(cols[ax]) > 0: + self.axis = ax + cols = set(cols[ax]) + break + else: + cols = list(data.dtype.fields.keys()) + + rem = set() + for c in self.columns: + if c not in cols: + self.removeTerminal(c) + rem.add(c) + self.columns -= rem + + self.columnList.blockSignals(True) + self.columnList.clear() + for c in cols: + item = QtGui.QListWidgetItem(c) + item.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsUserCheckable) + if c in self.columns: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + self.columnList.addItem(item) + self.columnList.blockSignals(False) + + + def itemChanged(self, item): + col = str(item.text()) + if item.checkState() == QtCore.Qt.Checked: + if col not in self.columns: + self.columns.add(col) + self.addOutput(col) + else: + if col in self.columns: + self.columns.remove(col) + self.removeTerminal(col) + self.update() + + def saveState(self): + state = Node.saveState(self) + state['columns'] = list(self.columns) + return state + + def restoreState(self, state): + Node.restoreState(self, state) + self.columns = set(state.get('columns', [])) + for c in self.columns: + self.addOutput(c) + + + +class RegionSelectNode(CtrlNode): + """Returns a slice from a 1-D array. Connect the 'widget' output to a plot to display a region-selection widget.""" + nodeName = "RegionSelect" + uiTemplate = [ + ('start', 'spin', {'value': 0, 'step': 0.1}), + ('stop', 'spin', {'value': 0.1, 'step': 0.1}), + ('display', 'check', {'value': True}), + ('movable', 'check', {'value': True}), + ] + + def __init__(self, name): + self.items = {} + CtrlNode.__init__(self, name, terminals={ + 'data': {'io': 'in'}, + 'selected': {'io': 'out'}, + 'region': {'io': 'out'}, + 'widget': {'io': 'out', 'multi': True} + }) + self.ctrls['display'].toggled.connect(self.displayToggled) + self.ctrls['movable'].toggled.connect(self.movableToggled) + + def displayToggled(self, b): + for item in self.items.values(): + item.setVisible(b) + + def movableToggled(self, b): + for item in self.items.values(): + item.setMovable(b) + + + def process(self, data=None, display=True): + #print "process.." + s = self.stateGroup.state() + region = [s['start'], s['stop']] + + if display: + conn = self['widget'].connections() + for c in conn: + plot = c.node().getPlot() + if plot is None: + continue + if c in self.items: + item = self.items[c] + item.setRegion(region) + #print " set rgn:", c, region + #item.setXVals(events) + else: + item = LinearRegionItem(values=region) + self.items[c] = item + #item.connect(item, QtCore.SIGNAL('regionChanged'), self.rgnChanged) + item.sigRegionChanged.connect(self.rgnChanged) + item.setVisible(s['display']) + item.setMovable(s['movable']) + #print " new rgn:", c, region + #self.items[c].setYRange([0., 0.2], relative=True) + + if self.selected.isConnected(): + if data is None: + sliced = None + elif (hasattr(data, 'implements') and data.implements('MetaArray')): + sliced = data[0:s['start']:s['stop']] + else: + mask = (data['time'] >= s['start']) * (data['time'] < s['stop']) + sliced = data[mask] + else: + sliced = None + + return {'selected': sliced, 'widget': self.items, 'region': region} + + + def rgnChanged(self, item): + region = item.getRegion() + self.stateGroup.setState({'start': region[0], 'stop': region[1]}) + self.update() + + +class EvalNode(Node): + """Return the output of a string evaluated/executed by the python interpreter. + The string may be either an expression or a python script, and inputs are accessed as the name of the terminal. + For expressions, a single value may be evaluated for a single output, or a dict for multiple outputs. + For a script, the text will be executed as the body of a function.""" + nodeName = 'PythonEval' + + def __init__(self, name): + Node.__init__(self, name, + terminals = { + 'input': {'io': 'in', 'renamable': True}, + 'output': {'io': 'out', 'renamable': True}, + }, + allowAddInput=True, allowAddOutput=True) + + self.ui = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + #self.addInBtn = QtGui.QPushButton('+Input') + #self.addOutBtn = QtGui.QPushButton('+Output') + self.text = QtGui.QTextEdit() + self.text.setTabStopWidth(30) + self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal") + #self.layout.addWidget(self.addInBtn, 0, 0) + #self.layout.addWidget(self.addOutBtn, 0, 1) + self.layout.addWidget(self.text, 1, 0, 1, 2) + self.ui.setLayout(self.layout) + + #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput) + #self.addInBtn.clicked.connect(self.addInput) + #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput) + #self.addOutBtn.clicked.connect(self.addOutput) + self.text.focusOutEvent = self.focusOutEvent + self.lastText = None + + def ctrlWidget(self): + return self.ui + + #def addInput(self): + #Node.addInput(self, 'input', renamable=True) + + #def addOutput(self): + #Node.addOutput(self, 'output', renamable=True) + + def focusOutEvent(self, ev): + text = str(self.text.toPlainText()) + if text != self.lastText: + self.lastText = text + print("eval node update") + self.update() + return QtGui.QTextEdit.focusOutEvent(self.text, ev) + + def process(self, display=True, **args): + l = locals() + l.update(args) + ## try eval first, then exec + try: + text = str(self.text.toPlainText()).replace('\n', ' ') + output = eval(text, globals(), l) + except SyntaxError: + fn = "def fn(**args):\n" + run = "\noutput=fn(**args)\n" + text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run + exec(text) + except: + print "Error processing node:", self.name() + raise + return output + + def saveState(self): + state = Node.saveState(self) + state['text'] = str(self.text.toPlainText()) + #state['terminals'] = self.saveTerminals() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + self.text.clear() + self.text.insertPlainText(state['text']) + self.restoreTerminals(state['terminals']) + self.update() + +class ColumnJoinNode(Node): + """Concatenates record arrays and/or adds new columns""" + nodeName = 'ColumnJoin' + + def __init__(self, name): + Node.__init__(self, name, terminals = { + 'output': {'io': 'out'}, + }) + + #self.items = [] + + self.ui = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + self.ui.setLayout(self.layout) + + self.tree = TreeWidget() + self.addInBtn = QtGui.QPushButton('+ Input') + self.remInBtn = QtGui.QPushButton('- Input') + + self.layout.addWidget(self.tree, 0, 0, 1, 2) + self.layout.addWidget(self.addInBtn, 1, 0) + self.layout.addWidget(self.remInBtn, 1, 1) + + self.addInBtn.clicked.connect(self.addInput) + self.remInBtn.clicked.connect(self.remInput) + self.tree.sigItemMoved.connect(self.update) + + def ctrlWidget(self): + return self.ui + + def addInput(self): + #print "ColumnJoinNode.addInput called." + term = Node.addInput(self, 'input', renamable=True, removable=True, multiable=True) + #print "Node.addInput returned. term:", term + item = QtGui.QTreeWidgetItem([term.name()]) + item.term = term + term.joinItem = item + #self.items.append((term, item)) + self.tree.addTopLevelItem(item) + + def remInput(self): + sel = self.tree.currentItem() + term = sel.term + term.joinItem = None + sel.term = None + self.tree.removeTopLevelItem(sel) + self.removeTerminal(term) + self.update() + + def process(self, display=True, **args): + order = self.order() + vals = [] + for name in order: + if name not in args: + continue + val = args[name] + if isinstance(val, np.ndarray) and len(val.dtype) > 0: + vals.append(val) + else: + vals.append((name, None, val)) + return {'output': functions.concatenateColumns(vals)} + + def order(self): + return [str(self.tree.topLevelItem(i).text(0)) for i in range(self.tree.topLevelItemCount())] + + def saveState(self): + state = Node.saveState(self) + state['order'] = self.order() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + inputs = self.inputs() + + ## Node.restoreState should have created all of the terminals we need + ## However: to maintain support for some older flowchart files, we need + ## to manually add any terminals that were not taken care of. + for name in [n for n in state['order'] if n not in inputs]: + Node.addInput(self, name, renamable=True, removable=True, multiable=True) + inputs = self.inputs() + + order = [name for name in state['order'] if name in inputs] + for name in inputs: + if name not in order: + order.append(name) + + self.tree.clear() + for name in order: + term = self[name] + item = QtGui.QTreeWidgetItem([name]) + item.term = term + term.joinItem = item + #self.items.append((term, item)) + self.tree.addTopLevelItem(item) + + def terminalRenamed(self, term, oldName): + Node.terminalRenamed(self, term, oldName) + item = term.joinItem + item.setText(0, term.name()) + self.update() + + diff --git a/pyqtgraph/flowchart/library/Display.py b/pyqtgraph/flowchart/library/Display.py new file mode 100644 index 00000000..7979d7a7 --- /dev/null +++ b/pyqtgraph/flowchart/library/Display.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +from ..Node import Node +import weakref +#from pyqtgraph import graphicsItems +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem +from pyqtgraph.graphicsItems.PlotCurveItem import PlotCurveItem +from pyqtgraph import PlotDataItem + +from .common import * +import numpy as np + +class PlotWidgetNode(Node): + """Connection to PlotWidget. Will plot arrays, metaarrays, and display event lists.""" + nodeName = 'PlotWidget' + sigPlotChanged = QtCore.Signal(object) + + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) + self.plot = None + self.items = {} + + def disconnected(self, localTerm, remoteTerm): + if localTerm is self.In and remoteTerm in self.items: + self.plot.removeItem(self.items[remoteTerm]) + del self.items[remoteTerm] + + def setPlot(self, plot): + #print "======set plot" + self.plot = plot + self.sigPlotChanged.emit(self) + + def getPlot(self): + return self.plot + + def process(self, In, display=True): + if display: + #self.plot.clearPlots() + items = set() + for name, vals in In.items(): + if vals is None: + continue + if type(vals) is not list: + vals = [vals] + + for val in vals: + vid = id(val) + if vid in self.items and self.items[vid].scene() is self.plot.scene(): + items.add(vid) + else: + #if isinstance(val, PlotCurveItem): + #self.plot.addItem(val) + #item = val + #if isinstance(val, ScatterPlotItem): + #self.plot.addItem(val) + #item = val + if isinstance(val, QtGui.QGraphicsItem): + self.plot.addItem(val) + item = val + else: + item = self.plot.plot(val) + self.items[vid] = item + items.add(vid) + for vid in list(self.items.keys()): + if vid not in items: + #print "remove", self.items[vid] + self.plot.removeItem(self.items[vid]) + del self.items[vid] + + def processBypassed(self, args): + for item in list(self.items.values()): + self.plot.removeItem(item) + self.items = {} + + #def setInput(self, **args): + #for k in args: + #self.plot.plot(args[k]) + + + +class CanvasNode(Node): + """Connection to a Canvas widget.""" + nodeName = 'CanvasWidget' + + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) + self.canvas = None + self.items = {} + + def disconnected(self, localTerm, remoteTerm): + if localTerm is self.In and remoteTerm in self.items: + self.canvas.removeItem(self.items[remoteTerm]) + del self.items[remoteTerm] + + def setCanvas(self, canvas): + self.canvas = canvas + + def getCanvas(self): + return self.canvas + + def process(self, In, display=True): + if display: + items = set() + for name, vals in In.items(): + if vals is None: + continue + if type(vals) is not list: + vals = [vals] + + for val in vals: + vid = id(val) + if vid in self.items: + items.add(vid) + else: + self.canvas.addItem(val) + item = val + self.items[vid] = item + items.add(vid) + for vid in list(self.items.keys()): + if vid not in items: + #print "remove", self.items[vid] + self.canvas.removeItem(self.items[vid]) + del self.items[vid] + + +class PlotCurve(CtrlNode): + """Generates a plot curve from x/y data""" + nodeName = 'PlotCurve' + uiTemplate = [ + ('color', 'color'), + ] + + def __init__(self, name): + CtrlNode.__init__(self, name, terminals={ + 'x': {'io': 'in'}, + 'y': {'io': 'in'}, + 'plot': {'io': 'out'} + }) + self.item = PlotDataItem() + + def process(self, x, y, display=True): + #print "scatterplot process" + if not display: + return {'plot': None} + + self.item.setData(x, y, pen=self.ctrls['color'].color()) + return {'plot': self.item} + + + + +class ScatterPlot(CtrlNode): + """Generates a scatter plot from a record array or nested dicts""" + nodeName = 'ScatterPlot' + uiTemplate = [ + ('x', 'combo', {'values': [], 'index': 0}), + ('y', 'combo', {'values': [], 'index': 0}), + ('sizeEnabled', 'check', {'value': False}), + ('size', 'combo', {'values': [], 'index': 0}), + ('absoluteSize', 'check', {'value': False}), + ('colorEnabled', 'check', {'value': False}), + ('color', 'colormap', {}), + ('borderEnabled', 'check', {'value': False}), + ('border', 'colormap', {}), + ] + + def __init__(self, name): + CtrlNode.__init__(self, name, terminals={ + 'input': {'io': 'in'}, + 'plot': {'io': 'out'} + }) + self.item = ScatterPlotItem() + self.keys = [] + + #self.ui = QtGui.QWidget() + #self.layout = QtGui.QGridLayout() + #self.ui.setLayout(self.layout) + + #self.xCombo = QtGui.QComboBox() + #self.yCombo = QtGui.QComboBox() + + + + def process(self, input, display=True): + #print "scatterplot process" + if not display: + return {'plot': None} + + self.updateKeys(input[0]) + + x = str(self.ctrls['x'].currentText()) + y = str(self.ctrls['y'].currentText()) + size = str(self.ctrls['size'].currentText()) + pen = QtGui.QPen(QtGui.QColor(0,0,0,0)) + points = [] + for i in input: + pt = {'pos': (i[x], i[y])} + if self.ctrls['sizeEnabled'].isChecked(): + pt['size'] = i[size] + if self.ctrls['borderEnabled'].isChecked(): + pt['pen'] = QtGui.QPen(self.ctrls['border'].getColor(i)) + else: + pt['pen'] = pen + if self.ctrls['colorEnabled'].isChecked(): + pt['brush'] = QtGui.QBrush(self.ctrls['color'].getColor(i)) + points.append(pt) + self.item.setPxMode(not self.ctrls['absoluteSize'].isChecked()) + + self.item.setPoints(points) + + return {'plot': self.item} + + + + def updateKeys(self, data): + if isinstance(data, dict): + keys = list(data.keys()) + elif isinstance(data, list) or isinstance(data, tuple): + keys = data + elif isinstance(data, np.ndarray) or isinstance(data, np.void): + keys = data.dtype.names + else: + print("Unknown data type:", type(data), data) + return + + for c in self.ctrls.values(): + c.blockSignals(True) + for c in [self.ctrls['x'], self.ctrls['y'], self.ctrls['size']]: + cur = str(c.currentText()) + c.clear() + for k in keys: + c.addItem(k) + if k == cur: + c.setCurrentIndex(c.count()-1) + for c in [self.ctrls['color'], self.ctrls['border']]: + c.setArgList(keys) + for c in self.ctrls.values(): + c.blockSignals(False) + + self.keys = keys + + + def saveState(self): + state = CtrlNode.saveState(self) + return {'keys': self.keys, 'ctrls': state} + + def restoreState(self, state): + self.updateKeys(state['keys']) + CtrlNode.restoreState(self, state['ctrls']) + +#class ImageItem(Node): + #"""Creates an ImageItem for display in a canvas from a file handle.""" + #nodeName = 'Image' + + #def __init__(self, name): + #Node.__init__(self, name, terminals={ + #'file': {'io': 'in'}, + #'image': {'io': 'out'} + #}) + #self.imageItem = graphicsItems.ImageItem() + #self.handle = None + + #def process(self, file, display=True): + #if not display: + #return {'image': None} + + #if file != self.handle: + #self.handle = file + #data = file.read() + #self.imageItem.updateImage(data) + + #pos = file. + + + \ No newline at end of file diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py new file mode 100644 index 00000000..090c261c --- /dev/null +++ b/pyqtgraph/flowchart/library/Filters.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from ..Node import Node +from scipy.signal import detrend +from scipy.ndimage import median_filter, gaussian_filter +#from pyqtgraph.SignalProxy import SignalProxy +from . import functions +from .common import * +import numpy as np + +import pyqtgraph.metaarray as metaarray + + +class Downsample(CtrlNode): + """Downsample by averaging samples together.""" + nodeName = 'Downsample' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + def processData(self, data): + return functions.downsample(data, self.ctrls['n'].value(), axis=0) + + +class Subsample(CtrlNode): + """Downsample by selecting every Nth sample.""" + nodeName = 'Subsample' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + def processData(self, data): + return data[::self.ctrls['n'].value()] + + +class Bessel(CtrlNode): + """Bessel filter. Input data must have time values.""" + nodeName = 'BesselFilter' + uiTemplate = [ + ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), + ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}), + ('bidir', 'check', {'checked': True}) + ] + + def processData(self, data): + s = self.stateGroup.state() + if s['band'] == 'lowpass': + mode = 'low' + else: + mode = 'high' + return functions.besselFilter(data, bidir=s['bidir'], btype=mode, cutoff=s['cutoff'], order=s['order']) + + +class Butterworth(CtrlNode): + """Butterworth filter""" + nodeName = 'ButterworthFilter' + uiTemplate = [ + ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), + ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('bidir', 'check', {'checked': True}) + ] + + def processData(self, data): + s = self.stateGroup.state() + if s['band'] == 'lowpass': + mode = 'low' + else: + mode = 'high' + ret = functions.butterworthFilter(data, bidir=s['bidir'], btype=mode, wPass=s['wPass'], wStop=s['wStop'], gPass=s['gPass'], gStop=s['gStop']) + return ret + + +class ButterworthNotch(CtrlNode): + """Butterworth notch filter""" + nodeName = 'ButterworthNotchFilter' + uiTemplate = [ + ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('bidir', 'check', {'checked': True}) + ] + + def processData(self, data): + s = self.stateGroup.state() + + low = functions.butterworthFilter(data, bidir=s['bidir'], btype='low', wPass=s['low_wPass'], wStop=s['low_wStop'], gPass=s['low_gPass'], gStop=s['low_gStop']) + high = functions.butterworthFilter(data, bidir=s['bidir'], btype='high', wPass=s['high_wPass'], wStop=s['high_wStop'], gPass=s['high_gPass'], gStop=s['high_gStop']) + return low + high + + +class Mean(CtrlNode): + """Filters data by taking the mean of a sliding window""" + nodeName = 'MeanFilter' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + n = self.ctrls['n'].value() + return functions.rollingSum(data, n) / n + + +class Median(CtrlNode): + """Filters data by taking the median of a sliding window""" + nodeName = 'MedianFilter' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + return median_filter(data, self.ctrls['n'].value()) + +class Mode(CtrlNode): + """Filters data by taking the mode (histogram-based) of a sliding window""" + nodeName = 'ModeFilter' + uiTemplate = [ + ('window', 'intSpin', {'value': 500, 'min': 1, 'max': 1000000}), + ] + + @metaArrayWrapper + def processData(self, data): + return functions.modeFilter(data, self.ctrls['window'].value()) + + +class Denoise(CtrlNode): + """Removes anomalous spikes from data, replacing with nearby values""" + nodeName = 'DenoiseFilter' + uiTemplate = [ + ('radius', 'intSpin', {'value': 2, 'min': 0, 'max': 1000000}), + ('threshold', 'doubleSpin', {'value': 4.0, 'min': 0, 'max': 1000}) + ] + + def processData(self, data): + #print "DENOISE" + s = self.stateGroup.state() + return functions.denoise(data, **s) + + +class Gaussian(CtrlNode): + """Gaussian smoothing filter.""" + nodeName = 'GaussianFilter' + uiTemplate = [ + ('sigma', 'doubleSpin', {'min': 0, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + return gaussian_filter(data, self.ctrls['sigma'].value()) + + +class Derivative(CtrlNode): + """Returns the pointwise derivative of the input""" + nodeName = 'DerivativeFilter' + + def processData(self, data): + if hasattr(data, 'implements') and data.implements('MetaArray'): + info = data.infoCopy() + if 'values' in info[0]: + info[0]['values'] = info[0]['values'][:-1] + return metaarray.MetaArray(data[1:] - data[:-1], info=info) + else: + return data[1:] - data[:-1] + + +class Integral(CtrlNode): + """Returns the pointwise integral of the input""" + nodeName = 'IntegralFilter' + + @metaArrayWrapper + def processData(self, data): + data[1:] += data[:-1] + return data + + +class Detrend(CtrlNode): + """Removes linear trend from the data""" + nodeName = 'DetrendFilter' + + @metaArrayWrapper + def processData(self, data): + return detrend(data) + + +class AdaptiveDetrend(CtrlNode): + """Removes baseline from data, ignoring anomalous events""" + nodeName = 'AdaptiveDetrend' + uiTemplate = [ + ('threshold', 'doubleSpin', {'value': 3.0, 'min': 0, 'max': 1000000}) + ] + + def processData(self, data): + return functions.adaptiveDetrend(data, threshold=self.ctrls['threshold'].value()) + +class HistogramDetrend(CtrlNode): + """Removes baseline from data by computing mode (from histogram) of beginning and end of data.""" + nodeName = 'HistogramDetrend' + uiTemplate = [ + ('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000, 'suffix': 'pts'}), + ('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000}), + ('offsetOnly', 'check', {'checked': False}), + ] + + def processData(self, data): + s = self.stateGroup.state() + #ws = self.ctrls['windowSize'].value() + #bn = self.ctrls['numBins'].value() + #offset = self.ctrls['offsetOnly'].checked() + return functions.histogramDetrend(data, window=s['windowSize'], bins=s['numBins'], offsetOnly=s['offsetOnly']) + + + +class RemovePeriodic(CtrlNode): + nodeName = 'RemovePeriodic' + uiTemplate = [ + #('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000, 'suffix': 'pts'}), + #('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000}) + ('f0', 'spin', {'value': 60, 'suffix': 'Hz', 'siPrefix': True, 'min': 0, 'max': None}), + ('harmonics', 'intSpin', {'value': 30, 'min': 0}), + ('samples', 'intSpin', {'value': 1, 'min': 1}), + ] + + def processData(self, data): + times = data.xvals('Time') + dt = times[1]-times[0] + + data1 = data.asarray() + ft = np.fft.fft(data1) + + ## determine frequencies in fft data + df = 1.0 / (len(data1) * dt) + freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft)) + + ## flatten spikes at f0 and harmonics + f0 = self.ctrls['f0'].value() + for i in xrange(1, self.ctrls['harmonics'].value()+2): + f = f0 * i # target frequency + + ## determine index range to check for this frequency + ind1 = int(np.floor(f / df)) + ind2 = int(np.ceil(f / df)) + (self.ctrls['samples'].value()-1) + if ind1 > len(ft)/2.: + break + mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5 + for j in range(ind1, ind2+1): + phase = np.angle(ft[j]) ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts. + re = mag * np.cos(phase) + im = mag * np.sin(phase) + ft[j] = re + im*1j + ft[len(ft)-j] = re - im*1j + + data2 = np.fft.ifft(ft).real + + ma = metaarray.MetaArray(data2, info=data.infoCopy()) + return ma + + + \ No newline at end of file diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py new file mode 100644 index 00000000..412af573 --- /dev/null +++ b/pyqtgraph/flowchart/library/Operators.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from ..Node import Node + +class UniOpNode(Node): + """Generic node for performing any operation like Out = In.fn()""" + def __init__(self, name, fn): + self.fn = fn + Node.__init__(self, name, terminals={ + 'In': {'io': 'in'}, + 'Out': {'io': 'out', 'bypass': 'In'} + }) + + def process(self, **args): + return {'Out': getattr(args['In'], self.fn)()} + +class BinOpNode(Node): + """Generic node for performing any operation like A.fn(B)""" + def __init__(self, name, fn): + self.fn = fn + Node.__init__(self, name, terminals={ + 'A': {'io': 'in'}, + 'B': {'io': 'in'}, + 'Out': {'io': 'out', 'bypass': 'A'} + }) + + def process(self, **args): + fn = getattr(args['A'], self.fn) + out = fn(args['B']) + if out is NotImplemented: + raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) + #print " ", fn, out + return {'Out': out} + + +class AbsNode(UniOpNode): + """Returns abs(Inp). Does not check input types.""" + nodeName = 'Abs' + def __init__(self, name): + UniOpNode.__init__(self, name, '__abs__') + +class AddNode(BinOpNode): + """Returns A + B. Does not check input types.""" + nodeName = 'Add' + def __init__(self, name): + BinOpNode.__init__(self, name, '__add__') + +class SubtractNode(BinOpNode): + """Returns A - B. Does not check input types.""" + nodeName = 'Subtract' + def __init__(self, name): + BinOpNode.__init__(self, name, '__sub__') + +class MultiplyNode(BinOpNode): + """Returns A * B. Does not check input types.""" + nodeName = 'Multiply' + def __init__(self, name): + BinOpNode.__init__(self, name, '__mul__') + +class DivideNode(BinOpNode): + """Returns A / B. Does not check input types.""" + nodeName = 'Divide' + def __init__(self, name): + BinOpNode.__init__(self, name, '__div__') + diff --git a/pyqtgraph/flowchart/library/__init__.py b/pyqtgraph/flowchart/library/__init__.py new file mode 100644 index 00000000..1e44edff --- /dev/null +++ b/pyqtgraph/flowchart/library/__init__.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.pgcollections import OrderedDict +from pyqtgraph import importModules +import os, types +from pyqtgraph.debug import printExc +from ..Node import Node +import pyqtgraph.reload as reload + + +NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses +NODE_TREE = OrderedDict() ## categorized tree of Node subclasses + +def getNodeType(name): + try: + return NODE_LIST[name] + except KeyError: + raise Exception("No node type called '%s'" % name) + +def getNodeTree(): + return NODE_TREE + +def registerNodeType(cls, paths, override=False): + """ + Register a new node type. If the type's name is already in use, + an exception will be raised (unless override=True). + + Arguments: + cls - a subclass of Node (must have typ.nodeName) + paths - list of tuples specifying the location(s) this + type will appear in the library tree. + override - if True, overwrite any class having the same name + """ + if not isNodeClass(cls): + raise Exception("Object %s is not a Node subclass" % str(cls)) + + name = cls.nodeName + if not override and name in NODE_LIST: + raise Exception("Node type name '%s' is already registered." % name) + + NODE_LIST[name] = cls + for path in paths: + root = NODE_TREE + for n in path: + if n not in root: + root[n] = OrderedDict() + root = root[n] + root[name] = cls + + + +def isNodeClass(cls): + try: + if not issubclass(cls, Node): + return False + except: + return False + return hasattr(cls, 'nodeName') + +def loadLibrary(reloadLibs=False, libPath=None): + """Import all Node subclasses found within files in the library module.""" + + global NODE_LIST, NODE_TREE + #if libPath is None: + #libPath = os.path.dirname(os.path.abspath(__file__)) + + if reloadLibs: + reload.reloadAll(libPath) + + mods = importModules('', globals(), locals()) + #for f in frozenSupport.listdir(libPath): + #pathName, ext = os.path.splitext(f) + #if ext not in ('.py', '.pyc') or '__init__' in pathName or '__pycache__' in pathName: + #continue + #try: + ##print "importing from", f + #mod = __import__(pathName, globals(), locals()) + #except: + #printExc("Error loading flowchart library %s:" % pathName) + #continue + + for name, mod in mods.items(): + nodes = [] + for n in dir(mod): + o = getattr(mod, n) + if isNodeClass(o): + #print " ", str(o) + registerNodeType(o, [(name,)], override=reloadLibs) + #nodes.append((o.nodeName, o)) + #if len(nodes) > 0: + #NODE_TREE[name] = OrderedDict(nodes) + #NODE_LIST.extend(nodes) + #NODE_LIST = OrderedDict(NODE_LIST) + +def reloadLibrary(): + loadLibrary(reloadLibs=True) + +loadLibrary() +#NODE_LIST = [] +#for o in locals().values(): + #if type(o) is type(AddNode) and issubclass(o, Node) and o is not Node and hasattr(o, 'nodeName'): + #NODE_LIST.append((o.nodeName, o)) +#NODE_LIST.sort(lambda a,b: cmp(a[0], b[0])) +#NODE_LIST = OrderedDict(NODE_LIST) \ No newline at end of file diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py new file mode 100644 index 00000000..65f8c1fd --- /dev/null +++ b/pyqtgraph/flowchart/library/common.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.widgets.SpinBox import SpinBox +#from pyqtgraph.SignalProxy import SignalProxy +from pyqtgraph.WidgetGroup import WidgetGroup +#from ColorMapper import ColorMapper +from ..Node import Node +import numpy as np +from pyqtgraph.widgets.ColorButton import ColorButton +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + +def generateUi(opts): + """Convenience function for generating common UI types""" + widget = QtGui.QWidget() + l = QtGui.QFormLayout() + l.setSpacing(0) + widget.setLayout(l) + ctrls = {} + row = 0 + for opt in opts: + if len(opt) == 2: + k, t = opt + o = {} + elif len(opt) == 3: + k, t, o = opt + else: + raise Exception("Widget specification must be (name, type) or (name, type, {opts})") + if t == 'intSpin': + w = QtGui.QSpinBox() + if 'max' in o: + w.setMaximum(o['max']) + if 'min' in o: + w.setMinimum(o['min']) + if 'value' in o: + w.setValue(o['value']) + elif t == 'doubleSpin': + w = QtGui.QDoubleSpinBox() + if 'max' in o: + w.setMaximum(o['max']) + if 'min' in o: + w.setMinimum(o['min']) + if 'value' in o: + w.setValue(o['value']) + elif t == 'spin': + w = SpinBox() + w.setOpts(**o) + elif t == 'check': + w = QtGui.QCheckBox() + if 'checked' in o: + w.setChecked(o['checked']) + elif t == 'combo': + w = QtGui.QComboBox() + for i in o['values']: + w.addItem(i) + #elif t == 'colormap': + #w = ColorMapper() + elif t == 'color': + w = ColorButton() + else: + raise Exception("Unknown widget type '%s'" % str(t)) + if 'tip' in o: + w.setToolTip(o['tip']) + w.setObjectName(k) + l.addRow(k, w) + if o.get('hidden', False): + w.hide() + label = l.labelForField(w) + label.hide() + + ctrls[k] = w + w.rowNum = row + row += 1 + group = WidgetGroup(widget) + return widget, group, ctrls + + +class CtrlNode(Node): + """Abstract class for nodes with auto-generated control UI""" + + sigStateChanged = QtCore.Signal(object) + + def __init__(self, name, ui=None, terminals=None): + if ui is None: + if hasattr(self, 'uiTemplate'): + ui = self.uiTemplate + else: + ui = [] + if terminals is None: + terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} + Node.__init__(self, name=name, terminals=terminals) + + self.ui, self.stateGroup, self.ctrls = generateUi(ui) + self.stateGroup.sigChanged.connect(self.changed) + + def ctrlWidget(self): + return self.ui + + def changed(self): + self.update() + self.sigStateChanged.emit(self) + + def process(self, In, display=True): + out = self.processData(In) + return {'Out': out} + + def saveState(self): + state = Node.saveState(self) + state['ctrl'] = self.stateGroup.state() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + if self.stateGroup is not None: + self.stateGroup.setState(state.get('ctrl', {})) + + def hideRow(self, name): + w = self.ctrls[name] + l = self.ui.layout().labelForField(w) + w.hide() + l.hide() + + def showRow(self, name): + w = self.ctrls[name] + l = self.ui.layout().labelForField(w) + w.show() + l.show() + + + +def metaArrayWrapper(fn): + def newFn(self, data, *args, **kargs): + if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + d1 = fn(self, data.view(np.ndarray), *args, **kargs) + info = data.infoCopy() + if d1.shape != data.shape: + for i in range(data.ndim): + if 'values' in info[i]: + info[i]['values'] = info[i]['values'][:d1.shape[i]] + return metaarray.MetaArray(d1, info=info) + else: + return fn(self, data, *args, **kargs) + return newFn + diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py new file mode 100644 index 00000000..0476e02f --- /dev/null +++ b/pyqtgraph/flowchart/library/functions.py @@ -0,0 +1,336 @@ +import scipy +import numpy as np +from pyqtgraph.metaarray import MetaArray + +def downsample(data, n, axis=0, xvals='subsample'): + """Downsample by averaging points together across axis. + If multiple axes are specified, runs once per axis. + If a metaArray is given, then the axis values can be either subsampled + or downsampled to match. + """ + ma = None + if (hasattr(data, 'implements') and data.implements('MetaArray')): + ma = data + data = data.view(np.ndarray) + + + if hasattr(axis, '__len__'): + if not hasattr(n, '__len__'): + n = [n]*len(axis) + for i in range(len(axis)): + data = downsample(data, n[i], axis[i]) + return data + + nPts = int(data.shape[axis] / n) + s = list(data.shape) + s[axis] = nPts + s.insert(axis+1, n) + sl = [slice(None)] * data.ndim + sl[axis] = slice(0, nPts*n) + d1 = data[tuple(sl)] + #print d1.shape, s + d1.shape = tuple(s) + d2 = d1.mean(axis+1) + + if ma is None: + return d2 + else: + info = ma.infoCopy() + if 'values' in info[axis]: + if xvals == 'subsample': + info[axis]['values'] = info[axis]['values'][::n][:nPts] + elif xvals == 'downsample': + info[axis]['values'] = downsample(info[axis]['values'], n) + return MetaArray(d2, info=info) + + +def applyFilter(data, b, a, padding=100, bidir=True): + """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering + and/or run the filter in both directions.""" + d1 = data.view(np.ndarray) + + if padding > 0: + d1 = np.hstack([d1[:padding], d1, d1[-padding:]]) + + if bidir: + d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1] + else: + d1 = scipy.signal.lfilter(b, a, d1) + + if padding > 0: + d1 = d1[padding:-padding] + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return MetaArray(d1, info=data.infoCopy()) + else: + return d1 + +def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): + """return data passed through bessel filter""" + if dt is None: + try: + tvals = data.xvals('Time') + dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) + except: + dt = 1.0 + + b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype) + + return applyFilter(data, b, a, bidir=bidir) + #base = data.mean() + #d1 = scipy.signal.lfilter(b, a, data.view(ndarray)-base) + base + #if (hasattr(data, 'implements') and data.implements('MetaArray')): + #return MetaArray(d1, info=data.infoCopy()) + #return d1 + +def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True): + """return data passed through bessel filter""" + if dt is None: + try: + tvals = data.xvals('Time') + dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) + except: + dt = 1.0 + + if wStop is None: + wStop = wPass * 2.0 + ord, Wn = scipy.signal.buttord(wPass*dt*2., wStop*dt*2., gPass, gStop) + #print "butterworth ord %f Wn %f c %f sc %f" % (ord, Wn, cutoff, stopCutoff) + b,a = scipy.signal.butter(ord, Wn, btype=btype) + + return applyFilter(data, b, a, bidir=bidir) + + +def rollingSum(data, n): + d1 = data.copy() + d1[1:] += d1[:-1] # integrate + d2 = np.empty(len(d1) - n + 1, dtype=data.dtype) + d2[0] = d1[n-1] # copy first point + d2[1:] = d1[n:] - d1[:-n] # subtract + return d2 + + +def mode(data, bins=None): + """Returns location max value from histogram.""" + if bins is None: + bins = int(len(data)/10.) + if bins < 2: + bins = 2 + y, x = np.histogram(data, bins=bins) + ind = np.argmax(y) + mode = 0.5 * (x[ind] + x[ind+1]) + return mode + +def modeFilter(data, window=500, step=None, bins=None): + """Filter based on histogram-based mode function""" + d1 = data.view(np.ndarray) + vals = [] + l2 = int(window/2.) + if step is None: + step = l2 + i = 0 + while True: + if i > len(data)-step: + break + vals.append(mode(d1[i:i+window], bins)) + i += step + + chunks = [np.linspace(vals[0], vals[0], l2)] + for i in range(len(vals)-1): + chunks.append(np.linspace(vals[i], vals[i+1], step)) + remain = len(data) - step*(len(vals)-1) - l2 + chunks.append(np.linspace(vals[-1], vals[-1], remain)) + d2 = np.hstack(chunks) + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return MetaArray(d2, info=data.infoCopy()) + return d2 + +def denoise(data, radius=2, threshold=4): + """Very simple noise removal function. Compares a point to surrounding points, + replaces with nearby values if the difference is too large.""" + + + r2 = radius * 2 + d1 = data.view(np.ndarray) + d2 = d1[radius:] - d1[:-radius] #a derivative + #d3 = data[r2:] - data[:-r2] + #d4 = d2 - d3 + stdev = d2.std() + #print "denoise: stdev of derivative:", stdev + mask1 = d2 > stdev*threshold #where derivative is large and positive + mask2 = d2 < -stdev*threshold #where derivative is large and negative + maskpos = mask1[:-radius] * mask2[radius:] #both need to be true + maskneg = mask1[radius:] * mask2[:-radius] + mask = maskpos + maskneg + d5 = np.where(mask, d1[:-r2], d1[radius:-radius]) #where both are true replace the value with the value from 2 points before + d6 = np.empty(d1.shape, dtype=d1.dtype) #add points back to the ends + d6[radius:-radius] = d5 + d6[:radius] = d1[:radius] + d6[-radius:] = d1[-radius:] + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return MetaArray(d6, info=data.infoCopy()) + return d6 + +def adaptiveDetrend(data, x=None, threshold=3.0): + """Return the signal with baseline removed. Discards outliers from baseline measurement.""" + if x is None: + x = data.xvals(0) + + d = data.view(np.ndarray) + + d2 = scipy.signal.detrend(d) + + stdev = d2.std() + mask = abs(d2) < stdev*threshold + #d3 = where(mask, 0, d2) + #d4 = d2 - lowPass(d3, cutoffs[1], dt=dt) + + lr = stats.linregress(x[mask], d[mask]) + base = lr[1] + lr[0]*x + d4 = d - base + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return MetaArray(d4, info=data.infoCopy()) + return d4 + + +def histogramDetrend(data, window=500, bins=50, threshold=3.0, offsetOnly=False): + """Linear detrend. Works by finding the most common value at the beginning and end of a trace, excluding outliers. + If offsetOnly is True, then only the offset from the beginning of the trace is subtracted. + """ + + d1 = data.view(np.ndarray) + d2 = [d1[:window], d1[-window:]] + v = [0, 0] + for i in [0, 1]: + d3 = d2[i] + stdev = d3.std() + mask = abs(d3-np.median(d3)) < stdev*threshold + d4 = d3[mask] + y, x = np.histogram(d4, bins=bins) + ind = np.argmax(y) + v[i] = 0.5 * (x[ind] + x[ind+1]) + + if offsetOnly: + d3 = data.view(np.ndarray) - v[0] + else: + base = np.linspace(v[0], v[1], len(data)) + d3 = data.view(np.ndarray) - base + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return MetaArray(d3, info=data.infoCopy()) + return d3 + +def concatenateColumns(data): + """Returns a single record array with columns taken from the elements in data. + data should be a list of elements, which can be either record arrays or tuples (name, type, data) + """ + + ## first determine dtype + dtype = [] + names = set() + maxLen = 0 + for element in data: + if isinstance(element, np.ndarray): + ## use existing columns + for i in range(len(element.dtype)): + name = element.dtype.names[i] + dtype.append((name, element.dtype[i])) + maxLen = max(maxLen, len(element)) + else: + name, type, d = element + if type is None: + type = suggestDType(d) + dtype.append((name, type)) + if isinstance(d, list) or isinstance(d, np.ndarray): + maxLen = max(maxLen, len(d)) + if name in names: + raise Exception('Name "%s" repeated' % name) + names.add(name) + + + + ## create empty array + out = np.empty(maxLen, dtype) + + ## fill columns + for element in data: + if isinstance(element, np.ndarray): + for i in range(len(element.dtype)): + name = element.dtype.names[i] + try: + out[name] = element[name] + except: + print("Column:", name) + print("Input shape:", element.shape, element.dtype) + print("Output shape:", out.shape, out.dtype) + raise + else: + name, type, d = element + out[name] = d + + return out + +def suggestDType(x): + """Return a suitable dtype for x""" + if isinstance(x, list) or isinstance(x, tuple): + if len(x) == 0: + raise Exception('can not determine dtype for empty list') + x = x[0] + + if hasattr(x, 'dtype'): + return x.dtype + elif isinstance(x, float): + return float + elif isinstance(x, int): + return int + #elif isinstance(x, basestring): ## don't try to guess correct string length; use object instead. + #return ' len(ft)/2.: + break + mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5 + for j in range(ind1, ind2+1): + phase = np.angle(ft[j]) ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts. + re = mag * np.cos(phase) + im = mag * np.sin(phase) + ft[j] = re + im*1j + ft[len(ft)-j] = re - im*1j + + data2 = np.fft.ifft(ft).real + + if (hasattr(data, 'implements') and data.implements('MetaArray')): + return metaarray.MetaArray(data2, info=data.infoCopy()) + else: + return data2 + + + \ No newline at end of file diff --git a/pyqtgraph/frozenSupport.py b/pyqtgraph/frozenSupport.py new file mode 100644 index 00000000..385bb435 --- /dev/null +++ b/pyqtgraph/frozenSupport.py @@ -0,0 +1,52 @@ +## Definitions helpful in frozen environments (eg py2exe) +import os, sys, zipfile + +def listdir(path): + """Replacement for os.listdir that works in frozen environments.""" + if not hasattr(sys, 'frozen'): + return os.listdir(path) + + (zipPath, archivePath) = splitZip(path) + if archivePath is None: + return os.listdir(path) + + with zipfile.ZipFile(zipPath, "r") as zipobj: + contents = zipobj.namelist() + results = set() + for name in contents: + # components in zip archive paths are always separated by forward slash + if name.startswith(archivePath) and len(name) > len(archivePath): + name = name[len(archivePath):].split('/')[0] + results.add(name) + return list(results) + +def isdir(path): + """Replacement for os.path.isdir that works in frozen environments.""" + if not hasattr(sys, 'frozen'): + return os.path.isdir(path) + + (zipPath, archivePath) = splitZip(path) + if archivePath is None: + return os.path.isdir(path) + with zipfile.ZipFile(zipPath, "r") as zipobj: + contents = zipobj.namelist() + archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end + for c in contents: + if c.startswith(archivePath): + return True + return False + + +def splitZip(path): + """Splits a path containing a zip file into (zipfile, subpath). + If there is no zip file, returns (path, None)""" + components = os.path.normpath(path).split(os.sep) + for index, component in enumerate(components): + if component.endswith('.zip'): + zipPath = os.sep.join(components[0:index+1]) + archivePath = ''.join([x+'/' for x in components[index+1:]]) + return (zipPath, archivePath) + else: + return (path, None) + + \ No newline at end of file diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py new file mode 100644 index 00000000..23b2580c --- /dev/null +++ b/pyqtgraph/functions.py @@ -0,0 +1,1874 @@ +# -*- coding: utf-8 -*- +""" +functions.py - Miscellaneous functions with no other home +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from .python2_3 import asUnicode +Colors = { + 'b': (0,0,255,255), + 'g': (0,255,0,255), + 'r': (255,0,0,255), + 'c': (0,255,255,255), + 'm': (255,0,255,255), + 'y': (255,255,0,255), + 'k': (0,0,0,255), + 'w': (255,255,255,255), +} + +SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') +SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' + + + +from .Qt import QtGui, QtCore, USE_PYSIDE +import numpy as np +import decimal, re +import ctypes + +try: + import scipy.ndimage + HAVE_SCIPY = True + try: + import scipy.weave + USE_WEAVE = True + except: + USE_WEAVE = False +except ImportError: + HAVE_SCIPY = False + +from . import debug + +def siScale(x, minVal=1e-25, allowUnicode=True): + """ + Return the recommended scale factor and SI prefix string for x. + + Example:: + + siScale(0.0001) # returns (1e6, 'μ') + # This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits + """ + + if isinstance(x, decimal.Decimal): + x = float(x) + + try: + if np.isnan(x) or np.isinf(x): + return(1, '') + except: + print(x, type(x)) + raise + if abs(x) < minVal: + m = 0 + x = 0 + else: + m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) + + if m == 0: + pref = '' + elif m < -8 or m > 8: + pref = 'e%d' % (m*3) + else: + if allowUnicode: + pref = SI_PREFIXES[m+8] + else: + pref = SI_PREFIXES_ASCII[m+8] + p = .001**m + + return (p, pref) + +def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): + """ + Return the number x formatted in engineering notation with SI prefix. + + Example:: + siFormat(0.0001, suffix='V') # returns "100 μV" + """ + + if space is True: + space = ' ' + if space is False: + space = '' + + + (p, pref) = siScale(x, minVal, allowUnicode) + if not (len(pref) > 0 and pref[0] == 'e'): + pref = space + pref + + if error is None: + fmt = "%." + str(precision) + "g%s%s" + return fmt % (x*p, pref, suffix) + else: + if allowUnicode: + plusminus = space + asUnicode("±") + space + else: + plusminus = " +/- " + fmt = "%." + str(precision) + "g%s%s%s%s" + return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) + +def siEval(s): + """ + Convert a value written in SI notation to its equivalent prefixless value + + Example:: + + siEval("100 μV") # returns 0.0001 + """ + + s = asUnicode(s) + m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s) + if m is None: + raise Exception("Can't convert string '%s' to number." % s) + v = float(m.groups()[0]) + p = m.groups()[6] + #if p not in SI_PREFIXES: + #raise Exception("Can't convert string '%s' to number--unknown prefix." % s) + if p == '': + n = 0 + elif p == 'u': + n = -2 + else: + n = SI_PREFIXES.index(p) - 8 + return v * 1000**n + + +class Color(QtGui.QColor): + def __init__(self, *args): + QtGui.QColor.__init__(self, mkColor(*args)) + + def glColor(self): + """Return (r,g,b,a) normalized for use in opengl""" + return (self.red()/255., self.green()/255., self.blue()/255., self.alpha()/255.) + + def __getitem__(self, ind): + return (self.red, self.green, self.blue, self.alpha)[ind]() + + +def mkColor(*args): + """ + Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: + + ================ ================================================ + 'c' one of: r, g, b, c, m, y, k, w + R, G, B, [A] integers 0-255 + (R, G, B, [A]) tuple of integers 0-255 + float greyscale, 0.0-1.0 + int see :func:`intColor() ` + (int, hues) see :func:`intColor() ` + "RGB" hexadecimal strings; may begin with '#' + "RGBA" + "RRGGBB" + "RRGGBBAA" + QColor QColor instance; makes a copy. + ================ ================================================ + """ + err = 'Not sure how to make a color from "%s"' % str(args) + if len(args) == 1: + if isinstance(args[0], QtGui.QColor): + return QtGui.QColor(args[0]) + elif isinstance(args[0], float): + r = g = b = int(args[0] * 255) + a = 255 + elif isinstance(args[0], basestring): + c = args[0] + if c[0] == '#': + c = c[1:] + if len(c) == 1: + (r, g, b, a) = Colors[c] + if len(c) == 3: + r = int(c[0]*2, 16) + g = int(c[1]*2, 16) + b = int(c[2]*2, 16) + a = 255 + elif len(c) == 4: + r = int(c[0]*2, 16) + g = int(c[1]*2, 16) + b = int(c[2]*2, 16) + a = int(c[3]*2, 16) + elif len(c) == 6: + r = int(c[0:2], 16) + g = int(c[2:4], 16) + b = int(c[4:6], 16) + a = 255 + elif len(c) == 8: + r = int(c[0:2], 16) + g = int(c[2:4], 16) + b = int(c[4:6], 16) + a = int(c[6:8], 16) + elif hasattr(args[0], '__len__'): + if len(args[0]) == 3: + (r, g, b) = args[0] + a = 255 + elif len(args[0]) == 4: + (r, g, b, a) = args[0] + elif len(args[0]) == 2: + return intColor(*args[0]) + else: + raise Exception(err) + elif type(args[0]) == int: + return intColor(args[0]) + else: + raise Exception(err) + elif len(args) == 3: + (r, g, b) = args + a = 255 + elif len(args) == 4: + (r, g, b, a) = args + else: + raise Exception(err) + + args = [r,g,b,a] + args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] + args = list(map(int, args)) + return QtGui.QColor(*args) + + +def mkBrush(*args, **kwds): + """ + | Convenience function for constructing Brush. + | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() ` + | Calling mkBrush(None) returns an invisible brush. + """ + if 'color' in kwds: + color = kwds['color'] + elif len(args) == 1: + arg = args[0] + if arg is None: + return QtGui.QBrush(QtCore.Qt.NoBrush) + elif isinstance(arg, QtGui.QBrush): + return QtGui.QBrush(arg) + else: + color = arg + elif len(args) > 1: + color = args + return QtGui.QBrush(mkColor(color)) + +def mkPen(*args, **kargs): + """ + Convenience function for constructing QPen. + + Examples:: + + mkPen(color) + mkPen(color, width=2) + mkPen(cosmetic=False, width=4.5, color='r') + mkPen({'color': "FF0", width: 2}) + mkPen(None) # (no pen) + + In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() ` """ + + color = kargs.get('color', None) + width = kargs.get('width', 1) + style = kargs.get('style', None) + cosmetic = kargs.get('cosmetic', True) + hsv = kargs.get('hsv', None) + + if len(args) == 1: + arg = args[0] + if isinstance(arg, dict): + return mkPen(**arg) + if isinstance(arg, QtGui.QPen): + return QtGui.QPen(arg) ## return a copy of this pen + elif arg is None: + style = QtCore.Qt.NoPen + else: + color = arg + if len(args) > 1: + color = args + + if color is None: + color = mkColor(200, 200, 200) + if hsv is not None: + color = hsvColor(*hsv) + else: + color = mkColor(color) + + pen = QtGui.QPen(QtGui.QBrush(color), width) + pen.setCosmetic(cosmetic) + if style is not None: + pen.setStyle(style) + return pen + +def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): + """Generate a QColor from HSVa values. (all arguments are float 0.0-1.0)""" + c = QtGui.QColor() + c.setHsvF(hue, sat, val, alpha) + return c + + +def colorTuple(c): + """Return a tuple (R,G,B,A) from a QColor""" + return (c.red(), c.green(), c.blue(), c.alpha()) + +def colorStr(c): + """Generate a hex string code from a QColor""" + return ('%02x'*4) % colorTuple(c) + +def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): + """ + Creates a QColor from a single index. Useful for stepping through a predefined list of colors. + + The argument *index* determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be + + Colors are chosen by cycling across hues while varying the value (brightness). + By default, this selects from a list of 9 hues.""" + hues = int(hues) + values = int(values) + ind = int(index) % (hues * values) + indh = ind % hues + indv = ind / hues + if values > 1: + v = minValue + indv * ((maxValue-minValue) / (values-1)) + else: + v = maxValue + h = minHue + (indh * (maxHue-minHue)) / hues + + c = QtGui.QColor() + c.setHsv(h, sat, v) + c.setAlpha(alpha) + return c + +def glColor(*args, **kargs): + """ + Convert a color to OpenGL color format (r,g,b,a) floats 0.0-1.0 + Accepts same arguments as :func:`mkColor `. + """ + c = mkColor(*args, **kargs) + return (c.red()/255., c.green()/255., c.blue()/255., c.alpha()/255.) + + + +def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): + """ + Construct a path outlining an arrow with the given dimensions. + The arrow points in the -x direction with tip positioned at 0,0. + If *tipAngle* is supplied (in degrees), it overrides *headWidth*. + If *tailLen* is None, no tail will be drawn. + """ + headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.) + path = QtGui.QPainterPath() + path.moveTo(0,0) + path.lineTo(headLen, -headWidth) + if tailLen is None: + innerY = headLen - headWidth * np.tan(baseAngle*np.pi/180.) + path.lineTo(innerY, 0) + else: + tailWidth *= 0.5 + innerY = headLen - (headWidth-tailWidth) * np.tan(baseAngle*np.pi/180.) + path.lineTo(innerY, -tailWidth) + path.lineTo(headLen + tailLen, -tailWidth) + path.lineTo(headLen + tailLen, tailWidth) + path.lineTo(innerY, tailWidth) + path.lineTo(headLen, headWidth) + path.lineTo(0,0) + return path + + + +def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): + """ + Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. + + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this). + + For a graphical interface to this function, see :func:`ROI.getArrayRegion ` + + ============== ==================================================================================================== + Arguments: + *data* (ndarray) the original dataset + *shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape)) + *origin* the location in the original dataset that will become the origin of the sliced data. + *vectors* list of unit vectors which point in the direction of the slice axes. Each vector must have the same + length as *axes*. If the vectors are not unit length, the result will be scaled relative to the + original data. If the vectors are not orthogonal, the result will be sheared relative to the + original data. + *axes* The axes in the original dataset which correspond to the slice *vectors* + *order* The order of spline interpolation. Default is 1 (linear). See scipy.ndimage.map_coordinates + for more information. + *returnCoords* If True, return a tuple (result, coords) where coords is the array of coordinates used to select + values from the original dataset. + *All extra keyword arguments are passed to scipy.ndimage.map_coordinates.* + -------------------------------------------------------------------------------------------------------------------- + ============== ==================================================================================================== + + Note the following must be true: + + | len(shape) == len(vectors) + | len(origin) == len(axes) == len(vectors[i]) + + Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes + + * data = array with dims (time, x, y, z) = (100, 40, 40, 40) + * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) + * The origin of the slice will be at (x,y,z) = (40, 0, 0) + * We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) + + The call for this example would look like:: + + affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) + + """ + if not HAVE_SCIPY: + raise Exception("This function requires the scipy library, but it does not appear to be importable.") + + # sanity check + if len(shape) != len(vectors): + raise Exception("shape and vectors must have same length.") + if len(origin) != len(axes): + raise Exception("origin and axes must have same length.") + for v in vectors: + if len(v) != len(axes): + raise Exception("each vector must be same length as axes.") + + shape = list(map(np.ceil, shape)) + + ## transpose data so slice axes come first + trAx = list(range(data.ndim)) + for x in axes: + trAx.remove(x) + tr1 = tuple(axes) + tuple(trAx) + data = data.transpose(tr1) + #print "tr1:", tr1 + ## dims are now [(slice axes), (other axes)] + + + ## make sure vectors are arrays + if not isinstance(vectors, np.ndarray): + vectors = np.array(vectors) + if not isinstance(origin, np.ndarray): + origin = np.array(origin) + origin.shape = (len(axes),) + (1,)*len(shape) + + ## Build array of sample locations. + grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes + #print shape, grid.shape + x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic + x += origin + #print "X values:" + #print x + ## iterate manually over unused axes since map_coordinates won't do it for us + extraShape = data.shape[len(axes):] + output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) + for inds in np.ndindex(*extraShape): + ind = (Ellipsis,) + inds + #print data[ind].shape, x.shape, output[ind].shape, output.shape + output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs) + + tr = list(range(output.ndim)) + trb = [] + for i in range(min(axes)): + ind = tr1.index(i) + (len(shape)-len(axes)) + tr.remove(ind) + trb.append(ind) + tr2 = tuple(trb+tr) + + ## Untranspose array before returning + output = output.transpose(tr2) + if returnCoords: + return (output, x) + else: + return output + +def transformToArray(tr): + """ + Given a QTransform, return a 3x3 numpy array. + Given a QMatrix4x4, return a 4x4 numpy array. + + Example: map an array of x,y coordinates through a transform:: + + ## coordinates to map are (1,5), (2,6), (3,7), and (4,8) + coords = np.array([[1,2,3,4], [5,6,7,8], [1,1,1,1]]) # the extra '1' coordinate is needed for translation to work + + ## Make an example transform + tr = QtGui.QTransform() + tr.translate(3,4) + tr.scale(2, 0.1) + + ## convert to array + m = pg.transformToArray()[:2] # ignore the perspective portion of the transformation + + ## map coordinates through transform + mapped = np.dot(m, coords) + """ + #return np.array([[tr.m11(), tr.m12(), tr.m13()],[tr.m21(), tr.m22(), tr.m23()],[tr.m31(), tr.m32(), tr.m33()]]) + ## The order of elements given by the method names m11..m33 is misleading-- + ## It is most common for x,y translation to occupy the positions 1,3 and 2,3 in + ## a transformation matrix. However, with QTransform these values appear at m31 and m32. + ## So the correct interpretation is transposed: + if isinstance(tr, QtGui.QTransform): + return np.array([[tr.m11(), tr.m21(), tr.m31()], [tr.m12(), tr.m22(), tr.m32()], [tr.m13(), tr.m23(), tr.m33()]]) + elif isinstance(tr, QtGui.QMatrix4x4): + return np.array(tr.copyDataTo()).reshape(4,4) + else: + raise Exception("Transform argument must be either QTransform or QMatrix4x4.") + +def transformCoordinates(tr, coords, transpose=False): + """ + Map a set of 2D or 3D coordinates through a QTransform or QMatrix4x4. + The shape of coords must be (2,...) or (3,...) + The mapping will _ignore_ any perspective transformations. + + For coordinate arrays with ndim=2, this is basically equivalent to matrix multiplication. + Most arrays, however, prefer to put the coordinate axis at the end (eg. shape=(...,3)). To + allow this, use transpose=True. + + """ + + if transpose: + ## move last axis to beginning. This transposition will be reversed before returning the mapped coordinates. + coords = coords.transpose((coords.ndim-1,) + tuple(range(0,coords.ndim-1))) + + nd = coords.shape[0] + if isinstance(tr, np.ndarray): + m = tr + else: + m = transformToArray(tr) + m = m[:m.shape[0]-1] # remove perspective + + ## If coords are 3D and tr is 2D, assume no change for Z axis + if m.shape == (2,3) and nd == 3: + m2 = np.zeros((3,4)) + m2[:2, :2] = m[:2,:2] + m2[:2, 3] = m[:2,2] + m2[2,2] = 1 + m = m2 + + ## if coords are 2D and tr is 3D, ignore Z axis + if m.shape == (3,4) and nd == 2: + m2 = np.empty((2,3)) + m2[:,:2] = m[:2,:2] + m2[:,2] = m[:2,3] + m = m2 + + ## reshape tr and coords to prepare for multiplication + m = m.reshape(m.shape + (1,)*(coords.ndim-1)) + coords = coords[np.newaxis, ...] + + # separate scale/rotate and translation + translate = m[:,-1] + m = m[:, :-1] + + ## map coordinates and return + mapped = (m*coords).sum(axis=1) ## apply scale/rotate + mapped += translate + + if transpose: + ## move first axis to end. + mapped = mapped.transpose(tuple(range(1,mapped.ndim)) + (0,)) + return mapped + + + + +def solve3DTransform(points1, points2): + """ + Find a 3D transformation matrix that maps points1 onto points2 + points must be specified as a list of 4 Vectors. + """ + if not HAVE_SCIPY: + raise Exception("This function depends on the scipy library, but it does not appear to be importable.") + A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)]) + B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)]) + + ## solve 3 sets of linear equations to determine transformation matrix elements + matrix = np.zeros((4,4)) + for i in range(3): + matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix + + return matrix + +def solveBilinearTransform(points1, points2): + """ + Find a bilinear transformation matrix (2x4) that maps points1 onto points2 + points must be specified as a list of 4 Vector, Point, QPointF, etc. + + To use this matrix to map a point [x,y]:: + + mapped = np.dot(matrix, [x*y, x, y, 1]) + """ + if not HAVE_SCIPY: + raise Exception("This function depends on the scipy library, but it does not appear to be importable.") + ## A is 4 rows (points) x 4 columns (xy, x, y, 1) + ## B is 4 rows (points) x 2 columns (x, y) + A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)]) + B = np.array([[points2[i].x(), points2[i].y()] for i in range(4)]) + + ## solve 2 sets of linear equations to determine transformation matrix elements + matrix = np.zeros((2,4)) + for i in range(2): + matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix + + return matrix + +def rescaleData(data, scale, offset, dtype=None): + """Return data rescaled and optionally cast to a new dtype:: + + data => (data-offset) * scale + + Uses scipy.weave (if available) to improve performance. + """ + global USE_WEAVE + if dtype is None: + dtype = data.dtype + + try: + if not USE_WEAVE: + raise Exception('Weave is disabled; falling back to slower version.') + + newData = np.empty((data.size,), dtype=dtype) + flat = np.ascontiguousarray(data).reshape(data.size) + size = data.size + + code = """ + double sc = (double)scale; + double off = (double)offset; + for( int i=0; i0 and max->*scale*:: + + rescaled = (clip(data, min, max) - min) * (*scale* / (max - min)) + + It is also possible to use a 2D (N,2) array of values for levels. In this case, + it is assumed that each pair of min,max values in the levels array should be + applied to a different subset of the input data (for example, the input data may + already have RGB values and the levels are used to independently scale each + channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. + scale The maximum value to which data will be rescaled before being passed through the + lookup table (or returned if there is no lookup table). By default this will + be set to the length of the lookup table, or 256 is no lookup table is provided. + For OpenGL color specifications (as in GLColor4f) use scale=1.0 + lut Optional lookup table (array with dtype=ubyte). + Values in data will be converted to color by indexing directly from lut. + The output data shape will be input.shape + lut.shape[1:]. + + Note: the output of makeARGB will have the same dtype as the lookup table, so + for conversion to QImage, the dtype must be ubyte. + + Lookup tables can be built using GradientWidget. + useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). + The default is False, which returns in ARGB order for use with QImage + (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + is BGRA). + ============ ================================================================================== + """ + prof = debug.Profiler('functions.makeARGB', disabled=True) + + if lut is not None and not isinstance(lut, np.ndarray): + lut = np.array(lut) + if levels is not None and not isinstance(levels, np.ndarray): + levels = np.array(levels) + + ## sanity checks + #if data.ndim == 3: + #if data.shape[2] not in (3,4): + #raise Exception("data.shape[2] must be 3 or 4") + ##if lut is not None: + ##raise Exception("can not use lookup table with 3D data") + #elif data.ndim != 2: + #raise Exception("data must be 2D or 3D") + + #if lut is not None: + ##if lut.ndim == 2: + ##if lut.shape[1] : + ##raise Exception("lut.shape[1] must be 3 or 4") + ##elif lut.ndim != 1: + ##raise Exception("lut must be 1D or 2D") + #if lut.dtype != np.ubyte: + #raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) + + + if levels is not None: + if levels.ndim == 1: + if len(levels) != 2: + raise Exception('levels argument must have length 2') + elif levels.ndim == 2: + if lut is not None and lut.ndim > 1: + raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2') + if levels.shape != (data.shape[-1], 2): + raise Exception('levels must have shape (data.shape[-1], 2)') + else: + print levels + raise Exception("levels argument must be 1D or 2D.") + #levels = np.array(levels) + #if levels.shape == (2,): + #pass + #elif levels.shape in [(3,2), (4,2)]: + #if data.ndim == 3: + #raise Exception("Can not use 2D levels with 3D data.") + #if lut is not None: + #raise Exception('Can not use 2D levels and lookup table together.') + #else: + #raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") + + prof.mark('1') + + if scale is None: + if lut is not None: + scale = lut.shape[0] + else: + scale = 255. + + ## Apply levels if given + if levels is not None: + + if isinstance(levels, np.ndarray) and levels.ndim == 2: + ## we are going to rescale each channel independently + if levels.shape[0] != data.shape[-1]: + raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])") + newData = np.empty(data.shape, dtype=int) + for i in range(data.shape[-1]): + minVal, maxVal = levels[i] + if minVal == maxVal: + maxVal += 1e-16 + newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int) + data = newData + else: + minVal, maxVal = levels + if minVal == maxVal: + maxVal += 1e-16 + data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) + + prof.mark('2') + + + ## apply LUT if given + if lut is not None: + data = applyLookupTable(data, lut) + else: + if data.dtype is not np.ubyte: + data = np.clip(data, 0, 255).astype(np.ubyte) + + prof.mark('3') + + + ## copy data into ARGB ordered array + imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) + if data.ndim == 2: + data = data[..., np.newaxis] + + prof.mark('4') + + if useRGBA: + order = [0,1,2,3] ## array comes out RGBA + else: + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + + if data.shape[2] == 1: + for i in range(3): + imgData[..., order[i]] = data[..., 0] + else: + for i in range(0, data.shape[2]): + imgData[..., order[i]] = data[..., i] + + prof.mark('5') + + if data.shape[2] == 4: + alpha = True + else: + alpha = False + imgData[..., 3] = 255 + + prof.mark('6') + + prof.finish() + return imgData, alpha + + +def makeQImage(imgData, alpha=None, copy=True, transpose=True): + """ + Turn an ARGB array into QImage. + By default, the data is copied; changes to the array will not + be reflected in the image. The image will be given a 'data' attribute + pointing to the array which shares its data to prevent python + freeing that memory while the image is in use. + + =========== =================================================================== + Arguments: + imgData Array of data to convert. Must have shape (width, height, 3 or 4) + and dtype=ubyte. The order of values in the 3rd axis must be + (b, g, r, a). + alpha If True, the QImage returned will have format ARGB32. If False, + the format will be RGB32. By default, _alpha_ is True if + array.shape[2] == 4. + copy If True, the data is copied before converting to QImage. + If False, the new QImage points directly to the data in the array. + Note that the array must be contiguous for this to work. + transpose If True (the default), the array x/y axes are transposed before + creating the image. Note that Qt expects the axes to be in + (height, width) order whereas pyqtgraph usually prefers the + opposite. + =========== =================================================================== + """ + ## create QImage from buffer + prof = debug.Profiler('functions.makeQImage', disabled=True) + + ## If we didn't explicitly specify alpha, check the array shape. + if alpha is None: + alpha = (imgData.shape[2] == 4) + + copied = False + if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp) + if copy is True: + d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype) + d2[:,:,:3] = imgData + d2[:,:,3] = 255 + imgData = d2 + copied = True + else: + raise Exception('Array has only 3 channels; cannot make QImage without copying.') + + if alpha: + imgFormat = QtGui.QImage.Format_ARGB32 + else: + imgFormat = QtGui.QImage.Format_RGB32 + + if transpose: + imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite + + if not imgData.flags['C_CONTIGUOUS']: + if copy is False: + extra = ' (try setting transpose=False)' if transpose else '' + raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra) + imgData = np.ascontiguousarray(imgData) + copied = True + + if copy is True and copied is False: + imgData = imgData.copy() + + if USE_PYSIDE: + ch = ctypes.c_char.from_buffer(imgData, 0) + img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) + else: + addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) + img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) + img.data = imgData + return img + #try: + #buf = imgData.data + #except AttributeError: ## happens when image data is non-contiguous + #buf = imgData.data + + #prof.mark('1') + #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) + #prof.mark('2') + #qimage.data = imgData + #prof.finish() + #return qimage + +def imageToArray(img, copy=False, transpose=True): + """ + Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied. + By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if + the QImage is collected before the array, there may be trouble). + The array will have shape (width, height, (b,g,r,a)). + """ + fmt = img.format() + ptr = img.bits() + if USE_PYSIDE: + arr = np.frombuffer(ptr, dtype=np.ubyte) + else: + ptr.setsize(img.byteCount()) + arr = np.asarray(ptr) + + if fmt == img.Format_RGB32: + arr = arr.reshape(img.height(), img.width(), 3) + elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: + arr = arr.reshape(img.height(), img.width(), 4) + + if copy: + arr = arr.copy() + + if transpose: + return arr.transpose((1,0,2)) + else: + return arr + +def colorToAlpha(data, color): + """ + Given an RGBA image in *data*, convert *color* to be transparent. + *data* must be an array (w, h, 3 or 4) of ubyte values and *color* must be + an array (3) of ubyte values. + This is particularly useful for use with images that have a black or white background. + + Algorithm is taken from Gimp's color-to-alpha function in plug-ins/common/colortoalpha.c + Credit: + /* + * Color To Alpha plug-in v1.0 by Seth Burgess, sjburges@gimp.org 1999/05/14 + * with algorithm by clahey + */ + + """ + data = data.astype(float) + if data.shape[-1] == 3: ## add alpha channel if needed + d2 = np.empty(data.shape[:2]+(4,), dtype=data.dtype) + d2[...,:3] = data + d2[...,3] = 255 + data = d2 + + color = color.astype(float) + alpha = np.zeros(data.shape[:2]+(3,), dtype=float) + output = data.copy() + + for i in [0,1,2]: + d = data[...,i] + c = color[i] + mask = d > c + alpha[...,i][mask] = (d[mask] - c) / (255. - c) + imask = d < c + alpha[...,i][imask] = (c - d[imask]) / c + + output[...,3] = alpha.max(axis=2) * 255. + + mask = output[...,3] >= 1.0 ## avoid zero division while processing alpha channel + correction = 255. / output[...,3][mask] ## increase value to compensate for decreased alpha + for i in [0,1,2]: + output[...,i][mask] = ((output[...,i][mask]-color[i]) * correction) + color[i] + output[...,3][mask] *= data[...,3][mask] / 255. ## combine computed and previous alpha values + + #raise Exception() + return np.clip(output, 0, 255).astype(np.ubyte) + + + +#def isosurface(data, level): + #""" + #Generate isosurface from volumetric data using marching tetrahedra algorithm. + #See Paul Bourke, "Polygonising a Scalar Field Using Tetrahedrons" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + + #*data* 3D numpy array of scalar values + #*level* The level at which to generate an isosurface + #""" + + #facets = [] + + ### mark everything below the isosurface level + #mask = data < level + + #### make eight sub-fields + #fields = np.empty((2,2,2), dtype=object) + #slices = [slice(0,-1), slice(1,None)] + #for i in [0,1]: + #for j in [0,1]: + #for k in [0,1]: + #fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + + + + ### split each cell into 6 tetrahedra + ### these all have the same 'orienation'; points 1,2,3 circle + ### clockwise around point 0 + #tetrahedra = [ + #[(0,1,0), (1,1,1), (0,1,1), (1,0,1)], + #[(0,1,0), (0,1,1), (0,0,1), (1,0,1)], + #[(0,1,0), (0,0,1), (0,0,0), (1,0,1)], + #[(0,1,0), (0,0,0), (1,0,0), (1,0,1)], + #[(0,1,0), (1,0,0), (1,1,0), (1,0,1)], + #[(0,1,0), (1,1,0), (1,1,1), (1,0,1)] + #] + + ### each tetrahedron will be assigned an index + ### which determines how to generate its facets. + ### this structure is: + ### facets[index][facet1, facet2, ...] + ### where each facet is triangular and its points are each + ### interpolated between two points on the tetrahedron + ### facet = [(p1a, p1b), (p2a, p2b), (p3a, p3b)] + ### facet points always circle clockwise if you are looking + ### at them from below the isosurface. + #indexFacets = [ + #[], ## all above + #[[(0,1), (0,2), (0,3)]], # 0 below + #[[(1,0), (1,3), (1,2)]], # 1 below + #[[(0,2), (1,3), (1,2)], [(0,2), (0,3), (1,3)]], # 0,1 below + #[[(2,0), (2,1), (2,3)]], # 2 below + #[[(0,3), (1,2), (2,3)], [(0,3), (0,1), (1,2)]], # 0,2 below + #[[(1,0), (2,3), (2,0)], [(1,0), (1,3), (2,3)]], # 1,2 below + #[[(3,0), (3,1), (3,2)]], # 3 above + #[[(3,0), (3,2), (3,1)]], # 3 below + #[[(1,0), (2,0), (2,3)], [(1,0), (2,3), (1,3)]], # 0,3 below + #[[(0,3), (2,3), (1,2)], [(0,3), (1,2), (0,1)]], # 1,3 below + #[[(2,0), (2,3), (2,1)]], # 0,1,3 below + #[[(0,2), (1,2), (1,3)], [(0,2), (1,3), (0,3)]], # 2,3 below + #[[(1,0), (1,2), (1,3)]], # 0,2,3 below + #[[(0,1), (0,3), (0,2)]], # 1,2,3 below + #[] ## all below + #] + + #for tet in tetrahedra: + + ### get the 4 fields for this tetrahedron + #tetFields = [fields[c] for c in tet] + + ### generate an index for each grid cell + #index = tetFields[0] + tetFields[1]*2 + tetFields[2]*4 + tetFields[3]*8 + + ### add facets + #for i in xrange(index.shape[0]): # data x-axis + #for j in xrange(index.shape[1]): # data y-axis + #for k in xrange(index.shape[2]): # data z-axis + #for f in indexFacets[index[i,j,k]]: # faces to generate for this tet + #pts = [] + #for l in [0,1,2]: # points in this face + #p1 = tet[f[l][0]] # tet corner 1 + #p2 = tet[f[l][1]] # tet corner 2 + #pts.append([(p1[x]+p2[x])*0.5+[i,j,k][x]+0.5 for x in [0,1,2]]) ## interpolate between tet corners + #facets.append(pts) + + #return facets + + +def isocurve(data, level, connected=False, extendToEdge=False, path=False): + """ + Generate isocurve from 2D data using marching squares algorithm. + + ============= ========================================================= + Arguments + data 2D numpy array of scalar values + level The level at which to generate an isosurface + connected If False, return a single long list of point pairs + If True, return multiple long lists of connected point + locations. (This is slower but better for drawing + continuous lines) + extendToEdge If True, extend the curves to reach the exact edges of + the data. + path if True, return a QPainterPath rather than a list of + vertex coordinates. This forces connected=True. + ============= ========================================================= + + This function is SLOW; plenty of room for optimization here. + """ + + if path is True: + connected = True + + if extendToEdge: + d2 = np.empty((data.shape[0]+2, data.shape[1]+2), dtype=data.dtype) + d2[1:-1, 1:-1] = data + d2[0, 1:-1] = data[0] + d2[-1, 1:-1] = data[-1] + d2[1:-1, 0] = data[:, 0] + d2[1:-1, -1] = data[:, -1] + d2[0,0] = d2[0,1] + d2[0,-1] = d2[1,-1] + d2[-1,0] = d2[-1,1] + d2[-1,-1] = d2[-1,-2] + data = d2 + + sideTable = [ + [], + [0,1], + [1,2], + [0,2], + [0,3], + [1,3], + [0,1,2,3], + [2,3], + [2,3], + [0,1,2,3], + [1,3], + [0,3], + [0,2], + [1,2], + [0,1], + [] + ] + + edgeKey=[ + [(0,1), (0,0)], + [(0,0), (1,0)], + [(1,0), (1,1)], + [(1,1), (0,1)] + ] + + + lines = [] + + ## mark everything below the isosurface level + mask = data < level + + ### make four sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + fields[i,j] = mask[slices[i], slices[j]] + #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + vertIndex = i+2*j + #print i,j,k," : ", fields[i,j,k], 2**vertIndex + index += fields[i,j] * 2**vertIndex + #print index + #print index + + ## add lines + for i in range(index.shape[0]): # data x-axis + for j in range(index.shape[1]): # data y-axis + sides = sideTable[index[i,j]] + for l in range(0, len(sides), 2): ## faces for this grid cell + edges = sides[l:l+2] + pts = [] + for m in [0,1]: # points in this face + p1 = edgeKey[edges[m]][0] # p1, p2 are points at either side of an edge + p2 = edgeKey[edges[m]][1] + v1 = data[i+p1[0], j+p1[1]] # v1 and v2 are the values at p1 and p2 + v2 = data[i+p2[0], j+p2[1]] + f = (level-v1) / (v2-v1) + fi = 1.0 - f + p = ( ## interpolate between corners + p1[0]*fi + p2[0]*f + i + 0.5, + p1[1]*fi + p2[1]*f + j + 0.5 + ) + if extendToEdge: + ## check bounds + p = ( + min(data.shape[0]-2, max(0, p[0]-1)), + min(data.shape[1]-2, max(0, p[1]-1)), + ) + if connected: + gridKey = i + (1 if edges[m]==2 else 0), j + (1 if edges[m]==3 else 0), edges[m]%2 + pts.append((p, gridKey)) ## give the actual position and a key identifying the grid location (for connecting segments) + else: + pts.append(p) + + lines.append(pts) + + if not connected: + return lines + + ## turn disjoint list of segments into continuous lines + + #lines = [[2,5], [5,4], [3,4], [1,3], [6,7], [7,8], [8,6], [11,12], [12,15], [11,13], [13,14]] + #lines = [[(float(a), a), (float(b), b)] for a,b in lines] + points = {} ## maps each point to its connections + for a,b in lines: + if a[1] not in points: + points[a[1]] = [] + points[a[1]].append([a,b]) + if b[1] not in points: + points[b[1]] = [] + points[b[1]].append([b,a]) + + ## rearrange into chains + for k in points.keys(): + try: + chains = points[k] + except KeyError: ## already used this point elsewhere + continue + #print "===========", k + for chain in chains: + #print " chain:", chain + x = None + while True: + if x == chain[-1][1]: + break ## nothing left to do on this chain + + x = chain[-1][1] + if x == k: + break ## chain has looped; we're done and can ignore the opposite chain + y = chain[-2][1] + connects = points[x] + for conn in connects[:]: + if conn[1][1] != y: + #print " ext:", conn + chain.extend(conn[1:]) + #print " del:", x + del points[x] + if chain[0][1] == chain[-1][1]: # looped chain; no need to continue the other direction + chains.pop() + break + + + ## extract point locations + lines = [] + for chain in points.values(): + if len(chain) == 2: + chain = chain[1][1:][::-1] + chain[0] # join together ends of chain + else: + chain = chain[0] + lines.append([p[0] for p in chain]) + + if not path: + return lines ## a list of pairs of points + + path = QtGui.QPainterPath() + for line in lines: + path.moveTo(*line[0]) + for p in line[1:]: + path.lineTo(*p) + + return path + + +def traceImage(image, values, smooth=0.5): + """ + Convert an image to a set of QPainterPath curves. + One curve will be generated for each item in *values*; each curve outlines the area + of the image that is closer to its value than to any others. + + If image is RGB or RGBA, then the shape of values should be (nvals, 3/4) + The parameter *smooth* is expressed in pixels. + """ + import scipy.ndimage as ndi + if values.ndim == 2: + values = values.T + values = values[np.newaxis, np.newaxis, ...].astype(float) + image = image[..., np.newaxis].astype(float) + diff = np.abs(image-values) + if values.ndim == 4: + diff = diff.sum(axis=2) + + labels = np.argmin(diff, axis=2) + + paths = [] + for i in range(diff.shape[-1]): + d = (labels==i).astype(float) + d = ndi.gaussian_filter(d, (smooth, smooth)) + lines = isocurve(d, 0.5, connected=True, extendToEdge=True) + path = QtGui.QPainterPath() + for line in lines: + path.moveTo(*line[0]) + for p in line[1:]: + path.lineTo(*p) + + paths.append(path) + return paths + + + +IsosurfaceDataCache = None +def isosurface(data, level): + """ + Generate isosurface from volumetric data using marching cubes algorithm. + See Paul Bourke, "Polygonising a Scalar Field" + (http://paulbourke.net/geometry/polygonise/) + + *data* 3D numpy array of scalar values + *level* The level at which to generate an isosurface + + Returns an array of vertex coordinates (Nv, 3) and an array of + per-face vertex indexes (Nf, 3) + """ + ## For improvement, see: + ## + ## Efficient implementation of Marching Cubes' cases with topological guarantees. + ## Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan Tavares. + ## Journal of Graphics Tools 8(2): pp. 1-15 (december 2003) + + ## Precompute lookup tables on the first run + global IsosurfaceDataCache + if IsosurfaceDataCache is None: + ## map from grid cell index to edge index. + ## grid cell index tells us which corners are below the isosurface, + ## edge index tells us which edges are cut by the isosurface. + ## (Data stolen from Bourk; see above.) + edgeTable = np.array([ + 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, + 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, + 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, + 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, + 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, + 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, + 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , + 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, + 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, + 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, + 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, + 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, + 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, + 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, + 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, + 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16) + + ## Table of triangles to use for filling each grid cell. + ## Each set of three integers tells us which three edges to + ## draw a triangle between. + ## (Data stolen from Bourk; see above.) + triTable = [ + [], + [0, 8, 3], + [0, 1, 9], + [1, 8, 3, 9, 8, 1], + [1, 2, 10], + [0, 8, 3, 1, 2, 10], + [9, 2, 10, 0, 2, 9], + [2, 8, 3, 2, 10, 8, 10, 9, 8], + [3, 11, 2], + [0, 11, 2, 8, 11, 0], + [1, 9, 0, 2, 3, 11], + [1, 11, 2, 1, 9, 11, 9, 8, 11], + [3, 10, 1, 11, 10, 3], + [0, 10, 1, 0, 8, 10, 8, 11, 10], + [3, 9, 0, 3, 11, 9, 11, 10, 9], + [9, 8, 10, 10, 8, 11], + [4, 7, 8], + [4, 3, 0, 7, 3, 4], + [0, 1, 9, 8, 4, 7], + [4, 1, 9, 4, 7, 1, 7, 3, 1], + [1, 2, 10, 8, 4, 7], + [3, 4, 7, 3, 0, 4, 1, 2, 10], + [9, 2, 10, 9, 0, 2, 8, 4, 7], + [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], + [8, 4, 7, 3, 11, 2], + [11, 4, 7, 11, 2, 4, 2, 0, 4], + [9, 0, 1, 8, 4, 7, 2, 3, 11], + [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], + [3, 10, 1, 3, 11, 10, 7, 8, 4], + [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], + [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], + [4, 7, 11, 4, 11, 9, 9, 11, 10], + [9, 5, 4], + [9, 5, 4, 0, 8, 3], + [0, 5, 4, 1, 5, 0], + [8, 5, 4, 8, 3, 5, 3, 1, 5], + [1, 2, 10, 9, 5, 4], + [3, 0, 8, 1, 2, 10, 4, 9, 5], + [5, 2, 10, 5, 4, 2, 4, 0, 2], + [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], + [9, 5, 4, 2, 3, 11], + [0, 11, 2, 0, 8, 11, 4, 9, 5], + [0, 5, 4, 0, 1, 5, 2, 3, 11], + [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], + [10, 3, 11, 10, 1, 3, 9, 5, 4], + [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], + [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], + [5, 4, 8, 5, 8, 10, 10, 8, 11], + [9, 7, 8, 5, 7, 9], + [9, 3, 0, 9, 5, 3, 5, 7, 3], + [0, 7, 8, 0, 1, 7, 1, 5, 7], + [1, 5, 3, 3, 5, 7], + [9, 7, 8, 9, 5, 7, 10, 1, 2], + [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], + [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], + [2, 10, 5, 2, 5, 3, 3, 5, 7], + [7, 9, 5, 7, 8, 9, 3, 11, 2], + [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], + [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], + [11, 2, 1, 11, 1, 7, 7, 1, 5], + [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], + [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], + [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], + [11, 10, 5, 7, 11, 5], + [10, 6, 5], + [0, 8, 3, 5, 10, 6], + [9, 0, 1, 5, 10, 6], + [1, 8, 3, 1, 9, 8, 5, 10, 6], + [1, 6, 5, 2, 6, 1], + [1, 6, 5, 1, 2, 6, 3, 0, 8], + [9, 6, 5, 9, 0, 6, 0, 2, 6], + [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], + [2, 3, 11, 10, 6, 5], + [11, 0, 8, 11, 2, 0, 10, 6, 5], + [0, 1, 9, 2, 3, 11, 5, 10, 6], + [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], + [6, 3, 11, 6, 5, 3, 5, 1, 3], + [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], + [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], + [6, 5, 9, 6, 9, 11, 11, 9, 8], + [5, 10, 6, 4, 7, 8], + [4, 3, 0, 4, 7, 3, 6, 5, 10], + [1, 9, 0, 5, 10, 6, 8, 4, 7], + [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], + [6, 1, 2, 6, 5, 1, 4, 7, 8], + [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], + [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], + [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], + [3, 11, 2, 7, 8, 4, 10, 6, 5], + [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], + [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], + [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], + [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], + [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], + [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], + [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], + [10, 4, 9, 6, 4, 10], + [4, 10, 6, 4, 9, 10, 0, 8, 3], + [10, 0, 1, 10, 6, 0, 6, 4, 0], + [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], + [1, 4, 9, 1, 2, 4, 2, 6, 4], + [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], + [0, 2, 4, 4, 2, 6], + [8, 3, 2, 8, 2, 4, 4, 2, 6], + [10, 4, 9, 10, 6, 4, 11, 2, 3], + [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], + [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], + [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], + [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], + [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], + [3, 11, 6, 3, 6, 0, 0, 6, 4], + [6, 4, 8, 11, 6, 8], + [7, 10, 6, 7, 8, 10, 8, 9, 10], + [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], + [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], + [10, 6, 7, 10, 7, 1, 1, 7, 3], + [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], + [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], + [7, 8, 0, 7, 0, 6, 6, 0, 2], + [7, 3, 2, 6, 7, 2], + [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], + [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], + [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], + [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], + [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], + [0, 9, 1, 11, 6, 7], + [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], + [7, 11, 6], + [7, 6, 11], + [3, 0, 8, 11, 7, 6], + [0, 1, 9, 11, 7, 6], + [8, 1, 9, 8, 3, 1, 11, 7, 6], + [10, 1, 2, 6, 11, 7], + [1, 2, 10, 3, 0, 8, 6, 11, 7], + [2, 9, 0, 2, 10, 9, 6, 11, 7], + [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], + [7, 2, 3, 6, 2, 7], + [7, 0, 8, 7, 6, 0, 6, 2, 0], + [2, 7, 6, 2, 3, 7, 0, 1, 9], + [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], + [10, 7, 6, 10, 1, 7, 1, 3, 7], + [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], + [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], + [7, 6, 10, 7, 10, 8, 8, 10, 9], + [6, 8, 4, 11, 8, 6], + [3, 6, 11, 3, 0, 6, 0, 4, 6], + [8, 6, 11, 8, 4, 6, 9, 0, 1], + [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], + [6, 8, 4, 6, 11, 8, 2, 10, 1], + [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], + [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], + [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], + [8, 2, 3, 8, 4, 2, 4, 6, 2], + [0, 4, 2, 4, 6, 2], + [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], + [1, 9, 4, 1, 4, 2, 2, 4, 6], + [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], + [10, 1, 0, 10, 0, 6, 6, 0, 4], + [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], + [10, 9, 4, 6, 10, 4], + [4, 9, 5, 7, 6, 11], + [0, 8, 3, 4, 9, 5, 11, 7, 6], + [5, 0, 1, 5, 4, 0, 7, 6, 11], + [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], + [9, 5, 4, 10, 1, 2, 7, 6, 11], + [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], + [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], + [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], + [7, 2, 3, 7, 6, 2, 5, 4, 9], + [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], + [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], + [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], + [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], + [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], + [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], + [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], + [6, 9, 5, 6, 11, 9, 11, 8, 9], + [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], + [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], + [6, 11, 3, 6, 3, 5, 5, 3, 1], + [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], + [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], + [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], + [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], + [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], + [9, 5, 6, 9, 6, 0, 0, 6, 2], + [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], + [1, 5, 6, 2, 1, 6], + [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], + [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], + [0, 3, 8, 5, 6, 10], + [10, 5, 6], + [11, 5, 10, 7, 5, 11], + [11, 5, 10, 11, 7, 5, 8, 3, 0], + [5, 11, 7, 5, 10, 11, 1, 9, 0], + [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], + [11, 1, 2, 11, 7, 1, 7, 5, 1], + [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], + [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], + [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], + [2, 5, 10, 2, 3, 5, 3, 7, 5], + [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], + [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], + [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], + [1, 3, 5, 3, 7, 5], + [0, 8, 7, 0, 7, 1, 1, 7, 5], + [9, 0, 3, 9, 3, 5, 5, 3, 7], + [9, 8, 7, 5, 9, 7], + [5, 8, 4, 5, 10, 8, 10, 11, 8], + [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], + [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], + [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], + [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], + [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], + [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], + [9, 4, 5, 2, 11, 3], + [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], + [5, 10, 2, 5, 2, 4, 4, 2, 0], + [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], + [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], + [8, 4, 5, 8, 5, 3, 3, 5, 1], + [0, 4, 5, 1, 0, 5], + [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], + [9, 4, 5], + [4, 11, 7, 4, 9, 11, 9, 10, 11], + [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], + [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], + [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], + [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], + [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], + [11, 7, 4, 11, 4, 2, 2, 4, 0], + [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], + [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], + [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], + [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], + [1, 10, 2, 8, 7, 4], + [4, 9, 1, 4, 1, 7, 7, 1, 3], + [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], + [4, 0, 3, 7, 4, 3], + [4, 8, 7], + [9, 10, 8, 10, 11, 8], + [3, 0, 9, 3, 9, 11, 11, 9, 10], + [0, 1, 10, 0, 10, 8, 8, 10, 11], + [3, 1, 10, 11, 3, 10], + [1, 2, 11, 1, 11, 9, 9, 11, 8], + [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], + [0, 2, 11, 8, 0, 11], + [3, 2, 11], + [2, 3, 8, 2, 8, 10, 10, 8, 9], + [9, 10, 2, 0, 9, 2], + [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], + [1, 10, 2], + [1, 3, 8, 9, 1, 8], + [0, 9, 1], + [0, 3, 8], + [] + ] + edgeShifts = np.array([ ## maps edge ID (0-11) to (x,y,z) cell offset and edge ID (0-2) + [0, 0, 0, 0], + [1, 0, 0, 1], + [0, 1, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + [1, 0, 1, 1], + [0, 1, 1, 0], + [0, 0, 1, 1], + [0, 0, 0, 2], + [1, 0, 0, 2], + [1, 1, 0, 2], + [0, 1, 0, 2], + #[9, 9, 9, 9] ## fake + ], dtype=np.ubyte) + nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte) + faceShiftTables = [None] + for i in range(1,6): + ## compute lookup table of index: vertexes mapping + faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) + faceTableInds = np.argwhere(nTableFaces == i) + faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) + faceTableI = faceTableI.reshape((len(triTable), i, 3)) + faceShiftTables.append(edgeShifts[faceTableI]) + + ## Let's try something different: + #faceTable = np.empty((256, 5, 3, 4), dtype=np.ubyte) # (grid cell index, faces, vertexes, edge lookup) + #for i,f in enumerate(triTable): + #f = np.array(f + [12] * (15-len(f))).reshape(5,3) + #faceTable[i] = edgeShifts[f] + + + IsosurfaceDataCache = (faceShiftTables, edgeShifts, edgeTable, nTableFaces) + else: + faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache + + + + ## mark everything below the isosurface level + mask = data < level + + ### make eight sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + for k in [0,1]: + fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + index += fields[i,j,k] * 2**vertIndex + + ### Generate table of edges that have been cut + cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) + edges = edgeTable[index] + for i, shift in enumerate(edgeShifts[:12]): + slices = [slice(shift[j],cutEdges.shape[j]+(shift[j]-1)) for j in range(3)] + cutEdges[slices[0], slices[1], slices[2], shift[3]] += edges & 2**i + + ## for each cut edge, interpolate to see where exactly the edge is cut and generate vertex positions + m = cutEdges > 0 + vertexInds = np.argwhere(m) ## argwhere is slow! + vertexes = vertexInds[:,:3].astype(np.float32) + dataFlat = data.reshape(data.shape[0]*data.shape[1]*data.shape[2]) + + ## re-use the cutEdges array as a lookup table for vertex IDs + cutEdges[vertexInds[:,0], vertexInds[:,1], vertexInds[:,2], vertexInds[:,3]] = np.arange(vertexInds.shape[0]) + + for i in [0,1,2]: + vim = vertexInds[:,3] == i + vi = vertexInds[vim, :3] + viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1) + v1 = dataFlat[viFlat] + v2 = dataFlat[viFlat + data.strides[i]/data.itemsize] + vertexes[vim,i] += (level-v1) / (v2-v1) + + ### compute the set of vertex indexes for each face. + + ## This works, but runs a bit slower. + #cells = np.argwhere((index != 0) & (index != 255)) ## all cells with at least one face + #cellInds = index[cells[:,0], cells[:,1], cells[:,2]] + #verts = faceTable[cellInds] + #mask = verts[...,0,0] != 9 + #verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + #verts = verts[mask] + #faces = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. + + + ## To allow this to be vectorized efficiently, we count the number of faces in each + ## grid cell and handle each group of cells with the same number together. + ## determine how many faces to assign to each grid cell + nFaces = nTableFaces[index] + totFaces = nFaces.sum() + faces = np.empty((totFaces, 3), dtype=np.uint32) + ptr = 0 + #import debug + #p = debug.Profiler('isosurface', disabled=False) + + ## this helps speed up an indexing operation later on + cs = np.array(cutEdges.strides)/cutEdges.itemsize + cutEdges = cutEdges.flatten() + + ## this, strangely, does not seem to help. + #ins = np.array(index.strides)/index.itemsize + #index = index.flatten() + + for i in range(1,6): + ### expensive: + #p.mark('1') + cells = np.argwhere(nFaces == i) ## all cells which require i faces (argwhere is expensive) + #p.mark('2') + if cells.shape[0] == 0: + continue + #cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)] + cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round + #p.mark('3') + + ### expensive: + verts = faceShiftTables[i][cellInds] + #p.mark('4') + verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) + #p.mark('5') + + ### expensive: + #print verts.shape + verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2) + #vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want. + vertInds = cutEdges[verts] + #p.mark('6') + nv = vertInds.shape[0] + #p.mark('7') + faces[ptr:ptr+nv] = vertInds #.reshape((nv, 3)) + #p.mark('8') + ptr += nv + + return vertexes, faces + + + +def invertQTransform(tr): + """Return a QTransform that is the inverse of *tr*. + Rasises an exception if tr is not invertible. + + Note that this function is preferred over QTransform.inverted() due to + bugs in that method. (specifically, Qt has floating-point precision issues + when determining whether a matrix is invertible) + """ + if not HAVE_SCIPY: + inv = tr.inverted() + if inv[1] is False: + raise Exception("Transform is not invertible.") + return inv[0] + arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]]) + inv = scipy.linalg.inv(arr) + return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) + + +def pseudoScatter(data, spacing=None, shuffle=True): + """ + Used for examining the distribution of values in a set. + + Given a list of x-values, construct a set of y-values such that an x,y scatter-plot + will not have overlapping points (it will look similar to a histogram). + """ + inds = np.arange(len(data)) + if shuffle: + np.random.shuffle(inds) + + data = data[inds] + + if spacing is None: + spacing = 2.*np.std(data)/len(data)**0.5 + s2 = spacing**2 + + yvals = np.empty(len(data)) + yvals[0] = 0 + for i in range(1,len(data)): + x = data[i] # current x value to be placed + x0 = data[:i] # all x values already placed + y0 = yvals[:i] # all y values already placed + y = 0 + + dx = (x0-x)**2 # x-distance to each previous point + xmask = dx < s2 # exclude anything too far away + + if xmask.sum() > 0: + dx = dx[xmask] + dy = (s2 - dx)**0.5 + limits = np.empty((2,len(dy))) # ranges of y-values to exclude + limits[0] = y0[xmask] - dy + limits[1] = y0[xmask] + dy + + while True: + # ignore anything below this y-value + mask = limits[1] >= y + limits = limits[:,mask] + + # are we inside an excluded region? + mask = (limits[0] < y) & (limits[1] > y) + if mask.sum() == 0: + break + y = limits[:,mask].max() + + yvals[i] = y + + return yvals[np.argsort(inds)] ## un-shuffle values before returning \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py new file mode 100644 index 00000000..153ea712 --- /dev/null +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -0,0 +1,56 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +import numpy as np +__all__ = ['ArrowItem'] + +class ArrowItem(QtGui.QGraphicsPathItem): + """ + For displaying scale-invariant arrows. + For arrows pointing to a location on a curve, see CurveArrow + + """ + + + def __init__(self, **opts): + QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None)) + if 'size' in opts: + opts['headLen'] = opts['size'] + if 'width' in opts: + opts['headWidth'] = opts['width'] + defOpts = { + 'pxMode': True, + 'angle': -150, ## If the angle is 0, the arrow points left + 'pos': (0,0), + 'headLen': 20, + 'tipAngle': 25, + 'baseAngle': 0, + 'tailLen': None, + 'tailWidth': 3, + 'pen': (200,200,200), + 'brush': (50,50,200), + } + defOpts.update(opts) + + self.setStyle(**defOpts) + + self.setPen(fn.mkPen(defOpts['pen'])) + self.setBrush(fn.mkBrush(defOpts['brush'])) + + self.rotate(self.opts['angle']) + self.moveBy(*self.opts['pos']) + + def setStyle(self, **opts): + self.opts = opts + + opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) + self.path = fn.makeArrowPath(**opt) + self.setPath(self.path) + + if opts['pxMode']: + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + else: + self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + QtGui.QGraphicsPathItem.paint(self, p, *args) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py new file mode 100644 index 00000000..82cbcfae --- /dev/null +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -0,0 +1,711 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode +import numpy as np +from pyqtgraph.Point import Point +import pyqtgraph.debug as debug +import weakref +import pyqtgraph.functions as fn +import pyqtgraph as pg +from .GraphicsWidget import GraphicsWidget + +__all__ = ['AxisItem'] +class AxisItem(GraphicsWidget): + """ + GraphicsItem showing a single plot axis with ticks, values, and label. + Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. + Ticks can be extended to draw a grid. + If maxTickLength is negative, ticks point into the plot. + """ + + def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True): + """ + ============== =============================================================== + **Arguments:** + orientation one of 'left', 'right', 'top', or 'bottom' + maxTickLength (px) maximum length of ticks to draw. Negative values draw + into the plot, positive values draw outward. + linkView (ViewBox) causes the range of values displayed in the axis + to be linked to the visible range of a ViewBox. + showValues (bool) Whether to display values adjacent to ticks + pen (QPen) Pen used when drawing ticks. + ============== =============================================================== + """ + + GraphicsWidget.__init__(self, parent) + self.label = QtGui.QGraphicsTextItem(self) + self.showValues = showValues + 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.setMinimumWidth(25) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Minimum, + #QtGui.QSizePolicy.Expanding + #)) + self.label.rotate(-90) + #else: + #self.setMinimumHeight(50) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Expanding, + #QtGui.QSizePolicy.Minimum + #)) + #self.drawLabel = False + + self.labelText = '' + self.labelUnits = '' + self.labelUnitPrefix='' + self.labelStyle = {} + self.logMode = False + + self.textHeight = 18 + self.tickLength = maxTickLength + self._tickLevels = None ## used to override the automatic ticking system with explicit ticks + self.scale = 1.0 + self.autoScale = True + + self.setRange(0, 1) + + self.setPen(pen) + + self._linkedView = None + if linkView is not None: + self.linkToView(linkView) + + self.showLabel(False) + + self.grid = False + #self.setCacheMode(self.DeviceCoordinateCache) + + def close(self): + self.scene().removeItem(self.label) + self.label = None + self.scene().removeItem(self) + + def setGrid(self, grid): + """Set the alpha value for the grid, or False to disable.""" + self.grid = grid + self.picture = None + self.prepareGeometryChange() + self.update() + + def setLogMode(self, log): + """ + If *log* is True, then ticks are displayed on a logarithmic scale and values + are adjusted accordingly. (This is usually accessed by changing the log mode + of a :func:`PlotItem `) + """ + self.logMode = log + self.picture = None + self.update() + + def resizeEvent(self, ev=None): + #s = self.size() + + ## Set the position of the label + nudge = 5 + br = self.label.boundingRect() + p = QtCore.QPointF(0, 0) + if self.orientation == 'left': + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(-nudge) + #s.setWidth(10) + elif self.orientation == 'right': + #s.setWidth(10) + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(int(self.size().width()-br.height()+nudge)) + elif self.orientation == 'top': + #s.setHeight(10) + p.setY(-nudge) + p.setX(int(self.size().width()/2. - br.width()/2.)) + elif self.orientation == 'bottom': + p.setX(int(self.size().width()/2. - br.width()/2.)) + #s.setHeight(10) + p.setY(int(self.size().height()-br.height()+nudge)) + #self.label.resize(s) + self.label.setPos(p) + self.picture = None + + def showLabel(self, show=True): + """Show/hide the label text for this axis.""" + #self.drawLabel = show + self.label.setVisible(show) + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + if self.autoScale: + self.setScale() + + def setLabel(self, text=None, units=None, unitPrefix=None, **args): + """Set the text displayed adjacent to the axis.""" + if text is not None: + self.labelText = text + self.showLabel() + if units is not None: + self.labelUnits = units + self.showLabel() + if unitPrefix is not None: + self.labelUnitPrefix = unitPrefix + if len(args) > 0: + self.labelStyle = args + self.label.setHtml(self.labelString()) + self.resizeEvent() + self.picture = None + self.update() + + def labelString(self): + if self.labelUnits == '': + if self.scale == 1.0: + units = '' + else: + units = asUnicode('(x%g)') % (1.0/self.scale) + else: + #print repr(self.labelUnitPrefix), repr(self.labelUnits) + units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits) + + s = asUnicode('%s %s') % (self.labelText, units) + + style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) + + return asUnicode("%s") % (style, s) + + def setHeight(self, h=None): + """Set the height of this axis reserved for ticks and tick labels. + The height of the axis label is automatically added.""" + if h is None: + h = self.textHeight + max(0, self.tickLength) + if self.label.isVisible(): + h += self.textHeight + self.setMaximumHeight(h) + self.setMinimumHeight(h) + self.picture = None + + + def setWidth(self, w=None): + """Set the width of this axis reserved for ticks and tick labels. + The width of the axis label is automatically added.""" + if w is None: + w = max(0, self.tickLength) + 40 + if self.label.isVisible(): + w += self.textHeight + self.setMaximumWidth(w) + self.setMinimumWidth(w) + + def pen(self): + if self._pen is None: + return fn.mkPen(pg.getConfigOption('foreground')) + return pg.mkPen(self._pen) + + def setPen(self, pen): + """ + Set the pen used for drawing text, axes, ticks, and grid lines. + if pen == None, the default will be used (see :func:`setConfigOption + `) + """ + self._pen = pen + self.picture = None + if pen is None: + pen = pg.getConfigOption('foreground') + self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6] + self.setLabel() + self.update() + + def setScale(self, scale=None): + """ + Set the value scaling for this axis. Values on the axis are multiplied + by this scale factor before being displayed as text. By default, + this scaling value is automatically determined based on the visible range + and the axis units are updated to reflect the chosen scale factor. + + For example: If the axis spans values from -0.1 to 0.1 and has units set + to 'V' then a scale of 1000 would cause the axis to display values -100 to 100 + and the units would appear as 'mV' + """ + if scale is None: + #if self.drawLabel: ## If there is a label, then we are free to rescale the values + if self.label.isVisible(): + #d = self.range[1] - self.range[0] + #(scale, prefix) = fn.siScale(d / 2.) + (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1]))) + if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. + scale = 1.0 + prefix = '' + self.setLabel(unitPrefix=prefix) + else: + scale = 1.0 + else: + self.setLabel(unitPrefix='') + self.autoScale = False + + if scale != self.scale: + self.scale = scale + self.setLabel() + self.picture = None + self.update() + + def setRange(self, mn, mx): + """Set the range of values displayed by the axis. + Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView `""" + if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))): + raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) + self.range = [mn, mx] + if self.autoScale: + self.setScale() + self.picture = None + self.update() + + def linkedView(self): + """Return the ViewBox this axis is linked to""" + if self._linkedView is None: + return None + else: + return self._linkedView() + + def linkToView(self, view): + """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view.""" + oldView = self.linkedView() + self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: + if oldView is not None: + oldView.sigYRangeChanged.disconnect(self.linkedViewChanged) + view.sigYRangeChanged.connect(self.linkedViewChanged) + else: + if oldView is not None: + oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) + view.sigXRangeChanged.connect(self.linkedViewChanged) + + def linkedViewChanged(self, view, newRange): + if self.orientation in ['right', 'left'] and view.yInverted(): + self.setRange(*newRange[::-1]) + else: + self.setRange(*newRange) + + def boundingRect(self): + linkedView = self.linkedView() + if linkedView is None or self.grid is False: + rect = self.mapRectFromParent(self.geometry()) + ## extend rect if ticks go in negative direction + if self.orientation == 'left': + rect.setRight(rect.right() - min(0,self.tickLength)) + elif self.orientation == 'right': + rect.setLeft(rect.left() + min(0,self.tickLength)) + elif self.orientation == 'top': + rect.setBottom(rect.bottom() - min(0,self.tickLength)) + elif self.orientation == 'bottom': + rect.setTop(rect.top() + min(0,self.tickLength)) + return rect + else: + return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) + + def paint(self, p, opt, widget): + if self.picture is None: + self.picture = QtGui.QPicture() + painter = QtGui.QPainter(self.picture) + try: + self.drawPicture(painter) + finally: + painter.end() + #p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ??? + #p.setRenderHint(p.TextAntialiasing, True) + self.picture.play(p) + + + def setTicks(self, ticks): + """Explicitly determine which ticks to display. + This overrides the behavior specified by tickSpacing(), tickValues(), and tickStrings() + The format for *ticks* looks like:: + + [ + [ (majorTickValue1, majorTickString1), (majorTickValue2, majorTickString2), ... ], + [ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ], + ... + ] + + If *ticks* is None, then the default tick system will be used instead. + """ + self._tickLevels = ticks + self.picture = None + self.update() + + def tickSpacing(self, minVal, maxVal, size): + """Return values describing the desired spacing and offset of ticks. + + This method is called whenever the axis needs to be redrawn and is a + good method to override in subclasses that require control over tick locations. + + The return value must be a list of three tuples:: + + [ + (major tick spacing, offset), + (minor tick spacing, offset), + (sub-minor tick spacing, offset), + ... + ] + """ + dif = abs(maxVal - minVal) + if dif == 0: + return [] + + ## decide optimal minor tick spacing in pixels (this is just aesthetics) + pixelSpacing = np.log(size+10) * 5 + optimalTickCount = size / pixelSpacing + if optimalTickCount < 1: + optimalTickCount = 1 + + ## optimal minor tick spacing + optimalSpacing = dif / optimalTickCount + + ## the largest power-of-10 spacing which is smaller than optimal + p10unit = 10 ** np.floor(np.log10(optimalSpacing)) + + ## Determine major/minor tick spacings which flank the optimal spacing. + intervals = np.array([1., 2., 10., 20., 100.]) * p10unit + minorIndex = 0 + while intervals[minorIndex+1] <= optimalSpacing: + minorIndex += 1 + + return [ + (intervals[minorIndex+2], 0), + (intervals[minorIndex+1], 0), + (intervals[minorIndex], 0) + ] + + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection + ### Determine major/minor tick spacings which flank the optimal spacing. + #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit + #minorIndex = 0 + #while intervals[minorIndex+1] <= optimalSpacing: + #minorIndex += 1 + + ### make sure we never see 5 and 2 at the same time + #intIndexes = [ + #[0,1,3], + #[0,2,3], + #[2,3,4], + #[3,4,6], + #[3,5,6], + #][minorIndex] + + #return [ + #(intervals[intIndexes[2]], 0), + #(intervals[intIndexes[1]], 0), + #(intervals[intIndexes[0]], 0) + #] + + + + def tickValues(self, minVal, maxVal, size): + """ + Return the values and spacing of ticks to draw:: + + [ + (spacing, [major ticks]), + (spacing, [minor ticks]), + ... + ] + + By default, this method calls tickSpacing to determine the correct tick locations. + This is a good method to override in subclasses. + """ + minVal, maxVal = sorted((minVal, maxVal)) + + + ticks = [] + tickLevels = self.tickSpacing(minVal, maxVal, size) + allValues = np.array([]) + for i in range(len(tickLevels)): + spacing, offset = tickLevels[i] + + ## determine starting tick + start = (np.ceil((minVal-offset) / spacing) * spacing) + offset + + ## determine number of ticks + num = int((maxVal-start) / spacing) + 1 + values = np.arange(num) * spacing + start + ## remove any ticks that were present in higher levels + ## we assume here that if the difference between a tick value and a previously seen tick value + ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. + values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) ) + allValues = np.concatenate([allValues, values]) + ticks.append((spacing, values)) + + if self.logMode: + return self.logTickValues(minVal, maxVal, size, ticks) + + return ticks + + def logTickValues(self, minVal, maxVal, size, stdTicks): + + ## start with the tick spacing given by tickValues(). + ## Any level whose spacing is < 1 needs to be converted to log scale + + ticks = [] + for (spacing, t) in stdTicks: + if spacing >= 1.0: + ticks.append((spacing, t)) + + if len(ticks) < 3: + v1 = int(np.floor(minVal)) + v2 = int(np.ceil(maxVal)) + #major = list(range(v1+1, v2)) + + minor = [] + for v in range(v1, v2): + minor.extend(v + np.log10(np.arange(1, 10))) + minor = [x for x in minor if x>minVal and x= 10000: + vstr = "%g" % vs + else: + vstr = ("%%0.%df" % places) % vs + strings.append(vstr) + return strings + + def logTickStrings(self, values, scale, spacing): + return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] + + def drawPicture(self, p): + + p.setRenderHint(p.Antialiasing, False) + p.setRenderHint(p.TextAntialiasing, True) + + prof = debug.Profiler("AxisItem.paint", disabled=True) + + #bounds = self.boundingRect() + bounds = self.mapRectFromParent(self.geometry()) + + linkedView = self.linkedView() + if linkedView is None or self.grid is False: + tickBounds = bounds + else: + tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) + + if self.orientation == 'left': + span = (bounds.topRight(), bounds.bottomRight()) + tickStart = tickBounds.right() + tickStop = bounds.right() + tickDir = -1 + axis = 0 + elif self.orientation == 'right': + span = (bounds.topLeft(), bounds.bottomLeft()) + tickStart = tickBounds.left() + tickStop = bounds.left() + tickDir = 1 + axis = 0 + elif self.orientation == 'top': + span = (bounds.bottomLeft(), bounds.bottomRight()) + tickStart = tickBounds.bottom() + tickStop = bounds.bottom() + tickDir = -1 + axis = 1 + elif self.orientation == 'bottom': + span = (bounds.topLeft(), bounds.topRight()) + tickStart = tickBounds.top() + tickStop = bounds.top() + tickDir = 1 + axis = 1 + #print tickStart, tickStop, span + + ## draw long line along axis + p.setPen(self.pen()) + p.drawLine(*span) + p.translate(0.5,0) ## resolves some damn pixel ambiguity + + ## determine size of this item in pixels + points = list(map(self.mapToDevice, span)) + if None in points: + return + lengthInPixels = Point(points[1] - points[0]).length() + if lengthInPixels == 0: + return + + if self._tickLevels is None: + tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels) + tickStrings = None + else: + ## parse self.tickLevels into the formats returned by tickLevels() and tickStrings() + tickLevels = [] + tickStrings = [] + for level in self._tickLevels: + values = [] + strings = [] + tickLevels.append((None, values)) + tickStrings.append(strings) + for val, strn in level: + 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 axis == 0: + xScale = -bounds.height() / dif + offset = self.range[0] * xScale - bounds.height() + else: + xScale = bounds.width() / dif + offset = self.range[0] * xScale + + xRange = [x * xScale - offset for x in self.range] + xMin = min(xRange) + xMax = max(xRange) + + prof.mark('init') + + tickPositions = [] # remembers positions of previously drawn ticks + + ## draw ticks + ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) + ## draw three different intervals, long ticks first + + for i in range(len(tickLevels)): + tickPositions.append([]) + ticks = tickLevels[i][1] + + ## length of tick + tickLength = self.tickLength / ((i*1.0)+1.0) + + lineAlpha = 255 / (i+1) + if self.grid is not False: + lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.) + + for v in ticks: + ## determine actual position to draw this tick + x = (v * xScale) - offset + if x < xMin or x > xMax: ## last check to make sure no out-of-bounds ticks are drawn + tickPositions[i].append(None) + continue + tickPositions[i].append(x) + + p1 = [x, x] + p2 = [x, x] + p1[axis] = tickStart + p2[axis] = tickStop + if self.grid is False: + p2[axis] += tickLength*tickDir + tickPen = self.pen() + color = tickPen.color() + color.setAlpha(lineAlpha) + tickPen.setColor(color) + p.setPen(tickPen) + p.drawLine(Point(p1), Point(p2)) + prof.mark('draw ticks') + + ## Draw text until there is no more room (or no more text) + textRects = [] + for i in range(len(tickLevels)): + ## Get the list of strings to display for this level + if tickStrings is None: + spacing, values = tickLevels[i] + strings = self.tickStrings(values, self.scale, spacing) + else: + strings = tickStrings[i] + + if len(strings) == 0: + continue + + ## ignore strings belonging to ticks that were previously ignored + for j in range(len(strings)): + if tickPositions[i][j] is None: + strings[j] = None + + textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) + if i > 0: ## always draw top level + ## measure all text, make sure there's enough room + if axis == 0: + textSize = np.sum([r.height() for r in textRects]) + else: + textSize = np.sum([r.width() for r in textRects]) + + ## If the strings are too crowded, stop drawing text now + textFillRatio = float(textSize) / lengthInPixels + if textFillRatio > 0.7: + break + #spacing, values = tickLevels[best] + #strings = self.tickStrings(values, self.scale, spacing) + for j in range(len(strings)): + vstr = strings[j] + if vstr is None:## this tick was ignored because it is out of bounds + continue + x = tickPositions[i][j] + textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + height = textRect.height() + self.textHeight = height + if self.orientation == 'left': + textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + + p.setPen(self.pen()) + p.drawText(rect, textFlags, vstr) + prof.mark('draw text') + prof.finish() + + def show(self): + + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + GraphicsWidget.show(self) + + def hide(self): + if self.orientation in ['left', 'right']: + self.setWidth(0) + else: + self.setHeight(0) + GraphicsWidget.hide(self) + + def wheelEvent(self, ev): + if self.linkedView() is None: + return + if self.orientation in ['left', 'right']: + self.linkedView().wheelEvent(ev, axis=1) + else: + self.linkedView().wheelEvent(ev, axis=0) + ev.accept() + + def mouseDragEvent(self, event): + if self.linkedView() is None: + return + if self.orientation in ['left', 'right']: + return self.linkedView().mouseDragEvent(event, axis=1) + else: + return self.linkedView().mouseDragEvent(event, axis=0) + + def mouseClickEvent(self, event): + if self.linkedView() is None: + return + return self.linkedView().mouseClickEvent(event) diff --git a/pyqtgraph/graphicsItems/ButtonItem.py b/pyqtgraph/graphicsItems/ButtonItem.py new file mode 100644 index 00000000..741f2666 --- /dev/null +++ b/pyqtgraph/graphicsItems/ButtonItem.py @@ -0,0 +1,58 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsObject import GraphicsObject + +__all__ = ['ButtonItem'] +class ButtonItem(GraphicsObject): + """Button graphicsItem displaying an image.""" + + clicked = QtCore.Signal(object) + + def __init__(self, imageFile=None, width=None, parentItem=None, pixmap=None): + self.enabled = True + GraphicsObject.__init__(self) + if imageFile is not None: + self.setImageFile(imageFile) + elif pixmap is not None: + self.setPixmap(pixmap) + + if width is not None: + s = float(width) / self.pixmap.width() + self.scale(s, s) + if parentItem is not None: + self.setParentItem(parentItem) + self.setOpacity(0.7) + + def setImageFile(self, imageFile): + self.setPixmap(QtGui.QPixmap(imageFile)) + + def setPixmap(self, pixmap): + self.pixmap = pixmap + self.update() + + def mouseClickEvent(self, ev): + if self.enabled: + self.clicked.emit(self) + + def mouseHoverEvent(self, ev): + if not self.enabled: + return + if ev.isEnter(): + self.setOpacity(1.0) + else: + self.setOpacity(0.7) + + def disable(self): + self.enabled = False + self.setOpacity(0.4) + + def enable(self): + self.enabled = True + self.setOpacity(0.7) + + def paint(self, p, *args): + p.setRenderHint(p.Antialiasing) + p.drawPixmap(0, 0, self.pixmap) + + def boundingRect(self): + return QtCore.QRectF(self.pixmap.rect()) + diff --git a/pyqtgraph/graphicsItems/CurvePoint.py b/pyqtgraph/graphicsItems/CurvePoint.py new file mode 100644 index 00000000..668830f7 --- /dev/null +++ b/pyqtgraph/graphicsItems/CurvePoint.py @@ -0,0 +1,117 @@ +from pyqtgraph.Qt import QtGui, QtCore +from . import ArrowItem +import numpy as np +from pyqtgraph.Point import Point +import weakref +from .GraphicsObject import GraphicsObject + +__all__ = ['CurvePoint', 'CurveArrow'] +class CurvePoint(GraphicsObject): + """A GraphicsItem that sets its location to a point on a PlotCurveItem. + Also rotates to be tangent to the curve. + The position along the curve is a Qt property, and thus can be easily animated. + + Note: This class does not display anything; see CurveArrow for an applied example + """ + + def __init__(self, curve, index=0, pos=None, rotate=True): + """Position can be set either as an index referring to the sample number or + the position 0.0 - 1.0 + If *rotate* is True, then the item rotates to match the tangent of the curve. + """ + + GraphicsObject.__init__(self) + #QObjectWorkaround.__init__(self) + self._rotate = rotate + self.curve = weakref.ref(curve) + self.setParentItem(curve) + self.setProperty('position', 0.0) + self.setProperty('index', 0) + + if hasattr(self, 'ItemHasNoContents'): + self.setFlags(self.flags() | self.ItemHasNoContents) + + if pos is not None: + self.setPos(pos) + else: + self.setIndex(index) + + def setPos(self, pos): + self.setProperty('position', float(pos))## cannot use numpy types here, MUST be python float. + + def setIndex(self, index): + self.setProperty('index', int(index)) ## cannot use numpy types here, MUST be python int. + + def event(self, ev): + if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: + return False + + if ev.propertyName() == 'index': + index = self.property('index') + if 'QVariant' in repr(index): + index = index.toInt()[0] + elif ev.propertyName() == 'position': + index = None + else: + return False + + (x, y) = self.curve().getData() + if index is None: + #print ev.propertyName(), self.property('position').toDouble()[0], self.property('position').typeName() + pos = self.property('position') + if 'QVariant' in repr(pos): ## need to support 2 APIs :( + pos = pos.toDouble()[0] + index = (len(x)-1) * np.clip(pos, 0.0, 1.0) + + if index != int(index): ## interpolate floating-point values + i1 = int(index) + i2 = np.clip(i1+1, 0, len(x)-1) + s2 = index-i1 + s1 = 1.0-s2 + newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2) + else: + index = int(index) + i1 = np.clip(index-1, 0, len(x)-1) + i2 = np.clip(index+1, 0, len(x)-1) + newPos = (x[index], y[index]) + + p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) + p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) + ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians + self.resetTransform() + if self._rotate: + self.rotate(180+ ang * 180 / np.pi) ## takes degrees + QtGui.QGraphicsItem.setPos(self, *newPos) + return True + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): + anim = QtCore.QPropertyAnimation(self, prop) + anim.setDuration(duration) + anim.setStartValue(start) + anim.setEndValue(end) + anim.setLoopCount(loop) + return anim + + +class CurveArrow(CurvePoint): + """Provides an arrow that points to any specific sample on a PlotCurveItem. + Provides properties that can be animated.""" + + def __init__(self, curve, index=0, pos=None, **opts): + CurvePoint.__init__(self, curve, index=index, pos=pos) + if opts.get('pxMode', True): + opts['pxMode'] = False + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + opts['angle'] = 0 + self.arrow = ArrowItem.ArrowItem(**opts) + self.arrow.setParentItem(self) + + def setStyle(**opts): + return self.arrow.setStyle(**opts) + diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py new file mode 100644 index 00000000..e0011177 --- /dev/null +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -0,0 +1,23 @@ +import pyqtgraph as pg + +class FillBetweenItem(pg.QtGui.QGraphicsPathItem): + """ + GraphicsItem filling the space between two PlotDataItems. + """ + def __init__(self, p1, p2, brush=None): + pg.QtGui.QGraphicsPathItem.__init__(self) + self.p1 = p1 + self.p2 = p2 + p1.sigPlotChanged.connect(self.updatePath) + p2.sigPlotChanged.connect(self.updatePath) + if brush is not None: + self.setBrush(pg.mkBrush(brush)) + self.setZValue(min(p1.zValue(), p2.zValue())-1) + self.updatePath() + + def updatePath(self): + p1 = self.p1.curve.path + p2 = self.p2.curve.path + path = pg.QtGui.QPainterPath() + path.addPolygon(p1.toSubpathPolygons()[0] + p2.toReversed().toSubpathPolygons()[0]) + self.setPath(path) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py new file mode 100644 index 00000000..3c078ede --- /dev/null +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -0,0 +1,880 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import sortList +import pyqtgraph.functions as fn +from .GraphicsObject import GraphicsObject +from .GraphicsWidget import GraphicsWidget +import weakref +from pyqtgraph.pgcollections import OrderedDict +import numpy as np + +__all__ = ['TickSliderItem', 'GradientEditorItem'] + + +Gradients = OrderedDict([ + ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), + ('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), + ('yellowy', {'ticks': [(0.0, (0, 0, 0, 255)), (0.2328863796753704, (32, 0, 129, 255)), (0.8362738179251941, (255, 255, 0, 255)), (0.5257586450247, (115, 15, 255, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'} ), + ('bipolar', {'ticks': [(0.0, (0, 255, 255, 255)), (1.0, (255, 255, 0, 255)), (0.5, (0, 0, 0, 255)), (0.25, (0, 0, 255, 255)), (0.75, (255, 0, 0, 255))], 'mode': 'rgb'}), + ('spectrum', {'ticks': [(1.0, (255, 0, 255, 255)), (0.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + ('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + ('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), + ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), +]) + + +class TickSliderItem(GraphicsWidget): + ## public class + """**Bases:** :class:`GraphicsWidget ` + + A rectangular item with tick marks along its length that can (optionally) be moved by the user.""" + + def __init__(self, orientation='bottom', allowAdd=True, **kargs): + """ + ============= ================================================================================= + **Arguments** + orientation Set the orientation of the gradient. Options are: 'left', 'right' + 'top', and 'bottom'. + allowAdd Specifies whether ticks can be added to the item by the user. + tickPen Default is white. Specifies the color of the outline of the ticks. + Can be any of the valid arguments for :func:`mkPen ` + ============= ================================================================================= + """ + ## public + GraphicsWidget.__init__(self) + self.orientation = orientation + self.length = 100 + self.tickSize = 15 + self.ticks = {} + self.maxDim = 20 + self.allowAdd = allowAdd + if 'tickPen' in kargs: + self.tickPen = fn.mkPen(kargs['tickPen']) + else: + self.tickPen = fn.mkPen('w') + + self.orientations = { + 'left': (90, 1, 1), + 'right': (90, 1, 1), + 'top': (0, 1, -1), + 'bottom': (0, 1, 1) + } + + self.setOrientation(orientation) + #self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + #self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setMouseTracking(True) + + #def boundingRect(self): + #return self.mapRectFromParent(self.geometry()).normalized() + + #def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise. + #p = QtGui.QPainterPath() + #p.addRect(self.boundingRect()) + #return p + + def paint(self, p, opt, widget): + #p.setPen(fn.mkPen('g', width=3)) + #p.drawRect(self.boundingRect()) + return + + def keyPressEvent(self, ev): + ev.ignore() + + def setMaxDim(self, mx=None): + if mx is None: + mx = self.maxDim + else: + self.maxDim = mx + + if self.orientation in ['bottom', 'top']: + self.setFixedHeight(mx) + self.setMaximumWidth(16777215) + else: + self.setFixedWidth(mx) + self.setMaximumHeight(16777215) + + + def setOrientation(self, orientation): + ## public + """Set the orientation of the TickSliderItem. + + ============= =================================================================== + **Arguments** + orientation Options are: 'left', 'right', 'top', 'bottom' + The orientation option specifies which side of the slider the + ticks are on, as well as whether the slider is vertical ('right' + and 'left') or horizontal ('top' and 'bottom'). + ============= =================================================================== + """ + self.orientation = orientation + self.setMaxDim() + self.resetTransform() + ort = orientation + if ort == 'top': + self.scale(1, -1) + self.translate(0, -self.height()) + elif ort == 'left': + self.rotate(270) + self.scale(1, -1) + self.translate(-self.height(), -self.maxDim) + elif ort == 'right': + self.rotate(270) + self.translate(-self.height(), 0) + #self.setPos(0, -self.height()) + elif ort != 'bottom': + raise Exception("%s is not a valid orientation. Options are 'left', 'right', 'top', and 'bottom'" %str(ort)) + + self.translate(self.tickSize/2., 0) + + def addTick(self, x, color=None, movable=True): + ## public + """ + Add a tick to the item. + + ============= ================================================================== + **Arguments** + x Position where tick should be added. + color Color of added tick. If color is not specified, the color will be + white. + movable Specifies whether the tick is movable with the mouse. + ============= ================================================================== + """ + + if color is None: + color = QtGui.QColor(255,255,255) + tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen) + self.ticks[tick] = x + tick.setParentItem(self) + return tick + + def removeTick(self, tick): + ## public + """ + Removes the specified tick. + """ + del self.ticks[tick] + tick.setParentItem(None) + if self.scene() is not None: + self.scene().removeItem(tick) + + def tickMoved(self, tick, pos): + #print "tick changed" + ## Correct position of tick if it has left bounds. + newX = min(max(0, pos.x()), self.length) + pos.setX(newX) + tick.setPos(pos) + self.ticks[tick] = float(newX) / self.length + + def tickMoveFinished(self, tick): + pass + + def tickClicked(self, tick, ev): + if ev.button() == QtCore.Qt.RightButton: + self.removeTick(tick) + + def widgetLength(self): + if self.orientation in ['bottom', 'top']: + return self.width() + else: + return self.height() + + def resizeEvent(self, ev): + wlen = max(40, self.widgetLength()) + self.setLength(wlen-self.tickSize-2) + self.setOrientation(self.orientation) + #bounds = self.scene().itemsBoundingRect() + #bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) + #bounds.setRight(max(self.length + self.tickSize, bounds.right())) + #self.setSceneRect(bounds) + #self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) + + def setLength(self, newLen): + #private + for t, x in list(self.ticks.items()): + t.setPos(x * newLen + 1, t.pos().y()) + self.length = float(newLen) + + #def mousePressEvent(self, ev): + #QtGui.QGraphicsView.mousePressEvent(self, ev) + #self.ignoreRelease = False + #for i in self.items(ev.pos()): + #if isinstance(i, Tick): + #self.ignoreRelease = True + #break + ##if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks + ##self.ignoreRelease = True + + #def mouseReleaseEvent(self, ev): + #QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + #if self.ignoreRelease: + #return + + #pos = self.mapToScene(ev.pos()) + + #if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + #if pos.x() < 0 or pos.x() > self.length: + #return + #if pos.y() < 0 or pos.y() > self.tickSize: + #return + #pos.setX(min(max(pos.x(), 0), self.length)) + #self.addTick(pos.x()/self.length) + #elif ev.button() == QtCore.Qt.RightButton: + #self.showMenu(ev) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + pos = ev.pos() + if pos.x() < 0 or pos.x() > self.length: + return + if pos.y() < 0 or pos.y() > self.tickSize: + return + pos.setX(min(max(pos.x(), 0), self.length)) + self.addTick(pos.x()/self.length) + elif ev.button() == QtCore.Qt.RightButton: + self.showMenu(ev) + + #if ev.button() == QtCore.Qt.RightButton: + #if self.moving: + #ev.accept() + #self.setPos(self.startPosition) + #self.moving = False + #self.sigMoving.emit(self) + #self.sigMoved.emit(self) + #else: + #pass + #self.view().tickClicked(self, ev) + ###remove + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.RightButton) + ## show ghost tick + #self.currentPen = fn.mkPen(255, 0,0) + #else: + #self.currentPen = self.pen + #self.update() + + def showMenu(self, ev): + pass + + def setTickColor(self, tick, color): + """Set the color of the specified tick. + + ============= ================================================================== + **Arguments** + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted to change the middle tick, the index would be 1. + color The color to make the tick. Can be any argument that is valid for + :func:`mkBrush ` + ============= ================================================================== + """ + tick = self.getTick(tick) + tick.color = color + tick.update() + #tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) + + def setTickValue(self, tick, val): + ## public + """ + Set the position (along the slider) of the tick. + + ============= ================================================================== + **Arguments** + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted to change the middle tick, the index would be 1. + val The desired position of the tick. If val is < 0, position will be + set to 0. If val is > 1, position will be set to 1. + ============= ================================================================== + """ + tick = self.getTick(tick) + val = min(max(0.0, val), 1.0) + x = val * self.length + pos = tick.pos() + pos.setX(x) + tick.setPos(pos) + self.ticks[tick] = val + + def tickValue(self, tick): + ## public + """Return the value (from 0.0 to 1.0) of the specified tick. + + ============= ================================================================== + **Arguments** + tick Can be either an integer corresponding to the index of the tick + or a Tick object. Ex: if you had a slider with 3 ticks and you + wanted the value of the middle tick, the index would be 1. + ============= ================================================================== + """ + tick = self.getTick(tick) + return self.ticks[tick] + + def getTick(self, tick): + ## public + """Return the Tick object at the specified index. + + ============= ================================================================== + **Arguments** + tick An integer corresponding to the index of the desired tick. If the + argument is not an integer it will be returned unchanged. + ============= ================================================================== + """ + if type(tick) is int: + tick = self.listTicks()[tick][0] + return tick + + #def mouseMoveEvent(self, ev): + #QtGui.QGraphicsView.mouseMoveEvent(self, ev) + + def listTicks(self): + """Return a sorted list of all the Tick objects on the slider.""" + ## public + ticks = list(self.ticks.items()) + sortList(ticks, lambda a,b: cmp(a[1], b[1])) ## see pyqtgraph.python2_3.sortList + return ticks + + +class GradientEditorItem(TickSliderItem): + """ + **Bases:** :class:`TickSliderItem ` + + An item that can be used to define a color gradient. Implements common pre-defined gradients that are + customizable by the user. :class: `GradientWidget ` provides a widget + with a GradientEditorItem that can be added to a GUI. + + ================================ =========================================================== + **Signals** + sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal + is emitted in real time while ticks are being dragged or + colors are being changed. + sigGradientChangeFinished(self) Signal is emitted when the gradient is finished changing. + ================================ =========================================================== + + """ + + sigGradientChanged = QtCore.Signal(object) + sigGradientChangeFinished = QtCore.Signal(object) + + def __init__(self, *args, **kargs): + """ + Create a new GradientEditorItem. + All arguments are passed to :func:`TickSliderItem.__init__ ` + + ============= ================================================================================= + **Arguments** + orientation Set the orientation of the gradient. Options are: 'left', 'right' + 'top', and 'bottom'. + allowAdd Default is True. Specifies whether ticks can be added to the item. + tickPen Default is white. Specifies the color of the outline of the ticks. + Can be any of the valid arguments for :func:`mkPen ` + ============= ================================================================================= + """ + self.currentTick = None + self.currentTickColor = None + self.rectSize = 15 + self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, self.rectSize, 100, self.rectSize)) + self.backgroundRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) + self.backgroundRect.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern)) + self.colorMode = 'rgb' + + TickSliderItem.__init__(self, *args, **kargs) + + self.colorDialog = QtGui.QColorDialog() + self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) + self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + + self.colorDialog.currentColorChanged.connect(self.currentColorChanged) + self.colorDialog.rejected.connect(self.currentColorRejected) + self.colorDialog.accepted.connect(self.currentColorAccepted) + + self.backgroundRect.setParentItem(self) + self.gradRect.setParentItem(self) + + self.setMaxDim(self.rectSize + self.tickSize) + + self.rgbAction = QtGui.QAction('RGB', self) + self.rgbAction.setCheckable(True) + self.rgbAction.triggered.connect(lambda: self.setColorMode('rgb')) + self.hsvAction = QtGui.QAction('HSV', self) + self.hsvAction.setCheckable(True) + self.hsvAction.triggered.connect(lambda: self.setColorMode('hsv')) + + self.menu = QtGui.QMenu() + + ## build context menu of gradients + l = self.length + self.length = 100 + global Gradients + for g in Gradients: + px = QtGui.QPixmap(100, 15) + p = QtGui.QPainter(px) + self.restoreState(Gradients[g]) + grad = self.getGradient() + brush = QtGui.QBrush(grad) + p.fillRect(QtCore.QRect(0, 0, 100, 15), brush) + p.end() + label = QtGui.QLabel() + label.setPixmap(px) + label.setContentsMargins(1, 1, 1, 1) + act = QtGui.QWidgetAction(self) + act.setDefaultWidget(label) + act.triggered.connect(self.contextMenuClicked) + act.name = g + self.menu.addAction(act) + self.length = l + self.menu.addSeparator() + self.menu.addAction(self.rgbAction) + self.menu.addAction(self.hsvAction) + + + for t in list(self.ticks.keys()): + self.removeTick(t) + self.addTick(0, QtGui.QColor(0,0,0), True) + self.addTick(1, QtGui.QColor(255,0,0), True) + self.setColorMode('rgb') + self.updateGradient() + + def setOrientation(self, orientation): + ## public + """ + Set the orientation of the GradientEditorItem. + + ============= =================================================================== + **Arguments** + orientation Options are: 'left', 'right', 'top', 'bottom' + The orientation option specifies which side of the gradient the + ticks are on, as well as whether the gradient is vertical ('right' + and 'left') or horizontal ('top' and 'bottom'). + ============= =================================================================== + """ + TickSliderItem.setOrientation(self, orientation) + self.translate(0, self.rectSize) + + def showMenu(self, ev): + #private + self.menu.popup(ev.screenPos().toQPoint()) + + def contextMenuClicked(self, b=None): + #private + #global Gradients + act = self.sender() + self.loadPreset(act.name) + + def loadPreset(self, name): + """ + Load a predefined gradient. + + """ ## TODO: provide image with names of defined gradients + #global Gradients + self.restoreState(Gradients[name]) + + def setColorMode(self, cm): + """ + Set the color mode for the gradient. Options are: 'hsv', 'rgb' + + """ + + ## public + if cm not in ['rgb', 'hsv']: + raise Exception("Unknown color mode %s. Options are 'rgb' and 'hsv'." % str(cm)) + + try: + self.rgbAction.blockSignals(True) + self.hsvAction.blockSignals(True) + self.rgbAction.setChecked(cm == 'rgb') + self.hsvAction.setChecked(cm == 'hsv') + finally: + self.rgbAction.blockSignals(False) + self.hsvAction.blockSignals(False) + self.colorMode = cm + self.updateGradient() + + def updateGradient(self): + #private + self.gradient = self.getGradient() + self.gradRect.setBrush(QtGui.QBrush(self.gradient)) + self.sigGradientChanged.emit(self) + + def setLength(self, newLen): + #private (but maybe public) + TickSliderItem.setLength(self, newLen) + self.backgroundRect.setRect(1, -self.rectSize, newLen, self.rectSize) + self.gradRect.setRect(1, -self.rectSize, newLen, self.rectSize) + self.updateGradient() + + def currentColorChanged(self, color): + #private + if color.isValid() and self.currentTick is not None: + self.setTickColor(self.currentTick, color) + self.updateGradient() + + def currentColorRejected(self): + #private + self.setTickColor(self.currentTick, self.currentTickColor) + self.updateGradient() + + def currentColorAccepted(self): + self.sigGradientChangeFinished.emit(self) + + def tickClicked(self, tick, ev): + #private + if ev.button() == QtCore.Qt.LeftButton: + if not tick.colorChangeAllowed: + return + self.currentTick = tick + self.currentTickColor = tick.color + self.colorDialog.setCurrentColor(tick.color) + self.colorDialog.open() + #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + #if color.isValid(): + #self.setTickColor(tick, color) + #self.updateGradient() + elif ev.button() == QtCore.Qt.RightButton: + if not tick.removeAllowed: + return + if len(self.ticks) > 2: + self.removeTick(tick) + self.updateGradient() + + def tickMoved(self, tick, pos): + #private + TickSliderItem.tickMoved(self, tick, pos) + self.updateGradient() + + def tickMoveFinished(self, tick): + self.sigGradientChangeFinished.emit(self) + + + def getGradient(self): + """Return a QLinearGradient object.""" + g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) + if self.colorMode == 'rgb': + ticks = self.listTicks() + g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) + elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop + ticks = self.listTicks() + stops = [] + stops.append((ticks[0][1], ticks[0][0].color)) + for i in range(1,len(ticks)): + x1 = ticks[i-1][1] + x2 = ticks[i][1] + dx = (x2-x1) / 10. + for j in range(1,10): + x = x1 + dx*j + stops.append((x, self.getColor(x))) + stops.append((x2, self.getColor(x2))) + g.setStops(stops) + return g + + def getColor(self, x, toQColor=True): + """ + Return a color for a given value. + + ============= ================================================================== + **Arguments** + x Value (position on gradient) of requested color. + toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple. + ============= ================================================================== + """ + ticks = self.listTicks() + if x <= ticks[0][1]: + c = ticks[0][0].color + if toQColor: + return QtGui.QColor(c) # always copy colors before handing them out + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + if x >= ticks[-1][1]: + c = ticks[-1][0].color + if toQColor: + return QtGui.QColor(c) # always copy colors before handing them out + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + + x2 = ticks[0][1] + for i in range(1,len(ticks)): + x1 = x2 + x2 = ticks[i][1] + if x1 <= x and x2 >= x: + break + + dx = (x2-x1) + if dx == 0: + f = 0. + else: + f = (x-x1) / dx + c1 = ticks[i-1][0].color + c2 = ticks[i][0].color + if self.colorMode == 'rgb': + r = c1.red() * (1.-f) + c2.red() * f + g = c1.green() * (1.-f) + c2.green() * f + b = c1.blue() * (1.-f) + c2.blue() * f + a = c1.alpha() * (1.-f) + c2.alpha() * f + if toQColor: + return QtGui.QColor(r, g, b,a) + else: + return (r,g,b,a) + elif self.colorMode == 'hsv': + h1,s1,v1,_ = c1.getHsv() + h2,s2,v2,_ = c2.getHsv() + h = h1 * (1.-f) + h2 * f + s = s1 * (1.-f) + s2 * f + v = v1 * (1.-f) + v2 * f + c = QtGui.QColor() + c.setHsv(h,s,v) + if toQColor: + return c + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + + def getLookupTable(self, nPts, alpha=None): + """ + Return an RGB(A) lookup table (ndarray). + + ============= ============================================================================ + **Arguments** + nPts The number of points in the returned lookup table. + alpha True, False, or None - Specifies whether or not alpha values are included + in the table.If alpha is None, alpha will be automatically determined. + ============= ============================================================================ + """ + if alpha is None: + alpha = self.usesAlpha() + if alpha: + table = np.empty((nPts,4), dtype=np.ubyte) + else: + table = np.empty((nPts,3), dtype=np.ubyte) + + for i in range(nPts): + x = float(i)/(nPts-1) + color = self.getColor(x, toQColor=False) + table[i] = color[:table.shape[1]] + + return table + + def usesAlpha(self): + """Return True if any ticks have an alpha < 255""" + + ticks = self.listTicks() + for t in ticks: + if t[0].color.alpha() < 255: + return True + + return False + + def isLookupTrivial(self): + """Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0""" + ticks = self.listTicks() + if len(ticks) != 2: + return False + if ticks[0][1] != 0.0 or ticks[1][1] != 1.0: + return False + c1 = fn.colorTuple(ticks[0][0].color) + c2 = fn.colorTuple(ticks[1][0].color) + if c1 != (0,0,0,255) or c2 != (255,255,255,255): + return False + return True + + + def mouseReleaseEvent(self, ev): + #private + TickSliderItem.mouseReleaseEvent(self, ev) + self.updateGradient() + + def addTick(self, x, color=None, movable=True, finish=True): + """ + Add a tick to the gradient. Return the tick. + + ============= ================================================================== + **Arguments** + x Position where tick should be added. + color Color of added tick. If color is not specified, the color will be + the color of the gradient at the specified position. + movable Specifies whether the tick is movable with the mouse. + ============= ================================================================== + """ + + + if color is None: + color = self.getColor(x) + t = TickSliderItem.addTick(self, x, color=color, movable=movable) + t.colorChangeAllowed = True + t.removeAllowed = True + + if finish: + self.sigGradientChangeFinished.emit(self) + return t + + + def removeTick(self, tick, finish=True): + TickSliderItem.removeTick(self, tick) + if finish: + self.sigGradientChangeFinished.emit(self) + + + def saveState(self): + """ + Return a dictionary with parameters for rebuilding the gradient. Keys will include: + + - 'mode': hsv or rgb + - 'ticks': a list of tuples (pos, (r,g,b,a)) + """ + ## public + ticks = [] + 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} + return state + + def restoreState(self, state): + """ + Restore the gradient specified in state. + + ============= ==================================================================== + **Arguments** + state A dictionary with same structure as those returned by + :func:`saveState ` + + Keys must include: + + - 'mode': hsv or rgb + - 'ticks': a list of tuples (pos, (r,g,b,a)) + ============= ==================================================================== + """ + ## public + self.setColorMode(state['mode']) + for t in list(self.ticks.keys()): + self.removeTick(t, finish=False) + for t in state['ticks']: + c = QtGui.QColor(*t[1]) + self.addTick(t[0], c, finish=False) + self.updateGradient() + self.sigGradientChangeFinished.emit(self) + + +class Tick(GraphicsObject): + ## private class + + sigMoving = QtCore.Signal(object) + sigMoved = QtCore.Signal(object) + + def __init__(self, view, pos, color, movable=True, scale=10, pen='w'): + self.movable = movable + self.moving = False + self.view = weakref.ref(view) + self.scale = scale + self.color = color + self.pen = fn.mkPen(pen) + self.hoverPen = fn.mkPen(255,255,0) + self.currentPen = self.pen + self.pg = QtGui.QPainterPath(QtCore.QPointF(0,0)) + self.pg.lineTo(QtCore.QPointF(-scale/3**0.5, scale)) + self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale)) + self.pg.closeSubpath() + + GraphicsObject.__init__(self) + self.setPos(pos[0], pos[1]) + if self.movable: + self.setZValue(1) + else: + self.setZValue(0) + + def boundingRect(self): + return self.pg.boundingRect() + + def shape(self): + return self.pg + + def paint(self, p, *args): + p.setRenderHints(QtGui.QPainter.Antialiasing) + p.fillPath(self.pg, fn.mkBrush(self.color)) + + p.setPen(self.currentPen) + p.drawPath(self.pg) + + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + newPos = self.cursorOffset + self.mapToParent(ev.pos()) + newPos.setY(self.pos().y()) + + self.setPos(newPos) + self.view().tickMoved(self, newPos) + self.sigMoving.emit(self) + if ev.isFinish(): + self.moving = False + self.sigMoved.emit(self) + self.view().tickMoveFinished(self) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton and self.moving: + ev.accept() + self.setPos(self.startPosition) + self.view().tickMoved(self, self.startPosition) + self.moving = False + self.sigMoving.emit(self) + 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): + ev.acceptClicks(QtCore.Qt.LeftButton) + ev.acceptClicks(QtCore.Qt.RightButton) + self.currentPen = self.hoverPen + else: + self.currentPen = self.pen + self.update() + + #def mouseMoveEvent(self, ev): + ##print self, "move", ev.scenePos() + #if not self.movable: + #return + #if not ev.buttons() & QtCore.Qt.LeftButton: + #return + + + #newPos = ev.scenePos() + self.mouseOffset + #newPos.setY(self.pos().y()) + ##newPos.setX(min(max(newPos.x(), 0), 100)) + #self.setPos(newPos) + #self.view().tickMoved(self, newPos) + #self.movedSincePress = True + ##self.emit(QtCore.SIGNAL('tickChanged'), self) + #ev.accept() + + #def mousePressEvent(self, ev): + #self.movedSincePress = False + #if ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.mouseOffset = self.pos() - ev.scenePos() + #self.pressPos = ev.scenePos() + #elif ev.button() == QtCore.Qt.RightButton: + #ev.accept() + ##if self.endTick: + ##return + ##self.view.tickChanged(self, delete=True) + + #def mouseReleaseEvent(self, ev): + ##print self, "release", ev.scenePos() + #if not self.movedSincePress: + #self.view().tickClicked(self, ev) + + ##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: + ##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + ##if color.isValid(): + ##self.color = color + ##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + ###self.emit(QtCore.SIGNAL('tickChanged'), self) + ##self.view.tickChanged(self) diff --git a/pyqtgraph/graphicsItems/GradientLegend.py b/pyqtgraph/graphicsItems/GradientLegend.py new file mode 100644 index 00000000..4528b7ed --- /dev/null +++ b/pyqtgraph/graphicsItems/GradientLegend.py @@ -0,0 +1,114 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .UIGraphicsItem import * +import pyqtgraph.functions as fn + +__all__ = ['GradientLegend'] + +class GradientLegend(UIGraphicsItem): + """ + Draws a color gradient rectangle along with text labels denoting the value at specific + points along the gradient. + """ + + def __init__(self, size, offset): + self.size = size + self.offset = offset + UIGraphicsItem.__init__(self) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.brush = QtGui.QBrush(QtGui.QColor(200,0,0)) + self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) + self.labels = {'max': 1, 'min': 0} + self.gradient = QtGui.QLinearGradient() + self.gradient.setColorAt(0, QtGui.QColor(0,0,0)) + self.gradient.setColorAt(1, QtGui.QColor(255,0,0)) + + def setGradient(self, g): + self.gradient = g + self.update() + + def setIntColorScale(self, minVal, maxVal, *args, **kargs): + colors = [fn.intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)] + g = QtGui.QLinearGradient() + for i in range(len(colors)): + x = float(i)/len(colors) + g.setColorAt(x, colors[i]) + self.setGradient(g) + if 'labels' not in kargs: + self.setLabels({str(minVal/10.): 0, str(maxVal): 1}) + else: + self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1}) + + def setLabels(self, l): + """Defines labels to appear next to the color scale. Accepts a dict of {text: value} pairs""" + self.labels = l + self.update() + + def paint(self, p, opt, widget): + UIGraphicsItem.paint(self, p, opt, widget) + rect = self.boundingRect() ## Boundaries of visible area in scene coords. + unit = self.pixelSize() ## Size of one view pixel in scene coords. + if unit[0] is None: + return + + ## determine max width of all labels + labelWidth = 0 + labelHeight = 0 + for k in self.labels: + b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) + labelWidth = max(labelWidth, b.width()) + labelHeight = max(labelHeight, b.height()) + + labelWidth *= unit[0] + labelHeight *= unit[1] + + textPadding = 2 # in px + + if self.offset[0] < 0: + x3 = rect.right() + unit[0] * self.offset[0] + x2 = x3 - labelWidth - unit[0]*textPadding*2 + x1 = x2 - unit[0] * self.size[0] + else: + x1 = rect.left() + unit[0] * self.offset[0] + x2 = x1 + unit[0] * self.size[0] + x3 = x2 + labelWidth + unit[0]*textPadding*2 + if self.offset[1] < 0: + y2 = rect.top() - unit[1] * self.offset[1] + y1 = y2 + unit[1] * self.size[1] + else: + y1 = rect.bottom() - unit[1] * self.offset[1] + y2 = y1 - unit[1] * self.size[1] + self.b = [x1,x2,x3,y1,y2,labelWidth] + + ## Draw background + p.setPen(self.pen) + p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100))) + rect = QtCore.QRectF( + QtCore.QPointF(x1 - unit[0]*textPadding, y1 + labelHeight/2 + unit[1]*textPadding), + QtCore.QPointF(x3, y2 - labelHeight/2 - unit[1]*textPadding) + ) + p.drawRect(rect) + + + ## Have to scale painter so that text and gradients are correct size. Bleh. + p.scale(unit[0], unit[1]) + + ## Draw color bar + self.gradient.setStart(0, y1/unit[1]) + self.gradient.setFinalStop(0, y2/unit[1]) + p.setBrush(self.gradient) + rect = QtCore.QRectF( + QtCore.QPointF(x1/unit[0], y1/unit[1]), + QtCore.QPointF(x2/unit[0], y2/unit[1]) + ) + p.drawRect(rect) + + + ## draw labels + p.setPen(QtGui.QPen(QtGui.QColor(0,0,0))) + tx = x2 + unit[0]*textPadding + lh = labelHeight/unit[1] + for k in self.labels: + y = y1 + self.labels[k] * (y2-y1) + p.drawText(QtCore.QRectF(tx/unit[0], y/unit[1] - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) + + diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py new file mode 100644 index 00000000..6a0825dd --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -0,0 +1,499 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.GraphicsScene import GraphicsScene +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +import weakref +import operator + +class GraphicsItem(object): + """ + **Bases:** :class:`object` + + Abstract class providing useful methods to GraphicsObject and GraphicsWidget. + (This is required because we cannot have multiple inheritance with QObject subclasses.) + + A note about Qt's GraphicsView framework: + + 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. + """ + def __init__(self, register=True): + if not hasattr(self, '_qtBaseClass'): + for b in self.__class__.__bases__: + if issubclass(b, QtGui.QGraphicsItem): + self.__class__._qtBaseClass = b + break + if not hasattr(self, '_qtBaseClass'): + raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self)) + + self._viewWidget = None + self._viewBox = None + self._connectedView = None + self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options. + if register: + GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() + + + + + def getViewWidget(self): + """ + Return the view widget for this item. If the scene has multiple views, only the first view is returned. + The return value is cached; clear the cached value with forgetViewWidget() + """ + if self._viewWidget is None: + scene = self.scene() + if scene is None: + return None + views = scene.views() + if len(views) < 1: + return None + self._viewWidget = weakref.ref(self.scene().views()[0]) + return self._viewWidget() + + def forgetViewWidget(self): + self._viewWidget = None + + def getViewBox(self): + """ + Return the first ViewBox or GraphicsView which bounds this item's visible space. + If this item is not contained within a ViewBox, then the GraphicsView is returned. + If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned. + The result is cached; clear the cache with forgetViewBox() + """ + if self._viewBox is None: + p = self + while True: + p = p.parentItem() + if p is None: + vb = self.getViewWidget() + if vb is None: + return None + else: + self._viewBox = weakref.ref(vb) + break + if hasattr(p, 'implements') and p.implements('ViewBox'): + self._viewBox = weakref.ref(p) + break + return self._viewBox() ## If we made it this far, _viewBox is definitely not None + + def forgetViewBox(self): + self._viewBox = None + + + def deviceTransform(self, viewportTransform=None): + """ + Return the transform that converts local item coordinates to device coordinates (usually pixels). + 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() + + if viewportTransform is None: + view = self.getViewWidget() + if view is None: + return None + viewportTransform = view.viewportTransform() + dt = self._qtBaseClass.deviceTransform(self, viewportTransform) + + #xmag = abs(dt.m11())+abs(dt.m12()) + #ymag = abs(dt.m21())+abs(dt.m22()) + #if xmag * ymag == 0: + if dt.determinant() == 0: ## occurs when deviceTransform is invalid because widget has not been displayed + return None + else: + return dt + + def viewTransform(self): + """Return the transform that maps from local coordinates to the item's ViewBox coordinates + If there is no ViewBox, return the scene transform. + Returns None if the item does not have a view.""" + view = self.getViewBox() + if view is None: + return None + if hasattr(view, 'implements') and view.implements('ViewBox'): + tr = self.itemTransform(view.innerSceneItem()) + if isinstance(tr, tuple): + tr = tr[0] ## difference between pyside and pyqt + return tr + else: + return self.sceneTransform() + #return self.deviceTransform(view.viewportTransform()) + + + + def getBoundingParents(self): + """Return a list of parents to this item that have child clipping enabled.""" + p = self + parents = [] + while True: + p = p.parentItem() + if p is None: + break + if p.flags() & self.ItemClipsChildrenToShape: + parents.append(p) + return parents + + def viewRect(self): + """Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget""" + view = self.getViewBox() + if view is None: + return None + bounds = self.mapRectFromView(view.viewRect()) + if bounds is None: + return None + + bounds = bounds.normalized() + + ## nah. + #for p in self.getBoundingParents(): + #bounds &= self.mapRectFromScene(p.sceneBoundingRect()) + + return bounds + + + + + def pixelVectors(self, direction=None): + """Return vectors in local coordinates representing the width and height of a view pixel. + If direction is specified, then return vectors parallel and orthogonal to it. + + Return (None, None) if pixel size is not yet defined (usually because the item has not yet been displayed) + or if pixel size is below floating-point precision limit. + """ + + dt = self.deviceTransform() + if dt is None: + return None, None + + if direction is None: + direction = Point(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 + if direction.x() == 0: + r = abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22())) + #r = 1.0/(abs(dt.m12()) + abs(dt.m22())) + elif direction.y() == 0: + r = abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21())) + #r = 1.0/(abs(dt.m11()) + abs(dt.m21())) + else: + r = ((abs(dt.m32())/(abs(dt.m12()) + abs(dt.m22()))) * (abs(dt.m31())/(abs(dt.m11()) + abs(dt.m21()))))**0.5 + directionr = direction * r + + viewDir = Point(dt.map(directionr) - dt.map(Point(0,0))) + if viewDir.manhattanLength() == 0: + return None, None ## pixel size cannot be represented on this scale + + orthoDir = Point(viewDir[1], -viewDir[0]) ## orthogonal to line in pixel-space + + try: + normView = viewDir.norm() ## direction of one pixel orthogonal to line + normOrtho = orthoDir.norm() + except: + raise Exception("Invalid direction %s" %directionr) + + + dti = fn.invertQTransform(dt) + return Point(dti.map(normView)-dti.map(Point(0,0))), Point(dti.map(normOrtho)-dti.map(Point(0,0))) + + #vt = self.deviceTransform() + #if vt is None: + #return None + #vt = vt.inverted()[0] + #orig = vt.map(QtCore.QPointF(0, 0)) + #return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig + + def pixelLength(self, direction, ortho=False): + """Return the length of one pixel in the direction indicated (in local coordinates) + If ortho=True, then return the length of one pixel orthogonal to the direction indicated. + + Return None if pixel size is not yet defined (usually because the item has not yet been displayed). + """ + normV, orthoV = self.pixelVectors(direction) + if normV == None or orthoV == None: + return None + if ortho: + return orthoV.length() + return normV.length() + + + + def pixelSize(self): + ## deprecated + v = self.pixelVectors() + if v == (None, None): + return None, None + return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5 + + def pixelWidth(self): + ## deprecated + vt = self.deviceTransform() + if vt is None: + return 0 + vt = fn.invertQTransform(vt) + return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length() + + def pixelHeight(self): + ## deprecated + vt = self.deviceTransform() + if vt is None: + return 0 + vt = fn.invertQTransform(vt) + return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length() + + + def mapToDevice(self, obj): + """ + Return *obj* mapped from local coordinates to device coordinates (pixels). + If there is no device mapping available, return None. + """ + vt = self.deviceTransform() + if vt is None: + return None + return vt.map(obj) + + def mapFromDevice(self, obj): + """ + Return *obj* mapped from device coordinates (pixels) to local coordinates. + If there is no device mapping available, return None. + """ + vt = self.deviceTransform() + if vt is None: + return None + vt = fn.invertQTransform(vt) + return vt.map(obj) + + def mapRectToDevice(self, rect): + """ + Return *rect* mapped from local coordinates to device coordinates (pixels). + If there is no device mapping available, return None. + """ + vt = self.deviceTransform() + if vt is None: + return None + return vt.mapRect(rect) + + def mapRectFromDevice(self, rect): + """ + Return *rect* mapped from device coordinates (pixels) to local coordinates. + If there is no device mapping available, return None. + """ + vt = self.deviceTransform() + if vt is None: + return None + vt = fn.invertQTransform(vt) + return vt.mapRect(rect) + + def mapToView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + return vt.map(obj) + + def mapRectToView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + return vt.mapRect(obj) + + def mapFromView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + vt = fn.invertQTransform(vt) + return vt.map(obj) + + def mapRectFromView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + vt = fn.invertQTransform(vt) + return vt.mapRect(obj) + + def pos(self): + return Point(self._qtBaseClass.pos(self)) + + def viewPos(self): + return self.mapToView(self.mapFromParent(self.pos())) + + def parentItem(self): + ## PyQt bug -- some items are returned incorrectly. + return GraphicsScene.translateGraphicsItem(self._qtBaseClass.parentItem(self)) + + def setParentItem(self, parent): + ## Workaround for Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616 + if parent is not None: + pscene = parent.scene() + if pscene is not None and self.scene() is not pscene: + pscene.addItem(self) + return self._qtBaseClass.setParentItem(self, parent) + + def childItems(self): + ## PyQt bug -- some child items are returned incorrectly. + return list(map(GraphicsScene.translateGraphicsItem, self._qtBaseClass.childItems(self))) + + + def sceneTransform(self): + ## Qt bug: do no allow access to sceneTransform() until + ## the item has a scene. + + if self.scene() is None: + return self.transform() + else: + return self._qtBaseClass.sceneTransform(self) + + + def transformAngle(self, relativeItem=None): + """Return the rotation produced by this item's transform (this assumes there is no shear in the transform) + If relativeItem is given, then the angle is determined relative to that item. + """ + if relativeItem is None: + relativeItem = self.parentItem() + + + tr = self.itemTransform(relativeItem) + if isinstance(tr, tuple): ## difference between pyside and pyqt + tr = tr[0] + vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) + return Point(vec).angle(Point(1,0)) + + + #def itemChange(self, change, value): + #ret = self._qtBaseClass.itemChange(self, change, value) + #if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: + #print "Item scene changed:", self + #self.setChildScene(self) ## This is bizarre. + #return ret + + #def setChildScene(self, ch): + #scene = self.scene() + #for ch2 in ch.childItems(): + #if ch2.scene() is not scene: + #print "item", ch2, "has different scene:", ch2.scene(), scene + #scene.addItem(ch2) + #QtGui.QApplication.processEvents() + #print " --> ", ch2.scene() + #self.setChildScene(ch2) + + def _updateView(self): + ## called to see whether this item has a new view to connect to + ## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange. + + ## It is possible this item has moved to a different ViewBox or widget; + ## clear out previously determined references to these. + self.forgetViewBox() + self.forgetViewWidget() + + ## check for this item's current viewbox or view widget + view = self.getViewBox() + #if view is None: + ##print " no view" + #return + + oldView = None + if self._connectedView is not None: + oldView = self._connectedView() + + if view is oldView: + #print " already have view", view + return + + ## disconnect from previous view + if oldView is not None: + #print "disconnect:", self, oldView + try: + oldView.sigRangeChanged.disconnect(self.viewRangeChanged) + except TypeError: + pass + + try: + oldView.sigTransformChanged.disconnect(self.viewTransformChanged) + except TypeError: + pass + + self._connectedView = None + + ## connect to new view + if view is not None: + #print "connect:", self, view + view.sigRangeChanged.connect(self.viewRangeChanged) + view.sigTransformChanged.connect(self.viewTransformChanged) + self._connectedView = weakref.ref(view) + self.viewRangeChanged() + self.viewTransformChanged() + + ## inform children that their view might have changed + self._replaceView(oldView) + + + def _replaceView(self, oldView, item=None): + if item is None: + item = self + for child in item.childItems(): + if isinstance(child, GraphicsItem): + if child.getViewBox() is oldView: + child._updateView() + #self._replaceView(oldView, child) + else: + self._replaceView(oldView, child) + + + + def viewRangeChanged(self): + """ + Called whenever the view coordinates of the ViewBox containing this item have changed. + """ + pass + + def viewTransformChanged(self): + """ + Called whenever the transformation matrix of the view has changed. + """ + pass + + #def prepareGeometryChange(self): + #self._qtBaseClass.prepareGeometryChange(self) + #self.informViewBoundsChanged() + + def informViewBoundsChanged(self): + """ + Inform this item's container ViewBox that the bounds of this item have changed. + This is used by ViewBox to react if auto-range is enabled. + """ + view = self.getViewBox() + if view is not None and hasattr(view, 'implements') and view.implements('ViewBox'): + view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + + def childrenShape(self): + """Return the union of the shapes of all descendants of this item in local coordinates.""" + childs = self.allChildItems() + shapes = [self.mapFromItem(c, c.shape()) for c in self.allChildItems()] + return reduce(operator.add, shapes) + + def allChildItems(self, root=None): + """Return list of the entire item tree descending from this item.""" + if root is None: + root = self + tree = [] + for ch in root.childItems(): + tree.append(ch) + tree.extend(self.allChildItems(ch)) + return tree + + + def setExportMode(self, export, opts=None): + """ + This method is called by exporters to inform items that they are being drawn for export + with a specific set of options. Items access these via self._exportOptions. + When exporting is complete, _exportOptions is set to False. + """ + if opts is None: + opts = {} + if export: + self._exportOpts = opts + #if 'antialias' not in opts: + #self._exportOpts['antialias'] = True + else: + self._exportOpts = False + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py new file mode 100644 index 00000000..9d48e627 --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -0,0 +1,154 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from .GraphicsWidget import GraphicsWidget +## Must be imported at the end to avoid cyclic-dependency hell: +from .ViewBox import ViewBox +from .PlotItem import PlotItem +from .LabelItem import LabelItem + +__all__ = ['GraphicsLayout'] +class GraphicsLayout(GraphicsWidget): + """ + Used for laying out GraphicsWidgets in a grid. + This is usually created automatically as part of a :class:`GraphicsWindow ` or :class:`GraphicsLayoutWidget `. + """ + + + def __init__(self, parent=None, border=None): + GraphicsWidget.__init__(self, parent) + if border is True: + border = (100,100,100) + self.border = border + self.layout = QtGui.QGraphicsGridLayout() + self.setLayout(self.layout) + self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item + self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item + self.currentRow = 0 + self.currentCol = 0 + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) + + #def resizeEvent(self, ev): + #ret = GraphicsWidget.resizeEvent(self, ev) + #print self.pos(), self.mapToDevice(self.rect().topLeft()) + #return ret + + def nextRow(self): + """Advance to next row for automatic item placement""" + self.currentRow += 1 + self.currentCol = -1 + self.nextColumn() + + def nextColumn(self): + """Advance to next available column + (generally only for internal use--called by addItem)""" + self.currentCol += 1 + while self.getItem(self.currentRow, self.currentCol) is not None: + self.currentCol += 1 + + def nextCol(self, *args, **kargs): + """Alias of nextColumn""" + return self.nextColumn(*args, **kargs) + + def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a PlotItem and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`PlotItem.__init__ ` + Returns the created item. + """ + plot = PlotItem(**kargs) + self.addItem(plot, row, col, rowspan, colspan) + return plot + + def addViewBox(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a ViewBox and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`ViewBox.__init__ ` + Returns the created item. + """ + vb = ViewBox(**kargs) + self.addItem(vb, row, col, rowspan, colspan) + return vb + + def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a LabelItem with *text* and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`LabelItem.__init__ ` + Returns the created item. + + To create a vertical label, use *angle* = -90. + """ + text = LabelItem(text, **kargs) + self.addItem(text, row, col, rowspan, colspan) + return text + + def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create an empty GraphicsLayout and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`GraphicsLayout.__init__ ` + Returns the created item. + """ + layout = GraphicsLayout(**kargs) + self.addItem(layout, row, col, rowspan, colspan) + return layout + + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): + """ + Add an item to the layout and place it in the next available cell (or in the cell specified). + The item must be an instance of a QGraphicsWidget subclass. + """ + if row is None: + row = self.currentRow + if col is None: + col = self.currentCol + + self.items[item] = [] + for i in range(rowspan): + for j in range(colspan): + row2 = row + i + col2 = col + j + if row2 not in self.rows: + self.rows[row2] = {} + self.rows[row2][col2] = item + self.items[item].append((row2, col2)) + + self.layout.addItem(item, row, col, rowspan, colspan) + self.nextColumn() + + def getItem(self, row, col): + """Return the item in (*row*, *col*). If the cell is empty, return None.""" + return self.rows.get(row, {}).get(col, None) + + def boundingRect(self): + return self.rect() + + def paint(self, p, *args): + if self.border is None: + return + p.setPen(fn.mkPen(self.border)) + for i in self.items: + r = i.mapRectToParent(i.boundingRect()) + p.drawRect(r) + + def itemIndex(self, item): + for i in range(self.layout.count()): + if self.layout.itemAt(i).graphicsItem() is item: + return i + raise Exception("Could not determine index of item " + str(item)) + + def removeItem(self, item): + """Remove *item* from the layout.""" + ind = self.itemIndex(item) + self.layout.removeAt(ind) + self.scene().removeItem(item) + + for r,c in self.items[item]: + del self.rows[r][c] + del self.items[item] + self.update() + + def clear(self): + items = [] + for i in list(self.items.keys()): + self.removeItem(i) + + diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py new file mode 100644 index 00000000..121a67ea --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -0,0 +1,31 @@ +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +if not USE_PYSIDE: + import sip +from .GraphicsItem import GraphicsItem + +__all__ = ['GraphicsObject'] +class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): + """ + **Bases:** :class:`GraphicsItem `, :class:`QtGui.QGraphicsObject` + + Extension of QGraphicsObject with some useful methods (provided by :class:`GraphicsItem `) + """ + _qtBaseClass = QtGui.QGraphicsObject + def __init__(self, *args): + QtGui.QGraphicsObject.__init__(self, *args) + self.setFlag(self.ItemSendsGeometryChanges) + GraphicsItem.__init__(self) + + def itemChange(self, change, value): + ret = QtGui.QGraphicsObject.itemChange(self, change, value) + if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: + self._updateView() + if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: + self.informViewBoundsChanged() + + ## workaround for pyqt bug: + ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html + if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + ret = sip.cast(ret, QtGui.QGraphicsItem) + + return ret diff --git a/pyqtgraph/graphicsItems/GraphicsWidget.py b/pyqtgraph/graphicsItems/GraphicsWidget.py new file mode 100644 index 00000000..8f28d208 --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphicsWidget.py @@ -0,0 +1,58 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.GraphicsScene import GraphicsScene +from .GraphicsItem import GraphicsItem + +__all__ = ['GraphicsWidget'] + +class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget): + + _qtBaseClass = QtGui.QGraphicsWidget + def __init__(self, *args, **kargs): + """ + **Bases:** :class:`GraphicsItem `, :class:`QtGui.QGraphicsWidget` + + Extends QGraphicsWidget with several helpful methods and workarounds for PyQt bugs. + Most of the extra functionality is inherited from :class:`GraphicsItem `. + """ + QtGui.QGraphicsWidget.__init__(self, *args, **kargs) + GraphicsItem.__init__(self) + + ## done by GraphicsItem init + #GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() + +## Removed because this causes segmentation faults. Don't know why. +# def itemChange(self, change, value): +# ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here +# if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: +# self._updateView() +# return ret + + #def getMenu(self): + #pass + + def setFixedHeight(self, h): + self.setMaximumHeight(h) + self.setMinimumHeight(h) + + def setFixedWidth(self, h): + self.setMaximumWidth(h) + self.setMinimumWidth(h) + + def height(self): + return self.geometry().height() + + def width(self): + return self.geometry().width() + + def boundingRect(self): + br = self.mapRectFromParent(self.geometry()).normalized() + #print "bounds:", br + return br + + def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise. + p = QtGui.QPainterPath() + p.addRect(self.boundingRect()) + #print "shape:", p.boundingRect() + return p + + diff --git a/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py new file mode 100644 index 00000000..9770b661 --- /dev/null +++ b/pyqtgraph/graphicsItems/GraphicsWidgetAnchor.py @@ -0,0 +1,63 @@ +from ..Qt import QtGui, QtCore +from ..Point import Point + + +class GraphicsWidgetAnchor(object): + """ + Class used to allow GraphicsWidgets to anchor to a specific position on their + parent. + + """ + + def __init__(self): + self.__parent = None + self.__parentAnchor = None + self.__itemAnchor = None + self.__offset = (0,0) + if hasattr(self, 'geometryChanged'): + self.geometryChanged.connect(self.__geometryChanged) + + def anchor(self, itemPos, parentPos, offset=(0,0)): + """ + Anchors the item at its local itemPos to the item's parent at parentPos. + Both positions are expressed in values relative to the size of the item or parent; + a value of 0 indicates left or top edge, while 1 indicates right or bottom edge. + + Optionally, offset may be specified to introduce an absolute offset. + + Example: anchor a box such that its upper-right corner is fixed 10px left + and 10px down from its parent's upper-right corner:: + + box.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10)) + """ + parent = self.parentItem() + if parent is None: + raise Exception("Cannot anchor; parent is not set.") + + if self.__parent is not parent: + if self.__parent is not None: + self.__parent.geometryChanged.disconnect(self.__geometryChanged) + + self.__parent = parent + parent.geometryChanged.connect(self.__geometryChanged) + + self.__itemAnchor = itemPos + self.__parentAnchor = parentPos + self.__offset = offset + self.__geometryChanged() + + def __geometryChanged(self): + if self.__parent is None: + return + if self.__itemAnchor is None: + return + + o = self.mapToParent(Point(0,0)) + a = self.boundingRect().bottomRight() * Point(self.__itemAnchor) + a = self.mapToParent(a) + p = self.__parent.boundingRect().bottomRight() * Point(self.__parentAnchor) + off = Point(self.__offset) + pos = p + (o-a) + off + self.setPos(pos) + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py new file mode 100644 index 00000000..29b0aa2c --- /dev/null +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -0,0 +1,120 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .UIGraphicsItem import * +import numpy as np +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn + +__all__ = ['GridItem'] +class GridItem(UIGraphicsItem): + """ + **Bases:** :class:`UIGraphicsItem ` + + Displays a rectangular grid of lines indicating major divisions within a coordinate system. + Automatically determines what divisions to use. + """ + + def __init__(self): + UIGraphicsItem.__init__(self) + #QtGui.QGraphicsItem.__init__(self, *args) + #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.picture = None + + + def viewRangeChanged(self): + UIGraphicsItem.viewRangeChanged(self) + self.picture = None + #UIGraphicsItem.viewRangeChanged(self) + #self.update() + + def paint(self, p, opt, widget): + #p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) + #p.drawRect(self.boundingRect()) + #UIGraphicsItem.paint(self, p, opt, widget) + ### draw picture + if self.picture is None: + #print "no pic, draw.." + self.generatePicture() + p.drawPicture(QtCore.QPointF(0, 0), self.picture) + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawLine(0, -100, 0, 100) + #p.drawLine(-100, 0, 100, 0) + #print "drawing Grid." + + + def generatePicture(self): + self.picture = QtGui.QPicture() + p = QtGui.QPainter() + p.begin(self.picture) + + dt = fn.invertQTransform(self.viewTransform()) + vr = self.getViewWidget().rect() + unit = self.pixelWidth(), self.pixelHeight() + dim = [vr.width(), vr.height()] + lvr = self.boundingRect() + ul = np.array([lvr.left(), lvr.top()]) + br = np.array([lvr.right(), lvr.bottom()]) + + texts = [] + + if ul[1] > br[1]: + x = ul[1] + ul[1] = br[1] + br[1] = x + for i in [2,1,0]: ## Draw three different scales of grid + dist = br-ul + nlTarget = 10.**i + d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) + ul1 = np.floor(ul / d) * d + br1 = np.ceil(br / d) * d + dist = br1-ul1 + nl = (dist / d) + 0.5 + #print "level", i + #print " dim", dim + #print " dist", dist + #print " d", d + #print " nl", nl + for ax in range(0,2): ## Draw grid for both axes + ppl = dim[ax] / nl[ax] + c = np.clip(3.*(ppl-3), 0., 30.) + linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) + textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) + #linePen.setCosmetic(True) + #linePen.setWidth(1) + bx = (ax+1) % 2 + for x in range(0, int(nl[ax])): + linePen.setCosmetic(False) + if ax == 0: + linePen.setWidthF(self.pixelWidth()) + #print "ax 0 height", self.pixelHeight() + else: + linePen.setWidthF(self.pixelHeight()) + #print "ax 1 width", self.pixelWidth() + p.setPen(linePen) + p1 = np.array([0.,0.]) + p2 = np.array([0.,0.]) + p1[ax] = ul1[ax] + x * d[ax] + p2[ax] = p1[ax] + p1[bx] = ul[bx] + p2[bx] = br[bx] + ## don't draw lines that are out of bounds. + if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]): + continue + p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) + if i < 2: + p.setPen(textPen) + if ax == 0: + x = p1[0] + unit[0] + y = ul[1] + unit[1] * 8. + else: + x = ul[0] + unit[0]*3 + y = p1[1] + unit[1] + texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) + tr = self.deviceTransform() + #tr.scale(1.5, 1.5) + p.setWorldTransform(fn.invertQTransform(tr)) + for t in texts: + x = tr.map(t[0]) + Point(0.5, 0.5) + p.drawText(x, t[1]) + p.end() diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py new file mode 100644 index 00000000..5a3b63d6 --- /dev/null +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -0,0 +1,205 @@ +""" +GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. +""" + + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from .GraphicsWidget import GraphicsWidget +from .ViewBox import * +from .GradientEditorItem import * +from .LinearRegionItem import * +from .PlotDataItem import * +from .AxisItem import * +from .GridItem import * +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +import numpy as np +import pyqtgraph.debug as debug + + +__all__ = ['HistogramLUTItem'] + + +class HistogramLUTItem(GraphicsWidget): + """ + This is a graphicsWidget which provides controls for adjusting the display of an image. + Includes: + + - Image histogram + - Movable region over histogram to select black/white levels + - Gradient editor to define color lookup table for single-channel images + """ + + sigLookupTableChanged = QtCore.Signal(object) + sigLevelsChanged = QtCore.Signal(object) + sigLevelChangeFinished = QtCore.Signal(object) + + def __init__(self, image=None, fillHistogram=True): + """ + If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. + By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. + """ + GraphicsWidget.__init__(self) + self.lut = None + self.imageItem = None + + self.layout = QtGui.QGraphicsGridLayout() + self.setLayout(self.layout) + self.layout.setContentsMargins(1,1,1,1) + self.layout.setSpacing(0) + self.vb = ViewBox() + self.vb.setMaximumWidth(152) + self.vb.setMinimumWidth(45) + self.vb.setMouseEnabled(x=False, y=True) + self.gradient = GradientEditorItem() + self.gradient.setOrientation('right') + self.gradient.loadPreset('grey') + self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) + self.region.setZValue(1000) + self.vb.addItem(self.region) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, showValues=False) + self.layout.addItem(self.axis, 0, 0) + self.layout.addItem(self.vb, 0, 1) + self.layout.addItem(self.gradient, 0, 2) + self.range = None + self.gradient.setFlag(self.gradient.ItemStacksBehindParent) + self.vb.setFlag(self.gradient.ItemStacksBehindParent) + + #self.grid = GridItem() + #self.vb.addItem(self.grid) + + self.gradient.sigGradientChanged.connect(self.gradientChanged) + self.region.sigRegionChanged.connect(self.regionChanging) + self.region.sigRegionChangeFinished.connect(self.regionChanged) + self.vb.sigRangeChanged.connect(self.viewRangeChanged) + self.plot = PlotDataItem() + self.plot.rotate(90) + self.fillHistogram(fillHistogram) + + self.vb.addItem(self.plot) + self.autoHistogramRange() + + if image is not None: + self.setImageItem(image) + #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + + def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): + if fill: + self.plot.setFillLevel(level) + self.plot.setFillBrush(color) + else: + self.plot.setFillLevel(None) + + #def sizeHint(self, *args): + #return QtCore.QSizeF(115, 200) + + def paint(self, p, *args): + pen = self.region.lines[0].pen + rgn = self.getLevels() + p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) + p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) + gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) + for pen in [fn.mkPen('k', width=3), pen]: + p.setPen(pen) + p.drawLine(p1, gradRect.bottomLeft()) + p.drawLine(p2, gradRect.topLeft()) + p.drawLine(gradRect.topLeft(), gradRect.topRight()) + p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) + #p.drawRect(self.boundingRect()) + + + def setHistogramRange(self, mn, mx, padding=0.1): + """Set the Y range on the histogram plot. This disables auto-scaling.""" + self.vb.enableAutoRange(self.vb.YAxis, False) + self.vb.setYRange(mn, mx, padding) + + #d = mx-mn + #mn -= d*padding + #mx += d*padding + #self.range = [mn,mx] + #self.updateRange() + #self.vb.setMouseEnabled(False, True) + #self.region.setBounds([mn,mx]) + + def autoHistogramRange(self): + """Enable auto-scaling on the histogram plot.""" + self.vb.enableAutoRange(self.vb.XYAxes) + #self.range = None + #self.updateRange() + #self.vb.setMouseEnabled(False, False) + + #def updateRange(self): + #self.vb.autoRange() + #if self.range is not None: + #self.vb.setYRange(*self.range) + #vr = self.vb.viewRect() + + #self.region.setBounds([vr.top(), vr.bottom()]) + + def setImageItem(self, img): + self.imageItem = img + img.sigImageChanged.connect(self.imageChanged) + img.setLookupTable(self.getLookupTable) ## send function pointer, not the result + #self.gradientChanged() + self.regionChanged() + self.imageChanged(autoLevel=True) + #self.vb.autoRange() + + def viewRangeChanged(self): + self.update() + + def gradientChanged(self): + if self.imageItem is not None: + if self.gradient.isLookupTrivial(): + self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + else: + self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + + self.lut = None + #if self.imageItem is not None: + #self.imageItem.setLookupTable(self.gradient.getLookupTable(512)) + self.sigLookupTableChanged.emit(self) + + def getLookupTable(self, img=None, n=None, alpha=None): + if n is None: + if img.dtype == np.uint8: + n = 256 + else: + n = 512 + if self.lut is None: + self.lut = self.gradient.getLookupTable(n, alpha=alpha) + return self.lut + + def regionChanged(self): + #if self.imageItem is not None: + #self.imageItem.setLevels(self.region.getRegion()) + self.sigLevelChangeFinished.emit(self) + #self.update() + + def regionChanging(self): + if self.imageItem is not None: + self.imageItem.setLevels(self.region.getRegion()) + self.sigLevelsChanged.emit(self) + self.update() + + def imageChanged(self, autoLevel=False, autoRange=False): + prof = debug.Profiler('HistogramLUTItem.imageChanged', disabled=True) + h = self.imageItem.getHistogram() + prof.mark('get histogram') + if h[0] is None: + return + self.plot.setData(*h) + prof.mark('set plot') + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region.setRegion([mn, mx]) + prof.mark('set region') + prof.finish() + + def getLevels(self): + return self.region.getRegion() + + def setLevels(self, mn, mx): + self.region.setRegion([mn, mx]) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py new file mode 100644 index 00000000..123612b8 --- /dev/null +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -0,0 +1,449 @@ +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import collections +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug +from .GraphicsObject import GraphicsObject + +__all__ = ['ImageItem'] +class ImageItem(GraphicsObject): + """ + **Bases:** :class:`GraphicsObject ` + + GraphicsObject displaying an image. Optimized for rapid update (ie video display). + This item displays either a 2D numpy array (height, width) or + a 3D array (height, width, RGBa). This array is optionally scaled (see + :func:`setLevels `) and/or colored + with a lookup table (see :func:`setLookupTable `) + before being displayed. + + ImageItem is frequently used in conjunction with + :class:`HistogramLUTItem ` or + :class:`HistogramLUTWidget ` to provide a GUI + for controlling the levels and lookup table used to display the image. + """ + + + sigImageChanged = QtCore.Signal() + sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu + + def __init__(self, image=None, **kargs): + """ + See :func:`setImage ` for all allowed initialization arguments. + """ + GraphicsObject.__init__(self) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) + #self.qimage = QtGui.QImage() + #self._pixmap = None + self.menu = None + self.image = None ## original image data + self.qimage = None ## rendered image for display + #self.clipMask = None + + self.paintMode = None + + self.levels = None ## [min, max] or [[redMin, redMax], ...] + self.lut = None + + #self.clipLevel = None + self.drawKernel = None + self.border = None + self.removable = False + + if image is not None: + self.setImage(image, **kargs) + else: + self.setOpts(**kargs) + + def setCompositionMode(self, mode): + """Change the composition mode of the item (see QPainter::CompositionMode + in the Qt documentation). This is useful when overlaying multiple ImageItems. + + ============================================ ============================================================ + **Most common arguments:** + QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it + is opaque. Otherwise, it uses the alpha channel to blend + the image with the background. + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + reflect the lightness or darkness of the background. + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + are added together. + QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. + ============================================ ============================================================ + """ + self.paintMode = mode + self.update() + + ## use setOpacity instead. + #def setAlpha(self, alpha): + #self.setOpacity(alpha) + #self.updateImage() + + def setBorder(self, b): + self.border = fn.mkPen(b) + self.update() + + def width(self): + if self.image is None: + return None + return self.image.shape[0] + + def height(self): + if self.image is None: + return None + return self.image.shape[1] + + def boundingRect(self): + if self.image is None: + return QtCore.QRectF(0., 0., 0., 0.) + return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) + + #def setClipLevel(self, level=None): + #self.clipLevel = level + #self.updateImage() + + #def paint(self, p, opt, widget): + #pass + #if self.pixmap is not None: + #p.drawPixmap(0, 0, self.pixmap) + #print "paint" + + def setLevels(self, levels, update=True): + """ + Set image scaling levels. Can be one of: + + * [blackLevel, whiteLevel] + * [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] + + Only the first format is compatible with lookup tables. See :func:`makeARGB ` + for more details on how levels are applied. + """ + self.levels = levels + if update: + self.updateImage() + + def getLevels(self): + return self.levels + #return self.whiteLevel, self.blackLevel + + def setLookupTable(self, lut, update=True): + """ + Set the lookup table (numpy array) to use for this image. (see + :func:`makeARGB ` for more information on how this is used). + Optionally, lut can be a callable that accepts the current image as an + argument and returns the lookup table to use. + + Ordinarily, this table is supplied by a :class:`HistogramLUTItem ` + or :class:`GradientEditorItem `. + """ + self.lut = lut + if update: + self.updateImage() + + def setOpts(self, update=True, **kargs): + if 'lut' in kargs: + self.setLookupTable(kargs['lut'], update=update) + if 'levels' in kargs: + self.setLevels(kargs['levels'], update=update) + #if 'clipLevel' in kargs: + #self.setClipLevel(kargs['clipLevel']) + if 'opacity' in kargs: + self.setOpacity(kargs['opacity']) + if 'compositionMode' in kargs: + self.setCompositionMode(kargs['compositionMode']) + if 'border' in kargs: + self.setBorder(kargs['border']) + if 'removable' in kargs: + self.removable = kargs['removable'] + self.menu = None + + def setRect(self, rect): + """Scale and translate the image to fit within rect (must be a QRect or QRectF).""" + self.resetTransform() + self.translate(rect.left(), rect.top()) + self.scale(rect.width() / self.width(), rect.height() / self.height()) + + def setImage(self, image=None, autoLevels=None, **kargs): + """ + Update the image displayed by this item. For more information on how the image + is processed before displaying, see :func:`makeARGB ` + + ================= ========================================================================= + **Arguments:** + image (numpy array) Specifies the image data. May be 2D (width, height) or + 3D (width, height, RGBa). The array dtype must be integer or floating + point of any bit depth. For 3D arrays, the third dimension must + be of length 3 (RGB) or 4 (RGBA). + autoLevels (bool) If True, this forces the image to automatically select + levels based on the maximum and minimum values in the data. + By default, this argument is true unless the levels argument is + given. + lut (numpy array) The color lookup table to use when displaying the image. + See :func:`setLookupTable `. + levels (min, max) The minimum and maximum values to use when rescaling the image + data. By default, this will be set to the minimum and maximum values + in the image. If the image array has dtype uint8, no rescaling is necessary. + opacity (float 0.0-1.0) + compositionMode see :func:`setCompositionMode ` + border Sets the pen used when drawing the image border. Default is None. + ================= ========================================================================= + """ + prof = debug.Profiler('ImageItem.setImage', disabled=True) + + gotNewData = False + if image is None: + if self.image is None: + return + else: + gotNewData = True + if self.image is None or image.shape != self.image.shape: + self.prepareGeometryChange() + self.image = image.view(np.ndarray) + + prof.mark('1') + + if autoLevels is None: + if 'levels' in kargs: + autoLevels = False + else: + autoLevels = True + if autoLevels: + img = self.image + while img.size > 2**16: + img = img[::2, ::2] + mn, mx = img.min(), img.max() + if mn == mx: + mn = 0 + mx = 255 + kargs['levels'] = [mn,mx] + prof.mark('2') + + self.setOpts(update=False, **kargs) + prof.mark('3') + + self.qimage = None + self.update() + prof.mark('4') + + if gotNewData: + self.sigImageChanged.emit() + + + prof.finish() + + + + def updateImage(self, *args, **kargs): + ## used for re-rendering qimage from self.image. + + ## can we make any assumptions here that speed things up? + ## dtype, range, size are all the same? + defaults = { + 'autoLevels': False, + } + defaults.update(kargs) + return self.setImage(*args, **defaults) + + + + + def render(self): + prof = debug.Profiler('ImageItem.render', disabled=True) + if self.image is None: + return + if isinstance(self.lut, collections.Callable): + lut = self.lut(self.image) + else: + lut = self.lut + #print lut.shape + #print self.lut + + argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) + self.qimage = fn.makeQImage(argb, alpha) + prof.finish() + + + def paint(self, p, *args): + prof = debug.Profiler('ImageItem.paint', disabled=True) + if self.image is None: + return + if self.qimage is None: + self.render() + prof.mark('render QImage') + if self.paintMode is not None: + p.setCompositionMode(self.paintMode) + prof.mark('set comp mode') + + p.drawImage(QtCore.QPointF(0,0), self.qimage) + prof.mark('p.drawImage') + if self.border is not None: + p.setPen(self.border) + p.drawRect(self.boundingRect()) + prof.finish() + + def save(self, fileName, *args): + """Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data.""" + if self.qimage is None: + self.render() + self.qimage.save(fileName, *args) + + def getHistogram(self, bins=500, step=3): + """Returns x and y arrays containing the histogram values for the current image. + The step argument causes pixels to be skipped when computing the histogram to save time. + This method is also used when automatically computing levels. + """ + if self.image is None: + return None,None + stepData = self.image[::step, ::step] + hist = np.histogram(stepData, bins=bins) + return hist[1][:-1], hist[0] + + def setPxMode(self, b): + """ + Set whether the item ignores transformations and draws directly to screen pixels. + If True, the item will not inherit any scale or rotation transformations from its + parent items, but its position will be transformed as usual. + (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation) + """ + self.setFlag(self.ItemIgnoresTransformations, b) + + def setScaledMode(self): + self.setPxMode(False) + + def getPixmap(self): + if self.qimage is None: + self.render() + if self.qimage is None: + return None + return QtGui.QPixmap.fromImage(self.qimage) + + def pixelSize(self): + """return scene-size of a single pixel in the image""" + br = self.sceneBoundingRect() + if self.image is None: + return 1,1 + return br.width()/self.width(), br.height()/self.height() + + #def mousePressEvent(self, ev): + #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: + #self.drawAt(ev.pos(), ev) + #ev.accept() + #else: + #ev.ignore() + + #def mouseMoveEvent(self, ev): + ##print "mouse move", ev.pos() + #if self.drawKernel is not None: + #self.drawAt(ev.pos(), ev) + + #def mouseReleaseEvent(self, ev): + #pass + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + elif self.drawKernel is not None: + ev.accept() + self.drawAt(ev.pos(), ev) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + if self.raiseContextMenu(ev): + ev.accept() + if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: + self.drawAt(ev.pos(), ev) + + def raiseContextMenu(self, ev): + menu = self.getMenu() + if menu is None: + return False + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + return True + + def getMenu(self): + if self.menu is None: + if not self.removable: + return None + self.menu = QtGui.QMenu() + self.menu.setTitle("Image") + remAct = QtGui.QAction("Remove image", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + return self.menu + + + def hoverEvent(self, ev): + if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. + ev.acceptClicks(QtCore.Qt.RightButton) + #self.box.setBrush(fn.mkBrush('w')) + elif not ev.isExit() and self.removable: + ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks + #else: + #self.box.setBrush(self.brush) + #self.update() + + + + def tabletEvent(self, ev): + print(ev.device()) + print(ev.pointerType()) + print(ev.pressure()) + + def drawAt(self, pos, ev=None): + pos = [int(pos.x()), int(pos.y())] + dk = self.drawKernel + kc = self.drawKernelCenter + sx = [0,dk.shape[0]] + sy = [0,dk.shape[1]] + tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] + ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] + + for i in [0,1]: + dx1 = -min(0, tx[i]) + dx2 = min(0, self.image.shape[0]-tx[i]) + tx[i] += dx1+dx2 + sx[i] += dx1+dx2 + + dy1 = -min(0, ty[i]) + dy2 = min(0, self.image.shape[1]-ty[i]) + ty[i] += dy1+dy2 + sy[i] += dy1+dy2 + + ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) + ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) + mask = self.drawMask + src = dk + + if isinstance(self.drawMode, collections.Callable): + self.drawMode(dk, self.image, mask, ss, ts, ev) + else: + src = src[ss] + if self.drawMode == 'set': + if mask is not None: + mask = mask[ss] + self.image[ts] = self.image[ts] * (1-mask) + src * mask + else: + self.image[ts] = src + elif self.drawMode == 'add': + self.image[ts] += src + else: + raise Exception("Unknown draw mode '%s'" % self.drawMode) + self.updateImage() + + def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): + self.drawKernel = kernel + self.drawKernelCenter = center + self.drawMode = mode + self.drawMask = mask + + def removeClicked(self): + ## Send remove event only after we have exited the menu event handler + self.removeTimer = QtCore.QTimer() + self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) + self.removeTimer.start(0) + diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py new file mode 100644 index 00000000..4f0df863 --- /dev/null +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -0,0 +1,277 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Point import Point +from .GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +import numpy as np +import weakref + + +__all__ = ['InfiniteLine'] +class InfiniteLine(GraphicsObject): + """ + **Bases:** :class:`GraphicsObject ` + + Displays a line of infinite length. + This line may be dragged to indicate a position in data coordinates. + + =============================== =================================================== + **Signals** + sigDragged(self) + sigPositionChangeFinished(self) + sigPositionChanged(self) + =============================== =================================================== + """ + + sigDragged = QtCore.Signal(object) + sigPositionChangeFinished = QtCore.Signal(object) + sigPositionChanged = QtCore.Signal(object) + + def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): + """ + ============= ================================================================== + **Arguments** + pos Position of the line. This can be a QPointF or a single value for + vertical/horizontal lines. + angle Angle of line in degrees. 0 is horizontal, 90 is vertical. + pen Pen to use when drawing line. Can be any arguments that are valid + for :func:`mkPen `. Default pen is transparent + yellow. + movable If True, the line can be dragged to a new position by the user. + bounds Optional [min, max] bounding values. Bounds are only valid if the + line is vertical or horizontal. + ============= ================================================================== + """ + + GraphicsObject.__init__(self) + + if bounds is None: ## allowed value boundaries for orthogonal lines + self.maxRange = [None, None] + else: + self.maxRange = bounds + self.moving = False + self.setMovable(movable) + self.mouseHovering = False + self.p = [0, 0] + self.setAngle(angle) + if pos is None: + pos = Point(0,0) + self.setPos(pos) + + if pen is None: + pen = (200, 200, 100) + self.setPen(pen) + self.currentPen = self.pen + #self.setFlag(self.ItemSendsScenePositionChanges) + + def setMovable(self, m): + """Set whether the line is movable by the user.""" + self.movable = m + self.setAcceptHoverEvents(m) + + def setBounds(self, bounds): + """Set the (minimum, maximum) allowable values when dragging.""" + self.maxRange = bounds + self.setValue(self.value()) + + def setPen(self, pen): + """Set the pen for drawing the line. Allowable arguments are any that are valid + for :func:`mkPen `.""" + self.pen = fn.mkPen(pen) + self.currentPen = self.pen + self.update() + + def setAngle(self, angle): + """ + Takes angle argument in degrees. + 0 is horizontal; 90 is vertical. + + Note that the use of value() and setValue() changes if the line is + not vertical or horizontal. + """ + self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 + self.resetTransform() + self.rotate(self.angle) + self.update() + + def setPos(self, pos): + + if type(pos) in [list, tuple]: + newPos = pos + elif isinstance(pos, QtCore.QPointF): + newPos = [pos.x(), pos.y()] + else: + if self.angle == 90: + newPos = [pos, 0] + elif self.angle == 0: + newPos = [0, pos] + else: + raise Exception("Must specify 2D coordinate for non-orthogonal lines.") + + ## check bounds (only works for orthogonal lines) + if self.angle == 90: + if self.maxRange[0] is not None: + newPos[0] = max(newPos[0], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[0] = min(newPos[0], self.maxRange[1]) + elif self.angle == 0: + if self.maxRange[0] is not None: + newPos[1] = max(newPos[1], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[1] = min(newPos[1], self.maxRange[1]) + + if self.p != newPos: + self.p = newPos + GraphicsObject.setPos(self, Point(self.p)) + self.update() + self.sigPositionChanged.emit(self) + + def getXPos(self): + return self.p[0] + + def getYPos(self): + return self.p[1] + + def getPos(self): + return self.p + + def value(self): + """Return the value of the line. Will be a single number for horizontal and + vertical lines, and a list of [x,y] values for diagonal lines.""" + if self.angle%180 == 0: + return self.getYPos() + elif self.angle%180 == 90: + return self.getXPos() + else: + return self.getPos() + + def setValue(self, v): + """Set the position of the line. If line is horizontal or vertical, v can be + a single value. Otherwise, a 2D coordinate must be specified (list, tuple and + QPointF are all acceptable).""" + self.setPos(v) + + ## broken in 4.7 + #def itemChange(self, change, val): + #if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]: + #self.updateLine() + #print "update", change + #print self.getBoundingParents() + #else: + #print "ignore", change + #return GraphicsObject.itemChange(self, change, val) + + def boundingRect(self): + #br = UIGraphicsItem.boundingRect(self) + br = self.viewRect() + ## add a 4-pixel radius around the line for mouse interaction. + + px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line + if px is None: + px = 0 + br.setBottom(-px*4) + br.setTop(px*4) + return br.normalized() + + def paint(self, p, *args): + br = self.boundingRect() + p.setPen(self.currentPen) + p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) + #p.drawRect(self.boundingRect()) + + def dataBounds(self, axis, frac=1.0, orthoRange=None): + if axis == 0: + return None ## x axis should never be auto-scaled + else: + return (0,0) + + #def mousePressEvent(self, ev): + #if self.movable and ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + #else: + #ev.ignore() + + #def mouseMoveEvent(self, ev): + #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + ##self.emit(QtCore.SIGNAL('dragged'), self) + #self.sigDragged.emit(self) + #self.hasMoved = True + + #def mouseReleaseEvent(self, ev): + #if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: + #self.hasMoved = False + ##self.emit(QtCore.SIGNAL('positionChangeFinished'), self) + #self.sigPositionChangeFinished.emit(self) + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + #pressDelta = self.mapToParent(ev.buttonDownPos()) - Point(self.p) + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) + self.sigDragged.emit(self) + if ev.isFinish(): + self.moving = False + self.sigPositionChangeFinished.emit(self) + #else: + #print ev + + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.setPos(self.startPosition) + self.moving = False + self.sigDragged.emit(self) + self.sigPositionChangeFinished.emit(self) + + def hoverEvent(self, ev): + if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton): + self.setMouseHover(True) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the item that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: + self.currentPen = fn.mkPen(255, 0,0) + else: + self.currentPen = self.pen + self.update() + + #def hoverEnterEvent(self, ev): + #print "line hover enter" + #ev.ignore() + #self.updateHoverPen() + + #def hoverMoveEvent(self, ev): + #print "line hover move" + #ev.ignore() + #self.updateHoverPen() + + #def hoverLeaveEvent(self, ev): + #print "line hover leave" + #ev.ignore() + #self.updateHoverPen(False) + + #def updateHoverPen(self, hover=None): + #if hover is None: + #scene = self.scene() + #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) + + #if hover: + #self.currentPen = fn.mkPen(255, 0,0) + #else: + #self.currentPen = self.pen + #self.update() + diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py new file mode 100644 index 00000000..01ef57b6 --- /dev/null +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -0,0 +1,121 @@ + + +from .GraphicsObject import * +import pyqtgraph.functions as fn +from pyqtgraph.Qt import QtGui, QtCore + + +class IsocurveItem(GraphicsObject): + """ + **Bases:** :class:`GraphicsObject ` + + Item displaying an isocurve of a 2D array.To align this item correctly with an + ImageItem,call isocurve.setParentItem(image) + """ + + + def __init__(self, data=None, level=0, pen='w'): + """ + Create a new isocurve item. + + ============= =============================================================== + **Arguments** + data A 2-dimensional ndarray. Can be initialized as None, and set + later using :func:`setData ` + level The cutoff value at which to draw the isocurve. + pen The color of the curve item. Can be anything valid for + :func:`mkPen ` + ============= =============================================================== + """ + GraphicsObject.__init__(self) + + self.level = level + self.data = None + self.path = None + self.setPen(pen) + self.setData(data, level) + + + + #if data is not None and level is not None: + #self.updateLines(data, level) + + + def setData(self, data, level=None): + """ + Set the data/image to draw isocurves for. + + ============= ======================================================================== + **Arguments** + data A 2-dimensional ndarray. + level The cutoff value at which to draw the curve. If level is not specified, + the previously set level is used. + ============= ======================================================================== + """ + if level is None: + level = self.level + self.level = level + self.data = data + self.path = None + self.prepareGeometryChange() + self.update() + + + def setLevel(self, level): + """Set the level at which the isocurve is drawn.""" + self.level = level + self.path = None + self.update() + + + def setPen(self, *args, **kwargs): + """Set the pen used to draw the isocurve. Arguments can be any that are valid + for :func:`mkPen `""" + self.pen = fn.mkPen(*args, **kwargs) + self.update() + + def setBrush(self, *args, **kwargs): + """Set the brush used to draw the isocurve. Arguments can be any that are valid + for :func:`mkBrush `""" + self.brush = fn.mkBrush(*args, **kwargs) + self.update() + + + def updateLines(self, data, level): + ##print "data:", data + ##print "level", level + #lines = fn.isocurve(data, level) + ##print len(lines) + #self.path = QtGui.QPainterPath() + #for line in lines: + #self.path.moveTo(*line[0]) + #self.path.lineTo(*line[1]) + #self.update() + self.setData(data, level) + + def boundingRect(self): + if self.data is None: + return QtCore.QRectF() + if self.path is None: + self.generatePath() + return self.path.boundingRect() + + def generatePath(self): + if self.data is None: + self.path = None + return + lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True) + self.path = QtGui.QPainterPath() + for line in lines: + self.path.moveTo(*line[0]) + for p in line[1:]: + self.path.lineTo(*p) + + def paint(self, p, *args): + if self.data is None: + return + if self.path is None: + self.generatePath() + p.setPen(self.pen) + p.drawPath(self.path) + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ItemGroup.py b/pyqtgraph/graphicsItems/ItemGroup.py new file mode 100644 index 00000000..930fdf80 --- /dev/null +++ b/pyqtgraph/graphicsItems/ItemGroup.py @@ -0,0 +1,23 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsObject import GraphicsObject + +__all__ = ['ItemGroup'] +class ItemGroup(GraphicsObject): + """ + Replacement for QGraphicsItemGroup + """ + + def __init__(self, *args): + GraphicsObject.__init__(self, *args) + if hasattr(self, "ItemHasNoContents"): + self.setFlag(self.ItemHasNoContents) + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def addItem(self, item): + item.setParentItem(self) + diff --git a/pyqtgraph/graphicsItems/LabelItem.py b/pyqtgraph/graphicsItems/LabelItem.py new file mode 100644 index 00000000..17301fb3 --- /dev/null +++ b/pyqtgraph/graphicsItems/LabelItem.py @@ -0,0 +1,140 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +import pyqtgraph as pg +from .GraphicsWidget import GraphicsWidget + + +__all__ = ['LabelItem'] + +class LabelItem(GraphicsWidget): + """ + GraphicsWidget displaying text. + Used mainly as axis labels, titles, etc. + + Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use TextItem + """ + + + def __init__(self, text=' ', parent=None, angle=0, **args): + GraphicsWidget.__init__(self, parent) + self.item = QtGui.QGraphicsTextItem(self) + self.opts = { + 'color': None, + 'justify': 'center' + } + self.opts.update(args) + self._sizeHint = {} + self.setText(text) + self.setAngle(angle) + + def setAttr(self, attr, value): + """Set default text properties. See setText() for accepted parameters.""" + self.opts[attr] = value + + def setText(self, text, **args): + """Set the text and text properties in the label. Accepts optional arguments for auto-generating + a CSS style string: + + ==================== ============================== + **Style Arguments:** + color (str) example: 'CCFF00' + size (str) example: '8pt' + bold (bool) + italic (bool) + ==================== ============================== + """ + self.text = text + opts = self.opts + for k in args: + opts[k] = args[k] + + optlist = [] + + color = self.opts['color'] + if color is None: + color = pg.getConfigOption('foreground') + color = fn.mkColor(color) + optlist.append('color: #' + fn.colorStr(color)[:6]) + if 'size' in opts: + optlist.append('font-size: ' + opts['size']) + if 'bold' in opts and opts['bold'] in [True, False]: + optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) + if 'italic' in opts and opts['italic'] in [True, False]: + optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) + full = "%s" % ('; '.join(optlist), text) + #print full + self.item.setHtml(full) + self.updateMin() + self.resizeEvent(None) + self.updateGeometry() + + def resizeEvent(self, ev): + #c1 = self.boundingRect().center() + #c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() + #dif = c1 - c2 + #self.item.moveBy(dif.x(), dif.y()) + #print c1, c2, dif, self.item.pos() + self.item.setPos(0,0) + bounds = self.itemRect() + left = self.mapFromItem(self.item, QtCore.QPointF(0,0)) - self.mapFromItem(self.item, QtCore.QPointF(1,0)) + rect = self.rect() + + if self.opts['justify'] == 'left': + if left.x() != 0: + bounds.moveLeft(rect.left()) + if left.y() < 0: + bounds.moveTop(rect.top()) + elif left.y() > 0: + bounds.moveBottom(rect.bottom()) + + elif self.opts['justify'] == 'center': + bounds.moveCenter(rect.center()) + #bounds = self.itemRect() + #self.item.setPos(self.width()/2. - bounds.width()/2., 0) + elif self.opts['justify'] == 'right': + if left.x() != 0: + bounds.moveRight(rect.right()) + if left.y() < 0: + bounds.moveBottom(rect.bottom()) + elif left.y() > 0: + bounds.moveTop(rect.top()) + #bounds = self.itemRect() + #self.item.setPos(self.width() - bounds.width(), 0) + + self.item.setPos(bounds.topLeft() - self.itemRect().topLeft()) + self.updateMin() + + def setAngle(self, angle): + self.angle = angle + self.item.resetTransform() + self.item.rotate(angle) + self.updateMin() + + + def updateMin(self): + bounds = self.itemRect() + self.setMinimumWidth(bounds.width()) + self.setMinimumHeight(bounds.height()) + + self._sizeHint = { + QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), + QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), + QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2), + QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? + } + self.updateGeometry() + + def sizeHint(self, hint, constraint): + if hint not in self._sizeHint: + return QtCore.QSizeF(0, 0) + return QtCore.QSizeF(*self._sizeHint[hint]) + + def itemRect(self): + return self.item.mapRectToParent(self.item.boundingRect()) + + #def paint(self, p, *args): + #p.setPen(fn.mkPen('r')) + #p.drawRect(self.rect()) + #p.setPen(fn.mkPen('g')) + #p.drawRect(self.itemRect()) + diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py new file mode 100644 index 00000000..c41feb95 --- /dev/null +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -0,0 +1,121 @@ +from .GraphicsWidget import GraphicsWidget +from .LabelItem import LabelItem +from ..Qt import QtGui, QtCore +from .. import functions as fn +from ..Point import Point +from .GraphicsWidgetAnchor import GraphicsWidgetAnchor +__all__ = ['LegendItem'] + +class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): + """ + Displays a legend used for describing the contents of a plot. + LegendItems are most commonly created by calling PlotItem.addLegend(). + + Note that this item should not be added directly to a PlotItem. Instead, + Make it a direct descendant of the PlotItem:: + + legend.setParentItem(plotItem) + + """ + def __init__(self, size=None, offset=None): + """ + ========== =============================================================== + Arguments + size Specifies the fixed size (width, height) of the legend. If + this argument is omitted, the legend will autimatically resize + to fit its contents. + offset Specifies the offset position relative to the legend's parent. + Positive values offset from the left or top; negative values + offset from the right or bottom. If offset is None, the + legend must be anchored manually by calling anchor() or + positioned by calling setPos(). + ========== =============================================================== + + """ + + + GraphicsWidget.__init__(self) + GraphicsWidgetAnchor.__init__(self) + self.setFlag(self.ItemIgnoresTransformations) + self.layout = QtGui.QGraphicsGridLayout() + self.setLayout(self.layout) + self.items = [] + self.size = size + self.offset = offset + if size is not None: + self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1])) + + def setParentItem(self, p): + ret = GraphicsWidget.setParentItem(self, p) + if self.offset is not None: + offset = Point(self.offset) + anchorx = 1 if offset[0] <= 0 else 0 + anchory = 1 if offset[1] <= 0 else 0 + anchor = (anchorx, anchory) + self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) + return ret + + def addItem(self, item, name): + """ + Add a new entry to the legend. + + =========== ======================================================== + Arguments + item A PlotDataItem from which the line and point style + of the item will be determined + title The title to display for this item. Simple HTML allowed. + =========== ======================================================== + """ + label = LabelItem(name) + sample = ItemSample(item) + row = len(self.items) + self.items.append((sample, label)) + self.layout.addItem(sample, row, 0) + self.layout.addItem(label, row, 1) + self.updateSize() + + def updateSize(self): + if self.size is not None: + return + + height = 0 + width = 0 + #print("-------") + for sample, label in self.items: + height += max(sample.height(), label.height()) + 3 + width = max(width, sample.width()+label.width()) + #print(width, height) + #print width, height + self.setGeometry(0, 0, width+25, height) + + def boundingRect(self): + return QtCore.QRectF(0, 0, self.width(), self.height()) + + def paint(self, p, *args): + p.setPen(fn.mkPen(255,255,255,100)) + p.setBrush(fn.mkBrush(100,100,100,50)) + p.drawRect(self.boundingRect()) + + +class ItemSample(GraphicsWidget): + def __init__(self, item): + GraphicsWidget.__init__(self) + self.item = item + + def boundingRect(self): + return QtCore.QRectF(0, 0, 20, 20) + + def paint(self, p, *args): + opts = self.item.opts + + if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: + p.setBrush(fn.mkBrush(opts['fillBrush'])) + p.setPen(fn.mkPen(None)) + p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) + + p.setPen(fn.mkPen(opts['pen'])) + p.drawLine(2, 18, 18, 2) + + + + diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py new file mode 100644 index 00000000..0b44c815 --- /dev/null +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -0,0 +1,291 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .UIGraphicsItem import UIGraphicsItem +from .InfiniteLine import InfiniteLine +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug + +__all__ = ['LinearRegionItem'] + +class LinearRegionItem(UIGraphicsItem): + """ + **Bases:** :class:`UIGraphicsItem ` + + Used for marking a horizontal or vertical region in plots. + The region can be dragged and is bounded by lines which can be dragged individually. + + =============================== ============================================================================= + **Signals:** + sigRegionChangeFinished(self) Emitted when the user has finished dragging the region (or one of its lines) + and when the region is changed programatically. + sigRegionChanged(self) Emitted while the user is dragging the region (or one of its lines) + and when the region is changed programatically. + =============================== ============================================================================= + """ + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + Vertical = 0 + Horizontal = 1 + + def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): + """Create a new LinearRegionItem. + + ============= ===================================================================== + **Arguments** + values A list of the positions of the lines in the region. These are not + limits; limits can be set by specifying bounds. + orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. + If not specified it will be vertical. + brush Defines the brush that fills the region. Can be any arguments that + are valid for :func:`mkBrush `. Default is + transparent blue. + movable If True, the region and individual lines are movable by the user; if + False, they are static. + bounds Optional [min, max] bounding values for the region + ============= ===================================================================== + """ + + UIGraphicsItem.__init__(self) + if orientation is None: + orientation = LinearRegionItem.Vertical + self.orientation = orientation + self.bounds = QtCore.QRectF() + self.blockLineSignal = False + self.moving = False + self.mouseHovering = False + + if orientation == LinearRegionItem.Horizontal: + self.lines = [ + InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] + elif orientation == LinearRegionItem.Vertical: + self.lines = [ + InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] + else: + raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') + + + for l in self.lines: + l.setParentItem(self) + l.sigPositionChangeFinished.connect(self.lineMoveFinished) + l.sigPositionChanged.connect(self.lineMoved) + + if brush is None: + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) + self.setBrush(brush) + + self.setMovable(movable) + + def getRegion(self): + """Return the values at the edges of the region.""" + #if self.orientation[0] == 'h': + #r = (self.bounds.top(), self.bounds.bottom()) + #else: + #r = (self.bounds.left(), self.bounds.right()) + r = [self.lines[0].value(), self.lines[1].value()] + return (min(r), max(r)) + + def setRegion(self, rgn): + """Set the values for the edges of the region. + + ============= ============================================== + **Arguments** + rgn A list or tuple of the lower and upper values. + ============= ============================================== + """ + if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]: + return + self.blockLineSignal = True + self.lines[0].setValue(rgn[0]) + self.blockLineSignal = False + self.lines[1].setValue(rgn[1]) + #self.blockLineSignal = False + self.lineMoved() + self.lineMoveFinished() + + def setBrush(self, *br, **kargs): + """Set the brush that fills the region. Can have any arguments that are valid + for :func:`mkBrush `. + """ + self.brush = fn.mkBrush(*br, **kargs) + self.currentBrush = self.brush + + def setBounds(self, bounds): + """Optional [min, max] bounding values for the region. To have no bounds on the + region use [None, None]. + Does not affect the current position of the region unless it is outside the new bounds. + See :func:`setRegion ` to set the position + of the region.""" + for l in self.lines: + l.setBounds(bounds) + + def setMovable(self, m): + """Set lines to be movable by the user, or not. If lines are movable, they will + also accept HoverEvents.""" + for l in self.lines: + l.setMovable(m) + self.movable = m + self.setAcceptHoverEvents(m) + + def boundingRect(self): + br = UIGraphicsItem.boundingRect(self) + rng = self.getRegion() + if self.orientation == LinearRegionItem.Vertical: + br.setLeft(rng[0]) + br.setRight(rng[1]) + else: + br.setTop(rng[0]) + br.setBottom(rng[1]) + return br.normalized() + + def paint(self, p, *args): + #prof = debug.Profiler('LinearRegionItem.paint') + UIGraphicsItem.paint(self, p, *args) + p.setBrush(self.currentBrush) + p.setPen(fn.mkPen(None)) + p.drawRect(self.boundingRect()) + #prof.finish() + + def dataBounds(self, axis, frac=1.0, orthoRange=None): + if axis == self.orientation: + return self.getRegion() + else: + return None + + def lineMoved(self): + if self.blockLineSignal: + return + self.prepareGeometryChange() + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) + + def lineMoveFinished(self): + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) + + + #def updateBounds(self): + #vb = self.view().viewRect() + #vals = [self.lines[0].value(), self.lines[1].value()] + #if self.orientation[0] == 'h': + #vb.setTop(min(vals)) + #vb.setBottom(max(vals)) + #else: + #vb.setLeft(min(vals)) + #vb.setRight(max(vals)) + #if vb != self.bounds: + #self.bounds = vb + #self.rect.setRect(vb) + + #def mousePressEvent(self, ev): + #if not self.movable: + #ev.ignore() + #return + #for l in self.lines: + #l.mousePressEvent(ev) ## pass event to both lines so they move together + ##if self.movable and ev.button() == QtCore.Qt.LeftButton: + ##ev.accept() + ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + ##else: + ##ev.ignore() + + #def mouseReleaseEvent(self, ev): + #for l in self.lines: + #l.mouseReleaseEvent(ev) + + #def mouseMoveEvent(self, ev): + ##print "move", ev.pos() + #if not self.movable: + #return + #self.lines[0].blockSignals(True) # only want to update once + #for l in self.lines: + #l.mouseMoveEvent(ev) + #self.lines[0].blockSignals(False) + ##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + ##self.emit(QtCore.SIGNAL('dragged'), self) + + def mouseDragEvent(self, ev): + if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: + return + ev.accept() + + if ev.isStart(): + bdp = ev.buttonDownPos() + self.cursorOffsets = [l.pos() - bdp for l in self.lines] + self.startPositions = [l.pos() for l in self.lines] + self.moving = True + + if not self.moving: + return + + #delta = ev.pos() - ev.lastPos() + self.lines[0].blockSignals(True) # only want to update once + for i, l in enumerate(self.lines): + l.setPos(self.cursorOffsets[i] + ev.pos()) + #l.setPos(l.pos()+delta) + #l.mouseDragEvent(ev) + self.lines[0].blockSignals(False) + self.prepareGeometryChange() + + if ev.isFinish(): + self.moving = False + self.sigRegionChangeFinished.emit(self) + else: + self.sigRegionChanged.emit(self) + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + for i, l in enumerate(self.lines): + l.setPos(self.startPositions[i]) + self.moving = False + self.sigRegionChanged.emit(self) + self.sigRegionChangeFinished.emit(self) + + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + self.setMouseHover(True) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the item that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: + c = self.brush.color() + c.setAlpha(c.alpha() * 2) + self.currentBrush = fn.mkBrush(c) + else: + self.currentBrush = self.brush + self.update() + + #def hoverEnterEvent(self, ev): + #print "rgn hover enter" + #ev.ignore() + #self.updateHoverBrush() + + #def hoverMoveEvent(self, ev): + #print "rgn hover move" + #ev.ignore() + #self.updateHoverBrush() + + #def hoverLeaveEvent(self, ev): + #print "rgn hover leave" + #ev.ignore() + #self.updateHoverBrush(False) + + #def updateHoverBrush(self, hover=None): + #if hover is None: + #scene = self.scene() + #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) + + #if hover: + #self.currentBrush = fn.mkBrush(255, 0,0,100) + #else: + #self.currentBrush = self.brush + #self.update() + diff --git a/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py similarity index 72% rename from MultiPlotItem.py rename to pyqtgraph/graphicsItems/MultiPlotItem.py index 2f73c9e5..d20467a9 100644 --- a/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -6,8 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from numpy import ndarray -from graphicsItems import * -from PlotItem import * +from . import GraphicsLayout try: from metaarray import * @@ -16,22 +15,18 @@ except: #raise HAVE_METAARRAY = False - -class MultiPlotItem(QtGui.QGraphicsWidget): - def __init__(self, parent=None): - QtGui.QGraphicsWidget.__init__(self, parent) - self.layout = QtGui.QGraphicsGridLayout() - self.layout.setContentsMargins(1,1,1,1) - self.setLayout(self.layout) - self.layout.setHorizontalSpacing(0) - self.layout.setVerticalSpacing(4) - self.plots = [] +__all__ = ['MultiPlotItem'] +class MultiPlotItem(GraphicsLayout.GraphicsLayout): + """ + Automaticaly generates a grid of plots from a multi-dimensional array + """ + def plot(self, data): #self.layout.clear() self.plots = [] - if HAVE_METAARRAY and isinstance(data, MetaArray): + if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): if data.ndim != 2: raise Exception("MultiPlot currently only accepts 2D MetaArray.") ic = data.infoCopy() @@ -42,11 +37,12 @@ class MultiPlotItem(QtGui.QGraphicsWidget): break #print "Plotting using axis %d as columns (%d plots)" % (ax, data.shape[ax]) for i in range(data.shape[ax]): - pi = PlotItem() + pi = self.addPlot() + self.nextRow() sl = [slice(None)] * 2 sl[ax] = i pi.plot(data[tuple(sl)]) - self.layout.addItem(pi, i, 0) + #self.layout.addItem(pi, i, 0) self.plots.append((pi, i, 0)) title = None units = None @@ -67,5 +63,7 @@ class MultiPlotItem(QtGui.QGraphicsWidget): for p in self.plots: p[0].close() self.plots = None - for i in range(self.layout.count()): - self.layout.removeAt(i) \ No newline at end of file + self.clear() + + + diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py new file mode 100644 index 00000000..5314b0f2 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -0,0 +1,514 @@ +from pyqtgraph.Qt import QtGui, QtCore +from scipy.fftpack import fft +import numpy as np +import scipy.stats +from .GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from pyqtgraph import debug +from pyqtgraph.Point import Point +import pyqtgraph as pg +import struct, sys + +__all__ = ['PlotCurveItem'] +class PlotCurveItem(GraphicsObject): + + + """ + Class representing a single plot curve. Instances of this class are created + automatically as part of PlotDataItem; these rarely need to be instantiated + directly. + + Features: + + - Fast data update + - FFT display mode (accessed via PlotItem context menu) + - Fill under curve + - Mouse interaction + + ==================== =============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data being plotted has changed + sigClicked(self) Emitted when the curve is clicked + ==================== =============================================== + """ + + sigPlotChanged = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + + def __init__(self, *args, **kargs): + """ + Forwards all arguments to :func:`setData `. + + Some extra arguments are accepted as well: + + ============== ======================================================= + **Arguments:** + parent The parent GraphicsObject (optional) + clickable If True, the item will emit sigClicked when it is + clicked on. Defaults to False. + ============== ======================================================= + """ + GraphicsObject.__init__(self, kargs.get('parent', None)) + self.clear() + self.path = None + self.fillPath = None + + + ## this is disastrous for performance. + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.metaData = {} + self.opts = { + 'pen': fn.mkPen('w'), + 'shadowPen': None, + 'fillLevel': None, + 'brush': None, + 'stepMode': False, + 'name': None, + 'antialias': pg.getConfigOption('antialias'), + } + self.setClickable(kargs.get('clickable', False)) + self.setData(*args, **kargs) + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def setClickable(self, s): + """Sets whether the item responds to mouse clicks.""" + self.clickable = s + + + def getData(self): + return self.xData, self.yData + + def dataBounds(self, ax, frac=1.0, orthoRange=None): + (x, y) = self.getData() + if x is None or len(x) == 0: + return (0, 0) + + if ax == 0: + d = x + d2 = y + elif ax == 1: + d = y + d2 = x + + if orthoRange is not None: + mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + d = d[mask] + d2 = d2[mask] + + + if frac >= 1.0: + return (d.min(), d.max()) + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + else: + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + def setPen(self, *args, **kargs): + """Set the pen used to draw the curve.""" + self.opts['pen'] = fn.mkPen(*args, **kargs) + self.update() + + def setShadowPen(self, *args, **kargs): + """Set the shadow pen used to draw behind tyhe primary pen. + This pen must have a larger width than the primary + pen to be visible. + """ + self.opts['shadowPen'] = fn.mkPen(*args, **kargs) + self.update() + + def setBrush(self, *args, **kargs): + """Set the brush used when filling the area under the curve""" + self.opts['brush'] = fn.mkBrush(*args, **kargs) + self.update() + + def setFillLevel(self, level): + """Set the level filled to when filling under the curve""" + self.opts['fillLevel'] = level + self.fillPath = None + self.update() + + #def setColor(self, color): + #self.pen.setColor(color) + #self.update() + + #def setAlpha(self, alpha, auto): + #self.opts['alphaHint'] = alpha + #self.opts['alphaMode'] = auto + #self.update() + + #def setSpectrumMode(self, mode): + #self.opts['spectrumMode'] = mode + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + #def setLogMode(self, mode): + #self.opts['logMode'] = mode + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + #def setPointMode(self, mode): + #self.opts['pointMode'] = mode + #self.update() + + + #def setDownsampling(self, ds): + #if self.opts['downsample'] != ds: + #self.opts['downsample'] = ds + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + def setData(self, *args, **kargs): + """ + ============== ======================================================== + **Arguments:** + x, y (numpy arrays) Data to show + pen Pen to use when drawing. Any single argument accepted by + :func:`mkPen ` is allowed. + shadowPen Pen for drawing behind the primary pen. Usually this + is used to emphasize the curve by providing a + high-contrast border. Any single argument accepted by + :func:`mkPen ` is allowed. + fillLevel (float or None) Fill the area 'under' the curve to + *fillLevel* + brush QBrush to use when filling. Any single argument accepted + by :func:`mkBrush ` is allowed. + antialias (bool) Whether to use antialiasing when drawing. This + is disabled by default because it decreases performance. + ============== ======================================================== + + If non-keyword arguments are used, they will be interpreted as + setData(y) for a single argument and setData(x, y) for two + arguments. + + + """ + self.updateData(*args, **kargs) + + def updateData(self, *args, **kargs): + prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) + + if len(args) == 1: + kargs['y'] = args[0] + elif len(args) == 2: + kargs['x'] = args[0] + kargs['y'] = args[1] + + if 'y' not in kargs or kargs['y'] is None: + kargs['y'] = np.array([]) + if 'x' not in kargs or kargs['x'] is None: + kargs['x'] = np.arange(len(kargs['y'])) + + for k in ['x', 'y']: + data = kargs[k] + if isinstance(data, list): + data = np.array(data) + kargs[k] = data + if not isinstance(data, np.ndarray) or data.ndim > 1: + raise Exception("Plot data must be 1D ndarray.") + if 'complex' in str(data.dtype): + raise Exception("Can not plot complex data types.") + + prof.mark("data checks") + + #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly + ## Test this bug with test_PlotWidget and zoom in on the animated plot + self.prepareGeometryChange() + self.yData = kargs['y'].view(np.ndarray) + self.xData = kargs['x'].view(np.ndarray) + + prof.mark('copy') + + if 'stepMode' in kargs: + self.opts['stepMode'] = kargs['stepMode'] + + if self.opts['stepMode'] is True: + if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots + raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (str(x.shape), str(y.shape))) + else: + if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots + raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) + + self.path = None + self.fillPath = None + #self.xDisp = self.yDisp = None + + if 'name' in kargs: + self.opts['name'] = kargs['name'] + + if 'pen' in kargs: + self.setPen(kargs['pen']) + if 'shadowPen' in kargs: + self.setShadowPen(kargs['shadowPen']) + if 'fillLevel' in kargs: + self.setFillLevel(kargs['fillLevel']) + if 'brush' in kargs: + self.setBrush(kargs['brush']) + if 'antialias' in kargs: + self.opts['antialias'] = kargs['antialias'] + + + prof.mark('set') + self.update() + prof.mark('update') + self.sigPlotChanged.emit(self) + prof.mark('emit') + prof.finish() + + def generatePath(self, x, y): + prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) + path = QtGui.QPainterPath() + + ## Create all vertices in path. The method used below creates a binary format so that all + ## vertices can be read in at once. This binary format may change in future versions of Qt, + ## so the original (slower) method is left here for emergencies: + #path.moveTo(x[0], y[0]) + #for i in range(1, y.shape[0]): + # path.lineTo(x[i], y[i]) + + ## Speed this up using >> operator + ## Format is: + ## numVerts(i4) 0(i4) + ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect + ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex + ## ... + ## 0(i4) + ## + ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') + + if self.opts['stepMode']: + ## each value in the x/y arrays generates 2 points. + x2 = np.empty((len(x),2), dtype=x.dtype) + x2[:] = x[:,np.newaxis] + if self.opts['fillLevel'] is None: + x = x2.reshape(x2.size)[1:-1] + y2 = np.empty((len(y),2), dtype=y.dtype) + y2[:] = y[:,np.newaxis] + y = y2.reshape(y2.size) + else: + ## If we have a fill level, add two extra points at either end + x = x2.reshape(x2.size) + y2 = np.empty((len(y)+2,2), dtype=y.dtype) + y2[1:-1] = y[:,np.newaxis] + y = y2.reshape(y2.size)[1:-1] + y[0] = self.opts['fillLevel'] + y[-1] = self.opts['fillLevel'] + + + + + + if sys.version_info[0] == 2: ## So this is disabled for python 3... why?? + n = x.shape[0] + # create empty array, pad with extra space on either end + arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) + # write first two integers + prof.mark('allocate empty') + arr.data[12:20] = struct.pack('>ii', n, 0) + prof.mark('pack header') + # Fill array with vertex values + arr[1:-1]['x'] = x + arr[1:-1]['y'] = y + arr[1:-1]['c'] = 1 + prof.mark('fill array') + # write last 0 + lastInd = 20*(n+1) + arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) + prof.mark('footer') + # create datastream object and stream into path + buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + prof.mark('create buffer') + ds = QtCore.QDataStream(buf) + prof.mark('create datastream') + ds >> path + prof.mark('load') + + prof.finish() + else: + path.moveTo(x[0], y[0]) + for i in range(1, y.shape[0]): + path.lineTo(x[i], y[i]) + + return path + + + def shape(self): + if self.path is None: + try: + self.path = self.generatePath(*self.getData()) + except: + return QtGui.QPainterPath() + return self.path + + def boundingRect(self): + (x, y) = self.getData() + if x is None or y is None or len(x) == 0 or len(y) == 0: + return QtCore.QRectF() + + + if self.opts['shadowPen'] is not None: + lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) + else: + lineWidth = (self.opts['pen'].width()+1) + + + pixels = self.pixelVectors() + if pixels == (None, None): + pixels = [Point(0,0), Point(0,0)] + + xmin = x.min() + xmax = x.max() + ymin = y.min() + ymax = y.max() + + if self.opts['fillLevel'] is not None: + ymin = min(ymin, self.opts['fillLevel']) + ymax = max(ymax, self.opts['fillLevel']) + + xmin -= pixels[0].x() * lineWidth + xmax += pixels[0].x() * lineWidth + ymin -= abs(pixels[1].y()) * lineWidth + ymax += abs(pixels[1].y()) * lineWidth + + return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + + def paint(self, p, opt, widget): + prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) + if self.xData is None: + return + #if self.opts['spectrumMode']: + #if self.specPath is None: + + #self.specPath = self.generatePath(*self.getData()) + #path = self.specPath + #else: + x = None + y = None + if self.path is None: + x,y = self.getData() + if x is None or len(x) == 0 or y is None or len(y) == 0: + return + self.path = self.generatePath(x,y) + self.fillPath = None + + + path = self.path + prof.mark('generate path') + + if self._exportOpts is not False: + aa = self._exportOpts.get('antialias', True) + else: + aa = self.opts['antialias'] + + p.setRenderHint(p.Antialiasing, aa) + + + if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: + if self.fillPath is None: + if x is None: + x,y = self.getData() + p2 = QtGui.QPainterPath(self.path) + p2.lineTo(x[-1], self.opts['fillLevel']) + p2.lineTo(x[0], self.opts['fillLevel']) + p2.lineTo(x[0], y[0]) + p2.closeSubpath() + self.fillPath = p2 + + prof.mark('generate fill path') + p.fillPath(self.fillPath, self.opts['brush']) + prof.mark('draw fill path') + + + ## Copy pens and apply alpha adjustment + sp = QtGui.QPen(self.opts['shadowPen']) + cp = QtGui.QPen(self.opts['pen']) + #for pen in [sp, cp]: + #if pen is None: + #continue + #c = pen.color() + #c.setAlpha(c.alpha() * self.opts['alphaHint']) + #pen.setColor(c) + ##pen.setCosmetic(True) + + + + if sp is not None and sp.style() != QtCore.Qt.NoPen: + p.setPen(sp) + p.drawPath(path) + p.setPen(cp) + p.drawPath(path) + prof.mark('drawPath') + + #print "Render hints:", int(p.renderHints()) + prof.finish() + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawRect(self.boundingRect()) + + + def clear(self): + self.xData = None ## raw values + self.yData = None + self.xDisp = None ## display values (after log / fft) + self.yDisp = None + self.path = None + #del self.xData, self.yData, self.xDisp, self.yDisp, self.path + + #def mousePressEvent(self, ev): + ##GraphicsObject.mousePressEvent(self, ev) + #if not self.clickable: + #ev.ignore() + #if ev.button() != QtCore.Qt.LeftButton: + #ev.ignore() + #self.mousePressPos = ev.pos() + #self.mouseMoved = False + + #def mouseMoveEvent(self, ev): + ##GraphicsObject.mouseMoveEvent(self, ev) + #self.mouseMoved = True + ##print "move" + + #def mouseReleaseEvent(self, ev): + ##GraphicsObject.mouseReleaseEvent(self, ev) + #if not self.mouseMoved: + #self.sigClicked.emit(self) + + def mouseClickEvent(self, ev): + if not self.clickable or ev.button() != QtCore.Qt.LeftButton: + return + ev.accept() + self.sigClicked.emit(self) + + +class ROIPlotItem(PlotCurveItem): + """Plot curve that monitors an ROI and image for changes to automatically replot.""" + def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): + self.roi = roi + self.roiData = data + self.roiImg = img + self.axes = axes + self.xVals = xVals + PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) + #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + roi.sigRegionChanged.connect(self.roiChangedEvent) + #self.roiChangedEvent() + + def getRoiData(self): + d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) + if d is None: + return + while d.ndim > 1: + d = d.mean(axis=1) + return d + + def roiChangedEvent(self): + d = self.getRoiData() + self.updateData(d, self.xVals) + diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py new file mode 100644 index 00000000..714210c4 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -0,0 +1,706 @@ +import pyqtgraph.metaarray as metaarray +from pyqtgraph.Qt import QtCore +from .GraphicsObject import GraphicsObject +from .PlotCurveItem import PlotCurveItem +from .ScatterPlotItem import ScatterPlotItem +import numpy as np +import scipy +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug +import pyqtgraph as pg + +class PlotDataItem(GraphicsObject): + """ + **Bases:** :class:`GraphicsObject ` + + GraphicsItem for displaying plot curves, scatter plots, or both. + While it is possible to use :class:`PlotCurveItem ` or + :class:`ScatterPlotItem ` individually, this class + provides a unified interface to both. Inspances of :class:`PlotDataItem` are + usually created by plot() methods such as :func:`pyqtgraph.plot` and + :func:`PlotItem.plot() `. + + ============================== ============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data in this item is updated. + sigClicked(self) Emitted when the item is clicked. + sigPointsClicked(self, points) Emitted when a plot point is clicked + Sends the list of points under the mouse. + ============================== ============================================== + """ + + sigPlotChanged = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + sigPointsClicked = QtCore.Signal(object, object) + + def __init__(self, *args, **kargs): + """ + There are many different ways to create a PlotDataItem: + + **Data initialization arguments:** (x,y data only) + + =================================== ====================================== + PlotDataItem(xValues, yValues) x and y values may be any sequence (including ndarray) of real numbers + PlotDataItem(yValues) y values only -- x will be automatically set to range(len(y)) + PlotDataItem(x=xValues, y=yValues) x and y given by keyword arguments + PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1] + =================================== ====================================== + + **Data initialization arguments:** (x,y data AND may include spot style) + + =========================== ========================================= + PlotDataItem(recarray) numpy array with dtype=[('x', float), ('y', float), ...] + PlotDataItem(list-of-dicts) [{'x': x, 'y': y, ...}, ...] + PlotDataItem(dict-of-lists) {'x': [...], 'y': [...], ...} + PlotDataItem(MetaArray) 1D array of Y values with X sepecified as axis values + OR 2D array with a column 'y' and extra columns as needed. + =========================== ========================================= + + **Line style keyword arguments:** + ========== ================================================ + pen Pen to use for drawing line between points. + Default is solid grey, 1px width. Use None to disable line drawing. + May be any single argument accepted by :func:`mkPen() ` + shadowPen Pen for secondary line to draw behind the primary line. disabled by default. + May be any single argument accepted by :func:`mkPen() ` + fillLevel Fill the area between the curve and fillLevel + fillBrush Fill to use when fillLevel is specified. + May be any single argument accepted by :func:`mkBrush() ` + ========== ================================================ + + **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) + + ============ ================================================ + symbol Symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. + Options are o, s, t, d, +, or any QPainterPath + symbolPen Outline pen for drawing points OR list of pens, one per point. + May be any single argument accepted by :func:`mkPen() ` + symbolBrush Brush for filling points OR list of brushes, one per point. + May be any single argument accepted by :func:`mkBrush() ` + symbolSize Diameter of symbols OR list of diameters. + pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is + specified in data coordinates. + ============ ================================================ + + **Optimization keyword arguments:** + + ========== ===================================================================== + antialias (bool) By default, antialiasing is disabled to improve performance. + Note that in some cases (in particluar, when pxMode=True), points + will be rendered antialiased even if this is set to False. + identical *deprecated* + decimate (int) sub-sample data by selecting every nth sample before plotting + ========== ===================================================================== + + **Meta-info keyword arguments:** + + ========== ================================================ + name name of dataset. This would appear in a legend + ========== ================================================ + """ + GraphicsObject.__init__(self) + self.setFlag(self.ItemHasNoContents) + self.xData = None + self.yData = None + self.xDisp = None + self.yDisp = None + #self.curves = [] + #self.scatters = [] + self.curve = PlotCurveItem() + self.scatter = ScatterPlotItem() + self.curve.setParentItem(self) + self.scatter.setParentItem(self) + + self.curve.sigClicked.connect(self.curveClicked) + self.scatter.sigClicked.connect(self.scatterClicked) + + + #self.clear() + self.opts = { + 'fftMode': False, + 'logMode': [False, False], + 'downsample': False, + 'alphaHint': 1.0, + 'alphaMode': False, + + 'pen': (200,200,200), + 'shadowPen': None, + 'fillLevel': None, + 'fillBrush': None, + + 'symbol': None, + 'symbolSize': 10, + 'symbolPen': (200,200,200), + 'symbolBrush': (50, 50, 150), + 'pxMode': True, + + 'antialias': pg.getConfigOption('antialias'), + 'pointMode': None, + + 'data': None, + } + self.setData(*args, **kargs) + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def boundingRect(self): + return QtCore.QRectF() ## let child items handle this + + def setAlpha(self, alpha, auto): + if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto: + return + self.opts['alphaHint'] = alpha + self.opts['alphaMode'] = auto + self.setOpacity(alpha) + #self.update() + + def setFftMode(self, mode): + if self.opts['fftMode'] == mode: + return + self.opts['fftMode'] = mode + self.xDisp = self.yDisp = None + self.updateItems() + + def setLogMode(self, xMode, yMode): + if self.opts['logMode'] == [xMode, yMode]: + return + self.opts['logMode'] = [xMode, yMode] + self.xDisp = self.yDisp = None + self.updateItems() + + def setPointMode(self, mode): + if self.opts['pointMode'] == mode: + return + self.opts['pointMode'] = mode + self.update() + + def setPen(self, *args, **kargs): + """ + | Sets the pen used to draw lines between points. + | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` + """ + pen = fn.mkPen(*args, **kargs) + self.opts['pen'] = pen + #self.curve.setPen(pen) + #for c in self.curves: + #c.setPen(pen) + #self.update() + self.updateItems() + + def setShadowPen(self, *args, **kargs): + """ + | Sets the shadow pen used to draw lines between points (this is for enhancing contrast or + emphacizing data). + | This line is drawn behind the primary pen (see :func:`setPen() `) + and should generally be assigned greater width than the primary pen. + | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` + """ + pen = fn.mkPen(*args, **kargs) + self.opts['shadowPen'] = pen + #for c in self.curves: + #c.setPen(pen) + #self.update() + self.updateItems() + + def setFillBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + if self.opts['fillBrush'] == brush: + return + self.opts['fillBrush'] = brush + self.updateItems() + + def setBrush(self, *args, **kargs): + return self.setFillBrush(*args, **kargs) + + def setFillLevel(self, level): + if self.opts['fillLevel'] == level: + return + self.opts['fillLevel'] = level + self.updateItems() + + def setSymbol(self, symbol): + if self.opts['symbol'] == symbol: + return + self.opts['symbol'] = symbol + #self.scatter.setSymbol(symbol) + self.updateItems() + + def setSymbolPen(self, *args, **kargs): + pen = fn.mkPen(*args, **kargs) + if self.opts['symbolPen'] == pen: + return + self.opts['symbolPen'] = pen + #self.scatter.setSymbolPen(pen) + self.updateItems() + + + + def setSymbolBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + if self.opts['symbolBrush'] == brush: + return + self.opts['symbolBrush'] = brush + #self.scatter.setSymbolBrush(brush) + self.updateItems() + + + def setSymbolSize(self, size): + if self.opts['symbolSize'] == size: + return + self.opts['symbolSize'] = size + #self.scatter.setSymbolSize(symbolSize) + self.updateItems() + + def setDownsampling(self, ds): + if self.opts['downsample'] == ds: + return + self.opts['downsample'] = ds + self.xDisp = self.yDisp = None + self.updateItems() + + def setData(self, *args, **kargs): + """ + Clear any data displayed by this item and display new data. + See :func:`__init__() ` for details; it accepts the same arguments. + """ + #self.clear() + prof = debug.Profiler('PlotDataItem.setData (0x%x)' % id(self), disabled=True) + y = None + x = None + if len(args) == 1: + data = args[0] + dt = dataType(data) + if dt == 'empty': + pass + elif dt == 'listOfValues': + y = np.array(data) + elif dt == 'Nx2array': + x = data[:,0] + y = data[:,1] + elif dt == 'recarray' or dt == 'dictOfLists': + if 'x' in data: + x = np.array(data['x']) + if 'y' in data: + y = np.array(data['y']) + elif dt == 'listOfDicts': + if 'x' in data[0]: + x = np.array([d.get('x',None) for d in data]) + if 'y' in data[0]: + y = np.array([d.get('y',None) for d in data]) + for k in ['data', 'symbolSize', 'symbolPen', 'symbolBrush', 'symbolShape']: + if k in data: + kargs[k] = [d.get(k, None) for d in data] + elif dt == 'MetaArray': + y = data.view(np.ndarray) + x = data.xvals(0).view(np.ndarray) + else: + raise Exception('Invalid data type %s' % type(data)) + + elif len(args) == 2: + seq = ('listOfValues', 'MetaArray') + if dataType(args[0]) not in seq or dataType(args[1]) not in seq: + raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) + if not isinstance(args[0], np.ndarray): + x = np.array(args[0]) + else: + x = args[0].view(np.ndarray) + if not isinstance(args[1], np.ndarray): + y = np.array(args[1]) + else: + y = args[1].view(np.ndarray) + + if 'x' in kargs: + x = kargs['x'] + if 'y' in kargs: + y = kargs['y'] + + prof.mark('interpret data') + ## pull in all style arguments. + ## Use self.opts to fill in anything not present in kargs. + + if 'name' in kargs: + self.opts['name'] = kargs['name'] + + ## if symbol pen/brush are given with no symbol, then assume symbol is 'o' + + if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): + kargs['symbol'] = 'o' + + if 'brush' in kargs: + kargs['fillBrush'] = kargs['brush'] + + for k in list(self.opts.keys()): + if k in kargs: + self.opts[k] = kargs[k] + + #curveArgs = {} + #for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: + #if k in kargs: + #self.opts[k] = kargs[k] + #curveArgs[k] = self.opts[k] + + #scatterArgs = {} + #for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: + #if k in kargs: + #self.opts[k] = kargs[k] + #scatterArgs[v] = self.opts[k] + + + if y is None: + return + if y is not None and x is None: + x = np.arange(len(y)) + + if isinstance(x, list): + x = np.array(x) + if isinstance(y, list): + y = np.array(y) + + self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by + self.yData = y.view(np.ndarray) + self.xDisp = None + self.yDisp = None + prof.mark('set data') + + self.updateItems() + prof.mark('update items') + + view = self.getViewBox() + if view is not None: + view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + + self.sigPlotChanged.emit(self) + prof.mark('emit') + prof.finish() + + + def updateItems(self): + #for c in self.curves+self.scatters: + #if c.scene() is not None: + #c.scene().removeItem(c) + + curveArgs = {} + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias')]: + curveArgs[v] = self.opts[k] + + scatterArgs = {} + for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data'), ('pxMode', 'pxMode'), ('antialias', 'antialias')]: + if k in self.opts: + scatterArgs[v] = self.opts[k] + + x,y = self.getData() + + if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None): + self.curve.setData(x=x, y=y, **curveArgs) + self.curve.show() + else: + self.curve.hide() + #curve = PlotCurveItem(x=x, y=y, **curveArgs) + #curve.setParentItem(self) + #self.curves.append(curve) + + if scatterArgs['symbol'] is not None: + self.scatter.setData(x=x, y=y, **scatterArgs) + self.scatter.show() + else: + self.scatter.hide() + #sp = ScatterPlotItem(x=x, y=y, **scatterArgs) + #sp.setParentItem(self) + #self.scatters.append(sp) + + + def getData(self): + if self.xData is None: + return (None, None) + if self.xDisp is None: + nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) + if any(nanMask): + x = self.xData[~nanMask] + y = self.yData[~nanMask] + else: + x = self.xData + y = self.yData + ds = self.opts['downsample'] + if ds > 1: + x = x[::ds] + #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + y = y[::ds] + if self.opts['fftMode']: + f = np.fft.fft(y) / len(y) + y = abs(f[1:len(f)/2]) + dt = x[-1] - x[0] + x = np.linspace(0, 0.5*len(x)/dt, len(y)) + if self.opts['logMode'][0]: + x = np.log10(x) + if self.opts['logMode'][1]: + y = np.log10(y) + if any(self.opts['logMode']): ## re-check for NANs after log + nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y) + if any(nanMask): + x = x[~nanMask] + y = y[~nanMask] + self.xDisp = x + self.yDisp = y + #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + return self.xDisp, self.yDisp + + def dataBounds(self, ax, frac=1.0, orthoRange=None): + """ + Returns the range occupied by the data (along a specific axis) in this item. + This method is called by ViewBox when auto-scaling. + + =============== ============================================================= + **Arguments:** + ax (0 or 1) the axis for which to return this item's data range + frac (float 0.0-1.0) Specifies what fraction of the total data + range to return. By default, the entire range is returned. + This allows the ViewBox to ignore large spikes in the data + when auto-scaling. + orthoRange ([min,max] or None) Specifies that only the data within the + given range (orthogonal to *ax*) should me measured when + returning the data range. (For example, a ViewBox might ask + what is the y-range of all data with x-values between min + and max) + =============== ============================================================= + """ + if frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + + (x, y) = self.getData() + if x is None or len(x) == 0: + return None + + if ax == 0: + d = x + d2 = y + elif ax == 1: + d = y + d2 = x + + if orthoRange is not None: + mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + d = d[mask] + #d2 = d2[mask] + + if len(d) > 0: + if frac >= 1.0: + return (np.min(d), np.max(d)) + else: + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + else: + return None + + + def clear(self): + #for i in self.curves+self.scatters: + #if i.scene() is not None: + #i.scene().removeItem(i) + #self.curves = [] + #self.scatters = [] + self.xData = None + self.yData = None + self.xDisp = None + self.yDisp = None + self.curve.setData([]) + self.scatter.setData([]) + + def appendData(self, *args, **kargs): + pass + + def curveClicked(self): + self.sigClicked.emit(self) + + def scatterClicked(self, plt, points): + self.sigClicked.emit(self) + self.sigPointsClicked.emit(self, points) + + +def dataType(obj): + if hasattr(obj, '__len__') and len(obj) == 0: + return 'empty' + if isinstance(obj, dict): + return 'dictOfLists' + elif isSequence(obj): + first = obj[0] + + if (hasattr(obj, 'implements') and obj.implements('MetaArray')): + return 'MetaArray' + elif isinstance(obj, np.ndarray): + if obj.ndim == 1: + if obj.dtype.names is None: + return 'listOfValues' + else: + return 'recarray' + elif obj.ndim == 2 and obj.dtype.names is None and obj.shape[1] == 2: + return 'Nx2array' + else: + raise Exception('array shape must be (N,) or (N,2); got %s instead' % str(obj.shape)) + elif isinstance(first, dict): + return 'listOfDicts' + else: + return 'listOfValues' + + +def isSequence(obj): + return hasattr(obj, '__iter__') or isinstance(obj, np.ndarray) or (hasattr(obj, 'implements') and obj.implements('MetaArray')) + + + +#class TableData: + #""" + #Class for presenting multiple forms of tabular data through a consistent interface. + #May contain: + #- numpy record array + #- list-of-dicts (all dicts are _not_ required to have the same keys) + #- dict-of-lists + #- dict (single record) + #Note: if all the values in this record are lists, it will be interpreted as multiple records + + #Data can be accessed and modified by column, by row, or by value + #data[columnName] + #data[rowId] + #data[columnName, rowId] = value + #data[columnName] = [value, value, ...] + #data[rowId] = {columnName: value, ...} + #""" + + #def __init__(self, data): + #self.data = data + #if isinstance(data, np.ndarray): + #self.mode = 'array' + #elif isinstance(data, list): + #self.mode = 'list' + #elif isinstance(data, dict): + #types = set(map(type, data.values())) + ### dict may be a dict-of-lists or a single record + #types -= set([list, np.ndarray]) ## if dict contains any non-sequence values, it is probably a single record. + #if len(types) != 0: + #self.data = [self.data] + #self.mode = 'list' + #else: + #self.mode = 'dict' + #elif isinstance(data, TableData): + #self.data = data.data + #self.mode = data.mode + #else: + #raise TypeError(type(data)) + + #for fn in ['__getitem__', '__setitem__']: + #setattr(self, fn, getattr(self, '_TableData'+fn+self.mode)) + + #def originalData(self): + #return self.data + + #def toArray(self): + #if self.mode == 'array': + #return self.data + #if len(self) < 1: + ##return np.array([]) ## need to return empty array *with correct columns*, but this is very difficult, so just return None + #return None + #rec1 = self[0] + #dtype = functions.suggestRecordDType(rec1) + ##print rec1, dtype + #arr = np.empty(len(self), dtype=dtype) + #arr[0] = tuple(rec1.values()) + #for i in xrange(1, len(self)): + #arr[i] = tuple(self[i].values()) + #return arr + + #def __getitem__array(self, arg): + #if isinstance(arg, tuple): + #return self.data[arg[0]][arg[1]] + #else: + #return self.data[arg] + + #def __getitem__list(self, arg): + #if isinstance(arg, basestring): + #return [d.get(arg, None) for d in self.data] + #elif isinstance(arg, int): + #return self.data[arg] + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #return self.data[arg[0]][arg[1]] + #else: + #raise TypeError(type(arg)) + + #def __getitem__dict(self, arg): + #if isinstance(arg, basestring): + #return self.data[arg] + #elif isinstance(arg, int): + #return dict([(k, v[arg]) for k, v in self.data.iteritems()]) + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #return self.data[arg[1]][arg[0]] + #else: + #raise TypeError(type(arg)) + + #def __setitem__array(self, arg, val): + #if isinstance(arg, tuple): + #self.data[arg[0]][arg[1]] = val + #else: + #self.data[arg] = val + + #def __setitem__list(self, arg, val): + #if isinstance(arg, basestring): + #if len(val) != len(self.data): + #raise Exception("Values (%d) and data set (%d) are not the same length." % (len(val), len(self.data))) + #for i, rec in enumerate(self.data): + #rec[arg] = val[i] + #elif isinstance(arg, int): + #self.data[arg] = val + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #self.data[arg[0]][arg[1]] = val + #else: + #raise TypeError(type(arg)) + + #def __setitem__dict(self, arg, val): + #if isinstance(arg, basestring): + #if len(val) != len(self.data[arg]): + #raise Exception("Values (%d) and data set (%d) are not the same length." % (len(val), len(self.data[arg]))) + #self.data[arg] = val + #elif isinstance(arg, int): + #for k in self.data: + #self.data[k][arg] = val[k] + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #self.data[arg[1]][arg[0]] = val + #else: + #raise TypeError(type(arg)) + + #def _orderArgs(self, args): + ### return args in (int, str) order + #if isinstance(args[0], basestring): + #return (args[1], args[0]) + #else: + #return args + + #def __iter__(self): + #for i in xrange(len(self)): + #yield self[i] + + #def __len__(self): + #if self.mode == 'array' or self.mode == 'list': + #return len(self.data) + #else: + #return max(map(len, self.data.values())) + + #def columnNames(self): + #"""returns column names in no particular order""" + #if self.mode == 'array': + #return self.data.dtype.names + #elif self.mode == 'list': + #names = set() + #for row in self.data: + #names.update(row.keys()) + #return list(names) + #elif self.mode == 'dict': + #return self.data.keys() + + #def keys(self): + #return self.columnNames() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py new file mode 100644 index 00000000..c362ffb5 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -0,0 +1,1163 @@ +# -*- coding: utf-8 -*- +""" +PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +This class is one of the workhorses of pyqtgraph. It implements a graphics item with +plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want +a widget that can be added to your GUI, see PlotWidget instead. + +This class is very heavily featured: + - Automatically creates and manages PlotCurveItems + - Fast display and update of plots + - Manages zoom/pan ViewBox, scale, and label elements + - Automatic scaling when data changes + - Control panel with a huge feature set including averaging, decimation, + display, power spectrum, svg/png export, plot linking, and more. +""" +from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +import pyqtgraph.pixmaps + +if USE_PYSIDE: + from .plotConfigTemplate_pyside import * +else: + from .plotConfigTemplate_pyqt import * + +import pyqtgraph.functions as fn +from pyqtgraph.widgets.FileDialog import FileDialog +import weakref +import numpy as np +import os +from .. PlotDataItem import PlotDataItem +from .. ViewBox import ViewBox +from .. AxisItem import AxisItem +from .. LabelItem import LabelItem +from .. LegendItem import LegendItem +from .. GraphicsWidget import GraphicsWidget +from .. ButtonItem import ButtonItem +from pyqtgraph.WidgetGroup import WidgetGroup + +__all__ = ['PlotItem'] + +try: + from metaarray import * + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + + + +class PlotItem(GraphicsWidget): + + """ + **Bases:** :class:`GraphicsWidget ` + + Plot graphics item that can be added to any graphics scene. Implements axes, titles, and interactive viewbox. + PlotItem also provides some basic analysis functionality that may be accessed from the context menu. + Use :func:`plot() ` to create a new PlotDataItem and add it to the view. + Use :func:`addItem() ` to add any QGraphicsItem to the view. + + This class wraps several methods from its internal ViewBox: + :func:`setXRange `, + :func:`setYRange `, + :func:`setRange `, + :func:`autoRange `, + :func:`setXLink `, + :func:`setYLink `, + :func:`setAutoPan `, + :func:`setAutoVisible `, + :func:`viewRect `, + :func:`viewRange `, + :func:`setMouseEnabled `, + :func:`enableAutoRange `, + :func:`disableAutoRange `, + :func:`setAspectLocked `, + :func:`invertY `, + :func:`register `, + :func:`unregister ` + + The ViewBox itself can be accessed by calling :func:`getViewBox() ` + + ==================== ======================================================================= + **Signals** + sigYRangeChanged wrapped from :class:`ViewBox ` + sigXRangeChanged wrapped from :class:`ViewBox ` + sigRangeChanged wrapped from :class:`ViewBox ` + ==================== ======================================================================= + """ + + sigRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox range has changed + sigYRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox Y range has changed + sigXRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox X range has changed + + + lastFileDir = None + managers = {} + + def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): + """ + Create a new PlotItem. All arguments are optional. + Any extra keyword arguments are passed to PlotItem.plot(). + + ============== ========================================================================================== + **Arguments** + *title* Title to display at the top of the item. Html is allowed. + *labels* A dictionary specifying the axis labels to display:: + + {'left': (args), 'bottom': (args), ...} + + The name of each axis and the corresponding arguments are passed to + :func:`PlotItem.setLabel() ` + Optionally, PlotItem my also be initialized with the keyword arguments left, + right, top, or bottom to achieve the same effect. + *name* Registers a name for this view so that others may link to it + *viewBox* If specified, the PlotItem will be constructed with this as its ViewBox. + *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items + for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') + and the values must be instances of AxisItem (or at least compatible with AxisItem). + ============== ========================================================================================== + """ + + GraphicsWidget.__init__(self, parent) + + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + + ## Set up control buttons + path = os.path.dirname(__file__) + #self.autoImageFile = os.path.join(path, 'auto.png') + #self.lockImageFile = os.path.join(path, 'lock.png') + self.autoBtn = ButtonItem(pyqtgraph.pixmaps.getPixmap('auto'), 14, self) + self.autoBtn.mode = 'auto' + self.autoBtn.clicked.connect(self.autoBtnClicked) + #self.autoBtn.hide() + self.buttonsHidden = False ## whether the user has requested buttons to be hidden + self.mouseHovering = False + + self.layout = QtGui.QGraphicsGridLayout() + self.layout.setContentsMargins(1,1,1,1) + self.setLayout(self.layout) + self.layout.setHorizontalSpacing(0) + self.layout.setVerticalSpacing(0) + + if viewBox is None: + viewBox = ViewBox() + self.vb = viewBox + self.vb.sigStateChanged.connect(self.viewStateChanged) + self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus + + if name is not None: + self.vb.register(name) + self.vb.sigRangeChanged.connect(self.sigRangeChanged) + self.vb.sigXRangeChanged.connect(self.sigXRangeChanged) + self.vb.sigYRangeChanged.connect(self.sigYRangeChanged) + + self.layout.addItem(self.vb, 2, 1) + self.alpha = 1.0 + self.autoAlpha = True + self.spectrumMode = False + + self.legend = None + + ## Create and place axis items + if axisItems is None: + axisItems = {} + self.axes = {} + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + axis = axisItems.get(k, 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) + + self.titleLabel = LabelItem('', size='11pt') + self.layout.addItem(self.titleLabel, 0, 1) + self.setTitle(None) ## hide + + + for i in range(4): + self.layout.setRowPreferredHeight(i, 0) + self.layout.setRowMinimumHeight(i, 0) + self.layout.setRowSpacing(i, 0) + self.layout.setRowStretchFactor(i, 1) + + for i in range(3): + self.layout.setColumnPreferredWidth(i, 0) + self.layout.setColumnMinimumWidth(i, 0) + self.layout.setColumnSpacing(i, 0) + self.layout.setColumnStretchFactor(i, 1) + self.layout.setRowStretchFactor(2, 100) + self.layout.setColumnStretchFactor(1, 100) + + + ## Wrap a few methods from viewBox + for m in [ + 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', + 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', + 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY', + 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. + setattr(self, m, getattr(self.vb, m)) + + self.items = [] + self.curves = [] + self.itemMeta = weakref.WeakKeyDictionary() + self.dataItems = [] + self.paramList = {} + self.avgCurves = {} + + ### Set up context menu + + w = QtGui.QWidget() + self.ctrl = c = Ui_Form() + c.setupUi(w) + dv = QtGui.QDoubleValidator(self) + + menuItems = [ + ('Transforms', c.transformGroup), + ('Downsample', c.decimateGroup), + ('Average', c.averageGroup), + ('Alpha', c.alphaGroup), + ('Grid', c.gridGroup), + ('Points', c.pointsGroup), + ] + + + self.ctrlMenu = QtGui.QMenu() + + self.ctrlMenu.setTitle('Plot Options') + self.subMenus = [] + for name, grp in menuItems: + sm = QtGui.QMenu(name) + act = QtGui.QWidgetAction(self) + act.setDefaultWidget(grp) + sm.addAction(act) + self.subMenus.append(sm) + self.ctrlMenu.addMenu(sm) + + self.stateGroup = WidgetGroup() + for name, w in menuItems: + self.stateGroup.autoAdd(w) + + self.fileDialog = None + + c.alphaGroup.toggled.connect(self.updateAlpha) + c.alphaSlider.valueChanged.connect(self.updateAlpha) + c.autoAlphaCheck.toggled.connect(self.updateAlpha) + + c.xGridCheck.toggled.connect(self.updateGrid) + c.yGridCheck.toggled.connect(self.updateGrid) + c.gridAlphaSlider.valueChanged.connect(self.updateGrid) + + c.fftCheck.toggled.connect(self.updateSpectrumMode) + c.logXCheck.toggled.connect(self.updateLogMode) + c.logYCheck.toggled.connect(self.updateLogMode) + + c.downsampleSpin.valueChanged.connect(self.updateDownsampling) + + self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) + self.ctrl.averageGroup.toggled.connect(self.avgToggled) + + self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) + self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) + + self.hideAxis('right') + self.hideAxis('top') + self.showAxis('left') + self.showAxis('bottom') + + if labels is None: + labels = {} + for label in list(self.axes.keys()): + if label in kargs: + labels[label] = kargs[label] + del kargs[label] + for k in labels: + if isinstance(labels[k], basestring): + labels[k] = (labels[k],) + self.setLabel(k, *labels[k]) + + if title is not None: + self.setTitle(title) + + if len(kargs) > 0: + self.plot(**kargs) + + + def implements(self, interface=None): + return interface in ['ViewBoxWrapper'] + + def getViewBox(self): + """Return the :class:`ViewBox ` contained within.""" + return self.vb + + + + def setLogMode(self, x, y): + """ + Set log scaling for x and y axes. + This informs PlotDataItems to transform logarithmically and switches + the axes to use log ticking. + + Note that *no other items* in the scene will be affected by + this; there is no generic way to redisplay a GraphicsItem + with log coordinates. + + """ + self.ctrl.logXCheck.setChecked(x) + self.ctrl.logYCheck.setChecked(y) + + def showGrid(self, x=None, y=None, alpha=None): + """ + Show or hide the grid for either axis. + + ============== ===================================== + **Arguments:** + x (bool) Whether to show the X grid + y (bool) Whether to show the Y grid + alpha (0.0-1.0) Opacity of the grid + ============== ===================================== + """ + if x is None and y is None and alpha is None: + raise Exception("Must specify at least one of x, y, or alpha.") ## prevent people getting confused if they just call showGrid() + + if x is not None: + self.ctrl.xGridCheck.setChecked(x) + if y is not None: + self.ctrl.yGridCheck.setChecked(y) + if alpha is not None: + v = np.clip(alpha, 0, 1)*self.ctrl.gridAlphaSlider.maximum() + self.ctrl.gridAlphaSlider.setValue(v) + + #def paint(self, *args): + #prof = debug.Profiler('PlotItem.paint', disabled=True) + #QtGui.QGraphicsWidget.paint(self, *args) + #prof.finish() + + ## bad idea. + #def __getattr__(self, attr): ## wrap ms + #return getattr(self.vb, attr) + + def close(self): + #print "delete", self + ## Most of this crap is needed to avoid PySide trouble. + ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) + ## the solution is to manually remove all widgets before scene.clear() is called + if self.ctrlMenu is None: ## already shut down + return + self.ctrlMenu.setParent(None) + self.ctrlMenu = None + + #self.ctrlBtn.setParent(None) + #self.ctrlBtn = None + #self.autoBtn.setParent(None) + #self.autoBtn = None + + for k in self.axes: + i = self.axes[k]['item'] + i.close() + + self.axes = None + self.scene().removeItem(self.vb) + self.vb = None + + ## causes invalid index errors: + #for i in range(self.layout.count()): + #self.layout.removeAt(i) + + #for p in self.proxies: + #try: + #p.setWidget(None) + #except RuntimeError: + #break + #self.scene().removeItem(p) + #self.proxies = [] + + #self.menuAction.releaseWidget(self.menuAction.defaultWidget()) + #self.menuAction.setParent(None) + #self.menuAction = None + + #if self.manager is not None: + #self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) + #self.manager.removeWidget(self.name) + #else: + #print "no manager" + + def registerPlot(self, name): ## for backward compatibility + self.vb.register(name) + + def updateGrid(self, *args): + alpha = self.ctrl.gridAlphaSlider.value() + x = alpha if self.ctrl.xGridCheck.isChecked() else False + y = alpha if self.ctrl.yGridCheck.isChecked() else False + self.getAxis('top').setGrid(x) + self.getAxis('bottom').setGrid(x) + self.getAxis('left').setGrid(y) + self.getAxis('right').setGrid(y) + + def viewGeometry(self): + """Return the screen geometry of the viewbox""" + v = self.scene().views()[0] + b = self.vb.mapRectToScene(self.vb.boundingRect()) + wr = v.mapFromScene(b).boundingRect() + pos = v.mapToGlobal(v.pos()) + wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) + return wr + + + def avgToggled(self, b): + if b: + self.recomputeAverages() + for k in self.avgCurves: + self.avgCurves[k][1].setVisible(b) + + def avgParamListClicked(self, item): + name = str(item.text()) + self.paramList[name] = (item.checkState() == QtCore.Qt.Checked) + self.recomputeAverages() + + def recomputeAverages(self): + if not self.ctrl.averageGroup.isChecked(): + return + for k in self.avgCurves: + self.removeItem(self.avgCurves[k][1]) + self.avgCurves = {} + for c in self.curves: + self.addAvgCurve(c) + self.replot() + + def addAvgCurve(self, curve): + ## Add a single curve into the pool of curves averaged together + + ## If there are plot parameters, then we need to determine which to average together. + remKeys = [] + addKeys = [] + if self.ctrl.avgParamList.count() > 0: + + ### First determine the key of the curve to which this new data should be averaged + for i in range(self.ctrl.avgParamList.count()): + item = self.ctrl.avgParamList.item(i) + if item.checkState() == QtCore.Qt.Checked: + remKeys.append(str(item.text())) + else: + addKeys.append(str(item.text())) + + if len(remKeys) < 1: ## In this case, there would be 1 average plot for each data plot; not useful. + return + + p = self.itemMeta.get(curve,{}).copy() + for k in p: + if type(k) is tuple: + p['.'.join(k)] = p[k] + del p[k] + for rk in remKeys: + if rk in p: + del p[rk] + for ak in addKeys: + if ak not in p: + p[ak] = None + key = tuple(p.items()) + + ### Create a new curve if needed + if key not in self.avgCurves: + plot = PlotDataItem() + plot.setPen(fn.mkPen([0, 200, 0])) + plot.setShadowPen(fn.mkPen([0, 0, 0, 100], width=3)) + plot.setAlpha(1.0, False) + plot.setZValue(100) + self.addItem(plot, skipAverage=True) + self.avgCurves[key] = [0, plot] + self.avgCurves[key][0] += 1 + (n, plot) = self.avgCurves[key] + + ### Average data together + (x, y) = curve.getData() + if plot.yData is not None: + newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) + plot.setData(plot.xData, newData) + else: + plot.setData(x, y) + + def autoBtnClicked(self): + if self.autoBtn.mode == 'auto': + self.enableAutoRange() + self.autoBtn.hide() + else: + self.disableAutoRange() + + def viewStateChanged(self): + self.updateButtons() + + def enableAutoScale(self): + """ + Enable auto-scaling. The plot will continuously scale to fit the boundaries of its data. + """ + print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.") + self.vb.enableAutoRange(self.vb.XYAxes) + + def addItem(self, item, *args, **kargs): + """ + Add a graphics item to the view box. + If the item has plot data (PlotDataItem, PlotCurveItem, ScatterPlotItem), it may + be included in analysis performed by the PlotItem. + """ + self.items.append(item) + vbargs = {} + if 'ignoreBounds' in kargs: + vbargs['ignoreBounds'] = kargs['ignoreBounds'] + self.vb.addItem(item, *args, **vbargs) + if hasattr(item, 'implements') and item.implements('plotData'): + self.dataItems.append(item) + #self.plotChanged() + + params = kargs.get('params', {}) + self.itemMeta[item] = params + #item.setMeta(params) + self.curves.append(item) + #self.addItem(c) + + if hasattr(item, 'setLogMode'): + item.setLogMode(self.ctrl.logXCheck.isChecked(), self.ctrl.logYCheck.isChecked()) + + if isinstance(item, PlotDataItem): + ## configure curve for this plot + (alpha, auto) = self.alphaState() + item.setAlpha(alpha, auto) + item.setFftMode(self.ctrl.fftCheck.isChecked()) + item.setDownsampling(self.downsampleMode()) + item.setPointMode(self.pointMode()) + + ## Hide older plots if needed + self.updateDecimation() + + ## Add to average if needed + self.updateParamList() + if self.ctrl.averageGroup.isChecked() and 'skipAverage' not in kargs: + self.addAvgCurve(item) + + #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #item.sigPlotChanged.connect(self.plotChanged) + #self.plotChanged() + name = kargs.get('name', getattr(item, 'opts', {}).get('name', None)) + if name is not None and hasattr(self, 'legend') and self.legend is not None: + self.legend.addItem(item, name=name) + + + def addDataItem(self, item, *args): + print("PlotItem.addDataItem is deprecated. Use addItem instead.") + self.addItem(item, *args) + + def addCurve(self, c, params=None): + print("PlotItem.addCurve is deprecated. Use addItem instead.") + self.addItem(c, params) + + def removeItem(self, item): + """ + Remove an item from the internal ViewBox. + """ + if not item in self.items: + return + self.items.remove(item) + if item in self.dataItems: + self.dataItems.remove(item) + + if item.scene() is not None: + self.vb.removeItem(item) + if item in self.curves: + self.curves.remove(item) + self.updateDecimation() + self.updateParamList() + #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #item.sigPlotChanged.connect(self.plotChanged) + + def clear(self): + """ + Remove all items from the ViewBox. + """ + for i in self.items[:]: + self.removeItem(i) + self.avgCurves = {} + + def clearPlots(self): + for i in self.curves[:]: + self.removeItem(i) + self.avgCurves = {} + + + def plot(self, *args, **kargs): + """ + Add and return a new plot. + See :func:`PlotDataItem.__init__ ` for data arguments + + Extra allowed arguments are: + clear - clear all plots before displaying new data + params - meta-parameters to associate with this data + """ + + + clear = kargs.get('clear', False) + params = kargs.get('params', None) + + if clear: + self.clear() + + item = PlotDataItem(*args, **kargs) + + if params is None: + params = {} + self.addItem(item, params=params) + + return item + + def addLegend(self, size=None, offset=(30, 30)): + """ + Create a new LegendItem and anchor it over the internal ViewBox. + Plots will be automatically displayed in the legend if they + are created with the 'name' argument. + """ + self.legend = LegendItem(size, offset) + self.legend.setParentItem(self.vb) + return self.legend + + def scatterPlot(self, *args, **kargs): + if 'pen' in kargs: + kargs['symbolPen'] = kargs['pen'] + kargs['pen'] = None + + if 'brush' in kargs: + kargs['symbolBrush'] = kargs['brush'] + del kargs['brush'] + + if 'size' in kargs: + kargs['symbolSize'] = kargs['size'] + del kargs['size'] + + return self.plot(*args, **kargs) + + def replot(self): + self.update() + + def updateParamList(self): + self.ctrl.avgParamList.clear() + ## Check to see that each parameter for each curve is present in the list + for c in self.curves: + for p in list(self.itemMeta.get(c, {}).keys()): + if type(p) is tuple: + p = '.'.join(p) + + ## If the parameter is not in the list, add it. + matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) + if len(matches) == 0: + i = QtGui.QListWidgetItem(p) + if p in self.paramList and self.paramList[p] is True: + i.setCheckState(QtCore.Qt.Checked) + else: + i.setCheckState(QtCore.Qt.Unchecked) + self.ctrl.avgParamList.addItem(i) + else: + i = matches[0] + + self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) + + + ## Qt's SVG-writing capabilities are pretty terrible. + def writeSvgCurves(self, fileName=None): + if fileName is None: + self.fileDialog = FileDialog() + if PlotItem.lastFileDir is not None: + self.fileDialog.setDirectory(PlotItem.lastFileDir) + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.writeSvg) + return + #if fileName is None: + #fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): + raise Exception("Not implemented yet..") + fileName = str(fileName) + PlotItem.lastFileDir = os.path.dirname(fileName) + + rect = self.vb.viewRect() + xRange = rect.left(), rect.right() + + svg = "" + fh = open(fileName, 'w') + + dx = max(rect.right(),0) - min(rect.left(),0) + ymn = min(rect.top(), rect.bottom()) + ymx = max(rect.top(), rect.bottom()) + dy = max(ymx,0) - min(ymn,0) + sx = 1. + sy = 1. + while dx*sx < 10: + sx *= 1000 + while dy*sy < 10: + sy *= 1000 + sy *= -1 + + #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) + fh.write('\n') + fh.write('\n' % (rect.left()*sx, rect.right()*sx)) + fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) + + + for item in self.curves: + if isinstance(item, PlotCurveItem): + color = fn.colorStr(item.pen.color()) + opacity = item.pen.color().alpha() / 255. + color = color[:6] + x, y = item.getData() + mask = (x > xRange[0]) * (x < xRange[1]) + mask[:-1] += mask[1:] + m2 = mask.copy() + mask[1:] += m2[:-1] + x = x[mask] + y = y[mask] + + x *= sx + y *= sy + + #fh.write('\n' % color) + fh.write('') + #fh.write("") + for item in self.dataItems: + if isinstance(item, ScatterPlotItem): + + pRect = item.boundingRect() + vRect = pRect.intersected(rect) + + for point in item.points(): + pos = point.pos() + if not rect.contains(pos): + continue + color = fn.colorStr(point.brush.color()) + opacity = point.brush.color().alpha() / 255. + color = color[:6] + x = pos.x() * sx + y = pos.y() * sy + + fh.write('\n' % (x, y, color, opacity)) + #fh.write('') + + ## get list of curves, scatter plots + + + fh.write("\n") + + + + def writeSvg(self, fileName=None): + if fileName is None: + fileName = QtGui.QFileDialog.getSaveFileName() + fileName = str(fileName) + PlotItem.lastFileDir = os.path.dirname(fileName) + + self.svg = QtSvg.QSvgGenerator() + self.svg.setFileName(fileName) + res = 120. + view = self.scene().views()[0] + bounds = view.viewport().rect() + bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) + + self.svg.setResolution(res) + self.svg.setViewBox(bounds) + + self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) + + painter = QtGui.QPainter(self.svg) + view.render(painter, bounds) + + painter.end() + + ## Workaround to set pen widths correctly + import re + data = open(fileName).readlines() + for i in range(len(data)): + line = data[i] + m = re.match(r'(= split: + curves[i].show() + else: + if self.ctrl.forgetTracesCheck.isChecked(): + curves[i].clear() + self.removeItem(curves[i]) + else: + curves[i].hide() + + + def updateAlpha(self, *args): + (alpha, auto) = self.alphaState() + for c in self.curves: + c.setAlpha(alpha**2, auto) + + def alphaState(self): + enabled = self.ctrl.alphaGroup.isChecked() + auto = self.ctrl.autoAlphaCheck.isChecked() + alpha = float(self.ctrl.alphaSlider.value()) / self.ctrl.alphaSlider.maximum() + if auto: + alpha = 1.0 ## should be 1/number of overlapping plots + if not enabled: + auto = False + alpha = 1.0 + return (alpha, auto) + + def pointMode(self): + if self.ctrl.pointsGroup.isChecked(): + if self.ctrl.autoPointsCheck.isChecked(): + mode = None + else: + mode = True + else: + mode = False + return mode + + + def resizeEvent(self, ev): + if self.autoBtn is None: ## already closed down + return + btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect()) + y = self.size().height() - btnRect.height() + self.autoBtn.setPos(0, y) + + + def getMenu(self): + return self.ctrlMenu + + def getContextMenus(self, event): + ## called when another item is displaying its context menu; we get to add extras to the end of the menu. + if self.menuEnabled(): + return self.ctrlMenu + else: + return None + + def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'): + """ + Enable or disable the context menu for this PlotItem. + By default, the ViewBox's context menu will also be affected. + (use enableViewBoxMenu=None to leave the ViewBox unchanged) + """ + self._menuEnabled = enableMenu + if enableViewBoxMenu is None: + return + if enableViewBoxMenu is 'same': + enableViewBoxMenu = enableMenu + self.vb.setMenuEnabled(enableViewBoxMenu) + + def menuEnabled(self): + return self._menuEnabled + + def hoverEvent(self, ev): + if ev.enter: + self.mouseHovering = True + if ev.exit: + self.mouseHovering = False + + self.updateButtons() + + + def getLabel(self, key): + pass + + def _checkScaleKey(self, key): + if key not in self.axes: + raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys())))) + + def getScale(self, key): + return self.getAxis(key) + + def getAxis(self, name): + """Return the specified AxisItem. + *name* should be 'left', 'bottom', 'top', or 'right'.""" + self._checkScaleKey(name) + return self.axes[name]['item'] + + def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): + """ + Set the label for an axis. Basic HTML formatting is allowed. + + ============= ================================================================= + **Arguments** + axis must be one of 'left', 'bottom', 'right', or 'top' + text text to display along the axis. HTML allowed. + units units to display after the title. If units are given, + then an SI prefix will be automatically appended + and the axis values will be scaled accordingly. + (ie, use 'V' instead of 'mV'; 'm' will be added automatically) + ============= ================================================================= + """ + self.getAxis(axis).setLabel(text=text, units=units, **args) + + def showLabel(self, axis, show=True): + """ + Show or hide one of the plot's axis labels (the axis itself will be unaffected). + axis must be one of 'left', 'bottom', 'right', or 'top' + """ + self.getScale(axis).showLabel(show) + + def setTitle(self, title=None, **args): + """ + Set the title of the plot. Basic HTML formatting is allowed. + If title is None, then the title will be hidden. + """ + if title is None: + self.titleLabel.setVisible(False) + self.layout.setRowFixedHeight(0, 0) + self.titleLabel.setMaximumHeight(0) + else: + self.titleLabel.setMaximumHeight(30) + self.layout.setRowFixedHeight(0, 30) + self.titleLabel.setVisible(True) + self.titleLabel.setText(title, **args) + + def showAxis(self, axis, show=True): + """ + 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'] + if show: + s.show() + else: + s.hide() + + def hideAxis(self, axis): + """Hide one of the PlotItem's axes. ('left', 'bottom', 'right', or 'top')""" + self.showAxis(axis, False) + + def showScale(self, *args, **kargs): + print("Deprecated. use showAxis() instead") + return self.showAxis(*args, **kargs) + + def hideButtons(self): + """Causes auto-scale button ('A' in lower-left corner) to be hidden for this PlotItem""" + #self.ctrlBtn.hide() + self.buttonsHidden = True + self.updateButtons() + + def showButtons(self): + """Causes auto-scale button ('A' in lower-left corner) to be visible for this PlotItem""" + #self.ctrlBtn.hide() + self.buttonsHidden = False + self.updateButtons() + + def updateButtons(self): + if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()): + self.autoBtn.show() + else: + self.autoBtn.hide() + + def _plotArray(self, arr, x=None, **kargs): + if arr.ndim != 1: + raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) + if x is None: + x = np.arange(arr.shape[0]) + if x.ndim != 1: + raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) + c = PlotCurveItem(arr, x=x, **kargs) + return c + + + + def _plotMetaArray(self, arr, x=None, autoLabel=True, **kargs): + inf = arr.infoCopy() + if arr.ndim != 1: + raise Exception('can only automatically plot 1 dimensional arrays.') + ## create curve + try: + xv = arr.xvals(0) + except: + if x is None: + xv = np.arange(arr.shape[0]) + else: + xv = x + c = PlotCurveItem(**kargs) + c.setData(x=xv, y=arr.view(np.ndarray)) + + if autoLabel: + name = arr._info[0].get('name', None) + units = arr._info[0].get('units', None) + self.setLabel('bottom', text=name, units=units) + + name = arr._info[1].get('name', None) + units = arr._info[1].get('units', None) + self.setLabel('left', text=name, units=units) + + return c + + + def setExportMode(self, export, opts=None): + GraphicsWidget.setExportMode(self, export, opts) + self.updateButtons() + #if export: + #self.autoBtn.hide() + #else: + #self.autoBtn.show() + diff --git a/pyqtgraph/graphicsItems/PlotItem/__init__.py b/pyqtgraph/graphicsItems/PlotItem/__init__.py new file mode 100644 index 00000000..d797978c --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/__init__.py @@ -0,0 +1 @@ +from .PlotItem import PlotItem diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui new file mode 100644 index 00000000..516ec721 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui @@ -0,0 +1,284 @@ + + + Form + + + + 0 + 0 + 258 + 605 + + + + Form + + + + + 10 + 200 + 242 + 182 + + + + Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available). + + + Average + + + true + + + false + + + + 0 + + + 0 + + + + + + + + + + 0 + 70 + 242 + 160 + + + + Downsample + + + true + + + + 0 + + + 0 + + + + + Manual + + + true + + + + + + + 1 + + + 100000 + + + 1 + + + + + + + Auto + + + false + + + + + + + If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. + + + Max Traces: + + + + + + + If multiple curves are displayed in this plot, check "Max Traces" and set this value to limit the number of traces that are displayed. + + + + + + + If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden). + + + Forget hidden traces + + + + + + + + + 0 + 0 + 154 + 79 + + + + + + + Power Spectrum (FFT) + + + + + + + Log X + + + + + + + Log Y + + + + + + + + + 10 + 550 + 234 + 58 + + + + Points + + + true + + + + + + Auto + + + true + + + + + + + + + 10 + 460 + 221 + 81 + + + + + + + Show X Grid + + + + + + + Show Y Grid + + + + + + + 255 + + + 128 + + + Qt::Horizontal + + + + + + + Opacity + + + + + + + + + 10 + 390 + 234 + 60 + + + + Alpha + + + true + + + + + + Auto + + + false + + + + + + + 1000 + + + 1000 + + + Qt::Horizontal + + + + + + + + + diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py new file mode 100644 index 00000000..d34cd297 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# +# Created: Sun Sep 9 14:41:32 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(258, 605) + self.averageGroup = QtGui.QGroupBox(Form) + self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setCheckable(True) + self.averageGroup.setChecked(False) + self.averageGroup.setObjectName(_fromUtf8("averageGroup")) + self.gridLayout_5 = QtGui.QGridLayout(self.averageGroup) + self.gridLayout_5.setMargin(0) + self.gridLayout_5.setSpacing(0) + self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) + self.avgParamList = QtGui.QListWidget(self.averageGroup) + self.avgParamList.setObjectName(_fromUtf8("avgParamList")) + self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) + self.decimateGroup = QtGui.QGroupBox(Form) + self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) + self.decimateGroup.setCheckable(True) + self.decimateGroup.setObjectName(_fromUtf8("decimateGroup")) + self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) + self.gridLayout_4.setMargin(0) + self.gridLayout_4.setSpacing(0) + self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) + self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.manualDecimateRadio.setChecked(True) + self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio")) + self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) + self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) + self.downsampleSpin.setMinimum(1) + self.downsampleSpin.setMaximum(100000) + self.downsampleSpin.setProperty("value", 1) + self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) + self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) + self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.autoDecimateRadio.setChecked(False) + self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio")) + self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) + self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) + self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) + self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) + self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) + self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.transformGroup = QtGui.QFrame(Form) + self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) + self.transformGroup.setObjectName(_fromUtf8("transformGroup")) + self.gridLayout = QtGui.QGridLayout(self.transformGroup) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.fftCheck = QtGui.QCheckBox(self.transformGroup) + self.fftCheck.setObjectName(_fromUtf8("fftCheck")) + self.gridLayout.addWidget(self.fftCheck, 0, 0, 1, 1) + self.logXCheck = QtGui.QCheckBox(self.transformGroup) + self.logXCheck.setObjectName(_fromUtf8("logXCheck")) + self.gridLayout.addWidget(self.logXCheck, 1, 0, 1, 1) + self.logYCheck = QtGui.QCheckBox(self.transformGroup) + self.logYCheck.setObjectName(_fromUtf8("logYCheck")) + self.gridLayout.addWidget(self.logYCheck, 2, 0, 1, 1) + self.pointsGroup = QtGui.QGroupBox(Form) + self.pointsGroup.setGeometry(QtCore.QRect(10, 550, 234, 58)) + self.pointsGroup.setCheckable(True) + self.pointsGroup.setObjectName(_fromUtf8("pointsGroup")) + self.verticalLayout_5 = QtGui.QVBoxLayout(self.pointsGroup) + self.verticalLayout_5.setObjectName(_fromUtf8("verticalLayout_5")) + self.autoPointsCheck = QtGui.QCheckBox(self.pointsGroup) + self.autoPointsCheck.setChecked(True) + self.autoPointsCheck.setObjectName(_fromUtf8("autoPointsCheck")) + self.verticalLayout_5.addWidget(self.autoPointsCheck) + self.gridGroup = QtGui.QFrame(Form) + self.gridGroup.setGeometry(QtCore.QRect(10, 460, 221, 81)) + self.gridGroup.setObjectName(_fromUtf8("gridGroup")) + self.gridLayout_2 = QtGui.QGridLayout(self.gridGroup) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.xGridCheck = QtGui.QCheckBox(self.gridGroup) + self.xGridCheck.setObjectName(_fromUtf8("xGridCheck")) + self.gridLayout_2.addWidget(self.xGridCheck, 0, 0, 1, 2) + self.yGridCheck = QtGui.QCheckBox(self.gridGroup) + self.yGridCheck.setObjectName(_fromUtf8("yGridCheck")) + self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2) + self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) + self.gridAlphaSlider.setMaximum(255) + self.gridAlphaSlider.setProperty("value", 128) + self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider")) + self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1) + self.label = QtGui.QLabel(self.gridGroup) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.alphaGroup = QtGui.QGroupBox(Form) + self.alphaGroup.setGeometry(QtCore.QRect(10, 390, 234, 60)) + self.alphaGroup.setCheckable(True) + self.alphaGroup.setObjectName(_fromUtf8("alphaGroup")) + self.horizontalLayout = QtGui.QHBoxLayout(self.alphaGroup) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.autoAlphaCheck = QtGui.QCheckBox(self.alphaGroup) + self.autoAlphaCheck.setChecked(False) + self.autoAlphaCheck.setObjectName(_fromUtf8("autoAlphaCheck")) + self.horizontalLayout.addWidget(self.autoAlphaCheck) + self.alphaSlider = QtGui.QSlider(self.alphaGroup) + self.alphaSlider.setMaximum(1000) + self.alphaSlider.setProperty("value", 1000) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setObjectName(_fromUtf8("alphaSlider")) + self.horizontalLayout.addWidget(self.alphaSlider) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) + self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) + self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) + self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) + self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) + self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.xGridCheck.setText(QtGui.QApplication.translate("Form", "Show X Grid", None, QtGui.QApplication.UnicodeUTF8)) + self.yGridCheck.setText(QtGui.QApplication.translate("Form", "Show Y Grid", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Opacity", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py new file mode 100644 index 00000000..85b563a7 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui' +# +# Created: Sun Sep 9 14:41:32 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(258, 605) + self.averageGroup = QtGui.QGroupBox(Form) + self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182)) + self.averageGroup.setCheckable(True) + self.averageGroup.setChecked(False) + self.averageGroup.setObjectName("averageGroup") + self.gridLayout_5 = QtGui.QGridLayout(self.averageGroup) + self.gridLayout_5.setContentsMargins(0, 0, 0, 0) + self.gridLayout_5.setSpacing(0) + self.gridLayout_5.setObjectName("gridLayout_5") + self.avgParamList = QtGui.QListWidget(self.averageGroup) + self.avgParamList.setObjectName("avgParamList") + self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) + self.decimateGroup = QtGui.QGroupBox(Form) + self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160)) + self.decimateGroup.setCheckable(True) + self.decimateGroup.setObjectName("decimateGroup") + self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) + self.gridLayout_4.setContentsMargins(0, 0, 0, 0) + self.gridLayout_4.setSpacing(0) + self.gridLayout_4.setObjectName("gridLayout_4") + self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.manualDecimateRadio.setChecked(True) + self.manualDecimateRadio.setObjectName("manualDecimateRadio") + self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) + self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) + self.downsampleSpin.setMinimum(1) + self.downsampleSpin.setMaximum(100000) + self.downsampleSpin.setProperty("value", 1) + self.downsampleSpin.setObjectName("downsampleSpin") + self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) + self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) + self.autoDecimateRadio.setChecked(False) + self.autoDecimateRadio.setObjectName("autoDecimateRadio") + self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) + self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName("maxTracesCheck") + self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) + self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName("maxTracesSpin") + self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) + self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName("forgetTracesCheck") + self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) + self.transformGroup = QtGui.QFrame(Form) + self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) + self.transformGroup.setObjectName("transformGroup") + self.gridLayout = QtGui.QGridLayout(self.transformGroup) + self.gridLayout.setObjectName("gridLayout") + self.fftCheck = QtGui.QCheckBox(self.transformGroup) + self.fftCheck.setObjectName("fftCheck") + self.gridLayout.addWidget(self.fftCheck, 0, 0, 1, 1) + self.logXCheck = QtGui.QCheckBox(self.transformGroup) + self.logXCheck.setObjectName("logXCheck") + self.gridLayout.addWidget(self.logXCheck, 1, 0, 1, 1) + self.logYCheck = QtGui.QCheckBox(self.transformGroup) + self.logYCheck.setObjectName("logYCheck") + self.gridLayout.addWidget(self.logYCheck, 2, 0, 1, 1) + self.pointsGroup = QtGui.QGroupBox(Form) + self.pointsGroup.setGeometry(QtCore.QRect(10, 550, 234, 58)) + self.pointsGroup.setCheckable(True) + self.pointsGroup.setObjectName("pointsGroup") + self.verticalLayout_5 = QtGui.QVBoxLayout(self.pointsGroup) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.autoPointsCheck = QtGui.QCheckBox(self.pointsGroup) + self.autoPointsCheck.setChecked(True) + self.autoPointsCheck.setObjectName("autoPointsCheck") + self.verticalLayout_5.addWidget(self.autoPointsCheck) + self.gridGroup = QtGui.QFrame(Form) + self.gridGroup.setGeometry(QtCore.QRect(10, 460, 221, 81)) + self.gridGroup.setObjectName("gridGroup") + self.gridLayout_2 = QtGui.QGridLayout(self.gridGroup) + self.gridLayout_2.setObjectName("gridLayout_2") + self.xGridCheck = QtGui.QCheckBox(self.gridGroup) + self.xGridCheck.setObjectName("xGridCheck") + self.gridLayout_2.addWidget(self.xGridCheck, 0, 0, 1, 2) + self.yGridCheck = QtGui.QCheckBox(self.gridGroup) + self.yGridCheck.setObjectName("yGridCheck") + self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2) + self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) + self.gridAlphaSlider.setMaximum(255) + self.gridAlphaSlider.setProperty("value", 128) + self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.gridAlphaSlider.setObjectName("gridAlphaSlider") + self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1) + self.label = QtGui.QLabel(self.gridGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.alphaGroup = QtGui.QGroupBox(Form) + self.alphaGroup.setGeometry(QtCore.QRect(10, 390, 234, 60)) + self.alphaGroup.setCheckable(True) + self.alphaGroup.setObjectName("alphaGroup") + self.horizontalLayout = QtGui.QHBoxLayout(self.alphaGroup) + self.horizontalLayout.setObjectName("horizontalLayout") + self.autoAlphaCheck = QtGui.QCheckBox(self.alphaGroup) + self.autoAlphaCheck.setChecked(False) + self.autoAlphaCheck.setObjectName("autoAlphaCheck") + self.horizontalLayout.addWidget(self.autoAlphaCheck) + self.alphaSlider = QtGui.QSlider(self.alphaGroup) + self.alphaSlider.setMaximum(1000) + self.alphaSlider.setProperty("value", 1000) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setObjectName("alphaSlider") + self.horizontalLayout.addWidget(self.alphaSlider) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) + self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) + self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) + self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) + self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) + self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8)) + self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8)) + self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8)) + self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.xGridCheck.setText(QtGui.QApplication.translate("Form", "Show X Grid", None, QtGui.QApplication.UnicodeUTF8)) + self.yGridCheck.setText(QtGui.QApplication.translate("Form", "Show Y Grid", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Opacity", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py new file mode 100644 index 00000000..e3f094ff --- /dev/null +++ b/pyqtgraph/graphicsItems/ROI.py @@ -0,0 +1,1893 @@ +# -*- coding: utf-8 -*- +""" +ROI.py - Interactive graphics items for GraphicsView (ROI widgets) +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Implements a series of graphics items which display movable/scalable/rotatable shapes +for use as region-of-interest markers. ROI class automatically handles extraction +of array data from ImageItems. + +The ROI class is meant to serve as the base for more specific types; see several examples +of how to build an ROI at the bottom of the file. +""" + +from pyqtgraph.Qt import QtCore, QtGui +#if not hasattr(QtCore, 'Signal'): + #QtCore.Signal = QtCore.pyqtSignal +import numpy as np +from numpy.linalg import norm +import scipy.ndimage as ndimage +from pyqtgraph.Point import * +from pyqtgraph.SRTTransform import SRTTransform +from math import cos, sin +import pyqtgraph.functions as fn +from .GraphicsObject import GraphicsObject +from .UIGraphicsItem import UIGraphicsItem + +__all__ = [ + 'ROI', + 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', +] + + +def rectStr(r): + return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) + +class ROI(GraphicsObject): + """Generic region-of-interest widget. + Can be used for implementing many types of selection box with rotate/translate/scale handles. + """ + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChangeStarted = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + sigHoverEvent = QtCore.Signal(object) + sigClicked = QtCore.Signal(object, object) + sigRemoveRequested = QtCore.Signal(object) + + def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): + #QObjectWorkaround.__init__(self) + GraphicsObject.__init__(self, parent) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + pos = Point(pos) + size = Point(size) + self.aspectLocked = False + self.translatable = movable + self.rotateAllowed = True + self.removable = removable + self.menu = None + + self.freeHandleMoved = False ## keep track of whether free handles have moved since last change signal was emitted. + self.mouseHovering = False + if pen is None: + pen = (255, 255, 255) + self.setPen(pen) + + self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) + self.handles = [] + self.state = {'pos': Point(0,0), 'size': Point(1,1), 'angle': 0} ## angle is in degrees for ease of Qt integration + self.lastState = None + self.setPos(pos) + self.setAngle(angle) + self.setSize(size) + self.setZValue(10) + self.isMoving = False + + self.handleSize = 5 + self.invertible = invertible + self.maxBounds = maxBounds + + self.snapSize = snapSize + self.translateSnap = translateSnap + self.rotateSnap = rotateSnap + self.scaleSnap = scaleSnap + #self.setFlag(self.ItemIsSelectable, True) + + def getState(self): + return self.stateCopy() + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + return sc + + def saveState(self): + """Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)""" + state = {} + state['pos'] = tuple(self.state['pos']) + state['size'] = tuple(self.state['size']) + state['angle'] = self.state['angle'] + return state + + def setState(self, state, update=True): + self.setPos(state['pos'], update=False) + self.setSize(state['size'], update=False) + self.setAngle(state['angle'], update=update) + + def setZValue(self, z): + QtGui.QGraphicsItem.setZValue(self, z) + for h in self.handles: + h['item'].setZValue(z+1) + + def parentBounds(self): + return self.mapToParent(self.boundingRect()).boundingRect() + + def setPen(self, pen): + self.pen = fn.mkPen(pen) + self.currentPen = self.pen + self.update() + + def size(self): + return self.getState()['size'] + + def pos(self): + return self.getState()['pos'] + + def angle(self): + return self.getState()['angle'] + + def setPos(self, pos, update=True, finish=True): + """Set the position of the ROI (in the parent's coordinate system). + By default, this will cause both sigRegionChanged and sigRegionChangeFinished to be emitted. + + If finish is False, then sigRegionChangeFinished will not be emitted. You can then use + stateChangeFinished() to cause the signal to be emitted after a series of state changes. + + If update is False, the state change will be remembered but not processed and no signals + will be emitted. You can then use stateChanged() to complete the state change. This allows + multiple change functions to be called sequentially while minimizing processing overhead + and repeated signals. Setting update=False also forces finish=False. + """ + + pos = Point(pos) + self.state['pos'] = pos + QtGui.QGraphicsItem.setPos(self, pos) + if update: + self.stateChanged(finish=finish) + + def setSize(self, size, update=True, finish=True): + """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. + See setPos() for an explanation of the update and finish arguments. + """ + size = Point(size) + self.prepareGeometryChange() + self.state['size'] = size + if update: + self.stateChanged(finish=finish) + + def setAngle(self, angle, update=True, finish=True): + """Set the angle of rotation (in degrees) for this ROI. + See setPos() for an explanation of the update and finish arguments. + """ + self.state['angle'] = angle + tr = QtGui.QTransform() + #tr.rotate(-angle * 180 / np.pi) + tr.rotate(angle) + self.setTransform(tr) + if update: + self.stateChanged(finish=finish) + + def scale(self, s, center=[0,0], update=True, finish=True): + """ + Resize the ROI by scaling relative to *center*. + See setPos() for an explanation of the *update* and *finish* arguments. + """ + c = self.mapToParent(Point(center) * self.state['size']) + self.prepareGeometryChange() + newSize = self.state['size'] * s + c1 = self.mapToParent(Point(center) * newSize) + newPos = self.state['pos'] + c - c1 + + self.setSize(newSize, update=False) + self.setPos(newPos, update=update, finish=finish) + + + def translate(self, *args, **kargs): + """ + Move the ROI to a new position. + Accepts either (x, y, snap) or ([x,y], snap) as arguments + If the ROI is bounded and the move would exceed boundaries, then the ROI + is moved to the nearest acceptable position instead. + + snap can be: + None (default): use self.translateSnap and self.snapSize to determine whether/how to snap + False: do not snap + Point(w,h) snap to rectangular grid with spacing (w,h) + True: snap using self.snapSize (and ignoring self.translateSnap) + + Also accepts *update* and *finish* arguments (see setPos() for a description of these). + """ + + if len(args) == 1: + pt = args[0] + else: + pt = args + + newState = self.stateCopy() + newState['pos'] = newState['pos'] + pt + + ## snap position + #snap = kargs.get('snap', None) + #if (snap is not False) and not (snap is None and self.translateSnap is False): + + snap = kargs.get('snap', None) + if snap is None: + snap = self.translateSnap + if snap is not False: + newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) + + #d = ev.scenePos() - self.mapToScene(self.pressPos) + if self.maxBounds is not None: + r = self.stateRect(newState) + #r0 = self.sceneTransform().mapRect(self.boundingRect()) + d = Point(0,0) + if self.maxBounds.left() > r.left(): + d[0] = self.maxBounds.left() - r.left() + elif self.maxBounds.right() < r.right(): + d[0] = self.maxBounds.right() - r.right() + if self.maxBounds.top() > r.top(): + d[1] = self.maxBounds.top() - r.top() + elif self.maxBounds.bottom() < r.bottom(): + d[1] = self.maxBounds.bottom() - r.bottom() + newState['pos'] += d + + #self.state['pos'] = newState['pos'] + update = kargs.get('update', True) + finish = kargs.get('finish', True) + self.setPos(newState['pos'], update=update, finish=finish) + #if 'update' not in kargs or kargs['update'] is True: + #self.stateChanged() + + def rotate(self, angle, update=True, finish=True): + self.setAngle(self.angle()+angle, update=update, finish=finish) + + def handleMoveStarted(self): + self.preMoveState = self.getState() + + def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): + pos = Point(pos) + return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index) + + def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None): + if pos is not None: + pos = Point(pos) + return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index) + + def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None): + pos = Point(pos) + center = Point(center) + info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} + if pos.x() == center.x(): + info['xoff'] = True + if pos.y() == center.y(): + info['yoff'] = True + return self.addHandle(info, index=index) + + def addRotateHandle(self, pos, center, item=None, name=None, index=None): + pos = Point(pos) + center = Point(center) + return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index) + + def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None): + pos = Point(pos) + center = Point(center) + if pos[0] != center[0] and pos[1] != center[1]: + raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") + return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index) + + def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None): + pos = Point(pos) + center = Point(center) + return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index) + + def addHandle(self, info, index=None): + ## If a Handle was not supplied, create it now + if 'item' not in info or info['item'] is None: + h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) + h.setPos(info['pos'] * self.state['size']) + info['item'] = h + else: + h = info['item'] + if info['pos'] is None: + info['pos'] = h.pos() + + ## connect the handle to this ROI + #iid = len(self.handles) + h.connectROI(self) + if index is None: + self.handles.append(info) + else: + self.handles.insert(index, info) + + h.setZValue(self.zValue()+1) + self.stateChanged() + return h + + def indexOfHandle(self, handle): + if isinstance(handle, Handle): + index = [i for i, info in enumerate(self.handles) if info['item'] is handle] + if len(index) == 0: + raise Exception("Cannot remove handle; it is not attached to this ROI") + return index[0] + else: + return handle + + def removeHandle(self, handle): + """Remove a handle from this ROI. Argument may be either a Handle instance or the integer index of the handle.""" + index = self.indexOfHandle(handle) + + handle = self.handles[index]['item'] + self.handles.pop(index) + handle.disconnectROI(self) + if len(handle.rois) == 0: + self.scene().removeItem(handle) + self.stateChanged() + + def replaceHandle(self, oldHandle, newHandle): + """Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together. + *oldHandle* may be a Handle instance or the index of a handle.""" + #print "=========================" + #print "replace", oldHandle, newHandle + #print self + #print self.handles + #print "-----------------" + index = self.indexOfHandle(oldHandle) + info = self.handles[index] + self.removeHandle(index) + info['item'] = newHandle + info['pos'] = newHandle.pos() + self.addHandle(info, index=index) + #print self.handles + + def checkRemoveHandle(self, handle): + ## This is used when displaying a Handle's context menu to determine + ## whether removing is allowed. + ## Subclasses may wish to override this to disable the menu entry. + ## Note: by default, handles are not user-removable even if this method returns True. + return True + + + def getLocalHandlePositions(self, index=None): + """Returns the position of a handle in ROI coordinates""" + if index == None: + positions = [] + for h in self.handles: + positions.append((h['name'], h['pos'])) + return positions + else: + return (self.handles[index]['name'], self.handles[index]['pos']) + + def getSceneHandlePositions(self, index=None): + if index == None: + positions = [] + for h in self.handles: + positions.append((h['name'], h['item'].scenePos())) + return positions + else: + return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) + + def getHandles(self): + return [h['item'] for h in self.handles] + + def mapSceneToParent(self, pt): + return self.mapToParent(self.mapFromScene(pt)) + + def setSelected(self, s): + QtGui.QGraphicsItem.setSelected(self, s) + #print "select", self, s + if s: + for h in self.handles: + h['item'].show() + else: + for h in self.handles: + h['item'].hide() + + + def hoverEvent(self, ev): + hover = False + if not ev.isExit(): + if self.translatable and ev.acceptDrags(QtCore.Qt.LeftButton): + hover=True + + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.RightButton, QtCore.Qt.MidButton]: + if int(self.acceptedMouseButtons() & btn) > 0 and ev.acceptClicks(btn): + hover=True + if self.contextMenuEnabled(): + ev.acceptClicks(QtCore.Qt.RightButton) + + if hover: + self.setMouseHover(True) + self.sigHoverEvent.emit(self) + ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion. + ev.acceptClicks(QtCore.Qt.RightButton) + ev.acceptClicks(QtCore.Qt.MidButton) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the ROI that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: + self.currentPen = fn.mkPen(255, 255, 0) + else: + self.currentPen = self.pen + self.update() + + def contextMenuEnabled(self): + return self.removable + + def raiseContextMenu(self, ev): + if not self.contextMenuEnabled(): + return + menu = self.getMenu() + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def getMenu(self): + if self.menu is None: + self.menu = QtGui.QMenu() + self.menu.setTitle("ROI") + remAct = QtGui.QAction("Remove ROI", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + return self.menu + + def removeClicked(self): + ## Send remove event only after we have exited the menu event handler + self.removeTimer = QtCore.QTimer() + self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) + self.removeTimer.start(0) + + + + def mouseDragEvent(self, ev): + if ev.isStart(): + #p = ev.pos() + #if not self.isMoving and not self.shape().contains(p): + #ev.ignore() + #return + if ev.button() == QtCore.Qt.LeftButton: + self.setSelected(True) + if self.translatable: + self.isMoving = True + self.preMoveState = self.getState() + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.sigRegionChangeStarted.emit(self) + ev.accept() + else: + ev.ignore() + + elif ev.isFinish(): + if self.translatable: + if self.isMoving: + self.stateChangeFinished() + self.isMoving = False + return + + if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: + snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None + newPos = self.mapToParent(ev.pos()) + self.cursorOffset + self.translate(newPos - self.pos(), snap=snap, finish=False) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton and self.isMoving: + ev.accept() + self.cancelMove() + if ev.button() == QtCore.Qt.RightButton and self.contextMenuEnabled(): + self.raiseContextMenu(ev) + ev.accept() + elif int(ev.button() & self.acceptedMouseButtons()) > 0: + ev.accept() + self.sigClicked.emit(self, ev) + else: + ev.ignore() + + + + + def cancelMove(self): + self.isMoving = False + self.setState(self.preMoveState) + + + #def pointDragEvent(self, pt, ev): + ### just for handling drag start/stop. + ### drag moves are handled through movePoint() + + #if ev.isStart(): + #self.isMoving = True + #self.preMoveState = self.getState() + + #self.sigRegionChangeStarted.emit(self) + #elif ev.isFinish(): + #self.isMoving = False + #self.sigRegionChangeFinished.emit(self) + #return + + + #def pointPressEvent(self, pt, ev): + ##print "press" + #self.isMoving = True + #self.preMoveState = self.getState() + + ##self.emit(QtCore.SIGNAL('regionChangeStarted'), self) + #self.sigRegionChangeStarted.emit(self) + ##self.pressPos = self.mapFromScene(ev.scenePos()) + ##self.pressHandlePos = self.handles[pt]['item'].pos() + + #def pointReleaseEvent(self, pt, ev): + ##print "release" + #self.isMoving = False + ##self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + #self.sigRegionChangeFinished.emit(self) + + #def pointMoveEvent(self, pt, ev): + #self.movePoint(pt, ev.scenePos(), ev.modifiers()) + + + def checkPointMove(self, handle, pos, modifiers): + """When handles move, they must ask the ROI if the move is acceptable. + By default, this always returns True. Subclasses may wish override. + """ + return True + + + def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'): + ## called by Handles when they are moved. + ## pos is the new position of the handle in scene coords, as requested by the handle. + + newState = self.stateCopy() + index = self.indexOfHandle(handle) + h = self.handles[index] + p0 = self.mapToParent(h['pos'] * self.state['size']) + p1 = Point(pos) + + if coords == 'parent': + pass + elif coords == 'scene': + p1 = self.mapSceneToParent(p1) + else: + raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") + + + ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. + #p0 = self.mapSceneToParent(p0) + #p1 = self.mapSceneToParent(p1) + + ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) + if 'center' in h: + c = h['center'] + cs = c * self.state['size'] + lp0 = self.mapFromParent(p0) - cs + lp1 = self.mapFromParent(p1) - cs + + if h['type'] == 't': + snap = True if (modifiers & QtCore.Qt.ControlModifier) else None + #if self.translateSnap or (): + #snap = Point(self.snapSize, self.snapSize) + self.translate(p1-p0, snap=snap, update=False) + + elif h['type'] == 'f': + newPos = self.mapFromParent(p1) + h['item'].setPos(newPos) + h['pos'] = newPos + self.freeHandleMoved = True + #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() + + elif h['type'] == 's': + ## If a handle and its center have the same x or y value, we can't scale across that axis. + if h['center'][0] == h['pos'][0]: + lp1[0] = 0 + if h['center'][1] == h['pos'][1]: + lp1[1] = 0 + + ## snap + if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize + lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize + + ## preserve aspect ratio (this can override snapping) + if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier): + #arv = Point(self.preMoveState['size']) - + lp1 = lp1.proj(lp0) + + ## determine scale factors and new size of ROI + hs = h['pos'] - c + if hs[0] == 0: + hs[0] = 1 + if hs[1] == 0: + hs[1] = 1 + newSize = lp1 / hs + + ## Perform some corrections and limit checks + if newSize[0] == 0: + newSize[0] = newState['size'][0] + if newSize[1] == 0: + newSize[1] = newState['size'][1] + if not self.invertible: + if newSize[0] < 0: + newSize[0] = newState['size'][0] + if newSize[1] < 0: + newSize[1] = newState['size'][1] + if self.aspectLocked: + newSize[0] = newSize[1] + + ## Move ROI so the center point occupies the same scene location after the scale + s0 = c * self.state['size'] + s1 = c * newSize + cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) + + ## update state, do more boundary checks + newState['size'] = newSize + newState['pos'] = newState['pos'] + cc + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + + self.setPos(newState['pos'], update=False) + self.setSize(newState['size'], update=False) + + elif h['type'] in ['r', 'rf']: + if h['type'] == 'rf': + self.freeHandleMoved = True + + if not self.rotateAllowed: + return + ## If the handle is directly over its center point, we can't compute an angle. + if lp1.length() == 0 or lp0.length() == 0: + return + + ## determine new rotation angle, constrained if necessary + ang = newState['angle'] - lp0.angle(lp1) + if ang is None: ## this should never happen.. + return + if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): + ang = round(ang / 15.) * 15. ## 180/12 = 15 + + ## create rotation transform + tr = QtGui.QTransform() + tr.rotate(ang) + + ## move ROI so that center point remains stationary after rotate + cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) + newState['angle'] = ang + newState['pos'] = newState['pos'] + cc + + ## check boundaries, update + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + #self.setTransform(tr) + self.setPos(newState['pos'], update=False) + self.setAngle(ang, update=False) + #self.state = newState + + ## If this is a free-rotate handle, its distance from the center may change. + + if h['type'] == 'rf': + h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle + + elif h['type'] == 'sr': + if h['center'][0] == h['pos'][0]: + scaleAxis = 1 + else: + scaleAxis = 0 + + if lp1.length() == 0 or lp0.length() == 0: + return + + ang = newState['angle'] - lp0.angle(lp1) + if ang is None: + return + if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): + #ang = round(ang / (np.pi/12.)) * (np.pi/12.) + ang = round(ang / 15.) * 15. + + hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) + newState['size'][scaleAxis] = lp1.length() / hs + #if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + if self.scaleSnap: ## use CTRL only for angular snap here. + newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize + if newState['size'][scaleAxis] == 0: + newState['size'][scaleAxis] = 1 + + c1 = c * newState['size'] + tr = QtGui.QTransform() + tr.rotate(ang) + + cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) + newState['angle'] = ang + newState['pos'] = newState['pos'] + cc + if self.maxBounds is not None: + r = self.stateRect(newState) + if not self.maxBounds.contains(r): + return + #self.setTransform(tr) + #self.setPos(newState['pos'], update=False) + #self.prepareGeometryChange() + #self.state = newState + self.setState(newState, update=False) + + self.stateChanged(finish=finish) + + def stateChanged(self, finish=True): + """Process changes to the state of the ROI. + If there are any changes, then the positions of handles are updated accordingly + and sigRegionChanged is emitted. If finish is True, then + sigRegionChangeFinished will also be emitted.""" + + changed = False + if self.lastState is None: + changed = True + else: + for k in list(self.state.keys()): + if self.state[k] != self.lastState[k]: + changed = True + + self.prepareGeometryChange() + if changed: + ## Move all handles to match the current configuration of the ROI + for h in self.handles: + if h['item'] in self.childItems(): + p = h['pos'] + h['item'].setPos(h['pos'] * self.state['size']) + #else: + # trans = self.state['pos']-self.lastState['pos'] + # h['item'].setPos(h['pos'] + h['item'].parentItem().mapFromParent(trans)) + + self.update() + self.sigRegionChanged.emit(self) + elif self.freeHandleMoved: + self.sigRegionChanged.emit(self) + + self.freeHandleMoved = False + self.lastState = self.stateCopy() + + if finish: + self.stateChangeFinished() + + def stateChangeFinished(self): + self.sigRegionChangeFinished.emit(self) + + def stateRect(self, state): + r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) + tr = QtGui.QTransform() + #tr.rotate(-state['angle'] * 180 / np.pi) + tr.rotate(-state['angle']) + r = tr.mapRect(r) + return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) + + + def getSnapPosition(self, pos, snap=None): + ## Given that pos has been requested, return the nearest snap-to position + ## optionally, snap may be passed in to specify a rectangular snap grid. + ## override this function for more interesting snap functionality.. + + if snap is None or snap is True: + if self.snapSize is None: + return pos + snap = Point(self.snapSize, self.snapSize) + + return Point( + round(pos[0] / snap[0]) * snap[0], + round(pos[1] / snap[1]) * snap[1] + ) + + + def boundingRect(self): + return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() + + def paint(self, p, opt, widget): + p.save() + r = self.boundingRect() + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + p.translate(r.left(), r.top()) + p.scale(r.width(), r.height()) + p.drawRect(0, 0, 1, 1) + p.restore() + + def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): + """Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. + Also returns the transform which maps the ROI into data coordinates. + + If returnSlice is set to False, the function returns a pair of tuples with the values that would have + been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" + #print "getArraySlice" + + ## Determine shape of array along ROI axes + dShape = (data.shape[axes[0]], data.shape[axes[1]]) + #print " dshape", dShape + + ## Determine transform that maps ROI bounding box to image coordinates + tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform()) + + ## Modify transform to scale from image coords to data coords + #m = QtGui.QTransform() + tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) + #tr = tr * m + + ## Transform ROI bounds into data bounds + dataBounds = tr.mapRect(self.boundingRect()) + #print " boundingRect:", self.boundingRect() + #print " dataBounds:", dataBounds + + ## Intersect transformed ROI bounds with data bounds + intBounds = dataBounds.intersect(QtCore.QRectF(0, 0, dShape[0], dShape[1])) + #print " intBounds:", intBounds + + ## Determine index values to use when referencing the array. + bounds = ( + (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), + (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) + ) + #print " bounds:", bounds + + if returnSlice: + ## Create slice objects + sl = [slice(None)] * data.ndim + sl[axes[0]] = slice(*bounds[0]) + sl[axes[1]] = slice(*bounds[1]) + return tuple(sl), tr + else: + return bounds, tr + + def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + """Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. + + This method uses :func:`affineSlice ` to generate + the slice from *data* and uses :func:`getAffineSliceParams ` to determine the parameters to + pass to :func:`affineSlice `. + + If *returnMappedCoords* is True, then the method returns a tuple (result, coords) + such that coords is the set of coordinates used to interpolate values from the original + data, mapped into the parent coordinate system of the image. This is useful, when slicing + data from images that have been transformed, for determining the location of each value + in the sliced data. + + All extra keyword arguments are passed to :func:`affineSlice `. + """ + + shape, vectors, origin = self.getAffineSliceParams(data, img, axes) + if not returnMappedCoords: + return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + else: + kwds['returnCoords'] = True + result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + #tr = fn.transformToArray(img.transform())[:2] ## remove perspective transform values + + ### separate translation from scale/rotate + #translate = tr[:,2] + #tr = tr[:,:2] + #tr = tr.reshape((2,2) + (1,)*(coords.ndim-1)) + #coords = coords[np.newaxis, ...] + + ### map coordinates and return + #mapped = (tr*coords).sum(axis=0) ## apply scale/rotate + #mapped += translate.reshape((2,1,1)) + mapped = fn.transformCoordinates(img.transform(), coords) + return result, mapped + + + ### transpose data so x and y are the first 2 axes + #trAx = range(0, data.ndim) + #trAx.remove(axes[0]) + #trAx.remove(axes[1]) + #tr1 = tuple(axes) + tuple(trAx) + #arr = data.transpose(tr1) + + ### Determine the minimal area of the data we will need + #(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes) + + ### Pad data boundaries by 1px if possible + #dataBounds = ( + #(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])), + #(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1])) + #) + + ### Extract minimal data from array + #arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]] + + ### Update roiDataTransform to reflect this extraction + #roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0]) + #### (roiDataTransform now maps from ROI coords to extracted data coords) + + + ### Rotate array + #if abs(self.state['angle']) > 1e-5: + #arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1) + + ### update data transforms to reflect this rotation + #rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi) + #roiDataTransform *= rot + + ### The rotation also causes a shift which must be accounted for: + #dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1]) + #rotBound = rot.mapRect(dataBound) + #roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top()) + + #else: + #arr2 = arr1 + + + + #### Shift off partial pixels + ## 1. map ROI into current data space + #roiBounds = roiDataTransform.mapRect(self.boundingRect()) + + ## 2. Determine amount to shift data + #shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom()) + #if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6: + ## 3. pad array with 0s before shifting + #arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype) + #arr2a[1:-1, 1:-1] = arr2 + + ## 4. shift array and udpate transforms + #arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1) + #roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1]) + #else: + #arr3 = arr2 + + + #### Extract needed region from rotated/shifted array + ## 1. map ROI into current data space (round these values off--they should be exact integer values at this point) + #roiBounds = roiDataTransform.mapRect(self.boundingRect()) + ##print self, roiBounds.height() + ##import traceback + ##traceback.print_stack() + + #roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height())) + + ##2. intersect ROI with data bounds + #dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1])) + + ##3. Extract data from array + #db = dataBounds + #bounds = ( + #(db.left(), db.right()+1), + #(db.top(), db.bottom()+1) + #) + #arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]] + + #### Create zero array in size of ROI + #arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype) + + ### Fill array with ROI data + #orig = Point(dataBounds.topLeft() - roiBounds.topLeft()) + #subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]] + #subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]] + + + ### figure out the reverse transpose order + #tr2 = np.array(tr1) + #for i in range(0, len(tr2)): + #tr2[tr1[i]] = i + #tr2 = tuple(tr2) + + ### Untranspose array before returning + #return arr5.transpose(tr2) + + def getAffineSliceParams(self, data, img, axes=(0,1)): + """ + Returns the parameters needed to use :func:`affineSlice ` to + extract a subset of *data* using this ROI and *img* to specify the subset. + + See :func:`getArrayRegion ` for more information. + """ + if self.scene() is not img.scene(): + raise Exception("ROI and target item must be members of the same scene.") + + shape = self.state['size'] + + origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + + ## vx and vy point in the directions of the slice axes, but must be scaled properly + vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin + vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + + lvx = np.sqrt(vx.x()**2 + vx.y()**2) + lvy = np.sqrt(vy.x()**2 + vy.y()**2) + pxLen = img.width() / float(data.shape[axes[0]]) + #img.width is number of pixels or width of item? + #need pxWidth and pxHeight instead of pxLen ? + sx = pxLen / lvx + sy = pxLen / lvy + + vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) + shape = self.state['size'] + shape = [abs(shape[0]/sx), abs(shape[1]/sy)] + + origin = (origin.x(), origin.y()) + return shape, vectors, origin + + def getGlobalTransform(self, relativeTo=None): + """Return global transformation (rotation angle+translation) required to move + from relative state to current state. If relative state isn't specified, + then we use the state of the ROI when mouse is pressed.""" + if relativeTo == None: + relativeTo = self.preMoveState + st = self.getState() + + ## this is only allowed because we will be comparing the two + relativeTo['scale'] = relativeTo['size'] + st['scale'] = st['size'] + + + + t1 = SRTTransform(relativeTo) + t2 = SRTTransform(st) + return t2/t1 + + + #st = self.getState() + + ### rotation + #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358 + #rot = QtGui.QTransform() + #rot.rotate(-ang) + + ### We need to come up with a universal transformation--one that can be applied to other objects + ### such that all maintain alignment. + ### More specifically, we need to turn the ROI's position and angle into + ### a rotation _around the origin_ and a translation. + + #p0 = Point(relativeTo['pos']) + + ### base position, rotated + #p1 = rot.map(p0) + + #trans = Point(st['pos']) - p1 + #return trans, ang + + def applyGlobalTransform(self, tr): + st = self.getState() + + st['scale'] = st['size'] + st = SRTTransform(st) + st = (st * tr).saveState() + st['size'] = st['scale'] + self.setState(st) + + +class Handle(UIGraphicsItem): + + types = { ## defines number of sides, start angle for each handle type + 't': (4, np.pi/4), + 'f': (4, np.pi/4), + 's': (4, 0), + 'r': (12, 0), + 'sr': (12, 0), + 'rf': (12, 0), + } + + sigClicked = QtCore.Signal(object, object) # self, event + sigRemoveRequested = QtCore.Signal(object) # self + + def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): + #print " create item with parent", parent + #self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) + #self.setFlags(self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges) + self.rois = [] + self.radius = radius + self.typ = typ + self.pen = fn.mkPen(pen) + self.currentPen = self.pen + self.pen.setWidth(0) + self.pen.setCosmetic(True) + self.isMoving = False + self.sides, self.startAng = self.types[typ] + self.buildPath() + self._shape = None + self.menu = self.buildMenu() + + UIGraphicsItem.__init__(self, parent=parent) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.deletable = deletable + if deletable: + self.setAcceptedMouseButtons(QtCore.Qt.RightButton) + #self.updateShape() + self.setZValue(11) + + def connectROI(self, roi): + ### roi is the "parent" roi, i is the index of the handle in roi.handles + self.rois.append(roi) + + def disconnectROI(self, roi): + self.rois.remove(roi) + #for i, r in enumerate(self.roi): + #if r[0] == roi: + #self.roi.pop(i) + + #def close(self): + #for r in self.roi: + #r.removeHandle(self) + + def setDeletable(self, b): + self.deletable = b + if b: + self.setAcceptedMouseButtons(self.acceptedMouseButtons() | QtCore.Qt.RightButton) + else: + self.setAcceptedMouseButtons(self.acceptedMouseButtons() & ~QtCore.Qt.RightButton) + + def removeClicked(self): + self.sigRemoveRequested.emit(self) + + def hoverEvent(self, ev): + hover = False + if not ev.isExit(): + if ev.acceptDrags(QtCore.Qt.LeftButton): + hover=True + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.RightButton, QtCore.Qt.MidButton]: + if int(self.acceptedMouseButtons() & btn) > 0 and ev.acceptClicks(btn): + hover=True + + if hover: + self.currentPen = fn.mkPen(255, 255,0) + else: + self.currentPen = self.pen + self.update() + #if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + #self.currentPen = fn.mkPen(255, 255,0) + #else: + #self.currentPen = self.pen + #self.update() + + + + def mouseClickEvent(self, ev): + ## right-click cancels drag + if ev.button() == QtCore.Qt.RightButton and self.isMoving: + self.isMoving = False ## prevents any further motion + self.movePoint(self.startPos, finish=True) + #for r in self.roi: + #r[0].cancelMove() + ev.accept() + elif int(ev.button() & self.acceptedMouseButtons()) > 0: + ev.accept() + if ev.button() == QtCore.Qt.RightButton and self.deletable: + self.raiseContextMenu(ev) + self.sigClicked.emit(self, ev) + else: + ev.ignore() + + #elif self.deletable: + #ev.accept() + #self.raiseContextMenu(ev) + #else: + #ev.ignore() + + def buildMenu(self): + menu = QtGui.QMenu() + menu.setTitle("Handle") + self.removeAction = menu.addAction("Remove handle", self.removeClicked) + return menu + + def getMenu(self): + return self.menu + + + def getContextMenus(self, event): + return [self.menu] + + def raiseContextMenu(self, ev): + menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) + + ## Make sure it is still ok to remove this handle + removeAllowed = all([r.checkRemoveHandle(self) for r in self.rois]) + self.removeAction.setEnabled(removeAllowed) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + return + ev.accept() + + ## Inform ROIs that a drag is happening + ## note: the ROI is informed that the handle has moved using ROI.movePoint + ## this is for other (more nefarious) purposes. + #for r in self.roi: + #r[0].pointDragEvent(r[1], ev) + + if ev.isFinish(): + if self.isMoving: + for r in self.rois: + r.stateChangeFinished() + self.isMoving = False + elif ev.isStart(): + for r in self.rois: + r.handleMoveStarted() + self.isMoving = True + self.startPos = self.scenePos() + self.cursorOffset = self.scenePos() - ev.buttonDownScenePos() + + if self.isMoving: ## note: isMoving may become False in mid-drag due to right-click. + pos = ev.scenePos() + self.cursorOffset + self.movePoint(pos, ev.modifiers(), finish=False) + + def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): + for r in self.rois: + if not r.checkPointMove(self, pos, modifiers): + return + #print "point moved; inform %d ROIs" % len(self.roi) + # A handle can be used by multiple ROIs; tell each to update its handle position + for r in self.rois: + r.movePoint(self, pos, modifiers, finish=finish, coords='scene') + + def buildPath(self): + size = self.radius + self.path = QtGui.QPainterPath() + ang = self.startAng + dt = 2*np.pi / self.sides + for i in range(0, self.sides+1): + x = size * cos(ang) + y = size * sin(ang) + ang += dt + if i == 0: + self.path.moveTo(x, y) + else: + self.path.lineTo(x, y) + + def paint(self, p, opt, widget): + ### determine rotation of transform + #m = self.sceneTransform() + ##mi = m.inverted()[0] + #v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) + #va = np.arctan2(v.y(), v.x()) + + ### Determine length of unit vector in painter's coords + ##size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) + ##size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 + #size = self.radius + + #bounds = QtCore.QRectF(-size, -size, size*2, size*2) + #if bounds != self.bounds: + #self.bounds = bounds + #self.prepareGeometryChange() + p.setRenderHints(p.Antialiasing, True) + p.setPen(self.currentPen) + + #p.rotate(va * 180. / 3.1415926) + #p.drawPath(self.path) + p.drawPath(self.shape()) + #ang = self.startAng + va + #dt = 2*np.pi / self.sides + #for i in range(0, self.sides): + #x1 = size * cos(ang) + #y1 = size * sin(ang) + #x2 = size * cos(ang+dt) + #y2 = size * sin(ang+dt) + #ang += dt + #p.drawLine(Point(x1, y1), Point(x2, y2)) + + def shape(self): + if self._shape is None: + s = self.generateShape() + if s is None: + return self.path + self._shape = s + self.prepareGeometryChange() ## beware--this can cause the view to adjust, which would immediately invalidate the shape. + return self._shape + + def boundingRect(self): + #print 'roi:', self.roi + s1 = self.shape() + #print " s1:", s1 + #s2 = self.shape() + #print " s2:", s2 + + return self.shape().boundingRect() + + def generateShape(self): + ## determine rotation of transform + #m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene. + #mi = m.inverted()[0] + + dt = self.deviceTransform() + + if dt is None: + self._shape = self.path + return None + + v = dt.map(QtCore.QPointF(1, 0)) - dt.map(QtCore.QPointF(0, 0)) + va = np.arctan2(v.y(), v.x()) + + dti = fn.invertQTransform(dt) + devPos = dt.map(QtCore.QPointF(0,0)) + tr = QtGui.QTransform() + tr.translate(devPos.x(), devPos.y()) + tr.rotate(va * 180. / 3.1415926) + + return dti.map(tr.map(self.path)) + + + def viewRangeChanged(self): + GraphicsObject.viewRangeChanged(self) + self._shape = None ## invalidate shape, recompute later if requested. + #self.updateShape() + + #def itemChange(self, change, value): + #if change == self.ItemScenePositionHasChanged: + #self.updateShape() + + +class TestROI(ROI): + def __init__(self, pos, size, **args): + #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) + ROI.__init__(self, pos, size, **args) + #self.addTranslateHandle([0, 0]) + self.addTranslateHandle([0.5, 0.5]) + self.addScaleHandle([1, 1], [0, 0]) + self.addScaleHandle([0, 0], [1, 1]) + self.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) + self.addScaleHandle([0.5, 1], [0.5, 0.5]) + self.addRotateHandle([1, 0], [0, 0]) + self.addRotateHandle([0, 1], [1, 1]) + + + +class RectROI(ROI): + def __init__(self, pos, size, centered=False, sideScalers=False, **args): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, pos, size, **args) + if centered: + center = [0.5, 0.5] + else: + center = [0, 0] + + #self.addTranslateHandle(center) + self.addScaleHandle([1, 1], center) + if sideScalers: + self.addScaleHandle([1, 0.5], [center[0], 0.5]) + self.addScaleHandle([0.5, 1], [0.5, center[1]]) + +class LineROI(ROI): + def __init__(self, pos1, pos2, width, **args): + pos1 = Point(pos1) + pos2 = Point(pos2) + d = pos2-pos1 + l = d.length() + ang = Point(1, 0).angle(d) + ra = ang * np.pi / 180. + c = Point(-width/2. * sin(ra), -width/2. * cos(ra)) + pos1 = pos1 + c + + ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args) + self.addScaleRotateHandle([0, 0.5], [1, 0.5]) + self.addScaleRotateHandle([1, 0.5], [0, 0.5]) + self.addScaleHandle([0.5, 1], [0.5, 0.5]) + + + +class MultiRectROI(QtGui.QGraphicsObject): + """ + Chain of rectangular ROIs connected by handles. + + This is generally used to mark a curved path through + an image similarly to PolyLineROI. It differs in that each segment + of the chain is rectangular instead of linear and thus has width. + """ + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChangeStarted = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + + def __init__(self, points, width, pen=None, **args): + QtGui.QGraphicsObject.__init__(self) + self.pen = pen + self.roiArgs = args + self.lines = [] + if len(points) < 2: + raise Exception("Must start with at least 2 points") + + ## create first segment + self.addSegment(points[1], connectTo=points[0], scaleHandle=True) + + ## create remaining segments + for p in points[2:]: + self.addSegment(p) + + + def paint(self, *args): + pass + + def boundingRect(self): + return QtCore.QRectF() + + def roiChangedEvent(self): + w = self.lines[0].state['size'][1] + for l in self.lines[1:]: + w0 = l.state['size'][1] + if w == w0: + continue + l.scale([1.0, w/w0], center=[0.5,0.5]) + self.sigRegionChanged.emit(self) + + def roiChangeStartedEvent(self): + self.sigRegionChangeStarted.emit(self) + + def roiChangeFinishedEvent(self): + self.sigRegionChangeFinished.emit(self) + + def getHandlePositions(self): + """Return the positions of all handles in local coordinates.""" + pos = [self.mapFromScene(self.lines[0].getHandles()[0].scenePos())] + for l in self.lines: + pos.append(self.mapFromScene(l.getHandles()[1].scenePos())) + return pos + + def getArrayRegion(self, arr, img=None, axes=(0,1)): + rgns = [] + for l in self.lines: + rgn = l.getArrayRegion(arr, img, axes=axes) + if rgn is None: + continue + #return None + rgns.append(rgn) + #print l.state['size'] + + ## make sure orthogonal axis is the same size + ## (sometimes fp errors cause differences) + ms = min([r.shape[axes[1]] for r in rgns]) + sl = [slice(None)] * rgns[0].ndim + sl[axes[1]] = slice(0,ms) + rgns = [r[sl] for r in rgns] + #print [r.shape for r in rgns], axes + + return np.concatenate(rgns, axis=axes[0]) + + def addSegment(self, pos=(0,0), scaleHandle=False, connectTo=None): + """ + Add a new segment to the ROI connecting from the previous endpoint to *pos*. + (pos is specified in the parent coordinate system of the MultiRectROI) + """ + + ## by default, connect to the previous endpoint + if connectTo is None: + connectTo = self.lines[-1].getHandles()[1] + + ## create new ROI + newRoi = ROI((0,0), [1, 5], parent=self, pen=self.pen, **self.roiArgs) + self.lines.append(newRoi) + + ## Add first SR handle + if isinstance(connectTo, Handle): + self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=connectTo) + newRoi.movePoint(connectTo, connectTo.scenePos(), coords='scene') + else: + h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) + newRoi.movePoint(h, connectTo, coords='scene') + + ## add second SR handle + h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) + newRoi.movePoint(h, pos) + + ## optionally add scale handle (this MUST come after the two SR handles) + if scaleHandle: + newRoi.addScaleHandle([0.5, 1], [0.5, 0.5]) + + newRoi.translatable = False + newRoi.sigRegionChanged.connect(self.roiChangedEvent) + newRoi.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) + newRoi.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) + self.sigRegionChanged.emit(self) + + + def removeSegment(self, index=-1): + """Remove a segment from the ROI.""" + roi = self.lines[index] + self.lines.pop(index) + self.scene().removeItem(roi) + roi.sigRegionChanged.disconnect(self.roiChangedEvent) + roi.sigRegionChangeStarted.disconnect(self.roiChangeStartedEvent) + roi.sigRegionChangeFinished.disconnect(self.roiChangeFinishedEvent) + + self.sigRegionChanged.emit(self) + + +class MultiLineROI(MultiRectROI): + def __init__(self, *args, **kwds): + MultiRectROI.__init__(self, *args, **kwds) + print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") + +class EllipseROI(ROI): + def __init__(self, pos, size, **args): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, pos, size, **args) + self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) + self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + + def paint(self, p, opt, widget): + r = self.boundingRect() + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + + p.scale(r.width(), r.height())## workaround for GL bug + r = QtCore.QRectF(r.x()/r.width(), r.y()/r.height(), 1,1) + + p.drawEllipse(r) + + def getArrayRegion(self, arr, img=None): + arr = ROI.getArrayRegion(self, arr, img) + if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: + return None + w = arr.shape[0] + h = arr.shape[1] + ## generate an ellipsoidal mask + mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) + + return arr * mask + + def shape(self): + self.path = QtGui.QPainterPath() + self.path.addEllipse(self.boundingRect()) + return self.path + + +class CircleROI(EllipseROI): + def __init__(self, pos, size, **args): + ROI.__init__(self, pos, size, **args) + self.aspectLocked = True + #self.addTranslateHandle([0.5, 0.5]) + self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + +class PolygonROI(ROI): + ## deprecated. Use PloyLineROI instead. + + def __init__(self, positions, pos=None, **args): + if pos is None: + pos = [0,0] + ROI.__init__(self, pos, [1,1], **args) + #ROI.__init__(self, positions[0]) + for p in positions: + self.addFreeHandle(p) + self.setZValue(1000) + print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.") + + + def listPoints(self): + return [p['item'].pos() for p in self.handles] + + #def movePoint(self, *args, **kargs): + #ROI.movePoint(self, *args, **kargs) + #self.prepareGeometryChange() + #for h in self.handles: + #h['pos'] = h['item'].pos() + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + for i in range(len(self.handles)): + h1 = self.handles[i]['item'].pos() + h2 = self.handles[i-1]['item'].pos() + p.drawLine(h1, h2) + + def boundingRect(self): + r = QtCore.QRectF() + for h in self.handles: + r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs + return r + + def shape(self): + p = QtGui.QPainterPath() + p.moveTo(self.handles[0]['item'].pos()) + for i in range(len(self.handles)): + p.lineTo(self.handles[i]['item'].pos()) + return p + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + #sc['handles'] = self.handles + return sc + +class PolyLineROI(ROI): + """Container class for multiple connected LineSegmentROIs. Responsible for adding new + line segments, and for translation/(rotation?) of multiple lines together.""" + def __init__(self, positions, closed=False, pos=None, **args): + + if pos is None: + pos = [0,0] + #pen=args.get('pen', fn.mkPen((100,100,255))) + ROI.__init__(self, pos, size=[1,1], **args) + self.closed = closed + self.segments = [] + + for p in positions: + self.addFreeHandle(p) + + start = -1 if self.closed else 0 + for i in range(start, len(self.handles)-1): + self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) + #for i in range(len(positions)-1): + #h2 = self.addFreeHandle(positions[i+1]) + #segment = LineSegmentROI(handles=(h, h2), pen=pen, parent=self, movable=False) + #self.segments.append(segment) + #h = h2 + + + #for i, s in enumerate(self.segments): + #h = s.handles[0] + #self.addFreeHandle(h['pos'], item=h['item']) + #s.setZValue(self.zValue() +1) + + #h = self.segments[-1].handles[1] + #self.addFreeHandle(h['pos'], item=h['item']) + + #if closed: + #h1 = self.handles[-1]['item'] + #h2 = self.handles[0]['item'] + #self.segments.append(LineSegmentROI([positions[-1], positions[0]], pos=pos, handles=(h1, h2), pen=pen, parent=self, movable=False)) + #h2.setParentItem(self.segments[-1]) + + + #for s in self.segments: + #self.setSegmentSettings(s) + + #def movePoint(self, *args, **kargs): + #pass + + def addSegment(self, h1, h2, index=None): + seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) + if index is None: + self.segments.append(seg) + else: + self.segments.insert(index, seg) + seg.sigClicked.connect(self.segmentClicked) + seg.setAcceptedMouseButtons(QtCore.Qt.LeftButton) + seg.setZValue(self.zValue()+1) + for h in seg.handles: + h['item'].setDeletable(True) + h['item'].setAcceptedMouseButtons(h['item'].acceptedMouseButtons() | QtCore.Qt.LeftButton) ## have these handles take left clicks too, so that handles cannot be added on top of other handles + + def setMouseHover(self, hover): + ## Inform all the ROI's segments that the mouse is(not) hovering over it + #if self.mouseHovering == hover: + #return + #self.mouseHovering = hover + ROI.setMouseHover(self, hover) + for s in self.segments: + s.setMouseHover(hover) + + def addHandle(self, info, index=None): + h = ROI.addHandle(self, info, index=index) + h.sigRemoveRequested.connect(self.removeHandle) + return h + + def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this item's coordinate system + if ev != None: + pos = segment.mapToParent(ev.pos()) + elif pos != None: + pos = pos + else: + raise Exception("Either an event or a position must be given.") + h1 = segment.handles[0]['item'] + h2 = segment.handles[1]['item'] + + i = self.segments.index(segment) + h3 = self.addFreeHandle(pos, index=self.indexOfHandle(h2)) + self.addSegment(h3, h2, index=i+1) + segment.replaceHandle(h2, h3) + + + #def report(self): + #for s in self.segments: + #print s + #for h in s.handles: + #print " ", h + #for h in self.handles: + #print h + + def removeHandle(self, handle, updateSegments=True): + ROI.removeHandle(self, handle) + handle.sigRemoveRequested.disconnect(self.removeHandle) + + if not updateSegments: + return + segments = handle.rois[:] + + if len(segments) == 1: + self.removeSegment(segments[0]) + else: + handles = [h['item'] for h in segments[1].handles] + handles.remove(handle) + segments[0].replaceHandle(handle, handles[0]) + self.removeSegment(segments[1]) + + def removeSegment(self, seg): + for handle in seg.handles[:]: + seg.removeHandle(handle['item']) + self.segments.remove(seg) + seg.sigClicked.disconnect(self.segmentClicked) + self.scene().removeItem(seg) + + def checkRemoveHandle(self, h): + ## called when a handle is about to display its context menu + if self.closed: + return len(self.handles) > 3 + else: + return len(self.handles) > 2 + + def paint(self, p, *args): + #for s in self.segments: + #s.update() + #p.setPen(self.currentPen) + #p.setPen(fn.mkPen('w')) + #p.drawRect(self.boundingRect()) + #p.drawPath(self.shape()) + pass + + def boundingRect(self): + return self.shape().boundingRect() + #r = QtCore.QRectF() + #for h in self.handles: + #r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs + #return r + + def shape(self): + p = QtGui.QPainterPath() + p.moveTo(self.handles[0]['item'].pos()) + for i in range(len(self.handles)): + p.lineTo(self.handles[i]['item'].pos()) + p.lineTo(self.handles[0]['item'].pos()) + return p + + +class LineSegmentROI(ROI): + """ + ROI subclass with two freely-moving handles defining a line. + """ + + def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args): + if pos is None: + pos = [0,0] + + ROI.__init__(self, pos, [1,1], **args) + #ROI.__init__(self, positions[0]) + if len(positions) > 2: + raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") + + for i, p in enumerate(positions): + self.addFreeHandle(p, item=handles[i]) + + + def listPoints(self): + return [p['item'].pos() for p in self.handles] + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + p.setPen(self.currentPen) + h1 = self.handles[0]['item'].pos() + h2 = self.handles[1]['item'].pos() + p.drawLine(h1, h2) + + def boundingRect(self): + return self.shape().boundingRect() + + def shape(self): + p = QtGui.QPainterPath() + + h1 = self.handles[0]['item'].pos() + h2 = self.handles[1]['item'].pos() + dh = h2-h1 + if dh.length() == 0: + return p + pxv = self.pixelVectors(h2-h1)[1] + + if pxv is None: + return p + + pxv *= 4 + + p.moveTo(h1+pxv) + p.lineTo(h2+pxv) + p.lineTo(h2-pxv) + p.lineTo(h1-pxv) + p.lineTo(h1+pxv) + + return p + + def getArrayRegion(self, data, img, axes=(0,1)): + """ + Use the position of this ROI relative to an imageItem to pull a slice from an array. + Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1 + """ + + imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles] + rgns = [] + for i in range(len(imgPts)-1): + d = Point(imgPts[i+1] - imgPts[i]) + o = Point(imgPts[i]) + r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[d.norm()], origin=o, axes=axes, order=1) + rgns.append(r) + + return np.concatenate(rgns, axis=axes[0]) + + +class SpiralROI(ROI): + def __init__(self, pos=None, size=None, **args): + if size == None: + size = [100e-6,100e-6] + if pos == None: + pos = [0,0] + ROI.__init__(self, pos, size, **args) + self.translateSnap = False + self.addFreeHandle([0.25,0], name='a') + self.addRotateFreeHandle([1,0], [0,0], name='r') + #self.getRadius() + #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self. + + + def getRadius(self): + radius = Point(self.handles[1]['item'].pos()).length() + #r2 = radius[1] + #r3 = r2[0] + return radius + + def boundingRect(self): + r = self.getRadius() + return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) + #return self.bounds + + #def movePoint(self, *args, **kargs): + #ROI.movePoint(self, *args, **kargs) + #self.prepareGeometryChange() + #for h in self.handles: + #h['pos'] = h['item'].pos()/self.state['size'][0] + + def stateChanged(self): + ROI.stateChanged(self) + if len(self.handles) > 1: + self.path = QtGui.QPainterPath() + h0 = Point(self.handles[0]['item'].pos()).length() + a = h0/(2.0*np.pi) + theta = 30.0*(2.0*np.pi)/360.0 + self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta))) + x0 = a*theta*cos(theta) + y0 = a*theta*sin(theta) + radius = self.getRadius() + theta += 20.0*(2.0*np.pi)/360.0 + i = 0 + while Point(x0, y0).length() < radius and i < 1000: + x1 = a*theta*cos(theta) + y1 = a*theta*sin(theta) + self.path.lineTo(QtCore.QPointF(x1,y1)) + theta += 20.0*(2.0*np.pi)/360.0 + x0 = x1 + y0 = y1 + i += 1 + + + return self.path + + + def shape(self): + p = QtGui.QPainterPath() + p.addEllipse(self.boundingRect()) + return p + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + #path = self.shape() + p.setPen(self.currentPen) + p.drawPath(self.path) + p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + p.drawPath(self.shape()) + p.setPen(QtGui.QPen(QtGui.QColor(0,0,255))) + p.drawRect(self.boundingRect()) + + + + + diff --git a/pyqtgraph/graphicsItems/ScaleBar.py b/pyqtgraph/graphicsItems/ScaleBar.py new file mode 100644 index 00000000..961f07d7 --- /dev/null +++ b/pyqtgraph/graphicsItems/ScaleBar.py @@ -0,0 +1,50 @@ +from pyqtgraph.Qt import QtGui, QtCore +from .UIGraphicsItem import * +import numpy as np +import pyqtgraph.functions as fn + +__all__ = ['ScaleBar'] +class ScaleBar(UIGraphicsItem): + """ + Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view. + """ + def __init__(self, size, width=5, color=(100, 100, 255)): + UIGraphicsItem.__init__(self) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + + self.brush = fn.mkBrush(color) + self.pen = fn.mkPen((0,0,0)) + self._width = width + self.size = size + + def paint(self, p, opt, widget): + UIGraphicsItem.paint(self, p, opt, widget) + + rect = self.boundingRect() + unit = self.pixelSize() + y = rect.top() + (rect.bottom()-rect.top()) * 0.02 + y1 = y + unit[1]*self._width + x = rect.right() + (rect.left()-rect.right()) * 0.02 + x1 = x - self.size + + p.setPen(self.pen) + p.setBrush(self.brush) + rect = QtCore.QRectF( + QtCore.QPointF(x1, y1), + QtCore.QPointF(x, y) + ) + p.translate(x1, y1) + p.scale(rect.width(), rect.height()) + p.drawRect(0, 0, 1, 1) + + alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255) + p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) + for i in range(1, 10): + #x2 = x + (x1-x) * 0.1 * i + x2 = 0.1 * i + p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) + + + def setSize(self, s): + self.size = s + diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py new file mode 100644 index 00000000..2e41cb7c --- /dev/null +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -0,0 +1,913 @@ +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +from .GraphicsItem import GraphicsItem +from .GraphicsObject import GraphicsObject +import numpy as np +import scipy.stats +import weakref +import pyqtgraph.debug as debug +from pyqtgraph.pgcollections import OrderedDict +import pyqtgraph as pg +#import pyqtgraph as pg + +__all__ = ['ScatterPlotItem', 'SpotItem'] + + +## Build all symbol paths +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']]) +Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) +Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) +coords = { + 't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)], + 'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)], + '+': [ + (-0.5, -0.05), (-0.5, 0.05), (-0.05, 0.05), (-0.05, 0.5), + (0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05), + (0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05) + ], +} +for k, c in coords.items(): + Symbols[k].moveTo(*c[0]) + for x,y in c[1:]: + Symbols[k].lineTo(x, y) + Symbols[k].closeSubpath() + + +def drawSymbol(painter, symbol, size, pen, brush): + painter.scale(size, size) + painter.setPen(pen) + painter.setBrush(brush) + if isinstance(symbol, basestring): + symbol = Symbols[symbol] + if np.isscalar(symbol): + symbol = Symbols.values()[symbol % len(Symbols)] + painter.drawPath(symbol) + + +def renderSymbol(symbol, size, pen, brush, device=None): + """ + Render a symbol specification to QImage. + Symbol may be either a QPainterPath or one of the keys in the Symbols dict. + If *device* is None, a new QPixmap will be returned. Otherwise, + the symbol will be rendered into the device specified (See QPainter documentation + for more information). + """ + ## see if this pixmap is already cached + #global SymbolPixmapCache + #key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + #if key in SymbolPixmapCache: + #return SymbolPixmapCache[key] + + ## Render a spot with the given parameters to a pixmap + penPxWidth = max(np.ceil(pen.width()), 1) + image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32) + image.fill(0) + p = QtGui.QPainter(image) + p.setRenderHint(p.Antialiasing) + p.translate(image.width()*0.5, image.height()*0.5) + drawSymbol(p, symbol, size, pen, brush) + p.end() + return image + #pixmap = QtGui.QPixmap(image) + #SymbolPixmapCache[key] = pixmap + #return pixmap + +def makeSymbolPixmap(size, pen, brush, symbol): + ## deprecated + img = renderSymbol(symbol, size, pen, brush) + return QtGui.QPixmap(img) + +class SymbolAtlas(object): + """ + Used to efficiently construct a single QPixmap containing all rendered symbols + for a ScatterPlotItem. This is required for fragment rendering. + + Use example: + atlas = SymbolAtlas() + sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..)) + sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..)) + pm = atlas.getAtlas() + + """ + class SymbolCoords(list): ## needed because lists are not allowed in weak references. + pass + + def __init__(self): + # symbol key : [x, y, w, h] atlas coordinates + # note that the coordinate list will always be the same list object as + # long as the symbol is in the atlas, but the coordinates may + # change if the atlas is rebuilt. + # weak value; if all external refs to this list disappear, + # the symbol will be forgotten. + self.symbolMap = weakref.WeakValueDictionary() + + self.atlasData = None # numpy array of atlas image + self.atlas = None # atlas as QPixmap + self.atlasValid = False + + def getSymbolCoords(self, opts): + """ + Given a list of spot records, return an object representing the coordinates of that symbol within the atlas + """ + coords = np.empty(len(opts), dtype=object) + for i, rec in enumerate(opts): + symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush'] + pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen + brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush + key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color())) + if key not in self.symbolMap: + newCoords = SymbolAtlas.SymbolCoords() + self.symbolMap[key] = newCoords + self.atlasValid = False + #try: + #self.addToAtlas(key) ## squeeze this into the atlas if there is room + #except: + #self.buildAtlas() ## otherwise, we need to rebuild + + coords[i] = self.symbolMap[key] + return coords + + def buildAtlas(self): + # get rendered array for all symbols, keep track of avg/max width + rendered = {} + avgWidth = 0.0 + maxWidth = 0 + images = [] + for key, coords in self.symbolMap.items(): + if len(coords) == 0: + pen = fn.mkPen(color=key[2], width=key[3], style=key[4]) + brush = fn.mkBrush(color=key[5]) + img = renderSymbol(key[0], key[1], pen, brush) + images.append(img) ## we only need this to prevent the images being garbage collected immediately + arr = fn.imageToArray(img, copy=False, transpose=False) + else: + (x,y,w,h) = self.symbolMap[key] + arr = self.atlasData[x:x+w, y:y+w] + rendered[key] = arr + w = arr.shape[0] + avgWidth += w + maxWidth = max(maxWidth, w) + + nSymbols = len(rendered) + if nSymbols > 0: + avgWidth /= nSymbols + width = max(maxWidth, avgWidth * (nSymbols**0.5)) + else: + avgWidth = 0 + width = 0 + + # sort symbols by height + symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True) + + self.atlasRows = [] + + x = width + y = 0 + rowheight = 0 + for key in symbols: + arr = rendered[key] + w,h = arr.shape[:2] + if x+w > width: + y += rowheight + x = 0 + rowheight = h + self.atlasRows.append([y, rowheight, 0]) + self.symbolMap[key][:] = x, y, w, h + x += w + self.atlasRows[-1][2] = x + height = y + rowheight + + self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) + for key in symbols: + x, y, w, h = self.symbolMap[key] + self.atlasData[x:x+w, y:y+h] = rendered[key] + self.atlas = None + self.atlasValid = True + + def getAtlas(self): + if not self.atlasValid: + self.buildAtlas() + if self.atlas is None: + if len(self.atlasData) == 0: + return QtGui.QPixmap(0,0) + img = fn.makeQImage(self.atlasData, copy=False, transpose=False) + self.atlas = QtGui.QPixmap(img) + return self.atlas + + + + +class ScatterPlotItem(GraphicsObject): + """ + Displays a set of x/y points. Instances of this class are created + automatically as part of PlotDataItem; these rarely need to be instantiated + directly. + + The size, shape, pen, and fill brush may be set for each point individually + or for all points. + + + ======================== =============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data being plotted has changed + sigClicked(self, points) Emitted when the curve is clicked. Sends a list + of all the points under the mouse pointer. + ======================== =============================================== + + """ + #sigPointClicked = QtCore.Signal(object, object) + sigClicked = QtCore.Signal(object, object) ## self, points + sigPlotChanged = QtCore.Signal(object) + def __init__(self, *args, **kargs): + """ + Accepts the same arguments as setData() + """ + prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) + GraphicsObject.__init__(self) + + self.picture = None # QPicture used for rendering when pxmode==False + self.fragments = None # fragment specification for pxmode; updated every time the view changes. + self.fragmentAtlas = SymbolAtlas() + + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)]) + self.bounds = [None, None] ## caches data bounds + self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots + self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots + self.opts = { + 'pxMode': True, + 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. + 'antialias': pg.getConfigOption('antialias'), + } + + self.setPen(200,200,200, update=False) + self.setBrush(100,100,150, update=False) + self.setSymbol('o', update=False) + self.setSize(7, update=False) + prof.mark('1') + self.setData(*args, **kargs) + prof.mark('setData') + prof.finish() + + #self.setCacheMode(self.DeviceCoordinateCache) + + def setData(self, *args, **kargs): + """ + **Ordered Arguments:** + + * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. + * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. + + ====================== =============================================================================================== + **Keyword Arguments:** + *spots* Optional list of dicts. Each dict specifies parameters for a single spot: + {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method + of passing in data for the corresponding arguments. + *x*,*y* 1D arrays of x,y values. + *pos* 2D structure of x,y pairs (such as Nx2 array or list of tuples) + *pxMode* If True, spots are always the same size regardless of scaling, and size is given in px. + Otherwise, size is in scene coordinates and the spots scale with the view. + Default is True + *symbol* can be one (or a list) of: + * 'o' circle (default) + * 's' square + * 't' triangle + * 'd' diamond + * '+' plus + * any QPainterPath to specify custom symbol shapes. To properly obey the position and size, + custom symbols should be centered at (0,0) and width and height of 1.0. Note that it is also + possible to 'install' custom shapes by setting ScatterPlotItem.Symbols[key] = shape. + *pen* The pen (or list of pens) to use for drawing spot outlines. + *brush* The brush (or list of brushes) to use for filling spots. + *size* The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, + it is in the item's local coordinate system. + *data* a list of python objects used to uniquely identify each spot. + *identical* *Deprecated*. This functionality is handled automatically now. + *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are + always rendered with antialiasing (since the rendered symbols can be cached, this + incurs very little performance cost) + ====================== =============================================================================================== + """ + oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered. + self.clear() ## clear out all old data + self.addPoints(*args, **kargs) + + def addPoints(self, *args, **kargs): + """ + Add new points to the scatter plot. + Arguments are the same as setData() + """ + + ## deal with non-keyword arguments + if len(args) == 1: + kargs['spots'] = args[0] + elif len(args) == 2: + kargs['x'] = args[0] + kargs['y'] = args[1] + elif len(args) > 2: + raise Exception('Only accepts up to two non-keyword arguments.') + + ## convert 'pos' argument to 'x' and 'y' + if 'pos' in kargs: + pos = kargs['pos'] + if isinstance(pos, np.ndarray): + kargs['x'] = pos[:,0] + kargs['y'] = pos[:,1] + else: + x = [] + y = [] + for p in pos: + if isinstance(p, QtCore.QPointF): + x.append(p.x()) + y.append(p.y()) + else: + x.append(p[0]) + y.append(p[1]) + kargs['x'] = x + kargs['y'] = y + + ## determine how many spots we have + if 'spots' in kargs: + numPts = len(kargs['spots']) + elif 'y' in kargs and kargs['y'] is not None: + numPts = len(kargs['y']) + else: + kargs['x'] = [] + kargs['y'] = [] + numPts = 0 + + ## Extend record array + oldData = self.data + self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype) + ## note that np.empty initializes object fields to None and string fields to '' + + self.data[:len(oldData)] = oldData + #for i in range(len(oldData)): + #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array + + newData = self.data[len(oldData):] + newData['size'] = -1 ## indicates to use default size + + if 'spots' in kargs: + spots = kargs['spots'] + for i in range(len(spots)): + spot = spots[i] + for k in spot: + #if k == 'pen': + #newData[k] = fn.mkPen(spot[k]) + #elif k == 'brush': + #newData[k] = fn.mkBrush(spot[k]) + if k == 'pos': + pos = spot[k] + if isinstance(pos, QtCore.QPointF): + x,y = pos.x(), pos.y() + else: + x,y = pos[0], pos[1] + newData[i]['x'] = x + newData[i]['y'] = y + elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']: + newData[i][k] = spot[k] + #elif k == 'data': + #self.pointData[i] = spot[k] + else: + raise Exception("Unknown spot parameter: %s" % k) + elif 'y' in kargs: + newData['x'] = kargs['x'] + newData['y'] = kargs['y'] + + if 'pxMode' in kargs: + self.setPxMode(kargs['pxMode']) + if 'antialias' in kargs: + self.opts['antialias'] = kargs['antialias'] + + ## Set any extra parameters provided in keyword arguments + for k in ['pen', 'brush', 'symbol', 'size']: + if k in kargs: + setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) + setMethod(kargs[k], update=False, dataSet=newData) + + if 'data' in kargs: + self.setPointData(kargs['data'], dataSet=newData) + + self.prepareGeometryChange() + self.bounds = [None, None] + self.invalidate() + self.updateSpots(newData) + self.sigPlotChanged.emit(self) + + def invalidate(self): + ## clear any cached drawing state + self.picture = None + self.fragments = None + self.update() + + def getData(self): + return self.data['x'], self.data['y'] + + + def setPoints(self, *args, **kargs): + ##Deprecated; use setData + return self.setData(*args, **kargs) + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def setPen(self, *args, **kargs): + """Set the pen(s) used to draw the outline around each spot. + If a list or array is provided, then the pen for each spot will be set separately. + Otherwise, the arguments are passed to pg.mkPen and used as the default pen for + all spots which do not have a pen explicitly set.""" + update = kargs.pop('update', True) + dataSet = kargs.pop('dataSet', self.data) + + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): + pens = args[0] + if len(pens) != len(dataSet): + raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) + dataSet['pen'] = pens + else: + self.opts['pen'] = fn.mkPen(*args, **kargs) + + dataSet['fragCoords'] = None + if update: + self.updateSpots(dataSet) + + def setBrush(self, *args, **kargs): + """Set the brush(es) used to fill the interior of each spot. + If a list or array is provided, then the brush for each spot will be set separately. + Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for + all spots which do not have a brush explicitly set.""" + update = kargs.pop('update', True) + dataSet = kargs.pop('dataSet', self.data) + + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): + brushes = args[0] + if len(brushes) != len(dataSet): + raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) + #for i in xrange(len(brushes)): + #self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs) + dataSet['brush'] = brushes + else: + self.opts['brush'] = fn.mkBrush(*args, **kargs) + #self._spotPixmap = None + + dataSet['fragCoords'] = None + if update: + self.updateSpots(dataSet) + + def setSymbol(self, symbol, update=True, dataSet=None): + """Set the symbol(s) used to draw each spot. + If a list or array is provided, then the symbol for each spot will be set separately. + Otherwise, the argument will be used as the default symbol for + all spots which do not have a symbol explicitly set.""" + if dataSet is None: + dataSet = self.data + + if isinstance(symbol, np.ndarray) or isinstance(symbol, list): + symbols = symbol + if len(symbols) != len(dataSet): + raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet))) + dataSet['symbol'] = symbols + else: + self.opts['symbol'] = symbol + self._spotPixmap = None + + dataSet['fragCoords'] = None + if update: + self.updateSpots(dataSet) + + def setSize(self, size, update=True, dataSet=None): + """Set the size(s) used to draw each spot. + If a list or array is provided, then the size for each spot will be set separately. + Otherwise, the argument will be used as the default size for + all spots which do not have a size explicitly set.""" + if dataSet is None: + dataSet = self.data + + if isinstance(size, np.ndarray) or isinstance(size, list): + sizes = size + if len(sizes) != len(dataSet): + raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet))) + dataSet['size'] = sizes + else: + self.opts['size'] = size + self._spotPixmap = None + + dataSet['fragCoords'] = None + if update: + self.updateSpots(dataSet) + + def setPointData(self, data, dataSet=None): + if dataSet is None: + dataSet = self.data + + if isinstance(data, np.ndarray) or isinstance(data, list): + if len(data) != len(dataSet): + raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) + + ## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time. + ## (otherwise they are converted to tuples and thus lose their field names. + if isinstance(data, np.ndarray) and len(data.dtype.fields) > 1: + for i, rec in enumerate(data): + dataSet['data'][i] = rec + else: + dataSet['data'] = data + + def setPxMode(self, mode): + if self.opts['pxMode'] == mode: + return + + self.opts['pxMode'] = mode + self.invalidate() + + def updateSpots(self, dataSet=None): + if dataSet is None: + dataSet = self.data + self._maxSpotWidth = 0 + self._maxSpotPxWidth = 0 + invalidate = False + self.measureSpotSizes(dataSet) + if self.opts['pxMode']: + mask = np.equal(dataSet['fragCoords'], None) + if np.any(mask): + invalidate = True + opts = self.getSpotOpts(dataSet[mask]) + coords = self.fragmentAtlas.getSymbolCoords(opts) + dataSet['fragCoords'][mask] = coords + + #for rec in dataSet: + #if rec['fragCoords'] is None: + #invalidate = True + #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) + if invalidate: + self.invalidate() + + def getSpotOpts(self, recs, scale=1.0): + if recs.ndim == 0: + rec = recs + symbol = rec['symbol'] + if symbol is None: + symbol = self.opts['symbol'] + size = rec['size'] + if size < 0: + size = self.opts['size'] + pen = rec['pen'] + if pen is None: + pen = self.opts['pen'] + brush = rec['brush'] + if brush is None: + brush = self.opts['brush'] + return (symbol, size*scale, fn.mkPen(pen), fn.mkBrush(brush)) + else: + recs = recs.copy() + recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol'] + recs['size'][np.equal(recs['size'], -1)] = self.opts['size'] + recs['size'] *= scale + recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) + 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 + symbol, size, pen, brush = self.getSpotOpts(rec) + width = 0 + pxWidth = 0 + if self.opts['pxMode']: + pxWidth = size + pen.width() + else: + width = size + if pen.isCosmetic(): + pxWidth += pen.width() + else: + width += pen.width() + self._maxSpotWidth = max(self._maxSpotWidth, width) + self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) + self.bounds = [None, None] + + + def clear(self): + """Remove all spots from the scatter plot""" + #self.clearItems() + self.data = np.empty(0, dtype=self.data.dtype) + self.bounds = [None, None] + self.invalidate() + + def dataBounds(self, ax, frac=1.0, orthoRange=None): + if frac >= 1.0 and self.bounds[ax] is not None: + return self.bounds[ax] + + #self.prepareGeometryChange() + if self.data is None or len(self.data) == 0: + return (None, None) + + if ax == 0: + d = self.data['x'] + d2 = self.data['y'] + elif ax == 1: + d = self.data['y'] + d2 = self.data['x'] + + if orthoRange is not None: + mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) + d = d[mask] + d2 = d2[mask] + + if frac >= 1.0: + ## increase size of bounds based on spot size and pen width + px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis + if px is None: + px = 0 + minIndex = np.argmin(d) + maxIndex = np.argmax(d) + minVal = d[minIndex] + maxVal = d[maxIndex] + spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth) + self.bounds[ax] = (minVal-spotSize, maxVal+spotSize) + return self.bounds[ax] + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + else: + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + + #def defaultSpotPixmap(self): + ### Return the default spot pixmap + #if self._spotPixmap is None: + #self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) + #return self._spotPixmap + + def boundingRect(self): + (xmn, xmx) = self.dataBounds(ax=0) + (ymn, ymx) = self.dataBounds(ax=1) + if xmn is None or xmx is None: + xmn = 0 + xmx = 0 + if ymn is None or ymx is None: + ymn = 0 + ymx = 0 + return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) + + def viewTransformChanged(self): + self.prepareGeometryChange() + GraphicsObject.viewTransformChanged(self) + self.bounds = [None, None] + self.fragments = None + + def generateFragments(self): + tr = self.deviceTransform() + if tr is None: + return + pts = np.empty((2,len(self.data['x']))) + pts[0] = self.data['x'] + pts[1] = self.data['y'] + pts = fn.transformCoordinates(tr, pts) + self.fragments = [] + for i in xrange(len(self.data)): + rec = self.data[i] + pos = QtCore.QPointF(pts[0,i], pts[1,i]) + x,y,w,h = rec['fragCoords'] + rect = QtCore.QRectF(y, x, h, w) + self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) + + def setExportMode(self, *args, **kwds): + GraphicsObject.setExportMode(self, *args, **kwds) + self.invalidate() + + def paint(self, p, *args): + #p.setPen(fn.mkPen('r')) + #p.drawRect(self.boundingRect()) + if self._exportOpts is not False: + aa = self._exportOpts.get('antialias', True) + scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed + else: + aa = self.opts['antialias'] + scale = 1.0 + + if self.opts['pxMode'] is True: + atlas = self.fragmentAtlas.getAtlas() + #arr = fn.imageToArray(atlas.toImage(), copy=True) + #if hasattr(self, 'lastAtlas'): + #if np.any(self.lastAtlas != arr): + #print "Atlas changed:", arr + #self.lastAtlas = arr + + if self.fragments is None: + self.updateSpots() + self.generateFragments() + + p.resetTransform() + + if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: + p.drawPixmapFragments(self.fragments, atlas) + else: + p.setRenderHint(p.Antialiasing, aa) + + for i in range(len(self.data)): + rec = self.data[i] + frag = self.fragments[i] + p.resetTransform() + p.translate(frag.x, frag.y) + drawSymbol(p, *self.getSpotOpts(rec, scale)) + else: + if self.picture is None: + self.picture = QtGui.QPicture() + p2 = QtGui.QPainter(self.picture) + for rec in self.data: + if scale != 1.0: + rec = rec.copy() + rec['size'] *= scale + p2.resetTransform() + p2.translate(rec['x'], rec['y']) + drawSymbol(p2, *self.getSpotOpts(rec, scale)) + p2.end() + + self.picture.play(p) + + + def points(self): + for rec in self.data: + if rec['item'] is None: + rec['item'] = SpotItem(rec, self) + return self.data['item'] + + def pointsAt(self, pos): + x = pos.x() + y = pos.y() + pw = self.pixelWidth() + ph = self.pixelHeight() + pts = [] + for s in self.points(): + sp = s.pos() + ss = s.size() + sx = sp.x() + sy = sp.y() + s2x = s2y = ss * 0.5 + if self.opts['pxMode']: + s2x *= pw + s2y *= ph + if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y: + pts.append(s) + #print "HIT:", x, y, sx, sy, s2x, s2y + #else: + #print "No hit:", (x, y), (sx, sy) + #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) + #pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) + return pts[::-1] + + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + pts = self.pointsAt(ev.pos()) + if len(pts) > 0: + self.ptsClicked = pts + self.sigClicked.emit(self, self.ptsClicked) + ev.accept() + else: + #print "no spots" + ev.ignore() + else: + ev.ignore() + + +class SpotItem(object): + """ + Class referring to individual spots in a scatter plot. + These can be retrieved by calling ScatterPlotItem.points() or + by connecting to the ScatterPlotItem's click signals. + """ + + def __init__(self, data, plot): + #GraphicsItem.__init__(self, register=False) + self._data = data + self._plot = plot + #self.setParentItem(plot) + #self.setPos(QtCore.QPointF(data['x'], data['y'])) + #self.updateItem() + + def data(self): + """Return the user data associated with this spot.""" + return self._data['data'] + + def size(self): + """Return the size of this spot. + If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" + if self._data['size'] == -1: + return self._plot.opts['size'] + else: + return self._data['size'] + + def pos(self): + return Point(self._data['x'], self._data['y']) + + def viewPos(self): + return self._plot.mapToView(self.pos()) + + def setSize(self, size): + """Set the size of this spot. + If the size is set to -1, then the ScatterPlotItem's default size + will be used instead.""" + self._data['size'] = size + self.updateItem() + + def symbol(self): + """Return the symbol of this spot. + If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. + """ + symbol = self._data['symbol'] + if symbol is None: + symbol = self._plot.opts['symbol'] + try: + n = int(symbol) + symbol = list(Symbols.keys())[n % len(Symbols)] + except: + pass + return symbol + + def setSymbol(self, symbol): + """Set the symbol for this spot. + If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" + self._data['symbol'] = symbol + self.updateItem() + + def pen(self): + pen = self._data['pen'] + if pen is None: + pen = self._plot.opts['pen'] + return fn.mkPen(pen) + + def setPen(self, *args, **kargs): + """Set the outline pen for this spot""" + pen = fn.mkPen(*args, **kargs) + self._data['pen'] = pen + self.updateItem() + + def resetPen(self): + """Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" + self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) + self.updateItem() + + def brush(self): + brush = self._data['brush'] + if brush is None: + brush = self._plot.opts['brush'] + return fn.mkBrush(brush) + + def setBrush(self, *args, **kargs): + """Set the fill brush for this spot""" + brush = fn.mkBrush(*args, **kargs) + self._data['brush'] = brush + self.updateItem() + + def resetBrush(self): + """Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" + self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) + self.updateItem() + + def setData(self, data): + """Set the user-data associated with this spot""" + self._data['data'] = data + + def updateItem(self): + self._data['fragCoords'] = None + self._plot.updateSpots([self._data]) + self._plot.invalidate() + +#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): + #def __init__(self, data, plot): + #QtGui.QGraphicsPixmapItem.__init__(self) + #self.setFlags(self.flags() | self.ItemIgnoresTransformations) + #SpotItem.__init__(self, data, plot) + + #def setPixmap(self, pixmap): + #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) + #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) + + #def updateItem(self): + #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) + + ### If all symbol options are default, use default pixmap + #if symbolOpts == (None, None, -1, ''): + #pixmap = self._plot.defaultSpotPixmap() + #else: + #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) + #self.setPixmap(pixmap) + + +#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): + #def __init__(self, data, plot): + #QtGui.QGraphicsPathItem.__init__(self) + #SpotItem.__init__(self, data, plot) + + #def updateItem(self): + #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) + #QtGui.QGraphicsPathItem.setPen(self, self.pen()) + #QtGui.QGraphicsPathItem.setBrush(self, self.brush()) + #size = self.size() + #self.resetTransform() + #self.scale(size, size) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py new file mode 100644 index 00000000..b5666f6e --- /dev/null +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -0,0 +1,123 @@ +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +from .UIGraphicsItem import * +import pyqtgraph.functions as fn + +class TextItem(UIGraphicsItem): + """ + GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). + """ + def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): + """ + =========== ================================================================================= + Arguments: + *text* The text to display + *color* The color of the text (any format accepted by pg.mkColor) + *html* If specified, this overrides both *text* and *color* + *anchor* A QPointF or (x,y) sequence indicating what region of the text box will + be anchored to the item's position. A value of (0,0) sets the upper-left corner + of the text box to be at the position specified by setPos(), while a value of (1,1) + sets the lower-right corner. + *border* A pen to use when drawing the border + *fill* A brush to use when filling within the border + =========== ================================================================================= + """ + + ## not working yet + #*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's + #transformation will be ignored) + + self.anchor = pg.Point(anchor) + #self.angle = 0 + UIGraphicsItem.__init__(self) + self.textItem = QtGui.QGraphicsTextItem() + self.textItem.setParentItem(self) + self.lastTransform = None + self._bounds = QtCore.QRectF() + if html is None: + self.setText(text, color) + else: + self.setHtml(html) + self.fill = pg.mkBrush(fill) + self.border = pg.mkPen(border) + self.rotate(angle) + self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport + + def setText(self, text, color=(200,200,200)): + color = pg.mkColor(color) + self.textItem.setDefaultTextColor(color) + self.textItem.setPlainText(text) + self.updateText() + #html = '%s' % (color, text) + #self.setHtml(html) + + def updateAnchor(self): + pass + #self.resetTransform() + #self.translate(0, 20) + + def setPlainText(self, *args): + self.textItem.setPlainText(*args) + self.updateText() + + def setHtml(self, *args): + self.textItem.setHtml(*args) + self.updateText() + + def setTextWidth(self, *args): + self.textItem.setTextWidth(*args) + self.updateText() + + def setFont(self, *args): + self.textItem.setFont(*args) + self.updateText() + + #def setAngle(self, angle): + #self.angle = angle + #self.updateText() + + + def updateText(self): + + ## Needed to maintain font size when rendering to image with increased resolution + self.textItem.resetTransform() + #self.textItem.rotate(self.angle) + if self._exportOpts is not False and 'resolutionScale' in self._exportOpts: + s = self._exportOpts['resolutionScale'] + self.textItem.scale(s, s) + + #br = self.textItem.mapRectToParent(self.textItem.boundingRect()) + self.textItem.setPos(0,0) + br = self.textItem.boundingRect() + apos = self.textItem.mapToParent(pg.Point(br.width()*self.anchor.x(), br.height()*self.anchor.y())) + #print br, apos + self.textItem.setPos(-apos.x(), -apos.y()) + + #def textBoundingRect(self): + ### return the bounds of the text box in device coordinates + #pos = self.mapToDevice(QtCore.QPointF(0,0)) + #if pos is None: + #return None + #tbr = self.textItem.boundingRect() + #return QtCore.QRectF(pos.x() - tbr.width()*self.anchor.x(), pos.y() - tbr.height()*self.anchor.y(), tbr.width(), tbr.height()) + + + def viewRangeChanged(self): + self.updateText() + + def boundingRect(self): + return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect() + + def paint(self, p, *args): + tr = p.transform() + if self.lastTransform is not None: + if tr != self.lastTransform: + self.viewRangeChanged() + self.lastTransform = tr + + p.setPen(self.border) + p.setBrush(self.fill) + p.setRenderHint(p.Antialiasing, True) + p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect())) + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/UIGraphicsItem.py b/pyqtgraph/graphicsItems/UIGraphicsItem.py new file mode 100644 index 00000000..19fda424 --- /dev/null +++ b/pyqtgraph/graphicsItems/UIGraphicsItem.py @@ -0,0 +1,124 @@ +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +import weakref +from .GraphicsObject import GraphicsObject +if not USE_PYSIDE: + import sip + +__all__ = ['UIGraphicsItem'] +class UIGraphicsItem(GraphicsObject): + """ + Base class for graphics items with boundaries relative to a GraphicsView or ViewBox. + The purpose of this class is to allow the creation of GraphicsItems which live inside + a scalable view, but whose boundaries will always stay fixed relative to the view's boundaries. + For example: GridItem, InfiniteLine + + The view can be specified on initialization or it can be automatically detected when the item is painted. + + NOTE: Only the item's boundingRect is affected; the item is not transformed in any way. Use viewRangeChanged + to respond to changes in the view. + """ + + #sigViewChanged = QtCore.Signal(object) ## emitted whenever the viewport coords have changed + + def __init__(self, bounds=None, parent=None): + """ + ============== ============================================================================= + **Arguments:** + bounds QRectF with coordinates relative to view box. The default is QRectF(0,0,1,1), + which means the item will have the same bounds as the view. + ============== ============================================================================= + """ + GraphicsObject.__init__(self, parent) + self.setFlag(self.ItemSendsScenePositionChanges) + + if bounds is None: + self._bounds = QtCore.QRectF(0, 0, 1, 1) + else: + self._bounds = bounds + + self._boundingRect = None + self._updateView() + + def paint(self, *args): + ## check for a new view object every time we paint. + #self.updateView() + pass + + def itemChange(self, change, value): + ret = GraphicsObject.itemChange(self, change, value) + + ## workaround for pyqt bug: + ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html + if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + ret = sip.cast(ret, QtGui.QGraphicsItem) + + if change == self.ItemScenePositionHasChanged: + self.setNewBounds() + return ret + + #def updateView(self): + ### called to see whether this item has a new view to connect to + + ### check for this item's current viewbox or view widget + #view = self.getViewBox() + #if view is None: + ##print " no view" + #return + + #if self._connectedView is not None and view is self._connectedView(): + ##print " already have view", view + #return + + ### disconnect from previous view + #if self._connectedView is not None: + #cv = self._connectedView() + #if cv is not None: + ##print "disconnect:", self + #cv.sigRangeChanged.disconnect(self.viewRangeChanged) + + ### connect to new view + ##print "connect:", self + #view.sigRangeChanged.connect(self.viewRangeChanged) + #self._connectedView = weakref.ref(view) + #self.setNewBounds() + + def boundingRect(self): + if self._boundingRect is None: + br = self.viewRect() + if br is None: + return QtCore.QRectF() + else: + self._boundingRect = br + return QtCore.QRectF(self._boundingRect) + + def dataBounds(self, axis, frac=1.0, orthoRange=None): + """Called by ViewBox for determining the auto-range bounds. + By default, UIGraphicsItems are excluded from autoRange.""" + return None + + def viewRangeChanged(self): + """Called when the view widget/viewbox is resized/rescaled""" + self.setNewBounds() + self.update() + + def setNewBounds(self): + """Update the item's bounding rect to match the viewport""" + self._boundingRect = None ## invalidate bounding rect, regenerate later if needed. + self.prepareGeometryChange() + + + def setPos(self, *args): + GraphicsObject.setPos(self, *args) + self.setNewBounds() + + def mouseShape(self): + """Return the shape of this item after expanding by 2 pixels""" + shape = self.shape() + ds = self.mapToDevice(shape) + stroker = QtGui.QPainterPathStroker() + stroker.setWidh(2) + ds2 = stroker.createStroke(ds).united(ds) + return self.mapFromDevice(ds2) + + + diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py new file mode 100644 index 00000000..85ed596e --- /dev/null +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -0,0 +1,180 @@ +if __name__ == '__main__': + import os, sys + path = os.path.abspath(os.path.dirname(__file__)) + sys.path.insert(0, os.path.join(path, '..', '..')) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +import weakref +from .UIGraphicsItem import UIGraphicsItem + +__all__ = ['VTickGroup'] +class VTickGroup(UIGraphicsItem): + """ + **Bases:** :class:`UIGraphicsItem ` + + Draws a set of tick marks which always occupy the same vertical range of the view, + but have x coordinates relative to the data within the view. + + """ + def __init__(self, xvals=None, yrange=None, pen=None): + """ + ============= =================================================================== + **Arguments** + xvals A list of x values (in data coordinates) at which to draw ticks. + yrange A list of [low, high] limits for the tick. 0 is the bottom of + the view, 1 is the top. [0.8, 1] would draw ticks in the top + fifth of the view. + pen The pen to use for drawing ticks. Default is grey. Can be specified + as any argument valid for :func:`mkPen` + ============= =================================================================== + """ + if yrange is None: + yrange = [0, 1] + if xvals is None: + xvals = [] + + #bounds = QtCore.QRectF(0, yrange[0], 1, yrange[1]-yrange[0]) + UIGraphicsItem.__init__(self)#, bounds=bounds) + + if pen is None: + pen = (200, 200, 200) + + self.path = QtGui.QGraphicsPathItem() + + self.ticks = [] + self.xvals = [] + #if view is None: + #self.view = None + #else: + #self.view = weakref.ref(view) + self.yrange = [0,1] + self.setPen(pen) + self.setYRange(yrange) + self.setXVals(xvals) + #self.valid = False + + def setPen(self, *args, **kwargs): + """Set the pen to use for drawing ticks. Can be specified as any arguments valid + for :func:`mkPen`""" + self.pen = fn.mkPen(*args, **kwargs) + + def setXVals(self, vals): + """Set the x values for the ticks. + + ============= ===================================================================== + **Arguments** + vals A list of x values (in data/plot coordinates) at which to draw ticks. + ============= ===================================================================== + """ + self.xvals = vals + self.rebuildTicks() + #self.valid = False + + def setYRange(self, vals): + """Set the y range [low, high] that the ticks are drawn on. 0 is the bottom of + the view, 1 is the top.""" + self.yrange = vals + #self.relative = relative + #if self.view is not None: + #if relative: + #self.view().sigRangeChanged.connect(self.rescale) + #else: + #try: + #self.view().sigRangeChanged.disconnect(self.rescale) + #except: + #pass + self.rebuildTicks() + #self.valid = False + + def dataBounds(self, *args, **kargs): + return None ## item should never affect view autoscaling + + #def viewRangeChanged(self): + ### called when the view is scaled + + #UIGraphicsItem.viewRangeChanged(self) + + #self.resetTransform() + ##vb = self.view().viewRect() + ##p1 = vb.bottom() - vb.height() * self.yrange[0] + ##p2 = vb.bottom() - vb.height() * self.yrange[1] + + ##br = self.boundingRect() + ##yr = [p1, p2] + + + + ##self.rebuildTicks() + + ##br = self.boundingRect() + ##print br + ##self.translate(0.0, br.y()) + ##self.scale(1.0, br.height()) + ##self.boundingRect() + #self.update() + + #def boundingRect(self): + #print "--request bounds:" + #b = self.path.boundingRect() + #b2 = UIGraphicsItem.boundingRect(self) + #b2.setY(b.y()) + #b2.setWidth(b.width()) + #print " ", b + #print " ", b2 + #print " ", self.mapRectToScene(b) + #return b2 + + def yRange(self): + #if self.relative: + #height = self.view.size().height() + #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) + #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) + #return [p1.y(), p2.y()] + #else: + #return self.yrange + + return self.yrange + + def rebuildTicks(self): + self.path = QtGui.QPainterPath() + yrange = self.yRange() + #print "rebuild ticks:", yrange + for x in self.xvals: + #path.moveTo(x, yrange[0]) + #path.lineTo(x, yrange[1]) + self.path.moveTo(x, 0.) + self.path.lineTo(x, 1.) + #self.setPath(self.path) + #self.valid = True + #self.rescale() + #print " done..", self.boundingRect() + + def paint(self, p, *args): + UIGraphicsItem.paint(self, p, *args) + + br = self.boundingRect() + h = br.height() + br.setY(br.y() + self.yrange[0] * h) + br.setHeight(h - (1.0-self.yrange[1]) * h) + p.translate(0, br.y()) + p.scale(1.0, br.height()) + p.setPen(self.pen) + p.drawPath(self.path) + #QtGui.QGraphicsPathItem.paint(self, *args) + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + import pyqtgraph as pg + vt = VTickGroup([1,3,4,7,9], [0.8, 1.0]) + p = pg.plot() + p.addItem(vt) + + if sys.flags.interactive == 0: + app.exec_() + + + + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py new file mode 100644 index 00000000..8da539fb --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -0,0 +1,1291 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import sortList +import numpy as np +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +from .. ItemGroup import ItemGroup +from .. GraphicsWidget import GraphicsWidget +from pyqtgraph.GraphicsScene import GraphicsScene +import pyqtgraph +import weakref +from copy import deepcopy + +__all__ = ['ViewBox'] + + +class ChildGroup(ItemGroup): + + sigItemsChanged = QtCore.Signal() + + def itemChange(self, change, value): + ret = ItemGroup.itemChange(self, change, value) + if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange: + self.sigItemsChanged.emit() + + return ret + + +class ViewBox(GraphicsWidget): + """ + **Bases:** :class:`GraphicsWidget ` + + Box that allows internal scaling/panning of children by mouse drag. + This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. + + Features: + + - Scaling contents by mouse or auto-scale when contents change + - View linking--multiple views display the same data ranges + - Configurable by context menu + - Item coordinate mapping methods + + Not really compatible with GraphicsView having the same functionality. + """ + + sigYRangeChanged = QtCore.Signal(object, object) + sigXRangeChanged = QtCore.Signal(object, object) + sigRangeChangedManually = QtCore.Signal(object) + sigRangeChanged = QtCore.Signal(object, object) + #sigActionPositionChanged = QtCore.Signal(object) + sigStateChanged = QtCore.Signal(object) + sigTransformChanged = QtCore.Signal(object) + + ## mouse modes + PanMode = 3 + RectMode = 1 + + ## axes + XAxis = 0 + YAxis = 1 + XYAxes = 2 + + ## for linking views together + NamedViews = weakref.WeakValueDictionary() # name: ViewBox + AllViews = weakref.WeakKeyDictionary() # ViewBox: None + + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): + """ + ============= ============================================================= + **Arguments** + *parent* (QGraphicsWidget) Optional parent widget + *border* (QPen) Do draw a border around the view, give any + single argument accepted by :func:`mkPen ` + *lockAspect* (False or float) The aspect ratio to lock the view + coorinates to. (or False to allow the ratio to change) + *enableMouse* (bool) Whether mouse can be used to scale/pan the view + *invertY* (bool) See :func:`invertY ` + ============= ============================================================= + """ + + + + GraphicsWidget.__init__(self, parent) + self.name = None + self.linksBlocked = False + self.addedItems = [] + #self.gView = view + #self.showGrid = showGrid + + self.state = { + + ## separating targetRange and viewRange allows the view to be resized + ## while keeping all previously viewed contents visible + 'targetRange': [[0,1], [0,1]], ## child coord. range visible [[xmin, xmax], [ymin, ymax]] + 'viewRange': [[0,1], [0,1]], ## actual range viewed + + 'yInverted': invertY, + 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. + 'autoRange': [True, True], ## False if auto range is disabled, + ## otherwise float gives the fraction of data that is visible + 'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled + 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot + 'linkedViews': [None, None], ## may be None, "viewName", or weakref.ref(view) + ## a name string indicates that the view *should* link to another, but no view with that name exists yet. + + 'mouseEnabled': [enableMouse, enableMouse], + 'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode, + 'enableMenu': enableMenu, + 'wheelScaleFactor': -1.0 / 8.0, + + 'background': None, + } + self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. + + self.locateGroup = None ## items displayed when using ViewBox.locate(item) + + self.setFlag(self.ItemClipsChildrenToShape) + self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses + + ## childGroup is required so that ViewBox has local coordinates similar to device coordinates. + ## this is a workaround for a Qt + OpenGL bug that causes improper clipping + ## https://bugreports.qt.nokia.com/browse/QTBUG-23723 + self.childGroup = ChildGroup(self) + self.childGroup.sigItemsChanged.connect(self.itemsChanged) + + self.background = QtGui.QGraphicsRectItem(self.rect()) + self.background.setParentItem(self) + self.background.setZValue(-1e6) + self.background.setPen(fn.mkPen(None)) + self.updateBackground() + + #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan + # this also enables capture of keyPressEvents. + + ## Make scale box that is shown when dragging on the view + self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) + self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) + self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) + self.rbScaleBox.hide() + self.addItem(self.rbScaleBox) + + self.axHistory = [] # maintain a history of zoom locations + self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" + + self.setZValue(-100) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) + + self.setAspectLocked(lockAspect) + + self.border = fn.mkPen(border) + self.menu = ViewBoxMenu(self) + + self.register(name) + if name is None: + self.updateViewLists() + + def register(self, name): + """ + Add this ViewBox to the registered list of views. + *name* will appear in the drop-down lists for axis linking in all other views. + The same can be accomplished by initializing the ViewBox with the *name* attribute. + """ + ViewBox.AllViews[self] = None + if self.name is not None: + del ViewBox.NamedViews[self.name] + self.name = name + if name is not None: + ViewBox.NamedViews[name] = self + ViewBox.updateAllViewLists() + sid = id(self) + self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None) + #self.destroyed.connect(self.unregister) + + def unregister(self): + """ + Remove this ViewBox forom the list of linkable views. (see :func:`register() `) + """ + del ViewBox.AllViews[self] + if self.name is not None: + del ViewBox.NamedViews[self.name] + + def close(self): + self.unregister() + + def implements(self, interface): + return interface == 'ViewBox' + + + def getState(self, copy=True): + """Return the current state of the ViewBox. + Linked views are always converted to view names in the returned state.""" + state = self.state.copy() + views = [] + for v in state['linkedViews']: + if isinstance(v, weakref.ref): + v = v() + if v is None or isinstance(v, basestring): + views.append(v) + else: + views.append(v.name) + state['linkedViews'] = views + if copy: + return deepcopy(state) + else: + return state + + def setState(self, state): + """Restore the state of this ViewBox. + (see also getState)""" + state = state.copy() + self.setXLink(state['linkedViews'][0]) + self.setYLink(state['linkedViews'][1]) + del state['linkedViews'] + + self.state.update(state) + self.updateMatrix() + self.sigStateChanged.emit(self) + + + def setMouseMode(self, mode): + """ + Set the mouse interaction mode. *mode* must be either ViewBox.PanMode or ViewBox.RectMode. + In PanMode, the left mouse button pans the view and the right button scales. + In RectMode, the left button draws a rectangle which updates the visible region (this mode is more suitable for single-button mice) + """ + if mode not in [ViewBox.PanMode, ViewBox.RectMode]: + raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode") + self.state['mouseMode'] = mode + self.sigStateChanged.emit(self) + + #def toggleLeftAction(self, act): ## for backward compatibility + #if act.text() is 'pan': + #self.setLeftButtonAction('pan') + #elif act.text() is 'zoom': + #self.setLeftButtonAction('rect') + + def setLeftButtonAction(self, mode='rect'): ## for backward compatibility + if mode.lower() == 'rect': + self.setMouseMode(ViewBox.RectMode) + elif mode.lower() == 'pan': + self.setMouseMode(ViewBox.PanMode) + else: + raise Exception('graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")' % mode) + + def innerSceneItem(self): + return self.childGroup + + def setMouseEnabled(self, x=None, y=None): + """ + Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False. + This allows the user to pan/scale one axis of the view while leaving the other axis unchanged. + """ + if x is not None: + self.state['mouseEnabled'][0] = x + if y is not None: + self.state['mouseEnabled'][1] = y + self.sigStateChanged.emit(self) + + def mouseEnabled(self): + return self.state['mouseEnabled'][:] + + def setMenuEnabled(self, enableMenu=True): + self.state['enableMenu'] = enableMenu + self.sigStateChanged.emit(self) + + def menuEnabled(self): + return self.state.get('enableMenu', True) + + def addItem(self, item, ignoreBounds=False): + """ + Add a QGraphicsItem to this view. The view will include this item when determining how to set its range + automatically unless *ignoreBounds* is True. + """ + if item.zValue() < self.zValue(): + item.setZValue(self.zValue()+1) + item.setParentItem(self.childGroup) + if not ignoreBounds: + self.addedItems.append(item) + self.updateAutoRange() + #print "addItem:", item, item.boundingRect() + + def removeItem(self, item): + """Remove an item from this view.""" + try: + self.addedItems.remove(item) + except: + pass + self.scene().removeItem(item) + self.updateAutoRange() + + def clear(self): + for i in self.addedItems[:]: + self.removeItem(i) + for ch in self.childGroup.childItems(): + ch.setParent(None) + + def resizeEvent(self, ev): + #self.setRange(self.range, padding=0) + #self.updateAutoRange() + self.updateMatrix() + self.sigStateChanged.emit(self) + self.background.setRect(self.rect()) + #self.linkedXChanged() + #self.linkedYChanged() + + def viewRange(self): + """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" + return [x[:] for x in self.state['viewRange']] ## return copy + + def viewRect(self): + """Return a QRectF bounding the region visible within the ViewBox""" + try: + vr0 = self.state['viewRange'][0] + vr1 = self.state['viewRange'][1] + return QtCore.QRectF(vr0[0], vr1[0], vr0[1]-vr0[0], vr1[1] - vr1[0]) + except: + print("make qrectf failed:", self.state['viewRange']) + raise + + def targetRange(self): + return [x[:] for x in self.state['targetRange']] ## return copy + + def targetRect(self): + """ + Return the region which has been requested to be visible. + (this is not necessarily the same as the region that is *actually* visible-- + resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ) + """ + try: + tr0 = self.state['targetRange'][0] + tr1 = self.state['targetRange'][1] + return QtCore.QRectF(tr0[0], tr1[0], tr0[1]-tr0[0], tr1[1] - tr1[0]) + except: + print("make qrectf failed:", self.state['targetRange']) + raise + + def setRange(self, rect=None, xRange=None, yRange=None, padding=0.02, update=True, disableAutoRange=True): + """ + Set the visible range of the ViewBox. + Must specify at least one of *range*, *xRange*, or *yRange*. + + ============= ===================================================================== + **Arguments** + *rect* (QRectF) The full range that should be visible in the view box. + *xRange* (min,max) The range that should be visible along the x-axis. + *yRange* (min,max) The range that should be visible along the y-axis. + *padding* (float) Expand the view by a fraction of the requested range. + By default, this value is 0.02 (2%) + ============= ===================================================================== + + """ + + changes = {} + + if rect is not None: + changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]} + if xRange is not None: + changes[0] = xRange + if yRange is not None: + changes[1] = yRange + + if len(changes) == 0: + print(rect) + raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) + + changed = [False, False] + for ax, range in changes.items(): + mn = min(range) + mx = max(range) + if mn == mx: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale. + dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0] + if dy == 0: + dy = 1 + mn -= dy*0.5 + mx += dy*0.5 + padding = 0.0 + if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): + raise Exception("Not setting range [%s, %s]" % (str(mn), str(mx))) + + p = (mx-mn) * padding + mn -= p + mx += p + + if self.state['targetRange'][ax] != [mn, mx]: + self.state['targetRange'][ax] = [mn, mx] + changed[ax] = True + + if any(changed) and disableAutoRange: + if all(changed): + ax = ViewBox.XYAxes + elif changed[0]: + ax = ViewBox.XAxis + elif changed[1]: + ax = ViewBox.YAxis + self.enableAutoRange(ax, False) + + + self.sigStateChanged.emit(self) + + if update: + self.updateMatrix(changed) + + for ax, range in changes.items(): + link = self.linkedView(ax) + if link is not None: + link.linkedViewChanged(self, ax) + + if changed[0] and self.state['autoVisibleOnly'][1]: + self.updateAutoRange() + elif changed[1] and self.state['autoVisibleOnly'][0]: + self.updateAutoRange() + + def setYRange(self, min, max, padding=0.02, update=True): + """ + Set the visible Y range of the view to [*min*, *max*]. + The *padding* argument causes the range to be set larger by the fraction specified. + """ + self.setRange(yRange=[min, max], update=update, padding=padding) + + def setXRange(self, min, max, padding=0.02, update=True): + """ + Set the visible X range of the view to [*min*, *max*]. + The *padding* argument causes the range to be set larger by the fraction specified. + """ + self.setRange(xRange=[min, max], update=update, padding=padding) + + def autoRange(self, padding=0.02, item=None): + """ + Set the range of the view box to make all children visible. + Note that this is not the same as enableAutoRange, which causes the view to + automatically auto-range whenever its contents are changed. + """ + if item is None: + bounds = self.childrenBoundingRect() + else: + bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect() + + if bounds is not None: + self.setRange(bounds, padding=padding) + + + def scaleBy(self, s, center=None): + """ + Scale by *s* around given center point (or center of view). + *s* may be a Point or tuple (x, y) + """ + scale = Point(s) + if self.state['aspectLocked'] is not False: + scale[0] = self.state['aspectLocked'] * scale[1] + + vr = self.targetRect() + if center is None: + center = Point(vr.center()) + else: + center = Point(center) + tl = center + (vr.topLeft()-center) * scale + br = center + (vr.bottomRight()-center) * scale + self.setRange(QtCore.QRectF(tl, br), padding=0) + + def translateBy(self, t): + """ + Translate the view by *t*, which may be a Point or tuple (x, y). + """ + t = Point(t) + #if viewCoords: ## scale from pixels + #o = self.mapToView(Point(0,0)) + #t = self.mapToView(t) - o + + vr = self.targetRect() + self.setRange(vr.translated(t), padding=0) + + def enableAutoRange(self, axis=None, enable=True): + """ + Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both + (if *axis* is omitted, both axes will be changed). + When enabled, the axis will automatically rescale when items are added/removed or change their shape. + The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should + be visible (this only works with items implementing a dataRange method, such as PlotDataItem). + """ + #print "autorange:", axis, enable + #if not enable: + #import traceback + #traceback.print_stack() + + if enable is True: + enable = 1.0 + + if axis is None: + axis = ViewBox.XYAxes + + if axis == ViewBox.XYAxes or axis == 'xy': + self.state['autoRange'][0] = enable + self.state['autoRange'][1] = enable + elif axis == ViewBox.XAxis or axis == 'x': + self.state['autoRange'][0] = enable + elif axis == ViewBox.YAxis or axis == 'y': + self.state['autoRange'][1] = enable + else: + raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.') + + if enable: + self.updateAutoRange() + self.sigStateChanged.emit(self) + + def disableAutoRange(self, axis=None): + """Disables auto-range. (See enableAutoRange)""" + self.enableAutoRange(axis, enable=False) + + def autoRangeEnabled(self): + return self.state['autoRange'][:] + + def setAutoPan(self, x=None, y=None): + if x is not None: + self.state['autoPan'][0] = x + if y is not None: + self.state['autoPan'][1] = y + if None not in [x,y]: + self.updateAutoRange() + + def setAutoVisible(self, x=None, y=None): + if x is not None: + self.state['autoVisibleOnly'][0] = x + if x is True: + self.state['autoVisibleOnly'][1] = False + if y is not None: + self.state['autoVisibleOnly'][1] = y + if y is True: + self.state['autoVisibleOnly'][0] = False + + if x is not None or y is not None: + self.updateAutoRange() + + def updateAutoRange(self): + ## Break recursive loops when auto-ranging. + ## This is needed because some items change their size in response + ## to a view change. + if self._updatingRange: + return + + self._updatingRange = True + try: + targetRect = self.viewRange() + if not any(self.state['autoRange']): + return + + fractionVisible = self.state['autoRange'][:] + for i in [0,1]: + if type(fractionVisible[i]) is bool: + fractionVisible[i] = 1.0 + + childRange = None + + order = [0,1] + if self.state['autoVisibleOnly'][0] is True: + order = [1,0] + + args = {} + for ax in order: + if self.state['autoRange'][ax] is False: + continue + if self.state['autoVisibleOnly'][ax]: + oRange = [None, None] + oRange[ax] = targetRect[1-ax] + childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange) + + else: + if childRange is None: + childRange = self.childrenBounds(frac=fractionVisible) + + ## Make corrections to range + xr = childRange[ax] + if xr is not None: + if self.state['autoPan'][ax]: + x = sum(xr) * 0.5 + #x = childRect.center().x() + w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2. + #childRect.setLeft(x-w2) + #childRect.setRight(x+w2) + childRange[ax] = [x-w2, x+w2] + else: + #wp = childRect.width() * 0.02 + wp = (xr[1] - xr[0]) * 0.02 + #childRect = childRect.adjusted(-wp, 0, wp, 0) + childRange[ax][0] -= wp + childRange[ax][1] += wp + #targetRect[ax][0] = childRect.left() + #targetRect[ax][1] = childRect.right() + targetRect[ax] = childRange[ax] + args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] + #else: + ### Make corrections to Y range + #if self.state['autoPan'][1]: + #y = childRect.center().y() + #h2 = (targetRect[1][1]-targetRect[1][0]) / 2. + #childRect.setTop(y-h2) + #childRect.setBottom(y+h2) + #else: + #hp = childRect.height() * 0.02 + #childRect = childRect.adjusted(0, -hp, 0, hp) + + #targetRect[1][0] = childRect.top() + #targetRect[1][1] = childRect.bottom() + #args['yRange'] = targetRect[1] + if len(args) == 0: + return + args['padding'] = 0 + args['disableAutoRange'] = False + #self.setRange(xRange=targetRect[0], yRange=targetRect[1], padding=0, disableAutoRange=False) + self.setRange(**args) + finally: + self._updatingRange = False + + def setXLink(self, view): + """Link this view's X axis to another view. (see LinkView)""" + self.linkView(self.XAxis, view) + + def setYLink(self, view): + """Link this view's Y axis to another view. (see LinkView)""" + self.linkView(self.YAxis, view) + + + def linkView(self, axis, view): + """ + Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. + If view is None, the axis is left unlinked. + """ + if isinstance(view, basestring): + if view == '': + view = None + else: + view = ViewBox.NamedViews.get(view, view) ## convert view name to ViewBox if possible + + if hasattr(view, 'implements') and view.implements('ViewBoxWrapper'): + view = view.getViewBox() + + ## used to connect/disconnect signals between a pair of views + if axis == ViewBox.XAxis: + signal = 'sigXRangeChanged' + slot = self.linkedXChanged + else: + signal = 'sigYRangeChanged' + slot = self.linkedYChanged + + + oldLink = self.linkedView(axis) + if oldLink is not None: + try: + getattr(oldLink, signal).disconnect(slot) + except TypeError: + ## This can occur if the view has been deleted already + pass + + + if view is None or isinstance(view, basestring): + self.state['linkedViews'][axis] = view + else: + self.state['linkedViews'][axis] = weakref.ref(view) + getattr(view, signal).connect(slot) + if view.autoRangeEnabled()[axis] is not False: + self.enableAutoRange(axis, False) + slot() + else: + if self.autoRangeEnabled()[axis] is False: + slot() + + self.sigStateChanged.emit(self) + + def blockLink(self, b): + self.linksBlocked = b ## prevents recursive plot-change propagation + + def linkedXChanged(self): + ## called when x range of linked view has changed + view = self.linkedView(0) + self.linkedViewChanged(view, ViewBox.XAxis) + + def linkedYChanged(self): + ## called when y range of linked view has changed + view = self.linkedView(1) + self.linkedViewChanged(view, ViewBox.YAxis) + + def linkedView(self, ax): + ## Return the linked view for axis *ax*. + ## this method _always_ returns either a ViewBox or None. + v = self.state['linkedViews'][ax] + if v is None or isinstance(v, basestring): + return None + else: + return v() ## dereference weakref pointer. If the reference is dead, this returns None + + def linkedViewChanged(self, view, axis): + if self.linksBlocked or view is None: + return + + vr = view.viewRect() + vg = view.screenGeometry() + sg = self.screenGeometry() + if vg is None or sg is None: + return + + view.blockLink(True) + try: + if axis == ViewBox.XAxis: + overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left()) + if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, + ## then just replicate the view + x1 = vr.left() + x2 = vr.right() + else: ## views overlap; line them up + upp = float(vr.width()) / vg.width() + x1 = vr.left() + (sg.x()-vg.x()) * upp + x2 = x1 + sg.width() * upp + self.enableAutoRange(ViewBox.XAxis, False) + self.setXRange(x1, x2, padding=0) + else: + overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top()) + if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, + ## then just replicate the view + y1 = vr.top() + y2 = vr.bottom() + else: ## views overlap; line them up + upp = float(vr.height()) / vg.height() + y2 = vr.bottom() - (sg.y()-vg.y()) * upp + y1 = y2 - sg.height() * upp + self.enableAutoRange(ViewBox.YAxis, False) + self.setYRange(y1, y2, padding=0) + finally: + view.blockLink(False) + + + def screenGeometry(self): + """return the screen geometry of the viewbox""" + v = self.getViewWidget() + if v is None: + return None + b = self.sceneBoundingRect() + wr = v.mapFromScene(b).boundingRect() + pos = v.mapToGlobal(v.pos()) + wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) + return wr + + + + def itemsChanged(self): + ## called when items are added/removed from self.childGroup + self.updateAutoRange() + + def itemBoundsChanged(self, item): + self.updateAutoRange() + + def invertY(self, b=True): + """ + By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. + """ + self.state['yInverted'] = b + self.updateMatrix(changed=(False, True)) + self.sigStateChanged.emit(self) + + def yInverted(self): + return self.state['yInverted'] + + def setAspectLocked(self, lock=True, ratio=1): + """ + If the aspect ratio is locked, view scaling must always preserve the aspect ratio. + By default, the ratio is set to 1; x and y both have the same scaling. + This ratio can be overridden (width/height), or use None to lock in the current ratio. + """ + if not lock: + self.state['aspectLocked'] = False + else: + vr = self.viewRect() + currentRatio = vr.width() / vr.height() + if ratio is None: + ratio = currentRatio + self.state['aspectLocked'] = ratio + if ratio != currentRatio: ## If this would change the current range, do that now + #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1]) + self.updateMatrix() + self.sigStateChanged.emit(self) + + def childTransform(self): + """ + Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. + (This maps from inside the viewbox to outside) + """ + m = self.childGroup.transform() + #m1 = QtGui.QTransform() + #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) + return m #*m1 + + def mapToView(self, obj): + """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" + m = fn.invertQTransform(self.childTransform()) + return m.map(obj) + + def mapFromView(self, obj): + """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox""" + m = self.childTransform() + return m.map(obj) + + def mapSceneToView(self, obj): + """Maps from scene coordinates to the coordinate system displayed inside the ViewBox""" + return self.mapToView(self.mapFromScene(obj)) + + def mapViewToScene(self, obj): + """Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" + return self.mapToScene(self.mapFromView(obj)) + + def mapFromItemToView(self, item, obj): + """Maps *obj* from the local coordinate system of *item* to the view coordinates""" + return self.childGroup.mapFromItem(item, obj) + #return self.mapSceneToView(item.mapToScene(obj)) + + def mapFromViewToItem(self, item, obj): + """Maps *obj* from view coordinates to the local coordinate system of *item*.""" + return self.childGroup.mapToItem(item, obj) + #return item.mapFromScene(self.mapViewToScene(obj)) + + def mapViewToDevice(self, obj): + return self.mapToDevice(self.mapFromView(obj)) + + def mapDeviceToView(self, obj): + return self.mapToView(self.mapFromDevice(obj)) + + def viewPixelSize(self): + """Return the (width, height) of a screen pixel in view coordinates.""" + o = self.mapToView(Point(0,0)) + px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] + return (px.length(), py.length()) + + + def itemBoundingRect(self, item): + """Return the bounding rect of the item in view coordinates""" + return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() + + #def viewScale(self): + #vr = self.viewRect() + ##print "viewScale:", self.range + #xd = vr.width() + #yd = vr.height() + #if xd == 0 or yd == 0: + #print "Warning: 0 range in view:", xd, yd + #return np.array([1,1]) + + ##cs = self.canvas().size() + #cs = self.boundingRect() + #scale = np.array([cs.width() / xd, cs.height() / yd]) + ##print "view scale:", scale + #return scale + + def wheelEvent(self, ev, axis=None): + mask = np.array(self.state['mouseEnabled'], dtype=np.float) + if axis is not None and axis >= 0 and axis < len(mask): + mv = mask[axis] + mask[:] = 0 + mask[axis] = mv + s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor + + center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) + #center = ev.pos() + + self.scaleBy(s, center) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + ev.accept() + + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): + ev.accept() + self.raiseContextMenu(ev) + + def raiseContextMenu(self, ev): + #print "viewbox.raiseContextMenu called." + + #menu = self.getMenu(ev) + menu = self.getMenu(ev) + self.scene().addParentContextMenus(self, menu, ev) + #print "2:", [str(a.text()) for a in self.menu.actions()] + pos = ev.screenPos() + #pos2 = ev.scenePos() + #print "3:", [str(a.text()) for a in self.menu.actions()] + #self.sigActionPositionChanged.emit(pos2) + + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + #print "4:", [str(a.text()) for a in self.menu.actions()] + + def getMenu(self, ev): + self._menuCopy = self.menu.copy() ## temporary storage to prevent menu disappearing + return self._menuCopy + + def getContextMenus(self, event): + if self.menuEnabled(): + return self.menu.subMenus() + else: + return None + #return [self.getMenu(event)] + + + def mouseDragEvent(self, ev, axis=None): + ## if axis is specified, event will only affect that axis. + ev.accept() ## we accept all buttons + + pos = ev.pos() + lastPos = ev.lastPos() + dif = pos - lastPos + dif = dif * -1 + + ## Ignore axes if mouse is disabled + mask = np.array(self.state['mouseEnabled'], dtype=np.float) + if axis is not None: + mask[1-axis] = 0.0 + + ## Scale or translate based on mouse button + if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): + if self.state['mouseMode'] == ViewBox.RectMode: + if ev.isFinish(): ## This is the final move in the drag; change the view scale now + #print "finish" + self.rbScaleBox.hide() + #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos)) + ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos)) + ax = self.childGroup.mapRectFromParent(ax) + self.showAxRect(ax) + self.axHistoryPointer += 1 + self.axHistory = self.axHistory[:self.axHistoryPointer] + [ax] + else: + ## update shape of scale box + self.updateScaleBox(ev.buttonDownPos(), ev.pos()) + else: + tr = dif*mask + tr = self.mapToView(tr) - self.mapToView(Point(0,0)) + self.translateBy(tr) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + elif ev.button() & QtCore.Qt.RightButton: + #print "vb.rightDrag" + if self.state['aspectLocked'] is not False: + mask[0] = 0 + + dif = ev.screenPos() - ev.lastScreenPos() + dif = np.array([dif.x(), dif.y()]) + dif[0] *= -1 + s = ((mask * 0.02) + 1) ** dif + + tr = self.childGroup.transform() + tr = fn.invertQTransform(tr) + + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) + self.scaleBy(s, center) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + + def keyPressEvent(self, ev): + """ + This routine should capture key presses in the current view box. + Key presses are used only when mouse mode is RectMode + The following events are implemented: + ctrl-A : zooms out to the default "full" view of the plot + ctrl-+ : moves forward in the zooming stack (if it exists) + ctrl-- : moves backward in the zooming stack (if it exists) + + """ + #print ev.key() + #print 'I intercepted a key press, but did not accept it' + + ## not implemented yet ? + #self.keypress.sigkeyPressEvent.emit() + + ev.accept() + if ev.text() == '-': + self.scaleHistory(-1) + elif ev.text() in ['+', '=']: + self.scaleHistory(1) + elif ev.key() == QtCore.Qt.Key_Backspace: + self.scaleHistory(len(self.axHistory)) + else: + ev.ignore() + + def scaleHistory(self, d): + ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d)) + if ptr != self.axHistoryPointer: + self.axHistoryPointer = ptr + self.showAxRect(self.axHistory[ptr]) + + + def updateScaleBox(self, p1, p2): + r = QtCore.QRectF(p1, p2) + r = self.childGroup.mapRectFromParent(r) + self.rbScaleBox.setPos(r.topLeft()) + self.rbScaleBox.resetTransform() + self.rbScaleBox.scale(r.width(), r.height()) + self.rbScaleBox.show() + + def showAxRect(self, ax): + self.setRange(ax.normalized()) # be sure w, h are correct coordinates + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + + #def mouseRect(self): + #vs = self.viewScale() + #vr = self.state['viewRange'] + ## Convert positions from screen (view) pixel coordinates to axis coordinates + #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]), + #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1]) + #return(ax) + + def allChildren(self, item=None): + """Return a list of all children and grandchildren of this ViewBox""" + if item is None: + item = self.childGroup + + children = [item] + for ch in item.childItems(): + children.extend(self.allChildren(ch)) + return children + + + + def childrenBounds(self, frac=None, orthoRange=(None,None)): + """Return the bounding range of all children. + [[xmin, xmax], [ymin, ymax]] + Values may be None if there are no specific bounds for an axis. + """ + + #items = self.allChildren() + items = self.addedItems + + #if item is None: + ##print "children bounding rect:" + #item = self.childGroup + + range = [None, None] + + for item in items: + if not item.isVisible(): + continue + + #print "=========", item + useX = True + useY = True + if hasattr(item, 'dataBounds'): + if frac is None: + frac = (1.0, 1.0) + xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) + yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) + #print " xr:", xr, " yr:", yr + if xr is None or xr == (None, None): + useX = False + xr = (0,0) + if yr is None or yr == (None, None): + useY = False + yr = (0,0) + + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) + #print " xr:", xr, " yr:", yr + #print " item real:", bounds + else: + if int(item.flags() & item.ItemHasNoContents) > 0: + continue + #print " empty" + else: + bounds = item.boundingRect() + #bounds = [[item.left(), item.top()], [item.right(), item.bottom()]] + #print " item:", bounds + #bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0]) + bounds = self.mapFromItemToView(item, bounds).boundingRect() + #print " ", bounds + + #print " useX:", useX, " useY:", useY + if not any([useX, useY]): + continue + + if useX != useY: ## != means xor + ang = item.transformAngle() + if ang == 0 or ang == 180: + pass + elif ang == 90 or ang == 270: + useX, useY = useY, useX + else: + continue ## need to check for item rotations and decide how best to apply this boundary. + + #print " useX:", useX, " useY:", useY + + #print " range:", range + #print " bounds (r,l,t,b):", bounds.right(), bounds.left(), bounds.top(), bounds.bottom() + + if useY: + if range[1] is not None: + range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])] + else: + range[1] = [bounds.top(), bounds.bottom()] + if useX: + if range[0] is not None: + range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])] + else: + range[0] = [bounds.left(), bounds.right()] + + #print " range:", range + + return range + + def childrenBoundingRect(self, *args, **kwds): + range = self.childrenBounds(*args, **kwds) + tr = self.targetRange() + if range[0] is None: + range[0] = tr[0] + if range[1] is None: + range[1] = tr[1] + + bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0]) + return bounds + + + + def updateMatrix(self, changed=None): + if changed is None: + changed = [False, False] + changed = list(changed) + #print "udpateMatrix:" + #print " range:", self.range + tr = self.targetRect() + bounds = self.rect() #boundingRect() + #print bounds + + ## set viewRect, given targetRect and possibly aspect ratio constraint + if self.state['aspectLocked'] is False or bounds.height() == 0: + self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + else: + viewRatio = bounds.width() / bounds.height() + targetRatio = self.state['aspectLocked'] * tr.width() / tr.height() + if targetRatio > viewRatio: + ## target is wider than view + dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height()) + if dy != 0: + changed[1] = True + self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]] + else: + dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width()) + if dx != 0: + changed[0] = True + self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]] + + vr = self.viewRect() + #print " bounds:", bounds + if vr.height() == 0 or vr.width() == 0: + return + scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) + if not self.state['yInverted']: + scale = scale * Point(1, -1) + m = QtGui.QTransform() + + ## First center the viewport at 0 + center = bounds.center() + m.translate(center.x(), center.y()) + + ## Now scale and translate properly + m.scale(scale[0], scale[1]) + st = Point(vr.center()) + m.translate(-st[0], -st[1]) + + self.childGroup.setTransform(m) + + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + if any(changed): + self.sigRangeChanged.emit(self, self.state['viewRange']) + + self.sigTransformChanged.emit(self) ## segfaults here: 1 + + def paint(self, p, opt, widget): + if self.border is not None: + bounds = self.shape() + p.setPen(self.border) + #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) + p.drawPath(bounds) + + def updateBackground(self): + bg = self.state['background'] + if bg is None: + self.background.hide() + else: + self.background.show() + self.background.setBrush(fn.mkBrush(bg)) + + + def updateViewLists(self): + try: + self.window() + except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. + return + + def cmpViews(a, b): + wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) + alpha = cmp(a.name, b.name) + return wins + alpha + + ## make a sorted list of all named views + nv = list(ViewBox.NamedViews.values()) + #print "new view list:", nv + sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList + + if self in nv: + nv.remove(self) + + self.menu.setViewList(nv) + + for ax in [0,1]: + link = self.state['linkedViews'][ax] + if isinstance(link, basestring): ## axis has not been linked yet; see if it's possible now + for v in nv: + if link == v.name: + self.linkView(ax, v) + #print "New view list:", nv + #print "linked views:", self.state['linkedViews'] + + @staticmethod + def updateAllViewLists(): + #print "Update:", ViewBox.AllViews.keys() + #print "Update:", ViewBox.NamedViews.keys() + for v in ViewBox.AllViews: + v.updateViewLists() + + + @staticmethod + def forgetView(vid, name): + if ViewBox is None: ## can happen as python is shutting down + return + ## Called with ID and name of view (the view itself is no longer available) + for v in ViewBox.AllViews.keys(): + if id(v) == vid: + ViewBox.AllViews.pop(v) + break + ViewBox.NamedViews.pop(name, None) + ViewBox.updateAllViewLists() + + @staticmethod + def quit(): + ## called when the application is about to exit. + ## this disables all callbacks, which might otherwise generate errors if invoked during exit. + for k in ViewBox.AllViews: + try: + k.destroyed.disconnect() + except RuntimeError: ## signal is already disconnected. + pass + + def locate(self, item, timeout=3.0, children=False): + """ + Temporarily display the bounding rect of an item and lines connecting to the center of the view. + This is useful for determining the location of items that may be out of the range of the ViewBox. + if allChildren is True, then the bounding rect of all item's children will be shown instead. + """ + self.clearLocate() + + if item.scene() is not self.scene(): + raise Exception("Item does not share a scene with this ViewBox.") + + c = self.viewRect().center() + if children: + br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect() + else: + br = self.mapFromItemToView(item, item.boundingRect()).boundingRect() + + g = ItemGroup() + g.setParentItem(self.childGroup) + self.locateGroup = g + g.box = QtGui.QGraphicsRectItem(br) + g.box.setParentItem(g) + g.lines = [] + for p in (br.topLeft(), br.bottomLeft(), br.bottomRight(), br.topRight()): + line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y()) + line.setParentItem(g) + g.lines.append(line) + + for item in g.childItems(): + item.setPen(fn.mkPen(color='y', width=3)) + g.setZValue(1000000) + + if children: + g.path = QtGui.QGraphicsPathItem(g.childrenShape()) + else: + g.path = QtGui.QGraphicsPathItem(g.shape()) + g.path.setParentItem(g) + g.path.setPen(fn.mkPen('g')) + g.path.setZValue(100) + + QtCore.QTimer.singleShot(timeout*1000, self.clearLocate) + + def clearLocate(self): + if self.locateGroup is None: + return + self.scene().removeItem(self.locateGroup) + self.locateGroup = None + + +from .ViewBoxMenu import ViewBoxMenu diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py new file mode 100644 index 00000000..bbb40efc --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -0,0 +1,268 @@ +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE +from pyqtgraph.python2_3 import asUnicode +from pyqtgraph.WidgetGroup import WidgetGroup + +if USE_PYSIDE: + from .axisCtrlTemplate_pyside import Ui_Form as AxisCtrlTemplate +else: + from .axisCtrlTemplate_pyqt import Ui_Form as AxisCtrlTemplate + +import weakref + +class ViewBoxMenu(QtGui.QMenu): + def __init__(self, view): + QtGui.QMenu.__init__(self) + + self.view = weakref.ref(view) ## keep weakref to view to avoid circular reference (don't know why, but this prevents the ViewBox from being collected) + self.valid = False ## tells us whether the ui needs to be updated + self.viewMap = weakref.WeakValueDictionary() ## weakrefs to all views listed in the link combos + + self.setTitle("ViewBox options") + self.viewAll = QtGui.QAction("View All", self) + self.viewAll.triggered.connect(self.autoRange) + self.addAction(self.viewAll) + + self.axes = [] + self.ctrl = [] + self.widgetGroups = [] + self.dv = QtGui.QDoubleValidator(self) + for axis in 'XY': + m = QtGui.QMenu() + m.setTitle("%s Axis" % axis) + w = QtGui.QWidget() + ui = AxisCtrlTemplate() + ui.setupUi(w) + a = QtGui.QWidgetAction(self) + a.setDefaultWidget(w) + m.addAction(a) + self.addMenu(m) + self.axes.append(m) + self.ctrl.append(ui) + wg = WidgetGroup(w) + self.widgetGroups.append(w) + + connects = [ + (ui.mouseCheck.toggled, 'MouseToggled'), + (ui.manualRadio.clicked, 'ManualClicked'), + (ui.minText.editingFinished, 'MinTextChanged'), + (ui.maxText.editingFinished, 'MaxTextChanged'), + (ui.autoRadio.clicked, 'AutoClicked'), + (ui.autoPercentSpin.valueChanged, 'AutoSpinChanged'), + (ui.linkCombo.currentIndexChanged, 'LinkComboChanged'), + (ui.autoPanCheck.toggled, 'AutoPanToggled'), + (ui.visibleOnlyCheck.toggled, 'VisibleOnlyToggled') + ] + + for sig, fn in connects: + sig.connect(getattr(self, axis.lower()+fn)) + + self.ctrl[0].invertCheck.hide() ## no invert for x-axis + self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled) + ## exporting is handled by GraphicsScene now + #self.export = QtGui.QMenu("Export") + #self.setExportMethods(view.exportMethods) + #self.addMenu(self.export) + + self.leftMenu = QtGui.QMenu("Mouse Mode") + group = QtGui.QActionGroup(self) + pan = self.leftMenu.addAction("3 button", self.set3ButtonMode) + zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode) + pan.setCheckable(True) + zoom.setCheckable(True) + pan.setActionGroup(group) + zoom.setActionGroup(group) + self.mouseModes = [pan, zoom] + self.addMenu(self.leftMenu) + + self.view().sigStateChanged.connect(self.viewStateChanged) + + self.updateState() + + def copy(self): + m = QtGui.QMenu() + for sm in self.subMenus(): + if isinstance(sm, QtGui.QMenu): + m.addMenu(sm) + else: + m.addAction(sm) + m.setTitle(self.title()) + return m + + def subMenus(self): + if not self.valid: + self.updateState() + return [self.viewAll] + self.axes + [self.leftMenu] + + + def setExportMethods(self, methods): + self.exportMethods = methods + self.export.clear() + for opt, fn in methods.items(): + self.export.addAction(opt, self.exportMethod) + + + def viewStateChanged(self): + self.valid = False + if self.ctrl[0].minText.isVisible() or self.ctrl[1].minText.isVisible(): + self.updateState() + + def updateState(self): + ## Something about the viewbox has changed; update the menu GUI + + state = self.view().getState(copy=False) + if state['mouseMode'] == ViewBox.PanMode: + self.mouseModes[0].setChecked(True) + else: + self.mouseModes[1].setChecked(True) + + for i in [0,1]: # x, y + tr = state['targetRange'][i] + self.ctrl[i].minText.setText("%0.5g" % tr[0]) + self.ctrl[i].maxText.setText("%0.5g" % tr[1]) + if state['autoRange'][i] is not False: + self.ctrl[i].autoRadio.setChecked(True) + if state['autoRange'][i] is not True: + self.ctrl[i].autoPercentSpin.setValue(state['autoRange'][i]*100) + else: + self.ctrl[i].manualRadio.setChecked(True) + self.ctrl[i].mouseCheck.setChecked(state['mouseEnabled'][i]) + + ## Update combo to show currently linked view + c = self.ctrl[i].linkCombo + c.blockSignals(True) + try: + view = state['linkedViews'][i] ## will always be string or None + if view is None: + view = '' + + ind = c.findText(view) + + if ind == -1: + ind = 0 + c.setCurrentIndex(ind) + finally: + c.blockSignals(False) + + self.ctrl[i].autoPanCheck.setChecked(state['autoPan'][i]) + self.ctrl[i].visibleOnlyCheck.setChecked(state['autoVisibleOnly'][i]) + + self.ctrl[1].invertCheck.setChecked(state['yInverted']) + self.valid = True + + + def autoRange(self): + self.view().autoRange() ## don't let signal call this directly--it'll add an unwanted argument + + def xMouseToggled(self, b): + self.view().setMouseEnabled(x=b) + + def xManualClicked(self): + self.view().enableAutoRange(ViewBox.XAxis, False) + + def xMinTextChanged(self): + self.ctrl[0].manualRadio.setChecked(True) + self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + + def xMaxTextChanged(self): + self.ctrl[0].manualRadio.setChecked(True) + self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + + def xAutoClicked(self): + val = self.ctrl[0].autoPercentSpin.value() * 0.01 + self.view().enableAutoRange(ViewBox.XAxis, val) + + def xAutoSpinChanged(self, val): + self.ctrl[0].autoRadio.setChecked(True) + self.view().enableAutoRange(ViewBox.XAxis, val*0.01) + + def xLinkComboChanged(self, ind): + self.view().setXLink(str(self.ctrl[0].linkCombo.currentText())) + + def xAutoPanToggled(self, b): + self.view().setAutoPan(x=b) + + def xVisibleOnlyToggled(self, b): + self.view().setAutoVisible(x=b) + + + def yMouseToggled(self, b): + self.view().setMouseEnabled(y=b) + + def yManualClicked(self): + self.view().enableAutoRange(ViewBox.YAxis, False) + + def yMinTextChanged(self): + self.ctrl[1].manualRadio.setChecked(True) + self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + + def yMaxTextChanged(self): + self.ctrl[1].manualRadio.setChecked(True) + self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + + def yAutoClicked(self): + val = self.ctrl[1].autoPercentSpin.value() * 0.01 + self.view().enableAutoRange(ViewBox.YAxis, val) + + def yAutoSpinChanged(self, val): + self.ctrl[1].autoRadio.setChecked(True) + self.view().enableAutoRange(ViewBox.YAxis, val*0.01) + + def yLinkComboChanged(self, ind): + self.view().setYLink(str(self.ctrl[1].linkCombo.currentText())) + + def yAutoPanToggled(self, b): + self.view().setAutoPan(y=b) + + def yVisibleOnlyToggled(self, b): + self.view().setAutoVisible(y=b) + + def yInvertToggled(self, b): + self.view().invertY(b) + + + def exportMethod(self): + act = self.sender() + self.exportMethods[str(act.text())]() + + + def set3ButtonMode(self): + self.view().setLeftButtonAction('pan') + + def set1ButtonMode(self): + self.view().setLeftButtonAction('rect') + + + def setViewList(self, views): + names = [''] + self.viewMap.clear() + + ## generate list of views to show in the link combo + for v in views: + name = v.name + if name is None: ## unnamed views do not show up in the view list (although they are linkable) + continue + names.append(name) + self.viewMap[name] = v + + for i in [0,1]: + c = self.ctrl[i].linkCombo + current = asUnicode(c.currentText()) + c.blockSignals(True) + changed = True + try: + c.clear() + for name in names: + c.addItem(name) + if name == current: + changed = False + c.setCurrentIndex(c.count()-1) + finally: + c.blockSignals(False) + + if changed: + c.setCurrentIndex(0) + c.currentIndexChanged.emit(c.currentIndex()) + +from .ViewBox import ViewBox + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/ViewBox/__init__.py b/pyqtgraph/graphicsItems/ViewBox/__init__.py new file mode 100644 index 00000000..685a314d --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/__init__.py @@ -0,0 +1 @@ +from .ViewBox import ViewBox diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui new file mode 100644 index 00000000..297fce75 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui @@ -0,0 +1,161 @@ + + + Form + + + + 0 + 0 + 186 + 154 + + + + + 200 + 16777215 + + + + Form + + + + 0 + + + 0 + + + + + Link Axis: + + + + + + + <html><head/><body><p>Links this axis with another view. When linked, both views will display the same data range.</p></body></html> + + + QComboBox::AdjustToContents + + + + + + + true + + + <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> + + + % + + + 1 + + + 100 + + + 1 + + + 100 + + + + + + + <html><head/><body><p>Automatically resize this axis whenever the displayed data is changed.</p></body></html> + + + Auto + + + true + + + + + + + <html><head/><body><p>Set the range for this axis manually. This disables automatic scaling. </p></body></html> + + + Manual + + + + + + + <html><head/><body><p>Minimum value to display for this axis.</p></body></html> + + + 0 + + + + + + + <html><head/><body><p>Maximum value to display for this axis.</p></body></html> + + + 0 + + + + + + + <html><head/><body><p>Inverts the display of this axis. (+y points downward instead of upward)</p></body></html> + + + Invert Axis + + + + + + + <html><head/><body><p>Enables mouse interaction (panning, scaling) for this axis.</p></body></html> + + + Mouse Enabled + + + true + + + + + + + <html><head/><body><p>When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.</p></body></html> + + + Visible Data Only + + + + + + + <html><head/><body><p>When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.</p></body></html> + + + Auto Pan Only + + + + + + + + diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py new file mode 100644 index 00000000..db14033e --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './graphicsItems/ViewBox/axisCtrlTemplate.ui' +# +# Created: Sun Sep 9 14:41:31 2012 +# by: PyQt4 UI code generator 4.9.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(186, 154) + Form.setMaximumSize(QtCore.QSize(200, 16777215)) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.label = QtGui.QLabel(Form) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 7, 0, 1, 2) + self.linkCombo = QtGui.QComboBox(Form) + self.linkCombo.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.linkCombo.setObjectName(_fromUtf8("linkCombo")) + self.gridLayout.addWidget(self.linkCombo, 7, 2, 1, 2) + self.autoPercentSpin = QtGui.QSpinBox(Form) + self.autoPercentSpin.setEnabled(True) + self.autoPercentSpin.setMinimum(1) + self.autoPercentSpin.setMaximum(100) + self.autoPercentSpin.setSingleStep(1) + self.autoPercentSpin.setProperty("value", 100) + self.autoPercentSpin.setObjectName(_fromUtf8("autoPercentSpin")) + self.gridLayout.addWidget(self.autoPercentSpin, 2, 2, 1, 2) + self.autoRadio = QtGui.QRadioButton(Form) + self.autoRadio.setChecked(True) + self.autoRadio.setObjectName(_fromUtf8("autoRadio")) + self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 2) + self.manualRadio = QtGui.QRadioButton(Form) + self.manualRadio.setObjectName(_fromUtf8("manualRadio")) + self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 2) + self.minText = QtGui.QLineEdit(Form) + self.minText.setObjectName(_fromUtf8("minText")) + self.gridLayout.addWidget(self.minText, 1, 2, 1, 1) + self.maxText = QtGui.QLineEdit(Form) + self.maxText.setObjectName(_fromUtf8("maxText")) + self.gridLayout.addWidget(self.maxText, 1, 3, 1, 1) + self.invertCheck = QtGui.QCheckBox(Form) + self.invertCheck.setObjectName(_fromUtf8("invertCheck")) + self.gridLayout.addWidget(self.invertCheck, 5, 0, 1, 4) + self.mouseCheck = QtGui.QCheckBox(Form) + self.mouseCheck.setChecked(True) + self.mouseCheck.setObjectName(_fromUtf8("mouseCheck")) + self.gridLayout.addWidget(self.mouseCheck, 6, 0, 1, 4) + self.visibleOnlyCheck = QtGui.QCheckBox(Form) + self.visibleOnlyCheck.setObjectName(_fromUtf8("visibleOnlyCheck")) + self.gridLayout.addWidget(self.visibleOnlyCheck, 3, 2, 1, 2) + self.autoPanCheck = QtGui.QCheckBox(Form) + self.autoPanCheck.setObjectName(_fromUtf8("autoPanCheck")) + self.gridLayout.addWidget(self.autoPanCheck, 4, 2, 1, 2) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) + self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "

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

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

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

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRadio.setToolTip(QtGui.QApplication.translate("Form", "

Automatically resize this axis whenever the displayed data is changed.

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.manualRadio.setToolTip(QtGui.QApplication.translate("Form", "

Set the range for this axis manually. This disables automatic scaling.

", None, QtGui.QApplication.UnicodeUTF8)) + self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.minText.setToolTip(QtGui.QApplication.translate("Form", "

Minimum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.maxText.setToolTip(QtGui.QApplication.translate("Form", "

Maximum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.invertCheck.setToolTip(QtGui.QApplication.translate("Form", "

Inverts the display of this axis. (+y points downward instead of upward)

", None, QtGui.QApplication.UnicodeUTF8)) + self.invertCheck.setText(QtGui.QApplication.translate("Form", "Invert Axis", None, QtGui.QApplication.UnicodeUTF8)) + self.mouseCheck.setToolTip(QtGui.QApplication.translate("Form", "

Enables mouse interaction (panning, scaling) for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) + self.visibleOnlyCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.visibleOnlyCheck.setText(QtGui.QApplication.translate("Form", "Visible Data Only", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPanCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py new file mode 100644 index 00000000..18510bc2 --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './graphicsItems/ViewBox/axisCtrlTemplate.ui' +# +# Created: Sun Sep 9 14:41:32 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(186, 154) + Form.setMaximumSize(QtCore.QSize(200, 16777215)) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtGui.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 7, 0, 1, 2) + self.linkCombo = QtGui.QComboBox(Form) + self.linkCombo.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.linkCombo.setObjectName("linkCombo") + self.gridLayout.addWidget(self.linkCombo, 7, 2, 1, 2) + self.autoPercentSpin = QtGui.QSpinBox(Form) + self.autoPercentSpin.setEnabled(True) + self.autoPercentSpin.setMinimum(1) + self.autoPercentSpin.setMaximum(100) + self.autoPercentSpin.setSingleStep(1) + self.autoPercentSpin.setProperty("value", 100) + self.autoPercentSpin.setObjectName("autoPercentSpin") + self.gridLayout.addWidget(self.autoPercentSpin, 2, 2, 1, 2) + self.autoRadio = QtGui.QRadioButton(Form) + self.autoRadio.setChecked(True) + self.autoRadio.setObjectName("autoRadio") + self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 2) + self.manualRadio = QtGui.QRadioButton(Form) + self.manualRadio.setObjectName("manualRadio") + self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 2) + self.minText = QtGui.QLineEdit(Form) + self.minText.setObjectName("minText") + self.gridLayout.addWidget(self.minText, 1, 2, 1, 1) + self.maxText = QtGui.QLineEdit(Form) + self.maxText.setObjectName("maxText") + self.gridLayout.addWidget(self.maxText, 1, 3, 1, 1) + self.invertCheck = QtGui.QCheckBox(Form) + self.invertCheck.setObjectName("invertCheck") + self.gridLayout.addWidget(self.invertCheck, 5, 0, 1, 4) + self.mouseCheck = QtGui.QCheckBox(Form) + self.mouseCheck.setChecked(True) + self.mouseCheck.setObjectName("mouseCheck") + self.gridLayout.addWidget(self.mouseCheck, 6, 0, 1, 4) + self.visibleOnlyCheck = QtGui.QCheckBox(Form) + self.visibleOnlyCheck.setObjectName("visibleOnlyCheck") + self.gridLayout.addWidget(self.visibleOnlyCheck, 3, 2, 1, 2) + self.autoPanCheck = QtGui.QCheckBox(Form) + self.autoPanCheck.setObjectName("autoPanCheck") + self.gridLayout.addWidget(self.autoPanCheck, 4, 2, 1, 2) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) + self.linkCombo.setToolTip(QtGui.QApplication.translate("Form", "

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

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

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

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRadio.setToolTip(QtGui.QApplication.translate("Form", "

Automatically resize this axis whenever the displayed data is changed.

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.manualRadio.setToolTip(QtGui.QApplication.translate("Form", "

Set the range for this axis manually. This disables automatic scaling.

", None, QtGui.QApplication.UnicodeUTF8)) + self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.minText.setToolTip(QtGui.QApplication.translate("Form", "

Minimum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.maxText.setToolTip(QtGui.QApplication.translate("Form", "

Maximum value to display for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.invertCheck.setToolTip(QtGui.QApplication.translate("Form", "

Inverts the display of this axis. (+y points downward instead of upward)

", None, QtGui.QApplication.UnicodeUTF8)) + self.invertCheck.setText(QtGui.QApplication.translate("Form", "Invert Axis", None, QtGui.QApplication.UnicodeUTF8)) + self.mouseCheck.setToolTip(QtGui.QApplication.translate("Form", "

Enables mouse interaction (panning, scaling) for this axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) + self.visibleOnlyCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.

", None, QtGui.QApplication.UnicodeUTF8)) + self.visibleOnlyCheck.setText(QtGui.QApplication.translate("Form", "Visible Data Only", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPanCheck.setToolTip(QtGui.QApplication.translate("Form", "

When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.

", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/pyqtgraph/graphicsItems/__init__.py b/pyqtgraph/graphicsItems/__init__.py new file mode 100644 index 00000000..8e411816 --- /dev/null +++ b/pyqtgraph/graphicsItems/__init__.py @@ -0,0 +1,21 @@ +### just import everything from sub-modules + +#import os + +#d = os.path.split(__file__)[0] +#files = [] +#for f in os.listdir(d): + #if os.path.isdir(os.path.join(d, f)): + #files.append(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.append(f[:-3]) + +#for modName in files: + #mod = __import__(modName, globals(), locals(), fromlist=['*']) + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + ##print modName, k + #globals()[k] = getattr(mod, k) diff --git a/graphicsWindows.py b/pyqtgraph/graphicsWindows.py similarity index 50% rename from graphicsWindows.py rename to pyqtgraph/graphicsWindows.py index 8b8e8678..6e7d6305 100644 --- a/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -5,9 +5,11 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from PyQt4 import QtCore, QtGui -from PlotWidget import * -from ImageView import * +from .Qt import QtCore, QtGui +from .widgets.PlotWidget import * +from .imageview import * +from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget +from .widgets.GraphicsView import GraphicsView QAPP = None def mkQApp(): @@ -15,54 +17,17 @@ def mkQApp(): global QAPP QAPP = QtGui.QApplication([]) -class GraphicsLayoutWidget(GraphicsView): - def __init__(self): - GraphicsView.__init__(self) - self.items = {} - self.currentRow = 0 - self.currentCol = 0 - - def nextRow(self): - """Advance to next row for automatic item placement""" - self.currentRow += 1 - self.currentCol = 0 - - def nextCol(self, colspan=1): - """Advance to next column, while returning the current column number - (generally only for internal use)""" - self.currentCol += colspan - return self.currentCol-colspan - - def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): - plot = PlotItem(**kargs) - self.addItem(plot, row, col, rowspan, colspan) - return plot - - def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): - if row not in self.items: - self.items[row] = {} - self.items[row][col] = item - - if row is None: - row = self.currentRow - if col is None: - col = self.nextCol(colspan) - self.centralLayout.addItem(item, row, col, rowspan, colspan) - - def getItem(self, row, col): - return self.items[row][col] - class GraphicsWindow(GraphicsLayoutWidget): - def __init__(self, title=None, size=(800,600)): + def __init__(self, title=None, size=(800,600), **kargs): mkQApp() - self.win = QtGui.QMainWindow() - GraphicsLayoutWidget.__init__(self) - self.win.setCentralWidget(self) - self.win.resize(*size) + #self.win = QtGui.QMainWindow() + GraphicsLayoutWidget.__init__(self, **kargs) + #self.win.setCentralWidget(self) + self.resize(*size) if title is not None: - self.win.setWindowTitle(title) - self.win.show() + self.setWindowTitle(title) + self.show() class TabWindow(QtGui.QMainWindow): @@ -83,19 +48,6 @@ class TabWindow(QtGui.QMainWindow): raise NameError(attr) -#class PlotWindow(QtGui.QMainWindow): - #def __init__(self, title=None, **kargs): - #mkQApp() - #QtGui.QMainWindow.__init__(self) - #self.cw = PlotWidget(**kargs) - #self.setCentralWidget(self.cw) - #for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear', 'viewRect']: - #setattr(self, m, getattr(self.cw, m)) - #if title is not None: - #self.setWindowTitle(title) - #self.show() - - class PlotWindow(PlotWidget): def __init__(self, title=None, **kargs): mkQApp() diff --git a/ImageView.py b/pyqtgraph/imageview/ImageView.py similarity index 62% rename from ImageView.py rename to pyqtgraph/imageview/ImageView.py index 3c293964..5c6573e3 100644 --- a/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,81 +12,135 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ +from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE -from ImageViewTemplate import * -from graphicsItems import * -from widgets import ROI -from PyQt4 import QtCore, QtGui +if USE_PYSIDE: + from .ImageViewTemplate_pyside import * +else: + from .ImageViewTemplate_pyqt import * + +from pyqtgraph.graphicsItems.ImageItem import * +from pyqtgraph.graphicsItems.ROI import * +from pyqtgraph.graphicsItems.LinearRegionItem import * +from pyqtgraph.graphicsItems.InfiniteLine import * +from pyqtgraph.graphicsItems.ViewBox import * +#from widgets import ROI import sys #from numpy import ndarray -import ptime +import pyqtgraph.ptime as ptime import numpy as np -import debug +import pyqtgraph.debug as debug -from SignalProxy import proxyConnect +from pyqtgraph.SignalProxy import SignalProxy + +#try: + #import pyqtgraph.metaarray as metaarray + #HAVE_METAARRAY = True +#except: + #HAVE_METAARRAY = False + class PlotROI(ROI): def __init__(self, size): - ROI.__init__(self, pos=[0,0], size=size, scaleSnap=True, translateSnap=True) + ROI.__init__(self, pos=[0,0], size=size) #, scaleSnap=True, translateSnap=True) self.addScaleHandle([1, 1], [0, 0]) + self.addRotateHandle([0, 0], [0.5, 0.5]) class ImageView(QtGui.QWidget): + """ + Widget used for display and analysis of image data. + Implements many features: + * Displays 2D and 3D image data. For 3D data, a z-axis + slider is displayed allowing the user to select which frame is displayed. + * Displays histogram of image data with movable region defining the dark/light levels + * Editable gradient provides a color lookup table + * Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end. + * Basic analysis features including: + + * ROI and embedded plot for measuring image values across frames + * Image normalization / background subtraction + + Basic Usage:: + + imv = pg.ImageView() + imv.show() + imv.setImage(data) + """ sigTimeChanged = QtCore.Signal(object, object) + sigProcessingChanged = QtCore.Signal(object) - def __init__(self, parent=None, name="ImageView", *args): + def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): + """ + By default, this class creates an :class:`ImageItem ` to display image data + and a :class:`ViewBox ` to contain the ImageItem. Custom items may be given instead + by specifying the *view* and/or *imageItem* arguments. + """ QtGui.QWidget.__init__(self, parent, *args) self.levelMax = 4096 self.levelMin = 0 self.name = name self.image = None + self.axes = {} self.imageDisp = None self.ui = Ui_Form() self.ui.setupUi(self) - self.scene = self.ui.graphicsView.sceneObj + self.scene = self.ui.graphicsView.scene() self.ignoreTimeLine = False - if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux. - self.ui.graphicsView.setViewport(QtGui.QWidget()) + #if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux. + # self.ui.graphicsView.setViewport(QtGui.QWidget()) - self.ui.graphicsView.enableMouse(True) - self.ui.graphicsView.autoPixelRange = False - self.ui.graphicsView.setAspectLocked(True) + #self.ui.graphicsView.enableMouse(True) + #self.ui.graphicsView.autoPixelRange = False + #self.ui.graphicsView.setAspectLocked(True) #self.ui.graphicsView.invertY() - self.ui.graphicsView.enableMouse() + #self.ui.graphicsView.enableMouse() + if view is None: + self.view = ViewBox() + else: + self.view = view + self.ui.graphicsView.setCentralItem(self.view) + self.view.setAspectLocked(True) + self.view.invertY() - self.ticks = [t[0] for t in self.ui.gradientWidget.listTicks()] - self.ticks[0].colorChangeAllowed = False - self.ticks[1].colorChangeAllowed = False - self.ui.gradientWidget.allowAdd = False - self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255)) - self.ui.gradientWidget.setOrientation('right') + #self.ticks = [t[0] for t in self.ui.gradientWidget.listTicks()] + #self.ticks[0].colorChangeAllowed = False + #self.ticks[1].colorChangeAllowed = False + #self.ui.gradientWidget.allowAdd = False + #self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255)) + #self.ui.gradientWidget.setOrientation('right') - self.imageItem = ImageItem() - self.scene.addItem(self.imageItem) + if imageItem is None: + self.imageItem = ImageItem() + else: + self.imageItem = imageItem + self.view.addItem(self.imageItem) self.currentIndex = 0 + self.ui.histogram.setImageItem(self.imageItem) + self.ui.normGroup.hide() self.roi = PlotROI(10) self.roi.setZValue(20) - self.scene.addItem(self.roi) + self.view.addItem(self.roi) self.roi.hide() self.normRoi = PlotROI(10) self.normRoi.setPen(QtGui.QPen(QtGui.QColor(255,255,0))) self.normRoi.setZValue(20) - self.scene.addItem(self.normRoi) + self.view.addItem(self.normRoi) self.normRoi.hide() #self.ui.roiPlot.hide() self.roiCurve = self.ui.roiPlot.plot() - self.timeLine = InfiniteLine(self.ui.roiPlot, 0, movable=True) + self.timeLine = InfiniteLine(0, movable=True) self.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200))) self.timeLine.setZValue(1) self.ui.roiPlot.addItem(self.timeLine) self.ui.splitter.setSizes([self.height()-35, 35]) - self.ui.roiPlot.showScale('left', False) + self.ui.roiPlot.hideAxis('left') self.keysPressed = {} self.playTimer = QtCore.QTimer() @@ -100,250 +154,65 @@ class ImageView(QtGui.QWidget): #self.ui.roiPlot.addItem(l) #self.normLines.append(l) #l.hide() - self.normRgn = LinearRegionItem(self.ui.roiPlot, 'vertical') + self.normRgn = LinearRegionItem() self.normRgn.setZValue(0) self.ui.roiPlot.addItem(self.normRgn) self.normRgn.hide() - ## wrap functions from graphics view + ## wrap functions from view box for fn in ['addItem', 'removeItem']: - setattr(self, fn, getattr(self.ui.graphicsView, fn)) + setattr(self, fn, getattr(self.view, fn)) + + ## wrap functions from histogram + for fn in ['setHistogramRange', 'autoHistogramRange', 'getLookupTable', 'getLevels']: + setattr(self, fn, getattr(self.ui.histogram, fn)) - #QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged) - #self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged) self.timeLine.sigPositionChanged.connect(self.timeLineChanged) - #QtCore.QObject.connect(self.ui.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) - #QtCore.QObject.connect(self.ui.blackSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage) - #QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage) - self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage) - #QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked) + #self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage) self.ui.roiBtn.clicked.connect(self.roiClicked) - #self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged) self.roi.sigRegionChanged.connect(self.roiChanged) - #QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled) self.ui.normBtn.toggled.connect(self.normToggled) - #QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - self.ui.normDivideRadio.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - self.ui.normSubtractRadio.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.ui.normOffRadio, QtCore.SIGNAL('clicked()'), self.updateNorm) - self.ui.normOffRadio.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.ui.normROICheck, QtCore.SIGNAL('clicked()'), self.updateNorm) + self.ui.normDivideRadio.clicked.connect(self.normRadioChanged) + self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged) + self.ui.normOffRadio.clicked.connect(self.normRadioChanged) self.ui.normROICheck.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.ui.normFrameCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) self.ui.normFrameCheck.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.ui.normTimeRangeCheck, QtCore.SIGNAL('clicked()'), self.updateNorm) self.ui.normTimeRangeCheck.clicked.connect(self.updateNorm) - #QtCore.QObject.connect(self.playTimer, QtCore.SIGNAL('timeout()'), self.timeout) self.playTimer.timeout.connect(self.timeout) - ##QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) - #QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm) - self.normProxy = proxyConnect(None, self.normRgn.sigRegionChanged, self.updateNorm) - #self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm) + self.normProxy = SignalProxy(self.normRgn.sigRegionChanged, slot=self.updateNorm) self.normRoi.sigRegionChangeFinished.connect(self.updateNorm) self.ui.roiPlot.registerPlot(self.name + '_ROI') self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] + + + self.roiClicked() ## initialize roi plot to correct shape / visibility - #def __dtor__(self): - ##print "Called ImageView sip destructor" - #self.quit() - #QtGui.QWidget.__dtor__(self) + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None): + """ + Set the image to be displayed in the widget. - def close(self): - self.ui.roiPlot.close() - self.ui.graphicsView.close() - self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) - self.scene.clear() - del self.image - del self.imageDisp - #self.image = None - #self.imageDisp = None - self.setParent(None) - - def keyPressEvent(self, ev): - if ev.key() == QtCore.Qt.Key_Space: - if self.playRate == 0: - fps = (self.getProcessedImage().shape[0]-1) / (self.tVals[-1] - self.tVals[0]) - self.play(fps) - #print fps - else: - self.play(0) - ev.accept() - elif ev.key() == QtCore.Qt.Key_Home: - self.setCurrentIndex(0) - self.play(0) - ev.accept() - elif ev.key() == QtCore.Qt.Key_End: - self.setCurrentIndex(self.getProcessedImage().shape[0]-1) - self.play(0) - ev.accept() - elif ev.key() in self.noRepeatKeys: - ev.accept() - if ev.isAutoRepeat(): - return - self.keysPressed[ev.key()] = 1 - self.evalKeyState() - else: - QtGui.QWidget.keyPressEvent(self, ev) - - def keyReleaseEvent(self, ev): - if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]: - ev.accept() - elif ev.key() in self.noRepeatKeys: - ev.accept() - if ev.isAutoRepeat(): - return - try: - del self.keysPressed[ev.key()] - except: - self.keysPressed = {} - self.evalKeyState() - else: - QtGui.QWidget.keyReleaseEvent(self, ev) - - - def evalKeyState(self): - if len(self.keysPressed) == 1: - key = self.keysPressed.keys()[0] - if key == QtCore.Qt.Key_Right: - self.play(20) - self.jumpFrames(1) - self.lastPlayTime = ptime.time() + 0.2 ## 2ms wait before start - ## This happens *after* jumpFrames, since it might take longer than 2ms - elif key == QtCore.Qt.Key_Left: - self.play(-20) - self.jumpFrames(-1) - self.lastPlayTime = ptime.time() + 0.2 - elif key == QtCore.Qt.Key_Up: - self.play(-100) - elif key == QtCore.Qt.Key_Down: - self.play(100) - elif key == QtCore.Qt.Key_PageUp: - self.play(-1000) - elif key == QtCore.Qt.Key_PageDown: - self.play(1000) - else: - self.play(0) - - def play(self, rate): - #print "play:", rate - self.playRate = rate - if rate == 0: - self.playTimer.stop() - return - - self.lastPlayTime = ptime.time() - if not self.playTimer.isActive(): - self.playTimer.start(16) - - - def timeout(self): - now = ptime.time() - dt = now - self.lastPlayTime - if dt < 0: - return - n = int(self.playRate * dt) - #print n, dt - if n != 0: - #print n, dt, self.lastPlayTime - self.lastPlayTime += (float(n)/self.playRate) - if self.currentIndex+n > self.image.shape[0]: - self.play(0) - self.jumpFrames(n) - - def setCurrentIndex(self, ind): - self.currentIndex = clip(ind, 0, self.getProcessedImage().shape[0]-1) - self.updateImage() - self.ignoreTimeLine = True - self.timeLine.setValue(self.tVals[self.currentIndex]) - self.ignoreTimeLine = False - - def jumpFrames(self, n): - """If this is a video, move ahead n frames""" - if self.axes['t'] is not None: - self.setCurrentIndex(self.currentIndex + n) - - def updateNorm(self): - #for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]): - #if self.ui.normTimeRangeCheck.isChecked(): - #l.show() - #else: - #l.hide() - - #i, t = self.timeIndex(sl) - #l.setPos(t) - - if self.ui.normTimeRangeCheck.isChecked(): - #print "show!" - self.normRgn.show() - else: - self.normRgn.hide() - - if self.ui.normROICheck.isChecked(): - #print "show!" - self.normRoi.show() - else: - self.normRoi.hide() - - self.imageDisp = None - self.updateImage() - self.roiChanged() - - def normToggled(self, b): - self.ui.normGroup.setVisible(b) - self.normRoi.setVisible(b and self.ui.normROICheck.isChecked()) - self.normRgn.setVisible(b and self.ui.normTimeRangeCheck.isChecked()) - - def roiClicked(self): - if self.ui.roiBtn.isChecked(): - self.roi.show() - #self.ui.roiPlot.show() - self.ui.roiPlot.setMouseEnabled(True, True) - self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) - self.roiCurve.show() - self.roiChanged() - self.ui.roiPlot.showScale('left', True) - else: - self.roi.hide() - self.ui.roiPlot.setMouseEnabled(False, False) - self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) - self.ui.splitter.setSizes([self.height()-35, 35]) - self.roiCurve.hide() - self.ui.roiPlot.showScale('left', False) - - def roiChanged(self): - if self.image is None: - return - - image = self.getProcessedImage() - if image.ndim == 2: - axes = (0, 1) - elif image.ndim == 3: - axes = (1, 2) - else: - return - data = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes) - if data is not None: - while data.ndim > 1: - data = data.mean(axis=1) - self.roiCurve.setData(y=data, x=self.tVals) - #self.ui.roiPlot.replot() - - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): - """Set the image to be displayed in the widget. - Options are: - img: ndarray; the image to be displayed. - autoRange: bool; whether to scale/pan the view to fit the image. - autoLevels: bool; whether to update the white/black levels to fit the image. - levels: (min, max); the white and black level values to use. - axes: {'t':0, 'x':1, 'y':2, 'c':3}; Dictionary indicating the interpretation for each axis. - This is only needed to override the default guess. + ============== ======================================================================= + **Arguments:** + *img* (numpy array) the image to be displayed. + *xvals* (numpy array) 1D array of z-axis values corresponding to the third axis + in a 3D image. For video, this array should contain the time of each frame. + *autoRange* (bool) whether to scale/pan the view to fit the image. + *autoLevels* (bool) whether to update the white/black levels to fit the image. + *levels* (min, max); the white and black level values to use. + *axes* Dictionary indicating the interpretation for each axis. + This is only needed to override the default guess. Format is:: + + {'t':0, 'x':1, 'y':2, 'c':3}; + ============== ======================================================================= """ prof = debug.Profiler('ImageView.setImage', disabled=True) + if hasattr(img, 'implements') and img.implements('MetaArray'): + img = img.asarray() + if not isinstance(img, np.ndarray): raise Exception("Image must be specified as ndarray.") self.image = img @@ -390,15 +259,16 @@ class ImageView(QtGui.QWidget): self.imageDisp = None + prof.mark('3') + + self.currentIndex = 0 + self.updateImage() if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. self.levelMax = levels[1] self.levelMin = levels[0] - prof.mark('3') - self.currentIndex = 0 - self.updateImage() if self.ui.roiBtn.isChecked(): self.roiChanged() prof.mark('4') @@ -429,6 +299,8 @@ class ImageView(QtGui.QWidget): self.imageItem.scale(*scale) if pos is not None: self.imageItem.setPos(*pos) + if transform is not None: + self.imageItem.setTransform(transform) prof.mark('6') if autoRange: @@ -436,30 +308,259 @@ class ImageView(QtGui.QWidget): self.roiClicked() prof.mark('7') prof.finish() + + + def play(self, rate): + """Begin automatically stepping frames forward at the given rate (in fps). + This can also be accessed by pressing the spacebar.""" + #print "play:", rate + self.playRate = rate + if rate == 0: + self.playTimer.stop() + return + self.lastPlayTime = ptime.time() + if not self.playTimer.isActive(): + self.playTimer.start(16) + + + def autoLevels(self): - image = self.getProcessedImage() + """Set the min/max levels automatically to match the image data.""" + #image = self.getProcessedImage() + self.setLevels(self.levelMin, self.levelMax) - #self.ui.whiteSlider.setValue(self.ui.whiteSlider.maximum()) - #self.ui.blackSlider.setValue(0) - - self.ui.gradientWidget.setTickValue(self.ticks[0], 0.0) - self.ui.gradientWidget.setTickValue(self.ticks[1], 1.0) - self.imageItem.setLevels(white=self.whiteLevel(), black=self.blackLevel()) + #self.ui.histogram.imageChanged(autoLevel=True) + def setLevels(self, min, max): + """Set the min/max (bright and dark) levels.""" + self.ui.histogram.setLevels(min, max) + def autoRange(self): + """Auto scale and pan the view around the image.""" image = self.getProcessedImage() #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.ui.graphicsView.setRange(self.imageItem.sceneBoundingRect(), padding=0., lockAspect=True) + self.view.autoRange() ##setRange(self.imageItem.viewBoundingRect(), padding=0.) def getProcessedImage(self): + """Returns the image data after it has been processed by any normalization options in use.""" if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image - self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) + self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp))) + self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) + return self.imageDisp + + + 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.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) + self.scene.clear() + del self.image + del self.imageDisp + self.setParent(None) + + def keyPressEvent(self, ev): + #print ev.key() + if ev.key() == QtCore.Qt.Key_Space: + if self.playRate == 0: + fps = (self.getProcessedImage().shape[0]-1) / (self.tVals[-1] - self.tVals[0]) + self.play(fps) + #print fps + else: + self.play(0) + ev.accept() + elif ev.key() == QtCore.Qt.Key_Home: + self.setCurrentIndex(0) + self.play(0) + ev.accept() + elif ev.key() == QtCore.Qt.Key_End: + self.setCurrentIndex(self.getProcessedImage().shape[0]-1) + self.play(0) + ev.accept() + elif ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + self.keysPressed[ev.key()] = 1 + self.evalKeyState() + else: + QtGui.QWidget.keyPressEvent(self, ev) + + def keyReleaseEvent(self, ev): + if ev.key() in [QtCore.Qt.Key_Space, QtCore.Qt.Key_Home, QtCore.Qt.Key_End]: + ev.accept() + elif ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + try: + del self.keysPressed[ev.key()] + except: + self.keysPressed = {} + self.evalKeyState() + else: + QtGui.QWidget.keyReleaseEvent(self, ev) + + + def evalKeyState(self): + if len(self.keysPressed) == 1: + key = list(self.keysPressed.keys())[0] + if key == QtCore.Qt.Key_Right: + self.play(20) + self.jumpFrames(1) + self.lastPlayTime = ptime.time() + 0.2 ## 2ms wait before start + ## This happens *after* jumpFrames, since it might take longer than 2ms + elif key == QtCore.Qt.Key_Left: + self.play(-20) + self.jumpFrames(-1) + self.lastPlayTime = ptime.time() + 0.2 + elif key == QtCore.Qt.Key_Up: + self.play(-100) + elif key == QtCore.Qt.Key_Down: + self.play(100) + elif key == QtCore.Qt.Key_PageUp: + self.play(-1000) + elif key == QtCore.Qt.Key_PageDown: + self.play(1000) + else: + self.play(0) + + + def timeout(self): + now = ptime.time() + dt = now - self.lastPlayTime + if dt < 0: + return + n = int(self.playRate * dt) + #print n, dt + if n != 0: + #print n, dt, self.lastPlayTime + self.lastPlayTime += (float(n)/self.playRate) + if self.currentIndex+n > self.image.shape[0]: + self.play(0) + self.jumpFrames(n) + + def setCurrentIndex(self, ind): + """Set the currently displayed frame index.""" + self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) + self.updateImage() + self.ignoreTimeLine = True + self.timeLine.setValue(self.tVals[self.currentIndex]) + self.ignoreTimeLine = False + + def jumpFrames(self, n): + """Move video frame ahead n frames (may be negative)""" + if self.axes['t'] is not None: + self.setCurrentIndex(self.currentIndex + n) + + def normRadioChanged(self): + self.imageDisp = None + self.updateImage() + self.roiChanged() + self.sigProcessingChanged.emit(self) + + + def updateNorm(self): + #for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]): + #if self.ui.normTimeRangeCheck.isChecked(): + #l.show() + #else: + #l.hide() + + #i, t = self.timeIndex(sl) + #l.setPos(t) + + if self.ui.normTimeRangeCheck.isChecked(): + #print "show!" + self.normRgn.show() + else: + self.normRgn.hide() + + if self.ui.normROICheck.isChecked(): + #print "show!" + self.normRoi.show() + else: + self.normRoi.hide() + + if not self.ui.normOffRadio.isChecked(): + self.imageDisp = None + self.updateImage() + self.roiChanged() + self.sigProcessingChanged.emit(self) + + def normToggled(self, b): + self.ui.normGroup.setVisible(b) + self.normRoi.setVisible(b and self.ui.normROICheck.isChecked()) + self.normRgn.setVisible(b and self.ui.normTimeRangeCheck.isChecked()) + + def hasTimeAxis(self): + return 't' in self.axes and self.axes['t'] is not None + + def roiClicked(self): + showRoiPlot = False + if self.ui.roiBtn.isChecked(): + showRoiPlot = True + self.roi.show() + #self.ui.roiPlot.show() + self.ui.roiPlot.setMouseEnabled(True, True) + self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) + self.roiCurve.show() + self.roiChanged() + self.ui.roiPlot.showAxis('left') + else: + self.roi.hide() + self.ui.roiPlot.setMouseEnabled(False, False) + self.roiCurve.hide() + self.ui.roiPlot.hideAxis('left') + + if self.hasTimeAxis(): + showRoiPlot = True + mn = self.tVals.min() + mx = self.tVals.max() + self.ui.roiPlot.setXRange(mn, mx, padding=0.01) + self.timeLine.show() + self.timeLine.setBounds([mn, mx]) + self.ui.roiPlot.show() + if not self.ui.roiBtn.isChecked(): + self.ui.splitter.setSizes([self.height()-35, 35]) + else: + self.timeLine.hide() + #self.ui.roiPlot.hide() + + self.ui.roiPlot.setVisible(showRoiPlot) + + def roiChanged(self): + if self.image is None: + return + + image = self.getProcessedImage() + if image.ndim == 2: + axes = (0, 1) + elif image.ndim == 3: + axes = (1, 2) + else: + return + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) + if data is not None: + while data.ndim > 1: + data = data.mean(axis=1) + if image.ndim == 3: + self.roiCurve.setData(y=data, x=self.tVals) + else: + while coords.ndim > 2: + coords = coords[:,:,0] + coords = coords - coords[:,0,np.newaxis] + xvals = (coords**2).sum(axis=0) ** 0.5 + self.roiCurve.setData(y=data, x=xvals) + + #self.ui.roiPlot.replot() + @staticmethod def quickMinMax(data): @@ -536,18 +637,18 @@ class ImageView(QtGui.QWidget): #print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel() if self.axes['t'] is None: #self.ui.timeSlider.hide() - self.imageItem.updateImage(image, white=self.whiteLevel(), black=self.blackLevel()) - self.ui.roiPlot.hide() - self.ui.roiBtn.hide() + self.imageItem.updateImage(image) + #self.ui.roiPlot.hide() + #self.ui.roiBtn.hide() else: - self.ui.roiBtn.show() + #self.ui.roiBtn.show() self.ui.roiPlot.show() #self.ui.timeSlider.show() - self.imageItem.updateImage(image[self.currentIndex], white=self.whiteLevel(), black=self.blackLevel()) + self.imageItem.updateImage(image[self.currentIndex]) def timeIndex(self, slider): - """Return the time and frame index indicated by a slider""" + ## Return the time and frame index indicated by a slider if self.image is None: return (0,0) #v = slider.value() @@ -574,11 +675,26 @@ class ImageView(QtGui.QWidget): #print ind return ind, t - def whiteLevel(self): - return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) - #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() + #def whiteLevel(self): + #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) + ##return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() - def blackLevel(self): - return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) - #return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() - \ No newline at end of file + #def blackLevel(self): + #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) + ##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() + + def getView(self): + """Return the ViewBox (or other compatible object) which displays the ImageItem""" + return self.view + + def getImageItem(self): + """Return the ImageItem for this ImageView.""" + return self.imageItem + + def getRoiPlot(self): + """Return the ROI PlotWidget for this ImageView""" + return self.ui.roiPlot + + def getHistogramWidget(self): + """Return the HistogramLUTWidget for this ImageView""" + return self.ui.histogram diff --git a/pyqtgraph/imageview/ImageViewTemplate.ui b/pyqtgraph/imageview/ImageViewTemplate.ui new file mode 100644 index 00000000..497c0c59 --- /dev/null +++ b/pyqtgraph/imageview/ImageViewTemplate.ui @@ -0,0 +1,252 @@ + + + Form + + + + 0 + 0 + 726 + 588 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + 0 + + + + + + + + + + + + 0 + 1 + + + + ROI + + + true + + + + + + + + 0 + 1 + + + + Norm + + + true + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + + + + + Normalization + + + + 0 + + + 0 + + + + + Subtract + + + + + + + Divide + + + false + + + + + + + + 75 + true + + + + Operation: + + + + + + + + 75 + true + + + + Mean: + + + + + + + + 75 + true + + + + Blur: + + + + + + + ROI + + + + + + + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + T + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Off + + + true + + + + + + + Time range + + + + + + + Frame + + + + + + + + + + + + + + PlotWidget + QWidget +
pyqtgraph.widgets.PlotWidget
+ 1 +
+ + GraphicsView + QGraphicsView +
pyqtgraph.widgets.GraphicsView
+
+ + HistogramLUTWidget + QGraphicsView +
pyqtgraph.widgets.HistogramLUTWidget
+
+
+ + +
diff --git a/ImageViewTemplate.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py similarity index 80% rename from ImageViewTemplate.py rename to pyqtgraph/imageview/ImageViewTemplate_pyqt.py index fe283a74..e6423276 100644 --- a/ImageViewTemplate.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './lib/util/pyqtgraph/ImageViewTemplate.ui' +# Form implementation generated from reading ui file './imageview/ImageViewTemplate.ui' # -# Created: Wed May 18 20:44:20 2011 -# by: PyQt4 UI code generator 4.8.3 +# Created: Sun Sep 9 14:41:30 2012 +# by: PyQt4 UI code generator 4.9.1 # # WARNING! All changes made in this file will be lost! @@ -18,57 +18,53 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) Form.resize(726, 588) - self.verticalLayout = QtGui.QVBoxLayout(Form) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.gridLayout_3 = QtGui.QGridLayout(Form) + self.gridLayout_3.setMargin(0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Vertical) self.splitter.setObjectName(_fromUtf8("splitter")) self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.gridLayout = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) self.gridLayout.setMargin(0) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.graphicsView = GraphicsView(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(10) - sizePolicy.setVerticalStretch(10) - sizePolicy.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) - self.graphicsView.setSizePolicy(sizePolicy) self.graphicsView.setObjectName(_fromUtf8("graphicsView")) - self.gridLayout.addWidget(self.graphicsView, 1, 0, 3, 1) + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName(_fromUtf8("histogram")) + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) self.roiBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) self.roiBtn.setSizePolicy(sizePolicy) - self.roiBtn.setMaximumSize(QtCore.QSize(30, 16777215)) self.roiBtn.setCheckable(True) self.roiBtn.setObjectName(_fromUtf8("roiBtn")) - self.gridLayout.addWidget(self.roiBtn, 3, 3, 1, 1) - self.gradientWidget = GradientWidget(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.gradientWidget.sizePolicy().hasHeightForWidth()) - self.gradientWidget.setSizePolicy(sizePolicy) - self.gradientWidget.setObjectName(_fromUtf8("gradientWidget")) - self.gridLayout.addWidget(self.gradientWidget, 1, 3, 1, 1) + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) self.normBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) self.normBtn.setSizePolicy(sizePolicy) - self.normBtn.setMaximumSize(QtCore.QSize(30, 16777215)) self.normBtn.setCheckable(True) self.normBtn.setObjectName(_fromUtf8("normBtn")) - self.gridLayout.addWidget(self.normBtn, 2, 3, 1, 1) - self.normGroup = QtGui.QGroupBox(self.layoutWidget) + self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName(_fromUtf8("roiPlot")) + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtGui.QGroupBox(Form) self.normGroup.setObjectName(_fromUtf8("normGroup")) self.gridLayout_2 = QtGui.QGridLayout(self.normGroup) self.gridLayout_2.setMargin(0) @@ -83,22 +79,22 @@ class Ui_Form(object): self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) self.label_5 = QtGui.QLabel(self.normGroup) font = QtGui.QFont() - font.setWeight(75) font.setBold(True) + font.setWeight(75) self.label_5.setFont(font) self.label_5.setObjectName(_fromUtf8("label_5")) self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) self.label_3 = QtGui.QLabel(self.normGroup) font = QtGui.QFont() - font.setWeight(75) font.setBold(True) + font.setWeight(75) self.label_3.setFont(font) self.label_3.setObjectName(_fromUtf8("label_3")) self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) self.label_4 = QtGui.QLabel(self.normGroup) font = QtGui.QFont() - font.setWeight(75) font.setBold(True) + font.setWeight(75) self.label_4.setFont(font) self.label_4.setObjectName(_fromUtf8("label_4")) self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) @@ -136,24 +132,15 @@ class Ui_Form(object): self.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) self.normTBlurSpin.setObjectName(_fromUtf8("normTBlurSpin")) self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) - self.gridLayout.addWidget(self.normGroup, 0, 0, 1, 4) - self.roiPlot = PlotWidget(self.splitter) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) - self.roiPlot.setSizePolicy(sizePolicy) - self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) - self.roiPlot.setObjectName(_fromUtf8("roiPlot")) - self.verticalLayout.addWidget(self.splitter) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.roiBtn.setText(QtGui.QApplication.translate("Form", "R", None, QtGui.QApplication.UnicodeUTF8)) - self.normBtn.setText(QtGui.QApplication.translate("Form", "N", None, QtGui.QApplication.UnicodeUTF8)) + self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) + self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) @@ -168,6 +155,6 @@ class Ui_Form(object): self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) -from GraphicsView import GraphicsView -from pyqtgraph.GradientWidget import GradientWidget -from PlotWidget import PlotWidget +from pyqtgraph.widgets.GraphicsView import GraphicsView +from pyqtgraph.widgets.PlotWidget import PlotWidget +from pyqtgraph.widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside.py b/pyqtgraph/imageview/ImageViewTemplate_pyside.py new file mode 100644 index 00000000..c17bbfe1 --- /dev/null +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './imageview/ImageViewTemplate.ui' +# +# Created: Sun Sep 9 14:41:31 2012 +# by: pyside-uic 0.2.13 running on PySide 1.1.0 +# +# WARNING! All changes made in this file will be lost! + +from PySide import QtCore, QtGui + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(726, 588) + self.gridLayout_3 = QtGui.QGridLayout(Form) + self.gridLayout_3.setContentsMargins(0, 0, 0, 0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtGui.QGridLayout(self.layoutWidget) + self.gridLayout.setSpacing(0) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.graphicsView = GraphicsView(self.layoutWidget) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName("histogram") + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) + self.roiBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) + self.roiBtn.setSizePolicy(sizePolicy) + self.roiBtn.setCheckable(True) + self.roiBtn.setObjectName("roiBtn") + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) + self.normBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) + self.normBtn.setSizePolicy(sizePolicy) + self.normBtn.setCheckable(True) + self.normBtn.setObjectName("normBtn") + self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName("roiPlot") + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtGui.QGroupBox(Form) + self.normGroup.setObjectName("normGroup") + self.gridLayout_2 = QtGui.QGridLayout(self.normGroup) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.normSubtractRadio = QtGui.QRadioButton(self.normGroup) + self.normSubtractRadio.setObjectName("normSubtractRadio") + self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) + self.normDivideRadio = QtGui.QRadioButton(self.normGroup) + self.normDivideRadio.setChecked(False) + self.normDivideRadio.setObjectName("normDivideRadio") + self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) + self.label_5 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) + self.label_3 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtGui.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.normROICheck = QtGui.QCheckBox(self.normGroup) + self.normROICheck.setObjectName("normROICheck") + self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) + self.normXBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normXBlurSpin.setObjectName("normXBlurSpin") + self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) + self.label_8 = QtGui.QLabel(self.normGroup) + self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_8.setObjectName("label_8") + self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) + self.label_9 = QtGui.QLabel(self.normGroup) + self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_9.setObjectName("label_9") + self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) + self.normYBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normYBlurSpin.setObjectName("normYBlurSpin") + self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) + self.label_10 = QtGui.QLabel(self.normGroup) + self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_10.setObjectName("label_10") + self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) + self.normOffRadio = QtGui.QRadioButton(self.normGroup) + self.normOffRadio.setChecked(True) + self.normOffRadio.setObjectName("normOffRadio") + self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) + self.normTimeRangeCheck = QtGui.QCheckBox(self.normGroup) + self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") + self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) + self.normFrameCheck = QtGui.QCheckBox(self.normGroup) + self.normFrameCheck.setObjectName("normFrameCheck") + self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) + self.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) + self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) + self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) + self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) + self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) + self.label_5.setText(QtGui.QApplication.translate("Form", "Operation:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("Form", "Mean:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("Form", "Blur:", None, QtGui.QApplication.UnicodeUTF8)) + self.normROICheck.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) + self.label_8.setText(QtGui.QApplication.translate("Form", "X", None, QtGui.QApplication.UnicodeUTF8)) + self.label_9.setText(QtGui.QApplication.translate("Form", "Y", None, QtGui.QApplication.UnicodeUTF8)) + self.label_10.setText(QtGui.QApplication.translate("Form", "T", None, QtGui.QApplication.UnicodeUTF8)) + self.normOffRadio.setText(QtGui.QApplication.translate("Form", "Off", None, QtGui.QApplication.UnicodeUTF8)) + self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) + self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.GraphicsView import GraphicsView +from pyqtgraph.widgets.PlotWidget import PlotWidget +from pyqtgraph.widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/pyqtgraph/imageview/__init__.py b/pyqtgraph/imageview/__init__.py new file mode 100644 index 00000000..0060e823 --- /dev/null +++ b/pyqtgraph/imageview/__init__.py @@ -0,0 +1,6 @@ +""" +Widget used for display and analysis of 2D and 3D image data. +Includes ROI plotting over time and image normalization. +""" + +from .ImageView import ImageView diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py new file mode 100644 index 00000000..0797c75e --- /dev/null +++ b/pyqtgraph/metaarray/MetaArray.py @@ -0,0 +1,1471 @@ +# -*- coding: utf-8 -*- +""" +MetaArray.py - Class encapsulating ndarray with meta data +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +MetaArray is an array class based on numpy.ndarray that allows storage of per-axis meta data +such as axis values, names, units, column names, etc. It also enables several +new methods for slicing and indexing the array based on this meta data. +More info at http://www.scipy.org/Cookbook/MetaArray +""" + +import numpy as np +import types, copy, threading, os, re +import pickle +from functools import reduce +#import traceback + +## By default, the library will use HDF5 when writing files. +## This can be overridden by setting USE_HDF5 = False +USE_HDF5 = True +try: + import h5py + HAVE_HDF5 = True +except: + USE_HDF5 = False + HAVE_HDF5 = False + + +def axis(name=None, cols=None, values=None, units=None): + """Convenience function for generating axis descriptions when defining MetaArrays""" + ax = {} + cNameOrder = ['name', 'units', 'title'] + if name is not None: + ax['name'] = name + if values is not None: + ax['values'] = values + if units is not None: + ax['units'] = units + if cols is not None: + ax['cols'] = [] + for c in cols: + if type(c) != list and type(c) != tuple: + c = [c] + col = {} + for i in range(0,len(c)): + col[cNameOrder[i]] = c[i] + ax['cols'].append(col) + return ax + +class sliceGenerator(object): + """Just a compact way to generate tuples of slice objects.""" + def __getitem__(self, arg): + return arg + def __getslice__(self, arg): + return arg +SLICER = sliceGenerator() + + +class MetaArray(object): + """N-dimensional array with meta data such as axis titles, units, and column names. + + May be initialized with a file name, a tuple representing the dimensions of the array, + or any arguments that could be passed on to numpy.array() + + The info argument sets the metadata for the entire array. It is composed of a list + of axis descriptions where each axis may have a name, title, units, and a list of column + descriptions. An additional dict at the end of the axis list may specify parameters + that apply to values in the entire array. + + For example: + A 2D array of altitude values for a topographical map might look like + info=[ + {'name': 'lat', 'title': 'Lattitude'}, + {'name': 'lon', 'title': 'Longitude'}, + {'title': 'Altitude', 'units': 'm'} + ] + In this case, every value in the array represents the altitude in feet at the lat, lon + position represented by the array index. All of the following return the + value at lat=10, lon=5: + array[10, 5] + array['lon':5, 'lat':10] + array['lat':10][5] + Now suppose we want to combine this data with another array of equal dimensions that + represents the average rainfall for each location. We could easily store these as two + separate arrays or combine them into a 3D array with this description: + info=[ + {'name': 'vals', 'cols': [ + {'name': 'altitude', 'units': 'm'}, + {'name': 'rainfall', 'units': 'cm/year'} + ]}, + {'name': 'lat', 'title': 'Lattitude'}, + {'name': 'lon', 'title': 'Longitude'} + ] + We can now access the altitude values with array[0] or array['altitude'], and the + rainfall values with array[1] or array['rainfall']. All of the following return + the rainfall value at lat=10, lon=5: + array[1, 10, 5] + array['lon':5, 'lat':10, 'val': 'rainfall'] + array['rainfall', 'lon':5, 'lat':10] + Notice that in the second example, there is no need for an extra (4th) axis description + since the actual values are described (name and units) in the column info for the first axis. + """ + + version = '2' + + ## Types allowed as axis or column names + nameTypes = [basestring, tuple] + @staticmethod + def isNameType(var): + return any([isinstance(var, t) for t in MetaArray.nameTypes]) + + + ## methods to wrap from embedded ndarray / HDF5 + wrapMethods = set(['__eq__', '__ne__', '__le__', '__lt__', '__ge__', '__gt__']) + + def __init__(self, data=None, info=None, dtype=None, file=None, copy=False, **kwargs): + object.__init__(self) + #self._infoOwned = False + self._isHDF = False + + if file is not None: + self._data = None + self.readFile(file, **kwargs) + if self._data is None: + raise Exception("File read failed: %s" % file) + else: + self._info = info + if (hasattr(data, 'implements') and data.implements('MetaArray')): + self._info = data._info + self._data = data.asarray() + elif isinstance(data, tuple): ## create empty array with specified shape + self._data = np.empty(data, dtype=dtype) + else: + self._data = np.array(data, dtype=dtype, copy=copy) + + ## run sanity checks on info structure + self.checkInfo() + + def checkInfo(self): + info = self._info + if info is None: + if self._data is None: + return + else: + self._info = [{} for i in range(self.ndim)] + return + else: + try: + info = list(info) + except: + raise Exception("Info must be a list of axis specifications") + if len(info) < self.ndim+1: + info.extend([{}]*(self.ndim+1-len(info))) + elif len(info) > self.ndim+1: + raise Exception("Info parameter must be list of length ndim+1 or less.") + for i in range(len(info)): + if not isinstance(info[i], dict): + if info[i] is None: + info[i] = {} + else: + raise Exception("Axis specification must be Dict or None") + if i < self.ndim and 'values' in info[i]: + if type(info[i]['values']) is list: + info[i]['values'] = np.array(info[i]['values']) + elif type(info[i]['values']) is not np.ndarray: + raise Exception("Axis values must be specified as list or ndarray") + if info[i]['values'].ndim != 1 or info[i]['values'].shape[0] != self.shape[i]: + raise Exception("Values array for axis %d has incorrect shape. (given %s, but should be %s)" % (i, str(info[i]['values'].shape), str((self.shape[i],)))) + if i < self.ndim and 'cols' in info[i]: + if not isinstance(info[i]['cols'], list): + info[i]['cols'] = list(info[i]['cols']) + if len(info[i]['cols']) != self.shape[i]: + raise Exception('Length of column list for axis %d does not match data. (given %d, but should be %d)' % (i, len(info[i]['cols']), self.shape[i])) + + def implements(self, name=None): + ## Rather than isinstance(obj, MetaArray) use object.implements('MetaArray') + if name is None: + return ['MetaArray'] + else: + return name == 'MetaArray' + + #def __array_finalize__(self,obj): + ### array_finalize is called every time a MetaArray is created + ### (whereas __new__ is not necessarily called every time) + + ### obj is the object from which this array was generated (for example, when slicing or view()ing) + + ## We use the getattr method to set a default if 'obj' doesn't have the 'info' attribute + ##print "Create new MA from object", str(type(obj)) + ##import traceback + ##traceback.print_stack() + ##print "finalize", type(self), type(obj) + #if not hasattr(self, '_info'): + ##if isinstance(obj, MetaArray): + ##print " copy info:", obj._info + #self._info = getattr(obj, '_info', [{}]*(obj.ndim+1)) + #self._infoOwned = False ## Do not make changes to _info until it is copied at least once + ##print " self info:", self._info + + ## We could have checked first whether self._info was already defined: + ##if not hasattr(self, 'info'): + ## self._info = getattr(obj, 'info', {}) + + + def __getitem__(self, ind): + #print "getitem:", ind + + ## should catch scalar requests as early as possible to speed things up (?) + + nInd = self._interpretIndexes(ind) + + #a = np.ndarray.__getitem__(self, nInd) + a = self._data[nInd] + if len(nInd) == self.ndim: + if np.all([not isinstance(ind, slice) for ind in nInd]): ## no slices; we have requested a single value from the array + return a + #if type(a) != type(self._data) and not isinstance(a, np.ndarray): ## indexing returned single value + #return a + + ## indexing returned a sub-array; generate new info array to go with it + #print " new MA:", type(a), a.shape + info = [] + extraInfo = self._info[-1].copy() + for i in range(0, len(nInd)): ## iterate over all axes + #print " axis", i + if type(nInd[i]) in [slice, list] or isinstance(nInd[i], np.ndarray): ## If the axis is sliced, keep the info but chop if necessary + #print " slice axis", i, nInd[i] + #a._info[i] = self._axisSlice(i, nInd[i]) + #print " info:", a._info[i] + info.append(self._axisSlice(i, nInd[i])) + else: ## If the axis is indexed, then move the information from that single index to the last info dictionary + #print "indexed:", i, nInd[i], type(nInd[i]) + newInfo = self._axisSlice(i, nInd[i]) + name = None + colName = None + for k in newInfo: + if k == 'cols': + if 'cols' not in extraInfo: + extraInfo['cols'] = [] + extraInfo['cols'].append(newInfo[k]) + if 'units' in newInfo[k]: + extraInfo['units'] = newInfo[k]['units'] + if 'name' in newInfo[k]: + colName = newInfo[k]['name'] + elif k == 'name': + name = newInfo[k] + else: + if k not in extraInfo: + extraInfo[k] = newInfo[k] + extraInfo[k] = newInfo[k] + if 'name' not in extraInfo: + if name is None: + if colName is not None: + extraInfo['name'] = colName + else: + if colName is not None: + extraInfo['name'] = str(name) + ': ' + str(colName) + else: + extraInfo['name'] = name + + + #print "Lost info:", newInfo + #a._info[i] = None + #if 'name' in newInfo: + #a._info[-1][newInfo['name']] = newInfo + info.append(extraInfo) + + #self._infoOwned = False + #while None in a._info: + #a._info.remove(None) + return MetaArray(a, info=info) + + @property + def ndim(self): + return len(self.shape) ## hdf5 objects do not have ndim property. + + @property + def shape(self): + return self._data.shape + + @property + def dtype(self): + return self._data.dtype + + def __len__(self): + return len(self._data) + + def __getslice__(self, *args): + return self.__getitem__(slice(*args)) + + def __setitem__(self, ind, val): + nInd = self._interpretIndexes(ind) + try: + self._data[nInd] = val + except: + print(self, nInd, val) + raise + + def __getattr__(self, attr): + if attr in self.wrapMethods: + return getattr(self._data, attr) + else: + raise AttributeError(attr) + #return lambda *args, **kwargs: MetaArray(getattr(a.view(ndarray), attr)(*args, **kwargs) + + def __eq__(self, b): + return self._binop('__eq__', b) + + def __ne__(self, b): + return self._binop('__ne__', b) + #if isinstance(b, MetaArray): + #b = b.asarray() + #return self.asarray() != b + + def __sub__(self, b): + return self._binop('__sub__', b) + #if isinstance(b, MetaArray): + #b = b.asarray() + #return MetaArray(self.asarray() - b, info=self.infoCopy()) + + def __add__(self, b): + return self._binop('__add__', b) + + def __mul__(self, b): + return self._binop('__mul__', b) + + def __div__(self, b): + return self._binop('__div__', b) + + def _binop(self, op, b): + if isinstance(b, MetaArray): + b = b.asarray() + a = self.asarray() + c = getattr(a, op)(b) + if c.shape != a.shape: + raise Exception("Binary operators with MetaArray must return an array of the same shape (this shape is %s, result shape was %s)" % (a.shape, c.shape)) + return MetaArray(c, info=self.infoCopy()) + + def asarray(self): + if isinstance(self._data, np.ndarray): + return self._data + else: + return np.array(self._data) + + def __array__(self): + ## supports np.array(metaarray_instance) + return self.asarray() + + def view(self, typ): + ## deprecated; kept for backward compatibility + if typ is np.ndarray: + return self.asarray() + else: + raise Exception('invalid view type: %s' % str(typ)) + + def axisValues(self, axis): + """Return the list of values for an axis""" + ax = self._interpretAxis(axis) + if 'values' in self._info[ax]: + return self._info[ax]['values'] + else: + raise Exception('Array axis %s (%d) has no associated values.' % (str(axis), ax)) + + def xvals(self, axis): + """Synonym for axisValues()""" + return self.axisValues(axis) + + def axisHasValues(self, axis): + ax = self._interpretAxis(axis) + return 'values' in self._info[ax] + + def axisHasColumns(self, axis): + ax = self._interpretAxis(axis) + return 'cols' in self._info[ax] + + def axisUnits(self, axis): + """Return the units for axis""" + ax = self._info[self._interpretAxis(axis)] + if 'units' in ax: + return ax['units'] + + def hasColumn(self, axis, col): + ax = self._info[self._interpretAxis(axis)] + if 'cols' in ax: + for c in ax['cols']: + if c['name'] == col: + return True + return False + + def listColumns(self, axis=None): + """Return a list of column names for axis. If axis is not specified, then return a dict of {axisName: (column names), ...}.""" + if axis is None: + ret = {} + for i in range(self.ndim): + if 'cols' in self._info[i]: + cols = [c['name'] for c in self._info[i]['cols']] + else: + cols = [] + ret[self.axisName(i)] = cols + return ret + else: + axis = self._interpretAxis(axis) + return [c['name'] for c in self._info[axis]['cols']] + + def columnName(self, axis, col): + ax = self._info[self._interpretAxis(axis)] + return ax['cols'][col]['name'] + + def axisName(self, n): + return self._info[n].get('name', n) + + def columnUnits(self, axis, column): + """Return the units for column in axis""" + ax = self._info[self._interpretAxis(axis)] + if 'cols' in ax: + for c in ax['cols']: + if c['name'] == column: + return c['units'] + raise Exception("Axis %s has no column named %s" % (str(axis), str(column))) + else: + raise Exception("Axis %s has no column definitions" % str(axis)) + + def rowsort(self, axis, key=0): + """Return this object with all records sorted along axis using key as the index to the values to compare. Does not yet modify meta info.""" + ## make sure _info is copied locally before modifying it! + + keyList = self[key] + order = keyList.argsort() + if type(axis) == int: + ind = [slice(None)]*axis + ind.append(order) + elif isinstance(axis, basestring): + ind = (slice(axis, order),) + return self[tuple(ind)] + + def append(self, val, axis): + """Return this object with val appended along axis. Does not yet combine meta info.""" + ## make sure _info is copied locally before modifying it! + + s = list(self.shape) + axis = self._interpretAxis(axis) + s[axis] += 1 + n = MetaArray(tuple(s), info=self._info, dtype=self.dtype) + ind = [slice(None)]*self.ndim + ind[axis] = slice(None,-1) + n[tuple(ind)] = self + ind[axis] = -1 + n[tuple(ind)] = val + return n + + def extend(self, val, axis): + """Return the concatenation along axis of this object and val. Does not yet combine meta info.""" + ## make sure _info is copied locally before modifying it! + + axis = self._interpretAxis(axis) + return MetaArray(np.concatenate(self, val, axis), info=self._info) + + def infoCopy(self, axis=None): + """Return a deep copy of the axis meta info for this object""" + if axis is None: + return copy.deepcopy(self._info) + else: + return copy.deepcopy(self._info[self._interpretAxis(axis)]) + + def copy(self): + return MetaArray(self._data.copy(), info=self.infoCopy()) + + + def _interpretIndexes(self, ind): + #print "interpret", ind + if not isinstance(ind, tuple): + ## a list of slices should be interpreted as a tuple of slices. + if isinstance(ind, list) and len(ind) > 0 and isinstance(ind[0], slice): + ind = tuple(ind) + ## everything else can just be converted to a length-1 tuple + else: + ind = (ind,) + + nInd = [slice(None)]*self.ndim + numOk = True ## Named indices not started yet; numbered sill ok + for i in range(0,len(ind)): + (axis, index, isNamed) = self._interpretIndex(ind[i], i, numOk) + #try: + nInd[axis] = index + #except: + #print "ndim:", self.ndim + #print "axis:", axis + #print "index spec:", ind[i] + #print "index num:", index + #raise + if isNamed: + numOk = False + return tuple(nInd) + + def _interpretAxis(self, axis): + if isinstance(axis, basestring) or isinstance(axis, tuple): + return self._getAxis(axis) + else: + return axis + + def _interpretIndex(self, ind, pos, numOk): + #print "Interpreting index", ind, pos, numOk + + ## should probably check for int first to speed things up.. + if type(ind) is int: + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " normal numerical index" + return (pos, ind, False) + if MetaArray.isNameType(ind): + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " String index, column is ", self._getIndex(pos, ind) + return (pos, self._getIndex(pos, ind), False) + elif type(ind) is slice: + #print " Slice index" + if MetaArray.isNameType(ind.start) or MetaArray.isNameType(ind.stop): ## Not an actual slice! + #print " ..not a real slice" + axis = self._interpretAxis(ind.start) + #print " axis is", axis + + ## x[Axis:Column] + if MetaArray.isNameType(ind.stop): + #print " column name, column is ", self._getIndex(axis, ind.stop) + index = self._getIndex(axis, ind.stop) + + ## x[Axis:min:max] + elif (isinstance(ind.stop, float) or isinstance(ind.step, float)) and ('values' in self._info[axis]): + #print " axis value range" + if ind.stop is None: + mask = self.xvals(axis) < ind.step + elif ind.step is None: + mask = self.xvals(axis) >= ind.stop + else: + mask = (self.xvals(axis) >= ind.stop) * (self.xvals(axis) < ind.step) + ##print "mask:", mask + index = mask + + ## x[Axis:columnIndex] + elif isinstance(ind.stop, int) or isinstance(ind.step, int): + #print " normal slice after named axis" + if ind.step is None: + index = ind.stop + else: + index = slice(ind.stop, ind.step) + + ## x[Axis: [list]] + elif type(ind.stop) is list: + #print " list of indexes from named axis" + index = [] + for i in ind.stop: + if type(i) is int: + index.append(i) + elif MetaArray.isNameType(i): + index.append(self._getIndex(axis, i)) + else: + ## unrecognized type, try just passing on to array + index = ind.stop + break + + else: + #print " other type.. forward on to array for handling", type(ind.stop) + index = ind.stop + #print "Axis %s (%s) : %s" % (ind.start, str(axis), str(type(index))) + #if type(index) is np.ndarray: + #print " ", index.shape + return (axis, index, True) + else: + #print " Looks like a real slice, passing on to array" + return (pos, ind, False) + elif type(ind) is list: + #print " List index., interpreting each element individually" + indList = [self._interpretIndex(i, pos, numOk)[1] for i in ind] + return (pos, indList, False) + else: + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " normal numerical index" + return (pos, ind, False) + + def _getAxis(self, name): + for i in range(0, len(self._info)): + axis = self._info[i] + if 'name' in axis and axis['name'] == name: + return i + raise Exception("No axis named %s.\n info=%s" % (name, self._info)) + + def _getIndex(self, axis, name): + ax = self._info[axis] + if ax is not None and 'cols' in ax: + for i in range(0, len(ax['cols'])): + if 'name' in ax['cols'][i] and ax['cols'][i]['name'] == name: + return i + raise Exception("Axis %d has no column named %s.\n info=%s" % (axis, name, self._info)) + + def _axisCopy(self, i): + return copy.deepcopy(self._info[i]) + + def _axisSlice(self, i, cols): + #print "axisSlice", i, cols + if 'cols' in self._info[i] or 'values' in self._info[i]: + ax = self._axisCopy(i) + if 'cols' in ax: + #print " slicing columns..", array(ax['cols']), cols + sl = np.array(ax['cols'])[cols] + if isinstance(sl, np.ndarray): + sl = list(sl) + ax['cols'] = sl + #print " result:", ax['cols'] + if 'values' in ax: + ax['values'] = np.array(ax['values'])[cols] + else: + ax = self._info[i] + #print " ", ax + return ax + + def prettyInfo(self): + s = '' + titles = [] + maxl = 0 + for i in range(len(self._info)-1): + ax = self._info[i] + axs = '' + if 'name' in ax: + axs += '"%s"' % str(ax['name']) + else: + axs += "%d" % i + if 'units' in ax: + axs += " (%s)" % str(ax['units']) + titles.append(axs) + if len(axs) > maxl: + maxl = len(axs) + + for i in range(min(self.ndim, len(self._info)-1)): + ax = self._info[i] + axs = titles[i] + axs += '%s[%d] :' % (' ' * (maxl + 2 - len(axs)), self.shape[i]) + if 'values' in ax: + v0 = ax['values'][0] + v1 = ax['values'][-1] + axs += " values: [%g ... %g] (step %g)" % (v0, v1, (v1-v0)/(self.shape[i]-1)) + if 'cols' in ax: + axs += " columns: " + colstrs = [] + for c in range(len(ax['cols'])): + col = ax['cols'][c] + cs = str(col.get('name', c)) + if 'units' in col: + cs += " (%s)" % col['units'] + colstrs.append(cs) + axs += '[' + ', '.join(colstrs) + ']' + s += axs + "\n" + s += str(self._info[-1]) + return s + + def __repr__(self): + return "%s\n-----------------------------------------------\n%s" % (self.view(np.ndarray).__repr__(), self.prettyInfo()) + + def __str__(self): + return self.__repr__() + + + def axisCollapsingFn(self, fn, axis=None, *args, **kargs): + #arr = self.view(np.ndarray) + fn = getattr(self._data, fn) + if axis is None: + return fn(axis, *args, **kargs) + else: + info = self.infoCopy() + axis = self._interpretAxis(axis) + info.pop(axis) + return MetaArray(fn(axis, *args, **kargs), info=info) + + def mean(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('mean', axis, *args, **kargs) + + + def min(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('min', axis, *args, **kargs) + + def max(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('max', axis, *args, **kargs) + + def transpose(self, *args): + if len(args) == 1 and hasattr(args[0], '__iter__'): + order = args[0] + else: + order = args + + order = [self._interpretAxis(ax) for ax in order] + infoOrder = order + list(range(len(order), len(self._info))) + info = [self._info[i] for i in infoOrder] + order = order + list(range(len(order), self.ndim)) + + try: + if self._isHDF: + return MetaArray(np.array(self._data).transpose(order), info=info) + else: + return MetaArray(self._data.transpose(order), info=info) + except: + print(order) + raise + + #### File I/O Routines + def readFile(self, filename, **kwargs): + """Load the data and meta info stored in *filename* + Different arguments are allowed depending on the type of file. + For HDF5 files: + + *writable* (bool) if True, then any modifications to data in the array will be stored to disk. + *readAllData* (bool) if True, then all data in the array is immediately read from disk + and the file is closed (this is the default for files < 500MB). Otherwise, the file will + be left open and data will be read only as requested (this is + the default for files >= 500MB). + + + """ + ## decide which read function to use + fd = open(filename, 'rb') + magic = fd.read(8) + if magic == '\x89HDF\r\n\x1a\n': + fd.close() + self._readHDF5(filename, **kwargs) + self._isHDF = True + else: + fd.seek(0) + meta = MetaArray._readMeta(fd) + if 'version' in meta: + ver = meta['version'] + else: + ver = 1 + rFuncName = '_readData%s' % str(ver) + if not hasattr(MetaArray, rFuncName): + raise Exception("This MetaArray library does not support array version '%s'" % ver) + rFunc = getattr(self, rFuncName) + rFunc(fd, meta, **kwargs) + self._isHDF = False + + @staticmethod + def _readMeta(fd): + """Read meta array from the top of a file. Read lines until a blank line is reached. + This function should ideally work for ALL versions of MetaArray. + """ + meta = '' + ## Read meta information until the first blank line + while True: + line = fd.readline().strip() + if line == '': + break + meta += line + ret = eval(meta) + #print ret + return ret + + def _readData1(self, fd, meta, mmap=False): + ## Read array data from the file descriptor for MetaArray v1 files + ## read in axis values for any axis that specifies a length + frameSize = 1 + for ax in meta['info']: + if 'values_len' in ax: + ax['values'] = np.fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + frameSize *= ax['values_len'] + del ax['values_len'] + del ax['values_type'] + ## the remaining data is the actual array + if mmap: + subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) + else: + subarr = np.fromstring(fd.read(), dtype=meta['type']) + subarr.shape = meta['shape'] + self._info = meta['info'] + self._data = subarr + + def _readData2(self, fd, meta, mmap=False, subset=None): + ## read in axis values + dynAxis = None + frameSize = 1 + ## read in axis values for any axis that specifies a length + for i in range(len(meta['info'])): + ax = meta['info'][i] + if 'values_len' in ax: + if ax['values_len'] == 'dynamic': + if dynAxis is not None: + raise Exception("MetaArray has more than one dynamic axis! (this is not allowed)") + dynAxis = i + else: + ax['values'] = np.fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + frameSize *= ax['values_len'] + del ax['values_len'] + del ax['values_type'] + + ## No axes are dynamic, just read the entire array in at once + if dynAxis is None: + #if rewriteDynamic is not None: + #raise Exception("") + if meta['type'] == 'object': + if mmap: + raise Exception('memmap not supported for arrays with dtype=object') + subarr = pickle.loads(fd.read()) + else: + if mmap: + subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) + else: + subarr = np.fromstring(fd.read(), dtype=meta['type']) + #subarr = subarr.view(subtype) + subarr.shape = meta['shape'] + #subarr._info = meta['info'] + ## One axis is dynamic, read in a frame at a time + else: + if mmap: + raise Exception('memmap not supported for non-contiguous arrays. Use rewriteContiguous() to convert.') + ax = meta['info'][dynAxis] + xVals = [] + frames = [] + frameShape = list(meta['shape']) + frameShape[dynAxis] = 1 + frameSize = reduce(lambda a,b: a*b, frameShape) + n = 0 + while True: + ## Extract one non-blank line + while True: + line = fd.readline() + if line != '\n': + break + if line == '': + break + + ## evaluate line + inf = eval(line) + + ## read data block + #print "read %d bytes as %s" % (inf['len'], meta['type']) + if meta['type'] == 'object': + data = pickle.loads(fd.read(inf['len'])) + else: + data = np.fromstring(fd.read(inf['len']), dtype=meta['type']) + + if data.size != frameSize * inf['numFrames']: + #print data.size, frameSize, inf['numFrames'] + raise Exception("Wrong frame size in MetaArray file! (frame %d)" % n) + + ## read in data block + shape = list(frameShape) + shape[dynAxis] = inf['numFrames'] + data.shape = shape + if subset is not None: + dSlice = subset[dynAxis] + if dSlice.start is None: + dStart = 0 + else: + dStart = max(0, dSlice.start - n) + if dSlice.stop is None: + dStop = data.shape[dynAxis] + else: + dStop = min(data.shape[dynAxis], dSlice.stop - n) + newSubset = list(subset[:]) + newSubset[dynAxis] = slice(dStart, dStop) + if dStop > dStart: + #print n, data.shape, " => ", newSubset, data[tuple(newSubset)].shape + frames.append(data[tuple(newSubset)].copy()) + else: + #data = data[subset].copy() ## what's this for?? + frames.append(data) + + n += inf['numFrames'] + if 'xVals' in inf: + xVals.extend(inf['xVals']) + subarr = np.concatenate(frames, axis=dynAxis) + if len(xVals)> 0: + ax['values'] = np.array(xVals, dtype=ax['values_type']) + del ax['values_len'] + del ax['values_type'] + #subarr = subarr.view(subtype) + #subarr._info = meta['info'] + self._info = meta['info'] + self._data = subarr + #raise Exception() ## stress-testing + #return subarr + + def _readHDF5(self, fileName, readAllData=None, writable=False, **kargs): + if 'close' in kargs and readAllData is None: ## for backward compatibility + readAllData = kargs['close'] + + if readAllData is True and writable is True: + raise Exception("Incompatible arguments: readAllData=True and writable=True") + + if not HAVE_HDF5: + try: + assert writable==False + assert readAllData != False + self._readHDF5Remote(fileName) + return + except: + raise Exception("The file '%s' is HDF5-formatted, but the HDF5 library (h5py) was not found." % fileName) + + ## by default, readAllData=True for files < 500MB + if readAllData is None: + size = os.stat(fileName).st_size + readAllData = (size < 500e6) + + if writable is True: + mode = 'r+' + else: + mode = 'r' + f = h5py.File(fileName, mode) + + ver = f.attrs['MetaArray'] + if ver > MetaArray.version: + print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version))) + meta = MetaArray.readHDF5Meta(f['info']) + self._info = meta + + if writable or not readAllData: ## read all data, convert to ndarray, close file + self._data = f['data'] + self._openFile = f + else: + self._data = f['data'][:] + f.close() + + def _readHDF5Remote(self, fileName): + ## Used to read HDF5 files via remote process. + ## This is needed in the case that HDF5 is not importable due to the use of python-dbg. + proc = getattr(MetaArray, '_hdf5Process', None) + + if proc == False: + raise Exception('remote read failed') + if proc == None: + import pyqtgraph.multiprocess as mp + #print "new process" + proc = mp.Process(executable='/usr/bin/python') + proc.setProxyOptions(deferGetattr=True) + MetaArray._hdf5Process = proc + MetaArray._h5py_metaarray = proc._import('pyqtgraph.metaarray') + ma = MetaArray._h5py_metaarray.MetaArray(file=fileName) + self._data = ma.asarray()._getValue() + self._info = ma._info._getValue() + #print MetaArray._hdf5Process + #import inspect + #print MetaArray, id(MetaArray), inspect.getmodule(MetaArray) + + + + @staticmethod + def mapHDF5Array(data, writable=False): + off = data.id.get_offset() + if writable: + mode = 'r+' + else: + mode = 'r' + if off is None: + raise Exception("This dataset uses chunked storage; it can not be memory-mapped. (store using mappable=True)") + return np.memmap(filename=data.file.filename, offset=off, dtype=data.dtype, shape=data.shape, mode=mode) + + + + + @staticmethod + def readHDF5Meta(root, mmap=False): + data = {} + + ## Pull list of values from attributes and child objects + for k in root.attrs: + val = root.attrs[k] + if isinstance(val, basestring): ## strings need to be re-evaluated to their original types + try: + val = eval(val) + except: + raise Exception('Can not evaluate string: "%s"' % val) + data[k] = val + for k in root: + obj = root[k] + if isinstance(obj, h5py.highlevel.Group): + val = MetaArray.readHDF5Meta(obj) + elif isinstance(obj, h5py.highlevel.Dataset): + if mmap: + val = MetaArray.mapHDF5Array(obj) + else: + val = obj[:] + else: + raise Exception("Don't know what to do with type '%s'" % str(type(obj))) + data[k] = val + + typ = root.attrs['_metaType_'] + del data['_metaType_'] + + if typ == 'dict': + return data + elif typ == 'list' or typ == 'tuple': + d2 = [None]*len(data) + for k in data: + d2[int(k)] = data[k] + if typ == 'tuple': + d2 = tuple(d2) + return d2 + else: + raise Exception("Don't understand metaType '%s'" % typ) + + + def write(self, fileName, **opts): + """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) + opts: + appendAxis: the name (or index) of the appendable axis. Allows the array to grow. + compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. + chunks: bool or tuple specifying chunk shape + """ + + if USE_HDF5 and HAVE_HDF5: + return self.writeHDF5(fileName, **opts) + else: + return self.writeMa(fileName, **opts) + + def writeMeta(self, fileName): + """Used to re-write meta info to the given file. + This feature is only available for HDF5 files.""" + f = h5py.File(fileName, 'r+') + if f.attrs['MetaArray'] != MetaArray.version: + raise Exception("The file %s was created with a different version of MetaArray. Will not modify." % fileName) + del f['info'] + + self.writeHDF5Meta(f, 'info', self._info) + f.close() + + + def writeHDF5(self, fileName, **opts): + ## default options for writing datasets + dsOpts = { + 'compression': 'lzf', + 'chunks': True, + } + + ## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending) + appAxis = opts.get('appendAxis', None) + if appAxis is not None: + appAxis = self._interpretAxis(appAxis) + cs = [min(100000, x) for x in self.shape] + cs[appAxis] = 1 + dsOpts['chunks'] = tuple(cs) + + ## if there are columns, then we can guess a different chunk shape + ## (read one column at a time) + else: + cs = [min(100000, x) for x in self.shape] + for i in range(self.ndim): + if 'cols' in self._info[i]: + cs[i] = 1 + dsOpts['chunks'] = tuple(cs) + + ## update options if they were passed in + for k in dsOpts: + if k in opts: + dsOpts[k] = opts[k] + + + ## If mappable is in options, it disables chunking/compression + if opts.get('mappable', False): + dsOpts = { + 'chunks': None, + 'compression': None + } + + + ## set maximum shape to allow expansion along appendAxis + append = False + if appAxis is not None: + maxShape = list(self.shape) + ax = self._interpretAxis(appAxis) + maxShape[ax] = None + if os.path.exists(fileName): + append = True + dsOpts['maxshape'] = tuple(maxShape) + else: + dsOpts['maxshape'] = None + + if append: + f = h5py.File(fileName, 'r+') + if f.attrs['MetaArray'] != MetaArray.version: + raise Exception("The file %s was created with a different version of MetaArray. Will not modify." % fileName) + + ## resize data and write in new values + data = f['data'] + shape = list(data.shape) + shape[ax] += self.shape[ax] + data.resize(tuple(shape)) + sl = [slice(None)] * len(data.shape) + sl[ax] = slice(-self.shape[ax], None) + data[tuple(sl)] = self.view(np.ndarray) + + ## add axis values if they are present. + axInfo = f['info'][str(ax)] + if 'values' in axInfo: + v = axInfo['values'] + v2 = self._info[ax]['values'] + shape = list(v.shape) + shape[0] += v2.shape[0] + v.resize(shape) + v[-v2.shape[0]:] = v2 + f.close() + else: + f = h5py.File(fileName, 'w') + f.attrs['MetaArray'] = MetaArray.version + #print dsOpts + f.create_dataset('data', data=self.view(np.ndarray), **dsOpts) + + ## dsOpts is used when storing meta data whenever an array is encountered + ## however, 'chunks' will no longer be valid for these arrays if it specifies a chunk shape. + ## 'maxshape' is right-out. + if isinstance(dsOpts['chunks'], tuple): + dsOpts['chunks'] = True + if 'maxshape' in dsOpts: + del dsOpts['maxshape'] + self.writeHDF5Meta(f, 'info', self._info, **dsOpts) + f.close() + + def writeHDF5Meta(self, root, name, data, **dsOpts): + if isinstance(data, np.ndarray): + dsOpts['maxshape'] = (None,) + data.shape[1:] + root.create_dataset(name, data=data, **dsOpts) + elif isinstance(data, list) or isinstance(data, tuple): + gr = root.create_group(name) + if isinstance(data, list): + gr.attrs['_metaType_'] = 'list' + else: + gr.attrs['_metaType_'] = 'tuple' + #n = int(np.log10(len(data))) + 1 + for i in range(len(data)): + self.writeHDF5Meta(gr, str(i), data[i], **dsOpts) + elif isinstance(data, dict): + gr = root.create_group(name) + gr.attrs['_metaType_'] = 'dict' + for k, v in data.items(): + self.writeHDF5Meta(gr, k, v, **dsOpts) + elif isinstance(data, int) or isinstance(data, float) or isinstance(data, np.integer) or isinstance(data, np.floating): + root.attrs[name] = data + else: + try: ## strings, bools, None are stored as repr() strings + root.attrs[name] = repr(data) + except: + print("Can not store meta data of type '%s' in HDF5. (key is '%s')" % (str(type(data)), str(name))) + raise + + + def writeMa(self, fileName, appendAxis=None, newFile=False): + """Write an old-style .ma file""" + meta = {'shape':self.shape, 'type':str(self.dtype), 'info':self.infoCopy(), 'version':MetaArray.version} + axstrs = [] + + ## copy out axis values for dynamic axis if requested + if appendAxis is not None: + if MetaArray.isNameType(appendAxis): + appendAxis = self._interpretAxis(appendAxis) + + + ax = meta['info'][appendAxis] + ax['values_len'] = 'dynamic' + if 'values' in ax: + ax['values_type'] = str(ax['values'].dtype) + dynXVals = ax['values'] + del ax['values'] + else: + dynXVals = None + + ## Generate axis data string, modify axis info so we know how to read it back in later + for ax in meta['info']: + if 'values' in ax: + axstrs.append(ax['values'].tostring()) + ax['values_len'] = len(axstrs[-1]) + ax['values_type'] = str(ax['values'].dtype) + del ax['values'] + + ## Decide whether to output the meta block for a new file + if not newFile: + ## If the file does not exist or its size is 0, then we must write the header + newFile = (not os.path.exists(fileName)) or (os.stat(fileName).st_size == 0) + + ## write data to file + if appendAxis is None or newFile: + fd = open(fileName, 'wb') + fd.write(str(meta) + '\n\n') + for ax in axstrs: + fd.write(ax) + else: + fd = open(fileName, 'ab') + + if self.dtype != object: + dataStr = self.view(np.ndarray).tostring() + else: + dataStr = pickle.dumps(self.view(np.ndarray)) + #print self.size, len(dataStr), self.dtype + if appendAxis is not None: + frameInfo = {'len':len(dataStr), 'numFrames':self.shape[appendAxis]} + if dynXVals is not None: + frameInfo['xVals'] = list(dynXVals) + fd.write('\n'+str(frameInfo)+'\n') + fd.write(dataStr) + fd.close() + + def writeCsv(self, fileName=None): + """Write 2D array to CSV file or return the string if no filename is given""" + if self.ndim > 2: + raise Exception("CSV Export is only for 2D arrays") + if fileName is not None: + file = open(fileName, 'w') + ret = '' + if 'cols' in self._info[0]: + s = ','.join([x['name'] for x in self._info[0]['cols']]) + '\n' + if fileName is not None: + file.write(s) + else: + ret += s + for row in range(0, self.shape[1]): + s = ','.join(["%g" % x for x in self[:, row]]) + '\n' + if fileName is not None: + file.write(s) + else: + ret += s + if fileName is not None: + file.close() + else: + return ret + + + +#class H5MetaList(): + + +#def rewriteContiguous(fileName, newName): + #"""Rewrite a dynamic array file as contiguous""" + #def _readData2(fd, meta, subtype, mmap): + ### read in axis values + #dynAxis = None + #frameSize = 1 + ### read in axis values for any axis that specifies a length + #for i in range(len(meta['info'])): + #ax = meta['info'][i] + #if ax.has_key('values_len'): + #if ax['values_len'] == 'dynamic': + #if dynAxis is not None: + #raise Exception("MetaArray has more than one dynamic axis! (this is not allowed)") + #dynAxis = i + #else: + #ax['values'] = fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + #frameSize *= ax['values_len'] + #del ax['values_len'] + #del ax['values_type'] + + ### No axes are dynamic, just read the entire array in at once + #if dynAxis is None: + #raise Exception('Array has no dynamic axes.') + ### One axis is dynamic, read in a frame at a time + #else: + #if mmap: + #raise Exception('memmap not supported for non-contiguous arrays. Use rewriteContiguous() to convert.') + #ax = meta['info'][dynAxis] + #xVals = [] + #frames = [] + #frameShape = list(meta['shape']) + #frameShape[dynAxis] = 1 + #frameSize = reduce(lambda a,b: a*b, frameShape) + #n = 0 + #while True: + ### Extract one non-blank line + #while True: + #line = fd.readline() + #if line != '\n': + #break + #if line == '': + #break + + ### evaluate line + #inf = eval(line) + + ### read data block + ##print "read %d bytes as %s" % (inf['len'], meta['type']) + #if meta['type'] == 'object': + #data = pickle.loads(fd.read(inf['len'])) + #else: + #data = fromstring(fd.read(inf['len']), dtype=meta['type']) + + #if data.size != frameSize * inf['numFrames']: + ##print data.size, frameSize, inf['numFrames'] + #raise Exception("Wrong frame size in MetaArray file! (frame %d)" % n) + + ### read in data block + #shape = list(frameShape) + #shape[dynAxis] = inf['numFrames'] + #data.shape = shape + #frames.append(data) + + #n += inf['numFrames'] + #if 'xVals' in inf: + #xVals.extend(inf['xVals']) + #subarr = np.concatenate(frames, axis=dynAxis) + #if len(xVals)> 0: + #ax['values'] = array(xVals, dtype=ax['values_type']) + #del ax['values_len'] + #del ax['values_type'] + #subarr = subarr.view(subtype) + #subarr._info = meta['info'] + #return subarr + + + + + +if __name__ == '__main__': + ## Create an array with every option possible + + arr = np.zeros((2, 5, 3, 5), dtype=int) + for i in range(arr.shape[0]): + for j in range(arr.shape[1]): + for k in range(arr.shape[2]): + for l in range(arr.shape[3]): + arr[i,j,k,l] = (i+1)*1000 + (j+1)*100 + (k+1)*10 + (l+1) + + info = [ + axis('Axis1'), + axis('Axis2', values=[1,2,3,4,5]), + axis('Axis3', cols=[ + ('Ax3Col1'), + ('Ax3Col2', 'mV', 'Axis3 Column2'), + (('Ax3','Col3'), 'A', 'Axis3 Column3')]), + {'name': 'Axis4', 'values': np.array([1.1, 1.2, 1.3, 1.4, 1.5]), 'units': 's'}, + {'extra': 'info'} + ] + + ma = MetaArray(arr, info=info) + + print("==== Original Array =======") + print(ma) + print("\n\n") + + #### Tests follow: + + + #### Index/slice tests: check that all values and meta info are correct after slice + print("\n -- normal integer indexing\n") + + print("\n ma[1]") + print(ma[1]) + + print("\n ma[1, 2:4]") + print(ma[1, 2:4]) + + print("\n ma[1, 1:5:2]") + print(ma[1, 1:5:2]) + + print("\n -- named axis indexing\n") + + print("\n ma['Axis2':3]") + print(ma['Axis2':3]) + + print("\n ma['Axis2':3:5]") + print(ma['Axis2':3:5]) + + print("\n ma[1, 'Axis2':3]") + print(ma[1, 'Axis2':3]) + + print("\n ma[:, 'Axis2':3]") + print(ma[:, 'Axis2':3]) + + print("\n ma['Axis2':3, 'Axis4':0:2]") + print(ma['Axis2':3, 'Axis4':0:2]) + + + print("\n -- column name indexing\n") + + print("\n ma['Axis3':'Ax3Col1']") + print(ma['Axis3':'Ax3Col1']) + + print("\n ma['Axis3':('Ax3','Col3')]") + print(ma['Axis3':('Ax3','Col3')]) + + print("\n ma[:, :, 'Ax3Col2']") + print(ma[:, :, 'Ax3Col2']) + + print("\n ma[:, :, ('Ax3','Col3')]") + print(ma[:, :, ('Ax3','Col3')]) + + + print("\n -- axis value range indexing\n") + + print("\n ma['Axis2':1.5:4.5]") + print(ma['Axis2':1.5:4.5]) + + print("\n ma['Axis4':1.15:1.45]") + print(ma['Axis4':1.15:1.45]) + + print("\n ma['Axis4':1.15:1.25]") + print(ma['Axis4':1.15:1.25]) + + + + print("\n -- list indexing\n") + + print("\n ma[:, [0,2,4]]") + print(ma[:, [0,2,4]]) + + print("\n ma['Axis4':[0,2,4]]") + print(ma['Axis4':[0,2,4]]) + + print("\n ma['Axis3':[0, ('Ax3','Col3')]]") + print(ma['Axis3':[0, ('Ax3','Col3')]]) + + + + print("\n -- boolean indexing\n") + + print("\n ma[:, array([True, True, False, True, False])]") + print(ma[:, np.array([True, True, False, True, False])]) + + print("\n ma['Axis4':array([True, False, False, False])]") + print(ma['Axis4':np.array([True, False, False, False])]) + + + + + + #### Array operations + # - Concatenate + # - Append + # - Extend + # - Rowsort + + + + + #### File I/O tests + + print("\n================ File I/O Tests ===================\n") + import tempfile + tf = tempfile.mktemp() + tf = 'test.ma' + # write whole array + + print("\n -- write/read test") + ma.write(tf) + ma2 = MetaArray(file=tf) + + #print ma2 + print("\nArrays are equivalent:", (ma == ma2).all()) + #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() + os.remove(tf) + + # CSV write + + # append mode + + + print("\n================append test (%s)===============" % tf) + ma['Axis2':0:2].write(tf, appendAxis='Axis2') + for i in range(2,ma.shape[1]): + ma['Axis2':[i]].write(tf, appendAxis='Axis2') + + ma2 = MetaArray(file=tf) + + #print ma2 + print("\nArrays are equivalent:", (ma == ma2).all()) + #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() + + os.remove(tf) + + + + ## memmap test + print("\n==========Memmap test============") + ma.write(tf, mappable=True) + ma2 = MetaArray(file=tf, mmap=True) + print("\nArrays are equivalent:", (ma == ma2).all()) + os.remove(tf) + \ No newline at end of file diff --git a/pyqtgraph/metaarray/__init__.py b/pyqtgraph/metaarray/__init__.py new file mode 100644 index 00000000..a12f40d5 --- /dev/null +++ b/pyqtgraph/metaarray/__init__.py @@ -0,0 +1 @@ +from .MetaArray import * diff --git a/license.txt b/pyqtgraph/metaarray/license.txt similarity index 89% rename from license.txt rename to pyqtgraph/metaarray/license.txt index 662ed4f4..7ef3e5e9 100644 --- a/license.txt +++ b/pyqtgraph/metaarray/license.txt @@ -1,6 +1,4 @@ -Copyright (c) 2011 University of North Carolina at Chapel Hill -Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') - +Copyright (c) 2010 Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/pyqtgraph/metaarray/readMeta.m b/pyqtgraph/metaarray/readMeta.m new file mode 100644 index 00000000..f468a75d --- /dev/null +++ b/pyqtgraph/metaarray/readMeta.m @@ -0,0 +1,86 @@ +function f = readMeta(file) +info = hdf5info(file); +f = readMetaRecursive(info.GroupHierarchy.Groups(1)); +end + + +function f = readMetaRecursive(root) +typ = 0; +for i = 1:length(root.Attributes) + if strcmp(root.Attributes(i).Shortname, '_metaType_') + typ = root.Attributes(i).Value.Data; + break + end +end +if typ == 0 + printf('group has no _metaType_') + typ = 'dict'; +end + +list = 0; +if strcmp(typ, 'list') || strcmp(typ, 'tuple') + data = {}; + list = 1; +elseif strcmp(typ, 'dict') + data = struct(); +else + printf('Unrecognized meta type %s', typ); + data = struct(); +end + +for i = 1:length(root.Attributes) + name = root.Attributes(i).Shortname; + if strcmp(name, '_metaType_') + continue + end + val = root.Attributes(i).Value; + if isa(val, 'hdf5.h5string') + val = val.Data; + end + if list + ind = str2num(name)+1; + data{ind} = val; + else + data.(name) = val; + end +end + +for i = 1:length(root.Datasets) + fullName = root.Datasets(i).Name; + name = stripName(fullName); + file = root.Datasets(i).Filename; + data2 = hdf5read(file, fullName); + if list + ind = str2num(name)+1; + data{ind} = data2; + else + data.(name) = data2; + end +end + +for i = 1:length(root.Groups) + name = stripName(root.Groups(i).Name); + data2 = readMetaRecursive(root.Groups(i)); + if list + ind = str2num(name)+1; + data{ind} = data2; + else + data.(name) = data2; + end +end +f = data; +return; +end + + +function f = stripName(str) +inds = strfind(str, '/'); +if isempty(inds) + f = str; +else + f = str(inds(length(inds))+1:length(str)); +end +end + + + diff --git a/pyqtgraph/multiprocess/__init__.py b/pyqtgraph/multiprocess/__init__.py new file mode 100644 index 00000000..843b42a3 --- /dev/null +++ b/pyqtgraph/multiprocess/__init__.py @@ -0,0 +1,24 @@ +""" +Multiprocessing utility library +(parallelization done the way I like it) + +Luke Campagnola +2012.06.10 + +This library provides: + + - simple mechanism for starting a new python interpreter process that can be controlled from the original process + (this allows, for example, displaying and manipulating plots in a remote process + while the parent process is free to do other work) + - proxy system that allows objects hosted in the remote process to be used as if they were local + - Qt signal connection between processes + - very simple in-line parallelization (fork only; does not work on windows) for number-crunching + +TODO: + allow remote processes to serve as rendering engines that pass pixmaps back to the parent process for display + (RemoteGraphicsView class) +""" + +from .processes import * +from .parallelizer import Parallelize, CanceledError +from .remoteproxy import proxy \ No newline at end of file diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py new file mode 100644 index 00000000..e0d1c02c --- /dev/null +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -0,0 +1,16 @@ +"""For starting up remote processes""" +import sys, pickle, os + +if __name__ == '__main__': + os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process + name, port, authkey, targetStr, path = pickle.load(sys.stdin) + if path is not None: + ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. + while len(sys.path) > 0: + sys.path.pop() + sys.path.extend(path) + #import pyqtgraph + #import pyqtgraph.multiprocess.processes + target = pickle.loads(targetStr) ## unpickling the target should import everything we need + target(name, port, authkey) + sys.exit(0) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py new file mode 100644 index 00000000..0d5d6f5c --- /dev/null +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -0,0 +1,330 @@ +import os, sys, time, multiprocessing, re +from processes import ForkedProcess +from remoteproxy import ExitError + +class CanceledError(Exception): + """Raised when the progress dialog is canceled during a processing operation.""" + pass + +class Parallelize(object): + """ + Class for ultra-simple inline parallelization on multi-core CPUs + + Example:: + + ## Here is the serial (single-process) task: + + tasks = [1, 2, 4, 8] + results = [] + for task in tasks: + result = processTask(task) + results.append(result) + print results + + + ## Here is the parallelized version: + + tasks = [1, 2, 4, 8] + results = [] + with Parallelize(tasks, workers=4, results=results) as tasker: + for task in tasker: + result = processTask(task) + tasker.results.append(result) + print results + + + The only major caveat is that *result* in the example above must be picklable, + since it is automatically sent via pipe back to the parent process. + """ + + def __init__(self, tasks=None, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds): + """ + =============== =================================================================== + Arguments: + tasks list of objects to be processed (Parallelize will determine how to + distribute the tasks). If unspecified, then each worker will receive + a single task with a unique id number. + workers number of worker processes or None to use number of CPUs in the + system + progressDialog optional dict of arguments for ProgressDialog + to update while tasks are processed + randomReseed If True, each forked process will reseed its random number generator + to ensure independent results. Works with the built-in random + and numpy.random. + kwds objects to be shared by proxy with child processes (they will + appear as attributes of the tasker) + =============== =================================================================== + """ + + ## Generate progress dialog. + ## Note that we want to avoid letting forked child processes play with progress dialogs.. + self.showProgress = False + if progressDialog is not None: + self.showProgress = True + if isinstance(progressDialog, basestring): + progressDialog = {'labelText': progressDialog} + import pyqtgraph as pg + self.progressDlg = pg.ProgressDialog(**progressDialog) + + if workers is None: + workers = self.suggestedWorkerCount() + if not hasattr(os, 'fork'): + workers = 1 + self.workers = workers + if tasks is None: + tasks = range(workers) + self.tasks = list(tasks) + self.reseed = randomReseed + self.kwds = kwds.copy() + self.kwds['_taskStarted'] = self._taskStarted + + def __enter__(self): + self.proc = None + if self.workers == 1: + return self.runSerial() + else: + return self.runParallel() + + def __exit__(self, *exc_info): + + if self.proc is not None: ## worker + exceptOccurred = exc_info[0] is not None ## hit an exception during processing. + + try: + if exceptOccurred: + sys.excepthook(*exc_info) + finally: + #print os.getpid(), 'exit' + os._exit(1 if exceptOccurred else 0) + + else: ## parent + if self.showProgress: + self.progressDlg.__exit__(None, None, None) + + def runSerial(self): + if self.showProgress: + self.progressDlg.__enter__() + self.progressDlg.setMaximum(len(self.tasks)) + self.progress = {os.getpid(): []} + return Tasker(self, None, self.tasks, self.kwds) + + + def runParallel(self): + self.childs = [] + + ## break up tasks into one set per worker + workers = self.workers + chunks = [[] for i in xrange(workers)] + i = 0 + for i in range(len(self.tasks)): + chunks[i%workers].append(self.tasks[i]) + + ## fork and assign tasks to each worker + for i in range(workers): + proc = ForkedProcess(target=None, preProxy=self.kwds, randomReseed=self.reseed) + if not proc.isParent: + self.proc = proc + return Tasker(self, proc, chunks[i], proc.forkedProxies) + else: + self.childs.append(proc) + + ## Keep track of the progress of each worker independently. + self.progress = {ch.childPid: [] for ch in self.childs} + ## for each child process, self.progress[pid] is a list + ## of task indexes. The last index is the task currently being + ## processed; all others are finished. + + + try: + if self.showProgress: + self.progressDlg.__enter__() + self.progressDlg.setMaximum(len(self.tasks)) + ## process events from workers until all have exited. + + activeChilds = self.childs[:] + self.exitCodes = [] + pollInterval = 0.01 + while len(activeChilds) > 0: + waitingChildren = 0 + rem = [] + for ch in activeChilds: + try: + n = ch.processRequests() + if n > 0: + waitingChildren += 1 + except ExitError: + #print ch.childPid, 'process finished' + rem.append(ch) + if self.showProgress: + self.progressDlg += 1 + #print "remove:", [ch.childPid for ch in rem] + for ch in rem: + activeChilds.remove(ch) + while True: + try: + pid, exitcode = os.waitpid(ch.childPid, 0) + self.exitCodes.append(exitcode) + break + except OSError as ex: + if ex.errno == 4: ## If we get this error, just try again + continue + #print "Ignored system call interruption" + else: + raise + + #print [ch.childPid for ch in activeChilds] + + if self.showProgress and self.progressDlg.wasCanceled(): + for ch in activeChilds: + ch.kill() + raise CanceledError() + + ## adjust polling interval--prefer to get exactly 1 event per poll cycle. + if waitingChildren > 1: + pollInterval *= 0.7 + elif waitingChildren == 0: + pollInterval /= 0.7 + pollInterval = max(min(pollInterval, 0.5), 0.0005) ## but keep it within reasonable limits + + time.sleep(pollInterval) + finally: + if self.showProgress: + self.progressDlg.__exit__(None, None, None) + if len(self.exitCodes) < len(self.childs): + raise Exception("Parallelizer started %d processes but only received exit codes from %d." % (len(self.childs), len(self.exitCodes))) + for code in self.exitCodes: + if code != 0: + raise Exception("Error occurred in parallel-executed subprocess (console output may have more information).") + return [] ## no tasks for parent process. + + + @staticmethod + def suggestedWorkerCount(): + if 'linux' in sys.platform: + ## I think we can do a little better here.. + ## cpu_count does not consider that there is little extra benefit to using hyperthreaded cores. + try: + cores = {} + pid = None + + for line in open('/proc/cpuinfo'): + m = re.match(r'physical id\s+:\s+(\d+)', line) + if m is not None: + pid = m.groups()[0] + m = re.match(r'cpu cores\s+:\s+(\d+)', line) + if m is not None: + cores[pid] = int(m.groups()[0]) + return sum(cores.values()) + except: + return multiprocessing.cpu_count() + + else: + return multiprocessing.cpu_count() + + def _taskStarted(self, pid, i, **kwds): + ## called remotely by tasker to indicate it has started working on task i + #print pid, 'reported starting task', i + if self.showProgress: + if len(self.progress[pid]) > 0: + self.progressDlg += 1 + if pid == os.getpid(): ## single-worker process + if self.progressDlg.wasCanceled(): + raise CanceledError() + self.progress[pid].append(i) + + +class Tasker(object): + def __init__(self, parallelizer, process, tasks, kwds): + self.proc = process + self.par = parallelizer + self.tasks = tasks + for k, v in kwds.iteritems(): + setattr(self, k, v) + + def __iter__(self): + ## we could fix this up such that tasks are retrieved from the parent process one at a time.. + for i, task in enumerate(self.tasks): + self.index = i + #print os.getpid(), 'starting task', i + self._taskStarted(os.getpid(), i, _callSync='off') + yield task + if self.proc is not None: + #print os.getpid(), 'no more tasks' + self.proc.close() + + def process(self): + """ + Process requests from parent. + Usually it is not necessary to call this unless you would like to + receive messages (such as exit requests) during an iteration. + """ + if self.proc is not None: + self.proc.processRequests() + + def numWorkers(self): + """ + Return the number of parallel workers + """ + return self.par.workers + +#class Parallelizer: + #""" + #Use:: + + #p = Parallelizer() + #with p(4) as i: + #p.finish(do_work(i)) + #print p.results() + + #""" + #def __init__(self): + #pass + + #def __call__(self, n): + #self.replies = [] + #self.conn = None ## indicates this is the parent process + #return Session(self, n) + + #def finish(self, data): + #if self.conn is None: + #self.replies.append((self.i, data)) + #else: + ##print "send", self.i, data + #self.conn.send((self.i, data)) + #os._exit(0) + + #def result(self): + #print self.replies + +#class Session: + #def __init__(self, par, n): + #self.par = par + #self.n = n + + #def __enter__(self): + #self.childs = [] + #for i in range(1, self.n): + #c1, c2 = multiprocessing.Pipe() + #pid = os.fork() + #if pid == 0: ## child + #self.par.i = i + #self.par.conn = c2 + #self.childs = None + #c1.close() + #return i + #else: + #self.childs.append(c1) + #c2.close() + #self.par.i = 0 + #return 0 + + + + #def __exit__(self, *exc_info): + #if exc_info[0] is not None: + #sys.excepthook(*exc_info) + #if self.childs is not None: + #self.par.replies.extend([conn.recv() for conn in self.childs]) + #else: + #self.par.finish(None) + diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py new file mode 100644 index 00000000..a4769679 --- /dev/null +++ b/pyqtgraph/multiprocess/processes.py @@ -0,0 +1,337 @@ +from remoteproxy import RemoteEventHandler, ExitError, NoResultError, LocalObjectProxy, ObjectProxy +import subprocess, atexit, os, sys, time, random, socket, signal +import cPickle as pickle +import multiprocessing.connection + +__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ExitError', 'NoResultError'] + +class Process(RemoteEventHandler): + """ + Bases: RemoteEventHandler + + This class is used to spawn and control a new python interpreter. + It uses subprocess.Popen to start the new process and communicates with it + using multiprocessing.Connection objects over a network socket. + + By default, the remote process will immediately enter an event-processing + loop that carries out requests send from the parent process. + + Remote control works mainly through proxy objects:: + + proc = Process() ## starts process, returns handle + rsys = proc._import('sys') ## asks remote process to import 'sys', returns + ## a proxy which references the imported module + rsys.stdout.write('hello\n') ## This message will be printed from the remote + ## process. Proxy objects can usually be used + ## exactly as regular objects are. + proc.close() ## Request the remote process shut down + + Requests made via proxy objects may be synchronous or asynchronous and may + return objects either by proxy or by value (if they are picklable). See + ProxyObject for more information. + """ + + def __init__(self, name=None, target=None, executable=None, copySysPath=True): + """ + ============ ============================================================= + Arguments: + name Optional name for this process used when printing messages + from the remote process. + target Optional function to call after starting remote process. + By default, this is startEventLoop(), which causes the remote + process to process requests from the parent process until it + is asked to quit. If you wish to specify a different target, + it must be picklable (bound methods are not). + copySysPath If true, copy the contents of sys.path to the remote process + ============ ============================================================= + + """ + if target is None: + target = startEventLoop + if name is None: + name = str(self) + if executable is None: + executable = sys.executable + + ## random authentication key + authkey = ''.join([chr(random.getrandbits(7)) for i in range(20)]) + + ## Listen for connection from remote process (and find free port number) + port = 10000 + while True: + try: + l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey) + break + except socket.error as ex: + if ex.errno != 98: + raise + port += 1 + + ## start remote process, instruct it to run target function + sysPath = sys.path if copySysPath else None + bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) + self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE) + targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to + ## set its sys.path properly before unpickling the target + pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin) + self.proc.stdin.close() + + ## open connection for remote process + conn = l.accept() + RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid) + + atexit.register(self.join) + + def join(self, timeout=10): + if self.proc.poll() is None: + self.close() + start = time.time() + while self.proc.poll() is None: + if timeout is not None and time.time() - start > timeout: + raise Exception('Timed out waiting for remote process to end.') + time.sleep(0.05) + + +def startEventLoop(name, port, authkey): + conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) + global HANDLER + HANDLER = RemoteEventHandler(conn, name, os.getppid()) + while True: + try: + HANDLER.processRequests() # exception raised when the loop should exit + time.sleep(0.01) + except ExitError: + break + + +class ForkedProcess(RemoteEventHandler): + """ + ForkedProcess is a substitute for Process that uses os.fork() to generate a new process. + This is much faster than starting a completely new interpreter and child processes + automatically have a copy of the entire program state from before the fork. This + makes it an appealing approach when parallelizing expensive computations. (see + also Parallelizer) + + However, fork() comes with some caveats and limitations: + + - fork() is not available on Windows. + - It is not possible to have a QApplication in both parent and child process + (unless both QApplications are created _after_ the call to fork()) + Attempts by the forked process to access Qt GUI elements created by the parent + will most likely cause the child to crash. + - Likewise, database connections are unlikely to function correctly in a forked child. + - Threads are not copied by fork(); the new process + will have only one thread that starts wherever fork() was called in the parent process. + - Forked processes are unceremoniously terminated when join() is called; they are not + given any opportunity to clean up. (This prevents them calling any cleanup code that + was only intended to be used by the parent process) + - Normally when fork()ing, open file handles are shared with the parent process, + which is potentially dangerous. ForkedProcess is careful to close all file handles + that are not explicitly needed--stdout, stderr, and a single pipe to the parent + process. + + """ + + def __init__(self, name=None, target=0, preProxy=None, randomReseed=True): + """ + When initializing, an optional target may be given. + If no target is specified, self.eventLoop will be used. + If None is given, no target will be called (and it will be up + to the caller to properly shut down the forked process) + + preProxy may be a dict of values that will appear as ObjectProxy + in the remote process (but do not need to be sent explicitly since + they are available immediately before the call to fork(). + Proxies will be availabe as self.proxies[name]. + + If randomReseed is True, the built-in random and numpy.random generators + will be reseeded in the child process. + """ + self.hasJoined = False + if target == 0: + target = self.eventLoop + if name is None: + name = str(self) + + conn, remoteConn = multiprocessing.Pipe() + + proxyIDs = {} + if preProxy is not None: + for k, v in preProxy.iteritems(): + proxyId = LocalObjectProxy.registerObject(v) + proxyIDs[k] = proxyId + + pid = os.fork() + if pid == 0: + self.isParent = False + ## We are now in the forked process; need to be extra careful what we touch while here. + ## - no reading/writing file handles/sockets owned by parent process (stdout is ok) + ## - don't touch QtGui or QApplication at all; these are landmines. + ## - don't let the process call exit handlers + + os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process + + ## close all file handles we do not want shared with parent + conn.close() + sys.stdin.close() ## otherwise we screw with interactive prompts. + fid = remoteConn.fileno() + os.closerange(3, fid) + os.closerange(fid+1, 4096) ## just guessing on the maximum descriptor count.. + + ## Override any custom exception hooks + def excepthook(*args): + import traceback + traceback.print_exception(*args) + sys.excepthook = excepthook + + ## Make it harder to access QApplication instance + if 'PyQt4.QtGui' in sys.modules: + sys.modules['PyQt4.QtGui'].QApplication = None + sys.modules.pop('PyQt4.QtGui', None) + sys.modules.pop('PyQt4.QtCore', None) + + ## sabotage atexit callbacks + atexit._exithandlers = [] + atexit.register(lambda: os._exit(0)) + + if randomReseed: + if 'numpy.random' in sys.modules: + sys.modules['numpy.random'].seed(os.getpid() ^ int(time.time()*10000%10000)) + if 'random' in sys.modules: + sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000)) + + RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid()) + + ppid = os.getppid() + self.forkedProxies = {} + for name, proxyId in proxyIDs.iteritems(): + self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) + + if target is not None: + target() + + else: + self.isParent = True + self.childPid = pid + remoteConn.close() + RemoteEventHandler.handlers = {} ## don't want to inherit any of this from the parent. + + RemoteEventHandler.__init__(self, conn, name+'_parent', pid=pid) + atexit.register(self.join) + + + def eventLoop(self): + while True: + try: + self.processRequests() # exception raised when the loop should exit + time.sleep(0.01) + except ExitError: + break + except: + print "Error occurred in forked event loop:" + sys.excepthook(*sys.exc_info()) + sys.exit(0) + + def join(self, timeout=10): + if self.hasJoined: + return + #os.kill(pid, 9) + try: + self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. + os.waitpid(self.childPid, 0) + except IOError: ## probably remote process has already quit + pass + self.hasJoined = True + + def kill(self): + """Immediately kill the forked remote process. + This is generally safe because forked processes are already + expected to _avoid_ any cleanup at exit.""" + os.kill(self.childPid, signal.SIGKILL) + self.hasJoined = True + + + +##Special set of subclasses that implement a Qt event loop instead. + +class RemoteQtEventHandler(RemoteEventHandler): + def __init__(self, *args, **kwds): + RemoteEventHandler.__init__(self, *args, **kwds) + + def startEventTimer(self): + from pyqtgraph.Qt import QtGui, QtCore + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.processRequests) + self.timer.start(10) + + def processRequests(self): + try: + RemoteEventHandler.processRequests(self) + except ExitError: + from pyqtgraph.Qt import QtGui, QtCore + QtGui.QApplication.instance().quit() + self.timer.stop() + #raise + +class QtProcess(Process): + """ + QtProcess is essentially the same as Process, with two major differences: + + - The remote process starts by running startQtEventLoop() which creates a + QApplication in the remote process and uses a QTimer to trigger + remote event processing. This allows the remote process to have its own + GUI. + - A QTimer is also started on the parent process which polls for requests + from the child process. This allows Qt signals emitted within the child + process to invoke slots on the parent process and vice-versa. + + Example:: + + proc = QtProcess() + rQtGui = proc._import('PyQt4.QtGui') + btn = rQtGui.QPushButton('button on child process') + btn.show() + + def slot(): + print 'slot invoked on parent process' + btn.clicked.connect(proxy(slot)) # be sure to send a proxy of the slot + """ + + def __init__(self, **kwds): + if 'target' not in kwds: + kwds['target'] = startQtEventLoop + Process.__init__(self, **kwds) + self.startEventTimer() + + def startEventTimer(self): + from pyqtgraph.Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy. + self.timer = QtCore.QTimer() + app = QtGui.QApplication.instance() + if app is None: + raise Exception("Must create QApplication before starting QtProcess") + self.timer.timeout.connect(self.processRequests) + self.timer.start(10) + + def processRequests(self): + try: + Process.processRequests(self) + except ExitError: + self.timer.stop() + +def startQtEventLoop(name, port, authkey): + conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey) + from pyqtgraph.Qt import QtGui, QtCore + #from PyQt4 import QtGui, QtCore + app = QtGui.QApplication.instance() + #print app + if app is None: + app = QtGui.QApplication([]) + app.setQuitOnLastWindowClosed(False) ## generally we want the event loop to stay open + ## until it is explicitly closed by the parent process. + + global HANDLER + HANDLER = RemoteQtEventHandler(conn, name, os.getppid()) + HANDLER.startEventTimer() + app.exec_() + + diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py new file mode 100644 index 00000000..f8da1bd7 --- /dev/null +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -0,0 +1,937 @@ +import os, __builtin__, time, sys, traceback, weakref +import cPickle as pickle + +class ExitError(Exception): + pass + +class NoResultError(Exception): + pass + + +class RemoteEventHandler(object): + """ + This class handles communication between two processes. One instance is present on + each process and listens for communication from the other process. This enables + (amongst other things) ObjectProxy instances to look up their attributes and call + their methods. + + This class is responsible for carrying out actions on behalf of the remote process. + Each instance holds one end of a Connection which allows python + objects to be passed between processes. + + For the most common operations, see _import(), close(), and transfer() + + To handle and respond to incoming requests, RemoteEventHandler requires that its + processRequests method is called repeatedly (this is usually handled by the Process + classes defined in multiprocess.processes). + + + + + """ + handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process + ## an object proxy belongs to + + def __init__(self, connection, name, pid): + self.conn = connection + self.name = name + self.results = {} ## reqId: (status, result); cache of request results received from the remote process + ## status is either 'result' or 'error' + ## if 'error', then result will be (exception, formatted exceprion) + ## where exception may be None if it could not be passed through the Connection. + + self.proxies = {} ## maps {weakref(proxy): proxyId}; used to inform the remote process when a proxy has been deleted. + + ## attributes that affect the behavior of the proxy. + ## See ObjectProxy._setProxyOptions for description + self.proxyOptions = { + 'callSync': 'sync', ## 'sync', 'async', 'off' + 'timeout': 10, ## float + 'returnType': 'auto', ## 'proxy', 'value', 'auto' + 'autoProxy': False, ## bool + 'deferGetattr': False, ## True, False + 'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ], + } + + self.nextRequestId = 0 + self.exited = False + + RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid + + @classmethod + def getHandler(cls, pid): + try: + return cls.handlers[pid] + except: + print pid, cls.handlers + raise + + def getProxyOption(self, opt): + return self.proxyOptions[opt] + + def setProxyOptions(self, **kwds): + """ + Set the default behavior options for object proxies. + See ObjectProxy._setProxyOptions for more info. + """ + self.proxyOptions.update(kwds) + + def processRequests(self): + """Process all pending requests from the pipe, return + after no more events are immediately available. (non-blocking) + Returns the number of events processed. + """ + if self.exited: + raise ExitError() + + numProcessed = 0 + while self.conn.poll(): + try: + self.handleRequest() + numProcessed += 1 + except ExitError: + self.exited = True + raise + except IOError as err: + if err.errno == 4: ## interrupted system call; try again + continue + else: + raise + except: + print "Error in process %s" % self.name + sys.excepthook(*sys.exc_info()) + + return numProcessed + + def handleRequest(self): + """Handle a single request from the remote process. + Blocks until a request is available.""" + result = None + try: + cmd, reqId, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails + except EOFError: + ## remote process has shut down; end event loop + raise ExitError() + except IOError: + raise ExitError() + #print os.getpid(), "received request:", cmd, reqId + + + try: + if cmd == 'result' or cmd == 'error': + resultId = reqId + reqId = None ## prevents attempt to return information from this request + ## (this is already a return from a previous request) + + opts = pickle.loads(optStr) + #print os.getpid(), "received request:", cmd, reqId, opts + returnType = opts.get('returnType', 'auto') + + if cmd == 'result': + self.results[resultId] = ('result', opts['result']) + elif cmd == 'error': + self.results[resultId] = ('error', (opts['exception'], opts['excString'])) + elif cmd == 'getObjAttr': + result = getattr(opts['obj'], opts['attr']) + elif cmd == 'callObj': + obj = opts['obj'] + fnargs = opts['args'] + fnkwds = opts['kwds'] + if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments. + #print obj, fnargs + result = obj(*fnargs) + else: + result = obj(*fnargs, **fnkwds) + elif cmd == 'getObjValue': + result = opts['obj'] ## has already been unpickled into its local value + returnType = 'value' + elif cmd == 'transfer': + result = opts['obj'] + returnType = 'proxy' + elif cmd == 'import': + name = opts['module'] + fromlist = opts.get('fromlist', []) + mod = __builtin__.__import__(name, fromlist=fromlist) + + if len(fromlist) == 0: + parts = name.lstrip('.').split('.') + result = mod + for part in parts[1:]: + result = getattr(result, part) + else: + result = map(mod.__getattr__, fromlist) + + elif cmd == 'del': + LocalObjectProxy.releaseProxyId(opts['proxyId']) + #del self.proxiedObjects[opts['objId']] + + elif cmd == 'close': + if reqId is not None: + result = True + returnType = 'value' + + exc = None + except: + exc = sys.exc_info() + + + + if reqId is not None: + if exc is None: + #print "returnValue:", returnValue, result + if returnType == 'auto': + result = self.autoProxy(result, self.proxyOptions['noProxyTypes']) + elif returnType == 'proxy': + result = LocalObjectProxy(result) + + try: + self.replyResult(reqId, result) + except: + sys.excepthook(*sys.exc_info()) + self.replyError(reqId, *sys.exc_info()) + else: + self.replyError(reqId, *exc) + + elif exc is not None: + sys.excepthook(*exc) + + if cmd == 'close': + if opts.get('noCleanup', False) is True: + os._exit(0) ## exit immediately, do not pass GO, do not collect $200. + ## (more importantly, do not call any code that would + ## normally be invoked at exit) + else: + raise ExitError() + + + + def replyResult(self, reqId, result): + self.send(request='result', reqId=reqId, callSync='off', opts=dict(result=result)) + + def replyError(self, reqId, *exc): + print "error:", self.name, reqId, exc[1] + excStr = traceback.format_exception(*exc) + try: + self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=exc[1], excString=excStr)) + except: + self.send(request='error', reqId=reqId, callSync='off', opts=dict(exception=None, excString=excStr)) + + def send(self, request, opts=None, reqId=None, callSync='sync', timeout=10, returnType=None, **kwds): + """Send a request or return packet to the remote process. + Generally it is not necessary to call this method directly; it is for internal use. + (The docstring has information that is nevertheless useful to the programmer + as it describes the internal protocol used to communicate between processes) + + ========== ==================================================================== + Arguments: + request String describing the type of request being sent (see below) + reqId Integer uniquely linking a result back to the request that generated + it. (most requests leave this blank) + callSync 'sync': return the actual result of the request + 'async': return a Request object which can be used to look up the + result later + 'off': return no result + timeout Time in seconds to wait for a response when callSync=='sync' + opts Extra arguments sent to the remote process that determine the way + the request will be handled (see below) + returnType 'proxy', 'value', or 'auto' + ========== ==================================================================== + + Description of request strings and options allowed for each: + + ============= ============= ======================================================== + request option description + ------------- ------------- -------------------------------------------------------- + getObjAttr Request the remote process return (proxy to) an + attribute of an object. + obj reference to object whose attribute should be + returned + attr string name of attribute to return + returnValue bool or 'auto' indicating whether to return a proxy or + the actual value. + + callObj Request the remote process call a function or + method. If a request ID is given, then the call's + return value will be sent back (or information + about the error that occurred while running the + function) + obj the (reference to) object to call + args tuple of arguments to pass to callable + kwds dict of keyword arguments to pass to callable + returnValue bool or 'auto' indicating whether to return a proxy or + the actual value. + + getObjValue Request the remote process return the value of + a proxied object (must be picklable) + obj reference to object whose value should be returned + + transfer Copy an object to the remote process and request + it return a proxy for the new object. + obj The object to transfer. + + import Request the remote process import new symbols + and return proxy(ies) to the imported objects + module the string name of the module to import + fromlist optional list of string names to import from module + + del Inform the remote process that a proxy has been + released (thus the remote process may be able to + release the original object) + proxyId id of proxy which is no longer referenced by + remote host + + close Instruct the remote process to stop its event loop + and exit. Optionally, this request may return a + confirmation. + + result Inform the remote process that its request has + been processed + result return value of a request + + error Inform the remote process that its request failed + exception the Exception that was raised (or None if the + exception could not be pickled) + excString string-formatted version of the exception and + traceback + ============= ===================================================================== + """ + #if len(kwds) > 0: + #print "Warning: send() ignored args:", kwds + + if opts is None: + opts = {} + + assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' + if reqId is None: + if callSync != 'off': ## requested return value; use the next available request ID + reqId = self.nextRequestId + self.nextRequestId += 1 + else: + ## If requestId is provided, this _must_ be a response to a previously received request. + assert request in ['result', 'error'] + + if returnType is not None: + opts['returnType'] = returnType + #print "send", opts + ## double-pickle args to ensure that at least status and request ID get through + try: + optStr = pickle.dumps(opts) + except: + print "==== Error pickling this object: ====" + print opts + print "=======================================" + raise + + request = (request, reqId, optStr) + self.conn.send(request) + + if callSync == 'off': + return + + req = Request(self, reqId, description=str(request), timeout=timeout) + if callSync == 'async': + return req + + if callSync == 'sync': + try: + return req.result() + except NoResultError: + return req + + def close(self, callSync='off', noCleanup=False, **kwds): + self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds) + + def getResult(self, reqId): + ## raises NoResultError if the result is not available yet + #print self.results.keys(), os.getpid() + if reqId not in self.results: + #self.readPipe() + try: + self.processRequests() + except ExitError: + pass + if reqId not in self.results: + raise NoResultError() + status, result = self.results.pop(reqId) + if status == 'result': + return result + elif status == 'error': + #print ''.join(result) + exc, excStr = result + if exc is not None: + print "===== Remote process raised exception on request: =====" + print ''.join(excStr) + print "===== Local Traceback to request follows: =====" + raise exc + else: + print ''.join(excStr) + raise Exception("Error getting result. See above for exception from remote process.") + + else: + raise Exception("Internal error.") + + def _import(self, mod, **kwds): + """ + Request the remote process import a module (or symbols from a module) + and return the proxied results. Uses built-in __import__() function, but + adds a bit more processing: + + _import('module') => returns module + _import('module.submodule') => returns submodule + (note this differs from behavior of __import__) + _import('module', fromlist=[name1, name2, ...]) => returns [module.name1, module.name2, ...] + (this also differs from behavior of __import__) + + """ + return self.send(request='import', callSync='sync', opts=dict(module=mod), **kwds) + + def getObjAttr(self, obj, attr, **kwds): + return self.send(request='getObjAttr', opts=dict(obj=obj, attr=attr), **kwds) + + def getObjValue(self, obj, **kwds): + return self.send(request='getObjValue', opts=dict(obj=obj), **kwds) + + def callObj(self, obj, args, kwds, **opts): + opts = opts.copy() + noProxyTypes = opts.pop('noProxyTypes', None) + if noProxyTypes is None: + noProxyTypes = self.proxyOptions['noProxyTypes'] + autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy']) + + if autoProxy is True: + args = tuple([self.autoProxy(v, noProxyTypes) for v in args]) + for k, v in kwds.iteritems(): + opts[k] = self.autoProxy(v, noProxyTypes) + + return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), **opts) + + def registerProxy(self, proxy): + ref = weakref.ref(proxy, self.deleteProxy) + self.proxies[ref] = proxy._proxyId + + def deleteProxy(self, ref): + proxyId = self.proxies.pop(ref) + try: + self.send(request='del', opts=dict(proxyId=proxyId), callSync='off') + except IOError: ## if remote process has closed down, there is no need to send delete requests anymore + pass + + def transfer(self, obj, **kwds): + """ + Transfer an object by value to the remote host (the object must be picklable) + and return a proxy for the new remote object. + """ + return self.send(request='transfer', opts=dict(obj=obj), **kwds) + + def autoProxy(self, obj, noProxyTypes): + ## Return object wrapped in LocalObjectProxy _unless_ its type is in noProxyTypes. + for typ in noProxyTypes: + if isinstance(obj, typ): + return obj + return LocalObjectProxy(obj) + + +class Request(object): + """ + Request objects are returned when calling an ObjectProxy in asynchronous mode + or if a synchronous call has timed out. Use hasResult() to ask whether + the result of the call has been returned yet. Use result() to get + the returned value. + """ + def __init__(self, process, reqId, description=None, timeout=10): + self.proc = process + self.description = description + self.reqId = reqId + self.gotResult = False + self._result = None + self.timeout = timeout + + def result(self, block=True, timeout=None): + """ + Return the result for this request. + + If block is True, wait until the result has arrived or *timeout* seconds passes. + If the timeout is reached, raise NoResultError. (use timeout=None to disable) + If block is False, raise NoResultError immediately if the result has not arrived yet. + """ + + if self.gotResult: + return self._result + + if timeout is None: + timeout = self.timeout + + if block: + start = time.time() + while not self.hasResult(): + time.sleep(0.005) + if timeout >= 0 and time.time() - start > timeout: + print "Request timed out:", self.description + import traceback + traceback.print_stack() + raise NoResultError() + return self._result + else: + self._result = self.proc.getResult(self.reqId) ## raises NoResultError if result is not available yet + self.gotResult = True + return self._result + + def hasResult(self): + """Returns True if the result for this request has arrived.""" + try: + self.result(block=False) + except NoResultError: + pass + + return self.gotResult + +class LocalObjectProxy(object): + """ + Used for wrapping local objects to ensure that they are send by proxy to a remote host. + Note that 'proxy' is just a shorter alias for LocalObjectProxy. + + For example:: + + data = [1,2,3,4,5] + remotePlot.plot(data) ## by default, lists are pickled and sent by value + remotePlot.plot(proxy(data)) ## force the object to be sent by proxy + + """ + nextProxyId = 0 + proxiedObjects = {} ## maps {proxyId: object} + + + @classmethod + def registerObject(cls, obj): + ## assign it a unique ID so we can keep a reference to the local object + + pid = cls.nextProxyId + cls.nextProxyId += 1 + cls.proxiedObjects[pid] = obj + #print "register:", cls.proxiedObjects + return pid + + @classmethod + def lookupProxyId(cls, pid): + return cls.proxiedObjects[pid] + + @classmethod + def releaseProxyId(cls, pid): + del cls.proxiedObjects[pid] + #print "release:", cls.proxiedObjects + + def __init__(self, obj, **opts): + """ + Create a 'local' proxy object that, when sent to a remote host, + will appear as a normal ObjectProxy to *obj*. + Any extra keyword arguments are passed to proxy._setProxyOptions() + on the remote side. + """ + self.processId = os.getpid() + #self.objectId = id(obj) + self.typeStr = repr(obj) + #self.handler = handler + self.obj = obj + self.opts = opts + + def __reduce__(self): + ## a proxy is being pickled and sent to a remote process. + ## every time this happens, a new proxy will be generated in the remote process, + ## so we keep a new ID so we can track when each is released. + pid = LocalObjectProxy.registerObject(self.obj) + return (unpickleObjectProxy, (self.processId, pid, self.typeStr, None, self.opts)) + +## alias +proxy = LocalObjectProxy + +def unpickleObjectProxy(processId, proxyId, typeStr, attributes=None, opts=None): + if processId == os.getpid(): + obj = LocalObjectProxy.lookupProxyId(proxyId) + if attributes is not None: + for attr in attributes: + obj = getattr(obj, attr) + return obj + else: + proxy = ObjectProxy(processId, proxyId=proxyId, typeStr=typeStr) + if opts is not None: + proxy._setProxyOptions(**opts) + return proxy + +class ObjectProxy(object): + """ + Proxy to an object stored by the remote process. Proxies are created + by calling Process._import(), Process.transfer(), or by requesting/calling + attributes on existing proxy objects. + + For the most part, this object can be used exactly as if it + were a local object:: + + rsys = proc._import('sys') # returns proxy to sys module on remote process + rsys.stdout # proxy to remote sys.stdout + rsys.stdout.write # proxy to remote sys.stdout.write + rsys.stdout.write('hello') # calls sys.stdout.write('hello') on remote machine + # and returns the result (None) + + When calling a proxy to a remote function, the call can be made synchronous + (result of call is returned immediately), asynchronous (result is returned later), + or return can be disabled entirely:: + + ros = proc._import('os') + + ## synchronous call; result is returned immediately + pid = ros.getpid() + + ## asynchronous call + request = ros.getpid(_callSync='async') + while not request.hasResult(): + time.sleep(0.01) + pid = request.result() + + ## disable return when we know it isn't needed + rsys.stdout.write('hello', _callSync='off') + + Additionally, values returned from a remote function call are automatically + returned either by value (must be picklable) or by proxy. + This behavior can be forced:: + + rnp = proc._import('numpy') + arrProxy = rnp.array([1,2,3,4], _returnType='proxy') + arrValue = rnp.array([1,2,3,4], _returnType='value') + + The default callSync and returnType behaviors (as well as others) can be set + for each proxy individually using ObjectProxy._setProxyOptions() or globally using + proc.setProxyOptions(). + + """ + def __init__(self, processId, proxyId, typeStr='', parent=None): + object.__init__(self) + ## can't set attributes directly because setattr is overridden. + self.__dict__['_processId'] = processId + self.__dict__['_typeStr'] = typeStr + self.__dict__['_proxyId'] = proxyId + self.__dict__['_attributes'] = () + ## attributes that affect the behavior of the proxy. + ## in all cases, a value of None causes the proxy to ask + ## its parent event handler to make the decision + self.__dict__['_proxyOptions'] = { + 'callSync': None, ## 'sync', 'async', None + 'timeout': None, ## float, None + 'returnType': None, ## 'proxy', 'value', 'auto', None + 'deferGetattr': None, ## True, False, None + 'noProxyTypes': None, ## list of types to send by value instead of by proxy + } + + self.__dict__['_handler'] = RemoteEventHandler.getHandler(processId) + self.__dict__['_handler'].registerProxy(self) ## handler will watch proxy; inform remote process when the proxy is deleted. + + def _setProxyOptions(self, **kwds): + """ + Change the behavior of this proxy. For all options, a value of None + will cause the proxy to instead use the default behavior defined + by its parent Process. + + Options are: + + ============= ============================================================= + callSync 'sync', 'async', 'off', or None. + If 'async', then calling methods will return a Request object + which can be used to inquire later about the result of the + method call. + If 'sync', then calling a method + will block until the remote process has returned its result + or the timeout has elapsed (in this case, a Request object + is returned instead). + If 'off', then the remote process is instructed _not_ to + reply and the method call will return None immediately. + returnType 'auto', 'proxy', 'value', or None. + If 'proxy', then the value returned when calling a method + will be a proxy to the object on the remote process. + If 'value', then attempt to pickle the returned object and + send it back. + If 'auto', then the decision is made by consulting the + 'noProxyTypes' option. + autoProxy bool or None. If True, arguments to __call__ are + automatically converted to proxy unless their type is + listed in noProxyTypes (see below). If False, arguments + are left untouched. Use proxy(obj) to manually convert + arguments before sending. + timeout float or None. Length of time to wait during synchronous + requests before returning a Request object instead. + deferGetattr True, False, or None. + If False, all attribute requests will be sent to the remote + process immediately and will block until a response is + received (or timeout has elapsed). + If True, requesting an attribute from the proxy returns a + new proxy immediately. The remote process is _not_ contacted + to make this request. This is faster, but it is possible to + request an attribute that does not exist on the proxied + object. In this case, AttributeError will not be raised + until an attempt is made to look up the attribute on the + remote process. + noProxyTypes List of object types that should _not_ be proxied when + sent to the remote process. + ============= ============================================================= + """ + self._proxyOptions.update(kwds) + + def _getValue(self): + """ + Return the value of the proxied object + (the remote object must be picklable) + """ + return self._handler.getObjValue(self) + + def _getProxyOption(self, opt): + val = self._proxyOptions[opt] + if val is None: + return self._handler.getProxyOption(opt) + return val + + def _getProxyOptions(self): + return {k: self._getProxyOption(k) for k in self._proxyOptions} + + def __reduce__(self): + return (unpickleObjectProxy, (self._processId, self._proxyId, self._typeStr, self._attributes)) + + def __repr__(self): + #objRepr = self.__getattr__('__repr__')(callSync='value') + return "" % (self._processId, self._proxyId, self._typeStr) + + + def __getattr__(self, attr, **kwds): + """ + Calls __getattr__ on the remote object and returns the attribute + by value or by proxy depending on the options set (see + ObjectProxy._setProxyOptions and RemoteEventHandler.setProxyOptions) + + If the option 'deferGetattr' is True for this proxy, then a new proxy object + is returned _without_ asking the remote object whether the named attribute exists. + This can save time when making multiple chained attribute requests, + but may also defer a possible AttributeError until later, making + them more difficult to debug. + """ + opts = self._getProxyOptions() + for k in opts: + if '_'+k in kwds: + opts[k] = kwds.pop('_'+k) + if opts['deferGetattr'] is True: + return self._deferredAttr(attr) + else: + #opts = self._getProxyOptions() + return self._handler.getObjAttr(self, attr, **opts) + + def _deferredAttr(self, attr): + return DeferredObjectProxy(self, attr) + + def __call__(self, *args, **kwds): + """ + Attempts to call the proxied object from the remote process. + Accepts extra keyword arguments: + + _callSync 'off', 'sync', or 'async' + _returnType 'value', 'proxy', or 'auto' + + If the remote call raises an exception on the remote process, + it will be re-raised on the local process. + + """ + opts = self._getProxyOptions() + for k in opts: + if '_'+k in kwds: + opts[k] = kwds.pop('_'+k) + return self._handler.callObj(obj=self, args=args, kwds=kwds, **opts) + + + ## Explicitly proxy special methods. Is there a better way to do this?? + + def _getSpecialAttr(self, attr): + ## this just gives us an easy way to change the behavior of the special methods + return self._deferredAttr(attr) + + def __getitem__(self, *args): + return self._getSpecialAttr('__getitem__')(*args) + + def __setitem__(self, *args): + return self._getSpecialAttr('__setitem__')(*args, _callSync='off') + + def __setattr__(self, *args): + return self._getSpecialAttr('__setattr__')(*args, _callSync='off') + + def __str__(self, *args): + return self._getSpecialAttr('__str__')(*args, _returnType='value') + + def __len__(self, *args): + return self._getSpecialAttr('__len__')(*args) + + def __add__(self, *args): + return self._getSpecialAttr('__add__')(*args) + + def __sub__(self, *args): + return self._getSpecialAttr('__sub__')(*args) + + def __div__(self, *args): + return self._getSpecialAttr('__div__')(*args) + + def __mul__(self, *args): + return self._getSpecialAttr('__mul__')(*args) + + def __pow__(self, *args): + return self._getSpecialAttr('__pow__')(*args) + + def __iadd__(self, *args): + return self._getSpecialAttr('__iadd__')(*args, _callSync='off') + + def __isub__(self, *args): + return self._getSpecialAttr('__isub__')(*args, _callSync='off') + + def __idiv__(self, *args): + return self._getSpecialAttr('__idiv__')(*args, _callSync='off') + + def __imul__(self, *args): + return self._getSpecialAttr('__imul__')(*args, _callSync='off') + + def __ipow__(self, *args): + return self._getSpecialAttr('__ipow__')(*args, _callSync='off') + + def __rshift__(self, *args): + return self._getSpecialAttr('__rshift__')(*args) + + def __lshift__(self, *args): + return self._getSpecialAttr('__lshift__')(*args) + + def __floordiv__(self, *args): + return self._getSpecialAttr('__pow__')(*args) + + def __irshift__(self, *args): + return self._getSpecialAttr('__rshift__')(*args, _callSync='off') + + def __ilshift__(self, *args): + return self._getSpecialAttr('__lshift__')(*args, _callSync='off') + + def __ifloordiv__(self, *args): + return self._getSpecialAttr('__pow__')(*args, _callSync='off') + + def __eq__(self, *args): + return self._getSpecialAttr('__eq__')(*args) + + def __ne__(self, *args): + return self._getSpecialAttr('__ne__')(*args) + + def __lt__(self, *args): + return self._getSpecialAttr('__lt__')(*args) + + def __gt__(self, *args): + return self._getSpecialAttr('__gt__')(*args) + + def __le__(self, *args): + return self._getSpecialAttr('__le__')(*args) + + def __ge__(self, *args): + return self._getSpecialAttr('__ge__')(*args) + + def __and__(self, *args): + return self._getSpecialAttr('__and__')(*args) + + def __or__(self, *args): + return self._getSpecialAttr('__or__')(*args) + + def __xor__(self, *args): + return self._getSpecialAttr('__xor__')(*args) + + def __iand__(self, *args): + return self._getSpecialAttr('__iand__')(*args, _callSync='off') + + def __ior__(self, *args): + return self._getSpecialAttr('__ior__')(*args, _callSync='off') + + def __ixor__(self, *args): + return self._getSpecialAttr('__ixor__')(*args, _callSync='off') + + def __mod__(self, *args): + return self._getSpecialAttr('__mod__')(*args) + + def __radd__(self, *args): + return self._getSpecialAttr('__radd__')(*args) + + def __rsub__(self, *args): + return self._getSpecialAttr('__rsub__')(*args) + + def __rdiv__(self, *args): + return self._getSpecialAttr('__rdiv__')(*args) + + def __rmul__(self, *args): + return self._getSpecialAttr('__rmul__')(*args) + + def __rpow__(self, *args): + return self._getSpecialAttr('__rpow__')(*args) + + def __rrshift__(self, *args): + return self._getSpecialAttr('__rrshift__')(*args) + + def __rlshift__(self, *args): + return self._getSpecialAttr('__rlshift__')(*args) + + def __rfloordiv__(self, *args): + return self._getSpecialAttr('__rpow__')(*args) + + def __rand__(self, *args): + return self._getSpecialAttr('__rand__')(*args) + + def __ror__(self, *args): + return self._getSpecialAttr('__ror__')(*args) + + def __rxor__(self, *args): + return self._getSpecialAttr('__ror__')(*args) + + def __rmod__(self, *args): + return self._getSpecialAttr('__rmod__')(*args) + +class DeferredObjectProxy(ObjectProxy): + """ + This class represents an attribute (or sub-attribute) of a proxied object. + It is used to speed up attribute requests. Take the following scenario:: + + rsys = proc._import('sys') + rsys.stdout.write('hello') + + For this simple example, a total of 4 synchronous requests are made to + the remote process: + + 1) import sys + 2) getattr(sys, 'stdout') + 3) getattr(stdout, 'write') + 4) write('hello') + + This takes a lot longer than running the equivalent code locally. To + speed things up, we can 'defer' the two attribute lookups so they are + only carried out when neccessary:: + + rsys = proc._import('sys') + rsys._setProxyOptions(deferGetattr=True) + rsys.stdout.write('hello') + + This example only makes two requests to the remote process; the two + attribute lookups immediately return DeferredObjectProxy instances + immediately without contacting the remote process. When the call + to write() is made, all attribute requests are processed at the same time. + + Note that if the attributes requested do not exist on the remote object, + making the call to write() will raise an AttributeError. + """ + def __init__(self, parentProxy, attribute): + ## can't set attributes directly because setattr is overridden. + for k in ['_processId', '_typeStr', '_proxyId', '_handler']: + self.__dict__[k] = getattr(parentProxy, k) + self.__dict__['_parent'] = parentProxy ## make sure parent stays alive + self.__dict__['_attributes'] = parentProxy._attributes + (attribute,) + self.__dict__['_proxyOptions'] = parentProxy._proxyOptions.copy() + + def __repr__(self): + return ObjectProxy.__repr__(self) + '.' + '.'.join(self._attributes) + + def _undefer(self): + """ + Return a non-deferred ObjectProxy referencing the same object + """ + return self._parent.__getattr__(self._attributes[-1], _deferGetattr=False) + diff --git a/pyqtgraph/numpy_fix.py b/pyqtgraph/numpy_fix.py new file mode 100644 index 00000000..2fa8ef1f --- /dev/null +++ b/pyqtgraph/numpy_fix.py @@ -0,0 +1,22 @@ +try: + import numpy as np + + ## Wrap np.concatenate to catch and avoid a segmentation fault bug + ## (numpy trac issue #2084) + if not hasattr(np, 'concatenate_orig'): + np.concatenate_orig = np.concatenate + def concatenate(vals, *args, **kwds): + """Wrapper around numpy.concatenate (see pyqtgraph/numpy_fix.py)""" + dtypes = [getattr(v, 'dtype', None) for v in vals] + names = [getattr(dt, 'names', None) for dt in dtypes] + if len(dtypes) < 2 or all([n is None for n in names]): + return np.concatenate_orig(vals, *args, **kwds) + if any([dt != dtypes[0] for dt in dtypes[1:]]): + raise TypeError("Cannot concatenate structured arrays of different dtype.") + return np.concatenate_orig(vals, *args, **kwds) + + np.concatenate = concatenate + +except ImportError: + pass + diff --git a/pyqtgraph/opengl/GLGraphicsItem.py b/pyqtgraph/opengl/GLGraphicsItem.py new file mode 100644 index 00000000..9babec3a --- /dev/null +++ b/pyqtgraph/opengl/GLGraphicsItem.py @@ -0,0 +1,267 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph import Transform3D +from OpenGL.GL import * +from OpenGL import GL + +GLOptions = { + 'opaque': { + GL_DEPTH_TEST: True, + GL_BLEND: False, + GL_ALPHA_TEST: False, + GL_CULL_FACE: False, + }, + 'translucent': { + GL_DEPTH_TEST: True, + GL_BLEND: True, + GL_ALPHA_TEST: False, + GL_CULL_FACE: False, + 'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), + }, + 'additive': { + GL_DEPTH_TEST: False, + GL_BLEND: True, + GL_ALPHA_TEST: False, + GL_CULL_FACE: False, + 'glBlendFunc': (GL_SRC_ALPHA, GL_ONE), + }, +} + + +class GLGraphicsItem(QtCore.QObject): + def __init__(self, parentItem=None): + QtCore.QObject.__init__(self) + self.__parent = None + self.__view = None + self.__children = set() + self.__transform = Transform3D() + self.__visible = True + self.setParentItem(parentItem) + self.setDepthValue(0) + self.__glOpts = {} + + def setParentItem(self, item): + if self.__parent is not None: + self.__parent.__children.remove(self) + if item is not None: + item.__children.add(self) + self.__parent = item + + if self.__parent is not None and self.view() is not self.__parent.view(): + if self.view() is not None: + self.view().removeItem(self) + self.__parent.view().addItem(self) + + def setGLOptions(self, opts): + """ + Set the OpenGL state options to use immediately before drawing this item. + (Note that subclasses must call setupGLState before painting for this to work) + + The simplest way to invoke this method is to pass in the name of + a predefined set of options (see the GLOptions variable): + + ============= ====================================================== + opaque Enables depth testing and disables blending + translucent Enables depth testing and blending + Elements must be drawn sorted back-to-front for + translucency to work correctly. + additive Disables depth testing, enables blending. + Colors are added together, so sorting is not required. + ============= ====================================================== + + It is also possible to specify any arbitrary settings as a dictionary. + This may consist of {'functionName': (args...)} pairs where functionName must + be a callable attribute of OpenGL.GL, or {GL_STATE_VAR: bool} pairs + which will be interpreted as calls to glEnable or glDisable(GL_STATE_VAR). + + For example:: + + { + GL_ALPHA_TEST: True, + GL_CULL_FACE: False, + 'glBlendFunc': (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA), + } + + + """ + if isinstance(opts, basestring): + opts = GLOptions[opts] + self.__glOpts = opts.copy() + self.update() + + def updateGLOptions(self, opts): + """ + Modify the OpenGL state options to use immediately before drawing this item. + *opts* must be a dictionary as specified by setGLOptions. + Values may also be None, in which case the key will be ignored. + """ + self.__glOpts.update(opts) + + + def parentItem(self): + return self.__parent + + def childItems(self): + return list(self.__children) + + def _setView(self, v): + self.__view = v + + def view(self): + return self.__view + + def setDepthValue(self, value): + """ + Sets the depth value of this item. Default is 0. + This controls the order in which items are drawn--those with a greater depth value will be drawn later. + Items with negative depth values are drawn before their parent. + (This is analogous to QGraphicsItem.zValue) + The depthValue does NOT affect the position of the item or the values it imparts to the GL depth buffer. + '""" + self.__depthValue = value + + def depthValue(self): + """Return the depth value of this item. See setDepthValue for mode information.""" + return self.__depthValue + + def setTransform(self, tr): + self.__transform = Transform3D(tr) + self.update() + + def resetTransform(self): + self.__transform.setToIdentity() + self.update() + + def applyTransform(self, tr, local): + """ + Multiply this object's transform by *tr*. + If local is True, then *tr* is multiplied on the right of the current transform: + newTransform = transform * tr + If local is False, then *tr* is instead multiplied on the left: + newTransform = tr * transform + """ + if local: + self.setTransform(self.transform() * tr) + else: + self.setTransform(tr * self.transform()) + + def transform(self): + return self.__transform + + def viewTransform(self): + tr = self.__transform + p = self + while True: + p = p.parentItem() + if p is None: + break + tr = p.transform() * tr + return Transform3D(tr) + + def translate(self, dx, dy, dz, local=False): + """ + Translate the object by (*dx*, *dy*, *dz*) in its parent's coordinate system. + If *local* is True, then translation takes place in local coordinates. + """ + tr = Transform3D() + tr.translate(dx, dy, dz) + self.applyTransform(tr, local=local) + + def rotate(self, angle, x, y, z, local=False): + """ + Rotate the object around the axis specified by (x,y,z). + *angle* is in degrees. + + """ + tr = Transform3D() + tr.rotate(angle, x, y, z) + self.applyTransform(tr, local=local) + + def scale(self, x, y, z, local=True): + """ + Scale the object by (*dx*, *dy*, *dz*) in its local coordinate system. + If *local* is False, then scale takes place in the parent's coordinates. + """ + tr = Transform3D() + tr.scale(x, y, z) + self.applyTransform(tr, local=local) + + + def hide(self): + self.setVisible(False) + + def show(self): + self.setVisible(True) + + def setVisible(self, vis): + self.__visible = vis + self.update() + + def visible(self): + return self.__visible + + + def initializeGL(self): + """ + Called after an item is added to a GLViewWidget. + The widget's GL context is made current before this method is called. + (So this would be an appropriate time to generate lists, upload textures, etc.) + """ + pass + + def setupGLState(self): + """ + This method is responsible for preparing the GL state options needed to render + this item (blending, depth testing, etc). The method is called immediately before painting the item. + """ + for k,v in self.__glOpts.items(): + if v is None: + continue + if isinstance(k, basestring): + func = getattr(GL, k) + func(*v) + else: + if v is True: + glEnable(k) + else: + glDisable(k) + + def paint(self): + """ + Called by the GLViewWidget to draw this item. + It is the responsibility of the item to set up its own modelview matrix, + but the caller will take care of pushing/popping. + """ + self.setupGLState() + + def update(self): + v = self.view() + if v is None: + return + v.updateGL() + + def mapToParent(self, point): + tr = self.transform() + if tr is None: + return point + return tr.map(point) + + def mapFromParent(self, point): + tr = self.transform() + if tr is None: + return point + return tr.inverted()[0].map(point) + + def mapToView(self, point): + tr = self.viewTransform() + if tr is None: + return point + return tr.map(point) + + def mapFromView(self, point): + tr = self.viewTransform() + if tr is None: + return point + return tr.inverted()[0].map(point) + + + \ No newline at end of file diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py new file mode 100644 index 00000000..d1c1d090 --- /dev/null +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -0,0 +1,289 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from OpenGL.GL import * +import numpy as np +from pyqtgraph import Vector +##Vector = QtGui.QVector3D + +class GLViewWidget(QtOpenGL.QGLWidget): + """ + Basic widget for displaying 3D data + - Rotation/scale controls + - Axis/grid display + - Export options + + """ + + ShareWidget = None + + def __init__(self, parent=None): + if GLViewWidget.ShareWidget is None: + ## create a dummy widget to allow sharing objects (textures, shaders, etc) between views + GLViewWidget.ShareWidget = QtOpenGL.QGLWidget() + + QtOpenGL.QGLWidget.__init__(self, parent, GLViewWidget.ShareWidget) + + self.setFocusPolicy(QtCore.Qt.ClickFocus) + + self.opts = { + 'center': Vector(0,0,0), ## will always appear at the center of the widget + 'distance': 10.0, ## distance of camera from center + 'fov': 60, ## horizontal field of view in degrees + 'elevation': 30, ## camera's angle of elevation in degrees + 'azimuth': 45, ## camera's azimuthal angle in degrees + ## (rotation around z-axis 0 points along x-axis) + } + self.items = [] + self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown] + self.keysPressed = {} + self.keyTimer = QtCore.QTimer() + self.keyTimer.timeout.connect(self.evalKeyState) + + self.makeCurrent() + + def addItem(self, item): + self.items.append(item) + if hasattr(item, 'initializeGL'): + self.makeCurrent() + try: + item.initializeGL() + except: + self.checkOpenGLVersion('Error while adding item %s to GLViewWidget.' % str(item)) + + item._setView(self) + #print "set view", item, self, item.view() + self.update() + + def removeItem(self, item): + self.items.remove(item) + item._setView(None) + self.update() + + + def initializeGL(self): + glClearColor(0.0, 0.0, 0.0, 0.0) + self.resizeGL(self.width(), self.height()) + + def resizeGL(self, w, h): + glViewport(0, 0, w, h) + #self.update() + + def setProjection(self): + ## Create the projection matrix + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + w = self.width() + h = self.height() + dist = self.opts['distance'] + fov = self.opts['fov'] + + nearClip = dist * 0.001 + farClip = dist * 1000. + + r = nearClip * np.tan(fov * 0.5 * np.pi / 180.) + t = r * h / w + glFrustum( -r, r, -t, t, nearClip, farClip) + + def setModelview(self): + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + glTranslatef( 0.0, 0.0, -self.opts['distance']) + glRotatef(self.opts['elevation']-90, 1, 0, 0) + glRotatef(self.opts['azimuth']+90, 0, 0, -1) + center = self.opts['center'] + glTranslatef(-center.x(), -center.y(), -center.z()) + + + def paintGL(self): + self.setProjection() + self.setModelview() + glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + self.drawItemTree() + + def drawItemTree(self, item=None): + if item is None: + items = [x for x in self.items if x.parentItem() is None] + else: + items = item.childItems() + items.append(item) + items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue())) + for i in items: + if not i.visible(): + continue + if i is item: + try: + glPushAttrib(GL_ALL_ATTRIB_BITS) + i.paint() + except: + import pyqtgraph.debug + pyqtgraph.debug.printExc() + msg = "Error while drawing item %s." % str(item) + ver = glGetString(GL_VERSION) + if ver is not None: + ver = ver.split()[0] + if int(ver.split('.')[0]) < 2: + print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) + else: + print(msg) + + finally: + glPopAttrib() + else: + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + try: + tr = i.transform() + a = np.array(tr.copyDataTo()).reshape((4,4)) + glMultMatrixf(a.transpose()) + self.drawItemTree(i) + finally: + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + + def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None): + if distance is not None: + self.opts['distance'] = distance + if elevation is not None: + self.opts['elevation'] = elevation + if azimuth is not None: + self.opts['azimuth'] = azimuth + self.update() + + + + def cameraPosition(self): + """Return current position of camera based on center, dist, elevation, and azimuth""" + center = self.opts['center'] + dist = self.opts['distance'] + elev = self.opts['elevation'] * np.pi/180. + azim = self.opts['azimuth'] * np.pi/180. + + pos = Vector( + center.x() + dist * np.cos(elev) * np.cos(azim), + center.y() + dist * np.cos(elev) * np.sin(azim), + center.z() + dist * np.sin(elev) + ) + + return pos + + def orbit(self, azim, elev): + """Orbits the camera around the center position. *azim* and *elev* are given in degrees.""" + self.opts['azimuth'] += azim + self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90) + self.update() + + def pan(self, dx, dy, dz, relative=False): + """ + Moves the center (look-at) position while holding the camera in place. + + If relative=True, then the coordinates are interpreted such that x + if in the global xy plane and points to the right side of the view, y is + in the global xy plane and orthogonal to x, and z points in the global z + direction. Distances are scaled roughly such that a value of 1.0 moves + by one pixel on screen. + + """ + if not relative: + self.opts['center'] += QtGui.QVector3D(dx, dy, dz) + else: + cPos = self.cameraPosition() + cVec = self.opts['center'] - cPos + dist = cVec.length() ## distance from camera to center + xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) ## approx. width of view at distance of center point + xScale = xDist / self.width() + zVec = QtGui.QVector3D(0,0,1) + xVec = QtGui.QVector3D.crossProduct(zVec, cVec).normalized() + yVec = QtGui.QVector3D.crossProduct(xVec, zVec).normalized() + self.opts['center'] = self.opts['center'] + xVec * xScale * dx + yVec * xScale * dy + zVec * xScale * dz + self.update() + + def pixelSize(self, pos): + """ + Return the approximate size of a screen pixel at the location pos + Pos may be a Vector or an (N,3) array of locations + """ + cam = self.cameraPosition() + if isinstance(pos, np.ndarray): + cam = np.array(cam).reshape((1,)*(pos.ndim-1)+(3,)) + dist = ((pos-cam)**2).sum(axis=-1)**0.5 + else: + dist = (pos-cam).length() + xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) + return xDist / self.width() + + def mousePressEvent(self, ev): + self.mousePos = ev.pos() + + def mouseMoveEvent(self, ev): + diff = ev.pos() - self.mousePos + self.mousePos = ev.pos() + + if ev.buttons() == QtCore.Qt.LeftButton: + self.orbit(-diff.x(), diff.y()) + #print self.opts['azimuth'], self.opts['elevation'] + elif ev.buttons() == QtCore.Qt.MidButton: + if (ev.modifiers() & QtCore.Qt.ControlModifier): + self.pan(diff.x(), 0, diff.y(), relative=True) + else: + self.pan(diff.x(), diff.y(), 0, relative=True) + + def mouseReleaseEvent(self, ev): + pass + + def wheelEvent(self, ev): + if (ev.modifiers() & QtCore.Qt.ControlModifier): + self.opts['fov'] *= 0.999**ev.delta() + else: + self.opts['distance'] *= 0.999**ev.delta() + self.update() + + def keyPressEvent(self, ev): + if ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + self.keysPressed[ev.key()] = 1 + self.evalKeyState() + + def keyReleaseEvent(self, ev): + if ev.key() in self.noRepeatKeys: + ev.accept() + if ev.isAutoRepeat(): + return + try: + del self.keysPressed[ev.key()] + except: + self.keysPressed = {} + self.evalKeyState() + + def evalKeyState(self): + speed = 2.0 + if len(self.keysPressed) > 0: + for key in self.keysPressed: + if key == QtCore.Qt.Key_Right: + self.orbit(azim=-speed, elev=0) + elif key == QtCore.Qt.Key_Left: + self.orbit(azim=speed, elev=0) + elif key == QtCore.Qt.Key_Up: + self.orbit(azim=0, elev=-speed) + elif key == QtCore.Qt.Key_Down: + self.orbit(azim=0, elev=speed) + elif key == QtCore.Qt.Key_PageUp: + pass + elif key == QtCore.Qt.Key_PageDown: + pass + self.keyTimer.start(16) + else: + self.keyTimer.stop() + + def checkOpenGLVersion(self, msg): + ## Only to be called from within exception handler. + ver = glGetString(GL_VERSION).split()[0] + if int(ver.split('.')[0]) < 2: + import pyqtgraph.debug + pyqtgraph.debug.printExc() + raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) + else: + raise + + + \ No newline at end of file diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py new file mode 100644 index 00000000..3e5938d1 --- /dev/null +++ b/pyqtgraph/opengl/MeshData.py @@ -0,0 +1,493 @@ +from pyqtgraph.Qt import QtGui +import pyqtgraph.functions as fn +import numpy as np + +class MeshData(object): + """ + Class for storing and operating on 3D mesh data. May contain: + + - list of vertex locations + - list of edges + - list of triangles + - colors per vertex, edge, or tri + - normals per vertex or tri + + This class handles conversion between the standard [list of vertexes, list of faces] + format (suitable for use with glDrawElements) and 'indexed' [list of vertexes] format + (suitable for use with glDrawArrays). It will automatically compute face normal + vectors as well as averaged vertex normal vectors. + + The class attempts to be as efficient as possible in caching conversion results and + avoiding unnecessary conversions. + """ + + def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None): + """ + ============= ===================================================== + Arguments + vertexes (Nv, 3) array of vertex coordinates. + If faces is not specified, then this will instead be + interpreted as (Nf, 3, 3) array of coordinates. + faces (Nf, 3) array of indexes into the vertex array. + edges [not available yet] + vertexColors (Nv, 4) array of vertex colors. + If faces is not specified, then this will instead be + interpreted as (Nf, 3, 4) array of colors. + faceColors (Nf, 4) array of face colors. + ============= ===================================================== + + All arguments are optional. + """ + self._vertexes = None # (Nv,3) array of vertex coordinates + self._vertexesIndexedByFaces = None # (Nf, 3, 3) array of vertex coordinates + self._vertexesIndexedByEdges = None # (Ne, 2, 3) array of vertex coordinates + + ## mappings between vertexes, faces, and edges + self._faces = None # Nx3 array of indexes into self._vertexes specifying three vertexes for each face + self._edges = None + self._vertexFaces = None ## maps vertex ID to a list of face IDs (inverse mapping of _faces) + self._vertexEdges = None ## maps vertex ID to a list of edge IDs (inverse mapping of _edges) + + ## Per-vertex data + self._vertexNormals = None # (Nv, 3) array of normals, one per vertex + self._vertexNormalsIndexedByFaces = None # (Nf, 3, 3) array of normals + self._vertexColors = None # (Nv, 3) array of colors + self._vertexColorsIndexedByFaces = None # (Nf, 3, 4) array of colors + self._vertexColorsIndexedByEdges = None # (Nf, 2, 4) array of colors + + ## Per-face data + self._faceNormals = None # (Nf, 3) array of face normals + self._faceNormalsIndexedByFaces = None # (Nf, 3, 3) array of face normals + self._faceColors = None # (Nf, 4) array of face colors + self._faceColorsIndexedByFaces = None # (Nf, 3, 4) array of face colors + self._faceColorsIndexedByEdges = None # (Ne, 2, 4) array of face colors + + ## Per-edge data + self._edgeColors = None # (Ne, 4) array of edge colors + self._edgeColorsIndexedByEdges = None # (Ne, 2, 4) array of edge colors + #self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given + + + + if vertexes is not None: + if faces is None: + self.setVertexes(vertexes, indexed='faces') + if vertexColors is not None: + self.setVertexColors(vertexColors, indexed='faces') + if faceColors is not None: + self.setFaceColors(faceColors, indexed='faces') + else: + self.setVertexes(vertexes) + self.setFaces(faces) + if vertexColors is not None: + self.setVertexColors(vertexColors) + if faceColors is not None: + self.setFaceColors(faceColors) + + #self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors) + + + #def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None): + #""" + #Set the faces in this data set. + #Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face):: + + #faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ] + + #or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex):: + + #faces = [ (p1, p2, p3), ... ] + #vertexes = [ (x, y, z), ... ] + + #""" + #if not isinstance(vertexes, np.ndarray): + #vertexes = np.array(vertexes) + #if vertexes.dtype != np.float: + #vertexes = vertexes.astype(float) + #if faces is None: + #self._setIndexedFaces(vertexes, vertexColors, faceColors) + #else: + #self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors) + ##print self.vertexes().shape + ##print self.faces().shape + + + #def setMeshColor(self, color): + #"""Set the color of the entire mesh. This removes any per-face or per-vertex colors.""" + #color = fn.Color(color) + #self._meshColor = color.glColor() + #self._vertexColors = None + #self._faceColors = None + + + #def __iter__(self): + #"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face.""" + #vnorms = self.vertexNormals() + #vcolors = self.vertexColors() + #for i in range(self._faces.shape[0]): + #face = [] + #for j in [0,1,2]: + #vind = self._faces[i,j] + #pos = self._vertexes[vind] + #norm = vnorms[vind] + #if vcolors is None: + #color = self._meshColor + #else: + #color = vcolors[vind] + #face.append((pos, norm, color)) + #yield face + + #def __len__(self): + #return len(self._faces) + + def faces(self): + """Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.""" + return self._faces + + def setFaces(self, faces): + """Set the (Nf, 3) array of faces. Each rown in the array contains + three indexes into the vertex array, specifying the three corners + of a triangular face.""" + self._faces = faces + self._vertexFaces = None + self._vertexesIndexedByFaces = None + self.resetNormals() + self._vertexColorsIndexedByFaces = None + self._faceColorsIndexedByFaces = None + + + + def vertexes(self, indexed=None): + """Return an array (N,3) of the positions of vertexes in the mesh. + By default, each unique vertex appears only once in the array. + If indexed is 'faces', then the array will instead contain three vertexes + per face in the mesh (and a single vertex may appear more than once in the array).""" + if indexed is None: + if self._vertexes is None and self._vertexesIndexedByFaces is not None: + self._computeUnindexedVertexes() + return self._vertexes + elif indexed == 'faces': + if self._vertexesIndexedByFaces is None and self._vertexes is not None: + self._vertexesIndexedByFaces = self._vertexes[self.faces()] + return self._vertexesIndexedByFaces + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def setVertexes(self, verts=None, indexed=None, resetNormals=True): + """ + Set the array (Nv, 3) of vertex coordinates. + If indexed=='faces', then the data must have shape (Nf, 3, 3) and is + assumed to be already indexed as a list of faces. + This will cause any pre-existing normal vectors to be cleared + unless resetNormals=False. + """ + if indexed is None: + if verts is not None: + self._vertexes = verts + self._vertexesIndexedByFaces = None + elif indexed=='faces': + self._vertexes = None + if verts is not None: + self._vertexesIndexedByFaces = verts + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + if resetNormals: + self.resetNormals() + + def resetNormals(self): + self._vertexNormals = None + self._vertexNormalsIndexedByFaces = None + self._faceNormals = None + self._faceNormalsIndexedByFaces = None + + + def hasFaceIndexedData(self): + """Return True if this object already has vertex positions indexed by face""" + return self._vertexesIndexedByFaces is not None + + def hasEdgeIndexedData(self): + return self._vertexesIndexedByEdges is not None + + def hasVertexColor(self): + """Return True if this data set has vertex color information""" + for v in (self._vertexColors, self._vertexColorsIndexedByFaces, self._vertexColorsIndexedByEdges): + if v is not None: + return True + return False + + def hasFaceColor(self): + """Return True if this data set has face color information""" + for v in (self._faceColors, self._faceColorsIndexedByFaces, self._faceColorsIndexedByEdges): + if v is not None: + return True + return False + + + def faceNormals(self, indexed=None): + """ + Return an array (Nf, 3) of normal vectors for each face. + If indexed='faces', then instead return an indexed array + (Nf, 3, 3) (this is just the same array with each vector + copied three times). + """ + if self._faceNormals is None: + v = self.vertexes(indexed='faces') + self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0]) + + + if indexed is None: + return self._faceNormals + elif indexed == 'faces': + if self._faceNormalsIndexedByFaces is None: + norms = np.empty((self._faceNormals.shape[0], 3, 3)) + norms[:] = self._faceNormals[:,np.newaxis,:] + self._faceNormalsIndexedByFaces = norms + return self._faceNormalsIndexedByFaces + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def vertexNormals(self, indexed=None): + """ + Return an array of normal vectors. + By default, the array will be (N, 3) with one entry per unique vertex in the mesh. + If indexed is 'faces', then the array will contain three normal vectors per face + (and some vertexes may be repeated). + """ + if self._vertexNormals is None: + faceNorms = self.faceNormals() + vertFaces = self.vertexFaces() + self._vertexNormals = np.empty(self._vertexes.shape, dtype=float) + for vindex in xrange(self._vertexes.shape[0]): + norms = faceNorms[vertFaces[vindex]] ## get all face normals + norm = norms.sum(axis=0) ## sum normals + norm /= (norm**2).sum()**0.5 ## and re-normalize + self._vertexNormals[vindex] = norm + + if indexed is None: + return self._vertexNormals + elif indexed == 'faces': + return self._vertexNormals[self.faces()] + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def vertexColors(self, indexed=None): + """ + Return an array (Nv, 4) of vertex colors. + If indexed=='faces', then instead return an indexed array + (Nf, 3, 4). + """ + if indexed is None: + return self._vertexColors + elif indexed == 'faces': + if self._vertexColorsIndexedByFaces is None: + self._vertexColorsIndexedByFaces = self._vertexColors[self.faces()] + return self._vertexColorsIndexedByFaces + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def setVertexColors(self, colors, indexed=None): + """ + Set the vertex color array (Nv, 4). + If indexed=='faces', then the array will be interpreted + as indexed and should have shape (Nf, 3, 4) + """ + if indexed is None: + self._vertexColors = colors + self._vertexColorsIndexedByFaces = None + elif indexed == 'faces': + self._vertexColors = None + self._vertexColorsIndexedByFaces = colors + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def faceColors(self, indexed=None): + """ + Return an array (Nf, 4) of face colors. + If indexed=='faces', then instead return an indexed array + (Nf, 3, 4) (note this is just the same array with each color + repeated three times). + """ + if indexed is None: + return self._faceColors + elif indexed == 'faces': + if self._faceColorsIndexedByFaces is None and self._faceColors is not None: + Nf = self._faceColors.shape[0] + self._faceColorsIndexedByFaces = np.empty((Nf, 3, 4), dtype=self._faceColors.dtype) + self._faceColorsIndexedByFaces[:] = self._faceColors.reshape(Nf, 1, 4) + return self._faceColorsIndexedByFaces + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def setFaceColors(self, colors, indexed=None): + """ + Set the face color array (Nf, 4). + If indexed=='faces', then the array will be interpreted + as indexed and should have shape (Nf, 3, 4) + """ + if indexed is None: + self._faceColors = colors + self._faceColorsIndexedByFaces = None + elif indexed == 'faces': + self._faceColors = None + self._faceColorsIndexedByFaces = colors + else: + raise Exception("Invalid indexing mode. Accepts: None, 'faces'") + + def faceCount(self): + """ + Return the number of faces in the mesh. + """ + if self._faces is not None: + return self._faces.shape[0] + elif self._vertexesIndexedByFaces is not None: + return self._vertexesIndexedByFaces.shape[0] + + def edgeColors(self): + return self._edgeColors + + #def _setIndexedFaces(self, faces, vertexColors=None, faceColors=None): + #self._vertexesIndexedByFaces = faces + #self._vertexColorsIndexedByFaces = vertexColors + #self._faceColorsIndexedByFaces = faceColors + + def _computeUnindexedVertexes(self): + ## Given (Nv, 3, 3) array of vertexes-indexed-by-face, convert backward to unindexed vertexes + ## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14) + + ## I think generally this should be discouraged.. + + faces = self._vertexesIndexedByFaces + verts = {} ## used to remember the index of each vertex position + self._faces = np.empty(faces.shape[:2], dtype=np.uint) + self._vertexes = [] + self._vertexFaces = [] + self._faceNormals = None + self._vertexNormals = None + for i in xrange(faces.shape[0]): + face = faces[i] + inds = [] + for j in range(face.shape[0]): + pt = face[j] + pt2 = tuple([round(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged + index = verts.get(pt2, None) + if index is None: + #self._vertexes.append(QtGui.QVector3D(*pt)) + self._vertexes.append(pt) + self._vertexFaces.append([]) + index = len(self._vertexes)-1 + verts[pt2] = index + self._vertexFaces[index].append(i) # keep track of which vertexes belong to which faces + self._faces[i,j] = index + self._vertexes = np.array(self._vertexes, dtype=float) + + #def _setUnindexedFaces(self, faces, vertexes, vertexColors=None, faceColors=None): + #self._vertexes = vertexes #[QtGui.QVector3D(*v) for v in vertexes] + #self._faces = faces.astype(np.uint) + #self._edges = None + #self._vertexFaces = None + #self._faceNormals = None + #self._vertexNormals = None + #self._vertexColors = vertexColors + #self._faceColors = faceColors + + def vertexFaces(self): + """ + Return list mapping each vertex index to a list of face indexes that use the vertex. + """ + if self._vertexFaces is None: + self._vertexFaces = [None] * len(self.vertexes()) + for i in xrange(self._faces.shape[0]): + face = self._faces[i] + for ind in face: + if self._vertexFaces[ind] is None: + self._vertexFaces[ind] = [] ## need a unique/empty list to fill + self._vertexFaces[ind].append(i) + return self._vertexFaces + + #def reverseNormals(self): + #""" + #Reverses the direction of all normal vectors. + #""" + #pass + + #def generateEdgesFromFaces(self): + #""" + #Generate a set of edges by listing all the edges of faces and removing any duplicates. + #Useful for displaying wireframe meshes. + #""" + #pass + + def save(self): + """Serialize this mesh to a string appropriate for disk storage""" + import pickle + if self._faces is not None: + names = ['_vertexes', '_faces'] + else: + names = ['_vertexesIndexedByFaces'] + + if self._vertexColors is not None: + names.append('_vertexColors') + elif self._vertexColorsIndexedByFaces is not None: + names.append('_vertexColorsIndexedByFaces') + + if self._faceColors is not None: + names.append('_faceColors') + elif self._faceColorsIndexedByFaces is not None: + names.append('_faceColorsIndexedByFaces') + + state = {n:getattr(self, n) for n in names} + return pickle.dumps(state) + + def restore(self, state): + """Restore the state of a mesh previously saved using save()""" + import pickle + state = pickle.loads(state) + for k in state: + if isinstance(state[k], list): + if isinstance(state[k][0], QtGui.QVector3D): + state[k] = [[v.x(), v.y(), v.z()] for v in state[k]] + state[k] = np.array(state[k]) + setattr(self, k, state[k]) + + + + @staticmethod + def sphere(rows, cols, radius=1.0, offset=True): + """ + Return a MeshData instance with vertexes and faces computed + for a spherical surface. + """ + verts = np.empty((rows+1, cols, 3), dtype=float) + + ## compute vertexes + phi = (np.arange(rows+1) * np.pi / rows).reshape(rows+1, 1) + s = radius * np.sin(phi) + verts[...,2] = radius * np.cos(phi) + th = ((np.arange(cols) * 2 * np.pi / cols).reshape(1, cols)) + if offset: + th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column + verts[...,0] = s * np.cos(th) + verts[...,1] = s * np.sin(th) + verts = verts.reshape((rows+1)*cols, 3)[cols-1:-(cols-1)] ## remove redundant vertexes from top and bottom + + ## compute faces + faces = np.empty((rows*cols*2, 3), dtype=np.uint) + rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]]) + rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]]) + for row in range(rows): + start = row * cols * 2 + faces[start:start+cols] = rowtemplate1 + row * cols + faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols + faces = faces[cols:-cols] ## cut off zero-area triangles at top and bottom + + ## adjust for redundant vertexes that were removed from top and bottom + vmin = cols-1 + faces[facesvmax] = vmax + + return MeshData(vertexes=verts, faces=faces) + + \ No newline at end of file diff --git a/pyqtgraph/opengl/__init__.py b/pyqtgraph/opengl/__init__.py new file mode 100644 index 00000000..199c372c --- /dev/null +++ b/pyqtgraph/opengl/__init__.py @@ -0,0 +1,30 @@ +from .GLViewWidget import GLViewWidget + +from pyqtgraph import importAll +#import os +#def importAll(path): + #d = os.path.join(os.path.split(__file__)[0], path) + #files = [] + #for f in os.listdir(d): + #if os.path.isdir(os.path.join(d, f)) and f != '__pycache__': + #files.append(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.append(f[:-3]) + + #for modName in files: + #mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + #if hasattr(mod, k): + #globals()[k] = getattr(mod, k) + +importAll('items', globals(), locals()) +\ +from MeshData import MeshData +## for backward compatibility: +#MeshData.MeshData = MeshData ## breaks autodoc. + +import shaders diff --git a/pyqtgraph/opengl/glInfo.py b/pyqtgraph/opengl/glInfo.py new file mode 100644 index 00000000..95f59630 --- /dev/null +++ b/pyqtgraph/opengl/glInfo.py @@ -0,0 +1,16 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from OpenGL.GL import * +app = QtGui.QApplication([]) + +class GLTest(QtOpenGL.QGLWidget): + def __init__(self): + QtOpenGL.QGLWidget.__init__(self) + self.makeCurrent() + print "GL version:", glGetString(GL_VERSION) + print "MAX_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_TEXTURE_SIZE) + print "MAX_3D_TEXTURE_SIZE:", glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE) + print "Extensions:", glGetString(GL_EXTENSIONS) + +GLTest() + + diff --git a/pyqtgraph/opengl/items/GLAxisItem.py b/pyqtgraph/opengl/items/GLAxisItem.py new file mode 100644 index 00000000..1586d70a --- /dev/null +++ b/pyqtgraph/opengl/items/GLAxisItem.py @@ -0,0 +1,58 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph import QtGui + +__all__ = ['GLAxisItem'] + +class GLAxisItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays three lines indicating origin and orientation of local coordinate system. + + """ + + def __init__(self, size=None): + GLGraphicsItem.__init__(self) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the axes (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + + def paint(self): + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + glEnable( GL_POINT_SMOOTH ) + #glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + x,y,z = self.size() + glColor4f(0, 1, 0, .6) # z is green + glVertex3f(0, 0, 0) + glVertex3f(0, 0, z) + + glColor4f(1, 1, 0, .6) # y is yellow + glVertex3f(0, 0, 0) + glVertex3f(0, y, 0) + + glColor4f(0, 0, 1, .6) # x is blue + glVertex3f(0, 0, 0) + glVertex3f(x, 0, 0) + glEnd() diff --git a/pyqtgraph/opengl/items/GLBoxItem.py b/pyqtgraph/opengl/items/GLBoxItem.py new file mode 100644 index 00000000..af888e91 --- /dev/null +++ b/pyqtgraph/opengl/items/GLBoxItem.py @@ -0,0 +1,85 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg + +__all__ = ['GLBoxItem'] + +class GLBoxItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays a wire-frame box. + """ + def __init__(self, size=None, color=None): + GLGraphicsItem.__init__(self) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + if color is None: + color = (255,255,255,80) + self.setColor(color) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the box (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + def setColor(self, *args): + """Set the color of the box. Arguments are the same as those accepted by functions.mkColor()""" + self.__color = pg.Color(*args) + + def color(self): + return self.__color + + def paint(self): + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + glColor4f(*self.color().glColor()) + x,y,z = self.size() + glVertex3f(0, 0, 0) + glVertex3f(0, 0, z) + glVertex3f(x, 0, 0) + glVertex3f(x, 0, z) + glVertex3f(0, y, 0) + glVertex3f(0, y, z) + glVertex3f(x, y, 0) + glVertex3f(x, y, z) + + glVertex3f(0, 0, 0) + glVertex3f(0, y, 0) + glVertex3f(x, 0, 0) + glVertex3f(x, y, 0) + glVertex3f(0, 0, z) + glVertex3f(0, y, z) + glVertex3f(x, 0, z) + glVertex3f(x, y, z) + + glVertex3f(0, 0, 0) + glVertex3f(x, 0, 0) + glVertex3f(0, y, 0) + glVertex3f(x, y, 0) + glVertex3f(0, 0, z) + glVertex3f(x, 0, z) + glVertex3f(0, y, z) + glVertex3f(x, y, z) + + glEnd() + + \ No newline at end of file diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py new file mode 100644 index 00000000..630b2aba --- /dev/null +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -0,0 +1,55 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph import QtGui + +__all__ = ['GLGridItem'] + +class GLGridItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays a wire-grame grid. + """ + + def __init__(self, size=None, color=None, glOptions='translucent'): + GLGraphicsItem.__init__(self) + self.setGLOptions(glOptions) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the axes (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + + def paint(self): + self.setupGLState() + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + glEnable( GL_POINT_SMOOTH ) + #glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + x,y,z = self.size() + glColor4f(1, 1, 1, .3) + for x in range(-10, 11): + glVertex3f(x, -10, 0) + glVertex3f(x, 10, 0) + for y in range(-10, 11): + glVertex3f(-10, y, 0) + glVertex3f( 10, y, 0) + + glEnd() diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py new file mode 100644 index 00000000..b292a7b7 --- /dev/null +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -0,0 +1,87 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import numpy as np + +__all__ = ['GLImageItem'] + +class GLImageItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays image data as a textured quad. + """ + + + def __init__(self, data, smooth=False): + """ + + ============== ======================================================================================= + **Arguments:** + data Volume data to be rendered. *Must* be 3D numpy array (x, y, RGBA) with dtype=ubyte. + (See functions.makeRGBA) + smooth (bool) If True, the volume slices are rendered with linear interpolation + ============== ======================================================================================= + """ + + self.smooth = smooth + self.data = data + GLGraphicsItem.__init__(self) + + def initializeGL(self): + glEnable(GL_TEXTURE_2D) + self.texture = glGenTextures(1) + glBindTexture(GL_TEXTURE_2D, self.texture) + if self.smooth: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + else: + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + shape = self.data.shape + + ## Test texture dimensions first + glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: + raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((1,0,2))) + glDisable(GL_TEXTURE_2D) + + #self.lists = {} + #for ax in [0,1,2]: + #for d in [-1, 1]: + #l = glGenLists(1) + #self.lists[(ax,d)] = l + #glNewList(l, GL_COMPILE) + #self.drawVolume(ax, d) + #glEndList() + + + def paint(self): + + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, self.texture) + + glEnable(GL_DEPTH_TEST) + #glDisable(GL_CULL_FACE) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + glColor4f(1,1,1,1) + + glBegin(GL_QUADS) + glTexCoord2f(0,0) + glVertex3f(0,0,0) + glTexCoord2f(1,0) + glVertex3f(self.data.shape[0], 0, 0) + glTexCoord2f(1,1) + glVertex3f(self.data.shape[0], self.data.shape[1], 0) + glTexCoord2f(0,1) + glVertex3f(0, self.data.shape[1], 0) + glEnd() + glDisable(GL_TEXTURE_3D) + diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py new file mode 100644 index 00000000..ef747d17 --- /dev/null +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -0,0 +1,81 @@ +from OpenGL.GL import * +from OpenGL.arrays import vbo +from .. GLGraphicsItem import GLGraphicsItem +from .. import shaders +from pyqtgraph import QtGui +import numpy as np + +__all__ = ['GLLinePlotItem'] + +class GLLinePlotItem(GLGraphicsItem): + """Draws line plots in 3D.""" + + def __init__(self, **kwds): + GLGraphicsItem.__init__(self) + glopts = kwds.pop('glOptions', 'additive') + self.setGLOptions(glopts) + self.pos = None + self.width = 1. + self.color = (1.0,1.0,1.0,1.0) + self.setData(**kwds) + + def setData(self, **kwds): + """ + Update the data displayed by this item. All arguments are optional; + for example it is allowed to update spot positions while leaving + colors unchanged, etc. + + ==================== ================================================== + Arguments: + ------------------------------------------------------------------------ + pos (N,3) array of floats specifying point locations. + color tuple of floats (0.0-1.0) specifying + a color for the entire item. + width float specifying line width + ==================== ================================================== + """ + args = ['pos', 'color', 'width', 'connected'] + for k in kwds.keys(): + if k not in args: + raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) + + for arg in args: + if arg in kwds: + setattr(self, arg, kwds[arg]) + #self.vbo.pop(arg, None) + self.update() + + def initializeGL(self): + pass + + #def setupGLState(self): + #"""Prepare OpenGL state for drawing. This function is called immediately before painting.""" + ##glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly. + #glBlendFunc(GL_SRC_ALPHA, GL_ONE) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + #glDisable( GL_DEPTH_TEST ) + + ##glEnable( GL_POINT_SMOOTH ) + + ##glHint(GL_POINT_SMOOTH_HINT, GL_NICEST) + ##glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3)) + ##glPointParameterfv(GL_POINT_SIZE_MAX, (65500,)) + ##glPointParameterfv(GL_POINT_SIZE_MIN, (0,)) + + def paint(self): + if self.pos is None: + return + self.setupGLState() + + glEnableClientState(GL_VERTEX_ARRAY) + try: + glVertexPointerf(self.pos) + glColor4f(*self.color) + + glPointSize(self.width) + glDrawArrays(GL_LINE_STRIP, 0, self.pos.size / self.pos.shape[-1]) + finally: + glDisableClientState(GL_VERTEX_ARRAY) + + diff --git a/pyqtgraph/opengl/items/GLMeshItem.py b/pyqtgraph/opengl/items/GLMeshItem.py new file mode 100644 index 00000000..4222c96b --- /dev/null +++ b/pyqtgraph/opengl/items/GLMeshItem.py @@ -0,0 +1,182 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from .. MeshData import MeshData +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg +from .. import shaders +import numpy as np + + + +__all__ = ['GLMeshItem'] + +class GLMeshItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays a 3D triangle mesh. + """ + def __init__(self, **kwds): + """ + ============== ===================================================== + Arguments + meshdata MeshData object from which to determine geometry for + this item. + color Default color used if no vertex or face colors are + specified. + shader Name of shader program to use (None for no shader) + smooth If True, normal vectors are computed for each vertex + and interpolated within each face. + computeNormals If False, then computation of normal vectors is + disabled. This can provide a performance boost for + meshes that do not make use of normals. + ============== ===================================================== + """ + self.opts = { + 'meshdata': None, + 'color': (1., 1., 1., 1.), + 'shader': None, + 'smooth': True, + 'computeNormals': True, + } + + GLGraphicsItem.__init__(self) + glopts = kwds.pop('glOptions', 'opaque') + self.setGLOptions(glopts) + shader = kwds.pop('shader', None) + self.setShader(shader) + + self.setMeshData(**kwds) + + ## storage for data compiled from MeshData object + self.vertexes = None + self.normals = None + self.colors = None + self.faces = None + + def setShader(self, shader): + """Set the shader used when rendering faces in the mesh. (see the GL shaders example)""" + self.opts['shader'] = shader + self.update() + + def shader(self): + return shaders.getShaderProgram(self.opts['shader']) + + def setColor(self, c): + """Set the default color to use when no vertex or face colors are specified.""" + self.opts['color'] = c + self.update() + + def setMeshData(self, **kwds): + """ + Set mesh data for this item. This can be invoked two ways: + + 1. Specify *meshdata* argument with a new MeshData object + 2. Specify keyword arguments to be passed to MeshData(..) to create a new instance. + """ + md = kwds.get('meshdata', None) + if md is None: + opts = {} + for k in ['vertexes', 'faces', 'edges', 'vertexColors', 'faceColors']: + try: + opts[k] = kwds.pop(k) + except KeyError: + pass + md = MeshData(**opts) + + self.opts['meshdata'] = md + self.opts.update(kwds) + self.meshDataChanged() + self.update() + + + def meshDataChanged(self): + """ + This method must be called to inform the item that the MeshData object + has been altered. + """ + + self.vertexes = None + self.faces = None + self.normals = None + self.colors = None + self.update() + + def parseMeshData(self): + ## interpret vertex / normal data before drawing + ## This can: + ## - automatically generate normals if they were not specified + ## - pull vertexes/noormals/faces from MeshData if that was specified + + if self.vertexes is not None and self.normals is not None: + return + #if self.opts['normals'] is None: + #if self.opts['meshdata'] is None: + #self.opts['meshdata'] = MeshData(vertexes=self.opts['vertexes'], faces=self.opts['faces']) + if self.opts['meshdata'] is not None: + md = self.opts['meshdata'] + if self.opts['smooth'] and not md.hasFaceIndexedData(): + self.vertexes = md.vertexes() + if self.opts['computeNormals']: + self.normals = md.vertexNormals() + self.faces = md.faces() + if md.hasVertexColor(): + self.colors = md.vertexColors() + if md.hasFaceColor(): + self.colors = md.faceColors() + else: + self.vertexes = md.vertexes(indexed='faces') + if self.opts['computeNormals']: + if self.opts['smooth']: + self.normals = md.vertexNormals(indexed='faces') + else: + self.normals = md.faceNormals(indexed='faces') + self.faces = None + if md.hasVertexColor(): + self.colors = md.vertexColors(indexed='faces') + elif md.hasFaceColor(): + self.colors = md.faceColors(indexed='faces') + + return + + def paint(self): + self.setupGLState() + + self.parseMeshData() + + with self.shader(): + verts = self.vertexes + norms = self.normals + color = self.colors + faces = self.faces + if verts is None: + return + glEnableClientState(GL_VERTEX_ARRAY) + try: + glVertexPointerf(verts) + + if self.colors is None: + color = self.opts['color'] + if isinstance(color, QtGui.QColor): + glColor4f(*pg.glColor(color)) + else: + glColor4f(*color) + else: + glEnableClientState(GL_COLOR_ARRAY) + glColorPointerf(color) + + + if norms is not None: + glEnableClientState(GL_NORMAL_ARRAY) + glNormalPointerf(norms) + + if faces is None: + glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1])) + else: + faces = faces.astype(np.uint).flatten() + glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces) + finally: + glDisableClientState(GL_NORMAL_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py new file mode 100644 index 00000000..e9bbde64 --- /dev/null +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -0,0 +1,183 @@ +from OpenGL.GL import * +from OpenGL.arrays import vbo +from .. GLGraphicsItem import GLGraphicsItem +from .. import shaders +from pyqtgraph import QtGui +import numpy as np + +__all__ = ['GLScatterPlotItem'] + +class GLScatterPlotItem(GLGraphicsItem): + """Draws points at a list of 3D positions.""" + + def __init__(self, **kwds): + GLGraphicsItem.__init__(self) + glopts = kwds.pop('glOptions', 'additive') + self.setGLOptions(glopts) + self.pos = [] + self.size = 10 + self.color = [1.0,1.0,1.0,0.5] + self.pxMode = True + #self.vbo = {} ## VBO does not appear to improve performance very much. + self.setData(**kwds) + + def setData(self, **kwds): + """ + Update the data displayed by this item. All arguments are optional; + for example it is allowed to update spot positions while leaving + colors unchanged, etc. + + ==================== ================================================== + Arguments: + ------------------------------------------------------------------------ + pos (N,3) array of floats specifying point locations. + color (N,4) array of floats (0.0-1.0) specifying + spot colors OR a tuple of floats specifying + a single color for all spots. + size (N,) array of floats specifying spot sizes or + a single value to apply to all spots. + pxMode If True, spot sizes are expressed in pixels. + Otherwise, they are expressed in item coordinates. + ==================== ================================================== + """ + args = ['pos', 'color', 'size', 'pxMode'] + for k in kwds.keys(): + if k not in args: + raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args))) + + args.remove('pxMode') + for arg in args: + if arg in kwds: + setattr(self, arg, kwds[arg]) + #self.vbo.pop(arg, None) + + self.pxMode = kwds.get('pxMode', self.pxMode) + self.update() + + def initializeGL(self): + + ## Generate texture for rendering points + w = 64 + def fn(x,y): + r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 + return 200 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) + pData = np.empty((w, w, 4)) + pData[:] = 255 + pData[:,:,3] = np.fromfunction(fn, pData.shape[:2]) + #print pData.shape, pData.min(), pData.max() + pData = pData.astype(np.ubyte) + + self.pointTexture = glGenTextures(1) + glActiveTexture(GL_TEXTURE0) + glEnable(GL_TEXTURE_2D) + glBindTexture(GL_TEXTURE_2D, self.pointTexture) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, pData.shape[0], pData.shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, pData) + + self.shader = shaders.getShaderProgram('pointSprite') + + #def getVBO(self, name): + #if name not in self.vbo: + #self.vbo[name] = vbo.VBO(getattr(self, name).astype('f')) + #return self.vbo[name] + + #def setupGLState(self): + #"""Prepare OpenGL state for drawing. This function is called immediately before painting.""" + ##glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly. + #glBlendFunc(GL_SRC_ALPHA, GL_ONE) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + #glDisable( GL_DEPTH_TEST ) + + ##glEnable( GL_POINT_SMOOTH ) + + ##glHint(GL_POINT_SMOOTH_HINT, GL_NICEST) + ##glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3)) + ##glPointParameterfv(GL_POINT_SIZE_MAX, (65500,)) + ##glPointParameterfv(GL_POINT_SIZE_MIN, (0,)) + + def paint(self): + self.setupGLState() + + glEnable(GL_POINT_SPRITE) + + glActiveTexture(GL_TEXTURE0) + glEnable( GL_TEXTURE_2D ) + glBindTexture(GL_TEXTURE_2D, self.pointTexture) + + glTexEnvi(GL_POINT_SPRITE, GL_COORD_REPLACE, GL_TRUE) + #glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) ## use texture color exactly + #glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ) ## texture modulates current color + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + glEnable(GL_PROGRAM_POINT_SIZE) + + + with self.shader: + #glUniform1i(self.shader.uniform('texture'), 0) ## inform the shader which texture to use + glEnableClientState(GL_VERTEX_ARRAY) + try: + pos = self.pos + #if pos.ndim > 2: + #pos = pos.reshape((reduce(lambda a,b: a*b, pos.shape[:-1]), pos.shape[-1])) + glVertexPointerf(pos) + + if isinstance(self.color, np.ndarray): + glEnableClientState(GL_COLOR_ARRAY) + glColorPointerf(self.color) + else: + if isinstance(self.color, QtGui.QColor): + glColor4f(*fn.glColor(self.color)) + else: + glColor4f(*self.color) + + if not self.pxMode or isinstance(self.size, np.ndarray): + glEnableClientState(GL_NORMAL_ARRAY) + norm = np.empty(pos.shape) + if self.pxMode: + norm[...,0] = self.size + else: + gpos = self.mapToView(pos.transpose()).transpose() + pxSize = self.view().pixelSize(gpos) + norm[...,0] = self.size / pxSize + + glNormalPointerf(norm) + else: + glNormal3f(self.size, 0, 0) ## vertex shader uses norm.x to determine point size + #glPointSize(self.size) + glDrawArrays(GL_POINTS, 0, pos.size / pos.shape[-1]) + finally: + glDisableClientState(GL_NORMAL_ARRAY) + glDisableClientState(GL_VERTEX_ARRAY) + glDisableClientState(GL_COLOR_ARRAY) + #posVBO.unbind() + + #for i in range(len(self.pos)): + #pos = self.pos[i] + + #if isinstance(self.color, np.ndarray): + #color = self.color[i] + #else: + #color = self.color + #if isinstance(self.color, QtGui.QColor): + #color = fn.glColor(self.color) + + #if isinstance(self.size, np.ndarray): + #size = self.size[i] + #else: + #size = self.size + + #pxSize = self.view().pixelSize(QtGui.QVector3D(*pos)) + + #glPointSize(size / pxSize) + #glBegin( GL_POINTS ) + #glColor4f(*color) # x is blue + ##glNormal3f(size, 0, 0) + #glVertex3f(*pos) + #glEnd() + + + + + diff --git a/pyqtgraph/opengl/items/GLSurfacePlotItem.py b/pyqtgraph/opengl/items/GLSurfacePlotItem.py new file mode 100644 index 00000000..69080fad --- /dev/null +++ b/pyqtgraph/opengl/items/GLSurfacePlotItem.py @@ -0,0 +1,139 @@ +from OpenGL.GL import * +from GLMeshItem import GLMeshItem +from .. MeshData import MeshData +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg +import numpy as np + + + +__all__ = ['GLSurfacePlotItem'] + +class GLSurfacePlotItem(GLMeshItem): + """ + **Bases:** :class:`GLMeshItem ` + + Displays a surface plot on a regular x,y grid + """ + def __init__(self, x=None, y=None, z=None, colors=None, **kwds): + """ + The x, y, z, and colors arguments are passed to setData(). + All other keyword arguments are passed to GLMeshItem.__init__(). + """ + + self._x = None + self._y = None + self._z = None + self._color = None + self._vertexes = None + self._meshdata = MeshData() + GLMeshItem.__init__(self, meshdata=self._meshdata, **kwds) + + self.setData(x, y, z, colors) + + + + def setData(self, x=None, y=None, z=None, colors=None): + """ + Update the data in this surface plot. + + ========== ===================================================================== + Arguments + x,y 1D arrays of values specifying the x,y positions of vertexes in the + grid. If these are omitted, then the values will be assumed to be + integers. + z 2D array of height values for each grid vertex. + colors (width, height, 4) array of vertex colors. + ========== ===================================================================== + + All arguments are optional. + + Note that if vertex positions are updated, the normal vectors for each triangle must + be recomputed. This is somewhat expensive if the surface was initialized with smooth=False + and very expensive if smooth=True. For faster performance, initialize with + computeNormals=False and use per-vertex colors or a normal-independent shader program. + """ + if x is not None: + if self._x is None or len(x) != len(self._x): + self._vertexes = None + self._x = x + + if y is not None: + if self._y is None or len(y) != len(self._y): + self._vertexes = None + self._y = y + + if z is not None: + #if self._x is None: + #self._x = np.arange(z.shape[0]) + #self._vertexes = None + #if self._y is None: + #self._y = np.arange(z.shape[1]) + #self._vertexes = None + + if self._x is not None and z.shape[0] != len(self._x): + raise Exception('Z values must have shape (len(x), len(y))') + if self._y is not None and z.shape[1] != len(self._y): + raise Exception('Z values must have shape (len(x), len(y))') + self._z = z + if self._vertexes is not None and self._z.shape != self._vertexes.shape[:2]: + self._vertexes = None + + if colors is not None: + self._colors = colors + self._meshdata.setVertexColors(colors) + + if self._z is None: + return + + updateMesh = False + newVertexes = False + + ## Generate vertex and face array + if self._vertexes is None: + newVertexes = True + self._vertexes = np.empty((self._z.shape[0], self._z.shape[1], 3), dtype=float) + self.generateFaces() + self._meshdata.setFaces(self._faces) + updateMesh = True + + ## Copy x, y, z data into vertex array + if newVertexes or x is not None: + if x is None: + if self._x is None: + x = np.arange(self._z.shape[0]) + else: + x = self._x + self._vertexes[:, :, 0] = x.reshape(len(x), 1) + updateMesh = True + + if newVertexes or y is not None: + if y is None: + if self._y is None: + y = np.arange(self._z.shape[1]) + else: + y = self._y + self._vertexes[:, :, 1] = y.reshape(1, len(y)) + updateMesh = True + + if newVertexes or z is not None: + self._vertexes[...,2] = self._z + updateMesh = True + + ## Update MeshData + if updateMesh: + self._meshdata.setVertexes(self._vertexes.reshape(self._vertexes.shape[0]*self._vertexes.shape[1], 3)) + self.meshDataChanged() + + + def generateFaces(self): + cols = self._z.shape[0]-1 + rows = self._z.shape[1]-1 + faces = np.empty((cols*rows*2, 3), dtype=np.uint) + rowtemplate1 = np.arange(cols).reshape(cols, 1) + np.array([[0, 1, cols+1]]) + rowtemplate2 = np.arange(cols).reshape(cols, 1) + np.array([[cols+1, 1, cols+2]]) + for row in range(rows): + start = row * cols * 2 + faces[start:start+cols] = rowtemplate1 + row * (cols+1) + faces[start+cols:start+(cols*2)] = rowtemplate2 + row * (cols+1) + self._faces = faces \ No newline at end of file diff --git a/pyqtgraph/opengl/items/GLVolumeItem.py b/pyqtgraph/opengl/items/GLVolumeItem.py new file mode 100644 index 00000000..4980239d --- /dev/null +++ b/pyqtgraph/opengl/items/GLVolumeItem.py @@ -0,0 +1,213 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import numpy as np + +__all__ = ['GLVolumeItem'] + +class GLVolumeItem(GLGraphicsItem): + """ + **Bases:** :class:`GLGraphicsItem ` + + Displays volumetric data. + """ + + + def __init__(self, data, sliceDensity=1, smooth=True, glOptions='translucent'): + """ + ============== ======================================================================================= + **Arguments:** + data Volume data to be rendered. *Must* be 4D numpy array (x, y, z, RGBA) with dtype=ubyte. + sliceDensity Density of slices to render through the volume. A value of 1 means one slice per voxel. + smooth (bool) If True, the volume slices are rendered with linear interpolation + ============== ======================================================================================= + """ + + self.sliceDensity = sliceDensity + self.smooth = smooth + self.data = data + GLGraphicsItem.__init__(self) + self.setGLOptions(glOptions) + + def initializeGL(self): + glEnable(GL_TEXTURE_3D) + self.texture = glGenTextures(1) + glBindTexture(GL_TEXTURE_3D, self.texture) + if self.smooth: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + else: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + shape = self.data.shape + + ## Test texture dimensions first + glTexImage3D(GL_PROXY_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_3D, 0, GL_TEXTURE_WIDTH) == 0: + raise Exception("OpenGL failed to create 3D texture (%dx%dx%d); too large for this hardware." % shape[:3]) + + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3))) + glDisable(GL_TEXTURE_3D) + + self.lists = {} + for ax in [0,1,2]: + for d in [-1, 1]: + l = glGenLists(1) + self.lists[(ax,d)] = l + glNewList(l, GL_COMPILE) + self.drawVolume(ax, d) + glEndList() + + + def paint(self): + self.setupGLState() + + glEnable(GL_TEXTURE_3D) + glBindTexture(GL_TEXTURE_3D, self.texture) + + #glEnable(GL_DEPTH_TEST) + #glDisable(GL_CULL_FACE) + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + glColor4f(1,1,1,1) + + view = self.view() + center = QtGui.QVector3D(*[x/2. for x in self.data.shape[:3]]) + cam = self.mapFromParent(view.cameraPosition()) - center + #print "center", center, "cam", view.cameraPosition(), self.mapFromParent(view.cameraPosition()), "diff", cam + cam = np.array([cam.x(), cam.y(), cam.z()]) + ax = np.argmax(abs(cam)) + d = 1 if cam[ax] > 0 else -1 + glCallList(self.lists[(ax,d)]) ## draw axes + glDisable(GL_TEXTURE_3D) + + def drawVolume(self, ax, d): + N = 5 + + imax = [0,1,2] + imax.remove(ax) + + tp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + vp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + nudge = [0.5/x for x in self.data.shape] + tp[0][imax[0]] = 0+nudge[imax[0]] + tp[0][imax[1]] = 0+nudge[imax[1]] + tp[1][imax[0]] = 1-nudge[imax[0]] + tp[1][imax[1]] = 0+nudge[imax[1]] + tp[2][imax[0]] = 1-nudge[imax[0]] + tp[2][imax[1]] = 1-nudge[imax[1]] + tp[3][imax[0]] = 0+nudge[imax[0]] + tp[3][imax[1]] = 1-nudge[imax[1]] + + vp[0][imax[0]] = 0 + vp[0][imax[1]] = 0 + vp[1][imax[0]] = self.data.shape[imax[0]] + vp[1][imax[1]] = 0 + vp[2][imax[0]] = self.data.shape[imax[0]] + vp[2][imax[1]] = self.data.shape[imax[1]] + vp[3][imax[0]] = 0 + vp[3][imax[1]] = self.data.shape[imax[1]] + slices = self.data.shape[ax] * self.sliceDensity + r = list(range(slices)) + if d == -1: + r = r[::-1] + + glBegin(GL_QUADS) + tzVals = np.linspace(nudge[ax], 1.0-nudge[ax], slices) + vzVals = np.linspace(0, self.data.shape[ax], slices) + for i in r: + z = tzVals[i] + w = vzVals[i] + + tp[0][ax] = z + tp[1][ax] = z + tp[2][ax] = z + tp[3][ax] = z + + vp[0][ax] = w + vp[1][ax] = w + vp[2][ax] = w + vp[3][ax] = w + + + glTexCoord3f(*tp[0]) + glVertex3f(*vp[0]) + glTexCoord3f(*tp[1]) + glVertex3f(*vp[1]) + glTexCoord3f(*tp[2]) + glVertex3f(*vp[2]) + glTexCoord3f(*tp[3]) + glVertex3f(*vp[3]) + glEnd() + + + + + + + + + + ## Interesting idea: + ## remove projection/modelview matrixes, recreate in texture coords. + ## it _sorta_ works, but needs tweaking. + #mvm = glGetDoublev(GL_MODELVIEW_MATRIX) + #pm = glGetDoublev(GL_PROJECTION_MATRIX) + #m = QtGui.QMatrix4x4(mvm.flatten()).inverted()[0] + #p = QtGui.QMatrix4x4(pm.flatten()).inverted()[0] + + #glMatrixMode(GL_PROJECTION) + #glPushMatrix() + #glLoadIdentity() + #N=1 + #glOrtho(-N,N,-N,N,-100,100) + + #glMatrixMode(GL_MODELVIEW) + #glLoadIdentity() + + + #glMatrixMode(GL_TEXTURE) + #glLoadIdentity() + #glMultMatrixf(m.copyDataTo()) + + #view = self.view() + #w = view.width() + #h = view.height() + #dist = view.opts['distance'] + #fov = view.opts['fov'] + #nearClip = dist * .1 + #farClip = dist * 5. + #r = nearClip * np.tan(fov) + #t = r * h / w + + #p = QtGui.QMatrix4x4() + #p.frustum( -r, r, -t, t, nearClip, farClip) + #glMultMatrixf(p.inverted()[0].copyDataTo()) + + + #glBegin(GL_QUADS) + + #M=1 + #for i in range(500): + #z = i/500. + #w = -i/500. + #glTexCoord3f(-M, -M, z) + #glVertex3f(-N, -N, w) + #glTexCoord3f(M, -M, z) + #glVertex3f(N, -N, w) + #glTexCoord3f(M, M, z) + #glVertex3f(N, N, w) + #glTexCoord3f(-M, M, z) + #glVertex3f(-N, N, w) + #glEnd() + #glDisable(GL_TEXTURE_3D) + + #glMatrixMode(GL_PROJECTION) + #glPopMatrix() + + + diff --git a/pyqtgraph/opengl/items/__init__.py b/pyqtgraph/opengl/items/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py new file mode 100644 index 00000000..b1652850 --- /dev/null +++ b/pyqtgraph/opengl/shaders.py @@ -0,0 +1,393 @@ +from OpenGL.GL import * +from OpenGL.GL import shaders +import re + +## For centralizing and managing vertex/fragment shader programs. + +def initShaders(): + global Shaders + Shaders = [ + ShaderProgram(None, []), + + ## increases fragment alpha as the normal turns orthogonal to the view + ## this is useful for viewing shells that enclose a volume (such as isosurfaces) + ShaderProgram('balloon', [ + VertexShader(""" + varying vec3 normal; + void main() { + // compute here for use in fragment shader + normal = normalize(gl_NormalMatrix * gl_Normal); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + gl_FragColor = color; + } + """) + ]), + + ## colors fragments based on face normals relative to view + ## This means that the colors will change depending on how the view is rotated + ShaderProgram('viewNormalColor', [ + VertexShader(""" + varying vec3 normal; + void main() { + // compute here for use in fragment shader + normal = normalize(gl_NormalMatrix * gl_Normal); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.x = (normal.x + 1.0) * 0.5; + color.y = (normal.y + 1.0) * 0.5; + color.z = (normal.z + 1.0) * 0.5; + gl_FragColor = color; + } + """) + ]), + + ## colors fragments based on absolute face normals. + ShaderProgram('normalColor', [ + VertexShader(""" + varying vec3 normal; + void main() { + // compute here for use in fragment shader + normal = normalize(gl_Normal); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.x = (normal.x + 1.0) * 0.5; + color.y = (normal.y + 1.0) * 0.5; + color.z = (normal.z + 1.0) * 0.5; + gl_FragColor = color; + } + """) + ]), + + ## very simple simulation of lighting. + ## The light source position is always relative to the camera. + ShaderProgram('shaded', [ + VertexShader(""" + varying vec3 normal; + void main() { + // compute here for use in fragment shader + normal = normalize(gl_NormalMatrix * gl_Normal); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + float p = dot(normal, normalize(vec3(1.0, -1.0, -1.0))); + p = p < 0. ? 0. : p * 0.8; + vec4 color = gl_Color; + color.x = color.x * (0.2 + p); + color.y = color.y * (0.2 + p); + color.z = color.z * (0.2 + p); + gl_FragColor = color; + } + """) + ]), + + ## colors get brighter near edges of object + ShaderProgram('edgeHilight', [ + VertexShader(""" + varying vec3 normal; + void main() { + // compute here for use in fragment shader + normal = normalize(gl_NormalMatrix * gl_Normal); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + varying vec3 normal; + void main() { + vec4 color = gl_Color; + float s = pow(normal.x*normal.x + normal.y*normal.y, 2.0); + color.x = color.x + s * (1.0-color.x); + color.y = color.y + s * (1.0-color.y); + color.z = color.z + s * (1.0-color.z); + gl_FragColor = color; + } + """) + ]), + + ## colors fragments by z-value. + ## This is useful for coloring surface plots by height. + ## This shader uses a uniform called "colorMap" to determine how to map the colors: + ## red = pow(z * colorMap[0] + colorMap[1], colorMap[2]) + ## green = pow(z * colorMap[3] + colorMap[4], colorMap[5]) + ## blue = pow(z * colorMap[6] + colorMap[7], colorMap[8]) + ## (set the values like this: shader['uniformMap'] = array([...]) + ShaderProgram('heightColor', [ + VertexShader(""" + varying vec4 pos; + void main() { + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + pos = gl_Vertex; + gl_Position = ftransform(); + } + """), + FragmentShader(""" + uniform float colorMap[9]; + varying vec4 pos; + //out vec4 gl_FragColor; // only needed for later glsl versions + //in vec4 gl_Color; + void main() { + vec4 color = gl_Color; + color.x = colorMap[0] * (pos.z + colorMap[1]); + if (colorMap[2] != 1.0) + color.x = pow(color.x, colorMap[2]); + color.x = color.x < 0. ? 0. : (color.x > 1. ? 1. : color.x); + + color.y = colorMap[3] * (pos.z + colorMap[4]); + if (colorMap[5] != 1.0) + color.y = pow(color.y, colorMap[5]); + color.y = color.y < 0. ? 0. : (color.y > 1. ? 1. : color.y); + + color.z = colorMap[6] * (pos.z + colorMap[7]); + if (colorMap[8] != 1.0) + color.z = pow(color.z, colorMap[8]); + color.z = color.z < 0. ? 0. : (color.z > 1. ? 1. : color.z); + + color.w = 1.0; + gl_FragColor = color; + } + """), + ], uniforms={'colorMap': [1, 1, 1, 1, 0.5, 1, 1, 0, 1]}), + ShaderProgram('pointSprite', [ ## allows specifying point size using normal.x + ## See: + ## + ## http://stackoverflow.com/questions/9609423/applying-part-of-a-texture-sprite-sheet-texture-map-to-a-point-sprite-in-ios + ## http://stackoverflow.com/questions/3497068/textured-points-in-opengl-es-2-0 + ## + ## + VertexShader(""" + void main() { + gl_FrontColor=gl_Color; + gl_PointSize = gl_Normal.x; + gl_Position = ftransform(); + } + """), + #FragmentShader(""" + ##version 120 + #uniform sampler2D texture; + #void main ( ) + #{ + #gl_FragColor = texture2D(texture, gl_PointCoord) * gl_Color; + #} + #""") + ]), + ] + + +CompiledShaderPrograms = {} + +def getShaderProgram(name): + return ShaderProgram.names[name] + +class Shader(object): + def __init__(self, shaderType, code): + self.shaderType = shaderType + self.code = code + self.compiled = None + + def shader(self): + if self.compiled is None: + try: + self.compiled = shaders.compileShader(self.code, self.shaderType) + except RuntimeError as exc: + ## Format compile errors a bit more nicely + if len(exc.args) == 3: + err, code, typ = exc.args + if not err.startswith('Shader compile failure'): + raise + code = code[0].split('\n') + err, c, msgs = err.partition(':') + err = err + '\n' + msgs = msgs.split('\n') + errNums = [()] * len(code) + for i, msg in enumerate(msgs): + msg = msg.strip() + if msg == '': + continue + m = re.match(r'(\d+\:)?\d+\((\d+)\)', msg) + if m is not None: + line = int(m.groups()[1]) + errNums[line-1] = errNums[line-1] + (str(i+1),) + #code[line-1] = '%d\t%s' % (i+1, code[line-1]) + err = err + "%d %s\n" % (i+1, msg) + errNums = [','.join(n) for n in errNums] + maxlen = max(map(len, errNums)) + code = [errNums[i] + " "*(maxlen-len(errNums[i])) + line for i, line in enumerate(code)] + err = err + '\n'.join(code) + raise Exception(err) + else: + raise + return self.compiled + +class VertexShader(Shader): + def __init__(self, code): + Shader.__init__(self, GL_VERTEX_SHADER, code) + +class FragmentShader(Shader): + def __init__(self, code): + Shader.__init__(self, GL_FRAGMENT_SHADER, code) + + + + +class ShaderProgram(object): + names = {} + + def __init__(self, name, shaders, uniforms=None): + self.name = name + ShaderProgram.names[name] = self + self.shaders = shaders + self.prog = None + self.blockData = {} + self.uniformData = {} + + ## parse extra options from the shader definition + if uniforms is not None: + for k,v in uniforms.items(): + self[k] = v + + def setBlockData(self, blockName, data): + if data is None: + del self.blockData[blockName] + else: + self.blockData[blockName] = data + + def setUniformData(self, uniformName, data): + if data is None: + del self.uniformData[uniformName] + else: + self.uniformData[uniformName] = data + + def __setitem__(self, item, val): + self.setUniformData(item, val) + + def __delitem__(self, item): + self.setUniformData(item, None) + + def program(self): + if self.prog is None: + try: + compiled = [s.shader() for s in self.shaders] ## compile all shaders + self.prog = shaders.compileProgram(*compiled) ## compile program + except: + self.prog = -1 + raise + return self.prog + + def __enter__(self): + if len(self.shaders) > 0 and self.program() != -1: + glUseProgram(self.program()) + + try: + ## load uniform values into program + for uniformName, data in self.uniformData.items(): + loc = self.uniform(uniformName) + if loc == -1: + raise Exception('Could not find uniform variable "%s"' % uniformName) + glUniform1fv(loc, len(data), data) + + ### bind buffer data to program blocks + #if len(self.blockData) > 0: + #bindPoint = 1 + #for blockName, data in self.blockData.items(): + ### Program should have a uniform block declared: + ### + ### layout (std140) uniform blockName { + ### vec4 diffuse; + ### }; + + ### pick any-old binding point. (there are a limited number of these per-program + #bindPoint = 1 + + ### get the block index for a uniform variable in the shader + #blockIndex = glGetUniformBlockIndex(self.program(), blockName) + + ### give the shader block a binding point + #glUniformBlockBinding(self.program(), blockIndex, bindPoint) + + ### create a buffer + #buf = glGenBuffers(1) + #glBindBuffer(GL_UNIFORM_BUFFER, buf) + #glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW) + ### also possible to use glBufferSubData to fill parts of the buffer + + ### bind buffer to the same binding point + #glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf) + except: + glUseProgram(0) + raise + + + + def __exit__(self, *args): + if len(self.shaders) > 0: + glUseProgram(0) + + def uniform(self, name): + """Return the location integer for a uniform variable in this program""" + return glGetUniformLocation(self.program(), name) + + #def uniformBlockInfo(self, blockName): + #blockIndex = glGetUniformBlockIndex(self.program(), blockName) + #count = glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS) + #indices = [] + #for i in range(count): + #indices.append(glGetActiveUniformBlockiv(self.program(), blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES)) + +class HeightColorShader(ShaderProgram): + def __enter__(self): + ## Program should have a uniform block declared: + ## + ## layout (std140) uniform blockName { + ## vec4 diffuse; + ## vec4 ambient; + ## }; + + ## pick any-old binding point. (there are a limited number of these per-program + bindPoint = 1 + + ## get the block index for a uniform variable in the shader + blockIndex = glGetUniformBlockIndex(self.program(), "blockName") + + ## give the shader block a binding point + glUniformBlockBinding(self.program(), blockIndex, bindPoint) + + ## create a buffer + buf = glGenBuffers(1) + glBindBuffer(GL_UNIFORM_BUFFER, buf) + glBufferData(GL_UNIFORM_BUFFER, size, data, GL_DYNAMIC_DRAW) + ## also possible to use glBufferSubData to fill parts of the buffer + + ## bind buffer to the same binding point + glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf) + +initShaders() \ No newline at end of file diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py new file mode 100644 index 00000000..c8ed4902 --- /dev/null +++ b/pyqtgraph/parametertree/Parameter.py @@ -0,0 +1,680 @@ +from pyqtgraph.Qt import QtGui, QtCore +import os, weakref, re +from pyqtgraph.pgcollections import OrderedDict +from .ParameterItem import ParameterItem + +PARAM_TYPES = {} +PARAM_NAMES = {} + +def registerParameterType(name, cls, override=False): + global PARAM_TYPES + if name in PARAM_TYPES and not override: + raise Exception("Parameter type '%s' already exists (use override=True to replace)" % name) + PARAM_TYPES[name] = cls + PARAM_NAMES[cls] = name + + + +class Parameter(QtCore.QObject): + """ + A Parameter is the basic unit of data in a parameter tree. Each parameter has + a name, a type, a value, and several other properties that modify the behavior of the + Parameter. Parameters may have parent / child / sibling relationships to construct + organized hierarchies. Parameters generally do not have any inherent GUI or visual + interpretation; instead they manage ParameterItem instances which take care of + display and user interaction. + + Note: It is fairly uncommon to use the Parameter class directly; mostly you + will use subclasses which provide specialized type and data handling. The static + pethod Parameter.create(...) is an easy way to generate instances of these subclasses. + + For more Parameter types, see ParameterTree.parameterTypes module. + + =================================== ========================================================= + **Signals:** + sigStateChanged(self, change, info) Emitted when anything changes about this parameter at + all. + The second argument is a string indicating what changed + ('value', 'childAdded', etc..) + The third argument can be any extra information about + the change + sigTreeStateChanged(self, changes) Emitted when any child in the tree changes state + (but only if monitorChildren() is called) + the format of *changes* is [(param, change, info), ...] + sigValueChanged(self, value) Emitted when value is finished changing + sigValueChanging(self, value) Emitted immediately for all value changes, + including during editing. + sigChildAdded(self, child, index) Emitted when a child is added + sigChildRemoved(self, child) Emitted when a child is removed + sigParentChanged(self, parent) Emitted when this parameter's parent has changed + sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed + 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 + =================================== ========================================================= + """ + ## name, type, limits, etc. + ## can also carry UI hints (slider vs spinbox, etc.) + + sigValueChanged = QtCore.Signal(object, object) ## self, value emitted when value is finished being edited + sigValueChanging = QtCore.Signal(object, object) ## self, value emitted as value is being edited + + sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index + sigChildRemoved = QtCore.Signal(object, object) ## self, child + sigParentChanged = QtCore.Signal(object, object) ## self, parent + sigLimitsChanged = QtCore.Signal(object, object) ## self, limits + sigDefaultChanged = QtCore.Signal(object, object) ## self, default + sigNameChanged = QtCore.Signal(object, object) ## self, name + sigOptionsChanged = QtCore.Signal(object, object) ## self, {opt:val, ...} + + ## Emitted when anything changes about this parameter at all. + ## The second argument is a string indicating what changed ('value', 'childAdded', etc..) + ## The third argument can be any extra information about the change + sigStateChanged = QtCore.Signal(object, object, object) ## self, change, info + + ## emitted when any child in the tree changes state + ## (but only if monitorChildren() is called) + sigTreeStateChanged = QtCore.Signal(object, object) # self, changes + # changes = [(param, change, info), ...] + + # bad planning. + #def __new__(cls, *args, **opts): + #try: + #cls = PARAM_TYPES[opts['type']] + #except KeyError: + #pass + #return QtCore.QObject.__new__(cls, *args, **opts) + + @staticmethod + def create(**opts): + """ + Create a new Parameter (or subclass) instance using opts['type'] to select the + appropriate class. + + Use registerParameterType() to add new class types. + """ + typ = opts.get('type', None) + if typ is None: + cls = Parameter + else: + cls = PARAM_TYPES[opts['type']] + return cls(**opts) + + def __init__(self, **opts): + QtCore.QObject.__init__(self) + + self.opts = { + 'type': None, + 'readonly': False, + 'visible': True, + 'enabled': True, + 'renamable': False, + 'removable': False, + 'strictNaming': False, # forces name to be usable as a python variable + #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. + } + self.opts.update(opts) + + self.childs = [] + self.names = {} ## map name:child + self.items = weakref.WeakKeyDictionary() ## keeps track of tree items representing this parameter + self._parent = None + self.treeStateChanges = [] ## cache of tree state changes to be delivered on next emit + self.blockTreeChangeEmit = 0 + #self.monitoringChildren = False ## prevent calling monitorChildren more than once + + if 'value' not in self.opts: + self.opts['value'] = None + + if 'name' not in self.opts or not isinstance(self.opts['name'], basestring): + raise Exception("Parameter must have a string name specified in opts.") + self.setName(opts['name']) + + self.addChildren(self.opts.get('children', [])) + + if 'value' in self.opts and 'default' not in self.opts: + self.opts['default'] = self.opts['value'] + + ## Connect all state changed signals to the general sigStateChanged + self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data)) + self.sigChildAdded.connect(lambda param, *data: self.emitStateChanged('childAdded', data)) + self.sigChildRemoved.connect(lambda param, data: self.emitStateChanged('childRemoved', data)) + self.sigParentChanged.connect(lambda param, data: self.emitStateChanged('parent', data)) + self.sigLimitsChanged.connect(lambda param, data: self.emitStateChanged('limits', data)) + 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.watchParam(self) ## emit treechange signals if our own state changes + + def name(self): + return self.opts['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)""" + if self.opts['strictNaming']: + if len(name) < 1 or re.search(r'\W', name) or re.match(r'\d', name[0]): + raise Exception("Parameter name '%s' is invalid. (Must contain only alphanumeric and underscore characters and may not start with a number)" % name) + parent = self.parent() + if parent is not None: + name = parent._renameChild(self, name) ## first ask parent if it's ok to rename + if self.opts['name'] != name: + self.opts['name'] = name + self.sigNameChanged.emit(self, name) + return name + + def type(self): + return self.opts['type'] + + def isType(self, typ): + """ + Return True if this parameter type matches the name *typ*. + This can occur either of two ways: + + - If self.type() == *typ* + - If this parameter's class is registered with the name *typ* + """ + if self.type() == typ: + return True + global PARAM_TYPES + cls = PARAM_TYPES.get(typ, None) + if cls is None: + raise Exception("Type name '%s' is not registered." % str(typ)) + return self.__class__ is cls + + def childPath(self, child): + """ + Return the path of parameter names from self to child. + If child is not a (grand)child of self, return None. + """ + path = [] + while child is not self: + path.insert(0, child.name()) + child = child.parent() + if child is None: + return None + return path + + def setValue(self, value, blockSignal=None): + ## return the actual value that was set + ## (this may be different from the value that was requested) + try: + if blockSignal is not None: + self.sigValueChanged.disconnect(blockSignal) + if self.opts['value'] == value: + return value + self.opts['value'] = value + self.sigValueChanged.emit(self, value) + finally: + if blockSignal is not None: + self.sigValueChanged.connect(blockSignal) + + return value + + def value(self): + return self.opts['value'] + + def getValues(self): + """Return a tree of all values that are children of this parameter""" + vals = OrderedDict() + for ch in self: + vals[ch.name()] = (ch.value(), ch.getValues()) + return vals + + def saveState(self): + """ + Return a structure representing the entire state of the parameter tree. + The tree state may be restored from this structure using restoreState() + """ + state = self.opts.copy() + state['children'] = OrderedDict([(ch.name(), ch.saveState()) for ch in self]) + if state['type'] is None: + global PARAM_NAMES + state['type'] = PARAM_NAMES.get(type(self), None) + return state + + def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True): + """ + Restore the state of this parameter and its children from a structure generated using saveState() + If recursive is True, then attempt to restore the state of child parameters as well. + If addChildren is True, then any children which are referenced in the state object will be + created if they do not already exist. + If removeChildren is True, then any children which are not referenced in the state object will + be removed. + If blockSignals is True, no signals will be emitted until the tree has been completely restored. + This prevents signal handlers from responding to a partially-rebuilt network. + """ + childState = state.get('children', []) + + ## list of children may be stored either as list or dict. + if isinstance(childState, dict): + childState = childState.values() + + + if blockSignals: + self.blockTreeChangeSignal() + + try: + self.setOpts(**state) + + if not recursive: + return + + ptr = 0 ## pointer to first child that has not been restored yet + foundChilds = set() + #print "==============", self.name() + + for ch in childState: + name = ch['name'] + typ = ch['type'] + #print('child: %s, %s' % (self.name()+'.'+name, typ)) + + ## First, see if there is already a child with this name and type + gotChild = False + for i, ch2 in enumerate(self.childs[ptr:]): + #print " ", ch2.name(), ch2.type() + if ch2.name() != name or not ch2.isType(typ): + continue + gotChild = True + #print " found it" + if i != 0: ## move parameter to next position + #self.removeChild(ch2) + self.insertChild(ptr, ch2) + #print " moved to position", ptr + ch2.restoreState(ch, recursive=recursive, addChildren=addChildren, removeChildren=removeChildren) + foundChilds.add(ch2) + + break + + if not gotChild: + if not addChildren: + #print " ignored child" + continue + #print " created new" + ch2 = Parameter.create(**ch) + self.insertChild(ptr, ch2) + foundChilds.add(ch2) + + ptr += 1 + + if removeChildren: + for ch in self.childs[:]: + if ch not in foundChilds: + #print " remove:", ch + self.removeChild(ch) + finally: + if blockSignals: + self.unblockTreeChangeSignal() + + + + def defaultValue(self): + """Return the default value for this parameter.""" + return self.opts['default'] + + def setDefault(self, val): + """Set the default value for this parameter.""" + if self.opts['default'] == val: + return + self.opts['default'] = val + self.sigDefaultChanged.emit(self, val) + + def setToDefault(self): + """Set this parameter's value to the default.""" + if self.hasDefault(): + self.setValue(self.defaultValue()) + + def hasDefault(self): + """Returns True if this parameter has a default value.""" + return 'default' in self.opts + + def valueIsDefault(self): + """Returns True if this parameter's value is equal to the default value.""" + return self.value() == self.defaultValue() + + def setLimits(self, limits): + """Set limits on the acceptable values for this parameter. + The format of limits depends on the type of the parameter and + some parameters do not make use of limits at all.""" + if 'limits' in self.opts and self.opts['limits'] == limits: + return + self.opts['limits'] = limits + self.sigLimitsChanged.emit(self, limits) + return limits + + def writable(self): + """ + Returns True if this parameter's value can be changed by the user. + Note that the value of the parameter can *always* be changed by + calling setValue(). + """ + return not self.opts.get('readonly', False) + + def setWritable(self, writable=True): + self.setOpts(readonly=not writable) + + def setReadonly(self, readonly=True): + self.setOpts(readonly=readonly) + + def setOpts(self, **opts): + """ + 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, and enabled. + """ + changed = OrderedDict() + for k in opts: + if k == 'value': + self.setValue(opts[k]) + elif k == 'name': + self.setName(opts[k]) + elif k == 'limits': + self.setLimits(opts[k]) + elif k == 'default': + self.setDefault(opts[k]) + elif k not in self.opts or self.opts[k] != opts[k]: + self.opts[k] = opts[k] + changed[k] = opts[k] + + if len(changed) > 0: + self.sigOptionsChanged.emit(self, changed) + + def emitStateChanged(self, changeDesc, data): + ## Emits stateChanged signal and + ## requests emission of new treeStateChanged signal + self.sigStateChanged.emit(self, changeDesc, data) + #self.treeStateChanged(self, changeDesc, data) + self.treeStateChanges.append((self, changeDesc, data)) + self.emitTreeChanges() + + def makeTreeItem(self, depth): + """Return a TreeWidgetItem suitable for displaying/controlling the content of this parameter. + Most subclasses will want to override this function. + """ + if hasattr(self, 'itemClass'): + #print "Param:", self, "Make item from itemClass:", self.itemClass + return self.itemClass(self, depth) + else: + return ParameterItem(self, depth=depth) + + + def addChild(self, child): + """Add another parameter to the end of this parameter's child list.""" + return self.insertChild(len(self.childs), child) + + def addChildren(self, children): + ## If children was specified as dict, then assume keys are the names. + if isinstance(children, dict): + ch2 = [] + for name, opts in children.items(): + if isinstance(opts, dict) and 'name' not in opts: + opts = opts.copy() + opts['name'] = name + ch2.append(opts) + children = ch2 + + for chOpts in children: + #print self, "Add child:", type(chOpts), id(chOpts) + self.addChild(chOpts) + + + def insertChild(self, pos, child): + """ + Insert a new child at pos. + If pos is a Parameter, then insert at the position of that Parameter. + If child is a dict, then a parameter is constructed as Parameter(\*\*child) + """ + if isinstance(child, dict): + child = Parameter.create(**child) + + name = child.name() + if name in self.names and child is not self.names[name]: + if child.opts.get('autoIncrementName', False): + name = self.incrementName(name) + child.setName(name) + else: + raise Exception("Already have child named %s" % str(name)) + if isinstance(pos, Parameter): + pos = self.childs.index(pos) + + with self.treeChangeBlocker(): + if child.parent() is not None: + child.remove() + + self.names[name] = child + self.childs.insert(pos, child) + + child.parentChanged(self) + self.sigChildAdded.emit(self, child, pos) + child.sigTreeStateChanged.connect(self.treeStateChanged) + return child + + def removeChild(self, child): + """Remove a child parameter.""" + name = child.name() + if name not in self.names or self.names[name] is not child: + raise Exception("Parameter %s is not my child; can't remove." % str(child)) + del self.names[name] + self.childs.pop(self.childs.index(child)) + child.parentChanged(None) + self.sigChildRemoved.emit(self, child) + try: + child.sigTreeStateChanged.disconnect(self.treeStateChanged) + except TypeError: ## already disconnected + pass + + def clearChildren(self): + """Remove all child parameters.""" + for ch in self.childs[:]: + self.removeChild(ch) + + def children(self): + """Return a list of this parameter's children.""" + ## warning -- this overrides QObject.children + return self.childs[:] + + def hasChildren(self): + return len(self.childs) > 0 + + def parentChanged(self, parent): + """This method is called when the parameter's parent has changed. + It may be useful to extend this method in subclasses.""" + self._parent = parent + self.sigParentChanged.emit(self, parent) + + def parent(self): + """Return the parent of this parameter.""" + return self._parent + + def remove(self): + """Remove this parameter from its parent's child list""" + parent = self.parent() + if parent is None: + raise Exception("Cannot remove; no parent.") + parent.removeChild(self) + + def incrementName(self, name): + ## return an unused name by adding a number to the name given + base, num = re.match('(.*)(\d*)', name).groups() + numLen = len(num) + if numLen == 0: + num = 2 + numLen = 1 + else: + num = int(num) + while True: + newName = base + ("%%0%dd"%numLen) % num + if newName not in self.names: + return newName + num += 1 + + def __iter__(self): + for ch in self.childs: + yield ch + + def __getitem__(self, names): + """Get the value of a child parameter. The name may also be a tuple giving + the path to a sub-parameter:: + + value = param[('child', 'grandchild')] + """ + if not isinstance(names, tuple): + names = (names,) + return self.param(*names).value() + + def __setitem__(self, names, value): + """Set the value of a child parameter. The name may also be a tuple giving + the path to a sub-parameter:: + + param[('child', 'grandchild')] = value + """ + if isinstance(names, basestring): + names = (names,) + return self.param(*names).setValue(value) + + def param(self, *names): + """Return a child parameter. + Accepts the name of the child or a tuple (path, to, child)""" + try: + param = self.names[names[0]] + except KeyError: + raise Exception("Parameter %s has no child named %s" % (self.name(), names[0])) + + if len(names) > 1: + return param.param(*names[1:]) + else: + return param + + def __repr__(self): + return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self)) + + def __getattr__(self, attr): + ## Leaving this undocumented because I might like to remove it in the future.. + #print type(self), attr + if 'names' not in self.__dict__: + raise AttributeError(attr) + if attr in self.names: + return self.param(attr) + else: + raise AttributeError(attr) + + def _renameChild(self, child, name): + ## Only to be called from Parameter.rename + if name in self.names: + return child.name() + self.names[name] = child + del self.names[child.name()] + return name + + def registerItem(self, item): + self.items[item] = None + + def hide(self): + """Hide this parameter. It and its children will no longer be visible in any ParameterTree + widgets it is connected to.""" + self.show(False) + + def show(self, s=True): + """Show this parameter. """ + self.opts['visible'] = s + self.sigOptionsChanged.emit(self, {'visible': s}) + + + #def monitorChildren(self): + #if self.monitoringChildren: + #raise Exception("Already monitoring children.") + #self.watchParam(self) + #self.monitoringChildren = True + + #def watchParam(self, param): + #param.sigChildAdded.connect(self.grandchildAdded) + #param.sigChildRemoved.connect(self.grandchildRemoved) + #param.sigStateChanged.connect(self.grandchildChanged) + #for ch in param: + #self.watchParam(ch) + + #def unwatchParam(self, param): + #param.sigChildAdded.disconnect(self.grandchildAdded) + #param.sigChildRemoved.disconnect(self.grandchildRemoved) + #param.sigStateChanged.disconnect(self.grandchildChanged) + #for ch in param: + #self.unwatchParam(ch) + + #def grandchildAdded(self, parent, child): + #self.watchParam(child) + + #def grandchildRemoved(self, parent, child): + #self.unwatchParam(child) + + #def grandchildChanged(self, param, change, data): + ##self.sigTreeStateChanged.emit(self, param, change, data) + #self.emitTreeChange((param, change, data)) + + def treeChangeBlocker(self): + """ + Return an object that can be used to temporarily block and accumulate + sigTreeStateChanged signals. This is meant to be used when numerous changes are + about to be made to the tree and only one change signal should be + emitted at the end. + + Example:: + + with param.treeChangeBlocker(): + param.addChild(...) + param.removeChild(...) + param.setValue(...) + """ + return SignalBlocker(self.blockTreeChangeSignal, self.unblockTreeChangeSignal) + + def blockTreeChangeSignal(self): + """ + Used to temporarily block and accumulate tree change signals. + *You must remember to unblock*, so it is advisable to use treeChangeBlocker() instead. + """ + self.blockTreeChangeEmit += 1 + + def unblockTreeChangeSignal(self): + """Unblocks enission of sigTreeStateChanged and flushes the changes out through a single signal.""" + self.blockTreeChangeEmit -= 1 + self.emitTreeChanges() + + + def treeStateChanged(self, param, changes): + """ + Called when the state of any sub-parameter has changed. + + ========== ================================================================ + Arguments: + param The immediate child whose tree state has changed. + note that the change may have originated from a grandchild. + changes List of tuples describing all changes that have been made + in this event: (param, changeDescr, data) + ========== ================================================================ + + This function can be extended to react to tree state changes. + """ + self.treeStateChanges.extend(changes) + self.emitTreeChanges() + + def emitTreeChanges(self): + if self.blockTreeChangeEmit == 0: + changes = self.treeStateChanges + self.treeStateChanges = [] + self.sigTreeStateChanged.emit(self, changes) + + +class SignalBlocker(object): + def __init__(self, enterFn, exitFn): + self.enterFn = enterFn + self.exitFn = exitFn + + def __enter__(self): + self.enterFn() + + def __exit__(self, exc_type, exc_value, tb): + self.exitFn() + + + diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py new file mode 100644 index 00000000..376e900d --- /dev/null +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -0,0 +1,159 @@ +from pyqtgraph.Qt import QtGui, QtCore +import os, weakref, re + +class ParameterItem(QtGui.QTreeWidgetItem): + """ + Abstract ParameterTree item. + Used to represent the state of a Parameter from within a ParameterTree. + + - Sets first column of item to name + - generates context menu if item is renamable or removable + - handles child added / removed events + - provides virtual functions for handling changes from parameter + + For more ParameterItem types, see ParameterTree.parameterTypes module. + """ + + def __init__(self, param, depth=0): + QtGui.QTreeWidgetItem.__init__(self, [param.name(), '']) + + self.param = param + self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging) + self.depth = depth + + param.sigValueChanged.connect(self.valueChanged) + param.sigChildAdded.connect(self.childAdded) + param.sigChildRemoved.connect(self.childRemoved) + param.sigNameChanged.connect(self.nameChanged) + param.sigLimitsChanged.connect(self.limitsChanged) + param.sigDefaultChanged.connect(self.defaultChanged) + param.sigOptionsChanged.connect(self.optsChanged) + param.sigParentChanged.connect(self.parentChanged) + + + opts = 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): + 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): + flags |= QtCore.Qt.ItemIsDragEnabled + if opts.get('dropEnabled', False): + flags |= QtCore.Qt.ItemIsDropEnabled + self.setFlags(flags) + + ## flag used internally during name editing + self.ignoreNameColumnChange = False + + + def valueChanged(self, param, val): + ## called when the parameter's value has changed + pass + + def isFocusable(self): + """Return True if this item should be included in the tab-focus order""" + return False + + def setFocus(self): + """Give input focus to this item. + Can be reimplemented to display editor widgets, etc. + """ + pass + + def focusNext(self, forward=True): + """Give focus to the next (or previous) focusable item in the parameter tree""" + self.treeWidget().focusNext(self, forward=forward) + + + def treeWidgetChanged(self): + """Called when this item is added or removed from a tree. + Expansion, visibility, and column widgets must all be configured AFTER + the item is added to a tree, not during __init__. + """ + self.setHidden(not self.param.opts.get('visible', True)) + self.setExpanded(self.param.opts.get('expanded', True)) + + def childAdded(self, param, child, pos): + item = child.makeTreeItem(depth=self.depth+1) + self.insertChild(pos, item) + item.treeWidgetChanged() + + for i, ch in enumerate(child): + item.childAdded(child, ch, i) + + def childRemoved(self, param, child): + for i in range(self.childCount()): + item = self.child(i) + if item.param is child: + self.takeChild(i) + break + + def parentChanged(self, param, parent): + ## called when the parameter's parent has changed. + pass + + def contextMenuEvent(self, ev): + if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False): + return + + self.contextMenu.popup(ev.globalPos()) + + def columnChangedEvent(self, col): + """Called when the text in a column has been edited. + By default, we only use changes to column 0 to rename the parameter. + """ + if col == 0: + if self.ignoreNameColumnChange: + return + try: + newName = self.param.setName(str(self.text(col))) + except: + self.setText(0, self.param.name()) + raise + + try: + self.ignoreNameColumnChange = True + self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back. + finally: + self.ignoreNameColumnChange = False + + def nameChanged(self, param, name): + ## called when the parameter's name has changed. + self.setText(0, name) + + def limitsChanged(self, param, limits): + """Called when the parameter's limits have changed""" + pass + + def defaultChanged(self, param, default): + """Called when the parameter's default value has changed""" + pass + + 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']) + + def editName(self): + self.treeWidget().editItem(self, 0) + + def selected(self, sel): + """Called when this item has been selected (sel=True) OR deselected (sel=False)""" + pass + + def requestRemove(self): + ## called when remove is selected from the context menu. + ## we need to delay removal until the action is complete + ## since destroying the menu in mid-action will cause a crash. + QtCore.QTimer.singleShot(0, self.param.remove) + diff --git a/pyqtgraph/parametertree/ParameterTree.py b/pyqtgraph/parametertree/ParameterTree.py new file mode 100644 index 00000000..e57430ea --- /dev/null +++ b/pyqtgraph/parametertree/ParameterTree.py @@ -0,0 +1,118 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.widgets.TreeWidget import TreeWidget +import os, weakref, re +#import functions as fn + + + +class ParameterTree(TreeWidget): + """Widget used to display or control data from a ParameterSet""" + + def __init__(self, parent=None, showHeader=True): + TreeWidget.__init__(self, parent) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setHorizontalScrollMode(self.ScrollPerPixel) + self.setAnimated(False) + self.setColumnCount(2) + self.setHeaderLabels(["Parameter", "Value"]) + self.setAlternatingRowColors(True) + self.paramSet = None + self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) + self.setHeaderHidden(not showHeader) + self.itemChanged.connect(self.itemChangedEvent) + self.lastSel = None + self.setRootIsDecorated(False) + + def setParameters(self, param, showTop=True): + self.clear() + self.addParameters(param, showTop=showTop) + + def addParameters(self, param, root=None, depth=0, showTop=True): + item = param.makeTreeItem(depth=depth) + if root is None: + root = self.invisibleRootItem() + ## Hide top-level item + if not showTop: + item.setText(0, '') + item.setSizeHint(0, QtCore.QSize(1,1)) + item.setSizeHint(1, QtCore.QSize(1,1)) + depth -= 1 + root.addChild(item) + item.treeWidgetChanged() + + for ch in param: + self.addParameters(ch, root=item, depth=depth+1) + + def clear(self): + self.invisibleRootItem().takeChildren() + + + def focusNext(self, item, forward=True): + ## Give input focus to the next (or previous) item after 'item' + while True: + parent = item.parent() + if parent is None: + return + nextItem = self.nextFocusableChild(parent, item, forward=forward) + if nextItem is not None: + nextItem.setFocus() + self.setCurrentItem(nextItem) + return + item = parent + + def focusPrevious(self, item): + self.focusNext(item, forward=False) + + def nextFocusableChild(self, root, startItem=None, forward=True): + if startItem is None: + if forward: + index = 0 + else: + index = root.childCount()-1 + else: + if forward: + index = root.indexOfChild(startItem) + 1 + else: + index = root.indexOfChild(startItem) - 1 + + if forward: + inds = list(range(index, root.childCount())) + else: + inds = list(range(index, -1, -1)) + + for i in inds: + item = root.child(i) + if hasattr(item, 'isFocusable') and item.isFocusable(): + return item + else: + item = self.nextFocusableChild(item, forward=forward) + if item is not None: + return item + return None + + def contextMenuEvent(self, ev): + item = self.currentItem() + if hasattr(item, 'contextMenuEvent'): + item.contextMenuEvent(ev) + + def itemChangedEvent(self, item, col): + if hasattr(item, 'columnChangedEvent'): + item.columnChangedEvent(col) + + def selectionChanged(self, *args): + sel = self.selectedItems() + if len(sel) != 1: + sel = None + if self.lastSel is not None: + self.lastSel.selected(False) + if sel is None: + self.lastSel = None + return + self.lastSel = sel[0] + if hasattr(sel[0], 'selected'): + sel[0].selected(True) + return TreeWidget.selectionChanged(self, *args) + + def wheelEvent(self, ev): + self.clearSelection() + return TreeWidget.wheelEvent(self, ev) diff --git a/pyqtgraph/parametertree/__init__.py b/pyqtgraph/parametertree/__init__.py new file mode 100644 index 00000000..acdb7a37 --- /dev/null +++ b/pyqtgraph/parametertree/__init__.py @@ -0,0 +1,5 @@ +from .Parameter import Parameter, registerParameterType +from .ParameterTree import ParameterTree +from .ParameterItem import ParameterItem + +from . import parameterTypes as types \ No newline at end of file diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py new file mode 100644 index 00000000..3aab5a6d --- /dev/null +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -0,0 +1,616 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.python2_3 import asUnicode +from .Parameter import Parameter, registerParameterType +from .ParameterItem import ParameterItem +from pyqtgraph.widgets.SpinBox import SpinBox +from pyqtgraph.widgets.ColorButton import ColorButton +import pyqtgraph as pg +import pyqtgraph.pixmaps as pixmaps +import os +from pyqtgraph.pgcollections import OrderedDict + +class WidgetParameterItem(ParameterItem): + """ + ParameterTree item with: + + - label in second column for displaying value + - simple widget for editing value (displayed instead of label when item is selected) + - button that resets value to default + - provides SpinBox, CheckBox, LineEdit, and ColorButton types + + This class can be subclassed by overriding makeWidget() to provide a custom widget. + """ + def __init__(self, param, depth): + ParameterItem.__init__(self, param, depth) + + self.hideWidget = True ## hide edit widget, replace with label when not selected + ## set this to False to keep the editor widget always visible + + + ## build widget into column 1 with a display label and default button. + w = self.makeWidget() + 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) + modDir = os.path.dirname(__file__) + self.defaultBtn.setIcon(QtGui.QIcon(pixmaps.getPixmap('default'))) + self.defaultBtn.clicked.connect(self.defaultClicked) + + self.displayLabel = QtGui.QLabel() + + layout = QtGui.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(2) + layout.addWidget(w) + layout.addWidget(self.displayLabel) + layout.addWidget(self.defaultBtn) + self.layoutWidget = QtGui.QWidget() + self.layoutWidget.setLayout(layout) + + if w.sigChanged is not None: + w.sigChanged.connect(self.widgetValueChanged) + + if hasattr(w, 'sigChanging'): + w.sigChanging.connect(self.widgetValueChanging) + + ## update value shown in widget. + self.valueChanged(self, opts['value'], force=True) + + + def makeWidget(self): + """ + Return a single widget that should be placed in the second tree column. + The widget must be given three attributes: + + ========== ============================================================ + sigChanged a signal that is emitted when the widget's value is changed + value a function that returns the value + setValue a function that sets the value + ========== ============================================================ + + This is a good function to override in subclasses. + """ + opts = self.param.opts + t = opts['type'] + if t == 'int': + defs = { + 'value': 0, 'min': None, 'max': None, 'int': True, + 'step': 1.0, 'minStep': 1.0, 'dec': False, + 'siPrefix': False, 'suffix': '' + } + defs.update(opts) + if 'limits' in opts: + defs['bounds'] = opts['limits'] + w = SpinBox() + w.setOpts(**defs) + w.sigChanged = w.sigValueChanged + w.sigChanging = w.sigValueChanging + elif t == 'float': + defs = { + 'value': 0, 'min': None, 'max': None, + 'step': 1.0, 'dec': False, + 'siPrefix': False, 'suffix': '' + } + defs.update(opts) + if 'limits' in opts: + defs['bounds'] = opts['limits'] + w = SpinBox() + w.setOpts(**defs) + w.sigChanged = w.sigValueChanged + w.sigChanging = w.sigValueChanging + elif t == 'bool': + w = QtGui.QCheckBox() + w.sigChanged = w.toggled + w.value = w.isChecked + w.setValue = w.setChecked + self.hideWidget = False + elif t == 'str': + w = QtGui.QLineEdit() + w.sigChanged = w.editingFinished + w.value = lambda: asUnicode(w.text()) + w.setValue = lambda v: w.setText(asUnicode(v)) + w.sigChanging = w.textChanged + elif t == 'color': + w = ColorButton() + w.sigChanged = w.sigColorChanged + w.sigChanging = w.sigColorChanging + w.value = w.color + w.setValue = w.setColor + self.hideWidget = False + w.setFlat(True) + else: + raise Exception("Unknown type '%s'" % asUnicode(t)) + return w + + def widgetEventFilter(self, obj, ev): + ## filter widget's events + ## catch TAB to change focus + ## catch focusOut to hide editor + if ev.type() == ev.KeyPress: + if ev.key() == QtCore.Qt.Key_Tab: + self.focusNext(forward=True) + return True ## don't let anyone else see this event + elif ev.key() == QtCore.Qt.Key_Backtab: + self.focusNext(forward=False) + return True ## don't let anyone else see this event + + #elif ev.type() == ev.FocusOut: + #self.hideEditor() + return False + + def setFocus(self): + self.showEditor() + + def isFocusable(self): + return self.param.writable() + + def valueChanged(self, param, val, force=False): + ## called when the parameter's value has changed + ParameterItem.valueChanged(self, param, val) + self.widget.sigChanged.disconnect(self.widgetValueChanged) + try: + if force or val != self.widget.value(): + self.widget.setValue(val) + self.updateDisplayLabel(val) ## always make sure label is updated, even if values match! + finally: + self.widget.sigChanged.connect(self.widgetValueChanged) + self.updateDefaultBtn() + + def updateDefaultBtn(self): + ## enable/disable default btn + self.defaultBtn.setEnabled(not self.param.valueIsDefault() and self.param.writable()) + + def updateDisplayLabel(self, value=None): + """Update the display label to reflect the value of the parameter.""" + if value is None: + value = self.param.value() + opts = self.param.opts + if isinstance(self.widget, QtGui.QAbstractSpinBox): + text = asUnicode(self.widget.lineEdit().text()) + elif isinstance(self.widget, QtGui.QComboBox): + text = self.widget.currentText() + else: + text = asUnicode(value) + self.displayLabel.setText(text) + + def widgetValueChanged(self): + ## called when the widget's value has been changed by the user + val = self.widget.value() + newVal = self.param.setValue(val) + + def widgetValueChanging(self): + """ + Called when the widget's value is changing, but not finalized. + For example: editing text before pressing enter or changing focus. + """ + pass + + def selected(self, sel): + """Called when this item has been selected (sel=True) OR deselected (sel=False)""" + ParameterItem.selected(self, sel) + + if self.widget is None: + return + if sel and self.param.writable(): + self.showEditor() + elif self.hideWidget: + self.hideEditor() + + def showEditor(self): + self.widget.show() + self.displayLabel.hide() + self.widget.setFocus(QtCore.Qt.OtherFocusReason) + + def hideEditor(self): + self.widget.hide() + self.displayLabel.show() + + def limitsChanged(self, param, limits): + """Called when the parameter's limits have changed""" + ParameterItem.limitsChanged(self, param, limits) + + t = self.param.opts['type'] + if t == 'int' or t == 'float': + self.widget.setOpts(bounds=limits) + else: + return ## don't know what to do with any other types.. + + def defaultChanged(self, param, value): + self.updateDefaultBtn() + + def treeWidgetChanged(self): + """Called when this item is added or removed from a tree.""" + ParameterItem.treeWidgetChanged(self) + + ## add all widgets for this item into the tree + if self.widget is not None: + tree = self.treeWidget() + if tree is None: + return + tree.setItemWidget(self, 1, self.layoutWidget) + self.displayLabel.hide() + self.selected(False) + + def defaultClicked(self): + self.param.setToDefault() + + def optsChanged(self, param, opts): + """Called when any options are changed that are not + name, value, default, or limits""" + #print "opts changed:", opts + ParameterItem.optsChanged(self, param, opts) + + if 'readonly' in opts: + self.updateDefaultBtn() + + ## If widget is a SpinBox, pass options straight through + if isinstance(self.widget, SpinBox): + if 'units' in opts and 'suffix' not in opts: + opts['suffix'] = opts['units'] + self.widget.setOpts(**opts) + self.updateDisplayLabel() + +class EventProxy(QtCore.QObject): + def __init__(self, qobj, callback): + QtCore.QObject.__init__(self) + self.callback = callback + qobj.installEventFilter(self) + + def eventFilter(self, obj, ev): + return self.callback(obj, ev) + + + + +class SimpleParameter(Parameter): + itemClass = WidgetParameterItem + + def __init__(self, *args, **kargs): + Parameter.__init__(self, *args, **kargs) + + ## override a few methods for color parameters + if self.opts['type'] == 'color': + self.value = self.colorValue + self.saveState = self.saveColorState + + def colorValue(self): + return pg.mkColor(Parameter.value(self)) + + def saveColorState(self): + state = Parameter.saveState(self) + state['value'] = pg.colorTuple(self.value()) + return state + + +registerParameterType('int', SimpleParameter, override=True) +registerParameterType('float', SimpleParameter, override=True) +registerParameterType('bool', SimpleParameter, override=True) +registerParameterType('str', SimpleParameter, override=True) +registerParameterType('color', SimpleParameter, override=True) + + + + +class GroupParameterItem(ParameterItem): + """ + Group parameters are used mainly as a generic parent item that holds (and groups!) a set + of child parameters. It also provides a simple mechanism for displaying a button or combo + that can be used to add new parameters to the group. + """ + def __init__(self, param, depth): + ParameterItem.__init__(self, param, depth) + self.updateDepth(depth) + + self.addItem = None + if 'addText' in param.opts: + addText = param.opts['addText'] + if 'addList' in param.opts: + self.addWidget = QtGui.QComboBox() + self.addWidget.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + self.updateAddList() + self.addWidget.currentIndexChanged.connect(self.addChanged) + else: + self.addWidget = QtGui.QPushButton(addText) + self.addWidget.clicked.connect(self.addClicked) + w = QtGui.QWidget() + l = QtGui.QHBoxLayout() + l.setContentsMargins(0,0,0,0) + w.setLayout(l) + l.addWidget(self.addWidget) + l.addStretch() + #l.addItem(QtGui.QSpacerItem(200, 10, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)) + self.addWidgetBox = w + self.addItem = QtGui.QTreeWidgetItem([]) + self.addItem.setFlags(QtCore.Qt.ItemIsEnabled) + ParameterItem.addChild(self, self.addItem) + + def updateDepth(self, depth): + ## Change item's appearance based on its depth in the tree + ## This allows highest-level groups to be displayed more prominently. + if depth == 0: + for c in [0,1]: + self.setBackground(c, QtGui.QBrush(QtGui.QColor(100,100,100))) + self.setForeground(c, QtGui.QBrush(QtGui.QColor(220,220,255))) + font = self.font(c) + font.setBold(True) + font.setPointSize(font.pointSize()+1) + self.setFont(c, font) + self.setSizeHint(0, QtCore.QSize(0, 25)) + else: + for c in [0,1]: + self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) + font = self.font(c) + font.setBold(True) + #font.setPointSize(font.pointSize()+1) + self.setFont(c, font) + self.setSizeHint(0, QtCore.QSize(0, 20)) + + def addClicked(self): + """Called when "add new" button is clicked + The parameter MUST have an 'addNew' method defined. + """ + self.param.addNew() + + def addChanged(self): + """Called when "add new" combo is changed + The parameter MUST have an 'addNew' method defined. + """ + if self.addWidget.currentIndex() == 0: + return + typ = asUnicode(self.addWidget.currentText()) + self.param.addNew(typ) + self.addWidget.setCurrentIndex(0) + + def treeWidgetChanged(self): + ParameterItem.treeWidgetChanged(self) + self.treeWidget().setFirstItemColumnSpanned(self, True) + if self.addItem is not None: + self.treeWidget().setItemWidget(self.addItem, 0, self.addWidgetBox) + self.treeWidget().setFirstItemColumnSpanned(self.addItem, True) + + def addChild(self, child): ## make sure added childs are actually inserted before add btn + if self.addItem is not None: + ParameterItem.insertChild(self, self.childCount()-1, child) + else: + ParameterItem.addChild(self, child) + + def optsChanged(self, param, changed): + if 'addList' in changed: + self.updateAddList() + + def updateAddList(self): + self.addWidget.blockSignals(True) + try: + self.addWidget.clear() + self.addWidget.addItem(self.param.opts['addText']) + for t in self.param.opts['addList']: + self.addWidget.addItem(t) + finally: + self.addWidget.blockSignals(False) + +class GroupParameter(Parameter): + """ + Group parameters are used mainly as a generic parent item that holds (and groups!) a set + of child parameters. + + It also provides a simple mechanism for displaying a button or combo + that can be used to add new parameters to the group. To enable this, the group + must be initialized with the 'addText' option (the text will be displayed on + a button which, when clicked, will cause addNew() to be called). If the 'addList' + option is specified as well, then a dropdown-list of addable items will be displayed + instead of a button. + """ + itemClass = GroupParameterItem + + def addNew(self, typ=None): + """ + This method is called when the user has requested to add a new item to the group. + """ + raise Exception("Must override this function in subclass.") + + def setAddList(self, vals): + """Change the list of options available for the user to add to the group.""" + self.setOpts(addList=vals) + + + +registerParameterType('group', GroupParameter, override=True) + + + + + +class ListParameterItem(WidgetParameterItem): + """ + WidgetParameterItem subclass providing comboBox that lets the user select from a list of options. + + """ + def __init__(self, param, depth): + self.targetValue = None + WidgetParameterItem.__init__(self, param, depth) + + + def makeWidget(self): + opts = self.param.opts + t = opts['type'] + w = QtGui.QComboBox() + w.setMaximumHeight(20) ## set to match height of spin box and line edit + w.sigChanged = w.currentIndexChanged + w.value = self.value + w.setValue = self.setValue + self.widget = w ## needs to be set before limits are changed + self.limitsChanged(self.param, self.param.opts['limits']) + if len(self.forward) > 0: + self.setValue(self.param.value()) + return w + + def value(self): + #vals = self.param.opts['limits'] + key = asUnicode(self.widget.currentText()) + #if isinstance(vals, dict): + #return vals[key] + #else: + #return key + #print key, self.forward + + return self.forward.get(key, None) + + def setValue(self, val): + #vals = self.param.opts['limits'] + #if isinstance(vals, dict): + #key = None + #for k,v in vals.iteritems(): + #if v == val: + #key = k + #if key is None: + #raise Exception("Value '%s' not allowed." % val) + #else: + #key = unicode(val) + self.targetValue = val + if val not in self.reverse: + self.widget.setCurrentIndex(0) + else: + key = self.reverse[val] + ind = self.widget.findText(key) + self.widget.setCurrentIndex(ind) + + def limitsChanged(self, param, limits): + # set up forward / reverse mappings for name:value + + if len(limits) == 0: + limits = [''] ## Can never have an empty list--there is always at least a singhe blank item. + + self.forward, self.reverse = ListParameter.mapping(limits) + try: + self.widget.blockSignals(True) + val = self.targetValue #asUnicode(self.widget.currentText()) + + self.widget.clear() + for k in self.forward: + self.widget.addItem(k) + if k == val: + self.widget.setCurrentIndex(self.widget.count()-1) + self.updateDisplayLabel() + finally: + self.widget.blockSignals(False) + + + +class ListParameter(Parameter): + itemClass = ListParameterItem + + def __init__(self, **opts): + self.forward = OrderedDict() ## name: value + self.reverse = OrderedDict() ## value: name + + ## Parameter uses 'limits' option to define the set of allowed values + if 'values' in opts: + opts['limits'] = opts['values'] + if opts.get('limits', None) is None: + opts['limits'] = [] + Parameter.__init__(self, **opts) + self.setLimits(opts['limits']) + + def setLimits(self, limits): + self.forward, self.reverse = self.mapping(limits) + + Parameter.setLimits(self, limits) + #print self.name(), self.value(), limits + if self.value() not in self.reverse and len(self.reverse) > 0: + self.setValue(list(self.reverse.keys())[0]) + + @staticmethod + def mapping(limits): + ## Return forward and reverse mapping dictionaries given a limit specification + forward = OrderedDict() ## name: value + reverse = OrderedDict() ## value: name + if isinstance(limits, dict): + for k, v in limits.items(): + forward[k] = v + reverse[v] = k + else: + for v in limits: + n = asUnicode(v) + forward[n] = v + reverse[v] = n + return forward, reverse + +registerParameterType('list', ListParameter, override=True) + + + +class ActionParameterItem(ParameterItem): + def __init__(self, param, depth): + ParameterItem.__init__(self, param, depth) + self.layoutWidget = QtGui.QWidget() + self.layout = QtGui.QHBoxLayout() + self.layoutWidget.setLayout(self.layout) + self.button = QtGui.QPushButton(param.name()) + #self.layout.addSpacing(100) + self.layout.addWidget(self.button) + self.layout.addStretch() + self.button.clicked.connect(self.buttonClicked) + param.sigNameChanged.connect(self.paramRenamed) + self.setText(0, '') + + def treeWidgetChanged(self): + ParameterItem.treeWidgetChanged(self) + tree = self.treeWidget() + if tree is None: + return + + tree.setFirstItemColumnSpanned(self, True) + tree.setItemWidget(self, 0, self.layoutWidget) + + def paramRenamed(self, param, name): + self.button.setText(name) + + def buttonClicked(self): + self.param.activate() + +class ActionParameter(Parameter): + """Used for displaying a button within the tree.""" + itemClass = ActionParameterItem + sigActivated = QtCore.Signal(object) + + def activate(self): + self.sigActivated.emit(self) + self.emitStateChanged('activated', None) + +registerParameterType('action', ActionParameter, override=True) + + + +class TextParameterItem(WidgetParameterItem): + def __init__(self, param, depth): + WidgetParameterItem.__init__(self, param, depth) + self.subItem = QtGui.QTreeWidgetItem() + self.addChild(self.subItem) + + def treeWidgetChanged(self): + self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) + self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + self.setExpanded(True) + + def makeWidget(self): + self.textBox = QtGui.QTextEdit() + self.textBox.setMaximumHeight(100) + self.textBox.value = lambda: str(self.textBox.toPlainText()) + self.textBox.setValue = self.textBox.setPlainText + self.textBox.sigChanged = self.textBox.textChanged + return self.textBox + +class TextParameter(Parameter): + """Editable string; displayed as large text box in the tree.""" + itemClass = TextParameterItem + + + +registerParameterType('text', TextParameter, override=True) diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py new file mode 100644 index 00000000..b0198526 --- /dev/null +++ b/pyqtgraph/pgcollections.py @@ -0,0 +1,543 @@ +# -*- coding: utf-8 -*- +""" +advancedTypes.py - Basic data structures not included with python +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Includes: + - OrderedDict - Dictionary which preserves the order of its elements + - BiDict, ReverseDict - Bi-directional dictionaries + - ThreadsafeDict, ThreadsafeList - Self-mutexed data structures +""" + +import threading, sys, copy, collections +#from debug import * + +try: + from collections import OrderedDict +except: + # Deprecated; this class is now present in Python 2.7 as collections.OrderedDict + # Only keeping this around for python2.6 support. + class OrderedDict(dict): + """extends dict so that elements are iterated in the order that they were added. + Since this class can not be instantiated with regular dict notation, it instead uses + a list of tuples: + od = OrderedDict([(key1, value1), (key2, value2), ...]) + items set using __setattr__ are added to the end of the key list. + """ + + def __init__(self, data=None): + self.order = [] + if data is not None: + for i in data: + self[i[0]] = i[1] + + def __setitem__(self, k, v): + if not self.has_key(k): + self.order.append(k) + dict.__setitem__(self, k, v) + + def __delitem__(self, k): + self.order.remove(k) + dict.__delitem__(self, k) + + def keys(self): + return self.order[:] + + def items(self): + it = [] + for k in self.keys(): + it.append((k, self[k])) + return it + + def values(self): + return [self[k] for k in self.order] + + def remove(self, key): + del self[key] + #self.order.remove(key) + + def __iter__(self): + for k in self.order: + yield k + + def update(self, data): + """Works like dict.update, but accepts list-of-tuples as well as dict.""" + if isinstance(data, dict): + for k, v in data.iteritems(): + self[k] = v + else: + for k,v in data: + self[k] = v + + def copy(self): + return OrderedDict(self.items()) + + def itervalues(self): + for k in self.order: + yield self[k] + + def iteritems(self): + for k in self.order: + yield (k, self[k]) + + def __deepcopy__(self, memo): + return OrderedDict([(k, copy.deepcopy(v, memo)) for k, v in self.iteritems()]) + + + +class ReverseDict(dict): + """extends dict so that reverse lookups are possible by requesting the key as a list of length 1: + d = BiDict({'x': 1, 'y': 2}) + d['x'] + 1 + d[[2]] + 'y' + """ + def __init__(self, data=None): + if data is None: + data = {} + self.reverse = {} + for k in data: + self.reverse[data[k]] = k + dict.__init__(self, data) + + def __getitem__(self, item): + if type(item) is list: + return self.reverse[item[0]] + else: + return dict.__getitem__(self, item) + + def __setitem__(self, item, value): + self.reverse[value] = item + dict.__setitem__(self, item, value) + + def __deepcopy__(self, memo): + raise Exception("deepcopy not implemented") + + +class BiDict(dict): + """extends dict so that reverse lookups are possible by adding each reverse combination to the dict. + This only works if all values and keys are unique.""" + def __init__(self, data=None): + if data is None: + data = {} + dict.__init__(self) + for k in data: + self[data[k]] = k + + def __setitem__(self, item, value): + dict.__setitem__(self, item, value) + dict.__setitem__(self, value, item) + + def __deepcopy__(self, memo): + raise Exception("deepcopy not implemented") + +class ThreadsafeDict(dict): + """Extends dict so that getitem, setitem, and contains are all thread-safe. + Also adds lock/unlock functions for extended exclusive operations + Converts all sub-dicts and lists to threadsafe as well. + """ + + def __init__(self, *args, **kwargs): + self.mutex = threading.RLock() + dict.__init__(self, *args, **kwargs) + for k in self: + if type(self[k]) is dict: + self[k] = ThreadsafeDict(self[k]) + + def __getitem__(self, attr): + self.lock() + try: + val = dict.__getitem__(self, attr) + finally: + self.unlock() + return val + + def __setitem__(self, attr, val): + if type(val) is dict: + val = ThreadsafeDict(val) + self.lock() + try: + dict.__setitem__(self, attr, val) + finally: + self.unlock() + + def __contains__(self, attr): + self.lock() + try: + val = dict.__contains__(self, attr) + finally: + self.unlock() + return val + + def __len__(self): + self.lock() + try: + val = dict.__len__(self) + finally: + self.unlock() + return val + + def clear(self): + self.lock() + try: + dict.clear(self) + finally: + self.unlock() + + def lock(self): + self.mutex.acquire() + + def unlock(self): + self.mutex.release() + + def __deepcopy__(self, memo): + raise Exception("deepcopy not implemented") + +class ThreadsafeList(list): + """Extends list so that getitem, setitem, and contains are all thread-safe. + Also adds lock/unlock functions for extended exclusive operations + Converts all sub-lists and dicts to threadsafe as well. + """ + + def __init__(self, *args, **kwargs): + self.mutex = threading.RLock() + list.__init__(self, *args, **kwargs) + for k in self: + self[k] = mkThreadsafe(self[k]) + + def __getitem__(self, attr): + self.lock() + try: + val = list.__getitem__(self, attr) + finally: + self.unlock() + return val + + def __setitem__(self, attr, val): + val = makeThreadsafe(val) + self.lock() + try: + list.__setitem__(self, attr, val) + finally: + self.unlock() + + def __contains__(self, attr): + self.lock() + try: + val = list.__contains__(self, attr) + finally: + self.unlock() + return val + + def __len__(self): + self.lock() + try: + val = list.__len__(self) + finally: + self.unlock() + return val + + def lock(self): + self.mutex.acquire() + + def unlock(self): + self.mutex.release() + + def __deepcopy__(self, memo): + raise Exception("deepcopy not implemented") + + +def makeThreadsafe(obj): + if type(obj) is dict: + return ThreadsafeDict(obj) + elif type(obj) is list: + return ThreadsafeList(obj) + elif type(obj) in [str, int, float, bool, tuple]: + return obj + else: + raise Exception("Not sure how to make object of type %s thread-safe" % str(type(obj))) + + +class Locker(object): + def __init__(self, lock): + self.lock = lock + self.lock.acquire() + def __del__(self): + try: + self.lock.release() + except: + pass + +class CaselessDict(OrderedDict): + """Case-insensitive dict. Values can be set and retrieved using keys of any case. + Note that when iterating, the original case is returned for each key.""" + def __init__(self, *args): + OrderedDict.__init__(self, {}) ## requirement for the empty {} here seems to be a python bug? + self.keyMap = OrderedDict([(k.lower(), k) for k in OrderedDict.keys(self)]) + if len(args) == 0: + return + elif len(args) == 1 and isinstance(args[0], dict): + for k in args[0]: + self[k] = args[0][k] + else: + raise Exception("CaselessDict may only be instantiated with a single dict.") + + #def keys(self): + #return self.keyMap.values() + + def __setitem__(self, key, val): + kl = key.lower() + if kl in self.keyMap: + OrderedDict.__setitem__(self, self.keyMap[kl], val) + else: + OrderedDict.__setitem__(self, key, val) + self.keyMap[kl] = key + + def __getitem__(self, key): + kl = key.lower() + if kl not in self.keyMap: + raise KeyError(key) + return OrderedDict.__getitem__(self, self.keyMap[kl]) + + def __contains__(self, key): + return key.lower() in self.keyMap + + def update(self, d): + for k, v in d.iteritems(): + self[k] = v + + def copy(self): + return CaselessDict(OrderedDict.copy(self)) + + def __delitem__(self, key): + kl = key.lower() + if kl not in self.keyMap: + raise KeyError(key) + OrderedDict.__delitem__(self, self.keyMap[kl]) + del self.keyMap[kl] + + def __deepcopy__(self, memo): + raise Exception("deepcopy not implemented") + + def clear(self): + OrderedDict.clear(self) + self.keyMap.clear() + + + +class ProtectedDict(dict): + """ + A class allowing read-only 'view' of a dict. + The object can be treated like a normal dict, but will never modify the original dict it points to. + Any values accessed from the dict will also be read-only. + """ + def __init__(self, data): + self._data_ = data + + ## List of methods to directly wrap from _data_ + wrapMethods = ['_cmp_', '__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'has_key', 'iterkeys', 'keys', ] + + ## List of methods which wrap from _data_ but return protected results + protectMethods = ['__getitem__', '__iter__', 'get', 'items', 'values'] + + ## List of methods to disable + disableMethods = ['__delitem__', '__setitem__', 'clear', 'pop', 'popitem', 'setdefault', 'update'] + + + ## Template methods + def wrapMethod(methodName): + return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) + + def protectMethod(methodName): + return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) + + def error(self, *args, **kargs): + raise Exception("Can not modify read-only list.") + + + ## Directly (and explicitly) wrap some methods from _data_ + ## Many of these methods can not be intercepted using __getattribute__, so they + ## must be implemented explicitly + for methodName in wrapMethods: + locals()[methodName] = wrapMethod(methodName) + + ## Wrap some methods from _data_ with the results converted to protected objects + for methodName in protectMethods: + locals()[methodName] = protectMethod(methodName) + + ## Disable any methods that could change data in the list + for methodName in disableMethods: + locals()[methodName] = error + + + ## Add a few extra methods. + def copy(self): + raise Exception("It is not safe to copy protected dicts! (instead try deepcopy, but be careful.)") + + def itervalues(self): + for v in self._data_.itervalues(): + yield protect(v) + + def iteritems(self): + for k, v in self._data_.iteritems(): + yield (k, protect(v)) + + def deepcopy(self): + return copy.deepcopy(self._data_) + + def __deepcopy__(self, memo): + return copy.deepcopy(self._data_, memo) + + + +class ProtectedList(collections.Sequence): + """ + A class allowing read-only 'view' of a list or dict. + The object can be treated like a normal list, but will never modify the original list it points to. + Any values accessed from the list will also be read-only. + + Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. + However, doing this causes tuple(obj) to return unprotected results (importantly, this means + unpacking into function arguments will also fail) + """ + def __init__(self, data): + self._data_ = data + #self.__mro__ = (ProtectedList, object) + + ## List of methods to directly wrap from _data_ + wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__gt__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] + + ## List of methods which wrap from _data_ but return protected results + protectMethods = ['__getitem__', '__getslice__', '__mul__', '__reversed__', '__rmul__'] + + ## List of methods to disable + disableMethods = ['__delitem__', '__delslice__', '__iadd__', '__imul__', '__setitem__', '__setslice__', 'append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'] + + + ## Template methods + def wrapMethod(methodName): + return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) + + def protectMethod(methodName): + return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) + + def error(self, *args, **kargs): + raise Exception("Can not modify read-only list.") + + + ## Directly (and explicitly) wrap some methods from _data_ + ## Many of these methods can not be intercepted using __getattribute__, so they + ## must be implemented explicitly + for methodName in wrapMethods: + locals()[methodName] = wrapMethod(methodName) + + ## Wrap some methods from _data_ with the results converted to protected objects + for methodName in protectMethods: + locals()[methodName] = protectMethod(methodName) + + ## Disable any methods that could change data in the list + for methodName in disableMethods: + locals()[methodName] = error + + + ## Add a few extra methods. + def __iter__(self): + for item in self._data_: + yield protect(item) + + + def __add__(self, op): + if isinstance(op, ProtectedList): + return protect(self._data_.__add__(op._data_)) + elif isinstance(op, list): + return protect(self._data_.__add__(op)) + else: + raise TypeError("Argument must be a list.") + + def __radd__(self, op): + if isinstance(op, ProtectedList): + return protect(op._data_.__add__(self._data_)) + elif isinstance(op, list): + return protect(op.__add__(self._data_)) + else: + raise TypeError("Argument must be a list.") + + def deepcopy(self): + return copy.deepcopy(self._data_) + + def __deepcopy__(self, memo): + return copy.deepcopy(self._data_, memo) + + def poop(self): + raise Exception("This is a list. It does not poop.") + + +class ProtectedTuple(collections.Sequence): + """ + A class allowing read-only 'view' of a tuple. + The object can be treated like a normal tuple, but its contents will be returned as protected objects. + + Note: It would be nice if we could inherit from list or tuple so that isinstance checks would work. + However, doing this causes tuple(obj) to return unprotected results (importantly, this means + unpacking into function arguments will also fail) + """ + def __init__(self, data): + self._data_ = data + + ## List of methods to directly wrap from _data_ + wrapMethods = ['__contains__', '__eq__', '__format__', '__ge__', '__getnewargs__', '__gt__', '__hash__', '__le__', '__len__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__repr__', '__str__', 'count', 'index'] + + ## List of methods which wrap from _data_ but return protected results + protectMethods = ['__getitem__', '__getslice__', '__iter__', '__add__', '__mul__', '__reversed__', '__rmul__'] + + + ## Template methods + def wrapMethod(methodName): + return lambda self, *a, **k: getattr(self._data_, methodName)(*a, **k) + + def protectMethod(methodName): + return lambda self, *a, **k: protect(getattr(self._data_, methodName)(*a, **k)) + + + ## Directly (and explicitly) wrap some methods from _data_ + ## Many of these methods can not be intercepted using __getattribute__, so they + ## must be implemented explicitly + for methodName in wrapMethods: + locals()[methodName] = wrapMethod(methodName) + + ## Wrap some methods from _data_ with the results converted to protected objects + for methodName in protectMethods: + locals()[methodName] = protectMethod(methodName) + + + ## Add a few extra methods. + def deepcopy(self): + return copy.deepcopy(self._data_) + + def __deepcopy__(self, memo): + return copy.deepcopy(self._data_, memo) + + + +def protect(obj): + if isinstance(obj, dict): + return ProtectedDict(obj) + elif isinstance(obj, list): + return ProtectedList(obj) + elif isinstance(obj, tuple): + return ProtectedTuple(obj) + else: + return obj + + +if __name__ == '__main__': + d = {'x': 1, 'y': [1,2], 'z': ({'a': 2, 'b': [3,4], 'c': (5,6)}, 1, 2)} + dp = protect(d) + + l = [1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}] + lp = protect(l) + + t = (1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2}) + tp = protect(t) \ No newline at end of file diff --git a/pyqtgraph/pixmaps/__init__.py b/pyqtgraph/pixmaps/__init__.py new file mode 100644 index 00000000..42bd3276 --- /dev/null +++ b/pyqtgraph/pixmaps/__init__.py @@ -0,0 +1,26 @@ +""" +Allows easy loading of pixmaps used in UI elements. +Provides support for frozen environments as well. +""" + +import os, sys, pickle +from ..functions import makeQImage +from ..Qt import QtGui +if sys.version_info[0] == 2: + from . import pixmapData_2 as pixmapData +else: + from . import pixmapData_3 as pixmapData + + +def getPixmap(name): + """ + Return a QPixmap corresponding to the image file with the given name. + (eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png) + """ + key = name+'.png' + data = pixmapData.pixmapData[key] + if isinstance(data, basestring) or isinstance(data, bytes): + pixmapData.pixmapData[key] = pickle.loads(data) + arr = pixmapData.pixmapData[key] + return QtGui.QPixmap(makeQImage(arr, alpha=True)) + diff --git a/pyqtgraph/pixmaps/auto.png b/pyqtgraph/pixmaps/auto.png new file mode 100644 index 00000000..a27ff4f8 Binary files /dev/null and b/pyqtgraph/pixmaps/auto.png differ diff --git a/pyqtgraph/pixmaps/compile.py b/pyqtgraph/pixmaps/compile.py new file mode 100644 index 00000000..ae07d487 --- /dev/null +++ b/pyqtgraph/pixmaps/compile.py @@ -0,0 +1,19 @@ +import numpy as np +from PyQt4 import QtGui +import os, pickle, sys + +path = os.path.abspath(os.path.split(__file__)[0]) +pixmaps = {} +for f in os.listdir(path): + if not f.endswith('.png'): + continue + print(f) + img = QtGui.QImage(os.path.join(path, f)) + ptr = img.bits() + ptr.setsize(img.byteCount()) + arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) + pixmaps[f] = pickle.dumps(arr) +ver = sys.version_info[0] +fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') +fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) + diff --git a/pyqtgraph/pixmaps/ctrl.png b/pyqtgraph/pixmaps/ctrl.png new file mode 100644 index 00000000..c8dc96e4 Binary files /dev/null and b/pyqtgraph/pixmaps/ctrl.png differ diff --git a/pyqtgraph/pixmaps/default.png b/pyqtgraph/pixmaps/default.png new file mode 100644 index 00000000..f1239421 Binary files /dev/null and b/pyqtgraph/pixmaps/default.png differ diff --git a/pyqtgraph/pixmaps/icons.svg b/pyqtgraph/pixmaps/icons.svg new file mode 100644 index 00000000..cfdfeba4 --- /dev/null +++ b/pyqtgraph/pixmaps/icons.svg @@ -0,0 +1,135 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + A + + + + + + + + + + + diff --git a/pyqtgraph/pixmaps/lock.png b/pyqtgraph/pixmaps/lock.png new file mode 100644 index 00000000..3f85dde0 Binary files /dev/null and b/pyqtgraph/pixmaps/lock.png differ diff --git a/pyqtgraph/pixmaps/pixmapData_2.py b/pyqtgraph/pixmaps/pixmapData_2.py new file mode 100644 index 00000000..7813b6a6 --- /dev/null +++ b/pyqtgraph/pixmaps/pixmapData_2.py @@ -0,0 +1 @@ +import numpy as np; pixmapData={'lock.png': "cnumpy.core.multiarray\n_reconstruct\np0\n(cnumpy\nndarray\np1\n(I0\ntp2\nS'b'\np3\ntp4\nRp5\n(I1\n(I32\nI32\nI4\ntp6\ncnumpy\ndtype\np7\n(S'u1'\np8\nI0\nI1\ntp9\nRp10\n(I3\nS'|'\np11\nNNNI-1\nI-1\nI0\ntp12\nbI00\nS'\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xad\\xad\\xad\\x19\\xa8\\xa8\\xa8\\x8d\\xa9\\xa9\\xa9\\xc1\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xaa\\xaa\\xaa\\xc2\\xa9\\xa9\\xa9\\x8e\\xad\\xad\\xad\\x19\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xa8\\xa8\\xa8X\\xa9\\xa9\\xa9\\xed\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xed\\xa8\\xa8\\xa8X\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x0c\\x0c\\x0c\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xd2\\xd2\\xd2\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe1\\xe1\\xe1\\xff{{{\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x0e\\x0e\\x0e\\xff***\\xff+++\\xff+++\\xff\\xaf\\xaf\\xaf\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe2\\xe2\\xe2\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x1e\\x1e\\x1e\\xff\\x93\\x93\\x93\\xff\\xc6\\xc6\\xc6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xffaaa\\xff\\xdc\\xdc\\xdc\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\\\\\\\\\\\\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe2\\xe2\\xe2\\xff\\xbb\\xbb\\xbb\\xff\\x9f\\x9f\\x9f\\xff\\x9f\\x9f\\x9f\\xff\\x9f\\x9f\\x9f\\xff\\xd7\\xd7\\xd7\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x1c\\x1c\\x1c\\xff\\xda\\xda\\xda\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x91\\x91\\x91\\xff\\x0f\\x0f\\x0f\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x87\\x87\\x87\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x98\\x98\\x98\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xba\\xba\\xba\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x19\\x19\\x19\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x08\\x08\\x08\\xff\\xe2\\xe2\\xe2\\xff\\xe6\\xe6\\xe6\\xff\\xcc\\xcc\\xcc\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x08\\x08\\x08\\xff\\xe2\\xe2\\xe2\\xff\\xe6\\xe6\\xe6\\xff\\xcc\\xcc\\xcc\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xba\\xba\\xba\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x19\\x19\\x19\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x85\\x85\\x85\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x98\\x98\\x98\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x19\\x19\\x19\\xff\\xd9\\xd9\\xd9\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x91\\x91\\x91\\xff\\x0f\\x0f\\x0f\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb4\\xb4\\xb4\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xffZZZ\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe2\\xe2\\xe2\\xff\\xbc\\xbc\\xbc\\xff\\x9f\\x9f\\x9f\\xff\\x9f\\x9f\\x9f\\xff\\x9f\\x9f\\x9f\\xff\\xd7\\xd7\\xd7\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xffaaa\\xff\\xdc\\xdc\\xdc\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x1e\\x1e\\x1e\\xff\\x93\\x93\\x93\\xff\\xc6\\xc6\\xc6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\x1d\\x1d\\x1d\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x0e\\x0e\\x0e\\xff***\\xff+++\\xff+++\\xff\\xaf\\xaf\\xaf\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe2\\xe2\\xe2\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xd2\\xd2\\xd2\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe6\\xe6\\xe6\\xff\\xe1\\xe1\\xe1\\xff{{{\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x16\\x16\\x16\\xff\\x0c\\x0c\\x0c\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00'\np13\ntp14\nb.", 'default.png': 'cnumpy.core.multiarray\n_reconstruct\np0\n(cnumpy\nndarray\np1\n(I0\ntp2\nS\'b\'\np3\ntp4\nRp5\n(I1\n(I16\nI16\nI4\ntp6\ncnumpy\ndtype\np7\n(S\'u1\'\np8\nI0\nI1\ntp9\nRp10\n(I3\nS\'|\'\np11\nNNNI-1\nI-1\nI0\ntp12\nbI00\nS\'\\x00\\x7f\\xa6\\x1b\\x0c\\x8a\\xad\\xdc\\r\\x91\\xb0\\xf3\\r\\x91\\xb0\\xf3\\r\\x91\\xb0\\xf4\\r\\x91\\xb1\\xf4\\r\\x90\\xb0\\xf4\\x05\\x85\\xa9\\xef\\x00\\x7f\\xa6<\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6!\\x1d\\x9c\\xb9\\xf5g\\xd9\\xf1\\xffi\\xd9\\xf3\\xffd\\xd1\\xee\\xff]\\xcb\\xeb\\xff@\\xbb\\xe3\\xff\\x16\\x9c\\xc2\\xf8\\x00\\x7f\\xa6\\xb4\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6U\\\'\\xac\\xc5\\xf9i\\xd9\\xf3\\xffc\\xd3\\xef\\xff\\\\\\xcf\\xeb\\xffP\\xc8\\xe6\\xff\\x17\\x9f\\xc4\\xfd\\x00\\x7f\\xa6\\xfc\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x02\\x83\\xa8lH\\xc5\\xdd\\xfah\\xdc\\xf3\\xffc\\xd4\\xef\\xffV\\xce\\xe9\\xffN\\xcf\\xe7\\xff&\\xaa\\xca\\xfd\\x00\\x7f\\xa6\\xff\\x03\\x81\\xc7\\x01\\x04\\x8d\\xda\\x01\\t\\x94\\xd9\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6"$\\xa9\\xc4\\xf7g\\xdf\\xf5\\xfff\\xdb\\xf3\\xffU\\xcd\\xeb\\xff\\x16\\xb3\\xda\\xff.\\xc9\\xe1\\xff(\\xb2\\xd0\\xfe\\x01\\x7f\\xa6\\xff\\x04\\x84\\xc9\\x05\\t\\x94\\xd9\\x06\\x10\\x9c\\xd7\\x01\\x16\\xa2\\xd6\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x02\\x83\\xa9\\x81T\\xd3\\xeb\\xffg\\xe5\\xf7\\xffe\\xda\\xf3\\xff!\\xaa\\xde\\xff\\x11\\x9d\\xc3\\xfe\\x11\\xba\\xd7\\xff \\xb9\\xd5\\xfe\\x00\\x7f\\xa6\\xff\\x16u\\x8d\\x03\\x14\\x84\\xae\\x05\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x10\\x92\\xb4\\xc0d\\xde\\xf3\\xffg\\xe5\\xf7\\xff_\\xcc\\xef\\xff\\x0e\\x9c\\xd5\\xff\\rx\\x95\\xf6\\x0e\\x89\\xab\\xf4\\x18\\xb2\\xd1\\xfc\\x00\\x7f\\xa6\\xff\\xff\\xff\\xff\\x00\\x1a~\\x91\\x01\\x1d\\xa5\\xce\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x005\\xa9\\xc3\\xefq\\xec\\xf9\\xffg\\xe5\\xf7\\xff>\\xb7\\xe8\\xff\\x14\\x96\\xc8\\xfe\\x02}\\xa3\\xb1\\x00\\x7f\\xa6Q\\x03\\x82\\xa9\\xe8\\x00\\x7f\\xa6\\xe9\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6\\x11\\x1c\\x98\\xb8\\x04%\\xb5\\xd3\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00D\\xad\\xc8\\xf3r\\xec\\xf9\\xffg\\xe5\\xf7\\xff:\\xb7\\xe8\\xff\\x19\\x90\\xc5\\xfe\\x03{\\xa0\\xa6\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6*\\x00\\x7f\\xa6*\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6\\x98\\x0f\\x8f\\xb1\\x13&\\xb5\\xd3\\x04.\\xc0\\xd1\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x19\\x93\\xb7\\xc6i\\xdf\\xf4\\xffg\\xe5\\xf7\\xffT\\xc8\\xee\\xff\\x06\\x88\\xcd\\xff\\x08g\\x85\\xf7\\x00\\x7f\\xa6\\x15\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6\\x1b\\x01\\x80\\xa7\\xeb\\x1d\\xa3\\xca\\x16#\\xb2\\xd4\\n*\\xbb\\xd2\\x04.\\xbc\\xd7\\x01\\xff\\xff\\xff\\x00\\x01\\x81\\xa7\\x88Y\\xd1\\xee\\xffg\\xe5\\xf7\\xfff\\xd9\\xf3\\xff\\\'\\xa2\\xe2\\xff\\x05e\\x99\\xf9\\x06~\\xa5\\xf3\\x01\\x81\\xa8\\x9c\\x01\\x80\\xa8\\x9f\\x04\\x85\\xad\\xef\\x08\\x8f\\xb9\\x92\\x17\\xa4\\xd6*\\x1e\\xac\\xd5\\x1a$\\xb3\\xd3\\x0c\\x19\\xa7\\xd5\\x02\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6+!\\xa3\\xc8\\xf5i\\xe0\\xf5\\xffe\\xd9\\xf3\\xff\\\\\\xca\\xee\\xff\\x1f\\x9c\\xe0\\xfa\\x03\\x84\\xca\\xd6\\x07\\x8b\\xc5\\xca\\x06\\x88\\xc1\\xb8\\x08\\x8e\\xd0l\\x0b\\x96\\xd8I\\x11\\x9e\\xd74\\x17\\xa5\\xd6 \\xab\\xd7\\x0b\\x17\\xa2\\xdc\\x01\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x01\\x80\\xa8~?\\xb9\\xe0\\xf9h\\xda\\xf3\\xff_\\xcc\\xef\\xffV\\xc1\\xec\\xfd3\\xa7\\xe3\\xe3\\x1a\\x96\\xde\\xae\\x04\\x8b\\xdb\\x89\\x00\\x89\\xdao\\x05\\x8f\\xd9T\\x0b\\x96\\xd8<\\x11\\x9b\\xd7\\x1d\\x18\\x95\\xc9\\x0c\\x00\\x80\\xd5\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6\\x04\\x03\\x83\\xaa\\xcd5\\xa2\\xc9\\xf9[\\xc6\\xea\\xffU\\xc1\\xec\\xffH\\xb4\\xe8\\xf39\\xa8\\xe4\\xc5\\x0b\\x8f\\xdc\\x9f\\x00\\x89\\xda{\\x00\\x89\\xda_\\x07\\x87\\xc4I\\x05|\\xa5s\\x05m\\xa3\\x02\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa6\\x06\\x01\\x7f\\xa6\\x89\\x12x\\x9e\\xf63\\x88\\xae\\xfe6\\x93\\xc3\\xfe4\\x9d\\xd6\\xdf\\x08\\x82\\xc7\\xb8\\x03k\\xa2\\xab\\x04k\\x97\\xa8\\x02w\\x9e\\xeb\\x00\\x7f\\xa6j\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\x00\\x7f\\xa67\\x00~\\xa5\\x95\\x03v\\x9c\\xd4\\x03h\\x8c\\xfa\\x02i\\x8e\\xf9\\x01x\\x9f\\xcc\\x00\\x7f\\xa6\\x92\\x00\\x7f\\xa63\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\'\np13\ntp14\nb.', 'ctrl.png': "cnumpy.core.multiarray\n_reconstruct\np0\n(cnumpy\nndarray\np1\n(I0\ntp2\nS'b'\np3\ntp4\nRp5\n(I1\n(I32\nI32\nI4\ntp6\ncnumpy\ndtype\np7\n(S'u1'\np8\nI0\nI1\ntp9\nRp10\n(I3\nS'|'\np11\nNNNI-1\nI-1\nI0\ntp12\nbI00\nS'\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xad\\xad\\xad\\x19\\xa8\\xa8\\xa8\\x8d\\xa9\\xa9\\xa9\\xc1\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xaa\\xaa\\xaa\\xc2\\xa9\\xa9\\xa9\\x8e\\xad\\xad\\xad\\x19\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xa8\\xa8\\xa8X\\xa9\\xa9\\xa9\\xed\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xed\\xa8\\xa8\\xa8X\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff555\\xffPPP\\xff\\x13\\x13\\x13\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x01\\x01\\x01\\xff\\xb2\\xb2\\xb2\\xff\\xe3\\xe3\\xe3\\xff\\xd9\\xd9\\xd9\\xff]]]\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x13\\x13\\x13\\xff\\xbb\\xbb\\xbb\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xffFFF\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x13\\x13\\x13\\xff\\xbb\\xbb\\xbb\\xff\\xe3\\xe3\\xe3\\xff\\xc4\\xc4\\xc4\\xff\\x06\\x06\\x06\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff```\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff:::\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff666\\xff\\xaf\\xaf\\xaf\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9b\\x9b\\x9b\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff@@@\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xffSSS\\xff\\xe3\\xe3\\xe3\\xff\\xb7\\xb7\\xb7\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x04\\x04\\x04\\xff\\xd5\\xd5\\xd5\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xffXXX\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x17\\x17\\x17\\xff\\xdb\\xdb\\xdb\\xff\\xe3\\xe3\\xe3\\xff\\xb7\\xb7\\xb7\\xff[[[\\xff\\x97\\x97\\x97\\xff\\xd4\\xd4\\xd4\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff```\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xffHHH\\xff\\xc6\\xc6\\xc6\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x07\\x07\\x07\\xff;;;\\xffAAA\\xff\\\\\\\\\\\\\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xda\\xda\\xda\\xff;;;\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xff\\xe3\\xe3\\xe3\\xff\\xc7\\xc7\\xc7\\xffZZZ\\xff~~~\\xff\\xd9\\xd9\\xd9\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xff\\xe3\\xe3\\xe3\\xffXXX\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xb0\\xb0\\xb0\\xfffff\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xdd\\xdd\\xdd\\xffyyy\\xff\\x00\\x00\\x00\\xff\\x06\\x06\\x06\\xff\\xcd\\xcd\\xcd\\xfffff\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff@@@\\xff\\xda\\xda\\xda\\xff\\xaf\\xaf\\xaf\\xff\\xcd\\xcd\\xcd\\xff\\xd7\\xd7\\xd7\\xff\\x10\\x10\\x10\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x12\\x12\\x12\\xffiii\\xffccc\\xff\\x0e\\x0e\\x0e\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00'\np13\ntp14\nb.", 'auto.png': "cnumpy.core.multiarray\n_reconstruct\np0\n(cnumpy\nndarray\np1\n(I0\ntp2\nS'b'\np3\ntp4\nRp5\n(I1\n(I32\nI32\nI4\ntp6\ncnumpy\ndtype\np7\n(S'u1'\np8\nI0\nI1\ntp9\nRp10\n(I3\nS'|'\np11\nNNNI-1\nI-1\nI0\ntp12\nbI00\nS'\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xad\\xad\\xad\\x19\\xa8\\xa8\\xa8\\x8d\\xa9\\xa9\\xa9\\xc1\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xaa\\xaa\\xaa\\xc2\\xa9\\xa9\\xa9\\x8e\\xad\\xad\\xad\\x19\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xa8\\xa8\\xa8X\\xa9\\xa9\\xa9\\xed\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xed\\xa8\\xa8\\xa8X\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x19\\x19\\x19\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x04\\x04\\x04\\xffHHH\\xff\\xa4\\xa4\\xa4\\xff\\xe5\\xe5\\xe5\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff \\xffyyy\\xff\\xd1\\xd1\\xd1\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x06\\x06\\x06\\xffPPP\\xff\\xab\\xab\\xab\\xff\\xe6\\xe6\\xe6\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff&&&\\xff\\x82\\x82\\x82\\xff\\xd6\\xd6\\xd6\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\t\\t\\t\\xffWWW\\xff\\xb2\\xb2\\xb2\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe5\\xe5\\xe5\\xff\\xa8\\xa8\\xa8\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff---\\xff\\x89\\x89\\x89\\xff\\xda\\xda\\xda\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xc1\\xc1\\xc1\\xfflll\\xff\\x18\\x18\\x18\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\r\\r\\r\\xff^^^\\xff\\xba\\xba\\xba\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xda\\xda\\xda\\xff...\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff555\\xff\\x90\\x90\\x90\\xff\\xde\\xde\\xde\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe2\\xe2\\xe2\\xff\\xe3\\xe3\\xe3\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff;;;\\xff\\xc1\\xc1\\xc1\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xb7\\xb7\\xb7\\xffbbb\\xff\\x12\\x12\\x12\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xffmmm\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xcd\\xcd\\xcd\\xffyyy\\xff$$$\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xffmmm\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe3\\xe3\\xe3\\xff\\x91\\x91\\x91\\xff<<<\\xff\\x01\\x01\\x01\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xffmmm\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xc3\\xc3\\xc3\\xfflll\\xff\\x18\\x18\\x18\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xffmmm\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe4\\xe4\\xe4\\xff\\xa6\\xa6\\xa6\\xffOOO\\xff\\x07\\x07\\x07\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff555\\xff\\xb4\\xb4\\xb4\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd9\\xd9\\xd9\\xff\\x8a\\x8a\\x8a\\xff333\\xff\\xcb\\xcb\\xcb\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff+++\\xff\\x88\\x88\\x88\\xff\\xda\\xda\\xda\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xd2\\xd2\\xd2\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\n\\n\\n\\xff[[[\\xff\\xb8\\xb8\\xb8\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xdc\\xdc\\xdc\\xffAAA\\xff\\x02\\x02\\x02\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff...\\xff\\x8c\\x8c\\x8c\\xff\\xdc\\xdc\\xdc\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xcc\\xcc\\xcc\\xffsss\\xff\\x1a\\x1a\\x1a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x0c\\x0c\\x0c\\xff___\\xff\\xbc\\xbc\\xbc\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe5\\xe5\\xe5\\xff\\xa5\\xa5\\xa5\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff222\\xff\\x8f\\x8f\\x8f\\xff\\xde\\xde\\xde\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x9a\\x9a\\x9a\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x0e\\x0e\\x0e\\xffccc\\xff\\xc0\\xc0\\xc0\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x9a\\x9a\\x9a\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff555\\xff\\x94\\x94\\x94\\xff\\xe0\\xe0\\xe0\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff)))\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x10\\x10\\x10\\xfffff\\xff\\xc4\\xc4\\xc4\\xff\\xe7\\xe7\\xe7\\xff\\x00\\x00\\x00\\xff)))\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff\\x03\\x03\\x03\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff:::\\xff\\x03\\x03\\x03\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\x88\\x88\\x88\\xff)))\\xff\\x05\\x05\\x05\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x00\\x00\\x00\\xff\\x05\\x05\\x05\\xff)))\\xff\\x88\\x88\\x88\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa6\\xa6\\xa6\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\x9a\\x9a\\x9a\\xff\\xa6\\xa6\\xa6\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaaW\\xa9\\xa9\\xa9\\xeb\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xeb\\xaa\\xaa\\xaaW\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xaa\\xaa\\xaa\\x15\\xa9\\xa9\\xa9\\x88\\xa9\\xa9\\xa9\\xbd\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xff\\xa9\\xa9\\xa9\\xf1\\xa9\\xa9\\xa9\\xbe\\xa9\\xa9\\xa9\\x88\\xaa\\xaa\\xaa\\x15\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00\\xff\\xff\\xff\\x00'\np13\ntp14\nb."} \ No newline at end of file diff --git a/pyqtgraph/pixmaps/pixmapData_3.py b/pyqtgraph/pixmaps/pixmapData_3.py new file mode 100644 index 00000000..bb512029 --- /dev/null +++ b/pyqtgraph/pixmaps/pixmapData_3.py @@ -0,0 +1 @@ +import numpy as np; pixmapData={'lock.png': b'\x80\x03cnumpy.core.multiarray\n_reconstruct\nq\x00cnumpy\nndarray\nq\x01K\x00\x85q\x02C\x01bq\x03\x87q\x04Rq\x05(K\x01K K K\x04\x87q\x06cnumpy\ndtype\nq\x07X\x02\x00\x00\x00u1q\x08K\x00K\x01\x87q\tRq\n(K\x03X\x01\x00\x00\x00|q\x0bNNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00tq\x0cb\x89B\x00\x10\x00\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xad\xad\xad\x19\xa8\xa8\xa8\x8d\xa9\xa9\xa9\xc1\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xaa\xaa\xaa\xc2\xa9\xa9\xa9\x8e\xad\xad\xad\x19\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xa8\xa8\xa8X\xa9\xa9\xa9\xed\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xed\xa8\xa8\xa8X\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x0c\x0c\x0c\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xd2\xd2\xd2\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe1\xe1\xe1\xff{{{\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x0e\x0e\x0e\xff***\xff+++\xff+++\xff\xaf\xaf\xaf\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe2\xe2\xe2\xff\x10\x10\x10\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x1e\x1e\x1e\xff\x93\x93\x93\xff\xc6\xc6\xc6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xffaaa\xff\xdc\xdc\xdc\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\\\\\\\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe2\xe2\xe2\xff\xbb\xbb\xbb\xff\x9f\x9f\x9f\xff\x9f\x9f\x9f\xff\x9f\x9f\x9f\xff\xd7\xd7\xd7\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x1c\x1c\x1c\xff\xda\xda\xda\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x91\x91\x91\xff\x0f\x0f\x0f\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x87\x87\x87\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x98\x98\x98\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\xba\xba\xba\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x19\x19\x19\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x08\x08\x08\xff\xe2\xe2\xe2\xff\xe6\xe6\xe6\xff\xcc\xcc\xcc\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x08\x08\x08\xff\xe2\xe2\xe2\xff\xe6\xe6\xe6\xff\xcc\xcc\xcc\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\xba\xba\xba\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x19\x19\x19\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x85\x85\x85\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x98\x98\x98\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x19\x19\x19\xff\xd9\xd9\xd9\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x91\x91\x91\xff\x0f\x0f\x0f\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb4\xb4\xb4\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xffZZZ\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe2\xe2\xe2\xff\xbc\xbc\xbc\xff\x9f\x9f\x9f\xff\x9f\x9f\x9f\xff\x9f\x9f\x9f\xff\xd7\xd7\xd7\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xffaaa\xff\xdc\xdc\xdc\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x1e\x1e\x1e\xff\x93\x93\x93\xff\xc6\xc6\xc6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\x1d\x1d\x1d\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x0e\x0e\x0e\xff***\xff+++\xff+++\xff\xaf\xaf\xaf\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe2\xe2\xe2\xff\x10\x10\x10\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xd2\xd2\xd2\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe6\xe6\xe6\xff\xe1\xe1\xe1\xff{{{\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x16\x16\x16\xff\x0c\x0c\x0c\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbd\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbe\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00q\rtq\x0eb.', 'default.png': b'\x80\x03cnumpy.core.multiarray\n_reconstruct\nq\x00cnumpy\nndarray\nq\x01K\x00\x85q\x02C\x01bq\x03\x87q\x04Rq\x05(K\x01K\x10K\x10K\x04\x87q\x06cnumpy\ndtype\nq\x07X\x02\x00\x00\x00u1q\x08K\x00K\x01\x87q\tRq\n(K\x03X\x01\x00\x00\x00|q\x0bNNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00tq\x0cb\x89B\x00\x04\x00\x00\x00\x7f\xa6\x1b\x0c\x8a\xad\xdc\r\x91\xb0\xf3\r\x91\xb0\xf3\r\x91\xb0\xf4\r\x91\xb1\xf4\r\x90\xb0\xf4\x05\x85\xa9\xef\x00\x7f\xa6<\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6!\x1d\x9c\xb9\xf5g\xd9\xf1\xffi\xd9\xf3\xffd\xd1\xee\xff]\xcb\xeb\xff@\xbb\xe3\xff\x16\x9c\xc2\xf8\x00\x7f\xa6\xb4\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6U\'\xac\xc5\xf9i\xd9\xf3\xffc\xd3\xef\xff\\\xcf\xeb\xffP\xc8\xe6\xff\x17\x9f\xc4\xfd\x00\x7f\xa6\xfc\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x02\x83\xa8lH\xc5\xdd\xfah\xdc\xf3\xffc\xd4\xef\xffV\xce\xe9\xffN\xcf\xe7\xff&\xaa\xca\xfd\x00\x7f\xa6\xff\x03\x81\xc7\x01\x04\x8d\xda\x01\t\x94\xd9\x01\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6"$\xa9\xc4\xf7g\xdf\xf5\xfff\xdb\xf3\xffU\xcd\xeb\xff\x16\xb3\xda\xff.\xc9\xe1\xff(\xb2\xd0\xfe\x01\x7f\xa6\xff\x04\x84\xc9\x05\t\x94\xd9\x06\x10\x9c\xd7\x01\x16\xa2\xd6\x01\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x02\x83\xa9\x81T\xd3\xeb\xffg\xe5\xf7\xffe\xda\xf3\xff!\xaa\xde\xff\x11\x9d\xc3\xfe\x11\xba\xd7\xff \xb9\xd5\xfe\x00\x7f\xa6\xff\x16u\x8d\x03\x14\x84\xae\x05\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x10\x92\xb4\xc0d\xde\xf3\xffg\xe5\xf7\xff_\xcc\xef\xff\x0e\x9c\xd5\xff\rx\x95\xf6\x0e\x89\xab\xf4\x18\xb2\xd1\xfc\x00\x7f\xa6\xff\xff\xff\xff\x00\x1a~\x91\x01\x1d\xa5\xce\x01\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x005\xa9\xc3\xefq\xec\xf9\xffg\xe5\xf7\xff>\xb7\xe8\xff\x14\x96\xc8\xfe\x02}\xa3\xb1\x00\x7f\xa6Q\x03\x82\xa9\xe8\x00\x7f\xa6\xe9\xff\xff\xff\x00\x00\x7f\xa6\x11\x1c\x98\xb8\x04%\xb5\xd3\x01\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00D\xad\xc8\xf3r\xec\xf9\xffg\xe5\xf7\xff:\xb7\xe8\xff\x19\x90\xc5\xfe\x03{\xa0\xa6\xff\xff\xff\x00\x00\x7f\xa6*\x00\x7f\xa6*\xff\xff\xff\x00\x00\x7f\xa6\x98\x0f\x8f\xb1\x13&\xb5\xd3\x04.\xc0\xd1\x01\xff\xff\xff\x00\xff\xff\xff\x00\x19\x93\xb7\xc6i\xdf\xf4\xffg\xe5\xf7\xffT\xc8\xee\xff\x06\x88\xcd\xff\x08g\x85\xf7\x00\x7f\xa6\x15\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6\x1b\x01\x80\xa7\xeb\x1d\xa3\xca\x16#\xb2\xd4\n*\xbb\xd2\x04.\xbc\xd7\x01\xff\xff\xff\x00\x01\x81\xa7\x88Y\xd1\xee\xffg\xe5\xf7\xfff\xd9\xf3\xff\'\xa2\xe2\xff\x05e\x99\xf9\x06~\xa5\xf3\x01\x81\xa8\x9c\x01\x80\xa8\x9f\x04\x85\xad\xef\x08\x8f\xb9\x92\x17\xa4\xd6*\x1e\xac\xd5\x1a$\xb3\xd3\x0c\x19\xa7\xd5\x02\xff\xff\xff\x00\x00\x7f\xa6+!\xa3\xc8\xf5i\xe0\xf5\xffe\xd9\xf3\xff\\\xca\xee\xff\x1f\x9c\xe0\xfa\x03\x84\xca\xd6\x07\x8b\xc5\xca\x06\x88\xc1\xb8\x08\x8e\xd0l\x0b\x96\xd8I\x11\x9e\xd74\x17\xa5\xd6 \xab\xd7\x0b\x17\xa2\xdc\x01\xff\xff\xff\x00\xff\xff\xff\x00\x01\x80\xa8~?\xb9\xe0\xf9h\xda\xf3\xff_\xcc\xef\xffV\xc1\xec\xfd3\xa7\xe3\xe3\x1a\x96\xde\xae\x04\x8b\xdb\x89\x00\x89\xdao\x05\x8f\xd9T\x0b\x96\xd8<\x11\x9b\xd7\x1d\x18\x95\xc9\x0c\x00\x80\xd5\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6\x04\x03\x83\xaa\xcd5\xa2\xc9\xf9[\xc6\xea\xffU\xc1\xec\xffH\xb4\xe8\xf39\xa8\xe4\xc5\x0b\x8f\xdc\x9f\x00\x89\xda{\x00\x89\xda_\x07\x87\xc4I\x05|\xa5s\x05m\xa3\x02\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa6\x06\x01\x7f\xa6\x89\x12x\x9e\xf63\x88\xae\xfe6\x93\xc3\xfe4\x9d\xd6\xdf\x08\x82\xc7\xb8\x03k\xa2\xab\x04k\x97\xa8\x02w\x9e\xeb\x00\x7f\xa6j\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\x00\x7f\xa67\x00~\xa5\x95\x03v\x9c\xd4\x03h\x8c\xfa\x02i\x8e\xf9\x01x\x9f\xcc\x00\x7f\xa6\x92\x00\x7f\xa63\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00q\rtq\x0eb.', 'ctrl.png': b'\x80\x03cnumpy.core.multiarray\n_reconstruct\nq\x00cnumpy\nndarray\nq\x01K\x00\x85q\x02C\x01bq\x03\x87q\x04Rq\x05(K\x01K K K\x04\x87q\x06cnumpy\ndtype\nq\x07X\x02\x00\x00\x00u1q\x08K\x00K\x01\x87q\tRq\n(K\x03X\x01\x00\x00\x00|q\x0bNNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00tq\x0cb\x89B\x00\x10\x00\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xad\xad\xad\x19\xa8\xa8\xa8\x8d\xa9\xa9\xa9\xc1\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xaa\xaa\xaa\xc2\xa9\xa9\xa9\x8e\xad\xad\xad\x19\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xa8\xa8\xa8X\xa9\xa9\xa9\xed\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xed\xa8\xa8\xa8X\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff555\xffPPP\xff\x13\x13\x13\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x01\x01\x01\xff\xb2\xb2\xb2\xff\xe3\xe3\xe3\xff\xd9\xd9\xd9\xff]]]\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x13\x13\x13\xff\xbb\xbb\xbb\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xffFFF\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x13\x13\x13\xff\xbb\xbb\xbb\xff\xe3\xe3\xe3\xff\xc4\xc4\xc4\xff\x06\x06\x06\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff```\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff:::\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff666\xff\xaf\xaf\xaf\xff\x10\x10\x10\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9b\x9b\x9b\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff@@@\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xffSSS\xff\xe3\xe3\xe3\xff\xb7\xb7\xb7\xff\x10\x10\x10\xff\x00\x00\x00\xff\x00\x00\x00\xff\x04\x04\x04\xff\xd5\xd5\xd5\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xffXXX\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x17\x17\x17\xff\xdb\xdb\xdb\xff\xe3\xe3\xe3\xff\xb7\xb7\xb7\xff[[[\xff\x97\x97\x97\xff\xd4\xd4\xd4\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff```\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xffHHH\xff\xc6\xc6\xc6\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x07\x07\x07\xff;;;\xffAAA\xff\\\\\\\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xda\xda\xda\xff;;;\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xff\xe3\xe3\xe3\xff\xc7\xc7\xc7\xffZZZ\xff~~~\xff\xd9\xd9\xd9\xff\x10\x10\x10\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xff\xe3\xe3\xe3\xffXXX\xff\x00\x00\x00\xff\x00\x00\x00\xff\xb0\xb0\xb0\xfffff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xdd\xdd\xdd\xffyyy\xff\x00\x00\x00\xff\x06\x06\x06\xff\xcd\xcd\xcd\xfffff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff@@@\xff\xda\xda\xda\xff\xaf\xaf\xaf\xff\xcd\xcd\xcd\xff\xd7\xd7\xd7\xff\x10\x10\x10\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x12\x12\x12\xffiii\xffccc\xff\x0e\x0e\x0e\xff\x00\x00\x00\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbd\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbe\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00q\rtq\x0eb.', 'auto.png': b'\x80\x03cnumpy.core.multiarray\n_reconstruct\nq\x00cnumpy\nndarray\nq\x01K\x00\x85q\x02C\x01bq\x03\x87q\x04Rq\x05(K\x01K K K\x04\x87q\x06cnumpy\ndtype\nq\x07X\x02\x00\x00\x00u1q\x08K\x00K\x01\x87q\tRq\n(K\x03X\x01\x00\x00\x00|q\x0bNNNJ\xff\xff\xff\xffJ\xff\xff\xff\xffK\x00tq\x0cb\x89B\x00\x10\x00\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xad\xad\xad\x19\xa8\xa8\xa8\x8d\xa9\xa9\xa9\xc1\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xaa\xaa\xaa\xc2\xa9\xa9\xa9\x8e\xad\xad\xad\x19\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xa8\xa8\xa8X\xa9\xa9\xa9\xed\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xed\xa8\xa8\xa8X\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x19\x19\x19\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x04\x04\x04\xffHHH\xff\xa4\xa4\xa4\xff\xe5\xe5\xe5\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbe\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff \xffyyy\xff\xd1\xd1\xd1\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x06\x06\x06\xffPPP\xff\xab\xab\xab\xff\xe6\xe6\xe6\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff&&&\xff\x82\x82\x82\xff\xd6\xd6\xd6\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\t\t\t\xffWWW\xff\xb2\xb2\xb2\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe5\xe5\xe5\xff\xa8\xa8\xa8\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff---\xff\x89\x89\x89\xff\xda\xda\xda\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xc1\xc1\xc1\xfflll\xff\x18\x18\x18\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\r\r\r\xff^^^\xff\xba\xba\xba\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xda\xda\xda\xff...\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff555\xff\x90\x90\x90\xff\xde\xde\xde\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe2\xe2\xe2\xff\xe3\xe3\xe3\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff;;;\xff\xc1\xc1\xc1\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xb7\xb7\xb7\xffbbb\xff\x12\x12\x12\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xffmmm\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xcd\xcd\xcd\xffyyy\xff$$$\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xffmmm\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe3\xe3\xe3\xff\x91\x91\x91\xff<<<\xff\x01\x01\x01\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xffmmm\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xc3\xc3\xc3\xfflll\xff\x18\x18\x18\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xffmmm\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe4\xe4\xe4\xff\xa6\xa6\xa6\xffOOO\xff\x07\x07\x07\xff\x00\x00\x00\xff\x00\x00\x00\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff555\xff\xb4\xb4\xb4\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd9\xd9\xd9\xff\x8a\x8a\x8a\xff333\xff\xcb\xcb\xcb\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff+++\xff\x88\x88\x88\xff\xda\xda\xda\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xd2\xd2\xd2\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\n\n\n\xff[[[\xff\xb8\xb8\xb8\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xdc\xdc\xdc\xffAAA\xff\x02\x02\x02\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff...\xff\x8c\x8c\x8c\xff\xdc\xdc\xdc\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xcc\xcc\xcc\xffsss\xff\x1a\x1a\x1a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x0c\x0c\x0c\xff___\xff\xbc\xbc\xbc\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe5\xe5\xe5\xff\xa5\xa5\xa5\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff222\xff\x8f\x8f\x8f\xff\xde\xde\xde\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x9a\x9a\x9a\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x0e\x0e\x0e\xffccc\xff\xc0\xc0\xc0\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x00\x00\x00\xff\x9a\x9a\x9a\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff555\xff\x94\x94\x94\xff\xe0\xe0\xe0\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff\x05\x05\x05\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff)))\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x10\x10\x10\xfffff\xff\xc4\xc4\xc4\xff\xe7\xe7\xe7\xff\x00\x00\x00\xff)))\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xbd\xa9\xa9\xa9\x88\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff\x03\x03\x03\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff:::\xff\x03\x03\x03\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\x88\x88\x88\xff)))\xff\x05\x05\x05\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\x05\x05\x05\xff)))\xff\x88\x88\x88\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaa\x15\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa6\xa6\xa6\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\x9a\x9a\x9a\xff\xa6\xa6\xa6\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaaW\xa9\xa9\xa9\xeb\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xeb\xaa\xaa\xaaW\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00\xaa\xaa\xaa\x15\xa9\xa9\xa9\x88\xa9\xa9\xa9\xbd\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xff\xa9\xa9\xa9\xf1\xa9\xa9\xa9\xbe\xa9\xa9\xa9\x88\xaa\xaa\xaa\x15\xff\xff\xff\x00\xff\xff\xff\x00\xff\xff\xff\x00q\rtq\x0eb.'} \ No newline at end of file diff --git a/ptime.py b/pyqtgraph/ptime.py similarity index 100% rename from ptime.py rename to pyqtgraph/ptime.py diff --git a/pyqtgraph/python2_3.py b/pyqtgraph/python2_3.py new file mode 100644 index 00000000..2182d3a1 --- /dev/null +++ b/pyqtgraph/python2_3.py @@ -0,0 +1,60 @@ +""" +Helper functions which smooth out the differences between python 2 and 3. +""" +import sys + +def asUnicode(x): + if sys.version_info[0] == 2: + if isinstance(x, unicode): + return x + elif isinstance(x, str): + return x.decode('UTF-8') + else: + return unicode(x) + else: + return str(x) + +def cmpToKey(mycmp): + 'Convert a cmp= function into a key= function' + class K(object): + def __init__(self, obj, *args): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + return K + +def sortList(l, cmpFunc): + if sys.version_info[0] == 2: + l.sort(cmpFunc) + else: + l.sort(key=cmpToKey(cmpFunc)) + +if sys.version_info[0] == 3: + import builtins + builtins.basestring = str + #builtins.asUnicode = asUnicode + #builtins.sortList = sortList + basestring = str + def cmp(a,b): + if a>b: + return 1 + elif b > a: + return -1 + else: + return 0 + builtins.cmp = cmp + builtins.xrange = range +#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe + #import __builtin__ + #__builtin__.asUnicode = asUnicode + #__builtin__.sortList = sortList diff --git a/pyqtgraph/rebuildUi.py b/pyqtgraph/rebuildUi.py new file mode 100644 index 00000000..92d5991a --- /dev/null +++ b/pyqtgraph/rebuildUi.py @@ -0,0 +1,23 @@ +import os, sys +## Search the package tree for all .ui files, compile each to +## a .py for pyqt and pyside + +pyqtuic = 'pyuic4' +pysideuic = 'pyside-uic' + +for path, sd, files in os.walk('.'): + for f in files: + base, ext = os.path.splitext(f) + if ext != '.ui': + continue + ui = os.path.join(path, f) + + py = os.path.join(path, base + '_pyqt.py') + if os.stat(ui).st_mtime > os.stat(py).st_mtime: + os.system('%s %s > %s' % (pyqtuic, ui, py)) + print(py) + + py = os.path.join(path, base + '_pyside.py') + if os.stat(ui).st_mtime > os.stat(py).st_mtime: + os.system('%s %s > %s' % (pysideuic, ui, py)) + print(py) diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py new file mode 100644 index 00000000..b9459073 --- /dev/null +++ b/pyqtgraph/reload.py @@ -0,0 +1,516 @@ +# -*- coding: utf-8 -*- +""" +Magic Reload Library +Luke Campagnola 2010 + +Python reload function that actually works (the way you expect it to) + - No re-importing necessary + - Modules can be reloaded in any order + - Replaces functions and methods with their updated code + - Changes instances to use updated classes + - Automatically decides which modules to update by comparing file modification times + +Does NOT: + - re-initialize exting instances, even if __init__ changes + - update references to any module-level objects + ie, this does not reload correctly: + from module import someObject + print someObject + ..but you can use this instead: (this works even for the builtin reload) + import module + print module.someObject +""" + + +import inspect, os, sys, gc, traceback +try: + import __builtin__ as builtins +except ImportError: + import builtins +from .debug import printExc + +def reloadAll(prefix=None, debug=False): + """Automatically reload everything whose __file__ begins with prefix. + - Skips reload if the file has not been updated (if .pyc is newer than .py) + - if prefix is None, checks all loaded modules + """ + failed = [] + changed = [] + for modName, mod in list(sys.modules.items()): ## don't use iteritems; size may change during reload + if not inspect.ismodule(mod): + continue + if modName == '__main__': + continue + + ## Ignore if the file name does not start with prefix + if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: + continue + if prefix is not None and mod.__file__[:len(prefix)] != prefix: + continue + + ## ignore if the .pyc is newer than the .py (or if there is no pyc or py) + py = os.path.splitext(mod.__file__)[0] + '.py' + pyc = py + 'c' + if py not in changed and os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime: + #if debug: + #print "Ignoring module %s; unchanged" % str(mod) + continue + changed.append(py) ## keep track of which modules have changed to insure that duplicate-import modules get reloaded. + + try: + reload(mod, debug=debug) + except: + printExc("Error while reloading module %s, skipping\n" % mod) + failed.append(mod.__name__) + + if len(failed) > 0: + raise Exception("Some modules failed to reload: %s" % ', '.join(failed)) + +def reload(module, debug=False, lists=False, dicts=False): + """Replacement for the builtin reload function: + - Reloads the module as usual + - Updates all old functions and class methods to use the new code + - Updates all instances of each modified class to use the new class + - Can update lists and dicts, but this is disabled by default + - Requires that class and function names have not changed + """ + if debug: + print("Reloading %s" % str(module)) + + ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison + oldDict = module.__dict__.copy() + builtins.reload(module) + newDict = module.__dict__ + + ## Allow modules access to the old dictionary after they reload + if hasattr(module, '__reload__'): + module.__reload__(oldDict) + + ## compare old and new elements from each dict; update where appropriate + for k in oldDict: + old = oldDict[k] + new = newDict.get(k, None) + if old is new or new is None: + continue + + if inspect.isclass(old): + if debug: + print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new))) + updateClass(old, new, debug) + + elif inspect.isfunction(old): + depth = updateFunction(old, new, debug) + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print(" Updating function %s.%s%s" % (module.__name__, k, extra)) + elif lists and isinstance(old, list): + l = old.len() + old.extend(new) + for i in range(l): + old.pop(0) + elif dicts and isinstance(old, dict): + old.update(new) + for k in old: + if k not in new: + del old[k] + + + +## For functions: +## 1) update the code and defaults to new versions. +## 2) keep a reference to the previous version so ALL versions get updated for every reload +def updateFunction(old, new, debug, depth=0, visited=None): + #if debug and depth > 0: + #print " -> also updating previous version", old, " -> ", new + + old.__code__ = new.__code__ + old.__defaults__ = new.__defaults__ + + if visited is None: + visited = [] + if old in visited: + return + visited.append(old) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + maxDepth = updateFunction(old.__previous_reload_version__, new, debug, depth=depth+1, visited=visited) + else: + maxDepth = depth + + ## We need to keep a pointer to the previous version so we remember to update BOTH + ## when the next reload comes around. + if depth == 0: + new.__previous_reload_version__ = old + return maxDepth + + + +## For classes: +## 1) find all instances of the old class and set instance.__class__ to the new class +## 2) update all old class methods to use code from the new class methods +def updateClass(old, new, debug): + + ## Track town all instances and subclasses of old + refs = gc.get_referrers(old) + for ref in refs: + try: + if isinstance(ref, old) and ref.__class__ is old: + ref.__class__ = new + if debug: + print(" Changed class for %s" % safeStr(ref)) + elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__: + ind = ref.__bases__.index(old) + + ## Does not work: + #ref.__bases__ = ref.__bases__[:ind] + (new,) + ref.__bases__[ind+1:] + ## reason: Even though we change the code on methods, they remain bound + ## to their old classes (changing im_class is not allowed). Instead, + ## we have to update the __bases__ such that this class will be allowed + ## as an argument to older methods. + + ## This seems to work. Is there any reason not to? + ## Note that every time we reload, the class hierarchy becomes more complex. + ## (and I presume this may slow things down?) + ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + if debug: + print(" Changed superclass for %s" % safeStr(ref)) + #else: + #if debug: + #print " Ignoring reference", type(ref) + except: + print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new))) + raise + + ## update all class methods to use new code. + ## Generally this is not needed since instances already know about the new class, + ## but it fixes a few specific cases (pyqt signals, for one) + for attr in dir(old): + oa = getattr(old, attr) + if inspect.ismethod(oa): + try: + na = getattr(new, attr) + except AttributeError: + if debug: + print(" Skipping method update for %s; new class does not have this attribute" % attr) + continue + + if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: + depth = updateFunction(oa.__func__, na.__func__, debug) + #oa.im_class = new ## bind old method to new class ## not allowed + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print(" Updating method %s%s" % (attr, extra)) + + ## And copy in new functions that didn't exist previously + for attr in dir(new): + if not hasattr(old, attr): + if debug: + print(" Adding missing attribute %s" % attr) + setattr(old, attr, getattr(new, attr)) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + updateClass(old.__previous_reload_version__, new, debug) + + +## It is possible to build classes for which str(obj) just causes an exception. +## Avoid thusly: +def safeStr(obj): + try: + s = str(obj) + except: + try: + s = repr(obj) + except: + s = "" % (safeStr(type(obj)), id(obj)) + return s + + + + + +## Tests: +# write modules to disk, import, then re-write and run again +if __name__ == '__main__': + doQtTest = True + try: + from PyQt4 import QtCore + if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal + #app = QtGui.QApplication([]) + class Btn(QtCore.QObject): + sig = QtCore.Signal() + def emit(self): + self.sig.emit() + btn = Btn() + except: + raise + print("Error; skipping Qt tests") + doQtTest = False + + + + import os + if not os.path.isdir('test1'): + os.mkdir('test1') + open('test1/__init__.py', 'w') + modFile1 = "test1/test1.py" + modCode1 = """ +import sys +class A(object): + def __init__(self, msg): + object.__init__(self) + self.msg = msg + def fn(self, pfx = ""): + print pfx+"A class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + +class B(A): + def fn(self, pfx=""): + print pfx+"B class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + print pfx+" calling superclass.. (%%s)" %% id(A) + A.fn(self, " ") +""" + + modFile2 = "test2.py" + modCode2 = """ +from test1.test1 import A +from test1.test1 import B + +a1 = A("ax1") +b1 = B("bx1") +class C(A): + def __init__(self, msg): + #print "| C init:" + #print "| C.__bases__ = ", map(id, C.__bases__) + #print "| A:", id(A) + #print "| A.__init__ = ", id(A.__init__.im_func), id(A.__init__.im_func.__code__), id(A.__init__.im_class) + A.__init__(self, msg + "(init from C)") + +def fn(): + print "fn: %s" +""" + + open(modFile1, 'w').write(modCode1%(1,1)) + open(modFile2, 'w').write(modCode2%"message 1") + import test1.test1 as test1 + import test2 + print("Test 1 originals:") + A1 = test1.A + B1 = test1.B + a1 = test1.A("a1") + b1 = test1.B("b1") + a1.fn() + b1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + from test2 import fn, C + + if doQtTest: + print("Button test before:") + btn.sig.connect(fn) + btn.sig.connect(a1.fn) + btn.emit() + #btn.sig.emit() + print("") + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + + print("Test2 before reload:") + + fn() + oldfn = fn + test2.a1.fn() + test2.b1.fn() + c1 = test2.C('c1') + c1.fn() + + os.remove(modFile1+'c') + open(modFile1, 'w').write(modCode1%(2,2)) + print("\n----RELOAD test1-----\n") + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + + print("Subclass test:") + c2 = test2.C('c2') + c2.fn() + + + os.remove(modFile2+'c') + open(modFile2, 'w').write(modCode2%"message 2") + print("\n----RELOAD test2-----\n") + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print("Button test after:") + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print("Test2 after reload:") + fn() + test2.a1.fn() + test2.b1.fn() + + print("\n==> Test 1 Old instances:") + a1.fn() + b1.fn() + c1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + print("\n==> Test 1 New instances:") + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + c2 = test2.C('c2') + c2.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + + + os.remove(modFile1+'c') + os.remove(modFile2+'c') + open(modFile1, 'w').write(modCode1%(3,3)) + open(modFile2, 'w').write(modCode2%"message 3") + + print("\n----RELOAD-----\n") + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print("Button test after:") + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print("Test2 after reload:") + fn() + test2.a1.fn() + test2.b1.fn() + + print("\n==> Test 1 Old instances:") + a1.fn() + b1.fn() + print("function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.__func__), id(a1.fn.__self__.__class__), id(b1.fn.__func__), id(b1.fn.__self__.__class__))) + + print("\n==> Test 1 New instances:") + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + print("function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.__func__), id(a1.fn.__self__.__class__), id(b1.fn.__func__), id(b1.fn.__self__.__class__))) + + + os.remove(modFile1) + os.remove(modFile2) + os.remove(modFile1+'c') + os.remove(modFile2+'c') + os.system('rm -r test1') + + + + + + + + +# +# Failure graveyard ahead: +# + + +"""Reload Importer: +Hooks into import system to +1) keep a record of module dependencies as they are imported +2) make sure modules are always reloaded in correct order +3) update old classes and functions to use reloaded code""" + +#import imp, sys + +## python's import hook mechanism doesn't work since we need to be +## informed every time there is an import statement, not just for new imports +#class ReloadImporter: + #def __init__(self): + #self.depth = 0 + + #def find_module(self, name, path): + #print " "*self.depth + "find: ", name, path + ##if name == 'PyQt4' and path is None: + ##print "PyQt4 -> PySide" + ##self.modData = imp.find_module('PySide') + ##return self + ##return None ## return none to allow the import to proceed normally; return self to intercept with load_module + #self.modData = imp.find_module(name, path) + #self.depth += 1 + ##sys.path_importer_cache = {} + #return self + + #def load_module(self, name): + #mod = imp.load_module(name, *self.modData) + #self.depth -= 1 + #print " "*self.depth + "load: ", name + #return mod + +#def pathHook(path): + #print "path hook:", path + #raise ImportError +#sys.path_hooks.append(pathHook) + +#sys.meta_path.append(ReloadImporter()) + + +### replace __import__ with a wrapper that tracks module dependencies +#modDeps = {} +#reloadModule = None +#origImport = __builtins__.__import__ +#def _import(name, globals=None, locals=None, fromlist=None, level=-1, stack=[]): + ### Note that stack behaves as a static variable. + ##print " "*len(importStack) + "import %s" % args[0] + #stack.append(set()) + #mod = origImport(name, globals, locals, fromlist, level) + #deps = stack.pop() + #if len(stack) > 0: + #stack[-1].add(mod) + #elif reloadModule is not None: ## If this is the top level import AND we're inside a module reload + #modDeps[reloadModule].add(mod) + + #if mod in modDeps: + #modDeps[mod] |= deps + #else: + #modDeps[mod] = deps + + + #return mod + +#__builtins__.__import__ = _import + +### replace +#origReload = __builtins__.reload +#def _reload(mod): + #reloadModule = mod + #ret = origReload(mod) + #reloadModule = None + #return ret +#__builtins__.reload = _reload + + +#def reload(mod, visited=None): + #if visited is None: + #visited = set() + #if mod in visited: + #return + #visited.add(mod) + #for dep in modDeps.get(mod, []): + #reload(dep, visited) + #__builtins__.reload(mod) diff --git a/pyqtgraph/units.py b/pyqtgraph/units.py new file mode 100644 index 00000000..6b7f3099 --- /dev/null +++ b/pyqtgraph/units.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +## Very simple unit support: +## - creates variable names like 'mV' and 'kHz' +## - the value assigned to the variable corresponds to the scale prefix +## (mV = 0.001) +## - the actual units are purely cosmetic for making code clearer: +## +## x = 20*pA is identical to x = 20*1e-12 + +## No unicode variable names (μ,Ω) allowed until python 3 + +SI_PREFIXES = 'yzafpnum kMGTPEZY' +UNITS = 'm,s,g,W,J,V,A,F,T,Hz,Ohm,S,N,C,px,b,B'.split(',') +allUnits = {} + +def addUnit(p, n): + g = globals() + v = 1000**n + for u in UNITS: + g[p+u] = v + allUnits[p+u] = v + +for p in SI_PREFIXES: + if p == ' ': + p = '' + n = 0 + elif p == 'u': + n = -2 + else: + n = SI_PREFIXES.index(p) - 8 + + addUnit(p, n) + +cm = 0.01 + + + + + + +def evalUnits(unitStr): + """ + Evaluate a unit string into ([numerators,...], [denominators,...]) + Examples: + N m/s^2 => ([N, m], [s, s]) + A*s / V => ([A, s], [V,]) + """ + pass + +def formatUnits(units): + """ + Format a unit specification ([numerators,...], [denominators,...]) + into a string (this is the inverse of evalUnits) + """ + pass + +def simplify(units): + """ + Cancel units that appear in both numerator and denominator, then attempt to replace + groups of units with single units where possible (ie, J/s => W) + """ + pass + + \ No newline at end of file diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py new file mode 100644 index 00000000..b013dda0 --- /dev/null +++ b/pyqtgraph/widgets/BusyCursor.py @@ -0,0 +1,24 @@ +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['BusyCursor'] + +class BusyCursor(object): + """Class for displaying a busy mouse cursor during long operations. + Usage:: + + with pyqtgraph.BusyCursor(): + doLongOperation() + + May be nested. + """ + active = [] + + def __enter__(self): + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + BusyCursor.active.append(self) + + def __exit__(self, *args): + BusyCursor.active.pop(-1) + if len(BusyCursor.active) == 0: + QtGui.QApplication.restoreOverrideCursor() + \ No newline at end of file diff --git a/pyqtgraph/widgets/CheckTable.py b/pyqtgraph/widgets/CheckTable.py new file mode 100644 index 00000000..dd33fd75 --- /dev/null +++ b/pyqtgraph/widgets/CheckTable.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from . import VerticalLabel + +__all__ = ['CheckTable'] + +class CheckTable(QtGui.QWidget): + + sigStateChanged = QtCore.Signal(object, object, object) # (row, col, state) + + def __init__(self, columns): + QtGui.QWidget.__init__(self) + self.layout = QtGui.QGridLayout() + self.layout.setSpacing(0) + self.setLayout(self.layout) + self.headers = [] + self.columns = columns + col = 1 + for c in columns: + label = VerticalLabel.VerticalLabel(c, orientation='vertical') + self.headers.append(label) + self.layout.addWidget(label, 0, col) + col += 1 + + self.rowNames = [] + self.rowWidgets = [] + self.oldRows = {} ## remember settings from removed rows; reapply if they reappear. + + + def updateRows(self, rows): + for r in self.rowNames[:]: + if r not in rows: + self.removeRow(r) + for r in rows: + if r not in self.rowNames: + self.addRow(r) + + def addRow(self, name): + label = QtGui.QLabel(name) + row = len(self.rowNames)+1 + self.layout.addWidget(label, row, 0) + checks = [] + col = 1 + for c in self.columns: + check = QtGui.QCheckBox('') + check.col = c + check.row = name + self.layout.addWidget(check, row, col) + checks.append(check) + if name in self.oldRows: + check.setChecked(self.oldRows[name][col]) + col += 1 + #QtCore.QObject.connect(check, QtCore.SIGNAL('stateChanged(int)'), self.checkChanged) + check.stateChanged.connect(self.checkChanged) + self.rowNames.append(name) + self.rowWidgets.append([label] + checks) + + def removeRow(self, name): + row = self.rowNames.index(name) + self.oldRows[name] = self.saveState()['rows'][row] ## save for later + self.rowNames.pop(row) + for w in self.rowWidgets[row]: + w.setParent(None) + #QtCore.QObject.disconnect(w, QtCore.SIGNAL('stateChanged(int)'), self.checkChanged) + if isinstance(w, QtGui.QCheckBox): + w.stateChanged.disconnect(self.checkChanged) + self.rowWidgets.pop(row) + for i in range(row, len(self.rowNames)): + widgets = self.rowWidgets[i] + for j in range(len(widgets)): + widgets[j].setParent(None) + self.layout.addWidget(widgets[j], i+1, j) + + def checkChanged(self, state): + check = QtCore.QObject.sender(self) + #self.emit(QtCore.SIGNAL('stateChanged'), check.row, check.col, state) + self.sigStateChanged.emit(check.row, check.col, state) + + def saveState(self): + rows = [] + for i in range(len(self.rowNames)): + row = [self.rowNames[i]] + [c.isChecked() for c in self.rowWidgets[i][1:]] + rows.append(row) + return {'cols': self.columns, 'rows': rows} + + def restoreState(self, state): + rows = [r[0] for r in state['rows']] + self.updateRows(rows) + for r in state['rows']: + rowNum = self.rowNames.index(r[0]) + for i in range(1, len(r)): + self.rowWidgets[rowNum][i].setChecked(r[i]) + diff --git a/ColorButton.py b/pyqtgraph/widgets/ColorButton.py similarity index 70% rename from ColorButton.py rename to pyqtgraph/widgets/ColorButton.py index cc967c3b..fafe2ae7 100644 --- a/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -1,11 +1,21 @@ # -*- coding: utf-8 -*- -from PyQt4 import QtGui, QtCore -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal -import functions +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as functions + +__all__ = ['ColorButton'] class ColorButton(QtGui.QPushButton): + """ + **Bases:** QtGui.QPushButton + Button displaying a color and allowing the user to select a new color. + + ====================== ============================================================ + **Signals**: + sigColorChanging(self) emitted whenever a new color is picked in the color dialog + sigColorChanged(self) emitted when the selected color is accepted (user clicks OK) + ====================== ============================================================ + """ sigColorChanging = QtCore.Signal(object) ## emitted whenever a new color is picked in the color dialog sigColorChanged = QtCore.Signal(object) ## emitted when the selected color is accepted (user clicks OK) @@ -27,11 +37,18 @@ class ColorButton(QtGui.QPushButton): def paintEvent(self, ev): QtGui.QPushButton.paintEvent(self, ev) p = QtGui.QPainter(self) + rect = self.rect().adjusted(6, 6, -6, -6) + ## draw white base, then texture for indicating transparency, then actual color + p.setBrush(functions.mkBrush('w')) + p.drawRect(rect) + p.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern)) + p.drawRect(rect) p.setBrush(functions.mkBrush(self._color)) - p.drawRect(self.rect().adjusted(5, 5, -5, -5)) + p.drawRect(rect) p.end() def setColor(self, color, finished=True): + """Sets the button's color and emits both sigColorChanged and sigColorChanging.""" self._color = functions.mkColor(color) if finished: self.sigColorChanged.emit(self) @@ -66,18 +83,3 @@ class ColorButton(QtGui.QPushButton): def widgetGroupInterface(self): return (self.sigColorChanged, ColorButton.saveState, ColorButton.restoreState) -if __name__ == '__main__': - app = QtGui.QApplication([]) - win = QtGui.QMainWindow() - btn = ColorButton() - win.setCentralWidget(btn) - win.show() - - def change(btn): - print "change", btn.color() - def done(btn): - print "done", btn.color() - - btn.sigColorChanging.connect(change) - btn.sigColorChanged.connect(done) - \ No newline at end of file diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py new file mode 100644 index 00000000..1884648c --- /dev/null +++ b/pyqtgraph/widgets/ComboBox.py @@ -0,0 +1,41 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.SignalProxy import SignalProxy + + +class ComboBox(QtGui.QComboBox): + """Extends QComboBox to add extra functionality. + - updateList() - updates the items in the comboBox while blocking signals, remembers and resets to the previous values if it's still in the list + """ + + + def __init__(self, parent=None, items=None, default=None): + QtGui.QComboBox.__init__(self, parent) + + #self.value = default + + if items is not None: + self.addItems(items) + if default is not None: + self.setValue(default) + + def setValue(self, value): + ind = self.findText(value) + if ind == -1: + return + #self.value = value + self.setCurrentIndex(ind) + + def updateList(self, items): + prevVal = str(self.currentText()) + try: + self.blockSignals(True) + self.clear() + self.addItems(items) + self.setValue(prevVal) + + finally: + self.blockSignals(False) + + if str(self.currentText()) != prevVal: + self.currentIndexChanged.emit(self.currentIndex()) + \ No newline at end of file diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py new file mode 100644 index 00000000..a6b5cac8 --- /dev/null +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.pgcollections import OrderedDict +import types, traceback +import numpy as np + +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +__all__ = ['DataTreeWidget'] + +class DataTreeWidget(QtGui.QTreeWidget): + """ + Widget for displaying hierarchical python data structures + (eg, nested dicts, lists, and arrays) + """ + + + def __init__(self, parent=None, data=None): + QtGui.QTreeWidget.__init__(self, parent) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setData(data) + self.setColumnCount(3) + self.setHeaderLabels(['key / index', 'type', 'value']) + + def setData(self, data, hideRoot=False): + """data should be a dictionary.""" + self.clear() + self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) + #node = self.mkNode('', data) + #while node.childCount() > 0: + #c = node.child(0) + #node.removeChild(c) + #self.invisibleRootItem().addChild(c) + self.expandToDepth(3) + self.resizeColumnToContents(0) + + def buildTree(self, data, parent, name='', hideRoot=False): + if hideRoot: + node = parent + else: + typeStr = type(data).__name__ + if typeStr == 'instance': + typeStr += ": " + data.__class__.__name__ + node = QtGui.QTreeWidgetItem([name, typeStr, ""]) + parent.addChild(node) + + if isinstance(data, types.TracebackType): ## convert traceback to a list of strings + data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) + elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + data = { + 'data': data.view(np.ndarray), + 'meta': data.infoCopy() + } + + if isinstance(data, dict): + for k in data: + self.buildTree(data[k], node, str(k)) + elif isinstance(data, list) or isinstance(data, tuple): + for i in range(len(data)): + self.buildTree(data[i], node, str(i)) + else: + node.setText(2, str(data)) + + + #def mkNode(self, name, v): + #if type(v) is list and len(v) > 0 and isinstance(v[0], dict): + #inds = map(unicode, range(len(v))) + #v = OrderedDict(zip(inds, v)) + #if isinstance(v, dict): + ##print "\nadd tree", k, v + #node = QtGui.QTreeWidgetItem([name]) + #for k in v: + #newNode = self.mkNode(k, v[k]) + #node.addChild(newNode) + #else: + ##print "\nadd value", k, str(v) + #node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)]) + #return node + diff --git a/pyqtgraph/widgets/FeedbackButton.py b/pyqtgraph/widgets/FeedbackButton.py new file mode 100644 index 00000000..f788f4b6 --- /dev/null +++ b/pyqtgraph/widgets/FeedbackButton.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui + +__all__ = ['FeedbackButton'] + +class FeedbackButton(QtGui.QPushButton): + """ + QPushButton which flashes success/failure indication for slow or asynchronous procedures. + """ + + + ### For thread-safetyness + sigCallSuccess = QtCore.Signal(object, object, object) + sigCallFailure = QtCore.Signal(object, object, object) + sigCallProcess = QtCore.Signal(object, object, object) + sigReset = QtCore.Signal() + + def __init__(self, *args): + QtGui.QPushButton.__init__(self, *args) + self.origStyle = None + self.origText = self.text() + self.origStyle = self.styleSheet() + self.origTip = self.toolTip() + self.limitedTime = True + + + #self.textTimer = QtCore.QTimer() + #self.tipTimer = QtCore.QTimer() + #self.textTimer.timeout.connect(self.setText) + #self.tipTimer.timeout.connect(self.setToolTip) + + self.sigCallSuccess.connect(self.success) + self.sigCallFailure.connect(self.failure) + self.sigCallProcess.connect(self.processing) + self.sigReset.connect(self.reset) + + + def feedback(self, success, message=None, tip="", limitedTime=True): + """Calls success() or failure(). If you want the message to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action.Threadsafe.""" + if success: + self.success(message, tip, limitedTime=limitedTime) + else: + self.failure(message, tip, limitedTime=limitedTime) + + def success(self, message=None, tip="", limitedTime=True): + """Displays specified message on button and flashes button green to let user know action was successful. If you want the success to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action. Threadsafe.""" + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(True) + #print "success" + self.startBlink("#0F0", message, tip, limitedTime=limitedTime) + else: + self.sigCallSuccess.emit(message, tip, limitedTime) + + def failure(self, message=None, tip="", limitedTime=True): + """Displays specified message on button and flashes button red to let user know there was an error. If you want the error to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action. Threadsafe. """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(True) + #print "fail" + self.startBlink("#F00", message, tip, limitedTime=limitedTime) + else: + self.sigCallFailure.emit(message, tip, limitedTime) + + def processing(self, message="Processing..", tip="", processEvents=True): + """Displays specified message on button to let user know the action is in progress. Threadsafe. """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(False) + self.setText(message, temporary=True) + self.setToolTip(tip, temporary=True) + if processEvents: + QtGui.QApplication.processEvents() + else: + self.sigCallProcess.emit(message, tip, processEvents) + + + def reset(self): + """Resets the button to its original text and style. Threadsafe.""" + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.limitedTime = True + self.setText() + self.setToolTip() + self.setStyleSheet() + else: + self.sigReset.emit() + + def startBlink(self, color, message=None, tip="", limitedTime=True): + #if self.origStyle is None: + #self.origStyle = self.styleSheet() + #self.origText = self.text() + self.setFixedHeight(self.height()) + + if message is not None: + self.setText(message, temporary=True) + self.setToolTip(tip, temporary=True) + self.count = 0 + #self.indStyle = "QPushButton {border: 2px solid %s; border-radius: 5px}" % color + self.indStyle = "QPushButton {background-color: %s}" % color + self.limitedTime = limitedTime + self.borderOn() + if limitedTime: + QtCore.QTimer.singleShot(2000, self.setText) + QtCore.QTimer.singleShot(10000, self.setToolTip) + + def borderOn(self): + self.setStyleSheet(self.indStyle, temporary=True) + if self.limitedTime or self.count <=2: + QtCore.QTimer.singleShot(100, self.borderOff) + + + def borderOff(self): + self.setStyleSheet() + self.count += 1 + if self.count >= 2: + if self.limitedTime: + return + QtCore.QTimer.singleShot(30, self.borderOn) + + + def setText(self, text=None, temporary=False): + if text is None: + text = self.origText + #print text + QtGui.QPushButton.setText(self, text) + if not temporary: + self.origText = text + + def setToolTip(self, text=None, temporary=False): + if text is None: + text = self.origTip + QtGui.QPushButton.setToolTip(self, text) + if not temporary: + self.origTip = text + + def setStyleSheet(self, style=None, temporary=False): + if style is None: + style = self.origStyle + QtGui.QPushButton.setStyleSheet(self, style) + if not temporary: + self.origStyle = style + + +if __name__ == '__main__': + import time + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + btn = FeedbackButton("Button") + fail = True + def click(): + btn.processing("Hold on..") + time.sleep(2.0) + + global fail + fail = not fail + if fail: + btn.failure(message="FAIL.", tip="There was a failure. Get over it.") + else: + btn.success(message="Bueno!") + btn.clicked.connect(click) + win.setCentralWidget(btn) + win.show() \ No newline at end of file diff --git a/pyqtgraph/widgets/FileDialog.py b/pyqtgraph/widgets/FileDialog.py new file mode 100644 index 00000000..33b838a2 --- /dev/null +++ b/pyqtgraph/widgets/FileDialog.py @@ -0,0 +1,14 @@ +from pyqtgraph.Qt import QtGui, QtCore +import sys + +__all__ = ['FileDialog'] + +class FileDialog(QtGui.QFileDialog): + ## Compatibility fix for OSX: + ## For some reason the native dialog doesn't show up when you set AcceptMode to AcceptSave on OS X, so we don't use the native dialog + + def __init__(self, *args): + QtGui.QFileDialog.__init__(self, *args) + + if sys.platform == 'darwin': + self.setOption(QtGui.QFileDialog.DontUseNativeDialog) \ No newline at end of file diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py new file mode 100644 index 00000000..2b9b52d2 --- /dev/null +++ b/pyqtgraph/widgets/GradientWidget.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsView import GraphicsView +from pyqtgraph.graphicsItems.GradientEditorItem import GradientEditorItem +import weakref +import numpy as np + +__all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider'] + + +class GradientWidget(GraphicsView): + + sigGradientChanged = QtCore.Signal(object) + sigGradientChangeFinished = QtCore.Signal(object) + + def __init__(self, parent=None, orientation='bottom', *args, **kargs): + GraphicsView.__init__(self, parent, useOpenGL=False, background=None) + self.maxDim = 31 + kargs['tickPen'] = 'k' + self.item = GradientEditorItem(*args, **kargs) + self.item.sigGradientChanged.connect(self.sigGradientChanged) + self.item.sigGradientChangeFinished.connect(self.sigGradientChangeFinished) + self.setCentralItem(self.item) + self.setOrientation(orientation) + self.setCacheMode(self.CacheNone) + self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) + self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + #self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush)) + #self.setAutoFillBackground(False) + #self.setAttribute(QtCore.Qt.WA_PaintOnScreen, False) + #self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True) + + def setOrientation(self, ort): + self.item.setOrientation(ort) + self.orientation = ort + self.setMaxDim() + + def setMaxDim(self, mx=None): + if mx is None: + mx = self.maxDim + else: + self.maxDim = mx + + if self.orientation in ['bottom', 'top']: + self.setFixedHeight(mx) + self.setMaximumWidth(16777215) + else: + self.setFixedWidth(mx) + self.setMaximumHeight(16777215) + + def __getattr__(self, attr): + ### wrap methods from GradientEditorItem + return getattr(self.item, attr) + + diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py new file mode 100644 index 00000000..1e667278 --- /dev/null +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -0,0 +1,12 @@ +from pyqtgraph.Qt import QtGui +from pyqtgraph.graphicsItems.GraphicsLayout import GraphicsLayout +from .GraphicsView import GraphicsView + +__all__ = ['GraphicsLayoutWidget'] +class GraphicsLayoutWidget(GraphicsView): + def __init__(self, parent=None, **kargs): + GraphicsView.__init__(self, parent) + self.ci = GraphicsLayout(**kargs) + for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel', 'addLayout', 'addLabel', 'addViewBox', 'removeItem', 'itemIndex', 'clear']: + setattr(self, n, getattr(self.ci, n)) + self.setCentralItem(self.ci) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py new file mode 100644 index 00000000..403aed9d --- /dev/null +++ b/pyqtgraph/widgets/GraphicsView.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +""" +GraphicsView.py - Extension of QGraphicsView +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg + +try: + from pyqtgraph.Qt import QtOpenGL + HAVE_OPENGL = True +except ImportError: + HAVE_OPENGL = False + +from pyqtgraph.Point import Point +import sys, os +from .FileDialog import FileDialog +from pyqtgraph.GraphicsScene import GraphicsScene +import numpy as np +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug +import pyqtgraph + +__all__ = ['GraphicsView'] + +class GraphicsView(QtGui.QGraphicsView): + """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the + viewed coordinate range. Also automatically creates a GraphicsScene and a central QGraphicsWidget + that is automatically scaled to the full view geometry. + + This widget is the basis for :class:`PlotWidget `, + :class:`GraphicsLayoutWidget `, and the view widget in + :class:`ImageView `. + + By default, the view coordinate system matches the widget's pixel coordinates and + automatically updates when the view is resized. This can be overridden by setting + autoPixelRange=False. The exact visible range can be set with setRange(). + + The view can be panned using the middle mouse button and scaled using the right mouse button if + enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" + + sigRangeChanged = QtCore.Signal(object, object) + sigTransformChanged = QtCore.Signal(object) + sigMouseReleased = QtCore.Signal(object) + sigSceneMouseMoved = QtCore.Signal(object) + #sigRegionChanged = QtCore.Signal(object) + sigScaleChanged = QtCore.Signal(object) + lastFileDir = None + + def __init__(self, parent=None, useOpenGL=None, background='default'): + """ + ============ ============================================================ + Arguments: + parent Optional parent widget + useOpenGL If True, the GraphicsView will use OpenGL to do all of its + rendering. This can improve performance on some systems, + but may also introduce bugs (the combination of + QGraphicsView and QGLWidget is still an 'experimental' + feature of Qt) + background Set the background color of the GraphicsView. Accepts any + single argument accepted by + :func:`mkColor `. By + default, the background color is determined using the + 'backgroundColor' configuration option (see + :func:`setConfigOption `. + ============ ============================================================ + """ + + self.closed = False + + QtGui.QGraphicsView.__init__(self, parent) + + if useOpenGL is None: + useOpenGL = pyqtgraph.getConfigOption('useOpenGL') + + self.useOpenGL(useOpenGL) + + self.setCacheMode(self.CacheBackground) + + ## This might help, but it's probably dangerous in the general case.. + #self.setOptimizationFlag(self.DontSavePainterState, True) + + self.setBackground(background) + + self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setFrameShape(QtGui.QFrame.NoFrame) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) + self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) + self.setViewportUpdateMode(QtGui.QGraphicsView.MinimalViewportUpdate) + + + self.lockedViewports = [] + self.lastMousePos = None + self.setMouseTracking(True) + self.aspectLocked = False + self.range = QtCore.QRectF(0, 0, 1, 1) + self.autoPixelRange = True + self.currentItem = None + self.clearMouse() + self.updateMatrix() + self.sceneObj = GraphicsScene() + self.setScene(self.sceneObj) + + ## Workaround for PySide crash + ## This ensures that the scene will outlive the view. + if pyqtgraph.Qt.USE_PYSIDE: + self.sceneObj._view_ref_workaround = self + + ## by default we set up a central widget with a grid layout. + ## this can be replaced if needed. + self.centralWidget = None + self.setCentralItem(QtGui.QGraphicsWidget()) + self.centralLayout = QtGui.QGraphicsGridLayout() + self.centralWidget.setLayout(self.centralLayout) + + self.mouseEnabled = False + self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False) + self.clickAccepted = False + + def setAntialiasing(self, aa): + """Enable or disable default antialiasing. + Note that this will only affect items that do not specify their own antialiasing options.""" + if aa: + self.setRenderHints(self.renderHints() | QtGui.QPainter.Antialiasing) + else: + self.setRenderHints(self.renderHints() & ~QtGui.QPainter.Antialiasing) + + def setBackground(self, background): + """ + Set the background color of the GraphicsView. + To use the defaults specified py pyqtgraph.setConfigOption, use background='default'. + To make the background transparent, use background=None. + """ + self._background = background + if background == 'default': + background = pyqtgraph.getConfigOption('background') + if background is None: + self.setBackgroundRole(QtGui.QPalette.NoRole) + else: + brush = fn.mkBrush(background) + self.setBackgroundBrush(brush) + + + def close(self): + self.centralWidget = None + self.scene().clear() + self.currentItem = None + self.sceneObj = None + self.closed = True + self.setViewport(None) + + def useOpenGL(self, b=True): + if b: + if not HAVE_OPENGL: + raise Exception("Requested to use OpenGL with QGraphicsView, but QtOpenGL module is not available.") + v = QtOpenGL.QGLWidget() + else: + v = QtGui.QWidget() + + self.setViewport(v) + + def keyPressEvent(self, ev): + self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene + ## (view likes to eat arrow key events) + + + def setCentralItem(self, item): + return self.setCentralWidget(item) + + def setCentralWidget(self, item): + """Sets a QGraphicsWidget to automatically fill the entire view (the item will be automatically + resize whenever the GraphicsView is resized).""" + if self.centralWidget is not None: + self.scene().removeItem(self.centralWidget) + self.centralWidget = item + self.sceneObj.addItem(item) + self.resizeEvent(None) + + def addItem(self, *args): + return self.scene().addItem(*args) + + def removeItem(self, *args): + return self.scene().removeItem(*args) + + def enableMouse(self, b=True): + self.mouseEnabled = b + self.autoPixelRange = (not b) + + def clearMouse(self): + self.mouseTrail = [] + self.lastButtonReleased = None + + def resizeEvent(self, ev): + if self.closed: + return + if self.autoPixelRange: + self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) + GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way. + self.updateMatrix() + + def updateMatrix(self, propagate=True): + self.setSceneRect(self.range) + if self.autoPixelRange: + self.resetTransform() + else: + if self.aspectLocked: + self.fitInView(self.range, QtCore.Qt.KeepAspectRatio) + else: + self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) + + self.sigRangeChanged.emit(self, self.range) + self.sigTransformChanged.emit(self) + + if propagate: + for v in self.lockedViewports: + v.setXRange(self.range, padding=0) + + def viewRect(self): + """Return the boundaries of the view in scene coordinates""" + ## easier to just return self.range ? + r = QtCore.QRectF(self.rect()) + return self.viewportTransform().inverted()[0].mapRect(r) + + def visibleRange(self): + ## for backward compatibility + return self.viewRect() + + def translate(self, dx, dy): + self.range.adjust(dx, dy, dx, dy) + self.updateMatrix() + + def scale(self, sx, sy, center=None): + scale = [sx, sy] + if self.aspectLocked: + scale[0] = scale[1] + + if self.scaleCenter: + center = None + if center is None: + center = self.range.center() + + w = self.range.width() / scale[0] + h = self.range.height() / scale[1] + self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) + + + self.updateMatrix() + self.sigScaleChanged.emit(self) + + def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True): + if disableAutoPixel: + self.autoPixelRange=False + if newRect is None: + newRect = self.visibleRange() + padding = 0 + + padding = Point(padding) + newRect = QtCore.QRectF(newRect) + pw = newRect.width() * padding[0] + ph = newRect.height() * padding[1] + newRect = newRect.adjusted(-pw, -ph, pw, ph) + scaleChanged = False + if self.range.width() != newRect.width() or self.range.height() != newRect.height(): + scaleChanged = True + self.range = newRect + #print "New Range:", self.range + self.centralWidget.setGeometry(self.range) + self.updateMatrix(propagate) + if scaleChanged: + self.sigScaleChanged.emit(self) + + def scaleToImage(self, image): + """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.""" + pxSize = image.pixelSize() + image.setPxMode(True) + try: + self.sigScaleChanged.disconnect(image.setScaledMode) + except TypeError: + pass + tl = image.sceneBoundingRect().topLeft() + w = self.size().width() * pxSize[0] + h = self.size().height() * pxSize[1] + range = QtCore.QRectF(tl.x(), tl.y(), w, h) + GraphicsView.setRange(self, range, padding=0) + self.sigScaleChanged.connect(image.setScaledMode) + + + + def lockXRange(self, v1): + if not v1 in self.lockedViewports: + self.lockedViewports.append(v1) + + def setXRange(self, r, padding=0.05): + r1 = QtCore.QRectF(self.range) + r1.setLeft(r.left()) + r1.setRight(r.right()) + GraphicsView.setRange(self, r1, padding=[padding, 0], propagate=False) + + def setYRange(self, r, padding=0.05): + r1 = QtCore.QRectF(self.range) + r1.setTop(r.top()) + r1.setBottom(r.bottom()) + GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False) + + def wheelEvent(self, ev): + QtGui.QGraphicsView.wheelEvent(self, ev) + if not self.mouseEnabled: + return + sc = 1.001 ** ev.delta() + #self.scale *= sc + #self.updateMatrix() + self.scale(sc, sc) + + def setAspectLocked(self, s): + self.aspectLocked = s + + def leaveEvent(self, ev): + self.scene().leaveEvent(ev) ## inform scene when mouse leaves + + def mousePressEvent(self, ev): + QtGui.QGraphicsView.mousePressEvent(self, ev) + + + if not self.mouseEnabled: + return + self.lastMousePos = Point(ev.pos()) + self.mousePressPos = ev.pos() + self.clickAccepted = ev.isAccepted() + if not self.clickAccepted: + self.scene().clearSelection() + return ## Everything below disabled for now.. + + def mouseReleaseEvent(self, ev): + QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + if not self.mouseEnabled: + return + self.sigMouseReleased.emit(ev) + self.lastButtonReleased = ev.button() + return ## Everything below disabled for now.. + + def mouseMoveEvent(self, ev): + if self.lastMousePos is None: + self.lastMousePos = Point(ev.pos()) + delta = Point(ev.pos() - self.lastMousePos) + self.lastMousePos = Point(ev.pos()) + + QtGui.QGraphicsView.mouseMoveEvent(self, ev) + if not self.mouseEnabled: + return + self.sigSceneMouseMoved.emit(self.mapToScene(ev.pos())) + + if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it. + return + + if ev.buttons() == QtCore.Qt.RightButton: + delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) + scale = 1.01 ** delta + self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos)) + self.sigRangeChanged.emit(self, self.range) + + elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button. + px = self.pixelSize() + tr = -delta * px + + self.translate(tr[0], tr[1]) + self.sigRangeChanged.emit(self, self.range) + + def pixelSize(self): + """Return vector with the length and width of one view pixel in scene coordinates""" + p0 = Point(0,0) + p1 = Point(1,1) + tr = self.transform().inverted()[0] + p01 = tr.map(p0) + p11 = tr.map(p1) + return Point(p11 - p01) + + def dragEnterEvent(self, ev): + ev.ignore() ## not sure why, but for some reason this class likes to consume drag events + + diff --git a/pyqtgraph/widgets/HistogramLUTWidget.py b/pyqtgraph/widgets/HistogramLUTWidget.py new file mode 100644 index 00000000..bc041595 --- /dev/null +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -0,0 +1,33 @@ +""" +Widget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. +This is a wrapper around HistogramLUTItem +""" + +from pyqtgraph.Qt import QtGui, QtCore +from .GraphicsView import GraphicsView +from pyqtgraph.graphicsItems.HistogramLUTItem import HistogramLUTItem + +__all__ = ['HistogramLUTWidget'] + + +class HistogramLUTWidget(GraphicsView): + + def __init__(self, parent=None, *args, **kargs): + background = kargs.get('background', 'k') + GraphicsView.__init__(self, parent, useOpenGL=False, background=background) + self.item = HistogramLUTItem(*args, **kargs) + self.setCentralItem(self.item) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + self.setMinimumWidth(95) + + + def sizeHint(self): + return QtCore.QSize(115, 200) + + + + def __getattr__(self, attr): + return getattr(self.item, attr) + + + diff --git a/pyqtgraph/widgets/JoystickButton.py b/pyqtgraph/widgets/JoystickButton.py new file mode 100644 index 00000000..201a957a --- /dev/null +++ b/pyqtgraph/widgets/JoystickButton.py @@ -0,0 +1,95 @@ +from pyqtgraph.Qt import QtGui, QtCore + + +__all__ = ['JoystickButton'] + +class JoystickButton(QtGui.QPushButton): + sigStateChanged = QtCore.Signal(object, object) ## self, state + + def __init__(self, parent=None): + QtGui.QPushButton.__init__(self, parent) + self.radius = 200 + self.setCheckable(True) + self.state = None + self.setState(0,0) + self.setFixedWidth(50) + self.setFixedHeight(50) + + + def mousePressEvent(self, ev): + self.setChecked(True) + self.pressPos = ev.pos() + ev.accept() + + def mouseMoveEvent(self, ev): + dif = ev.pos()-self.pressPos + self.setState(dif.x(), -dif.y()) + + def mouseReleaseEvent(self, ev): + self.setChecked(False) + self.setState(0,0) + + def wheelEvent(self, ev): + ev.accept() + + + def doubleClickEvent(self, ev): + ev.accept() + + def getState(self): + return self.state + + def setState(self, *xy): + xy = list(xy) + d = (xy[0]**2 + xy[1]**2)**0.5 + nxy = [0,0] + for i in [0,1]: + if xy[i] == 0: + nxy[i] = 0 + else: + nxy[i] = xy[i]/d + + if d > self.radius: + d = self.radius + d = (d/self.radius)**2 + xy = [nxy[0]*d, nxy[1]*d] + + w2 = self.width()/2. + h2 = self.height()/2 + self.spotPos = QtCore.QPoint(w2*(1+xy[0]), h2*(1-xy[1])) + self.update() + if self.state == xy: + return + self.state = xy + self.sigStateChanged.emit(self, self.state) + + def paintEvent(self, ev): + QtGui.QPushButton.paintEvent(self, ev) + p = QtGui.QPainter(self) + p.setBrush(QtGui.QBrush(QtGui.QColor(0,0,0))) + p.drawEllipse(self.spotPos.x()-3,self.spotPos.y()-3,6,6) + + def resizeEvent(self, ev): + self.setState(*self.state) + QtGui.QPushButton.resizeEvent(self, ev) + + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + w = QtGui.QMainWindow() + b = JoystickButton() + w.setCentralWidget(b) + w.show() + w.resize(100, 100) + + def fn(b, s): + print("state changed:", s) + + b.sigStateChanged.connect(fn) + + ## Start Qt event loop unless running in interactive mode. + import sys + if sys.flags.interactive != 1: + app.exec_() + \ No newline at end of file diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py new file mode 100644 index 00000000..f567ad74 --- /dev/null +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -0,0 +1,101 @@ +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['LayoutWidget'] +class LayoutWidget(QtGui.QWidget): + """ + Convenience class used for laying out QWidgets in a grid. + (It's just a little less effort to use than QGridLayout) + """ + + def __init__(self, parent=None): + QtGui.QWidget.__init__(self, parent) + self.layout = QtGui.QGridLayout() + self.setLayout(self.layout) + self.items = {} + self.rows = {} + self.currentRow = 0 + self.currentCol = 0 + + def nextRow(self): + """Advance to next row for automatic widget placement""" + self.currentRow += 1 + self.currentCol = 0 + + def nextColumn(self, colspan=1): + """Advance to next column, while returning the current column number + (generally only for internal use--called by addWidget)""" + self.currentCol += colspan + return self.currentCol-colspan + + def nextCol(self, *args, **kargs): + """Alias of nextColumn""" + return self.nextColumn(*args, **kargs) + + + def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a QLabel with *text* and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to QLabel(). + Returns the created widget. + """ + text = QtGui.QLabel(text, **kargs) + self.addItem(text, row, col, rowspan, colspan) + return text + + def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create an empty LayoutWidget and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`LayoutWidget.__init__ ` + Returns the created widget. + """ + layout = LayoutWidget(**kargs) + self.addItem(layout, row, col, rowspan, colspan) + return layout + + def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1): + """ + Add a widget to the layout and place it in the next available cell (or in the cell specified). + """ + if row == 'next': + self.nextRow() + row = self.currentRow + elif row is None: + row = self.currentRow + + + if col is None: + col = self.nextCol(colspan) + + if row not in self.rows: + self.rows[row] = {} + self.rows[row][col] = item + self.items[item] = (row, col) + + self.layout.addWidget(item, row, col, rowspan, colspan) + + def getWidget(self, row, col): + """Return the widget in (*row*, *col*)""" + return self.row[row][col] + + #def itemIndex(self, item): + #for i in range(self.layout.count()): + #if self.layout.itemAt(i).graphicsItem() is item: + #return i + #raise Exception("Could not determine index of item " + str(item)) + + #def removeItem(self, item): + #"""Remove *item* from the layout.""" + #ind = self.itemIndex(item) + #self.layout.removeAt(ind) + #self.scene().removeItem(item) + #r,c = self.items[item] + #del self.items[item] + #del self.rows[r][c] + #self.update() + + #def clear(self): + #items = [] + #for i in list(self.items.keys()): + #self.removeItem(i) + + diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py new file mode 100644 index 00000000..25e058f9 --- /dev/null +++ b/pyqtgraph/widgets/MatplotlibWidget.py @@ -0,0 +1,37 @@ +from pyqtgraph.Qt import QtGui, QtCore +import matplotlib +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar +from matplotlib.figure import Figure + +class MatplotlibWidget(QtGui.QWidget): + """ + Implements a Matplotlib figure inside a QWidget. + Use getFigure() and redraw() to interact with matplotlib. + + Example:: + + mw = MatplotlibWidget() + subplot = mw.getFigure().add_subplot(111) + subplot.plot(x,y) + mw.draw() + """ + + def __init__(self, size=(5.0, 4.0), dpi=100): + QtGui.QWidget.__init__(self) + self.fig = Figure(size, dpi=dpi) + self.canvas = FigureCanvas(self.fig) + self.canvas.setParent(self) + self.toolbar = NavigationToolbar(self.canvas, self) + + self.vbox = QtGui.QVBoxLayout() + self.vbox.addWidget(self.toolbar) + self.vbox.addWidget(self.canvas) + + self.setLayout(self.vbox) + + def getFigure(self): + return self.fig + + def draw(self): + self.canvas.draw() diff --git a/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py similarity index 86% rename from MultiPlotWidget.py rename to pyqtgraph/widgets/MultiPlotWidget.py index 8071127a..400bee92 100644 --- a/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -5,16 +5,16 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from GraphicsView import * -from MultiPlotItem import * -import exceptions +from .GraphicsView import GraphicsView +import pyqtgraph.graphicsItems.MultiPlotItem as MultiPlotItem +__all__ = ['MultiPlotWidget'] class MultiPlotWidget(GraphicsView): """Widget implementing a graphicsView with a single PlotItem inside.""" def __init__(self, parent=None): GraphicsView.__init__(self, parent) self.enableMouse(False) - self.mPlotItem = MultiPlotItem() + self.mPlotItem = MultiPlotItem.MultiPlotItem() self.setCentralItem(self.mPlotItem) ## Explicitly wrap methods from mPlotItem #for m in ['setData']: @@ -25,7 +25,7 @@ class MultiPlotWidget(GraphicsView): m = getattr(self.mPlotItem, attr) if hasattr(m, '__call__'): return m - raise exceptions.NameError(attr) + raise NameError(attr) def widgetGroupInterface(self): return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState) diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py new file mode 100644 index 00000000..7950a53d --- /dev/null +++ b/pyqtgraph/widgets/PathButton.py @@ -0,0 +1,50 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +__all__ = ['PathButton'] + + +class PathButton(QtGui.QPushButton): + """Simple PushButton extension which paints a QPainterPath on its face""" + def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30)): + QtGui.QPushButton.__init__(self, parent) + self.path = None + if pen == 'default': + pen = 'k' + self.setPen(pen) + self.setBrush(brush) + if path is not None: + self.setPath(path) + if size is not None: + self.setFixedWidth(size[0]) + self.setFixedHeight(size[1]) + + + def setBrush(self, brush): + self.brush = pg.mkBrush(brush) + + def setPen(self, pen): + self.pen = pg.mkPen(pen) + + def setPath(self, path): + self.path = path + self.update() + + def paintEvent(self, ev): + QtGui.QPushButton.paintEvent(self, ev) + margin = 7 + geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin) + rect = self.path.boundingRect() + scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height())) + + p = QtGui.QPainter(self) + p.setRenderHint(p.Antialiasing) + p.translate(geom.center()) + p.scale(scale, scale) + p.translate(-rect.center()) + p.setPen(self.pen) + p.setBrush(self.brush) + p.drawPath(self.path) + p.end() + + + \ No newline at end of file diff --git a/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py similarity index 50% rename from PlotWidget.py rename to pyqtgraph/widgets/PlotWidget.py index 1254b963..1fa07f2a 100644 --- a/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -5,34 +5,56 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from GraphicsView import * -from PlotItem import * -import exceptions +from pyqtgraph.Qt import QtCore, QtGui +from .GraphicsView import * +from pyqtgraph.graphicsItems.PlotItem import * +__all__ = ['PlotWidget'] class PlotWidget(GraphicsView): #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView - """Widget implementing a graphicsView with a single PlotItem inside.""" + """ + :class:`GraphicsView ` widget with a single + :class:`PlotItem ` inside. + + The following methods are wrapped directly from PlotItem: + :func:`addItem `, + :func:`removeItem `, + :func:`clear `, + :func:`setXRange `, + :func:`setYRange `, + :func:`setRange `, + :func:`autoRange `, + :func:`setXLink `, + :func:`setYLink `, + :func:`viewRect `, + :func:`setMouseEnabled `, + :func:`enableAutoRange `, + :func:`disableAutoRange `, + :func:`setAspectLocked `, + :func:`register `, + :func:`unregister ` + + + For all + other methods, use :func:`getPlotItem `. + """ def __init__(self, parent=None, **kargs): + """When initializing PlotWidget, all keyword arguments except *parent* are passed + to :func:`PlotItem.__init__() `.""" GraphicsView.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False) self.plotItem = PlotItem(**kargs) self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange']: + ## NOTE: If you change this list, update the documentation above as well. + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) - - #def __dtor__(self): - ##print "Called plotWidget sip destructor" - #self.quit() - - - #def quit(self): - + def close(self): self.plotItem.close() self.plotItem = None @@ -46,8 +68,8 @@ class PlotWidget(GraphicsView): m = getattr(self.plotItem, attr) if hasattr(m, '__call__'): return m - raise exceptions.NameError(attr) - + raise NameError(attr) + def viewRangeChanged(self, view, range): #self.emit(QtCore.SIGNAL('viewChanged'), *args) self.sigRangeChanged.emit(self, range) @@ -62,6 +84,7 @@ class PlotWidget(GraphicsView): return self.plotItem.restoreState(state) def getPlotItem(self): + """Return the PlotItem contained within.""" return self.plotItem diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py new file mode 100644 index 00000000..0f55e227 --- /dev/null +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['ProgressDialog'] +class ProgressDialog(QtGui.QProgressDialog): + """ + Extends QProgressDialog for use in 'with' statements. + + Example:: + + with ProgressDialog("Processing..", minVal, maxVal) as dlg: + # do stuff + dlg.setValue(i) ## could also use dlg += 1 + if dlg.wasCanceled(): + raise Exception("Processing canceled by user") + """ + def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False): + """ + ============== ================================================================ + **Arguments:** + labelText (required) + cancelText Text to display on cancel button, or None to disable it. + minimum + maximum + parent + wait Length of time (im ms) to wait before displaying dialog + busyCursor If True, show busy cursor until dialog finishes + disable If True, the progress dialog will not be displayed + and calls to wasCanceled() will always return False. + If ProgressDialog is entered from a non-gui thread, it will + always be disabled. + ============== ================================================================ + """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + self.disabled = disable or (not isGuiThread) + if self.disabled: + return + + noCancel = False + if cancelText is None: + cancelText = '' + noCancel = True + + self.busyCursor = busyCursor + + QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) + self.setMinimumDuration(wait) + self.setWindowModality(QtCore.Qt.WindowModal) + self.setValue(self.minimum()) + if noCancel: + self.setCancelButton(None) + + + def __enter__(self): + if self.disabled: + return self + if self.busyCursor: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + return self + + def __exit__(self, exType, exValue, exTrace): + if self.disabled: + return + if self.busyCursor: + QtGui.QApplication.restoreOverrideCursor() + self.setValue(self.maximum()) + + def __iadd__(self, val): + """Use inplace-addition operator for easy incrementing.""" + if self.disabled: + return self + self.setValue(self.value()+val) + return self + + + ## wrap all other functions to make sure they aren't being called from non-gui threads + + def setValue(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setValue(self, val) + + def setLabelText(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setLabelText(self, val) + + def setMaximum(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setMaximum(self, val) + + def setMinimum(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setMinimum(self, val) + + def wasCanceled(self): + if self.disabled: + return False + return QtGui.QProgressDialog.wasCanceled(self) + + def maximum(self): + if self.disabled: + return 0 + return QtGui.QProgressDialog.maximum(self) + + def minimum(self): + if self.disabled: + return 0 + return QtGui.QProgressDialog.minimum(self) + diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py new file mode 100644 index 00000000..ea5c98a0 --- /dev/null +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -0,0 +1,84 @@ +from pyqtgraph.Qt import QtCore, QtGui +try: + from pyqtgraph.Qt import QtOpenGL + HAVE_OPENGL = True +except ImportError: + HAVE_OPENGL = False + +import pyqtgraph.functions as fn +import numpy as np + +class RawImageWidget(QtGui.QWidget): + """ + Widget optimized for very fast video display. + Generally using an ImageItem inside GraphicsView is fast enough, + but if you need even more performance, this widget is about as fast as it gets (but only in unscaled mode). + """ + def __init__(self, parent=None, scaled=False): + """ + Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. + """ + QtGui.QWidget.__init__(self, parent=None) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) + self.scaled = scaled + self.opts = None + self.image = None + + def setImage(self, img, *args, **kargs): + """ + img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). + Extra arguments are sent to functions.makeARGB + """ + self.opts = (img, args, kargs) + self.image = None + self.update() + + def paintEvent(self, ev): + if self.opts is None: + return + if self.image is None: + argb, alpha = fn.makeARGB(self.opts[0], *self.opts[1], **self.opts[2]) + self.image = fn.makeQImage(argb, alpha) + self.opts = () + #if self.pixmap is None: + #self.pixmap = QtGui.QPixmap.fromImage(self.image) + p = QtGui.QPainter(self) + if self.scaled: + rect = self.rect() + ar = rect.width() / float(rect.height()) + imar = self.image.width() / float(self.image.height()) + if ar > imar: + rect.setWidth(int(rect.width() * imar/ar)) + else: + rect.setHeight(int(rect.height() * ar/imar)) + + p.drawImage(rect, self.image) + else: + p.drawImage(QtCore.QPointF(), self.image) + #p.drawPixmap(self.rect(), self.pixmap) + p.end() + +if HAVE_OPENGL: + class RawImageGLWidget(QtOpenGL.QGLWidget): + """ + Similar to RawImageWidget, but uses a GL widget to do all drawing. + Generally this will be about as fast as using GraphicsView + ImageItem, + but performance may vary on some platforms. + """ + def __init__(self, parent=None, scaled=False): + QtOpenGL.QGLWidget.__init__(self, parent=None) + self.scaled = scaled + self.image = None + + def setImage(self, img): + self.image = fn.makeQImage(img) + self.update() + + def paintEvent(self, ev): + if self.image is None: + return + p = QtGui.QPainter(self) + p.drawImage(self.rect(), self.image) + p.end() + + diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py new file mode 100644 index 00000000..3752a6bb --- /dev/null +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -0,0 +1,172 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.multiprocess as mp +import pyqtgraph as pg +from .GraphicsView import GraphicsView +import numpy as np +import mmap, tempfile, ctypes, atexit + +__all__ = ['RemoteGraphicsView'] + +class RemoteGraphicsView(QtGui.QWidget): + """ + Replacement for GraphicsView that does all scene management and rendering on a remote process, + while displaying on the local widget. + + GraphicsItems must be created by proxy to the remote process. + + """ + def __init__(self, parent=None, *args, **kwds): + self._img = None + self._imgReq = None + QtGui.QWidget.__init__(self) + self._proc = mp.QtProcess() + self.pg = self._proc._import('pyqtgraph') + self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS) + rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView') + self._view = rpgRemote.Renderer(*args, **kwds) + self._view._setProxyOptions(deferGetattr=True) + self.setFocusPolicy(self._view.focusPolicy()) + + shmFileName = self._view.shmFileName() + self.shmFile = open(shmFileName, 'r') + self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ) + + self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) + + for method in ['scene', 'setCentralItem']: + setattr(self, method, getattr(self._view, method)) + + def resizeEvent(self, ev): + ret = QtGui.QWidget.resizeEvent(self, ev) + self._view.resize(self.size(), _callSync='off') + return ret + + def remoteSceneChanged(self, data): + w, h, size = data + if self.shm.size != size: + self.shm.close() + self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) + self.shm.seek(0) + self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32) + self.update() + + def paintEvent(self, ev): + if self._img is None: + return + p = QtGui.QPainter(self) + p.drawImage(self.rect(), self._img, QtCore.QRect(0, 0, self._img.width(), self._img.height())) + p.end() + + def mousePressEvent(self, ev): + self._view.mousePressEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mousePressEvent(self, ev) + + def mouseReleaseEvent(self, ev): + self._view.mouseReleaseEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mouseReleaseEvent(self, ev) + + def mouseMoveEvent(self, ev): + self._view.mouseMoveEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + ev.accept() + return QtGui.QWidget.mouseMoveEvent(self, ev) + + def wheelEvent(self, ev): + self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), ev.orientation(), _callSync='off') + ev.accept() + return QtGui.QWidget.wheelEvent(self, ev) + + def keyEvent(self, ev): + if self._view.keyEvent(ev.type(), int(ev.modifiers()), text, autorep, count): + ev.accept() + return QtGui.QWidget.keyEvent(self, ev) + + + +class Renderer(GraphicsView): + + sceneRendered = QtCore.Signal(object) + + def __init__(self, *args, **kwds): + ## Create shared memory for rendered image + #fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR) + #os.write(fd, '\x00' * mmap.PAGESIZE) + self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') + self.shmFile.write('\x00' * mmap.PAGESIZE) + #fh.flush() + fd = self.shmFile.fileno() + self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) + atexit.register(self.close) + + GraphicsView.__init__(self, *args, **kwds) + self.scene().changed.connect(self.update) + self.img = None + self.renderTimer = QtCore.QTimer() + self.renderTimer.timeout.connect(self.renderView) + self.renderTimer.start(16) + + def close(self): + self.shm.close() + self.shmFile.close() + + def shmFileName(self): + return self.shmFile.name + + def update(self): + self.img = None + return GraphicsView.update(self) + + def resize(self, size): + oldSize = self.size() + GraphicsView.resize(self, size) + self.resizeEvent(QtGui.QResizeEvent(size, oldSize)) + self.update() + + def renderView(self): + if self.img is None: + ## make sure shm is large enough and get its address + size = self.width() * self.height() * 4 + if size > self.shm.size(): + self.shm.resize(size) + address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0)) + + ## render the scene directly to shared memory + self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32) + self.img.fill(0xffffffff) + p = QtGui.QPainter(self.img) + self.render(p, self.viewRect(), self.rect()) + p.end() + self.sceneRendered.emit((self.width(), self.height(), self.shm.size())) + + def mousePressEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods): + typ = QtCore.QEvent.Type(typ) + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) + + def wheelEvent(self, pos, gpos, d, btns, mods, ori): + btns = QtCore.Qt.MouseButtons(btns) + mods = QtCore.Qt.KeyboardModifiers(mods) + return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) + + def keyEvent(self, typ, mods, text, autorep, count): + typ = QtCore.QEvent.Type(typ) + mods = QtCore.Qt.KeyboardModifiers(mods) + GraphicsView.keyEvent(self, QtGui.QKeyEvent(typ, mods, text, autorep, count)) + return ev.accepted() + + + \ No newline at end of file diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py new file mode 100644 index 00000000..71695f4a --- /dev/null +++ b/pyqtgraph/widgets/SpinBox.py @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode +from pyqtgraph.SignalProxy import SignalProxy + +import pyqtgraph.functions as fn +from math import log +from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors +from decimal import * +import weakref + +__all__ = ['SpinBox'] +class SpinBox(QtGui.QAbstractSpinBox): + """ + **Bases:** QtGui.QAbstractSpinBox + + QSpinBox widget on steroids. Allows selection of numerical value, with extra features: + + - SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V") + - Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) + - Option for unbounded values + - Delayed signals (allows multiple rapid changes with only one change signal) + + ============================= ============================================== + **Signals:** + valueChanged(value) Same as QSpinBox; emitted every time the value + has changed. + sigValueChanged(self) Emitted when value has changed, but also combines + multiple rapid changes into one signal (eg, + when rolling the mouse wheel). + sigValueChanging(self, value) Emitted immediately for all value changes. + ============================= ============================================== + """ + + ## There's a PyQt bug that leaks a reference to the + ## QLineEdit returned from QAbstractSpinBox.lineEdit() + ## This makes it possible to crash the entire program + ## by making accesses to the LineEdit after the spinBox has been deleted. + ## I have no idea how to get around this.. + + + valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox + sigValueChanged = QtCore.Signal(object) # (self) + sigValueChanging = QtCore.Signal(object, object) # (self, value) sent immediately; no delay. + + def __init__(self, parent=None, value=0.0, **kwargs): + """ + ============== ======================================================================== + **Arguments:** + parent Sets the parent widget for this SpinBox (optional) + value (float/int) initial value + bounds (min,max) Minimum and maximum values allowed in the SpinBox. + Either may be None to leave the value unbounded. + suffix (str) suffix (units) to display after the numerical value + siPrefix (bool) If True, then an SI prefix is automatically prepended + to the units and the value is scaled accordingly. For example, + if value=0.003 and suffix='V', then the SpinBox will display + "300 mV" (but a call to SpinBox.value will still return 0.003). + step (float) The size of a single step. This is used when clicking the up/ + down arrows, when rolling the mouse wheel, or when pressing + keyboard arrows while the widget has keyboard focus. Note that + the interpretation of this value is different when specifying + the 'dec' argument. + dec (bool) If True, then the step value will be adjusted to match + the current size of the variable (for example, a value of 15 + might step in increments of 1 whereas a value of 1500 would + step in increments of 100). In this case, the 'step' argument + is interpreted *relative* to the current value. The most common + 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. + minStep (float) When dec=True, this specifies the minimum allowable step size. + int (bool) if True, the value is forced to integer type + decimals (int) Number of decimal values to display + ============== ======================================================================== + """ + QtGui.QAbstractSpinBox.__init__(self, parent) + self.lastValEmitted = None + self.lastText = '' + self.textValid = True ## If false, we draw a red border + self.setMinimumWidth(0) + self.setMaximumHeight(20) + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.opts = { + 'bounds': [None, None], + + ## Log scaling options #### Log mode is no longer supported. + #'step': 0.1, + #'minStep': 0.001, + #'log': True, + #'dec': False, + + ## decimal scaling option - example + #'step': 0.1, + #'minStep': .001, + #'log': False, + #'dec': True, + + ## normal arithmetic step + 'step': D('0.01'), ## if 'dec' is false, the spinBox steps by 'step' every time + ## if 'dec' is True, the step size is relative to the value + ## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True) + 'log': False, + 'dec': False, ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc. + ## if true, minStep must be set in order to cross zero. + + + 'int': False, ## Set True to force value to be integer + + 'suffix': '', + 'siPrefix': False, ## Set to True to display numbers with SI prefix (ie, 100pA instead of 1e-10A) + + 'delay': 0.3, ## delay sending wheel update signals for 300ms + + 'delayUntilEditFinished': True, ## do not send signals until text editing has finished + + ## for compatibility with QDoubleSpinBox and QSpinBox + 'decimals': 2, + + } + + self.decOpts = ['step', 'minStep'] + + self.val = D(asUnicode(value)) ## Value is precise decimal. Ordinary math not allowed. + self.updateText() + self.skipValidate = False + self.setCorrectionMode(self.CorrectToPreviousValue) + self.setKeyboardTracking(False) + self.setOpts(**kwargs) + + + self.editingFinished.connect(self.editingFinishedEvent) + self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) + + def event(self, ev): + ret = QtGui.QAbstractSpinBox.event(self, ev) + if ev.type() == QtCore.QEvent.KeyPress and ev.key() == QtCore.Qt.Key_Return: + ret = True ## For some reason, spinbox pretends to ignore return key press + return ret + + ##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap. + def setOpts(self, **opts): + """ + Changes the behavior of the SpinBox. Accepts most of the arguments + allowed in :func:`__init__ `. + + """ + #print opts + for k in opts: + if k == 'bounds': + #print opts[k] + self.setMinimum(opts[k][0], update=False) + self.setMaximum(opts[k][1], update=False) + #for i in [0,1]: + #if opts[k][i] is None: + #self.opts[k][i] = None + #else: + #self.opts[k][i] = D(unicode(opts[k][i])) + elif k in ['step', 'minStep']: + self.opts[k] = D(asUnicode(opts[k])) + elif k == 'value': + pass ## don't set value until bounds have been set + else: + self.opts[k] = opts[k] + if 'value' in opts: + self.setValue(opts['value']) + + ## If bounds have changed, update value to match + if 'bounds' in opts and 'value' not in opts: + self.setValue() + + ## sanity checks: + if self.opts['int']: + if 'step' in opts: + step = opts['step'] + ## not necessary.. + #if int(step) != step: + #raise Exception('Integer SpinBox must have integer step size.') + else: + self.opts['step'] = int(self.opts['step']) + + if 'minStep' in opts: + step = opts['minStep'] + if int(step) != step: + raise Exception('Integer SpinBox must have integer minStep size.') + else: + ms = int(self.opts.get('minStep', 1)) + if ms < 1: + ms = 1 + self.opts['minStep'] = ms + + if 'delay' in opts: + self.proxy.setDelay(opts['delay']) + + self.updateText() + + + + def setMaximum(self, m, update=True): + """Set the maximum allowed value (or None for no limit)""" + if m is not None: + m = D(asUnicode(m)) + self.opts['bounds'][1] = m + if update: + self.setValue() + + def setMinimum(self, m, update=True): + """Set the minimum allowed value (or None for no limit)""" + if m is not None: + m = D(asUnicode(m)) + self.opts['bounds'][0] = m + if update: + self.setValue() + + def setPrefix(self, p): + self.setOpts(prefix=p) + + def setRange(self, r0, r1): + self.setOpts(bounds = [r0,r1]) + + def setProperty(self, prop, val): + ## for QSpinBox compatibility + if prop == 'value': + #if type(val) is QtCore.QVariant: + #val = val.toDouble()[0] + self.setValue(val) + else: + print("Warning: SpinBox.setProperty('%s', ..) not supported." % prop) + + def setSuffix(self, suf): + self.setOpts(suffix=suf) + + def setSingleStep(self, step): + self.setOpts(step=step) + + def setDecimals(self, decimals): + self.setOpts(decimals=decimals) + + def value(self): + """ + Return the value of this SpinBox. + + """ + if self.opts['int']: + return int(self.val) + else: + return float(self.val) + + def setValue(self, value=None, update=True, delaySignal=False): + """ + Set the value of this spin. + If the value is out of bounds, it will be clipped to the nearest boundary. + If the spin is integer type, the value will be coerced to int. + Returns the actual value set. + + If value is None, then the current value is used (this is for resetting + the value after bounds, etc. have changed) + """ + + if value is None: + value = self.value() + + bounds = self.opts['bounds'] + if bounds[0] is not None and value < bounds[0]: + value = bounds[0] + if bounds[1] is not None and value > bounds[1]: + value = bounds[1] + + if self.opts['int']: + value = int(value) + + value = D(asUnicode(value)) + if value == self.val: + return + prev = self.val + + self.val = value + if update: + self.updateText(prev=prev) + + self.sigValueChanging.emit(self, float(self.val)) ## change will be emitted in 300ms if there are no subsequent changes. + if not delaySignal: + self.emitChanged() + + return value + + + def emitChanged(self): + self.lastValEmitted = self.val + self.valueChanged.emit(float(self.val)) + self.sigValueChanged.emit(self) + + def delayedChange(self): + try: + if self.val != self.lastValEmitted: + self.emitChanged() + except RuntimeError: + pass ## This can happen if we try to handle a delayed signal after someone else has already deleted the underlying C++ object. + + def widgetGroupInterface(self): + return (self.valueChanged, SpinBox.value, SpinBox.setValue) + + def sizeHint(self): + return QtCore.QSize(120, 0) + + + def stepEnabled(self): + return self.StepUpEnabled | self.StepDownEnabled + + #def fixup(self, *args): + #print "fixup:", args + + def stepBy(self, n): + n = D(int(n)) ## n must be integral number of steps. + s = [D(-1), D(1)][n >= 0] ## determine sign of step + val = self.val + + for i in range(abs(n)): + + if self.opts['log']: + raise Exception("Log mode no longer supported.") + # step = abs(val) * self.opts['step'] + # if 'minStep' in self.opts: + # step = max(step, self.opts['minStep']) + # val += step * s + if self.opts['dec']: + if val == 0: + step = self.opts['minStep'] + exp = None + else: + vs = [D(-1), D(1)][val >= 0] + #exp = D(int(abs(val*(D('1.01')**(s*vs))).log10())) + fudge = D('1.01')**(s*vs) ## fudge factor. at some places, the step size depends on the step sign. + exp = abs(val * fudge).log10().quantize(1, ROUND_FLOOR) + step = self.opts['step'] * D(10)**exp + if 'minStep' in self.opts: + step = max(step, self.opts['minStep']) + val += s * step + #print "Exp:", exp, "step", step, "val", val + else: + val += s*self.opts['step'] + + if 'minStep' in self.opts and abs(val) < self.opts['minStep']: + val = D(0) + self.setValue(val, delaySignal=True) ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only. + + + def valueInRange(self, value): + bounds = self.opts['bounds'] + if bounds[0] is not None and value < bounds[0]: + return False + if bounds[1] is not None and value > bounds[1]: + return False + if self.opts.get('int', False): + if int(value) != value: + return False + return True + + + def updateText(self, prev=None): + #print "Update text." + self.skipValidate = True + if self.opts['siPrefix']: + if self.val == 0 and prev is not None: + (s, p) = fn.siScale(prev) + txt = "0.0 %s%s" % (p, self.opts['suffix']) + else: + txt = fn.siFormat(float(self.val), suffix=self.opts['suffix']) + else: + txt = '%g%s' % (self.val , self.opts['suffix']) + self.lineEdit().setText(txt) + self.lastText = txt + self.skipValidate = False + + def validate(self, strn, pos): + if self.skipValidate: + #print "skip validate" + #self.textValid = False + ret = QtGui.QValidator.Acceptable + else: + try: + ## first make sure we didn't mess with the suffix + suff = self.opts.get('suffix', '') + if len(suff) > 0 and asUnicode(strn)[-len(suff):] != suff: + #print '"%s" != "%s"' % (unicode(strn)[-len(suff):], suff) + ret = QtGui.QValidator.Invalid + + ## next see if we actually have an interpretable value + else: + val = self.interpret() + if val is False: + #print "can't interpret" + #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') + #self.textValid = False + ret = QtGui.QValidator.Intermediate + else: + if self.valueInRange(val): + if not self.opts['delayUntilEditFinished']: + self.setValue(val, update=False) + #print " OK:", self.val + #self.setStyleSheet('') + #self.textValid = True + + ret = QtGui.QValidator.Acceptable + else: + ret = QtGui.QValidator.Intermediate + + except: + #print " BAD" + #import sys + #sys.excepthook(*sys.exc_info()) + #self.textValid = False + #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') + ret = QtGui.QValidator.Intermediate + + ## draw / clear border + if ret == QtGui.QValidator.Intermediate: + self.textValid = False + elif ret == QtGui.QValidator.Acceptable: + self.textValid = True + ## note: if text is invalid, we don't change the textValid flag + ## since the text will be forced to its previous state anyway + self.update() + + ## support 2 different pyqt APIs. Bleh. + if hasattr(QtCore, 'QString'): + return (ret, pos) + else: + return (ret, strn, pos) + + def paintEvent(self, ev): + QtGui.QAbstractSpinBox.paintEvent(self, ev) + + ## draw red border if text is invalid + if not self.textValid: + p = QtGui.QPainter(self) + p.setRenderHint(p.Antialiasing) + p.setPen(fn.mkPen((200,50,50), width=2)) + p.drawRoundedRect(self.rect().adjusted(2, 2, -2, -2), 4, 4) + p.end() + + + def interpret(self): + """Return value of text. Return False if text is invalid, raise exception if text is intermediate""" + strn = self.lineEdit().text() + suf = self.opts['suffix'] + if len(suf) > 0: + if strn[-len(suf):] != suf: + return False + #raise Exception("Units are invalid.") + strn = strn[:-len(suf)] + try: + val = fn.siEval(strn) + except: + #sys.excepthook(*sys.exc_info()) + #print "invalid" + return False + #print val + return val + + #def interpretText(self, strn=None): + #print "Interpret:", strn + #if strn is None: + #strn = self.lineEdit().text() + #self.setValue(siEval(strn), update=False) + ##QtGui.QAbstractSpinBox.interpretText(self) + + + def editingFinishedEvent(self): + """Edit has finished; set value.""" + #print "Edit finished." + if asUnicode(self.lineEdit().text()) == self.lastText: + #print "no text change." + return + try: + val = self.interpret() + except: + return + + if val is False: + #print "value invalid:", str(self.lineEdit().text()) + return + if val == self.val: + #print "no value change:", val, self.val + return + self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like + + #def textChanged(self): + #print "Text changed." + + +### Drop-in replacement for SpinBox; just for crash-testing +#class SpinBox(QtGui.QDoubleSpinBox): + #valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox + #sigValueChanged = QtCore.Signal(object) # (self) + #sigValueChanging = QtCore.Signal(object) # (value) + #def __init__(self, parent=None, *args, **kargs): + #QtGui.QSpinBox.__init__(self, parent) + + #def __getattr__(self, attr): + #return lambda *args, **kargs: None + + #def widgetGroupInterface(self): + #return (self.valueChanged, SpinBox.value, SpinBox.setValue) + diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py new file mode 100644 index 00000000..dc4f875b --- /dev/null +++ b/pyqtgraph/widgets/TableWidget.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.python2_3 import asUnicode + +import numpy as np +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +__all__ = ['TableWidget'] +class TableWidget(QtGui.QTableWidget): + """Extends QTableWidget with some useful functions for automatic data handling + and copy / export context menu. Can automatically format and display: + + - numpy arrays + - numpy record arrays + - metaarrays + - list-of-lists [[1,2,3], [4,5,6]] + - dict-of-lists {'x': [1,2,3], 'y': [4,5,6]} + - list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...] + """ + + def __init__(self, *args): + QtGui.QTableWidget.__init__(self, *args) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) + self.clear() + self.contextMenu = QtGui.QMenu() + self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) + self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll) + self.contextMenu.addAction('Save Selection').triggered.connect(self.saveSel) + self.contextMenu.addAction('Save All').triggered.connect(self.saveAll) + + def clear(self): + QtGui.QTableWidget.clear(self) + self.verticalHeadersSet = False + self.horizontalHeadersSet = False + self.items = [] + self.setRowCount(0) + self.setColumnCount(0) + + def setData(self, data): + self.clear() + self.appendData(data) + + def appendData(self, data): + """Types allowed: + 1 or 2D numpy array or metaArray + 1D numpy record array + list-of-lists, list-of-dicts or dict-of-lists + """ + fn0, header0 = self.iteratorFn(data) + if fn0 is None: + self.clear() + return + it0 = fn0(data) + try: + first = next(it0) + except StopIteration: + return + #if type(first) == type(np.float64(1)): + # return + fn1, header1 = self.iteratorFn(first) + if fn1 is None: + self.clear() + return + + #print fn0, header0 + #print fn1, header1 + firstVals = [x for x in fn1(first)] + self.setColumnCount(len(firstVals)) + + #print header0, header1 + if not self.verticalHeadersSet and header0 is not None: + #print "set header 0:", header0 + self.setRowCount(len(header0)) + self.setVerticalHeaderLabels(header0) + self.verticalHeadersSet = True + if not self.horizontalHeadersSet and header1 is not None: + #print "set header 1:", header1 + self.setHorizontalHeaderLabels(header1) + self.horizontalHeadersSet = True + + self.setRow(0, firstVals) + i = 1 + for row in it0: + self.setRow(i, [x for x in fn1(row)]) + i += 1 + + def iteratorFn(self, data): + """Return 1) a function that will provide an iterator for data and 2) a list of header strings""" + if isinstance(data, list): + return lambda d: d.__iter__(), None + elif isinstance(data, dict): + return lambda d: iter(d.values()), list(map(str, data.keys())) + elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + if data.axisHasColumns(0): + header = [str(data.columnName(0, i)) for i in range(data.shape[0])] + elif data.axisHasValues(0): + header = list(map(str, data.xvals(0))) + else: + header = None + return self.iterFirstAxis, header + elif isinstance(data, np.ndarray): + return self.iterFirstAxis, None + elif isinstance(data, np.void): + return self.iterate, list(map(str, data.dtype.names)) + elif data is None: + return (None,None) + else: + raise Exception("Don't know how to iterate over data type: %s" % str(type(data))) + + def iterFirstAxis(self, data): + for i in range(data.shape[0]): + yield data[i] + + def iterate(self, data): ## for numpy.void, which can be iterated but mysteriously has no __iter__ (??) + for x in data: + yield x + + def appendRow(self, data): + self.appendData([data]) + + def addRow(self, vals): + #print "add row:", vals + row = self.rowCount() + self.setRowCount(row+1) + self.setRow(row, vals) + + def setRow(self, row, vals): + if row > self.rowCount()-1: + self.setRowCount(row+1) + for col in range(self.columnCount()): + val = vals[col] + if isinstance(val, float) or isinstance(val, np.floating): + s = "%0.3g" % val + else: + s = str(val) + item = QtGui.QTableWidgetItem(s) + item.value = val + #print "add item to row %d:"%row, item, item.value + self.items.append(item) + self.setItem(row, col, item) + + def serialize(self, useSelection=False): + """Convert entire table (or just selected area) into tab-separated text values""" + if useSelection: + selection = self.selectedRanges()[0] + rows = list(range(selection.topRow(), selection.bottomRow()+1)) + columns = list(range(selection.leftColumn(), selection.rightColumn()+1)) + else: + rows = list(range(self.rowCount())) + columns = list(range(self.columnCount())) + + + data = [] + if self.horizontalHeadersSet: + row = [] + if self.verticalHeadersSet: + row.append(asUnicode('')) + + for c in columns: + row.append(asUnicode(self.horizontalHeaderItem(c).text())) + data.append(row) + + for r in rows: + row = [] + if self.verticalHeadersSet: + row.append(asUnicode(self.verticalHeaderItem(r).text())) + for c in columns: + item = self.item(r, c) + if item is not None: + row.append(asUnicode(item.value)) + else: + row.append(asUnicode('')) + data.append(row) + + s = '' + for row in data: + s += ('\t'.join(row) + '\n') + return s + + def copySel(self): + """Copy selected data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=True)) + + def copyAll(self): + """Copy all data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=False)) + + def saveSel(self): + """Save selected data to file.""" + self.save(self.serialize(useSelection=True)) + + def saveAll(self): + """Save all data to file.""" + self.save(self.serialize(useSelection=False)) + + def save(self, data): + fileName = QtGui.QFileDialog.getSaveFileName(self, "Save As..", "", "Tab-separated values (*.tsv)") + if fileName == '': + return + open(fileName, 'w').write(data) + + + def contextMenuEvent(self, ev): + self.contextMenu.popup(ev.globalPos()) + + def keyPressEvent(self, ev): + if ev.text() == 'c' and ev.modifiers() == QtCore.Qt.ControlModifier: + ev.accept() + self.copy() + else: + ev.ignore() + + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + t = TableWidget() + win.setCentralWidget(t) + win.resize(800,600) + win.show() + + ll = [[1,2,3,4,5]] * 20 + ld = [{'x': 1, 'y': 2, 'z': 3}] * 20 + dl = {'x': list(range(20)), 'y': list(range(20)), 'z': list(range(20))} + + a = np.ones((20, 5)) + ra = np.ones((20,), dtype=[('x', int), ('y', int), ('z', int)]) + + t.setData(ll) + + if HAVE_METAARRAY: + ma = metaarray.MetaArray(np.ones((20, 3)), info=[ + {'values': np.linspace(1, 5, 20)}, + {'cols': [ + {'name': 'x'}, + {'name': 'y'}, + {'name': 'z'}, + ]} + ]) + t.setData(ma) + diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py new file mode 100644 index 00000000..97fbe953 --- /dev/null +++ b/pyqtgraph/widgets/TreeWidget.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from weakref import * + +__all__ = ['TreeWidget', 'TreeWidgetItem'] +class TreeWidget(QtGui.QTreeWidget): + """Extends QTreeWidget to allow internal drag/drop with widgets in the tree. + Also maintains the expanded state of subtrees as they are moved. + This class demonstrates the absurd lengths one must go to to make drag/drop work.""" + + sigItemMoved = QtCore.Signal(object, object, object) # (item, parent, index) + + def __init__(self, parent=None): + QtGui.QTreeWidget.__init__(self, parent) + #self.itemWidgets = WeakKeyDictionary() + self.setAcceptDrops(True) + self.setDragEnabled(True) + self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) + self.placeholders = [] + self.childNestingLimit = None + + def setItemWidget(self, item, col, wid): + """ + Overrides QTreeWidget.setItemWidget such that widgets are added inside an invisible wrapper widget. + This makes it possible to move the item in and out of the tree without its widgets being automatically deleted. + """ + w = QtGui.QWidget() ## foster parent / surrogate child widget + l = QtGui.QVBoxLayout() + l.setContentsMargins(0,0,0,0) + w.setLayout(l) + w.setSizePolicy(wid.sizePolicy()) + w.setMinimumHeight(wid.minimumHeight()) + w.setMinimumWidth(wid.minimumWidth()) + l.addWidget(wid) + w.realChild = wid + self.placeholders.append(w) + QtGui.QTreeWidget.setItemWidget(self, item, col, w) + + def itemWidget(self, item, col): + w = QtGui.QTreeWidget.itemWidget(self, item, col) + if w is not None: + w = w.realChild + return w + + def dropMimeData(self, parent, index, data, action): + item = self.currentItem() + p = parent + #print "drop", item, "->", parent, index + while True: + if p is None: + break + if p is item: + return False + #raise Exception("Can not move item into itself.") + p = p.parent() + + if not self.itemMoving(item, parent, index): + return False + + currentParent = item.parent() + if currentParent is None: + currentParent = self.invisibleRootItem() + if parent is None: + parent = self.invisibleRootItem() + + if currentParent is parent and index > parent.indexOfChild(item): + index -= 1 + + self.prepareMove(item) + + currentParent.removeChild(item) + #print " insert child to index", index + parent.insertChild(index, item) ## index will not be correct + self.setCurrentItem(item) + + self.recoverMove(item) + #self.emit(QtCore.SIGNAL('itemMoved'), item, parent, index) + self.sigItemMoved.emit(item, parent, index) + return True + + def itemMoving(self, item, parent, index): + """Called when item has been dropped elsewhere in the tree. + Return True to accept the move, False to reject.""" + return True + + def prepareMove(self, item): + item.__widgets = [] + item.__expanded = item.isExpanded() + for i in range(self.columnCount()): + w = self.itemWidget(item, i) + item.__widgets.append(w) + if w is None: + continue + w.setParent(None) + for i in range(item.childCount()): + self.prepareMove(item.child(i)) + + def recoverMove(self, item): + for i in range(self.columnCount()): + w = item.__widgets[i] + if w is None: + continue + self.setItemWidget(item, i, w) + for i in range(item.childCount()): + self.recoverMove(item.child(i)) + + item.setExpanded(False) ## Items do not re-expand correctly unless they are collapsed first. + QtGui.QApplication.instance().processEvents() + item.setExpanded(item.__expanded) + + def collapseTree(self, item): + item.setExpanded(False) + for i in range(item.childCount()): + self.collapseTree(item.child(i)) + + def removeTopLevelItem(self, item): + for i in range(self.topLevelItemCount()): + if self.topLevelItem(i) is item: + self.takeTopLevelItem(i) + return + raise Exception("Item '%s' not in top-level items." % str(item)) + + def listAllItems(self, item=None): + items = [] + if item != None: + items.append(item) + else: + item = self.invisibleRootItem() + + for cindex in range(item.childCount()): + foundItems = self.listAllItems(item=item.child(cindex)) + for f in foundItems: + items.append(f) + return items + + def dropEvent(self, ev): + QtGui.QTreeWidget.dropEvent(self, ev) + self.updateDropFlags() + + + def updateDropFlags(self): + ### intended to put a limit on how deep nests of children can go. + ### self.childNestingLimit is upheld when moving items without children, but if the item being moved has children/grandchildren, the children/grandchildren + ### can end up over the childNestingLimit. + if self.childNestingLimit == None: + pass # enable drops in all items (but only if there are drops that aren't enabled? for performance...) + else: + items = self.listAllItems() + for item in items: + parentCount = 0 + p = item.parent() + while p is not None: + parentCount += 1 + p = p.parent() + if parentCount >= self.childNestingLimit: + item.setFlags(item.flags() & (~QtCore.Qt.ItemIsDropEnabled)) + else: + item.setFlags(item.flags() | QtCore.Qt.ItemIsDropEnabled) + + @staticmethod + def informTreeWidgetChange(item): + if hasattr(item, 'treeWidgetChanged'): + item.treeWidgetChanged() + else: + for i in xrange(item.childCount()): + TreeWidget.informTreeWidgetChange(item.child(i)) + + + def addTopLevelItem(self, item): + QtGui.QTreeWidget.addTopLevelItem(self, item) + self.informTreeWidgetChange(item) + + def addTopLevelItems(self, items): + QtGui.QTreeWidget.addTopLevelItems(self, items) + for item in items: + self.informTreeWidgetChange(item) + + def insertTopLevelItem(self, index, item): + QtGui.QTreeWidget.insertTopLevelItem(self, index, item) + self.informTreeWidgetChange(item) + + def insertTopLevelItems(self, index, items): + QtGui.QTreeWidget.insertTopLevelItems(self, index, items) + for item in items: + self.informTreeWidgetChange(item) + + def takeTopLevelItem(self, index): + item = self.topLevelItem(index) + if item is not None: + self.prepareMove(item) + item = QtGui.QTreeWidget.takeTopLevelItem(self, index) + self.prepareMove(item) + self.informTreeWidgetChange(item) + return item + + def topLevelItems(self): + return map(self.topLevelItem, xrange(self.topLevelItemCount())) + + def clear(self): + items = self.topLevelItems() + for item in items: + self.prepareMove(item) + QtGui.QTreeWidget.clear(self) + + ## Why do we want to do this? It causes RuntimeErrors. + #for item in items: + #self.informTreeWidgetChange(item) + + +class TreeWidgetItem(QtGui.QTreeWidgetItem): + """ + TreeWidgetItem that keeps track of its own widgets. + Widgets may be added to columns before the item is added to a tree. + """ + def __init__(self, *args): + QtGui.QTreeWidgetItem.__init__(self, *args) + self._widgets = {} # col: widget + self._tree = None + + + def setChecked(self, column, checked): + self.setCheckState(column, QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked) + + def setWidget(self, column, widget): + if column in self._widgets: + self.removeWidget(column) + self._widgets[column] = widget + tree = self.treeWidget() + if tree is None: + return + else: + tree.setItemWidget(self, column, widget) + + def removeWidget(self, column): + del self._widgets[column] + tree = self.treeWidget() + if tree is None: + return + tree.removeItemWidget(self, column) + + def treeWidgetChanged(self): + tree = self.treeWidget() + if self._tree is tree: + return + self._tree = self.treeWidget() + if tree is None: + return + for col, widget in self._widgets.items(): + tree.setItemWidget(self, col, widget) + + def addChild(self, child): + QtGui.QTreeWidgetItem.addChild(self, child) + TreeWidget.informTreeWidgetChange(child) + + def addChildren(self, childs): + QtGui.QTreeWidgetItem.addChildren(self, childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def insertChild(self, index, child): + QtGui.QTreeWidgetItem.insertChild(self, index, child) + TreeWidget.informTreeWidgetChange(child) + + def insertChildren(self, index, childs): + QtGui.QTreeWidgetItem.addChildren(self, index, childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def removeChild(self, child): + QtGui.QTreeWidgetItem.removeChild(self, child) + TreeWidget.informTreeWidgetChange(child) + + def takeChild(self, index): + child = QtGui.QTreeWidgetItem.takeChild(self, index) + TreeWidget.informTreeWidgetChange(child) + return child + + def takeChildren(self): + childs = QtGui.QTreeWidgetItem.takeChildren(self) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + return childs + + diff --git a/pyqtgraph/widgets/ValueLabel.py b/pyqtgraph/widgets/ValueLabel.py new file mode 100644 index 00000000..7f6fa84b --- /dev/null +++ b/pyqtgraph/widgets/ValueLabel.py @@ -0,0 +1,73 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.ptime import time +import pyqtgraph as pg +from functools import reduce + +__all__ = ['ValueLabel'] + +class ValueLabel(QtGui.QLabel): + """ + QLabel specifically for displaying numerical values. + Extends QLabel adding some extra functionality: + + - displaying units with si prefix + - built-in exponential averaging + """ + + def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None): + """ + ============ ================================================================================== + Arguments + suffix (str or None) The suffix to place after the value + siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value + averageTime (float) The length of time in seconds to average values. If this value + is 0, then no averaging is performed. As this value increases + the display value will appear to change more slowly and smoothly. + formatStr (str) Optionally, provide a format string to use when displaying text. The text + will be generated by calling formatStr.format(value=, avgValue=, suffix=) + (see Python documentation on str.format) + This option is not compatible with siPrefix + ============ ================================================================================== + """ + QtGui.QLabel.__init__(self, parent) + self.values = [] + self.averageTime = averageTime ## no averaging by default + self.suffix = suffix + self.siPrefix = siPrefix + if formatStr is None: + formatStr = '{avgValue:0.2g} {suffix}' + self.formatStr = formatStr + + def setValue(self, value): + now = time() + self.values.append((now, value)) + cutoff = now - self.averageTime + while len(self.values) > 0 and self.values[0][0] < cutoff: + self.values.pop(0) + self.update() + + def setFormatStr(self, text): + self.formatStr = text + self.update() + + def setAverageTime(self, t): + self.averageTime = t + + def averageValue(self): + return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values)) + + + def paintEvent(self, ev): + self.setText(self.generateText()) + return QtGui.QLabel.paintEvent(self, ev) + + def generateText(self): + if len(self.values) == 0: + return '' + avg = self.averageValue() + val = self.values[-1][1] + if self.siPrefix: + return pg.siFormat(avg, suffix=self.suffix) + else: + return self.formatStr.format(value=val, avgValue=avg, suffix=self.suffix) + diff --git a/pyqtgraph/widgets/VerticalLabel.py b/pyqtgraph/widgets/VerticalLabel.py new file mode 100644 index 00000000..fa45ae5d --- /dev/null +++ b/pyqtgraph/widgets/VerticalLabel.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['VerticalLabel'] +#class VerticalLabel(QtGui.QLabel): + #def paintEvent(self, ev): + #p = QtGui.QPainter(self) + #p.rotate(-90) + #self.hint = p.drawText(QtCore.QRect(-self.height(), 0, self.height(), self.width()), QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter, self.text()) + #p.end() + #self.setMinimumWidth(self.hint.height()) + #self.setMinimumHeight(self.hint.width()) + + #def sizeHint(self): + #if hasattr(self, 'hint'): + #return QtCore.QSize(self.hint.height(), self.hint.width()) + #else: + #return QtCore.QSize(16, 50) + +class VerticalLabel(QtGui.QLabel): + def __init__(self, text, orientation='vertical', forceWidth=True): + QtGui.QLabel.__init__(self, text) + self.forceWidth = forceWidth + self.orientation = None + self.setOrientation(orientation) + + def setOrientation(self, o): + if self.orientation == o: + return + self.orientation = o + self.update() + self.updateGeometry() + + 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)) + + #p.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) + + if self.orientation == 'vertical': + p.rotate(-90) + rgn = QtCore.QRect(-self.height(), 0, self.height(), self.width()) + else: + rgn = self.contentsRect() + align = self.alignment() + #align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter + + self.hint = p.drawText(rgn, align, self.text()) + p.end() + + if self.orientation == 'vertical': + self.setMaximumWidth(self.hint.height()) + self.setMinimumWidth(0) + self.setMaximumHeight(16777215) + if self.forceWidth: + self.setMinimumHeight(self.hint.width()) + else: + self.setMinimumHeight(0) + else: + self.setMaximumHeight(self.hint.height()) + self.setMinimumHeight(0) + self.setMaximumWidth(16777215) + if self.forceWidth: + self.setMinimumWidth(self.hint.width()) + else: + self.setMinimumWidth(0) + + def sizeHint(self): + if self.orientation == 'vertical': + if hasattr(self, 'hint'): + return QtCore.QSize(self.hint.height(), self.hint.width()) + else: + return QtCore.QSize(19, 50) + else: + if hasattr(self, 'hint'): + return QtCore.QSize(self.hint.width(), self.hint.height()) + else: + return QtCore.QSize(50, 19) + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + w = QtGui.QWidget() + l = QtGui.QGridLayout() + w.setLayout(l) + + l1 = VerticalLabel("text 1", orientation='horizontal') + l2 = VerticalLabel("text 2") + l3 = VerticalLabel("text 3") + l4 = VerticalLabel("text 4", orientation='horizontal') + l.addWidget(l1, 0, 0) + l.addWidget(l2, 1, 1) + l.addWidget(l3, 2, 2) + l.addWidget(l4, 3, 3) + win.setCentralWidget(w) + win.show() \ No newline at end of file diff --git a/pyqtgraph/widgets/__init__.py b/pyqtgraph/widgets/__init__.py new file mode 100644 index 00000000..a81fe391 --- /dev/null +++ b/pyqtgraph/widgets/__init__.py @@ -0,0 +1,21 @@ +## just import everything from sub-modules + +#import os + +#d = os.path.split(__file__)[0] +#files = [] +#for f in os.listdir(d): + #if os.path.isdir(os.path.join(d, f)): + #files.append(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.append(f[:-3]) + +#for modName in files: + #mod = __import__(modName, globals(), locals(), fromlist=['*']) + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + #print modName, k + #globals()[k] = getattr(mod, k) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..e4dc07cf --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +from distutils.core import setup +import os + +## generate list of all sub-packages +subdirs = [i[0].split(os.path.sep)[1:] for i in os.walk('./pyqtgraph') if '__init__.py' in i[2]] +subdirs = filter(lambda p: len(p) == 1 or p[1] != 'build', subdirs) +all_packages = ['.'.join(p) for p in subdirs] + ['pyqtgraph.examples'] + +setup(name='pyqtgraph', + version='', + description='Scientific Graphics and GUI Library for Python', + long_description="""\ +PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy. + +It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching, Qt's GraphicsView framework for 2D display, and OpenGL for 3D display. +""", + license='MIT', + url='http://www.pyqtgraph.org', + author='Luke Campagnola', + author_email='luke.campagnola@gmail.com', + packages=all_packages, + package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source + #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, + classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: User Interfaces", + ], + requires = [ + 'numpy', + 'scipy', + ], +) + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/svg.py b/tests/svg.py new file mode 100644 index 00000000..7c26833e --- /dev/null +++ b/tests/svg.py @@ -0,0 +1,70 @@ +""" +SVG export test +""" +import test +import pyqtgraph as pg +app = pg.mkQApp() + +class SVGTest(test.TestCase): + #def test_plotscene(self): + #pg.setConfigOption('foreground', (0,0,0)) + #w = pg.GraphicsWindow() + #w.show() + #p1 = w.addPlot() + #p2 = w.addPlot() + #p1.plot([1,3,2,3,1,6,9,8,4,2,3,5,3], pen={'color':'k'}) + #p1.setXRange(0,5) + #p2.plot([1,5,2,3,4,6,1,2,4,2,3,5,3], pen={'color':'k', 'cosmetic':False, 'width': 0.3}) + #app.processEvents() + #app.processEvents() + + #ex = pg.exporters.SVGExporter.SVGExporter(w.scene()) + #ex.export(fileName='test.svg') + + + def test_simple(self): + scene = pg.QtGui.QGraphicsScene() + #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) + #scene.addItem(rect) + #rect.setPos(20,20) + #rect.translate(50, 50) + #rect.rotate(30) + #rect.scale(0.5, 0.5) + + #rect1 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) + #rect1.setParentItem(rect) + #rect1.setFlag(rect1.ItemIgnoresTransformations) + #rect1.setPos(20, 20) + #rect1.scale(2,2) + + #el1 = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 100) + #el1.setParentItem(rect1) + ##grp = pg.ItemGroup() + #grp.setParentItem(rect) + #grp.translate(200,0) + ##grp.rotate(30) + + #rect2 = pg.QtGui.QGraphicsRectItem(0, 0, 100, 25) + #rect2.setFlag(rect2.ItemClipsChildrenToShape) + #rect2.setParentItem(grp) + #rect2.setPos(0,25) + #rect2.rotate(30) + #el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50) + #el.translate(10,-5) + #el.scale(0.5,2) + #el.setParentItem(rect2) + + grp2 = pg.ItemGroup() + scene.addItem(grp2) + grp2.scale(100,100) + + rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2) + rect3.setPen(pg.mkPen(width=1, cosmetic=False)) + grp2.addItem(rect3) + + ex = pg.exporters.SVGExporter.SVGExporter(scene) + ex.export(fileName='test.svg') + + +if __name__ == '__main__': + test.unittest.main() \ No newline at end of file diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 00000000..f24a7d42 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,8 @@ +import unittest +import os, sys +## make sure this instance of pyqtgraph gets imported first +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +## all tests should be defined with this class so we have the option to tweak it later. +class TestCase(unittest.TestCase): + pass \ No newline at end of file diff --git a/tools/py2exe.bat b/tools/py2exe.bat new file mode 100644 index 00000000..f2b070fb --- /dev/null +++ b/tools/py2exe.bat @@ -0,0 +1,10 @@ +rem +rem This is a simple windows batch file containing the commands needed to package +rem a program with pyqtgraph and py2exe. See the packaging tutorial at +rem http://luke.campagnola.me/code/pyqtgraph for more information. +rem + +rmdir /S /Q dist +rmdir /S /Q build +python .\py2exeSetupWindows.py py2exe --includes sip +pause diff --git a/tools/py2exeSetupWindows.py b/tools/py2exeSetupWindows.py new file mode 100644 index 00000000..086b5a4a --- /dev/null +++ b/tools/py2exeSetupWindows.py @@ -0,0 +1,26 @@ +""" +Example distutils setup script for packaging a program with +pyqtgraph and py2exe. See the packaging tutorial at +http://luke.campagnola.me/code/pyqtgraph for more information. +""" + +from distutils.core import setup +from glob import glob +import py2exe +import sys + +## This path must contain msvcm90.dll, msvcp90.dll, msvcr90.dll, and Microsoft.VC90.CRT.manifest +## (see http://www.py2exe.org/index.cgi/Tutorial) +dllpath = r'C:\Windows\WinSxS\x86_Microsoft.VC90.CRT...' + +sys.path.append(dllpath) +data_files = [ + ## Instruct setup to copy the needed DLL files into the build directory + ("Microsoft.VC90.CRT", glob(dllpath + r'\*.*')), +] + +setup( + data_files=data_files, + windows=['main.py'] , + options={"py2exe": {"excludes":["Tkconstants", "Tkinter", "tcl"]}} +) diff --git a/widgets.py b/widgets.py deleted file mode 100644 index 8516fefc..00000000 --- a/widgets.py +++ /dev/null @@ -1,1302 +0,0 @@ -# -*- coding: utf-8 -*- -""" -widgets.py - Interactive graphics items for GraphicsView (ROI widgets) -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -Implements a series of graphics items which display movable/scalable/rotatable shapes -for use as region-of-interest markers. ROI class automatically handles extraction -of array data from ImageItems. -""" - -from PyQt4 import QtCore, QtGui -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal -#from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor, arctan2 -import numpy as np -from numpy.linalg import norm -import scipy.ndimage as ndimage -from Point import * -from Transform import Transform -from math import cos, sin -import functions as fn -#from ObjectWorkaround import * - -def rectStr(r): - return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height()) - -# Multiple inheritance not allowed in PyQt. Retarded workaround: -#class QObjectWorkaround: - #def __init__(self): - #self._qObj_ = QtCore.QObject() - #def __getattr__(self, attr): - #if attr == '_qObj_': - #raise Exception("QObjectWorkaround not initialized!") - #return getattr(self._qObj_, attr) - #def connect(self, *args): - #return QtCore.QObject.connect(self._qObj_, *args) - - -class ROI(QtGui.QGraphicsObject): - """Generic region-of-interest widget. - Can be used for implementing many types of selection box with rotate/translate/scale handles.""" - - sigRegionChangeFinished = QtCore.Signal(object) - sigRegionChangeStarted = QtCore.Signal(object) - sigRegionChanged = QtCore.Signal(object) - - def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None): - #QObjectWorkaround.__init__(self) - QtGui.QGraphicsObject.__init__(self, parent) - pos = Point(pos) - size = Point(size) - self.aspectLocked = False - self.translatable = True - - if pen is None: - self.pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) - else: - self.pen = fn.mkPen(pen) - self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) - self.handles = [] - self.state = {'pos': pos, 'size': size, 'angle': angle} ## angle is in degrees for ease of Qt integration - self.lastState = None - self.setPos(pos) - #self.rotate(-angle * 180. / np.pi) - self.rotate(-angle) - self.setZValue(10) - self.isMoving = False - - self.handleSize = 5 - self.invertible = invertible - self.maxBounds = maxBounds - - self.snapSize = snapSize - self.translateSnap = translateSnap - self.rotateSnap = rotateSnap - self.scaleSnap = scaleSnap - self.setFlag(self.ItemIsSelectable, True) - - def getState(self): - return self.state.copy() - - def setState(self, state): - self.setPos(state['pos'], update=False) - self.setSize(state['size'], update=False) - self.setAngle(state['angle']) - - def setZValue(self, z): - QtGui.QGraphicsItem.setZValue(self, z) - for h in self.handles: - h['item'].setZValue(z+1) - - def sceneBounds(self): - return self.sceneTransform().mapRect(self.boundingRect()) - - def parentBounds(self): - return self.mapToParent(self.boundingRect()).boundingRect() - - def setPen(self, pen): - self.pen = pen - self.update() - - def setPos(self, pos, update=True): - #print "setPos() called." - pos = Point(pos) - self.state['pos'] = pos - QtGui.QGraphicsItem.setPos(self, pos) - if update: - self.updateHandles() - self.handleChange() - - def setSize(self, size, update=True): - size = Point(size) - self.prepareGeometryChange() - self.state['size'] = size - if update: - self.updateHandles() - self.handleChange() - - def setAngle(self, angle, update=True): - self.state['angle'] = angle - tr = QtGui.QTransform() - #tr.rotate(-angle * 180 / np.pi) - tr.rotate(angle) - self.setTransform(tr) - if update: - self.updateHandles() - self.handleChange() - - - def addTranslateHandle(self, pos, axes=None, item=None, name=None): - pos = Point(pos) - return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}) - - def addFreeHandle(self, pos, axes=None, item=None, name=None): - pos = Point(pos) - return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}) - - def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False): - pos = Point(pos) - center = Point(center) - info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect} - if pos.x() == center.x(): - info['xoff'] = True - if pos.y() == center.y(): - info['yoff'] = True - return self.addHandle(info) - - def addRotateHandle(self, pos, center, item=None, name=None): - pos = Point(pos) - center = Point(center) - return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}) - - def addScaleRotateHandle(self, pos, center, item=None, name=None): - pos = Point(pos) - center = Point(center) - if pos[0] != center[0] and pos[1] != center[1]: - raise Exception("Scale/rotate handles must have either the same x or y coordinate as their center point.") - return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}) - - def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None): - pos = Point(pos) - center = Point(center) - return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}) - - def addHandle(self, info): - if not info.has_key('item') or info['item'] is None: - #print "BEFORE ADD CHILD:", self.childItems() - h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) - #print "AFTER ADD CHILD:", self.childItems() - h.setPos(info['pos'] * self.state['size']) - info['item'] = h - else: - h = info['item'] - iid = len(self.handles) - h.connectROI(self, iid) - #h.mouseMoveEvent = lambda ev: self.pointMoveEvent(iid, ev) - #h.mousePressEvent = lambda ev: self.pointPressEvent(iid, ev) - #h.mouseReleaseEvent = lambda ev: self.pointReleaseEvent(iid, ev) - self.handles.append(info) - h.setZValue(self.zValue()+1) - #if self.isSelected(): - #h.show() - #else: - #h.hide() - return h - - def getLocalHandlePositions(self, index=None): - """Returns the position of a handle in ROI coordinates""" - if index == None: - positions = [] - for h in self.handles: - positions.append((h['name'], h['pos'])) - return positions - else: - return (self.handles[index]['name'], self.handles[index]['pos']) - - def getSceneHandlePositions(self, index = None): - if index == None: - positions = [] - for h in self.handles: - positions.append((h['name'], h['item'].scenePos())) - return positions - else: - return (self.handles[index]['name'], self.handles[index]['item'].scenePos()) - - - def mapSceneToParent(self, pt): - return self.mapToParent(self.mapFromScene(pt)) - - def setSelected(self, s): - QtGui.QGraphicsItem.setSelected(self, s) - #print "select", self, s - if s: - for h in self.handles: - h['item'].show() - else: - for h in self.handles: - h['item'].hide() - - def mousePressEvent(self, ev): - ## Bug: sometimes we get events we shouldn't. - p = ev.pos() - if not self.isMoving and not self.shape().contains(p): - ev.ignore() - return - if ev.button() == QtCore.Qt.LeftButton: - self.setSelected(True) - if self.translatable: - self.isMoving = True - self.preMoveState = self.getState() - self.cursorOffset = self.scenePos() - ev.scenePos() - #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - self.sigRegionChangeStarted.emit(self) - ev.accept() - elif ev.button() == QtCore.Qt.RightButton: - if self.isMoving: - ev.accept() - self.cancelMove() - else: - ev.ignore() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: - snap = None - if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): - snap = Point(self.snapSize, self.snapSize) - newPos = ev.scenePos() + self.cursorOffset - newPos = self.mapSceneToParent(newPos) - self.translate(newPos - self.pos(), snap=snap) - - def mouseReleaseEvent(self, ev): - if self.translatable: - self.isMoving = False - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) - - def cancelMove(self): - self.isMoving = False - self.setState(self.preMoveState) - - def pointPressEvent(self, pt, ev): - #print "press" - self.isMoving = True - self.preMoveState = self.getState() - - #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - self.sigRegionChangeStarted.emit(self) - #self.pressPos = self.mapFromScene(ev.scenePos()) - #self.pressHandlePos = self.handles[pt]['item'].pos() - - def pointReleaseEvent(self, pt, ev): - #print "release" - self.isMoving = False - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) - - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - return sc - - def updateHandles(self): - #print "update", self.handles - for h in self.handles: - #print " try", h - if h['item'] in self.childItems(): - p = h['pos'] - h['item'].setPos(h['pos'] * self.state['size']) - #else: - #print " Not child!", self.childItems() - - - def checkPointMove(self, pt, pos, modifiers): - return True - - def pointMoveEvent(self, pt, ev): - self.movePoint(pt, ev.scenePos(), ev.modifiers()) - - - def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): - #print "movePoint() called." - ## pos is the new position of the handle in scene coords, as requested by the handle. - - newState = self.stateCopy() - h = self.handles[pt] - #p0 = self.mapToScene(h['item'].pos()) - ## p0 is current (before move) position of handle in scene coords - p0 = self.mapToScene(h['pos'] * self.state['size']) - p1 = Point(pos) - - ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. - p0 = self.mapSceneToParent(p0) - p1 = self.mapSceneToParent(p1) - - ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) - if h.has_key('center'): - c = h['center'] - cs = c * self.state['size'] - #lpOrig = h['pos'] - - #lp0 = self.mapFromScene(p0) - cs - #lp1 = self.mapFromScene(p1) - cs - lp0 = self.mapFromParent(p0) - cs - lp1 = self.mapFromParent(p1) - cs - - if h['type'] == 't': - #p0 = Point(self.mapToScene(h['item'].pos())) - #p1 = Point(pos + self.mapToScene(self.pressHandlePos) - self.mapToScene(self.pressPos)) - snap = None - if self.translateSnap or (modifiers & QtCore.Qt.ControlModifier): - snap = Point(self.snapSize, self.snapSize) - self.translate(p1-p0, snap=snap, update=False) - - elif h['type'] == 'f': - h['item'].setPos(self.mapFromScene(pos)) - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) - - elif h['type'] == 's': - #c = h['center'] - #cs = c * self.state['size'] - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - - ## If a handle and its center have the same x or y value, we can't scale across that axis. - if h['center'][0] == h['pos'][0]: - lp1[0] = 0 - if h['center'][1] == h['pos'][1]: - lp1[1] = 0 - - ## snap - if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): - lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize - lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize - - ## preserve aspect ratio (this can override snapping) - if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier): - #arv = Point(self.preMoveState['size']) - - lp1 = lp1.proj(lp0) - - ## determine scale factors and new size of ROI - hs = h['pos'] - c - if hs[0] == 0: - hs[0] = 1 - if hs[1] == 0: - hs[1] = 1 - newSize = lp1 / hs - - ## Perform some corrections and limit checks - if newSize[0] == 0: - newSize[0] = newState['size'][0] - if newSize[1] == 0: - newSize[1] = newState['size'][1] - if not self.invertible: - if newSize[0] < 0: - newSize[0] = newState['size'][0] - if newSize[1] < 0: - newSize[1] = newState['size'][1] - if self.aspectLocked: - newSize[0] = newSize[1] - - ## Move ROI so the center point occupies the same scene location after the scale - s0 = c * self.state['size'] - s1 = c * newSize - cc = self.mapToParent(s0 - s1) - self.mapToParent(Point(0, 0)) - - ## update state, do more boundary checks - newState['size'] = newSize - newState['pos'] = newState['pos'] + cc - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - - self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState - - ## move handles to their new locations - self.updateHandles() - - elif h['type'] in ['r', 'rf']: - ## If the handle is directly over its center point, we can't compute an angle. - if lp1.length() == 0 or lp0.length() == 0: - return - - ## determine new rotation angle, constrained if necessary - ang = newState['angle'] - lp0.angle(lp1) - if ang is None: ## this should never happen.. - return - if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / 15.) * 15. ## 180/12 = 15 - - ## create rotation transform - tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) - tr.rotate(ang) - - ## mvoe ROI so that center point remains stationary after rotate - cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) - newState['angle'] = ang - newState['pos'] = newState['pos'] + cc - - ## check boundaries, update - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - self.setTransform(tr) - self.setPos(newState['pos'], update=False) - self.state = newState - - ## If this is a free-rotate handle, its distance from the center may change. - - if h['type'] == 'rf': - h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle - - - #elif h['type'] == 'rf': - ### If the handle is directly over its center point, we can't compute an angle. - #if lp1.length() == 0 or lp0.length() == 0: - #return - - ### determine new rotation angle, constrained if necessary - #pos = Point(pos) - #ang = newState['angle'] + lp0.angle(lp1) - #if ang is None: - ##h['item'].setPos(self.mapFromScene(Point(pos[0], 0.0))) ## changes ROI coordinates of handle - #h['item'].setPos(self.mapFromScene(pos)) - #return - #if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - #ang = round(ang / (np.pi/12.)) * (np.pi/12.) - - - #tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) - - #cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) - #newState['angle'] = ang - #newState['pos'] = newState['pos'] + cc - #if self.maxBounds is not None: - #r = self.stateRect(newState) - #if not self.maxBounds.contains(r): - #return - #self.setTransform(tr) - #self.setPos(newState['pos'], update=False) - #self.state = newState - - #h['item'].setPos(self.mapFromScene(pos)) ## changes ROI coordinates of handle - ##self.emit(QtCore.SIGNAL('regionChanged'), self) - - elif h['type'] == 'sr': - #newState = self.stateCopy() - if h['center'][0] == h['pos'][0]: - scaleAxis = 1 - else: - scaleAxis = 0 - - #c = h['center'] - #cs = c * self.state['size'] - #p0 = Point(h['item'].pos()) - cs - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - if lp1.length() == 0 or lp0.length() == 0: - return - - ang = newState['angle'] - lp0.angle(lp1) - if ang is None: - return - if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - #ang = round(ang / (np.pi/12.)) * (np.pi/12.) - ang = round(ang / 15.) * 15. - - hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) - newState['size'][scaleAxis] = lp1.length() / hs - if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): - newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize - if newState['size'][scaleAxis] == 0: - newState['size'][scaleAxis] = 1 - - c1 = c * newState['size'] - tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) - tr.rotate(ang) - - cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) - newState['angle'] = ang - newState['pos'] = newState['pos'] + cc - if self.maxBounds is not None: - r = self.stateRect(newState) - if not self.maxBounds.contains(r): - return - self.setTransform(tr) - self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState - - self.updateHandles() - - self.handleChange() - - def handleChange(self): - """The state of the ROI has changed; redraw if needed.""" - #print "handleChange() called." - changed = False - #print "self.lastState:", self.lastState - if self.lastState is None: - changed = True - else: - for k in self.state.keys(): - #print k, self.state[k], self.lastState[k] - if self.state[k] != self.lastState[k]: - #print "state %s has changed; emit signal" % k - changed = True - self.lastState = self.stateCopy() - #print "changed =", changed - if changed: - #print "handle changed." - self.update() - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) - - - def scale(self, s, center=[0,0]): - c = self.mapToScene(Point(center) * self.state['size']) - self.prepareGeometryChange() - self.state['size'] = self.state['size'] * s - c1 = self.mapToScene(Point(center) * self.state['size']) - self.state['pos'] = self.state['pos'] + c - c1 - self.setPos(self.state['pos']) - self.updateHandles() - - - def translate(self, *args, **kargs): - """accepts either (x, y, snap) or ([x,y], snap) as arguments""" - if 'snap' not in kargs: - snap = None - else: - snap = kargs['snap'] - - if len(args) == 1: - pt = args[0] - else: - pt = args - - newState = self.stateCopy() - newState['pos'] = newState['pos'] + pt - if snap != None: - newState['pos'][0] = round(newState['pos'][0] / snap[0]) * snap[0] - newState['pos'][1] = round(newState['pos'][1] / snap[1]) * snap[1] - - - #d = ev.scenePos() - self.mapToScene(self.pressPos) - if self.maxBounds is not None: - r = self.stateRect(newState) - #r0 = self.sceneTransform().mapRect(self.boundingRect()) - d = Point(0,0) - if self.maxBounds.left() > r.left(): - d[0] = self.maxBounds.left() - r.left() - elif self.maxBounds.right() < r.right(): - d[0] = self.maxBounds.right() - r.right() - if self.maxBounds.top() > r.top(): - d[1] = self.maxBounds.top() - r.top() - elif self.maxBounds.bottom() < r.bottom(): - d[1] = self.maxBounds.bottom() - r.bottom() - newState['pos'] += d - - self.state['pos'] = newState['pos'] - self.setPos(self.state['pos']) - #if 'update' not in kargs or kargs['update'] is True: - self.handleChange() - - def stateRect(self, state): - r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) - tr = QtGui.QTransform() - #tr.rotate(-state['angle'] * 180 / np.pi) - tr.rotate(-state['angle']) - r = tr.mapRect(r) - return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) - - def boundingRect(self): - return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]) - - def paint(self, p, opt, widget): - p.save() - r = self.boundingRect() - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - p.translate(r.left(), r.top()) - p.scale(r.width(), r.height()) - p.drawRect(0, 0, 1, 1) - p.restore() - - def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): - """Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. - Also returns the transform which maps the ROI into data coordinates. - - If returnSlice is set to False, the function returns a pair of tuples with the values that would have - been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))""" - #print "getArraySlice" - - ## Determine shape of array along ROI axes - dShape = (data.shape[axes[0]], data.shape[axes[1]]) - #print " dshape", dShape - - ## Determine transform that maps ROI bounding box to image coordinates - tr = self.sceneTransform() * img.sceneTransform().inverted()[0] - - ## Modify transform to scale from image coords to data coords - #m = QtGui.QTransform() - tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) - #tr = tr * m - - ## Transform ROI bounds into data bounds - dataBounds = tr.mapRect(self.boundingRect()) - #print " boundingRect:", self.boundingRect() - #print " dataBounds:", dataBounds - - ## Intersect transformed ROI bounds with data bounds - intBounds = dataBounds.intersect(QtCore.QRectF(0, 0, dShape[0], dShape[1])) - #print " intBounds:", intBounds - - ## Determine index values to use when referencing the array. - bounds = ( - (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), - (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) - ) - #print " bounds:", bounds - - if returnSlice: - ## Create slice objects - sl = [slice(None)] * data.ndim - sl[axes[0]] = slice(*bounds[0]) - sl[axes[1]] = slice(*bounds[1]) - return tuple(sl), tr - else: - return bounds, tr - - - def getArrayRegion(self, data, img, axes=(0,1)): - shape = self.state['size'] - - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) - - ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin - - lvx = np.sqrt(vx.x()**2 + vx.y()**2) - lvy = np.sqrt(vy.x()**2 + vy.y()**2) - pxLen = img.width() / data.shape[axes[0]] - sx = pxLen / lvx - sy = pxLen / lvy - - vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) - shape = self.state['size'] - shape = [abs(shape[0]/sx), abs(shape[1]/sy)] - - origin = (origin.x(), origin.y()) - - #print "shape", shape, "vectors", vectors, "origin", origin - - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, order=1) - - ### transpose data so x and y are the first 2 axes - #trAx = range(0, data.ndim) - #trAx.remove(axes[0]) - #trAx.remove(axes[1]) - #tr1 = tuple(axes) + tuple(trAx) - #arr = data.transpose(tr1) - - ### Determine the minimal area of the data we will need - #(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes) - - ### Pad data boundaries by 1px if possible - #dataBounds = ( - #(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])), - #(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1])) - #) - - ### Extract minimal data from array - #arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]] - - ### Update roiDataTransform to reflect this extraction - #roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0]) - #### (roiDataTransform now maps from ROI coords to extracted data coords) - - - ### Rotate array - #if abs(self.state['angle']) > 1e-5: - #arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1) - - ### update data transforms to reflect this rotation - #rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi) - #roiDataTransform *= rot - - ### The rotation also causes a shift which must be accounted for: - #dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1]) - #rotBound = rot.mapRect(dataBound) - #roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top()) - - #else: - #arr2 = arr1 - - - - #### Shift off partial pixels - ## 1. map ROI into current data space - #roiBounds = roiDataTransform.mapRect(self.boundingRect()) - - ## 2. Determine amount to shift data - #shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom()) - #if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6: - ## 3. pad array with 0s before shifting - #arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype) - #arr2a[1:-1, 1:-1] = arr2 - - ## 4. shift array and udpate transforms - #arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1) - #roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1]) - #else: - #arr3 = arr2 - - - #### Extract needed region from rotated/shifted array - ## 1. map ROI into current data space (round these values off--they should be exact integer values at this point) - #roiBounds = roiDataTransform.mapRect(self.boundingRect()) - ##print self, roiBounds.height() - ##import traceback - ##traceback.print_stack() - - #roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height())) - - ##2. intersect ROI with data bounds - #dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1])) - - ##3. Extract data from array - #db = dataBounds - #bounds = ( - #(db.left(), db.right()+1), - #(db.top(), db.bottom()+1) - #) - #arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]] - - #### Create zero array in size of ROI - #arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype) - - ### Fill array with ROI data - #orig = Point(dataBounds.topLeft() - roiBounds.topLeft()) - #subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]] - #subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]] - - - ### figure out the reverse transpose order - #tr2 = np.array(tr1) - #for i in range(0, len(tr2)): - #tr2[tr1[i]] = i - #tr2 = tuple(tr2) - - ### Untranspose array before returning - #return arr5.transpose(tr2) - - def getGlobalTransform(self, relativeTo=None): - """Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn't specified, - then we use the state of the ROI when mouse is pressed.""" - if relativeTo == None: - relativeTo = self.preMoveState - st = self.getState() - - ## this is only allowed because we will be comparing the two - relativeTo['scale'] = relativeTo['size'] - st['scale'] = st['size'] - - - - t1 = Transform(relativeTo) - t2 = Transform(st) - return t2/t1 - - - #st = self.getState() - - ### rotation - #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358 - #rot = QtGui.QTransform() - #rot.rotate(-ang) - - ### We need to come up with a universal transformation--one that can be applied to other objects - ### such that all maintain alignment. - ### More specifically, we need to turn the ROI's position and angle into - ### a rotation _around the origin_ and a translation. - - #p0 = Point(relativeTo['pos']) - - ### base position, rotated - #p1 = rot.map(p0) - - #trans = Point(st['pos']) - p1 - #return trans, ang - - def applyGlobalTransform(self, tr): - st = self.getState() - - st['scale'] = st['size'] - st = Transform(st) - st = (st * tr).saveState() - st['size'] = st['scale'] - self.setState(st) - - -class Handle(QtGui.QGraphicsItem): - - types = { ## defines number of sides, start angle for each handle type - 't': (4, np.pi/4), - 'f': (4, np.pi/4), - 's': (4, 0), - 'r': (12, 0), - 'sr': (12, 0), - 'rf': (12, 0), - } - - def __init__(self, radius, typ=None, pen=QtGui.QPen(QtGui.QColor(200, 200, 220)), parent=None): - #print " create item with parent", parent - self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) - QtGui.QGraphicsItem.__init__(self, parent) - self.setFlag(self.ItemIgnoresTransformations) - self.setZValue(11) - self.roi = [] - self.radius = radius - self.typ = typ - self.prepareGeometryChange() - self.pen = pen - self.pen.setWidth(0) - self.pen.setCosmetic(True) - self.isMoving = False - self.sides, self.startAng = self.types[typ] - self.buildPath() - - def connectROI(self, roi, i): - self.roi.append((roi, i)) - - def boundingRect(self): - return self.bounds - - def mousePressEvent(self, ev): - # Bug: sometimes we get events not meant for us! - p = ev.pos() - if not self.isMoving and not self.path.contains(p): - ev.ignore() - return - - #print "handle press" - if ev.button() == QtCore.Qt.LeftButton: - self.isMoving = True - self.cursorOffset = self.scenePos() - ev.scenePos() - for r in self.roi: - r[0].pointPressEvent(r[1], ev) - #print " accepted." - ev.accept() - elif ev.button() == QtCore.Qt.RightButton: - if self.isMoving: - self.isMoving = False ## prevents any further motion - for r in self.roi: - r[0].cancelMove() - ev.accept() - else: - ev.ignore() - else: - ev.ignore() - - - def mouseReleaseEvent(self, ev): - #print "release" - if ev.button() == QtCore.Qt.LeftButton: - self.isMoving = False - for r in self.roi: - r[0].pointReleaseEvent(r[1], ev) - - def mouseMoveEvent(self, ev): - #print "handle mouseMove", ev.pos() - if self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: - pos = ev.scenePos() + self.cursorOffset - self.movePoint(pos, ev.modifiers()) - - def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): - for r in self.roi: - if not r[0].checkPointMove(r[1], pos, modifiers): - return - #print "point moved; inform %d ROIs" % len(self.roi) - # A handle can be used by multiple ROIs; tell each to update its handle position - for r in self.roi: - r[0].movePoint(r[1], pos, modifiers) - - def buildPath(self): - size = self.radius - self.path = QtGui.QPainterPath() - ang = self.startAng - dt = 2*np.pi / self.sides - for i in range(0, self.sides+1): - x = size * cos(ang) - y = size * sin(ang) - ang += dt - if i == 0: - self.path.moveTo(x, y) - else: - self.path.lineTo(x, y) - - def paint(self, p, opt, widget): - ## determine rotation of transform - m = self.sceneTransform() - #mi = m.inverted()[0] - v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) - va = np.arctan2(v.y(), v.x()) - - ## Determine length of unit vector in painter's coords - #size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) - #size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 - size = self.radius - - bounds = QtCore.QRectF(-size, -size, size*2, size*2) - if bounds != self.bounds: - self.bounds = bounds - self.prepareGeometryChange() - p.setRenderHints(p.Antialiasing, True) - p.setPen(self.pen) - - p.rotate(va * 180. / 3.1415926) - p.drawPath(self.path) - - #ang = self.startAng + va - #dt = 2*np.pi / self.sides - #for i in range(0, self.sides): - #x1 = size * cos(ang) - #y1 = size * sin(ang) - #x2 = size * cos(ang+dt) - #y2 = size * sin(ang+dt) - #ang += dt - #p.drawLine(Point(x1, y1), Point(x2, y2)) - - - - - -class TestROI(ROI): - def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) - ROI.__init__(self, pos, size, **args) - #self.addTranslateHandle([0, 0]) - self.addTranslateHandle([0.5, 0.5]) - self.addScaleHandle([1, 1], [0, 0]) - self.addScaleHandle([0, 0], [1, 1]) - self.addScaleRotateHandle([1, 0.5], [0.5, 0.5]) - self.addScaleHandle([0.5, 1], [0.5, 0.5]) - self.addRotateHandle([1, 0], [0, 0]) - self.addRotateHandle([0, 1], [1, 1]) - - - -class RectROI(ROI): - def __init__(self, pos, size, centered=False, sideScalers=False, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) - ROI.__init__(self, pos, size, **args) - if centered: - center = [0.5, 0.5] - else: - center = [0, 0] - - #self.addTranslateHandle(center) - self.addScaleHandle([1, 1], center) - if sideScalers: - self.addScaleHandle([1, 0.5], [center[0], 0.5]) - self.addScaleHandle([0.5, 1], [0.5, center[1]]) - -class LineROI(ROI): - def __init__(self, pos1, pos2, width, **args): - pos1 = Point(pos1) - pos2 = Point(pos2) - d = pos2-pos1 - l = d.length() - ang = Point(1, 0).angle(d) - ra = ang * np.pi / 180. - c = Point(-width/2. * sin(ra), -width/2. * cos(ra)) - pos1 = pos1 + c - - ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args) - self.addScaleRotateHandle([0, 0.5], [1, 0.5]) - self.addScaleRotateHandle([1, 0.5], [0, 0.5]) - self.addScaleHandle([0.5, 1], [0.5, 0.5]) - - -class MultiLineROI(QtGui.QGraphicsObject): - - sigRegionChangeFinished = QtCore.Signal(object) - sigRegionChangeStarted = QtCore.Signal(object) - sigRegionChanged = QtCore.Signal(object) - - def __init__(self, points, width, pen=None, **args): - QtGui.QGraphicsObject.__init__(self) - self.pen = pen - self.roiArgs = args - if len(points) < 2: - raise Exception("Must start with at least 2 points") - self.lines = [] - self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args)) - self.lines[-1].addScaleHandle([0.5, 1], [0.5, 0.5]) - h = self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5]) - h.movePoint(points[0]) - h.movePoint(points[0]) - for i in range(1, len(points)): - h = self.lines[-1].addScaleRotateHandle([1, 0.5], [0, 0.5]) - if i < len(points)-1: - self.lines.append(ROI([0, 0], [1, 5], parent=self, pen=pen, **args)) - self.lines[-1].addScaleRotateHandle([0, 0.5], [1, 0.5], item=h) - h.movePoint(points[i]) - h.movePoint(points[i]) - - for l in self.lines: - l.translatable = False - #self.addToGroup(l) - #l.connect(l, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) - l.sigRegionChanged.connect(self.roiChangedEvent) - #l.connect(l, QtCore.SIGNAL('regionChangeStarted'), self.roiChangeStartedEvent) - l.sigRegionChangeStarted.connect(self.roiChangeStartedEvent) - #l.connect(l, QtCore.SIGNAL('regionChangeFinished'), self.roiChangeFinishedEvent) - l.sigRegionChangeFinished.connect(self.roiChangeFinishedEvent) - - def paint(self, *args): - pass - - def boundingRect(self): - return QtCore.QRectF() - - def roiChangedEvent(self): - w = self.lines[0].state['size'][1] - for l in self.lines[1:]: - w0 = l.state['size'][1] - l.scale([1.0, w/w0], center=[0.5,0.5]) - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) - - def roiChangeStartedEvent(self): - #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - self.sigRegionChangeStarted.emit(self) - - def roiChangeFinishedEvent(self): - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) - - - def getArrayRegion(self, arr, img=None): - rgns = [] - for l in self.lines: - rgn = l.getArrayRegion(arr, img) - if rgn is None: - continue - #return None - rgns.append(rgn) - #print l.state['size'] - #print [(r.shape) for r in rgns] - return np.vstack(rgns) - - -class EllipseROI(ROI): - def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) - ROI.__init__(self, pos, size, **args) - self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) - self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) - - def paint(self, p, opt, widget): - r = self.boundingRect() - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - - p.scale(r.width(), r.height())## workaround for GL bug - r = QtCore.QRectF(r.x()/r.width(), r.y()/r.height(), 1,1) - - p.drawEllipse(r) - - def getArrayRegion(self, arr, img=None): - arr = ROI.getArrayRegion(self, arr, img) - if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: - return None - w = arr.shape[0] - h = arr.shape[1] - ## generate an ellipsoidal mask - mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) - - return arr * mask - - def shape(self): - self.path = QtGui.QPainterPath() - self.path.addEllipse(self.boundingRect()) - return self.path - - -class CircleROI(EllipseROI): - def __init__(self, pos, size, **args): - ROI.__init__(self, pos, size, **args) - self.aspectLocked = True - #self.addTranslateHandle([0.5, 0.5]) - self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) - -class PolygonROI(ROI): - def __init__(self, positions, pos=None, **args): - if pos is None: - pos = [0,0] - ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) - for p in positions: - self.addFreeHandle(p) - self.setZValue(1000) - - def listPoints(self): - return [p['item'].pos() for p in self.handles] - - def movePoint(self, *args, **kargs): - ROI.movePoint(self, *args, **kargs) - self.prepareGeometryChange() - for h in self.handles: - h['pos'] = h['item'].pos() - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - for i in range(len(self.handles)): - h1 = self.handles[i]['item'].pos() - h2 = self.handles[i-1]['item'].pos() - p.drawLine(h1, h2) - - def boundingRect(self): - r = QtCore.QRectF() - for h in self.handles: - r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs - return r - - def shape(self): - p = QtGui.QPainterPath() - p.moveTo(self.handles[0]['item'].pos()) - for i in range(len(self.handles)): - p.lineTo(self.handles[i]['item'].pos()) - return p - - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - #sc['handles'] = self.handles - return sc - - -class LineSegmentROI(ROI): - def __init__(self, positions, pos=None, **args): - if pos is None: - pos = [0,0] - ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) - for p in positions: - self.addFreeHandle(p) - self.setZValue(1000) - - def listPoints(self): - return [p['item'].pos() for p in self.handles] - - def movePoint(self, *args, **kargs): - ROI.movePoint(self, *args, **kargs) - self.prepareGeometryChange() - for h in self.handles: - h['pos'] = h['item'].pos() - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - p.setPen(self.pen) - for i in range(len(self.handles)-1): - h1 = self.handles[i]['item'].pos() - h2 = self.handles[i-1]['item'].pos() - p.drawLine(h1, h2) - - def boundingRect(self): - r = QtCore.QRectF() - for h in self.handles: - r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs - return r - - def shape(self): - p = QtGui.QPainterPath() - p.moveTo(self.handles[0]['item'].pos()) - for i in range(len(self.handles)): - p.lineTo(self.handles[i]['item'].pos()) - return p - - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - #sc['handles'] = self.handles - return sc - - - -class SpiralROI(ROI): - def __init__(self, pos=None, size=None, **args): - if size == None: - size = [100e-6,100e-6] - if pos == None: - pos = [0,0] - ROI.__init__(self, pos, size, **args) - self.translateSnap = False - self.addFreeHandle([0.25,0], name='a') - self.addRotateFreeHandle([1,0], [0,0], name='r') - #self.getRadius() - #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self. - - - def getRadius(self): - radius = Point(self.handles[1]['item'].pos()).length() - #r2 = radius[1] - #r3 = r2[0] - return radius - - def boundingRect(self): - r = self.getRadius() - return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) - #return self.bounds - - def movePoint(self, *args, **kargs): - ROI.movePoint(self, *args, **kargs) - self.prepareGeometryChange() - for h in self.handles: - h['pos'] = h['item'].pos()/self.state['size'][0] - - def handleChange(self): - ROI.handleChange(self) - if len(self.handles) > 1: - self.path = QtGui.QPainterPath() - h0 = Point(self.handles[0]['item'].pos()).length() - a = h0/(2.0*np.pi) - theta = 30.0*(2.0*np.pi)/360.0 - self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta))) - x0 = a*theta*cos(theta) - y0 = a*theta*sin(theta) - radius = self.getRadius() - theta += 20.0*(2.0*np.pi)/360.0 - i = 0 - while Point(x0, y0).length() < radius and i < 1000: - x1 = a*theta*cos(theta) - y1 = a*theta*sin(theta) - self.path.lineTo(QtCore.QPointF(x1,y1)) - theta += 20.0*(2.0*np.pi)/360.0 - x0 = x1 - y0 = y1 - i += 1 - - - return self.path - - - def shape(self): - p = QtGui.QPainterPath() - p.addEllipse(self.boundingRect()) - return p - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - #path = self.shape() - p.setPen(self.pen) - p.drawPath(self.path) - p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - p.drawPath(self.shape()) - p.setPen(QtGui.QPen(QtGui.QColor(0,0,255))) - p.drawRect(self.boundingRect()) - - - - - - \ No newline at end of file