From 905a541253845eb8176792631fb40d7622eddce6 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 15 Jan 2016 09:17:52 +0100 Subject: [PATCH 01/33] new markers --- examples/Markers.py | 34 +++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 263 +++++++++++---------- 2 files changed, 171 insertions(+), 126 deletions(-) create mode 100755 examples/Markers.py diff --git a/examples/Markers.py b/examples/Markers.py new file mode 100755 index 00000000..304aa3fd --- /dev/null +++ b/examples/Markers.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +This example shows all the markers available into 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="Pyqtgraph markers") +win.resize(1000,600) + +pg.setConfigOptions(antialias=True) + +plot = win.addPlot(title="Plotting with markers") +plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o') +plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t') +plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1') +plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2') +plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3') +plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s') +plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p') +plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h') +plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star') +plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+') +plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d') + +## 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/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index e6be9acd..11ebfd37 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -19,17 +19,28 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { 't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)], + 't1': [(-0.5, 0.5), (0, -0.5), (0.5, 0.5)], + 't2': [(-0.5, -0.5), (-0.5, 0.5), (0.5, 0)], + 't3': [(0.5, 0.5), (0.5, -0.5), (-0.5, 0)], 'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)], '+': [ (-0.5, -0.05), (-0.5, 0.05), (-0.05, 0.05), (-0.05, 0.5), - (0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05), + (0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05), (0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05) ], + 'p': [(0, -0.5), (-0.4755, -0.1545), (-0.2939, 0.4045), + (0.2939, 0.4045), (0.4755, -0.1545)], + 'h': [(0.433, 0.25), (0., 0.5), (-0.433, 0.25), (-0.433, -0.25), + (0, -0.5), (0.433, -0.25)], + 'star': [(0, -0.5), (-0.1123, -0.1545), (-0.4755, -0.1545), + (-0.1816, 0.059), (-0.2939, 0.4045), (0, 0.1910), + (0.2939, 0.4045), (0.1816, 0.059), (0.4755, -0.1545), + (0.1123, -0.1545)] } for k, c in coords.items(): Symbols[k].moveTo(*c[0]) @@ -40,7 +51,7 @@ tr = QtGui.QTransform() tr.rotate(45) Symbols['x'] = tr.map(Symbols['+']) - + def drawSymbol(painter, symbol, size, pen, brush): if symbol is None: return @@ -53,13 +64,13 @@ def drawSymbol(painter, symbol, size, pen, brush): symbol = list(Symbols.values())[symbol % len(Symbols)] painter.drawPath(symbol) - + def renderSymbol(symbol, size, pen, brush, device=None): """ Render a symbol specification to QImage. Symbol may be either a QPainterPath or one of the keys in the Symbols dict. If *device* is None, a new QPixmap will be returned. Otherwise, - the symbol will be rendered into the device specified (See QPainter documentation + the symbol will be rendered into the device specified (See QPainter documentation for more information). """ ## Render a spot with the given parameters to a pixmap @@ -80,33 +91,33 @@ def makeSymbolPixmap(size, pen, brush, symbol): ## deprecated img = renderSymbol(symbol, size, pen, brush) return QtGui.QPixmap(img) - + class SymbolAtlas(object): """ Used to efficiently construct a single QPixmap containing all rendered symbols for a ScatterPlotItem. This is required for fragment rendering. - + Use example: atlas = SymbolAtlas() sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..)) sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..)) pm = atlas.getAtlas() - + """ def __init__(self): # symbol key : QRect(...) coordinates where symbol can be found in atlas. - # note that the coordinate list will always be the same list object as + # note that the coordinate list will always be the same list object as # long as the symbol is in the atlas, but the coordinates may # change if the atlas is rebuilt. - # weak value; if all external refs to this list disappear, + # weak value; if all external refs to this list disappear, # the symbol will be forgotten. self.symbolMap = weakref.WeakValueDictionary() - + self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap self.atlasValid = False self.max_width=0 - + def getSymbolCoords(self, opts): """ Given a list of spot records, return an object representing the coordinates of that symbol within the atlas @@ -131,7 +142,7 @@ class SymbolAtlas(object): keyi = key sourceRecti = newRectSrc return sourceRect - + def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width rendered = {} @@ -150,7 +161,7 @@ class SymbolAtlas(object): w = arr.shape[0] avgWidth += w maxWidth = max(maxWidth, w) - + nSymbols = len(rendered) if nSymbols > 0: avgWidth /= nSymbols @@ -158,10 +169,10 @@ class SymbolAtlas(object): else: avgWidth = 0 width = 0 - + # sort symbols by height symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True) - + self.atlasRows = [] x = width @@ -187,7 +198,7 @@ class SymbolAtlas(object): self.atlas = None self.atlasValid = True self.max_width = maxWidth - + def getAtlas(self): if not self.atlasValid: self.buildAtlas() @@ -197,27 +208,27 @@ class SymbolAtlas(object): img = fn.makeQImage(self.atlasData, copy=False, transpose=False) self.atlas = QtGui.QPixmap(img) return self.atlas - - - - + + + + class ScatterPlotItem(GraphicsObject): """ Displays a set of x/y points. Instances of this class are created automatically as part of PlotDataItem; these rarely need to be instantiated directly. - - The size, shape, pen, and fill brush may be set for each point individually - or for all points. - - + + The size, shape, pen, and fill brush may be set for each point individually + or for all points. + + ======================== =============================================== **Signals:** sigPlotChanged(self) Emitted when the data being plotted has changed sigClicked(self, points) Emitted when the curve is clicked. Sends a list of all the points under the mouse pointer. ======================== =============================================== - + """ #sigPointClicked = QtCore.Signal(object, object) sigClicked = QtCore.Signal(object, object) ## self, points @@ -228,17 +239,17 @@ class ScatterPlotItem(GraphicsObject): """ profiler = debug.Profiler() GraphicsObject.__init__(self) - + self.picture = None # QPicture used for rendering when pxmode==False self.fragmentAtlas = SymbolAtlas() - + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots self.opts = { - 'pxMode': True, - 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. + 'pxMode': True, + 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': getConfigOption('antialias'), 'name': None, } @@ -252,14 +263,14 @@ class ScatterPlotItem(GraphicsObject): profiler('setData') #self.setCacheMode(self.DeviceCoordinateCache) - + def setData(self, *args, **kargs): """ **Ordered Arguments:** - + * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. - + ====================== =============================================================================================== **Keyword Arguments:** *spots* Optional list of dicts. Each dict specifies parameters for a single spot: @@ -285,8 +296,8 @@ class ScatterPlotItem(GraphicsObject): it is in the item's local coordinate system. *data* a list of python objects used to uniquely identify each spot. *identical* *Deprecated*. This functionality is handled automatically now. - *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are - always rendered with antialiasing (since the rendered symbols can be cached, this + *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are + always rendered with antialiasing (since the rendered symbols can be cached, this incurs very little performance cost) *name* The name of this item. Names are used for automatically generating LegendItem entries and by some exporters. @@ -298,10 +309,10 @@ class ScatterPlotItem(GraphicsObject): def addPoints(self, *args, **kargs): """ - Add new points to the scatter plot. + Add new points to the scatter plot. Arguments are the same as setData() """ - + ## deal with non-keyword arguments if len(args) == 1: kargs['spots'] = args[0] @@ -310,7 +321,7 @@ class ScatterPlotItem(GraphicsObject): kargs['y'] = args[1] elif len(args) > 2: raise Exception('Only accepts up to two non-keyword arguments.') - + ## convert 'pos' argument to 'x' and 'y' if 'pos' in kargs: pos = kargs['pos'] @@ -329,7 +340,7 @@ class ScatterPlotItem(GraphicsObject): y.append(p[1]) kargs['x'] = x kargs['y'] = y - + ## determine how many spots we have if 'spots' in kargs: numPts = len(kargs['spots']) @@ -339,16 +350,16 @@ class ScatterPlotItem(GraphicsObject): kargs['x'] = [] kargs['y'] = [] numPts = 0 - + ## Extend record array oldData = self.data self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype) ## note that np.empty initializes object fields to None and string fields to '' - + self.data[:len(oldData)] = oldData #for i in range(len(oldData)): #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array - + newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size @@ -376,12 +387,12 @@ class ScatterPlotItem(GraphicsObject): elif 'y' in kargs: newData['x'] = kargs['x'] newData['y'] = kargs['y'] - + if 'pxMode' in kargs: self.setPxMode(kargs['pxMode']) if 'antialias' in kargs: self.opts['antialias'] = kargs['antialias'] - + ## Set any extra parameters provided in keyword arguments for k in ['pen', 'brush', 'symbol', 'size']: if k in kargs: @@ -397,32 +408,32 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() self.updateSpots(newData) self.sigPlotChanged.emit(self) - + def invalidate(self): ## clear any cached drawing state self.picture = None self.update() - + def getData(self): - return self.data['x'], self.data['y'] - + return self.data['x'], self.data['y'] + def setPoints(self, *args, **kargs): ##Deprecated; use setData return self.setData(*args, **kargs) - + def implements(self, interface=None): ints = ['plotData'] if interface is None: return ints return interface in ints - + def name(self): return self.opts.get('name', None) - + def setPen(self, *args, **kargs): - """Set the pen(s) used to draw the outline around each spot. + """Set the pen(s) used to draw the outline around each spot. If a list or array is provided, then the pen for each spot will be set separately. - Otherwise, the arguments are passed to pg.mkPen and used as the default pen for + Otherwise, the arguments are passed to pg.mkPen and used as the default pen for all spots which do not have a pen explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) @@ -436,19 +447,19 @@ class ScatterPlotItem(GraphicsObject): dataSet['pen'] = pens else: self.opts['pen'] = fn.mkPen(*args, **kargs) - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setBrush(self, *args, **kargs): - """Set the brush(es) used to fill the interior of each spot. + """Set the brush(es) used to fill the interior of each spot. If a list or array is provided, then the brush for each spot will be set separately. - Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for + Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for all spots which do not have a brush explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) - + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): brushes = args[0] if 'mask' in kargs and kargs['mask'] is not None: @@ -459,19 +470,19 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['brush'] = fn.mkBrush(*args, **kargs) #self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) def setSymbol(self, symbol, update=True, dataSet=None, mask=None): - """Set the symbol(s) used to draw each spot. + """Set the symbol(s) used to draw each spot. If a list or array is provided, then the symbol for each spot will be set separately. - Otherwise, the argument will be used as the default symbol for + Otherwise, the argument will be used as the default symbol for all spots which do not have a symbol explicitly set.""" if dataSet is None: dataSet = self.data - + if isinstance(symbol, np.ndarray) or isinstance(symbol, list): symbols = symbol if mask is not None: @@ -482,19 +493,19 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['symbol'] = symbol self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setSize(self, size, update=True, dataSet=None, mask=None): - """Set the size(s) used to draw each spot. + """Set the size(s) used to draw each spot. If a list or array is provided, then the size for each spot will be set separately. - Otherwise, the argument will be used as the default size for + Otherwise, the argument will be used as the default size for all spots which do not have a size explicitly set.""" if dataSet is None: dataSet = self.data - + if isinstance(size, np.ndarray) or isinstance(size, list): sizes = size if mask is not None: @@ -505,21 +516,21 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['size'] = size self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setPointData(self, data, dataSet=None, mask=None): if dataSet is None: dataSet = self.data - + if isinstance(data, np.ndarray) or isinstance(data, list): if mask is not None: data = data[mask] if len(data) != len(dataSet): raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) - + ## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time. ## (otherwise they are converted to tuples and thus lose their field names. if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1: @@ -527,14 +538,14 @@ class ScatterPlotItem(GraphicsObject): dataSet['data'][i] = rec else: dataSet['data'] = data - + def setPxMode(self, mode): if self.opts['pxMode'] == mode: return - + self.opts['pxMode'] = mode self.invalidate() - + def updateSpots(self, dataSet=None): if dataSet is None: dataSet = self.data @@ -547,9 +558,9 @@ class ScatterPlotItem(GraphicsObject): opts = self.getSpotOpts(dataSet[mask]) sourceRect = self.fragmentAtlas.getSymbolCoords(opts) dataSet['sourceRect'][mask] = sourceRect - + self.fragmentAtlas.getAtlas() # generate atlas so source widths are available. - + dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2 dataSet['targetRect'] = None self._maxSpotPxWidth = self.fragmentAtlas.max_width @@ -585,9 +596,9 @@ class ScatterPlotItem(GraphicsObject): recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) return recs - - - + + + def measureSpotSizes(self, dataSet): for rec in dataSet: ## keep track of the maximum spot size and pixel size @@ -605,8 +616,8 @@ class ScatterPlotItem(GraphicsObject): self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] - - + + def clear(self): """Remove all spots from the scatter plot""" #self.clearItems() @@ -617,23 +628,23 @@ class ScatterPlotItem(GraphicsObject): def dataBounds(self, ax, frac=1.0, orthoRange=None): if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None: return self.bounds[ax] - + #self.prepareGeometryChange() if self.data is None or len(self.data) == 0: return (None, None) - + if ax == 0: d = self.data['x'] d2 = self.data['y'] elif ax == 1: d = self.data['y'] d2 = self.data['x'] - + if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] d2 = d2[mask] - + if frac >= 1.0: self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072) return self.bounds[ax] @@ -656,11 +667,11 @@ class ScatterPlotItem(GraphicsObject): if ymn is None or ymx is None: ymn = 0 ymx = 0 - + px = py = 0.0 pxPad = self.pixelPadding() if pxPad > 0: - # determine length of pixel in local x, y directions + # determine length of pixel in local x, y directions px, py = self.pixelVectors() try: px = 0 if px is None else px.length() @@ -670,7 +681,7 @@ class ScatterPlotItem(GraphicsObject): py = 0 if py is None else py.length() except OverflowError: py = 0 - + # return bounds expanded by pixel size px *= pxPad py *= pxPad @@ -688,7 +699,7 @@ class ScatterPlotItem(GraphicsObject): def mapPointsToDevice(self, pts): - # Map point locations to device + # Map point locations to device tr = self.deviceTransform() if tr is None: return None @@ -699,7 +710,7 @@ class ScatterPlotItem(GraphicsObject): pts = fn.transformCoordinates(tr, pts) pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. - + return pts def getViewMask(self, pts): @@ -713,48 +724,48 @@ class ScatterPlotItem(GraphicsObject): mask = ((pts[0] + w > viewBounds.left()) & (pts[0] - w < viewBounds.right()) & (pts[1] + w > viewBounds.top()) & - (pts[1] - w < viewBounds.bottom())) ## remove out of view points + (pts[1] - w < viewBounds.bottom())) ## remove out of view points return mask - - + + @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) - + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed else: aa = self.opts['antialias'] scale = 1.0 - + if self.opts['pxMode'] is True: p.resetTransform() - + # Map point coordinates to device pts = np.vstack([self.data['x'], self.data['y']]) pts = self.mapPointsToDevice(pts) if pts is None: return - + # Cull points that are outside view viewMask = self.getViewMask(pts) #pts = pts[:,mask] #data = self.data[mask] - + if self.opts['useCache'] and self._exportOpts is False: # Draw symbols from pre-rendered atlas atlas = self.fragmentAtlas.getAtlas() - + # Update targetRects if necessary updateMask = viewMask & np.equal(self.data['targetRect'], None) if np.any(updateMask): updatePts = pts[:,updateMask] width = self.data[updateMask]['width']*2 self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) - + data = self.data[viewMask] if USE_PYSIDE or USE_PYQT5: list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) @@ -782,16 +793,16 @@ class ScatterPlotItem(GraphicsObject): p2.translate(rec['x'], rec['y']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() - + p.setRenderHint(p.Antialiasing, aa) self.picture.play(p) - + def points(self): for rec in self.data: if rec['item'] is None: rec['item'] = SpotItem(rec, self) return self.data['item'] - + def pointsAt(self, pos): x = pos.x() y = pos.y() @@ -814,7 +825,7 @@ class ScatterPlotItem(GraphicsObject): #print "No hit:", (x, y), (sx, sy) #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) return pts[::-1] - + def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: @@ -833,7 +844,7 @@ class ScatterPlotItem(GraphicsObject): class SpotItem(object): """ Class referring to individual spots in a scatter plot. - These can be retrieved by calling ScatterPlotItem.points() or + These can be retrieved by calling ScatterPlotItem.points() or by connecting to the ScatterPlotItem's click signals. """ @@ -844,34 +855,34 @@ class SpotItem(object): #self.setParentItem(plot) #self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.updateItem() - + def data(self): """Return the user data associated with this spot.""" return self._data['data'] - + def size(self): - """Return the size of this spot. + """Return the size of this spot. If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" if self._data['size'] == -1: return self._plot.opts['size'] else: return self._data['size'] - + def pos(self): return Point(self._data['x'], self._data['y']) - + def viewPos(self): return self._plot.mapToView(self.pos()) - + def setSize(self, size): - """Set the size of this spot. - If the size is set to -1, then the ScatterPlotItem's default size + """Set the size of this spot. + If the size is set to -1, then the ScatterPlotItem's default size will be used instead.""" self._data['size'] = size self.updateItem() - + def symbol(self): - """Return the symbol of this spot. + """Return the symbol of this spot. If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. """ symbol = self._data['symbol'] @@ -883,7 +894,7 @@ class SpotItem(object): except: pass return symbol - + def setSymbol(self, symbol): """Set the symbol for this spot. If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" @@ -895,35 +906,35 @@ class SpotItem(object): if pen is None: pen = self._plot.opts['pen'] return fn.mkPen(pen) - + def setPen(self, *args, **kargs): """Set the outline pen for this spot""" pen = fn.mkPen(*args, **kargs) self._data['pen'] = pen self.updateItem() - + def resetPen(self): """Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) self.updateItem() - + def brush(self): brush = self._data['brush'] if brush is None: brush = self._plot.opts['brush'] return fn.mkBrush(brush) - + def setBrush(self, *args, **kargs): """Set the fill brush for this spot""" brush = fn.mkBrush(*args, **kargs) self._data['brush'] = brush self.updateItem() - + def resetBrush(self): """Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) self.updateItem() - + def setData(self, data): """Set the user-data associated with this spot""" self._data['data'] = data @@ -938,14 +949,14 @@ class SpotItem(object): #QtGui.QGraphicsPixmapItem.__init__(self) #self.setFlags(self.flags() | self.ItemIgnoresTransformations) #SpotItem.__init__(self, data, plot) - + #def setPixmap(self, pixmap): #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) - + #def updateItem(self): #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) - + ### If all symbol options are default, use default pixmap #if symbolOpts == (None, None, -1, ''): #pixmap = self._plot.defaultSpotPixmap() From ce36ea4eb63b22c9e9823ee3acfb9d3e8fb6cc79 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 15 Jan 2016 16:10:24 +0100 Subject: [PATCH 02/33] 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 03/33] 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 04/33] 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 98ff70e8a04094d718d95508fa10e63323abe73b Mon Sep 17 00:00:00 2001 From: Alessandro Bacchini Date: Tue, 2 Feb 2016 15:31:48 +0100 Subject: [PATCH 05/33] Improve drawing performance by caching the line and bounding rect. --- pyqtgraph/graphicsItems/InfiniteLine.py | 48 +++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 240dfe97..6984a7a4 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -63,6 +63,9 @@ class InfiniteLine(GraphicsObject): self.setPen(pen) self.setHoverPen(color=(255,0,0), width=self.pen.width()) self.currentPen = self.pen + + self._boundingRect = None + self._line = None def setMovable(self, m): """Set whether the line is movable by the user.""" @@ -135,6 +138,10 @@ class InfiniteLine(GraphicsObject): newPos[1] = min(newPos[1], self.maxRange[1]) if self.p != newPos: + # Invalidate bounding rect and line + self._boundingRect = None + self._line = None + self.p = newPos GraphicsObject.setPos(self, Point(self.p)) self.update() @@ -174,24 +181,37 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) - + + def viewTransformChanged(self): + self._boundingRect = None + self._line = None + GraphicsObject.viewTransformChanged(self) + + def viewChanged(self, view, oldView): + self._boundingRect = None + self._line = None + GraphicsObject.viewChanged(self, view, oldView) + def boundingRect(self): - #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() + 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, br.left(), 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: From 51b8be2bd17aacfdeba048736bdf26652fed56ef Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Wed, 3 Feb 2016 12:52:01 +0100 Subject: [PATCH 06/33] 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 07/33] 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 2b9f613eab82494c3d70a3a90067748e061f3fb0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 3 Feb 2016 23:13:58 -0800 Subject: [PATCH 08/33] Added unit tests checking infiniteline interactivity --- pyqtgraph/GraphicsScene/GraphicsScene.py | 5 +- .../graphicsItems/tests/test_InfiniteLine.py | 41 ++++++++++++++ pyqtgraph/tests/__init__.py | 1 + pyqtgraph/tests/ui_testing.py | 55 +++++++++++++++++++ 4 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_InfiniteLine.py create mode 100644 pyqtgraph/tests/__init__.py create mode 100644 pyqtgraph/tests/ui_testing.py diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 840e3135..bab0f776 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -98,6 +98,7 @@ class GraphicsScene(QtGui.QGraphicsScene): self.lastDrag = None self.hoverItems = weakref.WeakKeyDictionary() self.lastHoverEvent = None + self.minDragTime = 0.5 # drags shorter than 0.5 sec are interpreted as clicks self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu[0].triggered.connect(self.showExportDialog) @@ -173,7 +174,7 @@ class GraphicsScene(QtGui.QGraphicsScene): if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] dist = Point(ev.screenPos() - cev.screenPos()) - if dist.length() < self._moveDistance and now - cev.time() < 0.5: + if dist.length() < self._moveDistance and now - cev.time() < self.minDragTime: continue init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True self.dragButtons.append(int(btn)) @@ -186,10 +187,8 @@ class GraphicsScene(QtGui.QGraphicsScene): def leaveEvent(self, ev): ## inform items that mouse is gone if len(self.dragButtons) == 0: self.sendHoverEvents(ev, exitOnly=True) - def mouseReleaseEvent(self, ev): - #print 'sceneRelease' if self.mouseGrabberItem() is None: if ev.button() in self.dragButtons: if self.sendDragEvent(ev, final=True): diff --git a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py new file mode 100644 index 00000000..53a4f6ea --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py @@ -0,0 +1,41 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtTest, QtGui, QtCore +from pyqtgraph.tests import mouseDrag +pg.mkQApp() + +qWait = QtTest.QTest.qWait + + +def test_mouseInteraction(): + plt = pg.plot() + plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. + vline = plt.addLine(x=0, movable=True) + plt.addItem(vline) + hline = plt.addLine(y=0, movable=True) + plt.setXRange(-10, 10) + plt.setYRange(-10, 10) + + # test horizontal drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(0,5)).toPoint() + pos2 = pos - QtCore.QPoint(200, 200) + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + px = vline.pixelLength(pg.Point(1, 0), ortho=True) + assert abs(vline.value() - plt.plotItem.vb.mapSceneToView(pos2).x()) <= px + + # test missed drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() + pos = pos + QtCore.QPoint(0, 6) + pos2 = pos + QtCore.QPoint(-20, -20) + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + assert hline.value() == 0 + + # test vertical drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() + pos2 = pos - QtCore.QPoint(50, 50) + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + px = hline.pixelLength(pg.Point(1, 0), ortho=True) + assert abs(hline.value() - plt.plotItem.vb.mapSceneToView(pos2).y()) <= px + + +if __name__ == '__main__': + test_mouseInteraction() diff --git a/pyqtgraph/tests/__init__.py b/pyqtgraph/tests/__init__.py new file mode 100644 index 00000000..7d9ccc9f --- /dev/null +++ b/pyqtgraph/tests/__init__.py @@ -0,0 +1 @@ +from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick diff --git a/pyqtgraph/tests/ui_testing.py b/pyqtgraph/tests/ui_testing.py new file mode 100644 index 00000000..383ba4f9 --- /dev/null +++ b/pyqtgraph/tests/ui_testing.py @@ -0,0 +1,55 @@ + +# Functions for generating user input events. +# We would like to use QTest for this purpose, but it seems to be broken. +# See: http://stackoverflow.com/questions/16299779/qt-qgraphicsview-unit-testing-how-to-keep-the-mouse-in-a-pressed-state + +from ..Qt import QtCore, QtGui, QT_LIB + + +def mousePress(widget, pos, button, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, pos, button, QtCore.Qt.NoButton, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseRelease(widget, pos, button, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, pos, button, QtCore.Qt.NoButton, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseMove(widget, pos, buttons=None, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if buttons is None: + buttons = QtCore.Qt.NoButton + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, pos, QtCore.Qt.NoButton, buttons, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseDrag(widget, pos1, pos2, button, modifier=None): + mouseMove(widget, pos1) + mousePress(widget, pos1, button, modifier) + mouseMove(widget, pos2, button, modifier) + mouseRelease(widget, pos2, button, modifier) + + +def mouseClick(widget, pos, button, modifier=None): + mouseMove(widget, pos) + mousePress(widget, pos, button, modifier) + mouseRelease(widget, pos, button, modifier) + From c1de24e82590eed5bd3696a62384efd62a9c6f92 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 3 Feb 2016 23:17:40 -0800 Subject: [PATCH 09/33] add hover tests --- pyqtgraph/graphicsItems/tests/test_InfiniteLine.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py index 53a4f6ea..bb1f48c4 100644 --- a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py +++ b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py @@ -1,6 +1,6 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtTest, QtGui, QtCore -from pyqtgraph.tests import mouseDrag +from pyqtgraph.tests import mouseDrag, mouseMove pg.mkQApp() qWait = QtTest.QTest.qWait @@ -18,6 +18,8 @@ def test_mouseInteraction(): # test horizontal drag pos = plt.plotItem.vb.mapViewToScene(pg.Point(0,5)).toPoint() pos2 = pos - QtCore.QPoint(200, 200) + mouseMove(plt, pos) + assert vline.mouseHovering is True and hline.mouseHovering is False mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) px = vline.pixelLength(pg.Point(1, 0), ortho=True) assert abs(vline.value() - plt.plotItem.vb.mapSceneToView(pos2).x()) <= px @@ -26,12 +28,16 @@ def test_mouseInteraction(): pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() pos = pos + QtCore.QPoint(0, 6) pos2 = pos + QtCore.QPoint(-20, -20) + mouseMove(plt, pos) + assert vline.mouseHovering is False and hline.mouseHovering is False mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) assert hline.value() == 0 # test vertical drag pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() pos2 = pos - QtCore.QPoint(50, 50) + mouseMove(plt, pos) + assert vline.mouseHovering is False and hline.mouseHovering is True mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) px = hline.pixelLength(pg.Point(1, 0), ortho=True) assert abs(hline.value() - plt.plotItem.vb.mapSceneToView(pos2).y()) <= px From ad8e169160ec6931f006c4b311bda15de33117ca Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 5 Feb 2016 00:12:21 -0800 Subject: [PATCH 10/33] infiniteline API testing --- .../graphicsItems/tests/test_InfiniteLine.py | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py index bb1f48c4..7d78b797 100644 --- a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py +++ b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py @@ -1,10 +1,49 @@ import pyqtgraph as pg -from pyqtgraph.Qt import QtTest, QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore, QtTest from pyqtgraph.tests import mouseDrag, mouseMove pg.mkQApp() -qWait = QtTest.QTest.qWait +def test_InfiniteLine(): + plt = pg.plot() + plt.setXRange(-10, 10) + plt.setYRange(-10, 10) + vline = plt.addLine(x=1) + plt.resize(600, 600) + QtGui.QApplication.processEvents() + QtTest.QTest.qWaitForWindowShown(plt) + QtTest.QTest.qWait(100) + assert vline.angle == 90 + br = vline.mapToView(QtGui.QPolygonF(vline.boundingRect())) + print(vline.boundingRect()) + print(list(QtGui.QPolygonF(vline.boundingRect()))) + print(list(br)) + assert br.containsPoint(pg.Point(1, 5), QtCore.Qt.OddEvenFill) + assert not br.containsPoint(pg.Point(5, 0), QtCore.Qt.OddEvenFill) + hline = plt.addLine(y=0) + assert hline.angle == 0 + assert hline.boundingRect().contains(pg.Point(5, 0)) + assert not hline.boundingRect().contains(pg.Point(0, 5)) + + vline.setValue(2) + assert vline.value() == 2 + vline.setPos(pg.Point(4, -5)) + assert vline.value() == 4 + + oline = pg.InfiniteLine(angle=30) + plt.addItem(oline) + oline.setPos(pg.Point(1, -1)) + assert oline.angle == 30 + assert oline.pos() == pg.Point(1, -1) + assert oline.value() == [1, -1] + + br = oline.mapToScene(oline.boundingRect()) + pos = oline.mapToScene(pg.Point(2, 0)) + assert br.containsPoint(pos, QtCore.Qt.OddEvenFill) + px = oline.pixelVectors(pg.Point(1, 0))[0] + assert br.containsPoint(pos + 4 * px, QtCore.Qt.OddEvenFill) + assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill) + def test_mouseInteraction(): plt = pg.plot() @@ -12,6 +51,7 @@ def test_mouseInteraction(): vline = plt.addLine(x=0, movable=True) plt.addItem(vline) hline = plt.addLine(y=0, movable=True) + hline2 = plt.addLine(y=-1, movable=False) plt.setXRange(-10, 10) plt.setYRange(-10, 10) @@ -42,6 +82,14 @@ def test_mouseInteraction(): px = hline.pixelLength(pg.Point(1, 0), ortho=True) assert abs(hline.value() - plt.plotItem.vb.mapSceneToView(pos2).y()) <= px + # test non-interactive line + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,-1)).toPoint() + pos2 = pos - QtCore.QPoint(50, 50) + mouseMove(plt, pos) + assert hline2.mouseHovering == False + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + assert hline2.value() == -1 + if __name__ == '__main__': test_mouseInteraction() From 4a3525eafdbb2111de2d4e83b37b562ce0d4a97f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 5 Feb 2016 00:55:34 -0800 Subject: [PATCH 11/33] infiniteline tests pass --- .../graphicsItems/tests/test_InfiniteLine.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py index 7d78b797..24438864 100644 --- a/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py +++ b/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py @@ -5,19 +5,19 @@ pg.mkQApp() def test_InfiniteLine(): + # Test basic InfiniteLine API plt = pg.plot() plt.setXRange(-10, 10) plt.setYRange(-10, 10) - vline = plt.addLine(x=1) plt.resize(600, 600) - QtGui.QApplication.processEvents() + + # seemingly arbitrary requirements; might need longer wait time for some platforms.. QtTest.QTest.qWaitForWindowShown(plt) QtTest.QTest.qWait(100) + + vline = plt.addLine(x=1) assert vline.angle == 90 br = vline.mapToView(QtGui.QPolygonF(vline.boundingRect())) - print(vline.boundingRect()) - print(list(QtGui.QPolygonF(vline.boundingRect()))) - print(list(br)) assert br.containsPoint(pg.Point(1, 5), QtCore.Qt.OddEvenFill) assert not br.containsPoint(pg.Point(5, 0), QtCore.Qt.OddEvenFill) hline = plt.addLine(y=0) @@ -37,11 +37,12 @@ def test_InfiniteLine(): assert oline.pos() == pg.Point(1, -1) assert oline.value() == [1, -1] + # test bounding rect for oblique line br = oline.mapToScene(oline.boundingRect()) pos = oline.mapToScene(pg.Point(2, 0)) assert br.containsPoint(pos, QtCore.Qt.OddEvenFill) - px = oline.pixelVectors(pg.Point(1, 0))[0] - assert br.containsPoint(pos + 4 * px, QtCore.Qt.OddEvenFill) + px = pg.Point(-0.5, -1.0 / 3**0.5) + assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill) assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill) From 0be3615c883ccb8580a490d8cc04b15de3223b09 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Fri, 5 Feb 2016 11:54:00 +0100 Subject: [PATCH 12/33] 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 13/33] 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 a8b56244441d880467e641645caeb3a6b8496c7b Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Mon, 15 Feb 2016 06:55:02 +0100 Subject: [PATCH 14/33] example modifications --- examples/{Markers.py => Symbols.py} | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) rename examples/{Markers.py => Symbols.py} (55%) diff --git a/examples/Markers.py b/examples/Symbols.py similarity index 55% rename from examples/Markers.py rename to examples/Symbols.py index 304aa3fd..2cbd60f7 100755 --- a/examples/Markers.py +++ b/examples/Symbols.py @@ -1,31 +1,32 @@ # -*- coding: utf-8 -*- """ -This example shows all the markers available into pyqtgraph. +This example shows all the symbols available into pyqtgraph. +New in version 0.9.11 """ 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="Pyqtgraph markers") +win = pg.GraphicsWindow(title="Pyqtgraph symbols") win.resize(1000,600) pg.setConfigOptions(antialias=True) -plot = win.addPlot(title="Plotting with markers") -plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o') -plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t') -plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1') -plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2') -plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3') -plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s') -plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p') -plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h') -plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star') -plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+') -plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d') +plot = win.addPlot(title="Plotting with symbols") +plot.addLegend() +plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o', symbolSize=14, name="symbol='o'") +plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t', symbolSize=14, name="symbol='t'") +plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1', symbolSize=14, name="symbol='t1'") +plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2', symbolSize=14, name="symbol='t2'") +plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3', symbolSize=14, name="symbol='t3'") +plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s', symbolSize=14, name="symbol='s'") +plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p', symbolSize=14, name="symbol='p'") +plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h', symbolSize=14, name="symbol='h'") +plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'") +plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'") +plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'") ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': From 6fc4e1a611f8306804d40b7b22ffd39bd0c6d6d9 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Mon, 15 Feb 2016 07:11:22 +0100 Subject: [PATCH 15/33] 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 392c3c6c17ad92bd552473b1e15a5dbc1dd4333e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 14 Feb 2016 23:15:39 -0800 Subject: [PATCH 16/33] Added symbol example to menu; minor cleanups to symbol example. --- examples/Symbols.py | 9 ++++++--- examples/utils.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/Symbols.py b/examples/Symbols.py index 2cbd60f7..3dd28e13 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """ -This example shows all the symbols available into pyqtgraph. -New in version 0.9.11 +This example shows all the scatter plot symbols available in pyqtgraph. + +These symbols are used to mark point locations for scatter plots and some line +plots, similar to "markers" in matplotlib and vispy. """ import initExample ## Add path to library (just for examples; you do not need this) @@ -9,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Pyqtgraph symbols") +win = pg.GraphicsWindow(title="Scatter Plot Symbols") win.resize(1000,600) pg.setConfigOptions(antialias=True) @@ -27,6 +29,7 @@ plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'") plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'") plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'") +plot.setXRange(-2, 4) ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/examples/utils.py b/examples/utils.py index 3ff265c4..cbdf69c6 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -22,6 +22,7 @@ examples = OrderedDict([ ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), + ('Symbols', 'Symbols.py'), ('Auto-range', 'PlotAutoRange.py'), ('Remote Plotting', 'RemoteSpeedTest.py'), ('Scrolling plots', 'scrollingPlots.py'), From de24d6db6ae426054ec9890fa76046ed79e15b73 Mon Sep 17 00:00:00 2001 From: lesauxvi Date: Tue, 16 Feb 2016 06:36:41 +0100 Subject: [PATCH 17/33] 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 18/33] 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 19/33] 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 20/33] 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 21/33] 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 22/33] 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 23/33] 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 24/33] 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 25/33] 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:** From 7a0dfd768a825ba2e065e63b5e52a904ed3fd989 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Feb 2016 00:23:36 -0800 Subject: [PATCH 26/33] Cleanup: add docstrings and setter methods to InfLineLabel, remove unused code --- examples/plottingItems.py | 12 +++--- pyqtgraph/graphicsItems/InfiniteLine.py | 57 ++++++++++++++++++------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index d90d81ab..50dd68e4 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -18,14 +18,12 @@ pg.setConfigOptions(antialias=True) p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100, scale=10), pen=0.5) p1.setYRange(-40, 40) -inf1 = pg.InfiniteLine(movable=True, angle=90, text='x={value:0.2f}', - textOpts={'position':0.1, 'color': (200,200,100), 'fill': (200,200,200,50), 'movable': True}) -inf2 = pg.InfiniteLine(movable=True, angle=0, pen=(0, 0, 200), bounds = [-20, 20], hoverPen=(0,200,0), text='y={value:0.2f}mm', - textOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)}) -inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', text='diagonal', textOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True}) +inf1 = pg.InfiniteLine(movable=True, angle=90, label='x={value:0.2f}', + labelOpts={'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), label='y={value:0.2f}mm', + labelOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)}) +inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', label='diagonal', labelOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True}) inf1.setPos([2,2]) -#inf1.setTextLocation(position=0.75) -#inf2.setTextLocation(shift=0.8) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 22c9a281..9d10a8ab 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -31,7 +31,7 @@ class InfiniteLine(GraphicsObject): sigPositionChanged = QtCore.Signal(object) def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, - hoverPen=None, text=None, textOpts=None, name=None): + hoverPen=None, label=None, labelOpts=None, name=None): """ =============== ================================================================== **Arguments:** @@ -47,10 +47,10 @@ class InfiniteLine(GraphicsObject): Default pen is red. bounds Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal. - text Text to be displayed in a label attached to the line, or + label 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 + labelOpts A dict of keyword arguments to use when constructing the text label. See :class:`InfLineLabel`. name Name of the item =============== ================================================================== @@ -68,15 +68,9 @@ class InfiniteLine(GraphicsObject): self.p = [0, 0] self.setAngle(angle) - if text is not None: - textOpts = {} if textOpts is None else textOpts - self.textItem = InfLineLabel(self, text=text, **textOpts) - self.textItem.setParentItem(self) - - self.anchorLeft = (1., 0.5) - self.anchorRight = (0., 0.5) - self.anchorUp = (0.5, 1.) - self.anchorDown = (0.5, 0.) + if label is not None: + labelOpts = {} if labelOpts is None else labelOpts + self.label = InfLineLabel(self, text=label, **labelOpts) if pos is None: pos = Point(0,0) @@ -167,10 +161,6 @@ class InfiniteLine(GraphicsObject): newPos[1] = min(newPos[1], self.maxRange[1]) if self.p != newPos: - # Invalidate bounding rect and line - self._boundingRect = None - self._line = None - self.p = newPos self._invalidateCache() GraphicsObject.setPos(self, Point(self.p)) @@ -308,6 +298,19 @@ class InfLineLabel(TextItem): 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. + * Optionally aligns to its parent line. + + =============== ================================================================== + **Arguments:** + line The InfiniteLine to which this label will be attached. + text String to display in the label. May contain a {value} formatting + string to display the current value of the line. + movable Bool; if True, then the label can be dragged along the line. + position Relative position (0.0-1.0) within the view to position the label + along the line. + =============== ================================================================== + + All extra keyword arguments are passed to TextItem. """ def __init__(self, line, text="", movable=False, position=0.5, **kwds): self.line = line @@ -316,6 +319,7 @@ class InfLineLabel(TextItem): self.format = text self.line.sigPositionChanged.connect(self.valueChanged) TextItem.__init__(self, **kwds) + self.setParentItem(line) self.valueChanged() def valueChanged(self): @@ -361,9 +365,30 @@ class InfLineLabel(TextItem): self.updatePosition() def setMovable(self, m): + """Set whether this label is movable by dragging along the line. + """ self.movable = m self.setAcceptHoverEvents(m) + def setPosition(self, p): + """Set the relative position (0.0-1.0) of this label within the view box + and along the line. + + For horizontal (angle=0) and vertical (angle=90) lines, a value of 0.0 + places the text at the bottom or left of the view, respectively. + """ + self.orthoPos = p + self.updatePosition() + + def setFormat(self, text): + """Set the text format string for this label. + + May optionally contain "{value}" to include the lines current value + (the text will be reformatted whenever the line is moved). + """ + self.format = format + self.valueChanged() + def mouseDragEvent(self, ev): if self.movable and ev.button() == QtCore.Qt.LeftButton: if ev.isStart(): From 4e424b5773fd2a73616d8f3df283f008fd024fc5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Feb 2016 22:12:36 -0800 Subject: [PATCH 27/33] Fixed label dragging for oblique lines --- pyqtgraph/graphicsItems/InfiniteLine.py | 62 ++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 9d10a8ab..0b9ddb21 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -318,6 +318,7 @@ class InfLineLabel(TextItem): 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) + self._endpoints = (None, None) TextItem.__init__(self, **kwds) self.setParentItem(line) self.valueChanged() @@ -328,34 +329,42 @@ class InfLineLabel(TextItem): value = self.line.value() self.setText(self.format.format(value=value)) self.updatePosition() + + def getEndpoints(self): + # calculate points where line intersects view box + # (in line coordinates) + if self._endpoints[0] is None: + view = self.getViewBox() + if not self.isVisible() or not isinstance(view, ViewBox): + # not in a viewbox, skip update + return (None, None) + + 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(QtGui.QTransform()) + if len(paths) > 0: + l = list(paths[0]) + pt1 = self.line.mapFromItem(view, l[0]) + pt2 = self.line.mapFromItem(view, l[1]) + self._endpoints = (pt1, pt2) + return self._endpoints 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 + self._endpoints = (None, None) + pt1, pt2 = self.getEndpoints() + if pt1 is None: return - - lr = self.line.boundingRect() - pt1 = Point(lr.left(), 0) - pt2 = Point(lr.right(), 0) - if self.line.angle % 90 != 0: - # more expensive to find text position for oblique lines. - p = QtGui.QPainterPath() - p.moveTo(pt1) - p.lineTo(pt2) - p = self.line.itemTransform(view)[0].map(p) - vr = QtGui.QPainterPath() - vr.addRect(view.boundingRect()) - paths = vr.intersected(p).toSubpathPolygons() - if len(paths) > 0: - l = list(paths[0]) - pt1 = self.line.mapFromItem(view, l[0]) - pt2 = self.line.mapFromItem(view, l[1]) - pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos) - self.setPos(pt) def setVisible(self, v): @@ -422,8 +431,9 @@ class InfLineLabel(TextItem): def _posToRel(self, pos): # convert local position to relative position along line between view bounds + pt1, pt2 = self.getEndpoints() + if pt1 is None: + return 0 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() + return (pos.x() - pt1.x()) / (pt2.x()-pt1.x()) From e4bdc17112782e0587d9349123393eb594cde872 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 22 Feb 2016 23:11:29 -0800 Subject: [PATCH 28/33] Add qWait surrogate for PySide --- pyqtgraph/Qt.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 3584bec0..c9700784 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -9,7 +9,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import sys, re +import sys, re, time from .python2_3 import asUnicode @@ -45,6 +45,15 @@ if QT_LIB == PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg try: from PySide import QtTest + if not hasattr(QtTest.QTest, 'qWait'): + @staticmethod + def qWait(msec): + start = time.time() + QtGui.QApplication.processEvents() + while time.time() < start + msec * 0.001: + QtGui.QApplication.processEvents() + QtTest.QTest.qWait = qWait + except ImportError: pass import PySide From bd0e490821ac645bc592c0d875bf77d4550c5bee Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 28 Feb 2016 12:26:05 -0800 Subject: [PATCH 29/33] cleanup: docs, default args --- examples/plottingItems.py | 9 ++++++++- pyqtgraph/graphicsItems/InfiniteLine.py | 6 ++++-- pyqtgraph/graphicsItems/TextItem.py | 11 ++++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/examples/plottingItems.py b/examples/plottingItems.py index 50dd68e4..50efbd04 100644 --- a/examples/plottingItems.py +++ b/examples/plottingItems.py @@ -16,20 +16,27 @@ win.resize(1000,600) # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) +# Create a plot with some random data p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100, scale=10), pen=0.5) p1.setYRange(-40, 40) + +# Add three infinite lines with labels inf1 = pg.InfiniteLine(movable=True, angle=90, label='x={value:0.2f}', labelOpts={'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), label='y={value:0.2f}mm', labelOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)}) -inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', label='diagonal', labelOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True}) +inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', label='diagonal', + labelOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True}) inf1.setPos([2,2]) p1.addItem(inf1) p1.addItem(inf2) p1.addItem(inf3) +# Add a linear region with a label lr = pg.LinearRegionItem(values=[70, 80]) p1.addItem(lr) +label = pg.InfLineLabel(lr.lines[1], "region 1", position=0.95, rotateAxis=(1,0), anchor=(1, 1)) + ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 0b9ddb21..1098f843 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -8,7 +8,7 @@ import numpy as np import weakref -__all__ = ['InfiniteLine'] +__all__ = ['InfiniteLine', 'InfLineLabel'] class InfiniteLine(GraphicsObject): @@ -310,7 +310,9 @@ class InfLineLabel(TextItem): along the line. =============== ================================================================== - All extra keyword arguments are passed to TextItem. + All extra keyword arguments are passed to TextItem. A particularly useful + option here is to use `rotateAxis=(1, 0)`, which will cause the text to + be automatically rotated parallel to the line. """ def __init__(self, line, text="", movable=False, position=0.5, **kwds): self.line = line diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 220d5859..47d9dac3 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=(1, 0)): + border=None, fill=None, angle=0, rotateAxis=None): """ ============== ================================================================================= **Arguments:** @@ -30,6 +30,15 @@ class TextItem(GraphicsObject): Allows text to follow both the position and orientation of its parent while still discarding any scale and shear factors. ============== ================================================================================= + + + The effects of the `rotateAxis` and `angle` arguments are added independently. So for example: + + * rotateAxis=None, angle=0 -> normal horizontal text + * rotateAxis=None, angle=90 -> normal vertical text + * rotateAxis=(1, 0), angle=0 -> text aligned with x axis of its parent + * rotateAxis=(0, 1), angle=0 -> text aligned with y axis of its parent + * rotateAxis=(1, 0), angle=90 -> text orthogonal to x axis of its parent """ self.anchor = Point(anchor) From b7bf6337d7cb0cec69950ec842c1fa2b2e628db8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 28 Feb 2016 18:45:42 -0800 Subject: [PATCH 30/33] minor efficiency boost --- pyqtgraph/graphicsItems/InfiniteLine.py | 31 ++++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 1098f843..428f6539 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -55,6 +55,10 @@ class InfiniteLine(GraphicsObject): name Name of the item =============== ================================================================== """ + self._boundingRect = None + self._line = None + + self._name = name GraphicsObject.__init__(self) @@ -68,10 +72,6 @@ class InfiniteLine(GraphicsObject): self.p = [0, 0] self.setAngle(angle) - if label is not None: - labelOpts = {} if labelOpts is None else labelOpts - self.label = InfLineLabel(self, text=label, **labelOpts) - if pos is None: pos = Point(0,0) self.setPos(pos) @@ -85,10 +85,9 @@ class InfiniteLine(GraphicsObject): self.setHoverPen(hoverPen) self.currentPen = self.pen - self._boundingRect = None - self._line = None - - self._name = name + if label is not None: + labelOpts = {} if labelOpts is None else labelOpts + self.label = InfLineLabel(self, text=label, **labelOpts) def setMovable(self, m): """Set whether the line is movable by the user.""" @@ -209,14 +208,17 @@ class InfiniteLine(GraphicsObject): if self._boundingRect is None: #br = UIGraphicsItem.boundingRect(self) br = self.viewRect() + if br is None: + return QtCore.QRectF() + ## 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) @@ -321,6 +323,7 @@ class InfLineLabel(TextItem): self.format = text self.line.sigPositionChanged.connect(self.valueChanged) self._endpoints = (None, None) + self.anchors = [(0, 0), (1, 0)] TextItem.__init__(self, **kwds) self.setParentItem(line) self.valueChanged() @@ -336,16 +339,16 @@ class InfLineLabel(TextItem): # calculate points where line intersects view box # (in line coordinates) if self._endpoints[0] is None: - view = self.getViewBox() - if not self.isVisible() or not isinstance(view, ViewBox): - # not in a viewbox, skip update - return (None, None) - 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. + view = self.getViewBox() + if not self.isVisible() or not isinstance(view, ViewBox): + # not in a viewbox, skip update + return (None, None) p = QtGui.QPainterPath() p.moveTo(pt1) p.lineTo(pt2) From ac14139c2de92f70266eaf15a7fa5138e35d3bb0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 28 Feb 2016 18:54:55 -0800 Subject: [PATCH 31/33] rename example --- examples/{plottingItems.py => InfiniteLine.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{plottingItems.py => InfiniteLine.py} (100%) diff --git a/examples/plottingItems.py b/examples/InfiniteLine.py similarity index 100% rename from examples/plottingItems.py rename to examples/InfiniteLine.py From bb97f2e98dcf6e2298de3f33acf65befe7ca1732 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 28 Feb 2016 20:52:07 -0800 Subject: [PATCH 32/33] Switch text anchor when line crosses center of view --- pyqtgraph/graphicsItems/InfiniteLine.py | 27 +++++++++++++++++++++++-- pyqtgraph/graphicsItems/TextItem.py | 26 ++++++++++-------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 428f6539..44903ed8 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -310,20 +310,38 @@ class InfLineLabel(TextItem): movable Bool; if True, then the label can be dragged along the line. position Relative position (0.0-1.0) within the view to position the label along the line. + anchors List of (x,y) pairs giving the text anchor positions that should + be used when the line is moved to one side of the view or the + other. This allows text to switch to the opposite side of the line + as it approaches the edge of the view. =============== ================================================================== All extra keyword arguments are passed to TextItem. A particularly useful option here is to use `rotateAxis=(1, 0)`, which will cause the text to be automatically rotated parallel to the line. """ - def __init__(self, line, text="", movable=False, position=0.5, **kwds): + def __init__(self, line, text="", movable=False, position=0.5, anchors=None, **kwds): self.line = line self.movable = movable self.orthoPos = position # text will always be placed on the line at a position relative to view bounds self.format = text self.line.sigPositionChanged.connect(self.valueChanged) self._endpoints = (None, None) - self.anchors = [(0, 0), (1, 0)] + if anchors is None: + # automatically pick sensible anchors + rax = kwds.get('rotateAxis', None) + if rax is not None: + if tuple(rax) == (1,0): + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + else: + if line.angle % 180 == 0: + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + + self.anchors = anchors TextItem.__init__(self, **kwds) self.setParentItem(line) self.valueChanged() @@ -372,6 +390,11 @@ class InfLineLabel(TextItem): pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos) self.setPos(pt) + # update anchor to keep text visible as it nears the view box edge + vr = self.line.viewRect() + if vr is not None: + self.setAnchor(self.anchors[0 if vr.center().y() < 0 else 1]) + def setVisible(self, v): TextItem.setVisible(self, v) if v: diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index 47d9dac3..dc240929 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -56,7 +56,6 @@ class TextItem(GraphicsObject): self.fill = fn.mkBrush(fill) self.border = fn.mkPen(border) 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)): """ @@ -67,14 +66,7 @@ class TextItem(GraphicsObject): color = fn.mkColor(color) self.textItem.setDefaultTextColor(color) self.textItem.setPlainText(text) - self.updateText() - #html = '%s' % (color, text) - #self.setHtml(html) - - def updateAnchor(self): - pass - #self.resetTransform() - #self.translate(0, 20) + self.updateTextPos() def setPlainText(self, *args): """ @@ -83,7 +75,7 @@ class TextItem(GraphicsObject): See QtGui.QGraphicsTextItem.setPlainText(). """ self.textItem.setPlainText(*args) - self.updateText() + self.updateTextPos() def setHtml(self, *args): """ @@ -92,7 +84,7 @@ class TextItem(GraphicsObject): See QtGui.QGraphicsTextItem.setHtml(). """ self.textItem.setHtml(*args) - self.updateText() + self.updateTextPos() def setTextWidth(self, *args): """ @@ -104,7 +96,7 @@ class TextItem(GraphicsObject): See QtGui.QGraphicsTextItem.setTextWidth(). """ self.textItem.setTextWidth(*args) - self.updateText() + self.updateTextPos() def setFont(self, *args): """ @@ -113,13 +105,17 @@ class TextItem(GraphicsObject): See QtGui.QGraphicsTextItem.setFont(). """ self.textItem.setFont(*args) - self.updateText() + self.updateTextPos() def setAngle(self, angle): self.angle = angle self.updateTransform() - def updateText(self): + def setAnchor(self, anchor): + self.anchor = Point(anchor) + self.updateTextPos() + + def updateTextPos(self): # update text position to obey anchor r = self.textItem.boundingRect() tl = self.textItem.mapToParent(r.topLeft()) @@ -184,6 +180,6 @@ class TextItem(GraphicsObject): self._lastTransform = pt - self.updateText() + self.updateTextPos() \ No newline at end of file From 36b3f11524a4c9f4418f7f546635209a0c2b6ffc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 28 Feb 2016 20:53:52 -0800 Subject: [PATCH 33/33] docstring update --- pyqtgraph/graphicsItems/InfiniteLine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 44903ed8..b76b4483 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -313,7 +313,9 @@ class InfLineLabel(TextItem): anchors List of (x,y) pairs giving the text anchor positions that should be used when the line is moved to one side of the view or the other. This allows text to switch to the opposite side of the line - as it approaches the edge of the view. + as it approaches the edge of the view. These are automatically + selected for some common cases, but may be specified if the + default values give unexpected results. =============== ================================================================== All extra keyword arguments are passed to TextItem. A particularly useful