Merge branch 'develop' into image-testing

Conflicts:
	pyqtgraph/tests/__init__.py
This commit is contained in:
Luke Campagnola 2016-03-17 22:29:06 -07:00
commit c65ca6d243
13 changed files with 808 additions and 241 deletions

45
examples/InfiniteLine.py Normal file
View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates some of the plotting items available in pyqtgraph.
"""
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([])
win = pg.GraphicsWindow(title="Plotting items examples")
win.resize(1000,600)
# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)
# Create a plot with some random data
p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100, scale=10), pen=0.5)
p1.setYRange(-40, 40)
# Add three infinite lines with labels
inf1 = pg.InfiniteLine(movable=True, angle=90, label='x={value:0.2f}',
labelOpts={'position':0.1, 'color': (200,200,100), 'fill': (200,200,200,50), 'movable': True})
inf2 = pg.InfiniteLine(movable=True, angle=0, pen=(0, 0, 200), bounds = [-20, 20], hoverPen=(0,200,0), label='y={value:0.2f}mm',
labelOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)})
inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', label='diagonal',
labelOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True})
inf1.setPos([2,2])
p1.addItem(inf1)
p1.addItem(inf2)
p1.addItem(inf3)
# Add a linear region with a label
lr = pg.LinearRegionItem(values=[70, 80])
p1.addItem(lr)
label = pg.InfLineLabel(lr.lines[1], "region 1", position=0.95, rotateAxis=(1,0), anchor=(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_()

38
examples/Symbols.py Executable file
View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
This example shows all the scatter plot symbols available in pyqtgraph.
These symbols are used to mark point locations for scatter plots and some line
plots, similar to "markers" in matplotlib and vispy.
"""
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([])
win = pg.GraphicsWindow(title="Scatter Plot Symbols")
win.resize(1000,600)
pg.setConfigOptions(antialias=True)
plot = win.addPlot(title="Plotting with symbols")
plot.addLegend()
plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o', symbolSize=14, name="symbol='o'")
plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t', symbolSize=14, name="symbol='t'")
plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1', symbolSize=14, name="symbol='t1'")
plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2', symbolSize=14, name="symbol='t2'")
plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3', symbolSize=14, name="symbol='t3'")
plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s', symbolSize=14, name="symbol='s'")
plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p', symbolSize=14, name="symbol='p'")
plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h', symbolSize=14, name="symbol='h'")
plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'")
plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'")
plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'")
plot.setXRange(-2, 4)
## 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_()

View File

@ -0,0 +1,52 @@
#!/usr/bin/python
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
from pyqtgraph.ptime import time
app = QtGui.QApplication([])
p = pg.plot()
p.setWindowTitle('pyqtgraph performance: InfiniteLine')
p.setRange(QtCore.QRectF(0, -10, 5000, 20))
p.setLabel('bottom', 'Index', units='B')
curve = p.plot()
# Add a large number of horizontal InfiniteLine to plot
for i in range(100):
line = pg.InfiniteLine(pos=np.random.randint(5000), movable=True)
p.addItem(line)
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_()

View File

@ -23,7 +23,7 @@ plot.setWindowTitle('pyqtgraph example: text')
curve = plot.plot(x,y) ## add a single curve curve = plot.plot(x,y) ## add a single curve
## Create text object, use HTML tags to specify color/size ## Create text object, use HTML tags to specify color/size
text = pg.TextItem(html='<div style="text-align: center"><span style="color: #FFF;">This is the</span><br><span style="color: #FF0; font-size: 16pt;">PEAK</span></div>', anchor=(-0.3,1.3), border='w', fill=(0, 0, 255, 100)) text = pg.TextItem(html='<div style="text-align: center"><span style="color: #FFF;">This is the</span><br><span style="color: #FF0; font-size: 16pt;">PEAK</span></div>', anchor=(-0.3,0.5), angle=45, border='w', fill=(0, 0, 255, 100))
plot.addItem(text) plot.addItem(text)
text.setPos(0, y.max()) text.setPos(0, y.max())

View File

@ -22,6 +22,7 @@ examples = OrderedDict([
('Console', 'ConsoleWidget.py'), ('Console', 'ConsoleWidget.py'),
('Histograms', 'histogram.py'), ('Histograms', 'histogram.py'),
('Beeswarm plot', 'beeswarm.py'), ('Beeswarm plot', 'beeswarm.py'),
('Symbols', 'Symbols.py'),
('Auto-range', 'PlotAutoRange.py'), ('Auto-range', 'PlotAutoRange.py'),
('Remote Plotting', 'RemoteSpeedTest.py'), ('Remote Plotting', 'RemoteSpeedTest.py'),
('Scrolling plots', 'scrollingPlots.py'), ('Scrolling plots', 'scrollingPlots.py'),

View File

@ -98,6 +98,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.lastDrag = None self.lastDrag = None
self.hoverItems = weakref.WeakKeyDictionary() self.hoverItems = weakref.WeakKeyDictionary()
self.lastHoverEvent = None self.lastHoverEvent = None
self.minDragTime = 0.5 # drags shorter than 0.5 sec are interpreted as clicks
self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu = [QtGui.QAction("Export...", self)]
self.contextMenu[0].triggered.connect(self.showExportDialog) self.contextMenu[0].triggered.connect(self.showExportDialog)
@ -173,7 +174,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet 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] cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
dist = Point(ev.screenPos() - cev.screenPos()) dist = Point(ev.screenPos() - cev.screenPos())
if dist.length() < self._moveDistance and now - cev.time() < 0.5: if dist.length() < self._moveDistance and now - cev.time() < self.minDragTime:
continue continue
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
self.dragButtons.append(int(btn)) self.dragButtons.append(int(btn))
@ -187,9 +188,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
if len(self.dragButtons) == 0: if len(self.dragButtons) == 0:
self.sendHoverEvents(ev, exitOnly=True) self.sendHoverEvents(ev, exitOnly=True)
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
#print 'sceneRelease'
if self.mouseGrabberItem() is None: if self.mouseGrabberItem() is None:
if ev.button() in self.dragButtons: if ev.button() in self.dragButtons:
if self.sendDragEvent(ev, final=True): if self.sendDragEvent(ev, final=True):

View File

@ -9,7 +9,7 @@ This module exists to smooth out some of the differences between PySide and PyQt
""" """
import sys, re import sys, re, time
from .python2_3 import asUnicode from .python2_3 import asUnicode
@ -45,6 +45,15 @@ if QT_LIB == PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg from PySide import QtGui, QtCore, QtOpenGL, QtSvg
try: try:
from PySide import QtTest from PySide import QtTest
if not hasattr(QtTest.QTest, 'qWait'):
@staticmethod
def qWait(msec):
start = time.time()
QtGui.QApplication.processEvents()
while time.time() < start + msec * 0.001:
QtGui.QApplication.processEvents()
QtTest.QTest.qWait = qWait
except ImportError: except ImportError:
pass pass
import PySide import PySide

View File

@ -1,12 +1,16 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from ..Point import Point from ..Point import Point
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from .TextItem import TextItem
from .ViewBox import ViewBox
from .. import functions as fn from .. import functions as fn
import numpy as np import numpy as np
import weakref import weakref
__all__ = ['InfiniteLine'] __all__ = ['InfiniteLine', 'InfLineLabel']
class InfiniteLine(GraphicsObject): class InfiniteLine(GraphicsObject):
""" """
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
@ -26,7 +30,8 @@ class InfiniteLine(GraphicsObject):
sigPositionChangeFinished = QtCore.Signal(object) sigPositionChangeFinished = QtCore.Signal(object)
sigPositionChanged = QtCore.Signal(object) sigPositionChanged = QtCore.Signal(object)
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None,
hoverPen=None, label=None, labelOpts=None, name=None):
""" """
=============== ================================================================== =============== ==================================================================
**Arguments:** **Arguments:**
@ -37,10 +42,23 @@ class InfiniteLine(GraphicsObject):
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
yellow. yellow.
movable If True, the line can be dragged to a new position by the user. movable If True, the line can be dragged to a new position by the user.
hoverPen Pen to use when drawing line when hovering over it. Can be any
arguments that are valid for :func:`mkPen <pyqtgraph.mkPen>`.
Default pen is red.
bounds Optional [min, max] bounding values. Bounds are only valid if the bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal. line is vertical or horizontal.
label Text to be displayed in a label attached to the line, or
None to show no label (default is None). May optionally
include formatting strings to display the line value.
labelOpts A dict of keyword arguments to use when constructing the
text label. See :class:`InfLineLabel`.
name Name of the item
=============== ================================================================== =============== ==================================================================
""" """
self._boundingRect = None
self._line = None
self._name = name
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
@ -53,17 +71,24 @@ class InfiniteLine(GraphicsObject):
self.mouseHovering = False self.mouseHovering = False
self.p = [0, 0] self.p = [0, 0]
self.setAngle(angle) self.setAngle(angle)
if pos is None: if pos is None:
pos = Point(0,0) pos = Point(0,0)
self.setPos(pos) self.setPos(pos)
if pen is None: if pen is None:
pen = (200, 200, 100) pen = (200, 200, 100)
self.setPen(pen) self.setPen(pen)
if hoverPen is None:
self.setHoverPen(color=(255,0,0), width=self.pen.width()) self.setHoverPen(color=(255,0,0), width=self.pen.width())
else:
self.setHoverPen(hoverPen)
self.currentPen = self.pen self.currentPen = self.pen
if label is not None:
labelOpts = {} if labelOpts is None else labelOpts
self.label = InfLineLabel(self, text=label, **labelOpts)
def setMovable(self, m): def setMovable(self, m):
"""Set whether the line is movable by the user.""" """Set whether the line is movable by the user."""
self.movable = m self.movable = m
@ -136,8 +161,8 @@ class InfiniteLine(GraphicsObject):
if self.p != newPos: if self.p != newPos:
self.p = newPos self.p = newPos
self._invalidateCache()
GraphicsObject.setPos(self, Point(self.p)) GraphicsObject.setPos(self, Point(self.p))
self.update()
self.sigPositionChanged.emit(self) self.sigPositionChanged.emit(self)
def getXPos(self): def getXPos(self):
@ -175,23 +200,33 @@ class InfiniteLine(GraphicsObject):
#print "ignore", change #print "ignore", change
#return GraphicsObject.itemChange(self, change, val) #return GraphicsObject.itemChange(self, change, val)
def _invalidateCache(self):
self._line = None
self._boundingRect = None
def boundingRect(self): def boundingRect(self):
if self._boundingRect is None:
#br = UIGraphicsItem.boundingRect(self) #br = UIGraphicsItem.boundingRect(self)
br = self.viewRect() br = self.viewRect()
## add a 4-pixel radius around the line for mouse interaction. if br is None:
return QtCore.QRectF()
## add a 4-pixel radius around the line for mouse interaction.
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
if px is None: if px is None:
px = 0 px = 0
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
br.setBottom(-w) br.setBottom(-w)
br.setTop(w) br.setTop(w)
return br.normalized()
br = br.normalized()
self._boundingRect = br
self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0)
return self._boundingRect
def paint(self, p, *args): def paint(self, p, *args):
br = self.boundingRect()
p.setPen(self.currentPen) p.setPen(self.currentPen)
p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) p.drawLine(self._line)
def dataBounds(self, axis, frac=1.0, orthoRange=None): def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == 0: if axis == 0:
@ -240,3 +275,195 @@ class InfiniteLine(GraphicsObject):
else: else:
self.currentPen = self.pen self.currentPen = self.pen
self.update() self.update()
def viewTransformChanged(self):
"""
Called whenever the transformation matrix of the view has changed.
(eg, the view range has changed or the view was resized)
"""
self._invalidateCache()
def setName(self, name):
self._name = name
def name(self):
return self._name
class InfLineLabel(TextItem):
"""
A TextItem that attaches itself to an InfiniteLine.
This class extends TextItem with the following features:
* Automatically positions adjacent to the line at a fixed position along
the line and within the view box.
* Automatically reformats text when the line value has changed.
* Can optionally be dragged to change its location along the line.
* Optionally aligns to its parent line.
=============== ==================================================================
**Arguments:**
line The InfiniteLine to which this label will be attached.
text String to display in the label. May contain a {value} formatting
string to display the current value of the line.
movable Bool; if True, then the label can be dragged along the line.
position Relative position (0.0-1.0) within the view to position the label
along the line.
anchors List of (x,y) pairs giving the text anchor positions that should
be used when the line is moved to one side of the view or the
other. This allows text to switch to the opposite side of the line
as it approaches the edge of the view. These are automatically
selected for some common cases, but may be specified if the
default values give unexpected results.
=============== ==================================================================
All extra keyword arguments are passed to TextItem. A particularly useful
option here is to use `rotateAxis=(1, 0)`, which will cause the text to
be automatically rotated parallel to the line.
"""
def __init__(self, line, text="", movable=False, position=0.5, anchors=None, **kwds):
self.line = line
self.movable = movable
self.orthoPos = position # text will always be placed on the line at a position relative to view bounds
self.format = text
self.line.sigPositionChanged.connect(self.valueChanged)
self._endpoints = (None, None)
if anchors is None:
# automatically pick sensible anchors
rax = kwds.get('rotateAxis', None)
if rax is not None:
if tuple(rax) == (1,0):
anchors = [(0.5, 0), (0.5, 1)]
else:
anchors = [(0, 0.5), (1, 0.5)]
else:
if line.angle % 180 == 0:
anchors = [(0.5, 0), (0.5, 1)]
else:
anchors = [(0, 0.5), (1, 0.5)]
self.anchors = anchors
TextItem.__init__(self, **kwds)
self.setParentItem(line)
self.valueChanged()
def valueChanged(self):
if not self.isVisible():
return
value = self.line.value()
self.setText(self.format.format(value=value))
self.updatePosition()
def getEndpoints(self):
# calculate points where line intersects view box
# (in line coordinates)
if self._endpoints[0] is None:
lr = self.line.boundingRect()
pt1 = Point(lr.left(), 0)
pt2 = Point(lr.right(), 0)
if self.line.angle % 90 != 0:
# more expensive to find text position for oblique lines.
view = self.getViewBox()
if not self.isVisible() or not isinstance(view, ViewBox):
# not in a viewbox, skip update
return (None, None)
p = QtGui.QPainterPath()
p.moveTo(pt1)
p.lineTo(pt2)
p = self.line.itemTransform(view)[0].map(p)
vr = QtGui.QPainterPath()
vr.addRect(view.boundingRect())
paths = vr.intersected(p).toSubpathPolygons(QtGui.QTransform())
if len(paths) > 0:
l = list(paths[0])
pt1 = self.line.mapFromItem(view, l[0])
pt2 = self.line.mapFromItem(view, l[1])
self._endpoints = (pt1, pt2)
return self._endpoints
def updatePosition(self):
# update text position to relative view location along line
self._endpoints = (None, None)
pt1, pt2 = self.getEndpoints()
if pt1 is None:
return
pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos)
self.setPos(pt)
# update anchor to keep text visible as it nears the view box edge
vr = self.line.viewRect()
if vr is not None:
self.setAnchor(self.anchors[0 if vr.center().y() < 0 else 1])
def setVisible(self, v):
TextItem.setVisible(self, v)
if v:
self.updateText()
self.updatePosition()
def setMovable(self, m):
"""Set whether this label is movable by dragging along the line.
"""
self.movable = m
self.setAcceptHoverEvents(m)
def setPosition(self, p):
"""Set the relative position (0.0-1.0) of this label within the view box
and along the line.
For horizontal (angle=0) and vertical (angle=90) lines, a value of 0.0
places the text at the bottom or left of the view, respectively.
"""
self.orthoPos = p
self.updatePosition()
def setFormat(self, text):
"""Set the text format string for this label.
May optionally contain "{value}" to include the lines current value
(the text will be reformatted whenever the line is moved).
"""
self.format = format
self.valueChanged()
def mouseDragEvent(self, ev):
if self.movable and ev.button() == QtCore.Qt.LeftButton:
if ev.isStart():
self._moving = True
self._cursorOffset = self._posToRel(ev.buttonDownPos())
self._startPosition = self.orthoPos
ev.accept()
if not self._moving:
return
rel = self._posToRel(ev.pos())
self.orthoPos = np.clip(self._startPosition + rel - self._cursorOffset, 0, 1)
self.updatePosition()
if ev.isFinish():
self._moving = False
def mouseClickEvent(self, ev):
if self.moving and ev.button() == QtCore.Qt.RightButton:
ev.accept()
self.orthoPos = self._startPosition
self.moving = False
def hoverEvent(self, ev):
if not ev.isExit() and self.movable:
ev.acceptDrags(QtCore.Qt.LeftButton)
def viewTransformChanged(self):
self.updatePosition()
TextItem.viewTransformChanged(self)
def _posToRel(self, pos):
# convert local position to relative position along line between view bounds
pt1, pt2 = self.getEndpoints()
if pt1 is None:
return 0
view = self.getViewBox()
pos = self.mapToParent(pos)
return (pos.x() - pt1.x()) / (pt2.x()-pt1.x())

View File

@ -19,17 +19,28 @@ __all__ = ['ScatterPlotItem', 'SpotItem']
## Build all symbol paths ## Build all symbol paths
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']])
Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
coords = { coords = {
't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)], 't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)],
't1': [(-0.5, 0.5), (0, -0.5), (0.5, 0.5)],
't2': [(-0.5, -0.5), (-0.5, 0.5), (0.5, 0)],
't3': [(0.5, 0.5), (0.5, -0.5), (-0.5, 0)],
'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)], '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.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.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) (0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05)
], ],
'p': [(0, -0.5), (-0.4755, -0.1545), (-0.2939, 0.4045),
(0.2939, 0.4045), (0.4755, -0.1545)],
'h': [(0.433, 0.25), (0., 0.5), (-0.433, 0.25), (-0.433, -0.25),
(0, -0.5), (0.433, -0.25)],
'star': [(0, -0.5), (-0.1123, -0.1545), (-0.4755, -0.1545),
(-0.1816, 0.059), (-0.2939, 0.4045), (0, 0.1910),
(0.2939, 0.4045), (0.1816, 0.059), (0.4755, -0.1545),
(0.1123, -0.1545)]
} }
for k, c in coords.items(): for k, c in coords.items():
Symbols[k].moveTo(*c[0]) Symbols[k].moveTo(*c[0])

View File

@ -1,13 +1,16 @@
import numpy as np
from ..Qt import QtCore, QtGui from ..Qt import QtCore, QtGui
from ..Point import Point from ..Point import Point
from .UIGraphicsItem import *
from .. import functions as fn from .. import functions as fn
from .GraphicsObject import GraphicsObject
class TextItem(UIGraphicsItem):
class TextItem(GraphicsObject):
""" """
GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). 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): def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0),
border=None, fill=None, angle=0, rotateAxis=None):
""" """
============== ================================================================================= ============== =================================================================================
**Arguments:** **Arguments:**
@ -20,19 +23,31 @@ class TextItem(UIGraphicsItem):
sets the lower-right corner. sets the lower-right corner.
*border* A pen to use when drawing the border *border* A pen to use when drawing the border
*fill* A brush to use when filling within the border *fill* A brush to use when filling within the border
*angle* Angle in degrees to rotate text. Default is 0; text will be displayed upright.
*rotateAxis* If None, then a text angle of 0 always points along the +x axis of the scene.
If a QPointF or (x,y) sequence is given, then it represents a vector direction
in the parent's coordinate system that the 0-degree line will be aligned to. This
Allows text to follow both the position and orientation of its parent while still
discarding any scale and shear factors.
============== ================================================================================= ============== =================================================================================
The effects of the `rotateAxis` and `angle` arguments are added independently. So for example:
* rotateAxis=None, angle=0 -> normal horizontal text
* rotateAxis=None, angle=90 -> normal vertical text
* rotateAxis=(1, 0), angle=0 -> text aligned with x axis of its parent
* rotateAxis=(0, 1), angle=0 -> text aligned with y axis of its parent
* rotateAxis=(1, 0), angle=90 -> text orthogonal to x axis of its parent
""" """
## 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 = Point(anchor) self.anchor = Point(anchor)
self.rotateAxis = None if rotateAxis is None else Point(rotateAxis)
#self.angle = 0 #self.angle = 0
UIGraphicsItem.__init__(self) GraphicsObject.__init__(self)
self.textItem = QtGui.QGraphicsTextItem() self.textItem = QtGui.QGraphicsTextItem()
self.textItem.setParentItem(self) self.textItem.setParentItem(self)
self.lastTransform = None self._lastTransform = None
self._bounds = QtCore.QRectF() self._bounds = QtCore.QRectF()
if html is None: if html is None:
self.setText(text, color) self.setText(text, color)
@ -40,8 +55,7 @@ class TextItem(UIGraphicsItem):
self.setHtml(html) self.setHtml(html)
self.fill = fn.mkBrush(fill) self.fill = fn.mkBrush(fill)
self.border = fn.mkPen(border) self.border = fn.mkPen(border)
self.rotate(angle) self.setAngle(angle)
self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)): def setText(self, text, color=(200,200,200)):
""" """
@ -52,14 +66,7 @@ class TextItem(UIGraphicsItem):
color = fn.mkColor(color) color = fn.mkColor(color)
self.textItem.setDefaultTextColor(color) self.textItem.setDefaultTextColor(color)
self.textItem.setPlainText(text) self.textItem.setPlainText(text)
self.updateText() self.updateTextPos()
#html = '<span style="color: #%s; text-align: center;">%s</span>' % (color, text)
#self.setHtml(html)
def updateAnchor(self):
pass
#self.resetTransform()
#self.translate(0, 20)
def setPlainText(self, *args): def setPlainText(self, *args):
""" """
@ -68,7 +75,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setPlainText(). See QtGui.QGraphicsTextItem.setPlainText().
""" """
self.textItem.setPlainText(*args) self.textItem.setPlainText(*args)
self.updateText() self.updateTextPos()
def setHtml(self, *args): def setHtml(self, *args):
""" """
@ -77,7 +84,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setHtml(). See QtGui.QGraphicsTextItem.setHtml().
""" """
self.textItem.setHtml(*args) self.textItem.setHtml(*args)
self.updateText() self.updateTextPos()
def setTextWidth(self, *args): def setTextWidth(self, *args):
""" """
@ -89,7 +96,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setTextWidth(). See QtGui.QGraphicsTextItem.setTextWidth().
""" """
self.textItem.setTextWidth(*args) self.textItem.setTextWidth(*args)
self.updateText() self.updateTextPos()
def setFont(self, *args): def setFont(self, *args):
""" """
@ -98,50 +105,43 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setFont(). See QtGui.QGraphicsTextItem.setFont().
""" """
self.textItem.setFont(*args) self.textItem.setFont(*args)
self.updateText() self.updateTextPos()
#def setAngle(self, angle): def setAngle(self, angle):
#self.angle = angle self.angle = angle
#self.updateText() self.updateTransform()
def setAnchor(self, anchor):
self.anchor = Point(anchor)
self.updateTextPos()
def updateText(self): def updateTextPos(self):
# update text position to obey anchor
r = self.textItem.boundingRect()
tl = self.textItem.mapToParent(r.topLeft())
br = self.textItem.mapToParent(r.bottomRight())
offset = (br - tl) * self.anchor
self.textItem.setPos(-offset)
## Needed to maintain font size when rendering to image with increased resolution ### Needed to maintain font size when rendering to image with increased resolution
self.textItem.resetTransform() #self.textItem.resetTransform()
#self.textItem.rotate(self.angle) ##self.textItem.rotate(self.angle)
if self._exportOpts is not False and 'resolutionScale' in self._exportOpts: #if self._exportOpts is not False and 'resolutionScale' in self._exportOpts:
s = self._exportOpts['resolutionScale'] #s = self._exportOpts['resolutionScale']
self.textItem.scale(s, s) #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(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): def boundingRect(self):
return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect() return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect()
def viewTransformChanged(self):
# called whenever view transform has changed.
# Do this here to avoid double-updates when view changes.
self.updateTransform()
def paint(self, p, *args): def paint(self, p, *args):
tr = p.transform() # this is not ideal because it causes another update to be scheduled.
if self.lastTransform is not None: # ideally, we would have a sceneTransformChanged event to react to..
if tr != self.lastTransform: self.updateTransform()
self.viewRangeChanged()
self.lastTransform = tr
if self.border.style() != QtCore.Qt.NoPen or self.fill.style() != QtCore.Qt.NoBrush: if self.border.style() != QtCore.Qt.NoPen or self.fill.style() != QtCore.Qt.NoBrush:
p.setPen(self.border) p.setPen(self.border)
@ -149,4 +149,37 @@ class TextItem(UIGraphicsItem):
p.setRenderHint(p.Antialiasing, True) p.setRenderHint(p.Antialiasing, True)
p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect())) p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect()))
def updateTransform(self):
# update transform such that this item has the correct orientation
# and scaling relative to the scene, but inherits its position from its
# parent.
# This is similar to setting ItemIgnoresTransformations = True, but
# does not break mouse interaction and collision detection.
p = self.parentItem()
if p is None:
pt = QtGui.QTransform()
else:
pt = p.sceneTransform()
if pt == self._lastTransform:
return
t = pt.inverted()[0]
# reset translation
t.setMatrix(t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), 0, 0, t.m33())
# apply rotation
angle = -self.angle
if self.rotateAxis is not None:
d = pt.map(self.rotateAxis) - pt.map(Point(0, 0))
a = np.arctan2(d.y(), d.x()) * 180 / np.pi
angle += a
t.rotate(angle)
self.setTransform(t)
self._lastTransform = pt
self.updateTextPos()

View File

@ -0,0 +1,96 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtTest
from pyqtgraph.tests import mouseDrag, mouseMove
pg.mkQApp()
def test_InfiniteLine():
# Test basic InfiniteLine API
plt = pg.plot()
plt.setXRange(-10, 10)
plt.setYRange(-10, 10)
plt.resize(600, 600)
# seemingly arbitrary requirements; might need longer wait time for some platforms..
QtTest.QTest.qWaitForWindowShown(plt)
QtTest.QTest.qWait(100)
vline = plt.addLine(x=1)
assert vline.angle == 90
br = vline.mapToView(QtGui.QPolygonF(vline.boundingRect()))
assert br.containsPoint(pg.Point(1, 5), QtCore.Qt.OddEvenFill)
assert not br.containsPoint(pg.Point(5, 0), QtCore.Qt.OddEvenFill)
hline = plt.addLine(y=0)
assert hline.angle == 0
assert hline.boundingRect().contains(pg.Point(5, 0))
assert not hline.boundingRect().contains(pg.Point(0, 5))
vline.setValue(2)
assert vline.value() == 2
vline.setPos(pg.Point(4, -5))
assert vline.value() == 4
oline = pg.InfiniteLine(angle=30)
plt.addItem(oline)
oline.setPos(pg.Point(1, -1))
assert oline.angle == 30
assert oline.pos() == pg.Point(1, -1)
assert oline.value() == [1, -1]
# test bounding rect for oblique line
br = oline.mapToScene(oline.boundingRect())
pos = oline.mapToScene(pg.Point(2, 0))
assert br.containsPoint(pos, QtCore.Qt.OddEvenFill)
px = pg.Point(-0.5, -1.0 / 3**0.5)
assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill)
assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill)
def test_mouseInteraction():
plt = pg.plot()
plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly.
vline = plt.addLine(x=0, movable=True)
plt.addItem(vline)
hline = plt.addLine(y=0, movable=True)
hline2 = plt.addLine(y=-1, movable=False)
plt.setXRange(-10, 10)
plt.setYRange(-10, 10)
# test horizontal drag
pos = plt.plotItem.vb.mapViewToScene(pg.Point(0,5)).toPoint()
pos2 = pos - QtCore.QPoint(200, 200)
mouseMove(plt, pos)
assert vline.mouseHovering is True and hline.mouseHovering is False
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
px = vline.pixelLength(pg.Point(1, 0), ortho=True)
assert abs(vline.value() - plt.plotItem.vb.mapSceneToView(pos2).x()) <= px
# test missed drag
pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint()
pos = pos + QtCore.QPoint(0, 6)
pos2 = pos + QtCore.QPoint(-20, -20)
mouseMove(plt, pos)
assert vline.mouseHovering is False and hline.mouseHovering is False
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
assert hline.value() == 0
# test vertical drag
pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint()
pos2 = pos - QtCore.QPoint(50, 50)
mouseMove(plt, pos)
assert vline.mouseHovering is False and hline.mouseHovering is True
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
px = hline.pixelLength(pg.Point(1, 0), ortho=True)
assert abs(hline.value() - plt.plotItem.vb.mapSceneToView(pos2).y()) <= px
# test non-interactive line
pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,-1)).toPoint()
pos2 = pos - QtCore.QPoint(50, 50)
mouseMove(plt, pos)
assert hline2.mouseHovering == False
mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton)
assert hline2.value() == -1
if __name__ == '__main__':
test_mouseInteraction()

View File

@ -1 +1,2 @@
from .image_testing import assertImageApproved from .image_testing import assertImageApproved
from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick

View File

@ -0,0 +1,55 @@
# Functions for generating user input events.
# We would like to use QTest for this purpose, but it seems to be broken.
# See: http://stackoverflow.com/questions/16299779/qt-qgraphicsview-unit-testing-how-to-keep-the-mouse-in-a-pressed-state
from ..Qt import QtCore, QtGui, QT_LIB
def mousePress(widget, pos, button, modifier=None):
if isinstance(widget, QtGui.QGraphicsView):
widget = widget.viewport()
if modifier is None:
modifier = QtCore.Qt.NoModifier
if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF):
pos = pos.toPoint()
event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, pos, button, QtCore.Qt.NoButton, modifier)
QtGui.QApplication.sendEvent(widget, event)
def mouseRelease(widget, pos, button, modifier=None):
if isinstance(widget, QtGui.QGraphicsView):
widget = widget.viewport()
if modifier is None:
modifier = QtCore.Qt.NoModifier
if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF):
pos = pos.toPoint()
event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, pos, button, QtCore.Qt.NoButton, modifier)
QtGui.QApplication.sendEvent(widget, event)
def mouseMove(widget, pos, buttons=None, modifier=None):
if isinstance(widget, QtGui.QGraphicsView):
widget = widget.viewport()
if modifier is None:
modifier = QtCore.Qt.NoModifier
if buttons is None:
buttons = QtCore.Qt.NoButton
if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF):
pos = pos.toPoint()
event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, pos, QtCore.Qt.NoButton, buttons, modifier)
QtGui.QApplication.sendEvent(widget, event)
def mouseDrag(widget, pos1, pos2, button, modifier=None):
mouseMove(widget, pos1)
mousePress(widget, pos1, button, modifier)
mouseMove(widget, pos2, button, modifier)
mouseRelease(widget, pos2, button, modifier)
def mouseClick(widget, pos, button, modifier=None):
mouseMove(widget, pos)
mousePress(widget, pos, button, modifier)
mouseRelease(widget, pos, button, modifier)