diff --git a/examples/plottingItems.py b/examples/plottingItems.py new file mode 100644 index 00000000..b5942a90 --- /dev/null +++ b/examples/plottingItems.py @@ -0,0 +1,35 @@ +# -*- 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) +win.setWindowTitle('pyqtgraph example: plotting with items') + +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) + +p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100)) +inf1 = pg.InfiniteLine(movable=True, angle=90, label=True, textColor=(200,200,100), textFill=(200,200,200,50)) +inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), bounds = [-2, 2], unit="mm", hoverPen=(0,200,0)) +inf3 = pg.InfiniteLine(movable=True, angle=45) +inf1.setPos([2,2]) +p1.addItem(inf1) +p1.addItem(inf2) +p1.addItem(inf3) +lr = pg.LinearRegionItem(values=[0, 10]) +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/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 240dfe97..bbd24fd2 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,32 +1,73 @@ from ..Qt import QtGui, QtCore from ..Point import Point -from .GraphicsObject import GraphicsObject +from .UIGraphicsItem import UIGraphicsItem +from .TextItem import TextItem from .. import functions as fn import numpy as np import weakref +import math __all__ = ['InfiniteLine'] -class InfiniteLine(GraphicsObject): + + +def _calcLine(pos, angle, xmin, ymin, xmax, ymax): """ - **Bases:** :class:`GraphicsObject ` - + Evaluate the location of the points that delimitates a line into a viewbox + described by x and y ranges. Depending on the angle value, pos can be a + float (if angle=0 and 90) or a list of float (x and y coordinates). + Could be possible to beautify this piece of code. + New in verson 0.9.11 + """ + if angle == 0: + x1, y1, x2, y2 = xmin, pos, xmax, pos + elif angle == 90: + x1, y1, x2, y2 = pos, ymin, pos, ymax + else: + x0, y0 = pos + tana = math.tan(angle*math.pi/180) + y1 = tana*(xmin-x0) + y0 + y2 = tana*(xmax-x0) + y0 + if angle > 0: + y1 = max(y1, ymin) + y2 = min(y2, ymax) + else: + y1 = min(y1, ymax) + y2 = max(y2, ymin) + x1 = (y1-y0)/tana + x0 + x2 = (y2-y0)/tana + x0 + p1 = Point(x1, y1) + p2 = Point(x2, y2) + return p1, p2 + + +class InfiniteLine(UIGraphicsItem): + """ + **Bases:** :class:`UIGraphicsItem ` + Displays a line of infinite length. This line may be dragged to indicate a position in data coordinates. - + =============================== =================================================== **Signals:** sigDragged(self) sigPositionChangeFinished(self) sigPositionChanged(self) =============================== =================================================== + + Major changes have been performed in this class since version 0.9.11. The + number of methods in the public API has been increased, but the already + existing methods can be used in the same way. """ - + 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=False, textColor=None, textFill=None, + textLocation=0.05, textShift=0.5, textFormat="{:.3f}", + unit=None, name=None): """ =============== ================================================================== **Arguments:** @@ -37,79 +78,125 @@ 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. + label if True, a label is displayed next to the line to indicate its + location in data coordinates + textColor color of the label. Can be any argument fn.mkColor can understand. + textFill A brush to use when filling within the border of the text. + textLocation A float [0-1] that defines the location of the text. + textShift A float [0-1] that defines when the text shifts from one side to + another. + textFormat Any new python 3 str.format() format. + unit If not None, corresponds to the unit to show next to the label + name If not None, corresponds to the name of the object =============== ================================================================== """ - - GraphicsObject.__init__(self) - + + UIGraphicsItem.__init__(self) + if bounds is None: ## allowed value boundaries for orthogonal lines self.maxRange = [None, None] else: self.maxRange = bounds self.moving = False - self.setMovable(movable) self.mouseHovering = False + + self.angle = ((angle+45) % 180) - 45 + if textColor is None: + textColor = (200, 200, 200) + self.textColor = textColor + self.location = textLocation + self.shift = textShift + self.label = label + self.format = textFormat + self.unit = unit + self._name = name + + self.anchorLeft = (1., 0.5) + self.anchorRight = (0., 0.5) + self.anchorUp = (0.5, 1.) + self.anchorDown = (0.5, 0.) + self.text = TextItem(fill=textFill) + self.text.setParentItem(self) # important self.p = [0, 0] - self.setAngle(angle) + + if pen is None: + pen = (200, 200, 100) + + self.setPen(pen) + + if hoverPen is None: + self.setHoverPen(color=(255,0,0), width=self.pen.width()) + else: + self.setHoverPen(hoverPen) + self.currentPen = self.pen + + self.setMovable(movable) + 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()) - self.currentPen = self.pen - + if (self.angle == 0 or self.angle == 90) and self.label: + self.text.show() + else: + self.text.hide() + + 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.resetTransform() # no longer needed since version 0.9.11 + # self.rotate(self.angle) # no longer needed since version 0.9.11 + if (self.angle == 0 or self.angle == 90) and self.label: + self.text.show() + else: + self.text.hide() self.update() - + def setPos(self, pos): - + if type(pos) in [list, tuple]: newPos = pos elif isinstance(pos, QtCore.QPointF): @@ -121,10 +208,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 +220,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 - GraphicsObject.setPos(self, Point(self.p)) + # UIGraphicsItem.setPos(self, Point(self.p)) # thanks Sylvain! 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 +245,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 +261,59 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) - + def boundingRect(self): - #br = UIGraphicsItem.boundingRect(self) - br = self.viewRect() - ## add a 4-pixel radius around the line for mouse interaction. - - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line - if px is None: - px = 0 - w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px - br.setBottom(-w) - br.setTop(w) + br = UIGraphicsItem.boundingRect(self) # directly in viewBox coordinates + # we need to limit the boundingRect to the appropriate value. + val = self.value() + if self.angle == 0: # horizontal line + self._p1, self._p2 = _calcLine(val, 0, *br.getCoords()) + 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 + o1, o2 = _calcLine(val-w, 0, *br.getCoords()) + o3, o4 = _calcLine(val+w, 0, *br.getCoords()) + elif self.angle == 90: # vertical line + self._p1, self._p2 = _calcLine(val, 90, *br.getCoords()) + px = self.pixelLength(direction=Point(0,1), 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 + o1, o2 = _calcLine(val-w, 90, *br.getCoords()) + o3, o4 = _calcLine(val+w, 90, *br.getCoords()) + else: # oblique line + self._p1, self._p2 = _calcLine(val, self.angle, *br.getCoords()) + pxy = self.pixelLength(direction=Point(0,1), ortho=True) + if pxy is None: + pxy = 0 + wy = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * pxy + pxx = self.pixelLength(direction=Point(1,0), ortho=True) + if pxx is None: + pxx = 0 + wx = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * pxx + o1, o2 = _calcLine([val[0]-wy, val[1]-wx], self.angle, *br.getCoords()) + o3, o4 = _calcLine([val[0]+wy, val[1]+wx], self.angle, *br.getCoords()) + self._polygon = QtGui.QPolygonF([o1, o2, o4, o3]) + br = self._polygon.boundingRect() return br.normalized() - + + + def shape(self): + # returns a QPainterPath. Needed when the item is non rectangular if + # accurate mouse click detection is required. + # New in version 0.9.11 + qpp = QtGui.QPainterPath() + qpp.addPolygon(self._polygon) + return qpp + + 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._p1, self._p2) + + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled @@ -203,19 +324,20 @@ class InfiniteLine(GraphicsObject): if self.movable and ev.button() == QtCore.Qt.LeftButton: if ev.isStart(): self.moving = True - self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) - self.startPosition = self.pos() + self.cursorOffset = self.value() - ev.buttonDownPos() + self.startPosition = self.value() ev.accept() - + if not self.moving: return - - self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) + + self.setPos(self.cursorOffset + ev.pos()) + self.prepareGeometryChange() # new in version 0.9.11 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 +362,122 @@ class InfiniteLine(GraphicsObject): else: self.currentPen = self.pen self.update() + + def update(self): + # new in version 0.9.11 + UIGraphicsItem.update(self) + br = UIGraphicsItem.boundingRect(self) # directly in viewBox coordinates + xmin, ymin, xmax, ymax = br.getCoords() + if self.angle == 90: # vertical line + diffX = xmax-xmin + diffMin = self.value()-xmin + limInf = self.shift*diffX + ypos = ymin+self.location*(ymax-ymin) + if diffMin < limInf: + self.text.anchor = Point(self.anchorRight) + else: + self.text.anchor = Point(self.anchorLeft) + fmt = " x = " + self.format + if self.unit is not None: + fmt = fmt + self.unit + self.text.setText(fmt.format(self.value()), color=self.textColor) + self.text.setPos(self.value(), ypos) + elif self.angle == 0: # horizontal line + diffY = ymax-ymin + diffMin = self.value()-ymin + limInf = self.shift*(ymax-ymin) + xpos = xmin+self.location*(xmax-xmin) + if diffMin < limInf: + self.text.anchor = Point(self.anchorUp) + else: + self.text.anchor = Point(self.anchorDown) + fmt = " y = " + self.format + if self.unit is not None: + fmt = fmt + self.unit + self.text.setText(fmt.format(self.value()), color=self.textColor) + self.text.setPos(xpos, self.value()) + + + def showLabel(self, state): + """ + Display or not the label indicating the location of the line in data + coordinates. + + ============== ============================================== + **Arguments:** + state If True, the label is shown. Otherwise, it is hidden. + ============== ============================================== + """ + if state: + self.text.show() + else: + self.text.hide() + self.update() + + def setLocation(self, loc): + """ + Set the location of the textItem with respect to a specific axis. If the + line is vertical, the location is based on the normalized range of the + yaxis. Otherwise, it is based on the normalized range of the xaxis. + + ============== ============================================== + **Arguments:** + loc the normalized location of the textItem. + ============== ============================================== + """ + if loc > 1.: + loc = 1. + if loc < 0.: + loc = 0. + self.location = loc + self.update() + + def setShift(self, shift): + """ + Set the value with respect to the normalized range of the corresponding + axis where the location of the textItem shifts from one side to another. + + ============== ============================================== + **Arguments:** + shift the normalized shift value of the textItem. + ============== ============================================== + """ + if shift > 1.: + shift = 1. + if shift < 0.: + shift = 0. + self.shift = shift + self.update() + + def setFormat(self, format): + """ + Set the format of the label used to indicate the location of the line. + + + ============== ============================================== + **Arguments:** + format Any format compatible with the new python + str.format() format style. + ============== ============================================== + """ + self.format = format + self.update() + + def setUnit(self, unit): + """ + Set the unit of the label used to indicate the location of the line. + + + ============== ============================================== + **Arguments:** + unit Any string. + ============== ============================================== + """ + self.unit = unit + self.update() + + def setName(self, name): + self._name = name + + def name(self): + return self._name diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index e139190b..96b27720 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -9,10 +9,10 @@ __all__ = ['LinearRegionItem'] class LinearRegionItem(UIGraphicsItem): """ **Bases:** :class:`UIGraphicsItem ` - + Used for marking a horizontal or vertical region in plots. The region can be dragged and is bounded by lines which can be dragged individually. - + =============================== ============================================================================= **Signals:** sigRegionChangeFinished(self) Emitted when the user has finished dragging the region (or one of its lines) @@ -21,15 +21,15 @@ class LinearRegionItem(UIGraphicsItem): and when the region is changed programatically. =============================== ============================================================================= """ - + sigRegionChangeFinished = QtCore.Signal(object) sigRegionChanged = QtCore.Signal(object) Vertical = 0 Horizontal = 1 - + def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): """Create a new LinearRegionItem. - + ============== ===================================================================== **Arguments:** values A list of the positions of the lines in the region. These are not @@ -44,7 +44,7 @@ class LinearRegionItem(UIGraphicsItem): bounds Optional [min, max] bounding values for the region ============== ===================================================================== """ - + UIGraphicsItem.__init__(self) if orientation is None: orientation = LinearRegionItem.Vertical @@ -53,30 +53,30 @@ class LinearRegionItem(UIGraphicsItem): self.blockLineSignal = False self.moving = False self.mouseHovering = False - + if orientation == LinearRegionItem.Horizontal: self.lines = [ - InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] elif orientation == LinearRegionItem.Vertical: self.lines = [ - InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] else: raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') - - + + for l in self.lines: l.setParentItem(self) l.sigPositionChangeFinished.connect(self.lineMoveFinished) l.sigPositionChanged.connect(self.lineMoved) - + if brush is None: brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) self.setBrush(brush) - + self.setMovable(movable) - + def getRegion(self): """Return the values at the edges of the region.""" #if self.orientation[0] == 'h': @@ -88,7 +88,7 @@ class LinearRegionItem(UIGraphicsItem): def setRegion(self, rgn): """Set the values for the edges of the region. - + ============== ============================================== **Arguments:** rgn A list or tuple of the lower and upper values. @@ -114,14 +114,14 @@ class LinearRegionItem(UIGraphicsItem): def setBounds(self, bounds): """Optional [min, max] bounding values for the region. To have no bounds on the region use [None, None]. - Does not affect the current position of the region unless it is outside the new bounds. - See :func:`setRegion ` to set the position + Does not affect the current position of the region unless it is outside the new bounds. + See :func:`setRegion ` to set the position of the region.""" for l in self.lines: l.setBounds(bounds) - + def setMovable(self, m): - """Set lines to be movable by the user, or not. If lines are movable, they will + """Set lines to be movable by the user, or not. If lines are movable, they will also accept HoverEvents.""" for l in self.lines: l.setMovable(m) @@ -138,7 +138,7 @@ class LinearRegionItem(UIGraphicsItem): br.setTop(rng[0]) br.setBottom(rng[1]) return br.normalized() - + def paint(self, p, *args): profiler = debug.Profiler() UIGraphicsItem.paint(self, p, *args) @@ -158,12 +158,12 @@ class LinearRegionItem(UIGraphicsItem): self.prepareGeometryChange() #self.emit(QtCore.SIGNAL('regionChanged'), self) self.sigRegionChanged.emit(self) - + def lineMoveFinished(self): #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) self.sigRegionChangeFinished.emit(self) - - + + #def updateBounds(self): #vb = self.view().viewRect() #vals = [self.lines[0].value(), self.lines[1].value()] @@ -176,7 +176,7 @@ class LinearRegionItem(UIGraphicsItem): #if vb != self.bounds: #self.bounds = vb #self.rect.setRect(vb) - + #def mousePressEvent(self, ev): #if not self.movable: #ev.ignore() @@ -188,11 +188,11 @@ class LinearRegionItem(UIGraphicsItem): ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) ##else: ##ev.ignore() - + #def mouseReleaseEvent(self, ev): #for l in self.lines: #l.mouseReleaseEvent(ev) - + #def mouseMoveEvent(self, ev): ##print "move", ev.pos() #if not self.movable: @@ -208,16 +208,16 @@ class LinearRegionItem(UIGraphicsItem): if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: return ev.accept() - + if ev.isStart(): bdp = ev.buttonDownPos() - self.cursorOffsets = [l.pos() - bdp for l in self.lines] - self.startPositions = [l.pos() for l in self.lines] + self.cursorOffsets = [l.value() - bdp for l in self.lines] + self.startPositions = [l.value() for l in self.lines] self.moving = True - + if not self.moving: return - + #delta = ev.pos() - ev.lastPos() self.lines[0].blockSignals(True) # only want to update once for i, l in enumerate(self.lines): @@ -226,13 +226,13 @@ class LinearRegionItem(UIGraphicsItem): #l.mouseDragEvent(ev) self.lines[0].blockSignals(False) self.prepareGeometryChange() - + if ev.isFinish(): self.moving = False self.sigRegionChangeFinished.emit(self) else: self.sigRegionChanged.emit(self) - + def mouseClickEvent(self, ev): if self.moving and ev.button() == QtCore.Qt.RightButton: ev.accept() @@ -248,7 +248,7 @@ class LinearRegionItem(UIGraphicsItem): self.setMouseHover(True) else: self.setMouseHover(False) - + def setMouseHover(self, hover): ## Inform the item that the mouse is(not) hovering over it if self.mouseHovering == hover: @@ -276,15 +276,14 @@ class LinearRegionItem(UIGraphicsItem): #print "rgn hover leave" #ev.ignore() #self.updateHoverBrush(False) - + #def updateHoverBrush(self, hover=None): #if hover is None: #scene = self.scene() #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) - + #if hover: #self.currentBrush = fn.mkBrush(255, 0,0,100) #else: #self.currentBrush = self.brush #self.update() -