pyqtgraph/pyqtgraph/graphicsItems/InfiniteLine.py

386 lines
14 KiB
Python
Raw Normal View History

from ..Qt import QtGui, QtCore
from ..Point import Point
2016-02-03 11:52:01 +00:00
from .GraphicsObject import GraphicsObject
2016-01-15 15:10:24 +00:00
from .TextItem import TextItem
2016-02-03 11:52:01 +00:00
from .ViewBox import ViewBox
from .. import functions as fn
import numpy as np
import weakref
__all__ = ['InfiniteLine']
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
class InfiniteLine(GraphicsObject):
2016-01-15 15:10:24 +00:00
"""
2016-02-03 11:52:01 +00:00
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
2016-01-15 15:10:24 +00:00
Displays a line of infinite length.
This line may be dragged to indicate a position in data coordinates.
2016-01-15 15:10:24 +00:00
=============================== ===================================================
**Signals:**
sigDragged(self)
sigPositionChangeFinished(self)
sigPositionChanged(self)
=============================== ===================================================
"""
2016-01-15 15:10:24 +00:00
sigDragged = QtCore.Signal(object)
sigPositionChangeFinished = QtCore.Signal(object)
sigPositionChanged = QtCore.Signal(object)
2016-01-15 15:10:24 +00:00
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None,
hoverPen=None, label=False, textColor=None, textFill=None,
2016-02-03 11:52:01 +00:00
textLocation=[0.05,0.5], textFormat="{:.3f}",
suffix=None, name='InfiniteLine'):
"""
=============== ==================================================================
**Arguments:**
pos Position of the line. This can be a QPointF or a single value for
vertical/horizontal lines.
angle Angle of line in degrees. 0 is horizontal, 90 is vertical.
pen Pen to use when drawing line. Can be any arguments that are valid
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.
2016-01-15 15:10:24 +00:00
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.
2016-01-15 15:10:24 +00:00
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.
2016-02-03 11:52:01 +00:00
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.
2016-01-15 15:10:24 +00:00
textFormat Any new python 3 str.format() format.
2016-02-03 11:52:01 +00:00
suffix If not None, corresponds to the unit to show next to the label
name name of the item
=============== ==================================================================
"""
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
GraphicsObject.__init__(self)
2016-01-15 15:10:24 +00:00
if bounds is None: ## allowed value boundaries for orthogonal lines
self.maxRange = [None, None]
else:
self.maxRange = bounds
self.moving = False
2016-02-03 11:52:01 +00:00
self.setMovable(movable)
self.mouseHovering = False
2016-02-03 11:52:01 +00:00
self.p = [0, 0]
self.setAngle(angle)
2016-01-15 15:10:24 +00:00
if textColor is None:
2016-02-03 11:52:01 +00:00
textColor = (200, 200, 100)
2016-01-15 15:10:24 +00:00
self.textColor = textColor
2016-02-03 11:52:01 +00:00
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
2016-01-15 15:10:24 +00:00
self.anchorLeft = (1., 0.5)
self.anchorRight = (0., 0.5)
self.anchorUp = (0.5, 1.)
self.anchorDown = (0.5, 0.)
2016-02-03 11:52:01 +00:00
if pos is None:
pos = Point(0,0)
self.setPos(pos)
if pen is None:
pen = (200, 200, 100)
self.setPen(pen)
2016-01-15 15:10:24 +00:00
if hoverPen is None:
self.setHoverPen(color=(255,0,0), width=self.pen.width())
else:
self.setHoverPen(hoverPen)
self.currentPen = self.pen
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
self.format = textFormat
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
self._name = name
2016-01-15 15:10:24 +00:00
2016-02-04 02:28:59 +00:00
# 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
self.setAcceptHoverEvents(m)
2016-01-15 15:10:24 +00:00
def setBounds(self, bounds):
"""Set the (minimum, maximum) allowable values when dragging."""
self.maxRange = bounds
self.setValue(self.value())
2016-01-15 15:10:24 +00:00
def setPen(self, *args, **kwargs):
2016-01-15 15:10:24 +00:00
"""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)
2014-05-10 18:19:27 +00:00
if not self.mouseHovering:
self.currentPen = self.pen
self.update()
2016-01-15 15:10:24 +00:00
2014-05-10 18:19:27 +00:00
def setHoverPen(self, *args, **kwargs):
2016-01-15 15:10:24 +00:00
"""Set the pen for drawing the line while the mouse hovers over it.
Allowable arguments are any that are valid
2014-05-10 18:19:27 +00:00
for :func:`mkPen <pyqtgraph.mkPen>`.
2016-01-15 15:10:24 +00:00
2014-05-10 18:19:27 +00:00
If the line is not movable, then hovering is also disabled.
2016-01-15 15:10:24 +00:00
2014-05-10 18:19:27 +00:00
Added in version 0.9.9."""
self.hoverPen = fn.mkPen(*args, **kwargs)
if self.mouseHovering:
self.currentPen = self.hoverPen
self.update()
2016-01-15 15:10:24 +00:00
def setAngle(self, angle):
"""
Takes angle argument in degrees.
0 is horizontal; 90 is vertical.
2016-01-15 15:10:24 +00:00
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
2016-02-03 11:52:01 +00:00
self.resetTransform()
self.rotate(self.angle)
self.update()
2016-01-15 15:10:24 +00:00
def setPos(self, pos):
2016-01-15 15:10:24 +00:00
if type(pos) in [list, tuple]:
newPos = pos
elif isinstance(pos, QtCore.QPointF):
newPos = [pos.x(), pos.y()]
else:
if self.angle == 90:
newPos = [pos, 0]
elif self.angle == 0:
newPos = [0, pos]
else:
raise Exception("Must specify 2D coordinate for non-orthogonal lines.")
2016-01-15 15:10:24 +00:00
## check bounds (only works for orthogonal lines)
if self.angle == 90:
2016-01-15 15:10:24 +00:00
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])
elif self.angle == 0:
if self.maxRange[0] is not None:
newPos[1] = max(newPos[1], self.maxRange[0])
if self.maxRange[1] is not None:
newPos[1] = min(newPos[1], self.maxRange[1])
2016-01-15 15:10:24 +00:00
if self.p != newPos:
self.p = newPos
2016-02-04 02:28:59 +00:00
self._invalidateCache()
2016-02-03 11:52:01 +00:00
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)
2016-02-03 11:52:01 +00:00
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]
2016-01-15 15:10:24 +00:00
def getYPos(self):
return self.p[1]
2016-01-15 15:10:24 +00:00
def getPos(self):
return self.p
def value(self):
2016-01-15 15:10:24 +00:00
"""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()
elif self.angle%180 == 90:
return self.getXPos()
else:
return self.getPos()
2016-01-15 15:10:24 +00:00
def setValue(self, v):
2016-01-15 15:10:24 +00:00
"""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)
## broken in 4.7
#def itemChange(self, change, val):
#if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]:
#self.updateLine()
#print "update", change
#print self.getBoundingParents()
#else:
#print "ignore", change
#return GraphicsObject.itemChange(self, change, val)
2016-01-15 15:10:24 +00:00
2016-02-04 02:28:59 +00:00
def _invalidateCache(self):
self._line = None
self._boundingRect = None
def boundingRect(self):
2016-02-04 02:28:59 +00:00
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
2016-01-15 15:10:24 +00:00
def paint(self, p, *args):
p.setPen(self.currentPen)
2016-02-04 02:28:59 +00:00
p.drawLine(self._line)
2016-01-15 15:10:24 +00:00
def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == 0:
return None ## x axis should never be auto-scaled
else:
return (0,0)
def mouseDragEvent(self, ev):
if self.movable and ev.button() == QtCore.Qt.LeftButton:
if ev.isStart():
self.moving = True
2016-02-03 11:52:01 +00:00
self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
self.startPosition = self.pos()
ev.accept()
2016-01-15 15:10:24 +00:00
if not self.moving:
return
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
self.setPos(self.cursorOffset + self.mapToParent(ev.pos()))
self.sigDragged.emit(self)
if ev.isFinish():
self.moving = False
self.sigPositionChangeFinished.emit(self)
2016-01-15 15:10:24 +00:00
def mouseClickEvent(self, ev):
if self.moving and ev.button() == QtCore.Qt.RightButton:
ev.accept()
self.setPos(self.startPosition)
self.moving = False
self.sigDragged.emit(self)
self.sigPositionChangeFinished.emit(self)
def hoverEvent(self, ev):
if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton):
self.setMouseHover(True)
else:
self.setMouseHover(False)
def setMouseHover(self, hover):
2014-05-10 18:19:27 +00:00
## Inform the item that the mouse is (not) hovering over it
if self.mouseHovering == hover:
return
self.mouseHovering = hover
if hover:
2014-05-10 18:19:27 +00:00
self.currentPen = self.hoverPen
else:
self.currentPen = self.pen
self.update()
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
def viewTransformChanged(self):
"""
Called whenever the transformation matrix of the view has changed.
(eg, the view range has changed or the view was resized)
"""
2016-02-04 02:28:59 +00:00
self._invalidateCache()
2016-02-03 11:52:01 +00:00
if self.getViewBox() is not None and isinstance(self.getViewBox(), ViewBox) and self.textItem is not None:
self.updateTextPosition()
2016-01-15 15:10:24 +00:00
def showLabel(self, state):
"""
Display or not the label indicating the location of the line in data
coordinates.
2016-02-05 10:54:00 +00:00
============== ======================================================
2016-01-15 15:10:24 +00:00
**Arguments:**
state If True, the label is shown. Otherwise, it is hidden.
2016-02-05 10:54:00 +00:00
============== ======================================================
2016-01-15 15:10:24 +00:00
"""
if state:
2016-02-03 11:52:01 +00:00
self.textItem = TextItem(fill=self.textFill)
self.textItem.setParentItem(self)
self.viewTransformChanged()
2016-01-15 15:10:24 +00:00
else:
2016-02-03 11:52:01 +00:00
self.textItem = None
2016-01-15 15:10:24 +00:00
2016-02-03 11:52:01 +00:00
def setTextLocation(self, loc):
2016-01-15 15:10:24 +00:00
"""
2016-02-03 11:52:01 +00:00
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.
2016-01-15 15:10:24 +00:00
"""
2016-02-03 11:52:01 +00:00
self.textLocation = [np.clip(loc[0], 0, 1), np.clip(loc[1], 0, 1)]
2016-01-15 15:10:24 +00:00
self.update()
def setName(self, name):
self._name = name
def name(self):
return self._name