Infiniteline enhancement

This commit is contained in:
lesauxvi 2016-01-15 16:10:24 +01:00
parent 9e8c2082ed
commit ce36ea4eb6
3 changed files with 377 additions and 102 deletions

35
examples/plottingItems.py Normal file
View File

@ -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_()

View File

@ -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 <pyqtgraph.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 <pyqtgraph.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 <pyqtgraph.mkPen>`. Default pen is transparent
yellow.
movable If True, the line can be dragged to a new position by the user.
hoverPen Pen to use when drawing line when hovering over it. Can be any
arguments that are valid for :func:`mkPen <pyqtgraph.mkPen>`.
Default pen is red.
bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal.
label 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 <pyqtgraph.mkPen>`."""
self.pen = fn.mkPen(*args, **kwargs)
if not self.mouseHovering:
self.currentPen = self.pen
self.update()
def setHoverPen(self, *args, **kwargs):
"""Set the pen for drawing the line while the mouse hovers over it.
Allowable arguments are any that are valid
"""Set the pen for drawing the line while the mouse hovers over it.
Allowable arguments are any that are valid
for :func:`mkPen <pyqtgraph.mkPen>`.
If the line is not movable, then hovering is also disabled.
Added in version 0.9.9."""
self.hoverPen = fn.mkPen(*args, **kwargs)
if self.mouseHovering:
self.currentPen = self.hoverPen
self.update()
def setAngle(self, angle):
"""
Takes angle argument in degrees.
0 is horizontal; 90 is vertical.
Note that the use of value() and setValue() changes if the line is
Note that the use of value() and setValue() changes if the line is
not vertical or horizontal.
"""
self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135
self.resetTransform()
self.rotate(self.angle)
# self.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

View File

@ -9,10 +9,10 @@ __all__ = ['LinearRegionItem']
class LinearRegionItem(UIGraphicsItem):
"""
**Bases:** :class:`UIGraphicsItem <pyqtgraph.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 <pyqtgraph.LinearRegionItem.setRegion>` to set the position
Does not affect the current position of the region unless it is outside the new bounds.
See :func:`setRegion <pyqtgraph.LinearRegionItem.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()