Source code for ibl_alignment_gui.utils.qt.adapted_axis

import numpy as np
import pyqtgraph as pg
from pyqtgraph import debug as debug


[docs] class AdaptedAxisItem(pg.AxisItem): """An AxisItem that does not hide overlapping labels.""" def __init__(self, orientation, parent=None,): pg.AxisItem.__init__(self, orientation, parent=parent) self.style['hideOverlappingLabels'] = False
[docs] def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): profiler = debug.Profiler() 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) profiler('draw ticks') # Draw all text if self.style['tickFont'] is not None: p.setFont(self.style['tickFont']) p.setPen(self.textPen()) for rect, flags, text in textSpecs: p.drawText(rect, int(flags), text) profiler('draw text')
[docs] 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(). """ profiler = debug.Profiler() if self.style['tickFont'] is not None: p.setFont(self.style['tickFont']) bounds = self.mapRectFromParent(self.geometry()) linkedView = self.linkedView() if linkedView is None or self.grid is False: tickBounds = bounds else: tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) if self.orientation == 'left': span = (bounds.topRight(), bounds.bottomRight()) tickStart = tickBounds.right() tickStop = bounds.right() tickDir = -1 axis = 0 elif self.orientation == 'right': span = (bounds.topLeft(), bounds.bottomLeft()) tickStart = tickBounds.left() tickStop = bounds.left() tickDir = 1 axis = 0 elif self.orientation == 'top': span = (bounds.bottomLeft(), bounds.bottomRight()) tickStart = tickBounds.bottom() tickStop = bounds.bottom() tickDir = -1 axis = 1 elif self.orientation == 'bottom': span = (bounds.topLeft(), bounds.topRight()) tickStart = tickBounds.top() tickStop = bounds.top() tickDir = 1 axis = 1 else: raise ValueError("self.orientation must be in ('left', 'right', 'top', 'bottom')") # print tickStart, tickStop, span ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: return lengthInPixels = pg.Point(points[1] - points[0]).length() if lengthInPixels == 0: return # Determine major / minor / subminor axis ticks if self._tickLevels is None: tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels) tickStrings = None else: ## parse self.tickLevels into the formats returned by tickLevels() and tickStrings() tickLevels = [] tickStrings = [] for level in self._tickLevels: values = [] strings = [] tickLevels.append((None, values)) tickStrings.append(strings) for val, strn in level: values.append(val) strings.append(strn) ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: xScale = 1 offset = 0 else: if axis == 0: xScale = -bounds.height() / dif offset = self.range[0] * xScale - bounds.height() else: xScale = bounds.width() / dif offset = self.range[0] * xScale xRange = [x * xScale - offset for x in self.range] xMin = min(xRange) xMax = max(xRange) profiler('init') tickPositions = [] # remembers positions of previously drawn ticks ## compute coordinates to draw ticks ## draw three different intervals, long ticks first tickSpecs = [] for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] ## length of tick tickLength = self.style['tickLength'] / ((i * 0.5) + 1.0) lineAlpha = self.style["tickAlpha"] if lineAlpha is None: lineAlpha = 255 / (i + 1) if self.grid is not False: lineAlpha *= self.grid / 255. * pg.functions.clip_scalar((0.05 * lengthInPixels / (len(ticks) + 1)), 0., 1.) elif isinstance(lineAlpha, float): lineAlpha *= 255 lineAlpha = max(0, int(round(lineAlpha))) lineAlpha = min(255, int(round(lineAlpha))) elif isinstance(lineAlpha, int): if (lineAlpha > 255) or (lineAlpha < 0): raise ValueError("lineAlpha should be [0..255]") else: raise TypeError("Line Alpha should be of type None, float or int") for v in ticks: # determine actual position to draw this tick x = (v * xScale) - offset if x < xMin or x > xMax: # last check to make sure no out-of-bounds ticks are drawn tickPositions[i].append(None) continue tickPositions[i].append(x) p1 = [x, x] p2 = [x, x] p1[axis] = tickStart p2[axis] = tickStop if self.grid is False: p2[axis] += tickLength * tickDir tickPen = self.pen() color = tickPen.color() color.setAlpha(int(lineAlpha)) tickPen.setColor(color) tickSpecs.append((tickPen, pg.Point(p1), pg.Point(p2))) profiler('compute ticks') if self.style['stopAxisAtTick'][0] is True: minTickPosition = min(map(min, tickPositions)) if axis == 0: stop = max(span[0].y(), minTickPosition) span[0].setY(stop) else: stop = max(span[0].x(), minTickPosition) span[0].setX(stop) if self.style['stopAxisAtTick'][1] is True: maxTickPosition = max(map(max, tickPositions)) if axis == 0: stop = min(span[1].y(), maxTickPosition) span[1].setY(stop) else: stop = min(span[1].x(), maxTickPosition) span[1].setX(stop) axisSpec = (self.pen(), span[0], span[1]) textOffset = self.style['tickTextOffset'][axis] # 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 textSize2 = 0 lastTextSize2 = 0 textRects = [] textSpecs = [] # list of draw # If values are hidden, return early if not self.style['showValues']: return (axisSpec, tickSpecs, textSpecs) for i in range(min(len(tickLevels), self.style['maxTextLevel'] + 1)): ## Get the list of strings to display for this level if tickStrings is None: spacing, values = tickLevels[i] strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) else: strings = tickStrings[i] if len(strings) == 0: continue ## ignore strings belonging to ticks that were previously ignored for j in range(len(strings)): if tickPositions[i][j] is None: strings[j] = 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(pg.QtCore.QRectF(0, 0, 100, 100), pg.QtCore.Qt.AlignmentFlag.AlignCenter, 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 len(textRects) > 0: ## 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]) else: textSize = 0 textSize2 = 0 if i > 0: # always draw top level # If the strings are too crowded, stop drawing text now. # We use three different crowding limits based on the number # of texts drawn so far. textFillRatio = float(textSize) / lengthInPixels finished = False for nTexts, limit in self.style['textFillLimits']: if len(textSpecs) >= nTexts and textFillRatio >= limit: finished = True break if finished: break lastTextSize2 = textSize2 # spacing, values = tickLevels[best] # strings = self.tickStrings(values, self.scale, spacing) # Determine exactly where tick text should be drawn for j in range(len(strings)): vstr = strings[j] if vstr is None: # this tick was ignored because it is out of bounds continue x = tickPositions[i][j] # textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignmentFlag.AlignCenter, vstr) textRect = rects[j] height = textRect.height() width = textRect.width() # self.textHeight = height offset = max(0, self.style['tickLength']) + textOffset rect = pg.QtCore.QRectF() if self.orientation == 'left': alignFlags = pg.QtCore.Qt.AlignmentFlag.AlignRight | pg.QtCore.Qt.AlignmentFlag.AlignVCenter rect = pg.QtCore.QRectF(tickStop - offset - width, x - (height / 2), width, height) elif self.orientation == 'right': alignFlags = pg.QtCore.Qt.AlignmentFlag.AlignLeft | pg.QtCore.Qt.AlignmentFlag.AlignVCenter rect = pg.QtCore.QRectF(tickStop + offset, x - (height / 2), width, height) elif self.orientation == 'top': alignFlags = pg.QtCore.Qt.AlignmentFlag.AlignHCenter | pg.QtCore.Qt.AlignmentFlag.AlignBottom rect = pg.QtCore.QRectF(x - width / 2., tickStop - offset - height, width, height) elif self.orientation == 'bottom': alignFlags = pg.QtCore.Qt.AlignmentFlag.AlignHCenter | pg.QtCore.Qt.AlignmentFlag.AlignTop rect = pg.QtCore.QRectF(x - width / 2., tickStop + offset, width, height) textFlags = alignFlags | pg.QtCore.Qt.TextFlag.TextDontClip # This is the part that has changed compared to the original AxisItem # p.setPen(self.pen()) # p.drawText(rect, textFlags, vstr) # br = self.boundingRect() # if not br.contains(rect): # continue textSpecs.append((rect, textFlags, vstr)) profiler('compute text') ## update max text size if needed. self._updateMaxTextSize(lastTextSize2) return (axisSpec, tickSpecs, textSpecs)
[docs] def replace_axis(plot_item: pg.PlotItem, orientation: str = 'left', pos: tuple = (2, 0)) -> None: """ Replace the axis of a PlotItem with an AdaptedAxisItem. Parameters ---------- plot_item: pg.PlotItem The PlotItem to modify. orientation: str The orientation of the axis to replace ('left', 'right', 'top', 'bottom pos: tuple The position in the layout to place the new axis. """ new_axis = AdaptedAxisItem(orientation, parent=plot_item) oldAxis = plot_item.axes[orientation]['item'] plot_item.layout.removeItem(oldAxis) oldAxis.unlinkFromView() # new_axis.linkToView(plot_item.vb) plot_item.axes[orientation] = {'item': new_axis, 'pos': pos} plot_item.layout.addItem(new_axis, *pos) new_axis.setZValue(-1000) new_axis.setFlag(new_axis.ItemNegativeZStacksBehindParent)