Cleanup for AxisItem
- Made more extensible by breaking out tick spacing and text generating into separate methods - Text now tries harder to avoid overlapping
This commit is contained in:
parent
78d4bc0838
commit
6aef85331e
@ -13,6 +13,7 @@ class AxisItem(GraphicsWidget):
|
||||
GraphicsItem showing a single plot axis with ticks, values, and label.
|
||||
Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items.
|
||||
Ticks can be extended to make a grid.
|
||||
If maxTickLength is negative, ticks point into the plot.
|
||||
"""
|
||||
|
||||
|
||||
@ -259,6 +260,100 @@ class AxisItem(GraphicsWidget):
|
||||
self.picture.play(p)
|
||||
|
||||
|
||||
|
||||
def tickSpacing(self, minVal, maxVal, size):
|
||||
"""Return values describing the desired spacing and offset of ticks.
|
||||
|
||||
This method is called whenever the axis needs to be redrawn and is a
|
||||
good method to override in subclasses that require control over tick locations.
|
||||
|
||||
The return value must be a list of three tuples:
|
||||
[
|
||||
(major tick spacing, offset),
|
||||
(minor tick spacing, offset),
|
||||
(sub-minor tick spacing, offset),
|
||||
...
|
||||
]
|
||||
"""
|
||||
dif = abs(maxVal - minVal)
|
||||
if dif == 0:
|
||||
return []
|
||||
|
||||
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
||||
pixelSpacing = np.log(size+10) * 5
|
||||
optimalTickCount = size / pixelSpacing
|
||||
if optimalTickCount < 1:
|
||||
optimalTickCount = 1
|
||||
|
||||
## optimal minor tick spacing
|
||||
optimalSpacing = dif / optimalTickCount
|
||||
|
||||
## the largest power-of-10 spacing which is smaller than optimal
|
||||
p10unit = 10 ** np.floor(np.log10(optimalSpacing))
|
||||
|
||||
## Determine major/minor tick spacings which flank the optimal spacing.
|
||||
intervals = np.array([1., 2., 10., 20., 100.]) * p10unit
|
||||
minorIndex = 0
|
||||
while intervals[minorIndex+1] <= optimalSpacing:
|
||||
minorIndex += 1
|
||||
|
||||
return [
|
||||
(intervals[minorIndex+2], 0),
|
||||
(intervals[minorIndex+1], 0),
|
||||
(intervals[minorIndex], 0)
|
||||
]
|
||||
|
||||
|
||||
def tickValues(self, minVal, maxVal, size):
|
||||
"""
|
||||
Return the values and spacing of ticks to draw
|
||||
[
|
||||
(spacing, [major ticks]),
|
||||
(spacing, [minor ticks]),
|
||||
...
|
||||
]
|
||||
|
||||
By default, this method calls tickSpacing to determine the correct tick locations.
|
||||
This is a good method to override in subclasses.
|
||||
"""
|
||||
ticks = []
|
||||
tickLevels = self.tickSpacing(minVal, maxVal, size)
|
||||
for i in range(len(tickLevels)):
|
||||
spacing, offset = tickLevels[i]
|
||||
|
||||
## determine starting tick
|
||||
start = (np.ceil((minVal-offset) / spacing) * spacing) + offset
|
||||
|
||||
## determine number of ticks
|
||||
num = int((maxVal-start) / spacing) + 1
|
||||
ticks.append((spacing, np.arange(num) * spacing + start))
|
||||
return ticks
|
||||
|
||||
|
||||
def tickStrings(self, values, scale, spacing):
|
||||
"""Return the strings that should be placed next to ticks. This method is called
|
||||
when redrawing the axis and is a good method to override in subclasses.
|
||||
The method is called with a list of tick values, a scaling factor (see below), and the
|
||||
spacing between ticks (this is required since, in some instances, there may be only
|
||||
one tick and thus no other way to determine the tick spacing)
|
||||
|
||||
The scale argument is used when the axis label is displaying units which may have an SI scaling prefix.
|
||||
When determining the text to display, use value*scale to correctly account for this prefix.
|
||||
For example, if the axis label's units are set to 'V', then a tick value of 0.001 might
|
||||
be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and
|
||||
thus the tick should display 0.001 * 1000 = 1.
|
||||
"""
|
||||
places = max(0, np.ceil(-np.log10(spacing*scale)))
|
||||
strings = []
|
||||
for v in values:
|
||||
vs = v * scale
|
||||
if abs(vs) < .001 or abs(vs) >= 10000:
|
||||
vstr = "%g" % vs
|
||||
else:
|
||||
vstr = ("%%0.%df" % places) % vs
|
||||
strings.append(vstr)
|
||||
return strings
|
||||
|
||||
def drawPicture(self, p):
|
||||
|
||||
p.setRenderHint(p.Antialiasing, False)
|
||||
@ -272,174 +367,115 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
linkedView = self.linkedView()
|
||||
if linkedView is None or self.grid is False:
|
||||
tbounds = bounds
|
||||
tickBounds = bounds
|
||||
else:
|
||||
tbounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||
tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||
|
||||
if self.orientation == 'left':
|
||||
span = (bounds.topRight(), bounds.bottomRight())
|
||||
tickStart = tbounds.right()
|
||||
tickStart = tickBounds.right()
|
||||
tickStop = bounds.right()
|
||||
tickDir = -1
|
||||
axis = 0
|
||||
elif self.orientation == 'right':
|
||||
span = (bounds.topLeft(), bounds.bottomLeft())
|
||||
tickStart = tbounds.left()
|
||||
tickStart = tickBounds.left()
|
||||
tickStop = bounds.left()
|
||||
tickDir = 1
|
||||
axis = 0
|
||||
elif self.orientation == 'top':
|
||||
span = (bounds.bottomLeft(), bounds.bottomRight())
|
||||
tickStart = tbounds.bottom()
|
||||
tickStart = tickBounds.bottom()
|
||||
tickStop = bounds.bottom()
|
||||
tickDir = -1
|
||||
axis = 1
|
||||
elif self.orientation == 'bottom':
|
||||
span = (bounds.topLeft(), bounds.topRight())
|
||||
tickStart = tbounds.top()
|
||||
tickStart = tickBounds.top()
|
||||
tickStop = bounds.top()
|
||||
tickDir = 1
|
||||
axis = 1
|
||||
#print tickStart, tickStop, span
|
||||
|
||||
## draw long line along axis
|
||||
p.drawLine(*span)
|
||||
p.translate(0.5,0) ## resolves some damn pixel ambiguity
|
||||
|
||||
## determine size of this item in pixels
|
||||
points = map(self.mapToDevice, span)
|
||||
lengthInPixels = Point(points[1] - points[0]).length()
|
||||
|
||||
## decide optimal tick spacing in pixels
|
||||
pixelSpacing = np.log(lengthInPixels+10) * 2
|
||||
optimalTickCount = lengthInPixels / pixelSpacing
|
||||
|
||||
## Determine optimal tick spacing
|
||||
#intervals = [1., 2., 5., 10., 20., 50.]
|
||||
#intervals = [1., 2.5, 5., 10., 25., 50.]
|
||||
intervals = np.array([0.1, 0.2, 1., 2., 10., 20., 100., 200.])
|
||||
dif = abs(self.range[1] - self.range[0])
|
||||
if dif == 0.0:
|
||||
if lengthInPixels == 0:
|
||||
return
|
||||
pw = 10 ** (np.floor(np.log10(dif))-1)
|
||||
scaledIntervals = intervals * pw
|
||||
scaledTickCounts = dif / scaledIntervals
|
||||
try:
|
||||
i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0]
|
||||
except:
|
||||
print "AxisItem can't determine tick spacing:"
|
||||
print "scaledTickCounts", scaledTickCounts
|
||||
print "optimalTickCount", optimalTickCount
|
||||
print "dif", dif
|
||||
print "scaledIntervals", scaledIntervals
|
||||
print "intervals", intervals
|
||||
print "pw", pw
|
||||
print "pixelSpacing", pixelSpacing
|
||||
i1 = 1
|
||||
|
||||
distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1])
|
||||
|
||||
#print optimalTickCount, i1, scaledIntervals, distBetweenIntervals
|
||||
|
||||
#for i in range(len(intervals)):
|
||||
#i1 = i
|
||||
#if dif / (pw*intervals[i]) < 10:
|
||||
#break
|
||||
tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
|
||||
|
||||
textLevel = 1 ## draw text at this scale level
|
||||
|
||||
#print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp)
|
||||
|
||||
#print " start at %f, %d ticks" % (start, num)
|
||||
|
||||
|
||||
## determine mapping between tick values and local coordinates
|
||||
dif = self.range[1] - self.range[0]
|
||||
if axis == 0:
|
||||
xs = -bounds.height() / dif
|
||||
xScale = -bounds.height() / dif
|
||||
offset = self.range[0] * xScale - bounds.height()
|
||||
else:
|
||||
xs = bounds.width() / dif
|
||||
xScale = bounds.width() / dif
|
||||
offset = self.range[0] * xScale
|
||||
|
||||
prof.mark('init')
|
||||
|
||||
tickPositions = set() # remembers positions of previously drawn ticks
|
||||
## draw ticks and generate list of texts to draw
|
||||
tickPositions = [] # remembers positions of previously drawn ticks
|
||||
|
||||
## 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
|
||||
texts = []
|
||||
for i in [2,1,0]:
|
||||
if i1+i >= len(intervals) or i1+i < 0:
|
||||
print "AxisItem.paint error: i1=%d, i=%d, len(intervals)=%d" % (i1, i, len(intervals))
|
||||
continue
|
||||
|
||||
## spacing for this interval
|
||||
sp = pw*intervals[i1+i]
|
||||
|
||||
## determine starting tick
|
||||
start = np.ceil(self.range[0] / sp) * sp
|
||||
|
||||
## determine number of ticks
|
||||
num = int(dif / sp) + 1
|
||||
|
||||
## last tick value
|
||||
last = start + sp * num
|
||||
|
||||
## Number of decimal places to print
|
||||
maxVal = max(abs(start), abs(last))
|
||||
places = max(0, np.ceil(-np.log10(sp*self.scale)))
|
||||
#print i, sp, sp*self.scale, np.log10(sp*self.scale), places
|
||||
for i in range(len(tickLevels)):
|
||||
tickPositions.append([])
|
||||
ticks = tickLevels[i][1]
|
||||
|
||||
## length of tick
|
||||
#h = np.clip((self.tickLength*3 / num) - 1., min(0, self.tickLength), max(0, self.tickLength))
|
||||
if i == 0:
|
||||
h = self.tickLength * distBetweenIntervals / 2.
|
||||
else:
|
||||
h = self.tickLength*i/2.
|
||||
|
||||
## alpha
|
||||
if i == 0:
|
||||
#a = min(255, (765. / num) - 1.)
|
||||
a = 255 * distBetweenIntervals
|
||||
else:
|
||||
a = 255
|
||||
|
||||
lineAlpha = a
|
||||
textAlpha = a
|
||||
tickLength = self.tickLength / ((i*1.0)+1.0)
|
||||
|
||||
lineAlpha = 255 / (i+1)
|
||||
if self.grid is not False:
|
||||
lineAlpha = int(lineAlpha * self.grid / 255.)
|
||||
lineAlpha = self.grid
|
||||
|
||||
if axis == 0:
|
||||
offset = self.range[0] * xs - bounds.height()
|
||||
else:
|
||||
offset = self.range[0] * xs
|
||||
|
||||
for j in range(num):
|
||||
v = start + sp * j
|
||||
x = (v * xs) - offset
|
||||
p1 = [0, 0]
|
||||
p2 = [0, 0]
|
||||
for v in ticks:
|
||||
x = (v * xScale) - offset
|
||||
p1 = [x, x]
|
||||
p2 = [x, x]
|
||||
p1[axis] = tickStart
|
||||
p2[axis] = tickStop
|
||||
if self.grid is False:
|
||||
p2[axis] += h*tickDir
|
||||
p1[1-axis] = p2[1-axis] = x
|
||||
|
||||
if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]:
|
||||
continue
|
||||
if p1[1-axis] < 0:
|
||||
continue
|
||||
p2[axis] += tickLength*tickDir
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
|
||||
|
||||
# draw tick only if there is none
|
||||
tickPos = p1[1-axis]
|
||||
|
||||
#if tickPos not in tickPositions:
|
||||
p.drawLine(Point(p1), Point(p2))
|
||||
#tickPositions.add(tickPos)
|
||||
if i == textLevel:
|
||||
if abs(v*self.scale) < .001 or abs(v*self.scale) >= 10000:
|
||||
vstr = "%g" % (v * self.scale)
|
||||
else:
|
||||
vstr = ("%%0.%df" % places) % (v * self.scale)
|
||||
#print " ", v*self.scale, places, vstr
|
||||
tickPositions[i].append(x)
|
||||
prof.mark('draw ticks')
|
||||
|
||||
## determine level to draw text
|
||||
best = 0
|
||||
for i in range(len(tickLevels)):
|
||||
## take a small sample of strings and measure their rendered text
|
||||
spacing, values = tickLevels[i]
|
||||
strings = self.tickStrings(values[:2], self.scale, spacing)
|
||||
textRects = [p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings]
|
||||
if axis == 0:
|
||||
textSize = np.max([r.height() for r in textRects])
|
||||
else:
|
||||
textSize = np.max([r.width() for r in textRects])
|
||||
|
||||
## If these strings are not too crowded, then this level is ok
|
||||
textFillRatio = float(textSize * len(values)) / lengthInPixels
|
||||
if textFillRatio < 0.7:
|
||||
best = i
|
||||
continue
|
||||
prof.mark('measure text')
|
||||
|
||||
spacing, values = tickLevels[best]
|
||||
strings = self.tickStrings(values, self.scale, spacing)
|
||||
for j in range(len(strings)):
|
||||
vstr = strings[j]
|
||||
x = tickPositions[best][j]
|
||||
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
|
||||
height = textRect.height()
|
||||
self.textHeight = height
|
||||
@ -456,14 +492,8 @@ class AxisItem(GraphicsWidget):
|
||||
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
|
||||
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
|
||||
|
||||
#p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a)))
|
||||
#p.drawText(rect, textFlags, vstr)
|
||||
texts.append((rect, textFlags, vstr, textAlpha))
|
||||
|
||||
prof.mark('draw ticks')
|
||||
for args in texts:
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, args[3])))
|
||||
p.drawText(*args[:3])
|
||||
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150)))
|
||||
p.drawText(rect, textFlags, vstr)
|
||||
prof.mark('draw text')
|
||||
prof.finish()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user