diff --git a/examples/infiniteline_performance.py b/examples/infiniteline_performance.py
new file mode 100644
index 00000000..86264142
--- /dev/null
+++ b/examples/infiniteline_performance.py
@@ -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_()
\ No newline at end of file
diff --git a/examples/plottingItems.py b/examples/plottingItems.py
new file mode 100644
index 00000000..d90d81ab
--- /dev/null
+++ b/examples/plottingItems.py
@@ -0,0 +1,40 @@
+# -*- 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)
+
+p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100, scale=10), pen=0.5)
+p1.setYRange(-40, 40)
+inf1 = pg.InfiniteLine(movable=True, angle=90, text='x={value:0.2f}',
+ textOpts={'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), text='y={value:0.2f}mm',
+ textOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)})
+inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', text='diagonal', textOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True})
+inf1.setPos([2,2])
+#inf1.setTextLocation(position=0.75)
+#inf2.setTextLocation(shift=0.8)
+p1.addItem(inf1)
+p1.addItem(inf2)
+p1.addItem(inf3)
+
+lr = pg.LinearRegionItem(values=[70, 80])
+p1.addItem(lr)
+
+## Start Qt event loop unless running in interactive mode or using pyside.
+if __name__ == '__main__':
+ import sys
+ if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
+ QtGui.QApplication.instance().exec_()
diff --git a/examples/text.py b/examples/text.py
index 23f527e3..43302e96 100644
--- a/examples/text.py
+++ b/examples/text.py
@@ -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='
This is the
PEAK
', anchor=(-0.3,1.3), border='w', fill=(0, 0, 255, 100))
+text = pg.TextItem(html='This is the
PEAK
', anchor=(-0.3,0.5), angle=45, border='w', fill=(0, 0, 255, 100))
plot.addItem(text)
text.setPos(0, y.max())
diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py
index 6984a7a4..22c9a281 100644
--- a/pyqtgraph/graphicsItems/InfiniteLine.py
+++ b/pyqtgraph/graphicsItems/InfiniteLine.py
@@ -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']
+
+
class InfiniteLine(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject `
-
+
Displays a line of infinite length.
This line may be dragged to indicate a position in data coordinates.
-
+
=============================== ===================================================
**Signals:**
sigDragged(self)
@@ -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, text=None, textOpts=None, name=None):
"""
=============== ==================================================================
**Arguments:**
@@ -37,13 +42,22 @@ class InfiniteLine(GraphicsObject):
for :func:`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 `.
+ Default pen is red.
bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal.
+ text 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.
+ textOpts A dict of keyword arguments to use when constructing the
+ text label. See :class:`InfLineLabel`.
+ name Name of the item
=============== ==================================================================
"""
-
+
GraphicsObject.__init__(self)
-
+
if bounds is None: ## allowed value boundaries for orthogonal lines
self.maxRange = [None, None]
else:
@@ -53,66 +67,81 @@ class InfiniteLine(GraphicsObject):
self.mouseHovering = False
self.p = [0, 0]
self.setAngle(angle)
+
+ if text is not None:
+ textOpts = {} if textOpts is None else textOpts
+ self.textItem = InfLineLabel(self, text=text, **textOpts)
+ self.textItem.setParentItem(self)
+
+ self.anchorLeft = (1., 0.5)
+ self.anchorRight = (0., 0.5)
+ self.anchorUp = (0.5, 1.)
+ self.anchorDown = (0.5, 0.)
+
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
self._boundingRect = None
self._line = None
-
+
+ self._name = name
+
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 `."""
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 `.
-
+
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):
@@ -124,10 +153,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])
@@ -136,28 +165,28 @@ 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:
# Invalidate bounding rect and line
self._boundingRect = None
self._line = None
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()
@@ -165,10 +194,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)
@@ -182,22 +211,16 @@ class InfiniteLine(GraphicsObject):
#print "ignore", change
#return GraphicsObject.itemChange(self, change, val)
- def viewTransformChanged(self):
- self._boundingRect = None
+ def _invalidateCache(self):
self._line = None
- GraphicsObject.viewTransformChanged(self)
-
- def viewChanged(self, view, oldView):
self._boundingRect = None
- self._line = None
- GraphicsObject.viewChanged(self, view, oldView)
def boundingRect(self):
if self._boundingRect is None:
#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
@@ -206,13 +229,13 @@ class InfiniteLine(GraphicsObject):
br.setTop(w)
br = br.normalized()
self._boundingRect = br
- self._line = QtCore.QLineF(br.right(), 0, br.left(), 0)
+ self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0)
return self._boundingRect
-
+
def paint(self, p, *args):
p.setPen(self.currentPen)
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
@@ -226,16 +249,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()
@@ -260,3 +283,122 @@ 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.
+ """
+ def __init__(self, line, text="", movable=False, position=0.5, **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)
+ TextItem.__init__(self, **kwds)
+ self.valueChanged()
+
+ def valueChanged(self):
+ if not self.isVisible():
+ return
+ value = self.line.value()
+ self.setText(self.format.format(value=value))
+ self.updatePosition()
+
+ def updatePosition(self):
+ # update text position to relative view location along line
+
+ view = self.getViewBox()
+ if not self.isVisible() or not isinstance(view, ViewBox):
+ # not in a viewbox, skip update
+ return
+
+ 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.
+ 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()
+ if len(paths) > 0:
+ l = list(paths[0])
+ pt1 = self.line.mapFromItem(view, l[0])
+ pt2 = self.line.mapFromItem(view, l[1])
+
+ pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos)
+
+ self.setPos(pt)
+
+ def setVisible(self, v):
+ TextItem.setVisible(self, v)
+ if v:
+ self.updateText()
+ self.updatePosition()
+
+ def setMovable(self, m):
+ self.movable = m
+ self.setAcceptHoverEvents(m)
+
+ 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
+ view = self.getViewBox()
+ tr = view.childGroup.itemTransform(self.line)[0]
+ vr = tr.mapRect(view.viewRect())
+ pos = self.mapToParent(pos)
+ return (pos.x() - vr.left()) / vr.width()
diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py
index d3c98006..220d5859 100644
--- a/pyqtgraph/graphicsItems/TextItem.py
+++ b/pyqtgraph/graphicsItems/TextItem.py
@@ -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=(1, 0)):
"""
============== =================================================================================
**Arguments:**
@@ -20,19 +23,22 @@ 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.
============== =================================================================================
"""
-
- ## 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 +46,8 @@ 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)
+ #self.textItem.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport
def setText(self, text, color=(200,200,200)):
"""
@@ -100,48 +106,37 @@ class TextItem(UIGraphicsItem):
self.textItem.setFont(*args)
self.updateText()
- #def setAngle(self, angle):
- #self.angle = angle
- #self.updateText()
-
+ def setAngle(self, angle):
+ self.angle = angle
+ self.updateTransform()
def updateText(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 +144,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.updateText()
+
\ No newline at end of file