From 380ec2e0b2fc3ca3d7432e23c9ff5eed1b5eb89d Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 11 Jun 2021 09:45:59 +0800 Subject: [PATCH 1/8] implement usage of QPainter.drawPixmapFragments --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 44 +++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 7ae0e372..54e3d6c5 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -19,6 +19,12 @@ from collections import OrderedDict from .. import debug from ..python2_3 import basestring +if QT_LIB == 'PySide2': + from shiboken2 import wrapInstance +elif QT_LIB == 'PySide6': + from shiboken6 import wrapInstance +elif QT_LIB in ['PyQt5', 'PyQt6']: + from ..Qt import sip __all__ = ['ScatterPlotItem', 'SpotItem'] @@ -36,7 +42,8 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] # as the separate calls to this method are the current bottleneck. # See: https://bugreports.qt.io/browse/PYSIDE-163 -_USE_QRECT = QT_LIB not in ['PySide2', 'PySide6'] +_USE_QRECT = False +_USE_PXFRAGS = True ## Build all symbol paths name_list = ['o', 's', 't', 't1', 't2', 't3', 'd', '+', 'x', 'p', 'h', 'star', @@ -1099,13 +1106,37 @@ class ScatterPlotItem(GraphicsObject): p.resetTransform() if self.opts['useCache'] and self._exportOpts is False: - # Map pts to (x, y) coordinates of targetRect - pts -= self.data['sourceRect']['w'] / 2 - # Draw symbols from pre-rendered atlas pm = self.fragmentAtlas.pixmap - if _USE_QRECT: + if _USE_PXFRAGS: + # x, y is the center of the target rect + # drawPixmapFragments takes floating-point coords, + # so casting to int here is for rounding towards zero + xy = pts[:, viewMask].T.astype(int) + sr = self.data['sourceRect'][viewMask] + + frags = np.empty((sr.size, 10), dtype=np.float64) + frags[:, 0:2] = xy + frags[:, 2:6] = np.frombuffer(sr, dtype=int).reshape((-1, 4)) # sx, sy, sw, sh + frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity + + if QT_LIB.startswith('PyQt'): + frags_lst = list(map(sip.wrapinstance, + itertools.count(frags.ctypes.data, frags.strides[0]), + itertools.repeat(QtGui.QPainter.PixmapFragment, frags.shape[0]))) + draw_args = frags_lst, pm + else: + frags_ptr = wrapInstance(frags.ctypes.data, QtGui.QPainter.PixmapFragment) + draw_args = frags_ptr, frags.shape[0], pm + + profiler('prep') + p.drawPixmapFragments(*draw_args) + profiler('draw') + elif _USE_QRECT: + # Map pts to (x, y) coordinates of targetRect + pts -= self.data['sourceRect']['w'] / 2 + # Update targetRects if necessary updateMask = viewMask & (~self.data['targetQRectValid']) if np.any(updateMask): @@ -1129,6 +1160,9 @@ class ScatterPlotItem(GraphicsObject): self.data['sourceQRect'][viewMask].tolist())) profiler('draw') else: + # Map pts to (x, y) coordinates of targetRect + pts -= self.data['sourceRect']['w'] / 2 + x, y = pts[:, viewMask].astype(int) sr = self.data['sourceRect'][viewMask] From ad3e3a6b8bd4458cb805dda3e6965be34d8229a3 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 11 Jun 2021 09:46:37 +0800 Subject: [PATCH 2/8] ScatterPlotSpeedTest: add toggle for drawPixmapFragments --- examples/ScatterPlotSpeedTest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index 1140a603..bcda7148 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -24,6 +24,7 @@ param = ptree.Parameter.create(name=translate('ScatterPlot', 'Parameters'), type dict(name='count', title=translate('ScatterPlot', 'Count: '), type='int', limits=[1, None], value=500, step=100), dict(name='size', title=translate('ScatterPlot', 'Size: '), type='int', limits=[1, None], value=10), dict(name='randomize', title=translate('ScatterPlot', 'Randomize: '), type='bool', value=False), + dict(name='_USE_PXFRAGS', title='_USE_PXFRAGS: ', type='bool', value=pyqtgraph.graphicsItems.ScatterPlotItem._USE_PXFRAGS), dict(name='_USE_QRECT', title='_USE_QRECT: ', type='bool', value=pyqtgraph.graphicsItems.ScatterPlotItem._USE_QRECT), dict(name='pxMode', title='pxMode: ', type='bool', value=True), dict(name='useCache', title='useCache: ', type='bool', value=True), @@ -68,6 +69,7 @@ def mkDataAndItem(): def mkItem(): global item + pyqtgraph.graphicsItems.ScatterPlotItem._USE_PXFRAGS = param['_USE_PXFRAGS'] pyqtgraph.graphicsItems.ScatterPlotItem._USE_QRECT = param['_USE_QRECT'] item = pg.ScatterPlotItem(pxMode=param['pxMode'], **getData()) item.opts['useCache'] = param['useCache'] @@ -122,7 +124,7 @@ def update(): mkDataAndItem() for name in ['count', 'size']: param.child(name).sigValueChanged.connect(mkDataAndItem) -for name in ['_USE_QRECT', 'useCache', 'pxMode', 'randomize']: +for name in ['_USE_PXFRAGS', '_USE_QRECT', 'useCache', 'pxMode', 'randomize']: param.child(name).sigValueChanged.connect(mkItem) param.child('paused').sigValueChanged.connect(lambda _, v: timer.stop() if v else timer.start()) timer.timeout.connect(update) From 6839ec937acd5d28f1829e21077fae3db8e674b3 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 14 Jun 2021 06:49:52 +0800 Subject: [PATCH 3/8] reuse pixmap fragment objects --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 39 ++++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 54e3d6c5..943292f6 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -171,6 +171,29 @@ def _mkBrush(*args, **kwargs): return fn.mkBrush(*args, **kwargs) +class PixmapFragments: + def __init__(self): + self._array = np.empty((0, 10), dtype=np.float64) + self.ptrs = None + + def array(self, size): + if size > self._array.shape[0]: + self._array = np.empty((size + 16, 10), dtype=np.float64) + if QT_LIB.startswith('PyQt'): + self.ptrs = list(map(sip.wrapinstance, + itertools.count(self._array.ctypes.data, self._array.strides[0]), + itertools.repeat(QtGui.QPainter.PixmapFragment, self._array.shape[0]))) + else: + self.ptrs = wrapInstance(self._array.ctypes.data, QtGui.QPainter.PixmapFragment) + return self._array[:size] + + def draw(self, painter, size, pixmap): + if QT_LIB.startswith('PyQt'): + painter.drawPixmapFragments(self.ptrs[:size], pixmap) + else: + painter.drawPixmapFragments(self.ptrs, size, pixmap) + + class SymbolAtlas(object): """ Used to efficiently construct a single QPixmap containing all rendered symbols @@ -1116,22 +1139,16 @@ class ScatterPlotItem(GraphicsObject): xy = pts[:, viewMask].T.astype(int) sr = self.data['sourceRect'][viewMask] - frags = np.empty((sr.size, 10), dtype=np.float64) + if not hasattr(self, 'pixmap_fragments'): + self.pixmap_fragments = PixmapFragments() + + frags = self.pixmap_fragments.array(sr.size) frags[:, 0:2] = xy frags[:, 2:6] = np.frombuffer(sr, dtype=int).reshape((-1, 4)) # sx, sy, sw, sh frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity - if QT_LIB.startswith('PyQt'): - frags_lst = list(map(sip.wrapinstance, - itertools.count(frags.ctypes.data, frags.strides[0]), - itertools.repeat(QtGui.QPainter.PixmapFragment, frags.shape[0]))) - draw_args = frags_lst, pm - else: - frags_ptr = wrapInstance(frags.ctypes.data, QtGui.QPainter.PixmapFragment) - draw_args = frags_ptr, frags.shape[0], pm - profiler('prep') - p.drawPixmapFragments(*draw_args) + self.pixmap_fragments.draw(p, sr.size, pm) profiler('draw') elif _USE_QRECT: # Map pts to (x, y) coordinates of targetRect From a1845cddbcec5994ce7fc2b314fd40aa7e1a99c4 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Mon, 14 Jun 2021 19:24:39 +0800 Subject: [PATCH 4/8] don't truncate floating point target coords --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 943292f6..99f1ec76 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1134,9 +1134,7 @@ class ScatterPlotItem(GraphicsObject): if _USE_PXFRAGS: # x, y is the center of the target rect - # drawPixmapFragments takes floating-point coords, - # so casting to int here is for rounding towards zero - xy = pts[:, viewMask].T.astype(int) + xy = pts[:, viewMask].T sr = self.data['sourceRect'][viewMask] if not hasattr(self, 'pixmap_fragments'): From abeae0b7fa4fc1f169fe51a00730868832782020 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 15 Jun 2021 06:37:22 +0800 Subject: [PATCH 5/8] init so that pointers are present --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 99f1ec76..e40bc456 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -173,19 +173,21 @@ def _mkBrush(*args, **kwargs): class PixmapFragments: def __init__(self): - self._array = np.empty((0, 10), dtype=np.float64) - self.ptrs = None + self.alloc(0) + + def alloc(self, size): + self.arr = np.empty((size, 10), dtype=np.float64) + if QT_LIB.startswith('PyQt'): + self.ptrs = list(map(sip.wrapinstance, + itertools.count(self.arr.ctypes.data, self.arr.strides[0]), + itertools.repeat(QtGui.QPainter.PixmapFragment, self.arr.shape[0]))) + else: + self.ptrs = wrapInstance(self.arr.ctypes.data, QtGui.QPainter.PixmapFragment) def array(self, size): - if size > self._array.shape[0]: - self._array = np.empty((size + 16, 10), dtype=np.float64) - if QT_LIB.startswith('PyQt'): - self.ptrs = list(map(sip.wrapinstance, - itertools.count(self._array.ctypes.data, self._array.strides[0]), - itertools.repeat(QtGui.QPainter.PixmapFragment, self._array.shape[0]))) - else: - self.ptrs = wrapInstance(self._array.ctypes.data, QtGui.QPainter.PixmapFragment) - return self._array[:size] + if size > self.arr.shape[0]: + self.alloc(size + 16) + return self.arr[:size] def draw(self, painter, size, pixmap): if QT_LIB.startswith('PyQt'): From 89f6c7da812dd42f7337ff58ab3c345f3c70b329 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 15 Jun 2021 09:43:31 +0800 Subject: [PATCH 6/8] remove codepaths other than pixmap fragments codepath --- examples/ScatterPlotSpeedTest.py | 6 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 109 +++------------------ 2 files changed, 12 insertions(+), 103 deletions(-) diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index bcda7148..d076b20b 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -24,8 +24,6 @@ param = ptree.Parameter.create(name=translate('ScatterPlot', 'Parameters'), type dict(name='count', title=translate('ScatterPlot', 'Count: '), type='int', limits=[1, None], value=500, step=100), dict(name='size', title=translate('ScatterPlot', 'Size: '), type='int', limits=[1, None], value=10), dict(name='randomize', title=translate('ScatterPlot', 'Randomize: '), type='bool', value=False), - dict(name='_USE_PXFRAGS', title='_USE_PXFRAGS: ', type='bool', value=pyqtgraph.graphicsItems.ScatterPlotItem._USE_PXFRAGS), - dict(name='_USE_QRECT', title='_USE_QRECT: ', type='bool', value=pyqtgraph.graphicsItems.ScatterPlotItem._USE_QRECT), dict(name='pxMode', title='pxMode: ', type='bool', value=True), dict(name='useCache', title='useCache: ', type='bool', value=True), dict(name='mode', title=translate('ScatterPlot', 'Mode: '), type='list', values={translate('ScatterPlot', 'New Item'): 'newItem', translate('ScatterPlot', 'Reuse Item'): 'reuseItem', translate('ScatterPlot', 'Simulate Pan/Zoom'): 'panZoom', translate('ScatterPlot', 'Simulate Hover'): 'hover'}, value='reuseItem'), @@ -69,8 +67,6 @@ def mkDataAndItem(): def mkItem(): global item - pyqtgraph.graphicsItems.ScatterPlotItem._USE_PXFRAGS = param['_USE_PXFRAGS'] - pyqtgraph.graphicsItems.ScatterPlotItem._USE_QRECT = param['_USE_QRECT'] item = pg.ScatterPlotItem(pxMode=param['pxMode'], **getData()) item.opts['useCache'] = param['useCache'] p.clear() @@ -124,7 +120,7 @@ def update(): mkDataAndItem() for name in ['count', 'size']: param.child(name).sigValueChanged.connect(mkDataAndItem) -for name in ['_USE_PXFRAGS', '_USE_QRECT', 'useCache', 'pxMode', 'randomize']: +for name in ['useCache', 'pxMode', 'randomize']: param.child(name).sigValueChanged.connect(mkItem) param.child('paused').sigValueChanged.connect(lambda _, v: timer.stop() if v else timer.start()) timer.timeout.connect(update) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index e40bc456..0724dc9b 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -29,22 +29,6 @@ elif QT_LIB in ['PyQt5', 'PyQt6']: __all__ = ['ScatterPlotItem', 'SpotItem'] -# When pxMode=True for ScatterPlotItem, QPainter.drawPixmap is used for drawing, which -# has multiple type signatures. One takes int coordinates of source and target -# rectangles, and another takes QRectF objects. The latter approach has the overhead of -# updating these objects, which can be almost as much as drawing. -# For PyQt5, drawPixmap is significantly faster with QRectF coordinates for some -# reason, offsetting this overhead. For PySide2 this is not the case, and the QRectF -# maintenance overhead is an unnecessary burden. If this performance issue is solved -# by PyQt5, the QRectF coordinate approach can be removed by simply deleting all of the -# "if _USE_QRECT" code blocks in ScatterPlotItem. Ideally, drawPixmap would accept the -# numpy arrays of coordinates directly, which would improve performance significantly, -# as the separate calls to this method are the current bottleneck. -# See: https://bugreports.qt.io/browse/PYSIDE-163 - -_USE_QRECT = False -_USE_PXFRAGS = True - ## Build all symbol paths name_list = ['o', 's', 't', 't1', 't2', 't3', 'd', '+', 'x', 'p', 'h', 'star', 'arrow_up', 'arrow_right', 'arrow_down', 'arrow_left', 'crosshair'] @@ -434,18 +418,11 @@ class ScatterPlotItem(GraphicsObject): ]) ] - if _USE_QRECT: - dtype.extend([ - ('sourceQRect', object), - ('targetQRect', object), - ('targetQRectValid', bool) - ]) - self._sourceQRect = {} - self.data = np.empty(0, dtype=dtype) 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._pixmapFragments = PixmapFragments() self.opts = { 'pxMode': True, 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. @@ -583,10 +560,6 @@ class ScatterPlotItem(GraphicsObject): newData['size'] = -1 ## indicates to use default size newData['visible'] = True - if _USE_QRECT: - newData['targetQRect'] = [QtCore.QRectF() for _ in range(numPts)] - newData['targetQRectValid'] = False - if 'spots' in kargs: spots = kargs['spots'] for i in range(len(spots)): @@ -846,18 +819,6 @@ class ScatterPlotItem(GraphicsObject): list(zip(*self._style(['symbol', 'size', 'pen', 'brush'], data=dataSet, idx=mask))) ] dataSet['sourceRect'][mask] = coords - if _USE_QRECT: - rects = [] - for c in coords: - try: - rect = self._sourceQRect[c] - except KeyError: - rect = QtCore.QRectF(*c) - self._sourceQRect[c] = rect - rects.append(rect) - - dataSet['sourceQRect'][mask] = rects - dataSet['targetQRectValid'][mask] = False self._maybeRebuildAtlas() else: @@ -875,8 +836,6 @@ class ScatterPlotItem(GraphicsObject): list(zip(*self._style(['symbol', 'size', 'pen', 'brush']))) ) self.data['sourceRect'] = 0 - if _USE_QRECT: - self._sourceQRect.clear() self.updateSpots() def _style(self, opts, data=None, idx=None, scale=None): @@ -1059,8 +1018,6 @@ class ScatterPlotItem(GraphicsObject): self.prepareGeometryChange() GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] - if _USE_QRECT: - self.data['targetQRectValid'] = False def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) @@ -1132,63 +1089,19 @@ class ScatterPlotItem(GraphicsObject): if self.opts['useCache'] and self._exportOpts is False: # Draw symbols from pre-rendered atlas - pm = self.fragmentAtlas.pixmap - if _USE_PXFRAGS: - # x, y is the center of the target rect - xy = pts[:, viewMask].T - sr = self.data['sourceRect'][viewMask] + # x, y is the center of the target rect + xy = pts[:, viewMask].T + sr = self.data['sourceRect'][viewMask] - if not hasattr(self, 'pixmap_fragments'): - self.pixmap_fragments = PixmapFragments() - - frags = self.pixmap_fragments.array(sr.size) - frags[:, 0:2] = xy - frags[:, 2:6] = np.frombuffer(sr, dtype=int).reshape((-1, 4)) # sx, sy, sw, sh - frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity - - profiler('prep') - self.pixmap_fragments.draw(p, sr.size, pm) - profiler('draw') - elif _USE_QRECT: - # Map pts to (x, y) coordinates of targetRect - pts -= self.data['sourceRect']['w'] / 2 - - # Update targetRects if necessary - updateMask = viewMask & (~self.data['targetQRectValid']) - if np.any(updateMask): - x, y = pts[:, updateMask].tolist() - tr = self.data['targetQRect'][updateMask].tolist() - w = self.data['sourceRect']['w'][updateMask].tolist() - list(imap(QtCore.QRectF.setRect, tr, x, y, w, w)) - self.data['targetQRectValid'][updateMask] = True - - profiler('prep') - if QT_LIB == 'PyQt4': - p.drawPixmapFragments( - self.data['targetQRect'][viewMask].tolist(), - self.data['sourceQRect'][viewMask].tolist(), - pm - ) - else: - list(imap(p.drawPixmap, - self.data['targetQRect'][viewMask].tolist(), - repeat(pm), - self.data['sourceQRect'][viewMask].tolist())) - profiler('draw') - else: - # Map pts to (x, y) coordinates of targetRect - pts -= self.data['sourceRect']['w'] / 2 - - x, y = pts[:, viewMask].astype(int) - sr = self.data['sourceRect'][viewMask] - - profiler('prep') - list(imap(p.drawPixmap, - x.tolist(), y.tolist(), repeat(pm), - sr['x'].tolist(), sr['y'].tolist(), sr['w'].tolist(), sr['h'].tolist())) - profiler('draw') + frags = self._pixmapFragments.array(sr.size) + frags[:, 0:2] = xy + frags[:, 2:6] = np.frombuffer(sr, dtype=int).reshape((-1, 4)) # sx, sy, sw, sh + frags[:, 6:10] = [1.0, 1.0, 0.0, 1.0] # scaleX, scaleY, rotation, opacity + profiler('prep') + self._pixmapFragments.draw(p, len(frags), self.fragmentAtlas.pixmap) + profiler('draw') else: # render each symbol individually p.setRenderHint(p.RenderHint.Antialiasing, aa) From 7c6d9fe6d540eb5f6faf5c88b2bf4f21306c4f71 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 15 Jun 2021 11:06:37 +0800 Subject: [PATCH 7/8] cleanup python2 and unused imports --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 0724dc9b..e8ffd29d 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,10 +1,5 @@ # -*- coding: utf-8 -*- import warnings -from itertools import repeat, chain -try: - from itertools import imap -except ImportError: - imap = map import itertools import math import numpy as np @@ -17,7 +12,6 @@ from .GraphicsObject import GraphicsObject from .. import getConfigOption from collections import OrderedDict from .. import debug -from ..python2_3 import basestring if QT_LIB == 'PySide2': from shiboken2 import wrapInstance @@ -92,7 +86,7 @@ def drawSymbol(painter, symbol, size, pen, brush): painter.scale(size, size) painter.setPen(pen) painter.setBrush(brush) - if isinstance(symbol, basestring): + if isinstance(symbol, str): symbol = Symbols[symbol] if np.isscalar(symbol): symbol = list(Symbols.values())[symbol % len(Symbols)] @@ -215,7 +209,7 @@ class SymbolAtlas(object): if new: self._extend(new) - return list(imap(self._coords.__getitem__, keys)) + return list(map(self._coords.__getitem__, keys)) def __len__(self): return len(self._coords) @@ -658,7 +652,7 @@ class ScatterPlotItem(GraphicsObject): pens = pens[kargs['mask']] if len(pens) != len(dataSet): raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet))) - dataSet['pen'] = list(imap(_mkPen, pens)) + dataSet['pen'] = list(map(_mkPen, pens)) else: self.opts['pen'] = _mkPen(*args, **kargs) @@ -680,7 +674,7 @@ class ScatterPlotItem(GraphicsObject): brushes = brushes[kargs['mask']] if len(brushes) != len(dataSet): raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) - dataSet['brush'] = list(imap(_mkBrush, brushes)) + dataSet['brush'] = list(map(_mkBrush, brushes)) else: self.opts['brush'] = _mkBrush(*args, **kargs) @@ -866,7 +860,7 @@ class ScatterPlotItem(GraphicsObject): if self.opts['pxMode'] and self.opts['useCache']: w, pw = 0, self.fragmentAtlas.maxWidth else: - w, pw = max(chain([(self._maxSpotWidth, self._maxSpotPxWidth)], + w, pw = max(itertools.chain([(self._maxSpotWidth, self._maxSpotPxWidth)], self._measureSpotSizes(**kwargs))) self._maxSpotWidth = w self._maxSpotPxWidth = pw From f63d1e42066f4e9ffd1f8d5890a6ca64359491ca Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 15 Jun 2021 11:40:27 +0800 Subject: [PATCH 8/8] document PyQt boxing and unboxing --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index e8ffd29d..42c689d3 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -154,6 +154,17 @@ class PixmapFragments: self.alloc(0) def alloc(self, size): + # The C++ native API is: + # drawPixmapFragments(const PixmapFragment *fragments, int fragmentCount, + # const QPixmap &pixmap) + # + # PySide exposes this API whereas PyQt wraps it to be more Pythonic. + # In PyQt, a Python list of PixmapFragment instances needs to be provided. + # This is inefficient because: + # 1) constructing the Python list involves calling sip.wrapinstance multiple times. + # - this is mitigated here by reusing the instance pointers + # 2) PyQt will anyway deconstruct the Python list and repack the PixmapFragment + # instances into a contiguous array, in order to call the underlying C++ native API. self.arr = np.empty((size, 10), dtype=np.float64) if QT_LIB.startswith('PyQt'): self.ptrs = list(map(sip.wrapinstance,