From 9b41c90034fb3ccab472a31ed0e9eea2aa34b9e0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Nov 2012 16:05:14 -0500 Subject: [PATCH] New features for LegendItem: - Can be anchored to parent item at any location - Support for filled plot styles - Automatically resizes to fit contents - PlotItem can auto-generate legend --- examples/Legend.py | 13 ++--- graphicsItems/GraphicsWidgetAnchor.py | 63 ++++++++++++++++++++++++ graphicsItems/LegendItem.py | 69 +++++++++++++++++++++++---- graphicsItems/PlotCurveItem.py | 4 ++ graphicsItems/PlotDataItem.py | 2 + graphicsItems/PlotItem/PlotItem.py | 14 ++++++ 6 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 graphicsItems/GraphicsWidgetAnchor.py diff --git a/examples/Legend.py b/examples/Legend.py index f615a6eb..af6fce8c 100644 --- a/examples/Legend.py +++ b/examples/Legend.py @@ -6,13 +6,14 @@ from pyqtgraph.Qt import QtCore, QtGui plt = pg.plot() -l = pg.LegendItem((100,60), (60,10)) # args are (size, position) -l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case +plt.addLegend() +#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) +#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case -c1 = plt.plot([1,3,2,4], pen='r') -c2 = plt.plot([2,1,4,3], pen='g') -l.addItem(c1, 'red plot') -l.addItem(c2, 'green plot') +c1 = plt.plot([1,3,2,4], pen='r', name='red plot') +c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot') +#l.addItem(c1, 'red plot') +#l.addItem(c2, 'green plot') ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/graphicsItems/GraphicsWidgetAnchor.py b/graphicsItems/GraphicsWidgetAnchor.py new file mode 100644 index 00000000..b71e781a --- /dev/null +++ b/graphicsItems/GraphicsWidgetAnchor.py @@ -0,0 +1,63 @@ +from ..Qt import QtGui, QtCore +from ..Point import Point + + +class GraphicsWidgetAnchor: + """ + Class used to allow GraphicsWidgets to anchor to a specific position on their + parent. + + """ + + def __init__(self): + self.__parent = None + self.__parentAnchor = None + self.__itemAnchor = None + self.__offset = (0,0) + if hasattr(self, 'geometryChanged'): + self.geometryChanged.connect(self.__geometryChanged) + + def anchor(self, itemPos, parentPos, offset=(0,0)): + """ + Anchors the item at its local itemPos to the item's parent at parentPos. + Both positions are expressed in values relative to the size of the item or parent; + a value of 0 indicates left or top edge, while 1 indicates right or bottom edge. + + Optionally, offset may be specified to introduce an absolute offset. + + Example: anchor a box such that its upper-right corner is fixed 10px left + and 10px down from its parent's upper-right corner:: + + box.anchor(itemPos=(1,0), parentPos=(1,0), offset=(-10,10)) + """ + parent = self.parentItem() + if parent is None: + raise Exception("Cannot anchor; parent is not set.") + + if self.__parent is not parent: + if self.__parent is not None: + self.__parent.geometryChanged.disconnect(self.__geometryChanged) + + self.__parent = parent + parent.geometryChanged.connect(self.__geometryChanged) + + self.__itemAnchor = itemPos + self.__parentAnchor = parentPos + self.__offset = offset + self.__geometryChanged() + + def __geometryChanged(self): + if self.__parent is None: + return + if self.__itemAnchor is None: + return + + o = self.mapToParent(Point(0,0)) + a = self.boundingRect().bottomRight() * Point(self.__itemAnchor) + a = self.mapToParent(a) + p = self.__parent.boundingRect().bottomRight() * Point(self.__parentAnchor) + off = Point(self.__offset) + pos = p + (o-a) + off + self.setPos(pos) + + \ No newline at end of file diff --git a/graphicsItems/LegendItem.py b/graphicsItems/LegendItem.py index a41201e1..14c690a5 100644 --- a/graphicsItems/LegendItem.py +++ b/graphicsItems/LegendItem.py @@ -2,12 +2,14 @@ from .GraphicsWidget import GraphicsWidget from .LabelItem import LabelItem from ..Qt import QtGui, QtCore from .. import functions as fn - +from ..Point import Point +from .GraphicsWidgetAnchor import GraphicsWidgetAnchor __all__ = ['LegendItem'] -class LegendItem(GraphicsWidget): +class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Displays a legend used for describing the contents of a plot. + LegendItems are most commonly created by calling PlotItem.addLegend(). Note that this item should not be added directly to a PlotItem. Instead, Make it a direct descendant of the PlotItem:: @@ -15,17 +17,45 @@ class LegendItem(GraphicsWidget): legend.setParentItem(plotItem) """ - def __init__(self, size, offset): + def __init__(self, size=None, offset=None): + """ + ========== =============================================================== + Arguments + size Specifies the fixed size (width, height) of the legend. If + this argument is omitted, the legend will autimatically resize + to fit its contents. + offset Specifies the offset position relative to the legend's parent. + Positive values offset from the left or top; negative values + offset from the right or bottom. If offset is None, the + legend must be anchored manually by calling anchor() or + positioned by calling setPos(). + ========== =============================================================== + + """ + + GraphicsWidget.__init__(self) + GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemIgnoresTransformations) self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) self.items = [] self.size = size self.offset = offset - self.setGeometry(QtCore.QRectF(self.offset[0], self.offset[1], self.size[0], self.size[1])) + if size is not None: + self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1])) - def addItem(self, item, title): + def setParentItem(self, p): + ret = GraphicsWidget.setParentItem(self, p) + if self.offset is not None: + offset = Point(self.offset) + anchorx = 1 if offset[0] <= 0 else 0 + anchory = 1 if offset[1] <= 0 else 0 + anchor = (anchorx, anchory) + self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) + return ret + + def addItem(self, item, name): """ Add a new entry to the legend. @@ -36,15 +66,30 @@ class LegendItem(GraphicsWidget): title The title to display for this item. Simple HTML allowed. =========== ======================================================== """ - label = LabelItem(title) + label = LabelItem(name) sample = ItemSample(item) row = len(self.items) self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) + self.updateSize() + + def updateSize(self): + if self.size is not None: + return + + height = 0 + width = 0 + print "-------" + for sample, label in self.items: + height += max(sample.height(), label.height()) + 3 + width = max(width, sample.width()+label.width()) + print width, height + print width, height + self.setGeometry(0, 0, width+25, height) def boundingRect(self): - return QtCore.QRectF(0, 0, self.size[0], self.size[1]) + return QtCore.QRectF(0, 0, self.width(), self.height()) def paint(self, p, *args): p.setPen(fn.mkPen(255,255,255,100)) @@ -61,8 +106,16 @@ class ItemSample(GraphicsWidget): return QtCore.QRectF(0, 0, 20, 20) def paint(self, p, *args): - p.setPen(fn.mkPen(self.item.opts['pen'])) + opts = self.item.opts + + if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: + p.setBrush(fn.mkBrush(opts['fillBrush'])) + p.setPen(fn.mkPen(None)) + p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) + + p.setPen(fn.mkPen(opts['pen'])) p.drawLine(2, 18, 18, 2) + diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 17ee4566..d267f58e 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -65,6 +65,7 @@ class PlotCurveItem(GraphicsObject): 'fillLevel': None, 'brush': None, 'stepMode': False, + 'name': None } self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) @@ -238,6 +239,9 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None #self.xDisp = self.yDisp = None + if 'name' in kargs: + self.opts['name'] = kargs['name'] + if 'pen' in kargs: self.setPen(kargs['pen']) if 'shadowPen' in kargs: diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index ce5b11aa..58158905 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -317,6 +317,8 @@ class PlotDataItem(GraphicsObject): ## pull in all style arguments. ## Use self.opts to fill in anything not present in kargs. + if 'name' in kargs: + self.opts['name'] = kargs['name'] ## if symbol pen/brush are given with no symbol, then assume symbol is 'o' diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 3177a176..8281f031 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -33,6 +33,7 @@ from .. PlotDataItem import PlotDataItem from .. ViewBox import ViewBox from .. AxisItem import AxisItem from .. LabelItem import LabelItem +from .. LegendItem import LegendItem from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem from pyqtgraph.WidgetGroup import WidgetGroup @@ -528,6 +529,9 @@ class PlotItem(GraphicsWidget): #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged) #self.plotChanged() + name = kargs.get('name', getattr(item, 'opts', {}).get('name', None)) + if name is not None and self.legend is not None: + self.legend.addItem(item, name=name) def addDataItem(self, item, *args): @@ -596,6 +600,16 @@ class PlotItem(GraphicsWidget): return item + def addLegend(self, size=None, offset=(30, 30)): + """ + Create a new LegendItem and anchor it over the internal ViewBox. + Plots will be automatically displayed in the legend if they + are created with the 'name' argument. + """ + self.legend = LegendItem(size, offset) + self.legend.setParentItem(self.vb) + return self.legend + def scatterPlot(self, *args, **kargs): if 'pen' in kargs: kargs['symbolPen'] = kargs['pen']