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.tickFont is not None: p.setFont(self.tickFont) p.setPen(self.pen()) for rect, flags, text in textSpecs: if float(text[:-1]) > 0: p.setPen(pg.mkPen('r')) else: p.setPen(pg.mkPen('g')) p.drawText(rect, flags, text) #p.drawRect(rect) profiler('draw text')
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()
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')
def __init__(self, *args, **kargs): """ Accepts the same arguments as setData() """ prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) GraphicsObject.__init__(self) self.picture = None # QPicture used for rendering when pxmode==False self.fragments = None # fragment specification for pxmode; updated every time the view changes. self.fragmentAtlas = SymbolAtlas() self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)]) 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. 'antialias': pg.getConfigOption('antialias'), } self.setPen(200,200,200, update=False) self.setBrush(100,100,150, update=False) self.setSymbol('o', update=False) self.setSize(7, update=False) prof.mark('1') self.setData(*args, **kargs) prof.mark('setData') prof.finish()
def paint(self, p, *args): profile = debug.Profiler() if self.image is None: return if self.qimage is None: self.shouldPaint = False return if not self.shouldPaint: # Skip first frame when displaying a new image and there was no image displayed before self.shouldPaint = True return if self.paintMode is not None: p.setCompositionMode(self.paintMode) profile('set comp mode') shape = self.image.shape[: 2] if self.axisOrder == 'col-major' else self.image.shape[: 2][:: -1] p.drawImage(QtCore.QRectF(0, 0, *shape), self.qimage) profile('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect())
def updateData(self, *args, **kargs): prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) if len(args) == 1: kargs['y'] = args[0] elif len(args) == 2: kargs['x'] = args[0] kargs['y'] = args[1] if 'y' not in kargs or kargs['y'] is None: kargs['y'] = np.array([]) if 'x' not in kargs or kargs['x'] is None: kargs['x'] = np.arange(len(kargs['y'])) for k in ['x', 'y']: data = kargs[k] if isinstance(data, list): data = np.array(data) kargs[k] = data if not isinstance(data, np.ndarray) or data.ndim > 1: raise Exception("Plot data must be 1D ndarray.") if 'complex' in str(data.dtype): raise Exception("Can not plot complex data types.") prof.mark("data checks") #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot self.prepareGeometryChange() self.yData = kargs['y'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray) prof.mark('copy') if self.xData.shape != self.yData.shape: raise Exception( "X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) self.path = None self.fillPath = None #self.xDisp = self.yDisp = None if 'pen' in kargs: self.setPen(kargs['pen']) if 'shadowPen' in kargs: self.setShadowPen(kargs['shadowPen']) if 'fillLevel' in kargs: self.setFillLevel(kargs['fillLevel']) if 'brush' in kargs: self.setBrush(kargs['brush']) prof.mark('set') self.update() prof.mark('update') self.sigPlotChanged.emit(self) prof.mark('emit') prof.finish()
def setImage(self, image=None, autoLevels=None, **kargs): """ Same this as ImageItem.setImage, but we don't update the drawing """ profile = debug.Profiler() gotNewData = False if image is None: if self.image is None: return else: gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) image = image.view(np.ndarray) if self.image is None or image.dtype != self.image.dtype: self._effectiveLut = None self.image = image if self.image.shape[0] > 2**15 - 1 or self.image.shape[ 1] > 2**15 - 1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True if shapeChanged: self.prepareGeometryChange() self.informViewBoundsChanged() profile() if autoLevels is None: if 'levels' in kargs: autoLevels = False else: autoLevels = True if autoLevels: img = self.image while img.size > 2**16: img = img[::2, ::2] mn, mx = img.min(), img.max() if mn == mx: mn = 0 mx = 255 kargs['levels'] = [mn, mx] profile() self.setOpts(update=False, **kargs) profile() self.qimage = None self.update() profile() if gotNewData: self.sigImageChanged.emit()
def setImage(self, image=None, autoLevels=None, **kargs): profile = debug.Profiler() gotNewData = False if image is None: if self.image is None: return else: gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) image = image.view(np.ndarray) if self.image is None or image.dtype != self.image.dtype: self._effectiveLut = None self.image = image if self.image.shape[0] > 2**15 - 1 or self.image.shape[ 1] > 2**15 - 1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True if shapeChanged: self.prepareGeometryChange() self.informViewBoundsChanged() profile() if autoLevels is None: if 'levels' in kargs: autoLevels = False else: autoLevels = True if autoLevels: img = self.image while img.size > 2**16: img = img[::2, ::2] mn, mx = np.nanmin(img), np.nanmax(img) # mn and mx can still be NaN if the data is all-NaN if mn == mx or np.isnan(mn) or np.isnan(mx): mn = 0 mx = 255 kargs['levels'] = [mn, mx] profile() self.setOpts(update=False, **kargs) profile() self.render() profile() if gotNewData: self.sigImageChanged.emit()
def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return self.plot.setData(*h) profiler('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) profiler('set region')
def render(self): prof = debug.Profiler('ImageItem.render', disabled=True) if self.image is None: return if isinstance(self.lut, collections.Callable): lut = self.lut(self.image) else: lut = self.lut #print lut.shape #print self.lut argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha) prof.finish()
def imageChanged(self, autoLevel=False, autoRange=False): prof = debug.Profiler('HistogramLUTItem.imageChanged', disabled=True) h = self.imageItem.getHistogram() prof.mark('get histogram') if h[0] is None: return self.plot.setData(*h) prof.mark('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) prof.mark('set region') prof.finish()
def generatePath(self, x, y): prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) path = QtGui.QPainterPath() ## Create all vertices in path. The method used below creates a binary format so that all ## vertices can be read in at once. This binary format may change in future versions of Qt, ## so the original (slower) method is left here for emergencies: #path.moveTo(x[0], y[0]) #for i in range(1, y.shape[0]): # path.lineTo(x[i], y[i]) ## Speed this up using >> operator ## Format is: ## numVerts(i4) 0(i4) ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex ## ... ## 0(i4) ## ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') n = x.shape[0] # create empty array, pad with extra space on either end arr = np.empty(n + 2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) # write first two integers prof.mark('allocate empty') arr.data[12:20] = struct.pack('>ii', n, 0) prof.mark('pack header') # Fill array with vertex values arr[1:-1]['x'] = x arr[1:-1]['y'] = y arr[1:-1]['c'] = 1 prof.mark('fill array') # write last 0 lastInd = 20 * (n + 1) arr.data[lastInd:lastInd + 4] = struct.pack('>i', 0) prof.mark('footer') # create datastream object and stream into path buf = QtCore.QByteArray( arr.data[12:lastInd + 4]) # I think one unnecessary copy happens here prof.mark('create buffer') ds = QtCore.QDataStream(buf) prof.mark('create datastream') ds >> path prof.mark('load') prof.finish() return path
def render(self): prof = debug.Profiler('ImageItem.render', disabled=True) if self.image is None: return if callable(self.lut): lut = self.lut(self.image) else: lut = self.lut #print lut.shape #print self.lut argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha) #self.pixmap = QtGui.QPixmap.fromImage(self.qimage) prof.finish()
def paint(self, p, *args): prof = debug.Profiler('ImageItem.paint', disabled=True) if self.image is None: return if self.qimage is None: self.render() prof.mark('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) prof.mark('set comp mode') p.drawImage(QtCore.QPointF(0, 0), self.qimage) prof.mark('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect()) prof.finish()
def paint(self, p, opt, widget): prof = debug.Profiler('AxisItem.paint', disabled=True) if self.picture is None: try: picture = QtGui.QPicture() painter = QtGui.QPainter(picture) specs = self.generateDrawSpecs(painter) prof.mark('generate specs') if specs is not None: self.drawPicture(painter, *specs) prof.mark('draw picture') 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) prof.finish()
def imageChanged(self, autoLevel=False, autoRange=False): if self.imageItem() is None: return if self.levelMode == 'mono': for plt in self.plots[1:]: plt.setVisible(False) self.plots[0].setVisible(True) # plot one histogram for all image data profiler = debug.Profiler() h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return self.plot.setData(*h) profiler('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) profiler('set region') else: mn, mx = self.imageItem().levels self.region.setRegion([mn, mx]) else: # plot one histogram for each channel self.plots[0].setVisible(False) ch = self.imageItem().getHistogram(perChannel=True) if ch[0] is None: return for i in range(1, 5): if len(ch) >= i: h = ch[i-1] self.plots[i].setVisible(True) self.plots[i].setData(*h) if autoLevel: mn = h[0][0] mx = h[0][-1] self.region[i].setRegion([mn, mx]) else: # hide channels not present in image data self.plots[i].setVisible(False) # make sure we are displaying the correct number of channels self._showRegions()
def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() img = self.imageItem().image if img is None: histRange = None else: histRange = (np.nanmin(img), np.nanmax(img)) h = self.imageItem().getHistogram(range=histRange) profiler('get histogram') if h[0] is None: return self.plot.setData(*h) profiler('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) profiler('set region')
def imageChanged(self, autoRange=False): prof = debug.Profiler('HistogramLUTItem.imageChanged', disabled=True) h = list(self.imageItem.getHistogram(bins=1000)) prof.mark('get histogram') if h[0] is None: return h[1][1:] = np.log(h[1][1:]) h[0][1:] = np.log(h[0][1:]) h[0][0] = 0 h[1][0] = h[1][1] if self.orientation == 'horizontal': self.plot.setData(h[0], h[1]) elif self.orientation == 'vertical': self.plot.setData(h[1], h[0]) self.hist_x_range = np.max(h[0]) - np.min(h[0])
def paint(self, p, *args): profile = debug.Profiler() if self.image is None: return if self.qimage is None: self.render() if self.qimage is None: return profile('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) profile('set comp mode') shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1] p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage) profile('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect())
def render(self): # Convert data to QImage for display. profile = debug.Profiler() if self.image is None or self.image.size == 0: return if isinstance(self.lut, collections.Callable): lut = self.lut(self.image) else: lut = self.lut if self.autoDownsample: # reduce dimensions of image based on screen resolution o = self.mapToDevice(QtCore.QPointF(0, 0)) x = self.mapToDevice(QtCore.QPointF(1, 0)) y = self.mapToDevice(QtCore.QPointF(0, 1)) w = Point(x - o).length() h = Point(y - o).length() if w == 0 or h == 0: self.qimage = None return xds = max(1, int(1.0 / w)) yds = max(1, int(1.0 / h)) axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] #TODO adapt downsample #image = fn.downsample(self.image, xds, axis=axes[0]) #image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) else: image = self.image # if the image data is a small int, then we can combine levels + lut # into a single lut for better performance levels = self.levels # Assume images are in column-major order for backward compatibility # (most images are in row-major order) self.triangulation, self.tri_data, rgba_values, alpha = makeAlphaTriangles( image, lut=lut, levels=levels, useRGBA=True) polygons = makePolygons(self.triangulation) self.qimage = dict(polygons=polygons, values=rgba_values, alpha=alpha)
def render(self): #The same as pyqtgraph's ImageItem.render, with the exception that the makeARGB function is slightly different profile = debug.Profiler() if self.image is None or self.image.size == 0: return if isinstance(self.lut, Callable): lut = self.lut(self.image) else: lut = self.lut if self.autoDownsample: # reduce dimensions of image based on screen resolution o = self.mapToDevice(QtCore.QPointF(0,0)) x = self.mapToDevice(QtCore.QPointF(1,0)) y = self.mapToDevice(QtCore.QPointF(0,1)) w = Point(x-o).length() h = Point(y-o).length() if w == 0 or h == 0: self.qimage = None return xds = max(1, int(1.0 / w)) yds = max(1, int(1.0 / h)) axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) else: image = self.image # Assume images are in column-major order for backward compatibility # (most images are in row-major order) if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = makeARGBwithNaNs(image, lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False)
def paint(self, p, *args): profile = debug.Profiler() if self.image is None: return if self.qimage is None: self.render() if self.qimage is None: return profile('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) profile('set comp mode') self.setTransform(self.dataTransform()) for pol, color in zip(self.qimage['polygons'], self.qimage['values']): p.setPen(fn.mkPen(255, 255, 255, 100, width=0.75)) p.setBrush(fn.mkBrush(*color)) p.drawPolygon(pol) profile('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect())
def render(self): # Convert data to QImage for display. profile = debug.Profiler() if self.image is None or self.image.size == 0: return if isinstance(self.lut, collections.Callable): lut = self.lut(self.image) else: lut = self.lut if self.logScale: image = self.image + 1 with np.errstate(invalid="ignore"): image = image.astype(np.float) np.log(image, where=image >= 0, out=image) # map to 0-255 else: image = self.image if self.autoDownsample: # reduce dimensions of image based on screen resolution o = self.mapToDevice(QPointF(0, 0)) x = self.mapToDevice(QPointF(1, 0)) y = self.mapToDevice(QPointF(0, 1)) w = Point(x - o).length() h = Point(y - o).length() if w == 0 or h == 0: self.qimage = None return xds = max(1, int(1.0 / w)) yds = max(1, int(1.0 / h)) axes = [1, 0] if self.axisOrder == "row-major" else [0, 1] image = fn.downsample(image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) else: pass # if the image data is a small int, then we can combine levels + lut # into a single lut for better performance levels = self.levels if levels is not None and levels.ndim == 1 and image.dtype in ( np.ubyte, np.uint16): if self._effectiveLut is None: eflsize = 2**(image.itemsize * 8) ind = np.arange(eflsize) minlev, maxlev = levels levdiff = maxlev - minlev levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 if lut is None: efflut = fn.rescaleData(ind, scale=255.0 / levdiff, offset=minlev, dtype=np.ubyte) else: lutdtype = np.min_scalar_type(lut.shape[0] - 1) efflut = fn.rescaleData(ind, scale=(lut.shape[0] - 1) / levdiff, offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0] - 1)) efflut = lut[efflut] self._effectiveLut = efflut lut = self._effectiveLut levels = None # Assume images are in column-major order for backward compatibility # (most images are in row-major order) if self.axisOrder == "col-major": image = image.transpose((1, 0, 2)[:image.ndim]) if self.logScale: with np.errstate(invalid="ignore"): levels = np.log(np.add(levels, 1)) levels[0] = np.nanmax([levels[0], 0]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False)
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): """ Set the image to be displayed in the widget. ================== =========================================================================== **Arguments:** img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and *notes* below. xvals (numpy array) 1D array of z-axis values corresponding to the third axis in a 3D image. For video, this array should contain the time of each frame. autoRange (bool) whether to scale/pan the view to fit the image. autoLevels (bool) whether to update the white/black levels to fit the image. levels (min, max); the white and black level values to use. axes Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. Format is:: {'t':0, 'x':1, 'y':2, 'c':3}; pos Change the position of the displayed image scale Change the scale of the displayed image transform Set the transform of the displayed image. This option overrides *pos* and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. ================== =========================================================================== **Notes:** For backward compatibility, image data is assumed to be in column-major order (column, row). However, most image data is stored in row-major order (row, column) and will need to be transposed before calling setImage():: imageview.setImage(imagedata.T) This requirement can be changed by the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`. """ profiler = debug.Profiler() if hasattr(img, 'implements') and img.implements('MetaArray'): img = img.asarray() if not isinstance(img, np.ndarray): required = ['dtype', 'max', 'min', 'ndim', 'shape', 'size'] if not all([hasattr(img, attr) for attr in required]): raise TypeError( "Image must be NumPy array or any object " "that provides compatible attributes/methods:\n" " %s" % str(required)) self.image = img self.imageDisp = None profiler() if axes is None: x, y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0) if img.ndim == 2: self.axes = {'t': None, 'x': x, 'y': y, 'c': None} elif img.ndim == 3: # Ambiguous case; make a guess if img.shape[2] <= 4: self.axes = {'t': None, 'x': x, 'y': y, 'c': 2} else: self.axes = {'t': 0, 'x': x + 1, 'y': y + 1, 'c': None} elif img.ndim == 4: # Even more ambiguous; just assume the default self.axes = {'t': 0, 'x': x + 1, 'y': y + 1, 'c': 3} else: raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): self.axes = axes.copy() elif isinstance(axes, list) or isinstance(axes, tuple): self.axes = {} for i in range(len(axes)): self.axes[axes[i]] = i else: raise Exception( "Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) axes = self.axes if xvals is not None: self.tVals = xvals elif axes['t'] is not None: if hasattr(img, 'xvals'): try: self.tVals = img.xvals(axes['t']) except: self.tVals = np.arange(img.shape[axes['t']]) else: self.tVals = np.arange(img.shape[axes['t']]) profiler() self.currentIndex = 0 self.updateImage(autoHistogramRange=autoHistogramRange) if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. self.setLevels(*levels) if self.ui.roiBtn.isChecked(): self.roiChanged() profiler() if self.axes['t'] is not None: #self.ui.roiPlot.show() self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) self.timeLine.setValue(0) #self.ui.roiPlot.setMouseEnabled(False, False) if len(self.tVals) > 1: start = self.tVals.min() stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 elif len(self.tVals) == 1: start = self.tVals[0] - 0.5 stop = self.tVals[0] + 0.5 else: start = 0 stop = 1 for s in [self.timeLine, self.normRgn]: s.setBounds([start, stop]) #else: #self.ui.roiPlot.hide() profiler() self.imageItem.resetTransform() if scale is not None: self.imageItem.scale(*scale) if pos is not None: self.imageItem.setPos(*pos) if transform is not None: self.imageItem.setTransform(transform) profiler() if autoRange: self.autoRange() self.roiClicked() profiler()
def setImage(self, image=None, autoLevels=None, **kargs): """ Update the image displayed by this item. For more information on how the image is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>` ================= ========================================================================= **Arguments:** image (numpy array) 2D array of: points coordinates (dim 0 is number of points) (dim 1 is x, y coordinates and point value) image.shape = (N, 3) Specifies the image data. May be 2D (width, height) or 3D (width, height, RGBa). The array dtype must be integer or floating point of any bit depth. For 3D arrays, the third dimension must be of length 3 (RGB) or 4 (RGBA). See *notes* below. autoLevels (bool) If True, this forces the image to automatically select levels based on the maximum and minimum values in the data. By default, this argument is true unless the levels argument is given. lut (numpy array) The color lookup table to use when displaying the image. See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`. levels (min, max) The minimum and maximum values to use when rescaling the image data. By default, this will be set to the minimum and maximum values in the image. If the image array has dtype uint8, no rescaling is necessary. opacity (float 0.0-1.0) compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>` border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the screen resolution. This improves performance for large images and reduces aliasing. ================= ========================================================================= **Notes:** For backward compatibility, image data is assumed to be in column-major order (column, row). However, most image data is stored in row-major order (row, column) and will need to be transposed before calling setImage():: imageitem.setImage(imagedata.T) This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`. """ profile = debug.Profiler() gotNewData = False if image is None: if self.image is None: return else: gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) image = image.view(np.ndarray) if self.image is None or image.dtype != self.image.dtype: self._effectiveLut = None self.image = image if self.image.shape[0] > 2**15 - 1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True if shapeChanged: self.prepareGeometryChange() self.informViewBoundsChanged() profile() if autoLevels is None: if 'levels' in kargs: autoLevels = False else: autoLevels = True if autoLevels: img = self.image while img.size > 2**16: img = img[::2, ...] mn, mx = img[:, 2].min(), img[:, 2].max() if mn == mx: mn = 0 mx = 255 kargs['levels'] = [mn, mx] profile() self.setOpts(update=False, **kargs) profile() self.qimage = None self.update() profile() if gotNewData: self.sigImageChanged.emit()
def setData(self, *args, **kargs): """ Clear any data displayed by this item and display new data. See :func:`__init__() <pyqtgraph.PlotDataItem.__init__>` for details; it accepts the same arguments. """ #self.clear() prof = debug.Profiler('PlotDataItem.setData (0x%x)' % id(self), disabled=True) y = None x = None if len(args) == 1: data = args[0] dt = dataType(data) if dt == 'empty': pass elif dt == 'listOfValues': y = np.array(data) elif dt == 'Nx2array': x = data[:,0] y = data[:,1] elif dt == 'recarray' or dt == 'dictOfLists': if 'x' in data: x = np.array(data['x']) if 'y' in data: y = np.array(data['y']) elif dt == 'listOfDicts': if 'x' in data[0]: x = np.array([d.get('x',None) for d in data]) if 'y' in data[0]: y = np.array([d.get('y',None) for d in data]) for k in ['data', 'symbolSize', 'symbolPen', 'symbolBrush', 'symbolShape']: if k in data: kargs[k] = [d.get(k, None) for d in data] elif dt == 'MetaArray': y = data.view(np.ndarray) x = data.xvals(0).view(np.ndarray) else: raise Exception('Invalid data type %s' % type(data)) elif len(args) == 2: seq = ('listOfValues', 'MetaArray', 'empty') if dataType(args[0]) not in seq or dataType(args[1]) not in seq: raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) if not isinstance(args[0], np.ndarray): x = np.array(args[0]) else: x = args[0].view(np.ndarray) if not isinstance(args[1], np.ndarray): y = np.array(args[1]) else: y = args[1].view(np.ndarray) if 'x' in kargs: x = kargs['x'] if 'y' in kargs: y = kargs['y'] prof.mark('interpret data') ## 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' if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): kargs['symbol'] = 'o' if 'brush' in kargs: kargs['fillBrush'] = kargs['brush'] for k in list(self.opts.keys()): if k in kargs: self.opts[k] = kargs[k] #curveArgs = {} #for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: #if k in kargs: #self.opts[k] = kargs[k] #curveArgs[k] = self.opts[k] #scatterArgs = {} #for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: #if k in kargs: #self.opts[k] = kargs[k] #scatterArgs[v] = self.opts[k] if y is None: return if y is not None and x is None: x = np.arange(len(y)) if isinstance(x, list): x = np.array(x) if isinstance(y, list): y = np.array(y) self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by self.yData = y.view(np.ndarray) self.xClean = self.yClean = None self.xDisp = None self.yDisp = None prof.mark('set data') self.updateItems() prof.mark('update items') self.informViewBoundsChanged() #view = self.getViewBox() #if view is not None: #view.itemBoundsChanged(self) ## inform view so it can update its range if it wants self.sigPlotChanged.emit(self) prof.mark('emit') prof.finish()
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): """ Set the image to be displayed in the widget. ============== ======================================================================= **Arguments:** *img* (numpy array) the image to be displayed. *xvals* (numpy array) 1D array of z-axis values corresponding to the third axis in a 3D image. For video, this array should contain the time of each frame. *autoRange* (bool) whether to scale/pan the view to fit the image. *autoLevels* (bool) whether to update the white/black levels to fit the image. *levels* (min, max); the white and black level values to use. *axes* Dictionary indicating the interpretation for each axis. This is only needed to override the default guess. Format is:: {'t':0, 'x':1, 'y':2, 'c':3}; ============== ======================================================================= """ prof = debug.Profiler('ImageView.setImage', disabled=True) if not isinstance(img, np.ndarray): raise Exception("Image must be specified as ndarray.") self.image = img if xvals is not None: self.tVals = xvals elif hasattr(img, 'xvals'): try: self.tVals = img.xvals(0) except: self.tVals = np.arange(img.shape[0]) else: self.tVals = np.arange(img.shape[0]) #self.ui.timeSlider.setValue(0) #self.ui.normStartSlider.setValue(0) #self.ui.timeSlider.setMaximum(img.shape[0]-1) prof.mark('1') if axes is None: if img.ndim == 2: self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} elif img.ndim == 3: if img.shape[2] <= 4: self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} else: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} elif img.ndim == 4: self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} else: raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): self.axes = axes.copy() elif isinstance(axes, list) or isinstance(axes, tuple): self.axes = {} for i in range(len(axes)): self.axes[axes[i]] = i else: raise Exception( "Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) prof.mark('2') self.imageDisp = None prof.mark('3') self.currentIndex = 0 self.updateImage() if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. self.levelMax = levels[1] self.levelMin = levels[0] if self.ui.roiBtn.isChecked(): self.roiChanged() prof.mark('4') if self.axes['t'] is not None: #self.ui.roiPlot.show() self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) self.timeLine.setValue(0) #self.ui.roiPlot.setMouseEnabled(False, False) if len(self.tVals) > 1: start = self.tVals.min() stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 elif len(self.tVals) == 1: start = self.tVals[0] - 0.5 stop = self.tVals[0] + 0.5 else: start = 0 stop = 1 for s in [self.timeLine, self.normRgn]: s.setBounds([start, stop]) #else: #self.ui.roiPlot.hide() prof.mark('5') self.imageItem.resetTransform() if scale is not None: self.imageItem.scale(*scale) if pos is not None: self.imageItem.setPos(*pos) prof.mark('6') if autoRange: self.autoRange() self.roiClicked() prof.mark('7') prof.finish()
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() # bounds = self.boundingRect() 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 # print tickStart, tickStop, span ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: return lengthInPixels = 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 = 255 / (i + 1) if self.grid is not False: lineAlpha *= self.grid / 255. * np.clip( (0.05 * lengthInPixels / (len(ticks) + 1)), 0., 1.) 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, Point(p1), Point(p2))) profiler('compute ticks') 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'][ 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 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(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() * 1.4) 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 # 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 vstr = str(vstr) x = tickPositions[i][j] # textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) textRect = rects[j] height = textRect.height() width = textRect.width() # self.textHeight = height offset = max(0, self.style['tickLength']) + textOffset if self.orientation == 'left': 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.TextDontClip | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter rect = QtCore.QRectF(tickStop + offset, x - (height / 2), width, height) elif self.orientation == 'top': 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.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) textSpecs.append((rect, textFlags, vstr)) profiler('compute text') ## update max text size if needed. self._updateMaxTextSize(textSize2) return (axisSpec, tickSpecs, textSpecs)
def paint(self, p, opt, widget): prof = debug.Profiler('PlotCurveItem.paint ' + str(id(self)), disabled=True) if self.xData is None: return #if self.opts['spectrumMode']: #if self.specPath is None: #self.specPath = self.generatePath(*self.getData()) #path = self.specPath #else: x = None y = None if self.path is None: x, y = self.getData() if x is None or len(x) == 0 or y is None or len(y) == 0: return self.path = self.generatePath(x, y) self.fillPath = None path = self.path prof.mark('generate path') if self.opts['brush'] is not None and self.opts[ 'fillLevel'] is not None: if self.fillPath is None: if x is None: x, y = self.getData() p2 = QtGui.QPainterPath(self.path) p2.lineTo(x[-1], self.opts['fillLevel']) p2.lineTo(x[0], self.opts['fillLevel']) p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 prof.mark('generate fill path') p.fillPath(self.fillPath, self.opts['brush']) prof.mark('draw fill path') ## Copy pens and apply alpha adjustment sp = QtGui.QPen(self.opts['shadowPen']) cp = QtGui.QPen(self.opts['pen']) #for pen in [sp, cp]: #if pen is None: #continue #c = pen.color() #c.setAlpha(c.alpha() * self.opts['alphaHint']) #pen.setColor(c) ##pen.setCosmetic(True) if self.exportOpts is not False: aa = self.exportOpts['antialias'] else: aa = self.antialias p.setRenderHint(p.Antialiasing, aa) if sp is not None: p.setPen(sp) p.drawPath(path) p.setPen(cp) p.drawPath(path) prof.mark('drawPath') #print "Render hints:", int(p.renderHints()) prof.finish()
def _generateItemSvg(item, nodes=None, root=None): ## This function is intended to work around some issues with Qt's SVG generator ## and SVG in general. ## 1) Qt SVG does not implement clipping paths. This is absurd. ## The solution is to let Qt generate SVG for each item independently, ## then glue them together manually with clipping. ## ## The format Qt generates for all items looks like this: ## ## <g> ## <g transform="matrix(...)"> ## one or more of: <path/> or <polyline/> or <text/> ## </g> ## <g transform="matrix(...)"> ## one or more of: <path/> or <polyline/> or <text/> ## </g> ## . . . ## </g> ## ## 2) There seems to be wide disagreement over whether path strokes ## should be scaled anisotropically. ## see: http://web.mit.edu/jonas/www/anisotropy/ ## Given that both inkscape and illustrator seem to prefer isotropic ## scaling, we will optimize for those cases. ## ## 3) Qt generates paths using non-scaling-stroke from SVG 1.2, but ## inkscape only supports 1.1. ## ## Both 2 and 3 can be addressed by drawing all items in world coordinates. profiler = debug.Profiler() if nodes is None: ## nodes maps all node IDs to their XML element. ## this allows us to ensure all elements receive unique names. nodes = {} if root is None: root = item ## Skip hidden items if hasattr(item, 'isVisible') and not item.isVisible(): return None ## If this item defines its own SVG generator, use that. if hasattr(item, 'generateSvg'): return item.generateSvg(nodes) ## Generate SVG text for just this item (exclude its children; we'll handle them later) tr = QtGui.QTransform() if isinstance(item, QtGui.QGraphicsScene): xmlStr = "<g>\n</g>\n" doc = xml.parseString(xmlStr) childs = [i for i in item.items() if i.parentItem() is None] elif item.__class__.paint == QtGui.QGraphicsItem.paint: xmlStr = "<g>\n</g>\n" doc = xml.parseString(xmlStr) childs = item.childItems() else: childs = item.childItems() tr = itemTransform(item, item.scene()) ## offset to corner of root item if isinstance(root, QtGui.QGraphicsScene): rootPos = QtCore.QPoint(0, 0) else: rootPos = root.scenePos() tr2 = QtGui.QTransform() tr2.translate(-rootPos.x(), -rootPos.y()) tr = tr * tr2 arr = QtCore.QByteArray() buf = QtCore.QBuffer(arr) svg = QtSvg.QSvgGenerator() svg.setOutputDevice(buf) dpi = QtGui.QDesktopWidget().logicalDpiX() svg.setResolution(dpi) p = QtGui.QPainter() p.begin(svg) if hasattr(item, 'setExportMode'): item.setExportMode(True, {'painter': p}) try: p.setTransform(tr) item.paint(p, QtGui.QStyleOptionGraphicsItem(), None) finally: p.end() ## Can't do this here--we need to wait until all children have painted as well. ## this is taken care of in generateSvg instead. #if hasattr(item, 'setExportMode'): #item.setExportMode(False) if USE_PYSIDE: xmlStr = str(arr) else: xmlStr = bytes(arr).decode('utf-8') doc = xml.parseString(xmlStr.encode('utf-8')) try: ## Get top-level group for this item g1 = doc.getElementsByTagName('g')[0] ## get list of sub-groups g2 = [ n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g' ] defs = doc.getElementsByTagName('defs') if len(defs) > 0: defs = [ n for n in defs[0].childNodes if isinstance(n, xml.Element) ] except: print(doc.toxml()) raise profiler('render') ## Get rid of group transformation matrices by applying ## transformation to inner coordinates correctCoordinates(g1, defs, item) profiler('correct') ## make sure g1 has the transformation matrix #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) #print "=================",item,"=====================" #print g1.toprettyxml(indent=" ", newl='') ## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1) ## So we need to correct anything attempting to use this. #correctStroke(g1, item, root) ## decide on a name for this item baseName = item.__class__.__name__ i = 1 while True: name = baseName + "_%d" % i if name not in nodes: break i += 1 nodes[name] = g1 g1.setAttribute('id', name) ## If this item clips its children, we need to take care of that. childGroup = g1 ## add children directly to this node unless we are clipping if not isinstance(item, QtGui.QGraphicsScene): ## See if this item clips its children if int(item.flags() & item.ItemClipsChildrenToShape) > 0: ## Generate svg for just the path #if isinstance(root, QtGui.QGraphicsScene): #path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) #else: #path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape()))) path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) item.scene().addItem(path) try: #pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] pathNode = _generateItemSvg( path, root=root)[0].getElementsByTagName('path')[0] # assume <defs> for this path is empty.. possibly problematic. finally: item.scene().removeItem(path) ## and for the clipPath element clip = name + '_clip' clipNode = g1.ownerDocument.createElement('clipPath') clipNode.setAttribute('id', clip) clipNode.appendChild(pathNode) g1.appendChild(clipNode) childGroup = g1.ownerDocument.createElement('g') childGroup.setAttribute('clip-path', 'url(#%s)' % clip) g1.appendChild(childGroup) profiler('clipping') ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: csvg = _generateItemSvg(ch, nodes, root) if csvg is None: continue cg, cdefs = csvg childGroup.appendChild( cg ) ### this isn't quite right--some items draw below their parent (good enough for now) defs.extend(cdefs) profiler('children') return g1, defs