From ce36ea4eb63b22c9e9823ee3acfb9d3e8fb6cc79 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 15 Jan 2016 16:10:24 +0100 Subject: [PATCH 01/17] Infiniteline enhancement --- examples/plottingItems.py | 35 ++ pyqtgraph/graphicsItems/InfiniteLine.py | 371 ++++++++++++++++---- pyqtgraph/graphicsItems/LinearRegionItem.py | 73 ++-- 3 files changed, 377 insertions(+), 102 deletions(-) create mode 100644 examples/plottingItems.py 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() - From 0d4c78a6bea699d33e85c41c9019171f4cddd9e0 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 15 Jan 2016 16:13:05 +0100 Subject: [PATCH 02/17] Infiniteline enhancement --- examples/plottingItems.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index b5942a90..6323e369 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -12,7 +12,6 @@ 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) From 07f610950d567decca820509bece93d0687e99df Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Mon, 1 Feb 2016 11:17:36 +0100 Subject: [PATCH 03/17] creation of a combined method for handling the label location --- examples/plottingItems.py | 1 + pyqtgraph/graphicsItems/InfiniteLine.py | 42 +++++++------------------ 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 6323e369..e4cb29bb 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -21,6 +21,7 @@ inf1 = pg.InfiniteLine(movable=True, angle=90, label=True, textColor=(200,200,10 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]) +inf1.setTextLocation([0.25, 0.9]) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index bbd24fd2..00b517cf 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -307,13 +307,11 @@ class InfiniteLine(UIGraphicsItem): qpp.addPolygon(self._polygon) return qpp - def paint(self, p, *args): br = self.boundingRect() p.setPen(self.currentPen) 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 @@ -397,7 +395,6 @@ class InfiniteLine(UIGraphicsItem): 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 @@ -414,39 +411,22 @@ class InfiniteLine(UIGraphicsItem): self.text.hide() self.update() - def setLocation(self, loc): + def setTextLocation(self, param): """ - 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. - + Set the location of the label. param is a list of two values. + param[0] defines the location of the label along the axis and + param[1] defines the shift value (defines the condition where the + label shifts from one side of the line to the other one). + New in version 0.9.11 ============== ============================================== **Arguments:** - loc the normalized location of the textItem. + param list of parameters. ============== ============================================== """ - 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 + if len(param) != 2: # check that the input data are correct + return + self.location = np.clip(param[0], 0, 1) + self.shift = np.clip(param[1], 0, 1) self.update() def setFormat(self, format): From 51b8be2bd17aacfdeba048736bdf26652fed56ef Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Wed, 3 Feb 2016 12:52:01 +0100 Subject: [PATCH 04/17] Infinite line extension --- examples/plottingItems.py | 10 +- pyqtgraph/graphicsItems/InfiniteLine.py | 310 +++++++------------- pyqtgraph/graphicsItems/LinearRegionItem.py | 73 ++--- 3 files changed, 152 insertions(+), 241 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index e4cb29bb..7815677d 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -17,14 +17,14 @@ win.resize(1000,600) 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 = pg.InfiniteLine(movable=True, angle=90, label=False, textColor=(200,200,100), textFill=(200,200,200,50)) +inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0)) +#inf3 = pg.InfiniteLine(movable=True, angle=45) inf1.setPos([2,2]) -inf1.setTextLocation([0.25, 0.9]) +##inf1.setTextLocation([0.25, 0.9]) p1.addItem(inf1) p1.addItem(inf2) -p1.addItem(inf3) +#p1.addItem(inf3) lr = pg.LinearRegionItem(values=[0, 10]) p1.addItem(lr) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 00b517cf..d645824b 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,49 +1,20 @@ from ..Qt import QtGui, QtCore from ..Point import Point -from .UIGraphicsItem import UIGraphicsItem +from .GraphicsObject import GraphicsObject +#from UIGraphicsItem import UIGraphicsItem from .TextItem import TextItem +from .ViewBox import ViewBox from .. import functions as fn import numpy as np import weakref -import math __all__ = ['InfiniteLine'] -def _calcLine(pos, angle, xmin, ymin, xmax, ymax): +class InfiniteLine(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 ` + **Bases:** :class:`GraphicsObject ` Displays a line of infinite length. This line may be dragged to indicate a position in data coordinates. @@ -54,10 +25,6 @@ class InfiniteLine(UIGraphicsItem): 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) @@ -66,8 +33,8 @@ class InfiniteLine(UIGraphicsItem): 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): + textLocation=[0.05,0.5], textFormat="{:.3f}", + suffix=None, name='InfiniteLine'): """ =============== ================================================================== **Arguments:** @@ -87,65 +54,63 @@ class InfiniteLine(UIGraphicsItem): 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. + textLocation list where list[0] defines the location of the text (if + vertical, a 0 value means that the textItem is on the bottom + axis, and a 1 value means that thet TextItem is on the top + axis, same thing if horizontal) and list[1] defines when the + text shifts from one side to the other side of the line. 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 + suffix If not None, corresponds to the unit to show next to the label + name name of the item =============== ================================================================== """ - UIGraphicsItem.__init__(self) + GraphicsObject.__init__(self) if bounds is None: ## allowed value boundaries for orthogonal lines self.maxRange = [None, None] else: self.maxRange = bounds self.moving = False + self.setMovable(movable) self.mouseHovering = False + self.p = [0, 0] + self.setAngle(angle) - self.angle = ((angle+45) % 180) - 45 if textColor is None: - textColor = (200, 200, 200) + textColor = (200, 200, 100) self.textColor = textColor - self.location = textLocation - self.shift = textShift - self.label = label - self.format = textFormat - self.unit = unit - self._name = name + self.textFill = textFill + self.textLocation = textLocation + self.suffix = suffix + + if (self.angle == 0 or self.angle == 90) and label: + self.textItem = TextItem(fill=textFill) + self.textItem.setParentItem(self) + else: + self.textItem = None 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] + + if pos is None: + pos = Point(0,0) + self.setPos(pos) 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 (self.angle == 0 or self.angle == 90) and self.label: - self.text.show() - else: - self.text.hide() + self.format = textFormat + self._name = name def setMovable(self, m): """Set whether the line is movable by the user.""" @@ -187,12 +152,8 @@ class InfiniteLine(UIGraphicsItem): not vertical or horizontal. """ self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 - # 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.resetTransform() + self.rotate(self.angle) self.update() def setPos(self, pos): @@ -223,10 +184,47 @@ class InfiniteLine(UIGraphicsItem): if self.p != newPos: self.p = newPos - # UIGraphicsItem.setPos(self, Point(self.p)) # thanks Sylvain! + GraphicsObject.setPos(self, Point(self.p)) + + if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): + self.updateTextPosition() + self.update() self.sigPositionChanged.emit(self) + def updateTextPosition(self): + """ + Update the location of the textItem. Called only if a textItem is + requested and if the item has already been added to a PlotItem. + """ + rangeX, rangeY = self.getViewBox().viewRange() + xmin, xmax = rangeX + ymin, ymax = rangeY + if self.angle == 90: # vertical line + diffMin = self.value()-xmin + limInf = self.textLocation[1]*(xmax-xmin) + ypos = ymin+self.textLocation[0]*(ymax-ymin) + if diffMin < limInf: + self.textItem.anchor = Point(self.anchorRight) + else: + self.textItem.anchor = Point(self.anchorLeft) + fmt = " x = " + self.format + if self.suffix is not None: + fmt = fmt + self.suffix + self.textItem.setText(fmt.format(self.value()), color=self.textColor) + elif self.angle == 0: # horizontal line + diffMin = self.value()-ymin + limInf = self.textLocation[1]*(ymax-ymin) + xpos = xmin+self.textLocation[0]*(xmax-xmin) + if diffMin < limInf: + self.textItem.anchor = Point(self.anchorUp) + else: + self.textItem.anchor = Point(self.anchorDown) + fmt = " y = " + self.format + if self.suffix is not None: + fmt = fmt + self.suffix + self.textItem.setText(fmt.format(self.value()), color=self.textColor) + def getXPos(self): return self.p[0] @@ -263,54 +261,22 @@ class InfiniteLine(UIGraphicsItem): #return GraphicsObject.itemChange(self, change, val) def boundingRect(self): - 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() + #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() - - 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(self._p1, self._p2) + p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: @@ -322,15 +288,14 @@ class InfiniteLine(UIGraphicsItem): if self.movable and ev.button() == QtCore.Qt.LeftButton: if ev.isStart(): self.moving = True - self.cursorOffset = self.value() - ev.buttonDownPos() - self.startPosition = self.value() + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() ev.accept() if not self.moving: return - self.setPos(self.cursorOffset + ev.pos()) - self.prepareGeometryChange() # new in version 0.9.11 + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) self.sigDragged.emit(self) if ev.isFinish(): self.moving = False @@ -361,39 +326,14 @@ class InfiniteLine(UIGraphicsItem): 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 viewTransformChanged(self): + """ + Called whenever the transformation matrix of the view has changed. + (eg, the view range has changed or the view was resized) + """ + if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: + self.updateTextPosition() + #GraphicsObject.viewTransformChanged(self) def showLabel(self, state): """ @@ -406,54 +346,24 @@ class InfiniteLine(UIGraphicsItem): ============== ============================================== """ if state: - self.text.show() + self.textItem = TextItem(fill=self.textFill) + self.textItem.setParentItem(self) + self.viewTransformChanged() else: - self.text.hide() - self.update() + self.textItem = None - def setTextLocation(self, param): + + def setTextLocation(self, loc): """ - Set the location of the label. param is a list of two values. - param[0] defines the location of the label along the axis and - param[1] defines the shift value (defines the condition where the - label shifts from one side of the line to the other one). - New in version 0.9.11 - ============== ============================================== - **Arguments:** - param list of parameters. - ============== ============================================== + Set the parameters that defines 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. + loc[0] defines the location of the text along the infiniteLine + loc[1] defines the location when the label shifts from one side of then + infiniteLine to the other. """ - if len(param) != 2: # check that the input data are correct - return - self.location = np.clip(param[0], 0, 1) - self.shift = np.clip(param[1], 0, 1) - 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.textLocation = [np.clip(loc[0], 0, 1), np.clip(loc[1], 0, 1)] self.update() def setName(self, name): diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 96b27720..e139190b 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.value() - bdp for l in self.lines] - self.startPositions = [l.value() for l in self.lines] + self.cursorOffsets = [l.pos() - bdp for l in self.lines] + self.startPositions = [l.pos() for l in self.lines] self.moving = True - + if not self.moving: return - + #delta = ev.pos() - ev.lastPos() self.lines[0].blockSignals(True) # only want to update once for i, l in enumerate(self.lines): @@ -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,14 +276,15 @@ 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() + From aec6ce8abb3ae755f59de2e78014910ba90dbfd0 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Thu, 4 Feb 2016 03:28:59 +0100 Subject: [PATCH 05/17] infinite line performance improvement --- examples/infiniteline_performance.py | 52 +++++++++++++++++++++++++ pyqtgraph/graphicsItems/InfiniteLine.py | 42 +++++++++++++------- 2 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 examples/infiniteline_performance.py 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/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index d645824b..b2327f8e 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,7 +1,6 @@ from ..Qt import QtGui, QtCore from ..Point import Point from .GraphicsObject import GraphicsObject -#from UIGraphicsItem import UIGraphicsItem from .TextItem import TextItem from .ViewBox import ViewBox from .. import functions as fn @@ -112,6 +111,10 @@ class InfiniteLine(GraphicsObject): self._name = name + # Cache complex value for drawing speed-up (#PR267) + self._line = None + self._boundingRect = None + def setMovable(self, m): """Set whether the line is movable by the user.""" self.movable = m @@ -184,6 +187,7 @@ class InfiniteLine(GraphicsObject): if self.p != newPos: self.p = newPos + self._invalidateCache() GraphicsObject.setPos(self, Point(self.p)) if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): @@ -260,23 +264,30 @@ class InfiniteLine(GraphicsObject): #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. + def _invalidateCache(self): + self._line = None + self._boundingRect = None - 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() + 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 + 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: @@ -331,9 +342,10 @@ class InfiniteLine(GraphicsObject): Called whenever the transformation matrix of the view has changed. (eg, the view range has changed or the view was resized) """ + self._invalidateCache() + if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: self.updateTextPosition() - #GraphicsObject.viewTransformChanged(self) def showLabel(self, state): """ From 0be3615c883ccb8580a490d8cc04b15de3223b09 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 5 Feb 2016 11:54:00 +0100 Subject: [PATCH 06/17] docstring correction --- pyqtgraph/graphicsItems/InfiniteLine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index b2327f8e..5efbb9ea 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -352,10 +352,10 @@ class InfiniteLine(GraphicsObject): 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.textItem = TextItem(fill=self.textFill) From e7b27c2726f53e34864965fb86cecfe0c38d8b31 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 5 Feb 2016 13:57:51 +0100 Subject: [PATCH 07/17] text location algorithm simplification --- examples/plottingItems.py | 6 ++--- pyqtgraph/graphicsItems/InfiniteLine.py | 36 +++++++++++-------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 7815677d..6a2445bc 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -17,14 +17,14 @@ win.resize(1000,600) 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=False, textColor=(200,200,100), textFill=(200,200,200,50)) +inf1 = pg.InfiniteLine(movable=True, angle=90, label=True, textShift=0.2, textColor=(200,200,100), textFill=(200,200,200,50)) inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0)) -#inf3 = pg.InfiniteLine(movable=True, angle=45) +inf3 = pg.InfiniteLine(movable=True, angle=45) inf1.setPos([2,2]) ##inf1.setTextLocation([0.25, 0.9]) p1.addItem(inf1) p1.addItem(inf2) -#p1.addItem(inf3) +p1.addItem(inf3) lr = pg.LinearRegionItem(values=[0, 10]) p1.addItem(lr) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 5efbb9ea..a96d2050 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -32,7 +32,7 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, hoverPen=None, label=False, textColor=None, textFill=None, - textLocation=[0.05,0.5], textFormat="{:.3f}", + textShift=0.5, textFormat="{:.3f}", suffix=None, name='InfiniteLine'): """ =============== ================================================================== @@ -53,11 +53,8 @@ class InfiniteLine(GraphicsObject): 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 list where list[0] defines the location of the text (if - vertical, a 0 value means that the textItem is on the bottom - axis, and a 1 value means that thet TextItem is on the top - axis, same thing if horizontal) and list[1] defines when the - text shifts from one side to the other side of the line. + textShift float (0-1) that defines when the text shifts from one side to + the other side of the line. textFormat Any new python 3 str.format() format. suffix If not None, corresponds to the unit to show next to the label name name of the item @@ -80,7 +77,7 @@ class InfiniteLine(GraphicsObject): textColor = (200, 200, 100) self.textColor = textColor self.textFill = textFill - self.textLocation = textLocation + self.textShift = textShift self.suffix = suffix if (self.angle == 0 or self.angle == 90) and label: @@ -206,8 +203,7 @@ class InfiniteLine(GraphicsObject): ymin, ymax = rangeY if self.angle == 90: # vertical line diffMin = self.value()-xmin - limInf = self.textLocation[1]*(xmax-xmin) - ypos = ymin+self.textLocation[0]*(ymax-ymin) + limInf = self.textShift*(xmax-xmin) if diffMin < limInf: self.textItem.anchor = Point(self.anchorRight) else: @@ -218,8 +214,7 @@ class InfiniteLine(GraphicsObject): self.textItem.setText(fmt.format(self.value()), color=self.textColor) elif self.angle == 0: # horizontal line diffMin = self.value()-ymin - limInf = self.textLocation[1]*(ymax-ymin) - xpos = xmin+self.textLocation[0]*(xmax-xmin) + limInf = self.textShift*(ymax-ymin) if diffMin < limInf: self.textItem.anchor = Point(self.anchorUp) else: @@ -364,18 +359,17 @@ class InfiniteLine(GraphicsObject): else: self.textItem = None + def setTextShift(self, shift): + """ + Set the parameter that defines the location when the label shifts from + one side of the infiniteLine to the other. - def setTextLocation(self, loc): + ============== ====================================================== + **Arguments:** + shift float (range of value = [0-1]). + ============== ====================================================== """ - Set the parameters that defines 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. - loc[0] defines the location of the text along the infiniteLine - loc[1] defines the location when the label shifts from one side of then - infiniteLine to the other. - """ - self.textLocation = [np.clip(loc[0], 0, 1), np.clip(loc[1], 0, 1)] + self.textShift = np.clip(shift, 0, 1) self.update() def setName(self, name): From 6fc4e1a611f8306804d40b7b22ffd39bd0c6d6d9 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Mon, 15 Feb 2016 07:11:22 +0100 Subject: [PATCH 08/17] renaming of a method for better consistency --- pyqtgraph/graphicsItems/InfiniteLine.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index a96d2050..4ee9f901 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -188,15 +188,16 @@ class InfiniteLine(GraphicsObject): GraphicsObject.setPos(self, Point(self.p)) if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): - self.updateTextPosition() + self.updateTextContent() self.update() self.sigPositionChanged.emit(self) - def updateTextPosition(self): + def updateTextContent(self): """ - Update the location of the textItem. Called only if a textItem is - requested and if the item has already been added to a PlotItem. + Update the content displayed by the textItem. Called only if a + textItem is requested and if the item has already been added to + a PlotItem. """ rangeX, rangeY = self.getViewBox().viewRange() xmin, xmax = rangeX @@ -340,7 +341,7 @@ class InfiniteLine(GraphicsObject): self._invalidateCache() if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: - self.updateTextPosition() + self.updateTextContent() def showLabel(self, state): """ From de24d6db6ae426054ec9890fa76046ed79e15b73 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Tue, 16 Feb 2016 06:36:41 +0100 Subject: [PATCH 09/17] correction of the text location bug --- pyqtgraph/graphicsItems/InfiniteLine.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 4ee9f901..e8bcc639 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -185,19 +185,19 @@ class InfiniteLine(GraphicsObject): if self.p != newPos: self.p = newPos self._invalidateCache() - GraphicsObject.setPos(self, Point(self.p)) if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): - self.updateTextContent() - + self.updateTextAndLocation() + else: + GraphicsObject.setPos(self, Point(self.p)) self.update() self.sigPositionChanged.emit(self) - def updateTextContent(self): + def updateTextAndLocation(self): """ - Update the content displayed by the textItem. Called only if a - textItem is requested and if the item has already been added to - a PlotItem. + Update the content displayed by the textItem and the location of the + item. Called only if a textItem is requested and if the item has + already been added to a PlotItem. """ rangeX, rangeY = self.getViewBox().viewRange() xmin, xmax = rangeX @@ -213,6 +213,8 @@ class InfiniteLine(GraphicsObject): if self.suffix is not None: fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) + posY = ymin+0.05*(ymax-ymin) + GraphicsObject.setPos(self, Point(self.value(), posY)) elif self.angle == 0: # horizontal line diffMin = self.value()-ymin limInf = self.textShift*(ymax-ymin) @@ -224,6 +226,8 @@ class InfiniteLine(GraphicsObject): if self.suffix is not None: fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) + posX = xmin+0.05*(xmax-xmin) + GraphicsObject.setPos(self, Point(posX, self.value())) def getXPos(self): return self.p[0] @@ -341,7 +345,7 @@ class InfiniteLine(GraphicsObject): self._invalidateCache() if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: - self.updateTextContent() + self.updateTextAndLocation() def showLabel(self, state): """ From ba4b6482639272c2f530f3c03cf4aced00f7d48a Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Tue, 16 Feb 2016 06:48:59 +0100 Subject: [PATCH 10/17] addition of a convenient method for handling the label position --- examples/plottingItems.py | 5 ++-- pyqtgraph/graphicsItems/InfiniteLine.py | 34 ++++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 6a2445bc..5bf14b62 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -17,11 +17,12 @@ win.resize(1000,600) 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, textShift=0.2, textColor=(200,200,100), textFill=(200,200,200,50)) +inf1 = pg.InfiniteLine(movable=True, angle=90, label=True, textPosition=[0.5, 0.2], textColor=(200,200,100), textFill=(200,200,200,50)) inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0)) inf3 = pg.InfiniteLine(movable=True, angle=45) inf1.setPos([2,2]) -##inf1.setTextLocation([0.25, 0.9]) +inf1.setTextLocation(position=0.75) +inf2.setTextLocation(shift=0.8) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index e8bcc639..70f8f60f 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -32,7 +32,7 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, hoverPen=None, label=False, textColor=None, textFill=None, - textShift=0.5, textFormat="{:.3f}", + textPosition=[0.05, 0.5], textFormat="{:.3f}", suffix=None, name='InfiniteLine'): """ =============== ================================================================== @@ -53,8 +53,11 @@ class InfiniteLine(GraphicsObject): 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. - textShift float (0-1) that defines when the text shifts from one side to - the other side of the line. + textPosition list of float (0-1) that defines when the precise location of the + label. The first float governs the location of the label in the + direction of the line, whereas the second one governs the shift + of the label from one side of the line to the other in the + orthogonal direction. textFormat Any new python 3 str.format() format. suffix If not None, corresponds to the unit to show next to the label name name of the item @@ -77,7 +80,7 @@ class InfiniteLine(GraphicsObject): textColor = (200, 200, 100) self.textColor = textColor self.textFill = textFill - self.textShift = textShift + self.textPosition = textPosition self.suffix = suffix if (self.angle == 0 or self.angle == 90) and label: @@ -202,9 +205,10 @@ class InfiniteLine(GraphicsObject): rangeX, rangeY = self.getViewBox().viewRange() xmin, xmax = rangeX ymin, ymax = rangeY + pos, shift = self.textPosition if self.angle == 90: # vertical line diffMin = self.value()-xmin - limInf = self.textShift*(xmax-xmin) + limInf = shift*(xmax-xmin) if diffMin < limInf: self.textItem.anchor = Point(self.anchorRight) else: @@ -213,11 +217,11 @@ class InfiniteLine(GraphicsObject): if self.suffix is not None: fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) - posY = ymin+0.05*(ymax-ymin) + posY = ymin+pos*(ymax-ymin) GraphicsObject.setPos(self, Point(self.value(), posY)) elif self.angle == 0: # horizontal line diffMin = self.value()-ymin - limInf = self.textShift*(ymax-ymin) + limInf = shift*(ymax-ymin) if diffMin < limInf: self.textItem.anchor = Point(self.anchorUp) else: @@ -226,7 +230,7 @@ class InfiniteLine(GraphicsObject): if self.suffix is not None: fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) - posX = xmin+0.05*(xmax-xmin) + posX = xmin+pos*(xmax-xmin) GraphicsObject.setPos(self, Point(posX, self.value())) def getXPos(self): @@ -364,17 +368,23 @@ class InfiniteLine(GraphicsObject): else: self.textItem = None - def setTextShift(self, shift): + def setTextLocation(self, position=0.05, shift=0.5): """ - Set the parameter that defines the location when the label shifts from - one side of the infiniteLine to the other. + Set the parameters that defines the location of the label on the axis. + The position *parameter* governs the location of the label in the + direction of the line, whereas the *shift* governs the shift of the + label from one side of the line to the other in the orthogonal + direction. ============== ====================================================== **Arguments:** + position float (range of value = [0-1]) shift float (range of value = [0-1]). ============== ====================================================== """ - self.textShift = np.clip(shift, 0, 1) + pos = np.clip(position, 0, 1) + shift = np.clip(shift, 0, 1) + self.textPosition = [pos, shift] self.update() def setName(self, name): From 5888603ebfe011d2d7c50af434defbdf5ce2fbc5 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Tue, 16 Feb 2016 08:14:53 +0100 Subject: [PATCH 11/17] addition of a draggable option for infiniteline --- examples/plottingItems.py | 5 ++-- pyqtgraph/graphicsItems/InfiniteLine.py | 38 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 5bf14b62..973e165c 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -18,7 +18,7 @@ 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, textPosition=[0.5, 0.2], textColor=(200,200,100), textFill=(200,200,200,50)) -inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0)) +inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0), draggableLabel=True) inf3 = pg.InfiniteLine(movable=True, angle=45) inf1.setPos([2,2]) inf1.setTextLocation(position=0.75) @@ -26,7 +26,8 @@ inf2.setTextLocation(shift=0.8) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) -lr = pg.LinearRegionItem(values=[0, 10]) + +lr = pg.LinearRegionItem(values=[5, 10]) p1.addItem(lr) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 70f8f60f..c7b4ab35 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -32,7 +32,7 @@ class InfiniteLine(GraphicsObject): def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, hoverPen=None, label=False, textColor=None, textFill=None, - textPosition=[0.05, 0.5], textFormat="{:.3f}", + textPosition=[0.05, 0.5], textFormat="{:.3f}", draggableLabel=False, suffix=None, name='InfiniteLine'): """ =============== ================================================================== @@ -59,6 +59,9 @@ class InfiniteLine(GraphicsObject): of the label from one side of the line to the other in the orthogonal direction. textFormat Any new python 3 str.format() format. + draggableLabel Bool. If True, the user can relocate the label during the dragging. + If set to True, the first entry of textPosition is no longer + useful. suffix If not None, corresponds to the unit to show next to the label name name of the item =============== ================================================================== @@ -81,6 +84,7 @@ class InfiniteLine(GraphicsObject): self.textColor = textColor self.textFill = textFill self.textPosition = textPosition + self.draggableLabel = draggableLabel self.suffix = suffix if (self.angle == 0 or self.angle == 90) and label: @@ -190,17 +194,20 @@ class InfiniteLine(GraphicsObject): self._invalidateCache() if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): - self.updateTextAndLocation() - else: + self.updateText() + if self.draggableLabel: + GraphicsObject.setPos(self, Point(self.p)) + else: # precise location needed + GraphicsObject.setPos(self, self._exactPos) + else: # no label displayed or called just before being dragged for the first time GraphicsObject.setPos(self, Point(self.p)) self.update() self.sigPositionChanged.emit(self) - def updateTextAndLocation(self): + def updateText(self): """ - Update the content displayed by the textItem and the location of the - item. Called only if a textItem is requested and if the item has - already been added to a PlotItem. + Update the content displayed by the textItem. Called only if a textItem + is requested and if the item has already been added to a PlotItem. """ rangeX, rangeY = self.getViewBox().viewRange() xmin, xmax = rangeX @@ -218,7 +225,8 @@ class InfiniteLine(GraphicsObject): fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) posY = ymin+pos*(ymax-ymin) - GraphicsObject.setPos(self, Point(self.value(), posY)) + #self.p = [self.value(), posY] + self._exactPos = Point(self.value(), posY) elif self.angle == 0: # horizontal line diffMin = self.value()-ymin limInf = shift*(ymax-ymin) @@ -231,7 +239,8 @@ class InfiniteLine(GraphicsObject): fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) posX = xmin+pos*(xmax-xmin) - GraphicsObject.setPos(self, Point(posX, self.value())) + #self.p = [posX, self.value()] + self._exactPos = Point(posX, self.value()) def getXPos(self): return self.p[0] @@ -349,7 +358,7 @@ class InfiniteLine(GraphicsObject): self._invalidateCache() if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: - self.updateTextAndLocation() + self.updateText() def showLabel(self, state): """ @@ -387,6 +396,15 @@ class InfiniteLine(GraphicsObject): self.textPosition = [pos, shift] self.update() + def setDraggableLabel(self, state): + """ + Set the state of the label regarding its behaviour during the dragging + of the line. If True, then the location of the label change during the + dragging of the line. + """ + self.draggableLabel = state + self.update() + def setName(self, name): self._name = name From 010cda004ba4df2818f52f0a0dfa47589d5d4aaa Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Wed, 17 Feb 2016 07:03:13 +0100 Subject: [PATCH 12/17] correction of a bug regarding the exact placement of the label --- pyqtgraph/graphicsItems/InfiniteLine.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index c7b4ab35..05c93bc8 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -193,12 +193,8 @@ class InfiniteLine(GraphicsObject): self.p = newPos self._invalidateCache() - if self.textItem is not None and self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox): + if self.textItem is not None and isinstance(self.getViewBox(), ViewBox): self.updateText() - if self.draggableLabel: - GraphicsObject.setPos(self, Point(self.p)) - else: # precise location needed - GraphicsObject.setPos(self, self._exactPos) else: # no label displayed or called just before being dragged for the first time GraphicsObject.setPos(self, Point(self.p)) self.update() @@ -225,7 +221,6 @@ class InfiniteLine(GraphicsObject): fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) posY = ymin+pos*(ymax-ymin) - #self.p = [self.value(), posY] self._exactPos = Point(self.value(), posY) elif self.angle == 0: # horizontal line diffMin = self.value()-ymin @@ -239,8 +234,11 @@ class InfiniteLine(GraphicsObject): fmt = fmt + self.suffix self.textItem.setText(fmt.format(self.value()), color=self.textColor) posX = xmin+pos*(xmax-xmin) - #self.p = [posX, self.value()] self._exactPos = Point(posX, self.value()) + if self.draggableLabel: + GraphicsObject.setPos(self, Point(self.p)) + else: # precise location needed + GraphicsObject.setPos(self, self._exactPos) def getXPos(self): return self.p[0] @@ -356,8 +354,7 @@ class InfiniteLine(GraphicsObject): (eg, the view range has changed or the view was resized) """ self._invalidateCache() - - if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: + if isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: self.updateText() def showLabel(self, state): From 5172b782b55dbfe5d5a9df896f295ace22ee22cf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 19 Feb 2016 00:41:42 -0800 Subject: [PATCH 13/17] Added inflinelabel class, label dragging and position update works. Update to TextItem to allow mouse interaction --- examples/plottingItems.py | 2 +- pyqtgraph/graphicsItems/InfiniteLine.py | 161 ++++++++++++++---------- pyqtgraph/graphicsItems/TextItem.py | 18 +-- 3 files changed, 99 insertions(+), 82 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 973e165c..ffb808b5 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -18,7 +18,7 @@ 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, textPosition=[0.5, 0.2], textColor=(200,200,100), textFill=(200,200,200,50)) -inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0), draggableLabel=True) +inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0), draggableLabel=True, textFill=0.5) inf3 = pg.InfiniteLine(movable=True, angle=45) inf1.setPos([2,2]) inf1.setTextLocation(position=0.75) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 05c93bc8..f4b25860 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -84,14 +84,13 @@ class InfiniteLine(GraphicsObject): self.textColor = textColor self.textFill = textFill self.textPosition = textPosition - self.draggableLabel = draggableLabel self.suffix = suffix - if (self.angle == 0 or self.angle == 90) and label: - self.textItem = TextItem(fill=textFill) - self.textItem.setParentItem(self) - else: - self.textItem = None + + self.textItem = InfLineLabel(self, fill=textFill) + self.textItem.setParentItem(self) + self.setDraggableLabel(draggableLabel) + self.showLabel(label) self.anchorLeft = (1., 0.5) self.anchorRight = (0., 0.5) @@ -192,53 +191,8 @@ class InfiniteLine(GraphicsObject): if self.p != newPos: self.p = newPos self._invalidateCache() - - if self.textItem is not None and isinstance(self.getViewBox(), ViewBox): - self.updateText() - else: # no label displayed or called just before being dragged for the first time - GraphicsObject.setPos(self, Point(self.p)) - self.update() - self.sigPositionChanged.emit(self) - - def updateText(self): - """ - Update the content displayed by the textItem. Called only if a textItem - is requested and if the item has already been added to a PlotItem. - """ - rangeX, rangeY = self.getViewBox().viewRange() - xmin, xmax = rangeX - ymin, ymax = rangeY - pos, shift = self.textPosition - if self.angle == 90: # vertical line - diffMin = self.value()-xmin - limInf = shift*(xmax-xmin) - if diffMin < limInf: - self.textItem.anchor = Point(self.anchorRight) - else: - self.textItem.anchor = Point(self.anchorLeft) - fmt = " x = " + self.format - if self.suffix is not None: - fmt = fmt + self.suffix - self.textItem.setText(fmt.format(self.value()), color=self.textColor) - posY = ymin+pos*(ymax-ymin) - self._exactPos = Point(self.value(), posY) - elif self.angle == 0: # horizontal line - diffMin = self.value()-ymin - limInf = shift*(ymax-ymin) - if diffMin < limInf: - self.textItem.anchor = Point(self.anchorUp) - else: - self.textItem.anchor = Point(self.anchorDown) - fmt = " y = " + self.format - if self.suffix is not None: - fmt = fmt + self.suffix - self.textItem.setText(fmt.format(self.value()), color=self.textColor) - posX = xmin+pos*(xmax-xmin) - self._exactPos = Point(posX, self.value()) - if self.draggableLabel: GraphicsObject.setPos(self, Point(self.p)) - else: # precise location needed - GraphicsObject.setPos(self, self._exactPos) + self.sigPositionChanged.emit(self) def getXPos(self): return self.p[0] @@ -354,8 +308,7 @@ class InfiniteLine(GraphicsObject): (eg, the view range has changed or the view was resized) """ self._invalidateCache() - if isinstance(self.getViewBox(), ViewBox) and self.textItem is not None: - self.updateText() + self.textItem.updatePosition() def showLabel(self, state): """ @@ -367,12 +320,7 @@ class InfiniteLine(GraphicsObject): state If True, the label is shown. Otherwise, it is hidden. ============== ====================================================== """ - if state: - self.textItem = TextItem(fill=self.textFill) - self.textItem.setParentItem(self) - self.viewTransformChanged() - else: - self.textItem = None + self.textItem.setVisible(state) def setTextLocation(self, position=0.05, shift=0.5): """ @@ -388,10 +336,9 @@ class InfiniteLine(GraphicsObject): shift float (range of value = [0-1]). ============== ====================================================== """ - pos = np.clip(position, 0, 1) - shift = np.clip(shift, 0, 1) - self.textPosition = [pos, shift] - self.update() + self.textItem.orthoPos = position + self.textItem.shiftPos = shift + self.textItem.updatePosition() def setDraggableLabel(self, state): """ @@ -399,11 +346,93 @@ class InfiniteLine(GraphicsObject): of the line. If True, then the location of the label change during the dragging of the line. """ - self.draggableLabel = state - self.update() + self.textItem.setMovable(state) def setName(self, name): self._name = name def name(self): return self._name + + +class InfLineLabel(TextItem): + # a text label that attaches itself to an InfiniteLine + def __init__(self, line, **kwds): + self.line = line + self.movable = False + self.dragAxis = None # 0=x, 1=y + self.orthoPos = 0.5 # text will always be placed on the line at a position relative to view bounds + self.format = "{value}" + 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): + view = self.getViewBox() + if not self.isVisible() or not isinstance(view, ViewBox): + # not in a viewbox, skip update + return + + # 1. determine view extents along line axis + tr = view.childGroup.itemTransform(self.line)[0] + vr = tr.mapRect(view.viewRect()) + pt1 = Point(vr.left(), 0) + pt2 = Point(vr.right(), 0) + + # 2. pick relative point between extents and set position + 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 = self._startPosition + rel - self._cursorOffset + 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 _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() + + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index d3c98006..5474b90c 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -41,7 +41,7 @@ class TextItem(UIGraphicsItem): 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.textItem.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport def setText(self, text, color=(200,200,200)): """ @@ -114,22 +114,10 @@ class TextItem(UIGraphicsItem): s = self._exportOpts['resolutionScale'] self.textItem.scale(s, s) - #br = self.textItem.mapRectToParent(self.textItem.boundingRect()) + self.textItem.setTransform(self.sceneTransform().inverted()[0]) 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()) + self.textItem.setPos(-self.textItem.mapToParent(Point(0,0))) - #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() From a8510c335403f7f7fa48afc347e9bc191fd1994d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 19 Feb 2016 09:33:47 -0800 Subject: [PATCH 14/17] clean up textitem, fix anchoring --- pyqtgraph/graphicsItems/TextItem.py | 68 ++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 5474b90c..c29b4f44 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -32,7 +32,7 @@ class TextItem(UIGraphicsItem): UIGraphicsItem.__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,7 +40,7 @@ class TextItem(UIGraphicsItem): self.setHtml(html) self.fill = fn.mkBrush(fill) self.border = fn.mkPen(border) - self.rotate(angle) + 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,36 +100,41 @@ class TextItem(UIGraphicsItem): self.textItem.setFont(*args) self.updateText() - #def setAngle(self, angle): - #self.angle = angle - #self.updateText() - + def setAngle(self, angle): + self.textItem.resetTransform() + self.textItem.rotate(angle) + self.updateText() 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) - - self.textItem.setTransform(self.sceneTransform().inverted()[0]) - self.textItem.setPos(0,0) - self.textItem.setPos(-self.textItem.mapToParent(Point(0,0))) + ### 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) 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) @@ -137,4 +142,25 @@ 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()) + self.setTransform(t) + + self._lastTransform = pt \ No newline at end of file From 069a5bfeeaf2ea412176981c59df023c0231efaf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 21 Feb 2016 00:17:17 -0800 Subject: [PATCH 15/17] Labels can rotate with line --- examples/plottingItems.py | 12 +-- examples/text.py | 2 +- pyqtgraph/graphicsItems/InfiniteLine.py | 110 +++++++----------------- pyqtgraph/graphicsItems/TextItem.py | 42 ++++++--- 4 files changed, 67 insertions(+), 99 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index ffb808b5..a7926826 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -17,12 +17,14 @@ win.resize(1000,600) 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, textPosition=[0.5, 0.2], textColor=(200,200,100), textFill=(200,200,200,50)) -inf2 = pg.InfiniteLine(movable=True, angle=0, label=True, pen=(0, 0, 200), textColor=(200,0,0), bounds = [-2, 2], suffix="mm", hoverPen=(0,200,0), draggableLabel=True, textFill=0.5) -inf3 = pg.InfiniteLine(movable=True, angle=45) +inf1 = pg.InfiniteLine(movable=True, angle=90, text='x={value:0.2f}', + textOpts={'position':0.2, 'color': (200,200,100), 'fill': (200,200,200,50)}) +inf2 = pg.InfiniteLine(movable=True, angle=0, pen=(0, 0, 200), bounds = [-2, 2], hoverPen=(0,200,0), text='y={value:0.2f}mm', + textOpts={'color': (200,0,0), 'movable': True, 'fill': 0.5}) +inf3 = pg.InfiniteLine(movable=True, angle=45, text='diagonal', textOpts={'rotateAxis': [1, 0]}) inf1.setPos([2,2]) -inf1.setTextLocation(position=0.75) -inf2.setTextLocation(shift=0.8) +#inf1.setTextLocation(position=0.75) +#inf2.setTextLocation(shift=0.8) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) 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 f4b25860..e7cc12ce 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -31,9 +31,7 @@ class InfiniteLine(GraphicsObject): sigPositionChanged = QtCore.Signal(object) def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, - hoverPen=None, label=False, textColor=None, textFill=None, - textPosition=[0.05, 0.5], textFormat="{:.3f}", draggableLabel=False, - suffix=None, name='InfiniteLine'): + hoverPen=None, text=None, textOpts=None, name=None): """ =============== ================================================================== **Arguments:** @@ -49,21 +47,12 @@ class InfiniteLine(GraphicsObject): 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. - textPosition list of float (0-1) that defines when the precise location of the - label. The first float governs the location of the label in the - direction of the line, whereas the second one governs the shift - of the label from one side of the line to the other in the - orthogonal direction. - textFormat Any new python 3 str.format() format. - draggableLabel Bool. If True, the user can relocate the label during the dragging. - If set to True, the first entry of textPosition is no longer - useful. - suffix If not None, corresponds to the unit to show next to the label - name name of the item + 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 =============== ================================================================== """ @@ -79,18 +68,10 @@ class InfiniteLine(GraphicsObject): self.p = [0, 0] self.setAngle(angle) - if textColor is None: - textColor = (200, 200, 100) - self.textColor = textColor - self.textFill = textFill - self.textPosition = textPosition - self.suffix = suffix - - - self.textItem = InfLineLabel(self, fill=textFill) - self.textItem.setParentItem(self) - self.setDraggableLabel(draggableLabel) - self.showLabel(label) + 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) @@ -110,8 +91,6 @@ class InfiniteLine(GraphicsObject): self.setHoverPen(hoverPen) self.currentPen = self.pen - self.format = textFormat - self._name = name # Cache complex value for drawing speed-up (#PR267) @@ -308,46 +287,7 @@ class InfiniteLine(GraphicsObject): (eg, the view range has changed or the view was resized) """ self._invalidateCache() - self.textItem.updatePosition() - - 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. - ============== ====================================================== - """ - self.textItem.setVisible(state) - - def setTextLocation(self, position=0.05, shift=0.5): - """ - Set the parameters that defines the location of the label on the axis. - The position *parameter* governs the location of the label in the - direction of the line, whereas the *shift* governs the shift of the - label from one side of the line to the other in the orthogonal - direction. - - ============== ====================================================== - **Arguments:** - position float (range of value = [0-1]) - shift float (range of value = [0-1]). - ============== ====================================================== - """ - self.textItem.orthoPos = position - self.textItem.shiftPos = shift - self.textItem.updatePosition() - - def setDraggableLabel(self, state): - """ - Set the state of the label regarding its behaviour during the dragging - of the line. If True, then the location of the label change during the - dragging of the line. - """ - self.textItem.setMovable(state) - + def setName(self, name): self._name = name @@ -356,13 +296,21 @@ class InfiniteLine(GraphicsObject): class InfLineLabel(TextItem): - # a text label that attaches itself to an InfiniteLine - def __init__(self, line, **kwds): + """ + 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 = False - self.dragAxis = None # 0=x, 1=y - self.orthoPos = 0.5 # text will always be placed on the line at a position relative to view bounds - self.format = "{value}" + 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() @@ -412,7 +360,7 @@ class InfLineLabel(TextItem): return rel = self._posToRel(ev.pos()) - self.orthoPos = self._startPosition + rel - self._cursorOffset + self.orthoPos = np.clip(self._startPosition + rel - self._cursorOffset, 0, 1) self.updatePosition() if ev.isFinish(): self._moving = False @@ -427,6 +375,10 @@ class InfLineLabel(TextItem): 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() diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index c29b4f44..657e425b 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=None): """ ============== ================================================================================= **Arguments:** @@ -20,16 +23,19 @@ 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 @@ -101,9 +107,8 @@ class TextItem(UIGraphicsItem): self.updateText() def setAngle(self, angle): - self.textItem.resetTransform() - self.textItem.rotate(angle) - self.updateText() + self.angle = angle + self.updateTransform() def updateText(self): # update text position to obey anchor @@ -120,9 +125,6 @@ class TextItem(UIGraphicsItem): #s = self._exportOpts['resolutionScale'] #self.textItem.scale(s, s) - def viewRangeChanged(self): - self.updateText() - def boundingRect(self): return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect() @@ -160,7 +162,19 @@ class TextItem(UIGraphicsItem): 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 From f3a584b8b72528576c6b208ffe7e8b69d745b24b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 21 Feb 2016 23:18:01 -0800 Subject: [PATCH 16/17] label correctly follows oblique lines --- pyqtgraph/graphicsItems/InfiniteLine.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index e7cc12ce..2a72f848 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -328,14 +328,25 @@ class InfLineLabel(TextItem): # not in a viewbox, skip update return - # 1. determine view extents along line axis - tr = view.childGroup.itemTransform(self.line)[0] - vr = tr.mapRect(view.viewRect()) - pt1 = Point(vr.left(), 0) - pt2 = Point(vr.right(), 0) - - # 2. pick relative point between extents and set position + 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): From 170592c29431f9d9660e2f193adf98242c054fae Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 21 Feb 2016 23:28:24 -0800 Subject: [PATCH 17/17] update example --- examples/plottingItems.py | 13 +++++++------ pyqtgraph/graphicsItems/InfiniteLine.py | 2 ++ pyqtgraph/graphicsItems/TextItem.py | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index a7926826..d90d81ab 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -16,12 +16,13 @@ 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)) +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.2, 'color': (200,200,100), 'fill': (200,200,200,50)}) -inf2 = pg.InfiniteLine(movable=True, angle=0, pen=(0, 0, 200), bounds = [-2, 2], hoverPen=(0,200,0), text='y={value:0.2f}mm', - textOpts={'color': (200,0,0), 'movable': True, 'fill': 0.5}) -inf3 = pg.InfiniteLine(movable=True, angle=45, text='diagonal', textOpts={'rotateAxis': [1, 0]}) + 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) @@ -29,7 +30,7 @@ p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) -lr = pg.LinearRegionItem(values=[5, 10]) +lr = pg.LinearRegionItem(values=[70, 80]) p1.addItem(lr) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 2a72f848..de7f99f6 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -323,6 +323,8 @@ class InfLineLabel(TextItem): 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 diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 657e425b..220d5859 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -10,7 +10,7 @@ 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, rotateAxis=None): + border=None, fill=None, angle=0, rotateAxis=(1, 0)): """ ============== ================================================================================= **Arguments:**