From 829503f3d291b67ee49caffcfdaf75539a75c0c6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 27 Mar 2013 20:24:01 -0400 Subject: [PATCH] AxisItem updates: - better handling of tick text / label area - ability to truncate axis lines at the last tick --- pyqtgraph/graphicsItems/AxisItem.py | 211 ++++++++++++++----- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 1 + 2 files changed, 160 insertions(+), 52 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c4e0138c..bf3c8743 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -39,19 +39,19 @@ class AxisItem(GraphicsWidget): if orientation not in ['left', 'right', 'top', 'bottom']: raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") if orientation in ['left', 'right']: - #self.setMinimumWidth(25) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Minimum, - #QtGui.QSizePolicy.Expanding - #)) self.label.rotate(-90) - #else: - #self.setMinimumHeight(50) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Expanding, - #QtGui.QSizePolicy.Minimum - #)) - #self.drawLabel = False + + self.style = { + 'tickTextOffset': 3, ## spacing between text and axis + 'tickTextWidth': 30, ## space reserved for tick text + 'tickTextHeight': 18, + 'autoExpandTextSpace': True, ## automatically expand text space if needed + 'tickFont': None, + 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + } + + self.textWidth = 30 ## Keeps track of maximum width / height of tick text + self.textHeight = 18 self.labelText = '' self.labelUnits = '' @@ -60,7 +60,6 @@ class AxisItem(GraphicsWidget): self.logMode = False self.tickFont = None - self.textHeight = 18 self.tickLength = maxTickLength self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self.scale = 1.0 @@ -184,7 +183,7 @@ class AxisItem(GraphicsWidget): if len(args) > 0: self.labelStyle = args self.label.setHtml(self.labelString()) - self.resizeEvent() + self._adjustSize() self.picture = None self.update() @@ -203,14 +202,43 @@ class AxisItem(GraphicsWidget): style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) return asUnicode("%s") % (style, s) + + def _updateMaxTextSize(self, x): + ## Informs that the maximum tick size orthogonal to the axis has + ## changed; we use this to decide whether the item needs to be resized + ## to accomodate. + if self.orientation in ['left', 'right']: + mx = max(self.textWidth, x) + if mx > self.textWidth: + self.textWidth = mx + if self.style['autoExpandTextSpace'] is True: + self.setWidth() + #return True ## size has changed + else: + mx = max(self.textHeight, x) + if mx > self.textHeight: + self.textHeight = mx + if self.style['autoExpandTextSpace'] is True: + self.setHeight() + #return True ## size has changed + def _adjustSize(self): + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added.""" if h is None: - h = self.textHeight + max(0, self.tickLength) + if self.style['autoExpandTextSpace'] is True: + h = self.textHeight + else: + h = self.style['tickTextHeight'] + h += max(0, self.tickLength) + self.style['tickTextOffset'] if self.label.isVisible(): - h += self.textHeight + h += self.label.boundingRect().height() * 0.8 self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None @@ -220,11 +248,16 @@ class AxisItem(GraphicsWidget): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added.""" if w is None: - w = max(0, self.tickLength) + 40 + if self.style['autoExpandTextSpace'] is True: + w = self.textWidth + else: + w = self.style['tickTextWidth'] + w += max(0, self.tickLength) + self.style['tickTextOffset'] if self.label.isVisible(): - w += self.textHeight + w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate self.setMaximumWidth(w) self.setMinimumWidth(w) + self.picture = None def pen(self): if self._pen is None: @@ -346,12 +379,14 @@ class AxisItem(GraphicsWidget): def paint(self, p, opt, widget): if self.picture is None: - self.picture = QtGui.QPicture() - painter = QtGui.QPainter(self.picture) try: - self.drawPicture(painter) + picture = QtGui.QPicture() + painter = QtGui.QPainter(picture) + specs = self.generateDrawSpecs(painter) + self.drawPicture(painter, *specs) finally: painter.end() + self.picture = picture #p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ??? #p.setRenderHint(p.TextAntialiasing, True) self.picture.play(p) @@ -540,12 +575,13 @@ class AxisItem(GraphicsWidget): def logTickStrings(self, values, scale, spacing): return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] - def drawPicture(self, p): - - p.setRenderHint(p.Antialiasing, False) - p.setRenderHint(p.TextAntialiasing, True) - - prof = debug.Profiler("AxisItem.paint", disabled=True) + def generateDrawSpecs(self, p): + """ + Calls tickValues() and tickStrings to determine where and how ticks should + be drawn, then generates from this a set of drawing commands to be + interpreted by drawPicture(). + """ + prof = debug.Profiler("AxisItem.generateDrawSpecs", disabled=True) #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) @@ -582,11 +618,6 @@ class AxisItem(GraphicsWidget): axis = 1 #print tickStart, tickStop, span - ## draw long line along axis - p.setPen(self.pen()) - p.drawLine(*span) - p.translate(0.5,0) ## resolves some damn pixel ambiguity - ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: @@ -633,7 +664,7 @@ class AxisItem(GraphicsWidget): ## draw ticks ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) ## draw three different intervals, long ticks first - + tickSpecs = [] for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] @@ -663,15 +694,38 @@ class AxisItem(GraphicsWidget): color = tickPen.color() color.setAlpha(lineAlpha) tickPen.setColor(color) - p.setPen(tickPen) - p.drawLine(Point(p1), Point(p2)) - prof.mark('draw ticks') + tickSpecs.append((tickPen, Point(p1), Point(p2))) + prof.mark('compute ticks') - ## Draw text until there is no more room (or no more text) - if self.tickFont is not None: - p.setFont(self.tickFont) + ## This is where the long axis line should be drawn + + if self.style['stopAxisAtTick'][0] is True: + stop = max(span[0].y(), min(map(min, tickPositions))) + if axis == 0: + span[0].setY(stop) + else: + span[0].setX(stop) + if self.style['stopAxisAtTick'][1] is True: + stop = min(span[1].y(), max(map(max, tickPositions))) + if axis == 0: + span[1].setY(stop) + else: + span[1].setX(stop) + axisSpec = (self.pen(), span[0], span[1]) + + + + textOffset = self.style['tickTextOffset'] ## spacing between axis and text + #if self.style['autoExpandTextSpace'] is True: + #textWidth = self.textWidth + #textHeight = self.textHeight + #else: + #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text + #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text + textRects = [] + textSpecs = [] ## list of draw for i in range(len(tickLevels)): ## Get the list of strings to display for this level if tickStrings is None: @@ -688,18 +742,34 @@ class AxisItem(GraphicsWidget): if tickPositions[i][j] is None: strings[j] = None - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) for s in strings if s is not None]) + ## Measure density of text; decide whether to draw this level + rects = [] + for s in strings: + if s is None: + rects.append(None) + else: + br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) + ## boundingRect is usually just a bit too large + ## (but this probably depends on per-font metrics?) + br.setHeight(br.height() * 0.8) + + rects.append(br) + textRects.append(rects[-1]) + if i > 0: ## always draw top level ## measure all text, make sure there's enough room if axis == 0: textSize = np.sum([r.height() for r in textRects]) + textSize2 = np.max([r.width() for r in textRects]) else: textSize = np.sum([r.width() for r in textRects]) + textSize2 = np.max([r.height() for r in textRects]) ## If the strings are too crowded, stop drawing text now textFillRatio = float(textSize) / lengthInPixels if textFillRatio > 0.7: break + #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) for j in range(len(strings)): @@ -708,24 +778,61 @@ class AxisItem(GraphicsWidget): continue vstr = str(vstr) x = tickPositions[i][j] - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + textRect = rects[j] height = textRect.height() - self.textHeight = height + width = textRect.width() + #self.textHeight = height + offset = max(0,self.tickLength) + textOffset if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height) elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height) elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height) elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height) - p.setPen(self.pen()) - p.drawText(rect, textFlags, vstr) + #p.setPen(self.pen()) + #p.drawText(rect, textFlags, vstr) + textSpecs.append((rect, textFlags, vstr)) + prof.mark('compute text') + + ## update max text size if needed. + self._updateMaxTextSize(textSize2) + + return (axisSpec, tickSpecs, textSpecs) + + def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): + prof = debug.Profiler("AxisItem.drawPicture", disabled=True) + + p.setRenderHint(p.Antialiasing, False) + p.setRenderHint(p.TextAntialiasing, True) + + ## draw long line along axis + pen, p1, p2 = axisSpec + p.setPen(pen) + p.drawLine(p1, p2) + p.translate(0.5,0) ## resolves some damn pixel ambiguity + + ## draw ticks + for pen, p1, p2 in tickSpecs: + p.setPen(pen) + p.drawLine(p1, p2) + prof.mark('draw ticks') + + ## Draw all text + if self.tickFont is not None: + p.setFont(self.tickFont) + p.setPen(self.pen()) + for rect, flags, text in textSpecs: + p.drawText(rect, flags, text) + #p.drawRect(rect) + prof.mark('draw text') prof.finish() diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 3100087a..c226b9c4 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1079,6 +1079,7 @@ class PlotItem(GraphicsWidget): ============= ================================================================= """ self.getAxis(axis).setLabel(text=text, units=units, **args) + self.showAxis(axis) def setLabels(self, **kwds): """