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
## 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)
text.setPos(0, y.max())

View File

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

View File

@ -98,6 +98,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.lastDrag = None
self.hoverItems = weakref.WeakKeyDictionary()
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[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
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:
if dist.length() < self._moveDistance and now - cev.time() < self.minDragTime:
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))
@ -186,10 +187,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
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:
if ev.button() in self.dragButtons:
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
@ -45,6 +45,15 @@ if QT_LIB == PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
try:
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:
pass
import PySide

View File

@ -1,19 +1,23 @@
from ..Qt import QtGui, QtCore
from ..Point import Point
from .GraphicsObject import GraphicsObject
from .TextItem import TextItem
from .ViewBox import ViewBox
from .. import functions as fn
import numpy as np
import weakref
__all__ = ['InfiniteLine']
__all__ = ['InfiniteLine', 'InfLineLabel']
class InfiniteLine(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
Displays a line of infinite length.
This line may be dragged to indicate a position in data coordinates.
=============================== ===================================================
**Signals:**
sigDragged(self)
@ -21,12 +25,13 @@ class InfiniteLine(GraphicsObject):
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):
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None,
hoverPen=None, label=None, labelOpts=None, name=None):
"""
=============== ==================================================================
**Arguments:**
@ -37,13 +42,26 @@ class InfiniteLine(GraphicsObject):
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
yellow.
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
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)
if bounds is None: ## allowed value boundaries for orthogonal lines
self.maxRange = [None, None]
else:
@ -53,63 +71,70 @@ class InfiniteLine(GraphicsObject):
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.setHoverPen(color=(255,0,0), width=self.pen.width())
if hoverPen is None:
self.setHoverPen(color=(255,0,0), width=self.pen.width())
else:
self.setHoverPen(hoverPen)
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):
"""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, *args, **kwargs):
"""Set the pen for drawing the line. Allowable arguments are any that are valid
"""Set the pen for drawing the line. Allowable arguments are any that are valid
for :func:`mkPen <pyqtgraph.mkPen>`."""
self.pen = fn.mkPen(*args, **kwargs)
if not self.mouseHovering:
self.currentPen = self.pen
self.update()
def setHoverPen(self, *args, **kwargs):
"""Set the pen for drawing the line while the mouse hovers over it.
Allowable arguments are any that are valid
"""Set the pen for drawing the line while the mouse hovers over it.
Allowable arguments are any that are valid
for :func:`mkPen <pyqtgraph.mkPen>`.
If the line is not movable, then hovering is also disabled.
Added in version 0.9.9."""
self.hoverPen = fn.mkPen(*args, **kwargs)
if self.mouseHovering:
self.currentPen = self.hoverPen
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
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):
@ -121,10 +146,10 @@ class InfiniteLine(GraphicsObject):
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:
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])
@ -133,24 +158,24 @@ class InfiniteLine(GraphicsObject):
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._invalidateCache()
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
"""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()
@ -158,10 +183,10 @@ class InfiniteLine(GraphicsObject):
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
"""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)
@ -174,25 +199,35 @@ class InfiniteLine(GraphicsObject):
#else:
#print "ignore", change
#return GraphicsObject.itemChange(self, change, val)
def _invalidateCache(self):
self._line = None
self._boundingRect = None
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
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
br.setBottom(-w)
br.setTop(w)
return br.normalized()
if self._boundingRect is None:
#br = UIGraphicsItem.boundingRect(self)
br = self.viewRect()
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
if px is None:
px = 0
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
br.setBottom(-w)
br.setTop(w)
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):
br = self.boundingRect()
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):
if axis == 0:
return None ## x axis should never be auto-scaled
@ -206,16 +241,16 @@ class InfiniteLine(GraphicsObject):
self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
self.startPosition = self.pos()
ev.accept()
if not self.moving:
return
self.setPos(self.cursorOffset + self.mapToParent(ev.pos()))
self.sigDragged.emit(self)
if ev.isFinish():
self.moving = False
self.sigPositionChangeFinished.emit(self)
def mouseClickEvent(self, ev):
if self.moving and ev.button() == QtCore.Qt.RightButton:
ev.accept()
@ -240,3 +275,195 @@ class InfiniteLine(GraphicsObject):
else:
self.currentPen = self.pen
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
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['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
coords = {
'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)],
'+': [
(-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)
],
'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():
Symbols[k].moveTo(*c[0])
@ -40,7 +51,7 @@ tr = QtGui.QTransform()
tr.rotate(45)
Symbols['x'] = tr.map(Symbols['+'])
def drawSymbol(painter, symbol, size, pen, brush):
if symbol is None:
return
@ -53,13 +64,13 @@ def drawSymbol(painter, symbol, size, pen, brush):
symbol = list(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
the symbol will be rendered into the device specified (See QPainter documentation
for more information).
"""
## Render a spot with the given parameters to a pixmap
@ -80,33 +91,33 @@ 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()
"""
def __init__(self):
# symbol key : QRect(...) coordinates where symbol can be found in atlas.
# note that the coordinate list will always be the same list object as
# 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,
# 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
self.max_width=0
def getSymbolCoords(self, opts):
"""
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
@ -131,7 +142,7 @@ class SymbolAtlas(object):
keyi = key
sourceRecti = newRectSrc
return sourceRect
def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width
rendered = {}
@ -150,7 +161,7 @@ class SymbolAtlas(object):
w = arr.shape[0]
avgWidth += w
maxWidth = max(maxWidth, w)
nSymbols = len(rendered)
if nSymbols > 0:
avgWidth /= nSymbols
@ -158,10 +169,10 @@ class SymbolAtlas(object):
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
@ -187,7 +198,7 @@ class SymbolAtlas(object):
self.atlas = None
self.atlasValid = True
self.max_width = maxWidth
def getAtlas(self):
if not self.atlasValid:
self.buildAtlas()
@ -197,27 +208,27 @@ class SymbolAtlas(object):
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.
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
@ -228,17 +239,17 @@ class ScatterPlotItem(GraphicsObject):
"""
profiler = debug.Profiler()
GraphicsObject.__init__(self)
self.picture = None # QPicture used for rendering when pxmode==False
self.fragmentAtlas = SymbolAtlas()
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)])
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.
'pxMode': True,
'useCache': True, ## If useCache is False, symbols are re-drawn on every paint.
'antialias': getConfigOption('antialias'),
'name': None,
}
@ -252,14 +263,14 @@ class ScatterPlotItem(GraphicsObject):
profiler('setData')
#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:
@ -285,8 +296,8 @@ class ScatterPlotItem(GraphicsObject):
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
*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)
*name* The name of this item. Names are used for automatically
generating LegendItem entries and by some exporters.
@ -298,10 +309,10 @@ class ScatterPlotItem(GraphicsObject):
def addPoints(self, *args, **kargs):
"""
Add new points to the scatter plot.
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]
@ -310,7 +321,7 @@ class ScatterPlotItem(GraphicsObject):
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']
@ -329,7 +340,7 @@ class ScatterPlotItem(GraphicsObject):
y.append(p[1])
kargs['x'] = x
kargs['y'] = y
## determine how many spots we have
if 'spots' in kargs:
numPts = len(kargs['spots'])
@ -339,16 +350,16 @@ class ScatterPlotItem(GraphicsObject):
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
@ -376,12 +387,12 @@ class ScatterPlotItem(GraphicsObject):
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:
@ -397,32 +408,32 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate()
self.updateSpots(newData)
self.sigPlotChanged.emit(self)
def invalidate(self):
## clear any cached drawing state
self.picture = None
self.update()
def getData(self):
return self.data['x'], self.data['y']
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 name(self):
return self.opts.get('name', None)
def setPen(self, *args, **kargs):
"""Set the pen(s) used to draw the outline around each spot.
"""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
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)
@ -436,19 +447,19 @@ class ScatterPlotItem(GraphicsObject):
dataSet['pen'] = pens
else:
self.opts['pen'] = fn.mkPen(*args, **kargs)
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
def setBrush(self, *args, **kargs):
"""Set the brush(es) used to fill the interior of each spot.
"""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
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 'mask' in kargs and kargs['mask'] is not None:
@ -459,19 +470,19 @@ class ScatterPlotItem(GraphicsObject):
else:
self.opts['brush'] = fn.mkBrush(*args, **kargs)
#self._spotPixmap = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
def setSymbol(self, symbol, update=True, dataSet=None, mask=None):
"""Set the symbol(s) used to draw each spot.
"""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
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 mask is not None:
@ -482,19 +493,19 @@ class ScatterPlotItem(GraphicsObject):
else:
self.opts['symbol'] = symbol
self._spotPixmap = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
def setSize(self, size, update=True, dataSet=None, mask=None):
"""Set the size(s) used to draw each spot.
"""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
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 mask is not None:
@ -505,21 +516,21 @@ class ScatterPlotItem(GraphicsObject):
else:
self.opts['size'] = size
self._spotPixmap = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
def setPointData(self, data, dataSet=None, mask=None):
if dataSet is None:
dataSet = self.data
if isinstance(data, np.ndarray) or isinstance(data, list):
if mask is not None:
data = data[mask]
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 (data.dtype.fields is not None)and len(data.dtype.fields) > 1:
@ -527,14 +538,14 @@ class ScatterPlotItem(GraphicsObject):
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
@ -547,9 +558,9 @@ class ScatterPlotItem(GraphicsObject):
opts = self.getSpotOpts(dataSet[mask])
sourceRect = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['sourceRect'][mask] = sourceRect
self.fragmentAtlas.getAtlas() # generate atlas so source widths are available.
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2
dataSet['targetRect'] = None
self._maxSpotPxWidth = self.fragmentAtlas.max_width
@ -585,9 +596,9 @@ class ScatterPlotItem(GraphicsObject):
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
@ -605,8 +616,8 @@ class ScatterPlotItem(GraphicsObject):
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()
@ -617,23 +628,23 @@ class ScatterPlotItem(GraphicsObject):
def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and orthoRange is None 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:
self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072)
return self.bounds[ax]
@ -656,11 +667,11 @@ class ScatterPlotItem(GraphicsObject):
if ymn is None or ymx is None:
ymn = 0
ymx = 0
px = py = 0.0
pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
try:
px = 0 if px is None else px.length()
@ -670,7 +681,7 @@ class ScatterPlotItem(GraphicsObject):
py = 0 if py is None else py.length()
except OverflowError:
py = 0
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
@ -688,7 +699,7 @@ class ScatterPlotItem(GraphicsObject):
def mapPointsToDevice(self, pts):
# Map point locations to device
# Map point locations to device
tr = self.deviceTransform()
if tr is None:
return None
@ -699,7 +710,7 @@ class ScatterPlotItem(GraphicsObject):
pts = fn.transformCoordinates(tr, pts)
pts -= self.data['width']
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
return pts
def getViewMask(self, pts):
@ -713,48 +724,48 @@ class ScatterPlotItem(GraphicsObject):
mask = ((pts[0] + w > viewBounds.left()) &
(pts[0] - w < viewBounds.right()) &
(pts[1] + w > viewBounds.top()) &
(pts[1] - w < viewBounds.bottom())) ## remove out of view points
(pts[1] - w < viewBounds.bottom())) ## remove out of view points
return mask
@debug.warnOnException ## raising an exception here causes crash
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:
p.resetTransform()
# Map point coordinates to device
pts = np.vstack([self.data['x'], self.data['y']])
pts = self.mapPointsToDevice(pts)
if pts is None:
return
# Cull points that are outside view
viewMask = self.getViewMask(pts)
#pts = pts[:,mask]
#data = self.data[mask]
if self.opts['useCache'] and self._exportOpts is False:
# Draw symbols from pre-rendered atlas
atlas = self.fragmentAtlas.getAtlas()
# Update targetRects if necessary
updateMask = viewMask & np.equal(self.data['targetRect'], None)
if np.any(updateMask):
updatePts = pts[:,updateMask]
width = self.data[updateMask]['width']*2
self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
data = self.data[viewMask]
if USE_PYSIDE or USE_PYQT5:
list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect']))
@ -782,16 +793,16 @@ class ScatterPlotItem(GraphicsObject):
p2.translate(rec['x'], rec['y'])
drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end()
p.setRenderHint(p.Antialiasing, aa)
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()
@ -814,7 +825,7 @@ class ScatterPlotItem(GraphicsObject):
#print "No hit:", (x, y), (sx, sy)
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
return pts[::-1]
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
@ -833,7 +844,7 @@ class ScatterPlotItem(GraphicsObject):
class SpotItem(object):
"""
Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or
These can be retrieved by calling ScatterPlotItem.points() or
by connecting to the ScatterPlotItem's click signals.
"""
@ -844,34 +855,34 @@ class SpotItem(object):
#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.
"""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
"""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.
"""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']
@ -883,7 +894,7 @@ class SpotItem(object):
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."""
@ -895,35 +906,35 @@ class SpotItem(object):
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
@ -938,14 +949,14 @@ class SpotItem(object):
#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()

View File

@ -1,13 +1,16 @@
import numpy as np
from ..Qt import QtCore, QtGui
from ..Point import Point
from .UIGraphicsItem import *
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).
"""
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:**
@ -20,19 +23,31 @@ class TextItem(UIGraphicsItem):
sets the lower-right corner.
*border* A pen to use when drawing 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.rotateAxis = None if rotateAxis is None else Point(rotateAxis)
#self.angle = 0
UIGraphicsItem.__init__(self)
GraphicsObject.__init__(self)
self.textItem = QtGui.QGraphicsTextItem()
self.textItem.setParentItem(self)
self.lastTransform = None
self._lastTransform = None
self._bounds = QtCore.QRectF()
if html is None:
self.setText(text, color)
@ -40,8 +55,7 @@ class TextItem(UIGraphicsItem):
self.setHtml(html)
self.fill = fn.mkBrush(fill)
self.border = fn.mkPen(border)
self.rotate(angle)
self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
self.setAngle(angle)
def setText(self, text, color=(200,200,200)):
"""
@ -52,14 +66,7 @@ class TextItem(UIGraphicsItem):
color = fn.mkColor(color)
self.textItem.setDefaultTextColor(color)
self.textItem.setPlainText(text)
self.updateText()
#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)
self.updateTextPos()
def setPlainText(self, *args):
"""
@ -68,7 +75,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setPlainText().
"""
self.textItem.setPlainText(*args)
self.updateText()
self.updateTextPos()
def setHtml(self, *args):
"""
@ -77,7 +84,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setHtml().
"""
self.textItem.setHtml(*args)
self.updateText()
self.updateTextPos()
def setTextWidth(self, *args):
"""
@ -89,7 +96,7 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setTextWidth().
"""
self.textItem.setTextWidth(*args)
self.updateText()
self.updateTextPos()
def setFont(self, *args):
"""
@ -98,50 +105,43 @@ class TextItem(UIGraphicsItem):
See QtGui.QGraphicsTextItem.setFont().
"""
self.textItem.setFont(*args)
self.updateText()
self.updateTextPos()
#def setAngle(self, angle):
#self.angle = angle
#self.updateText()
def setAngle(self, angle):
self.angle = angle
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
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)
### 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(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 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):
tr = p.transform()
if self.lastTransform is not None:
if tr != self.lastTransform:
self.viewRangeChanged()
self.lastTransform = tr
# this is not ideal because it causes another update to be scheduled.
# ideally, we would have a sceneTransformChanged event to react to..
self.updateTransform()
if self.border.style() != QtCore.Qt.NoPen or self.fill.style() != QtCore.Qt.NoBrush:
p.setPen(self.border)
@ -149,4 +149,37 @@ class TextItem(UIGraphicsItem):
p.setRenderHint(p.Antialiasing, True)
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 .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)