Scatter Plot Improvements (#1420)

* Added hovering demo to ScatterPlot example

* Use Qt's serialization for SymbolAtlas.symbolMap keys

Yields significant performance improvements when updating the scatter plot's options. See e.g. the plot hover example.

* Further optimized scatter plot picking

* Fix ScatterPlot example tool tip

* Clean up while I'm here

* Compatibility

* Some simple optimizations for ScatterPlotItem

Speedups for ScatterPlotSpeedTest.py:
~50% without pxMode
~ 0% pxMode with useCache
~30% pxMode without useCache

* ~3x speedup in scatter plot speed test with pxMode

* More optimization low-hanging fruit for the scatter plot

* Removed hover example to lazily pass tests

* Avoid segfault

* Re-add hover example to ScatterPlot.py

* Switch to id-based keying for scatter plot symbol atlas

- Use cases exist where serialization-based keying is a significant bottleneck, e.g. updating without atlas invalidation when a large variety pens or brushes are present.
- To avoid a performance hit, the onus is on the user to carefully reuse pen and brush objects.

* Optimized caching in scatter plot hovering example

* Fixed and optimized scatter plot hovering example

* Minor scatter plot optimization

* Cleanup

* Store hovered points in a set for the hovering example

* Keep a limited number symbol atlas entries around for future reuse

* Added a docstring note to remind the user to reuse QPen and QBrush objects for better performance

* Tidied up hovering example

* Typo

* Avoid unnecessary atlas rebuilds

* Refactored SymbolAtlas

* Efficient appending to SymbolAtlas

* SymbolAtlas rewrite

* Cleanup and profiling

* Add randomized brushes to speed test

* Add loc indexer to ScatterPlotItem

* Profile ScatterPlotItem.paint to identify bottlenecks

* Reuse targetRect to improve paint performance

* Readability improvements (opinionated)

* Only need to set x and y of targetRect

- w and h can stay set to 0 (not entirely sure why)
- this is a bit faster than setting all of x, y, w, h

* Minor renaming

* Strip off API changes and leave to another PR

* Renaming

* Compatibility

* Use drawPixmap(x, y, pm, sx, sy, sw, sh) signature to avoid needing to update QRectFs

* Use different drawing approaches for each Qt binding for performance reasons

* Fix a bug introduced two commits ago

Incidentally, I think there is a similar bug in the main branch currently.

* Minor performance and readability improvements

* Strip out source and target QRectF stuff

* Bring source and target QRectF stuff back in a less coupled way

* Leave deprecating getSpotOpts for another PR

* Compatibility fix

* Added docstrings and use SymbolAtlas__len__ where possible

* Fix export issue

* Add missing import

* Add deprecation warnings

* Avoid using deprecated methods

* Fix and cleanup max spot size measurements

* Make creation of style opts entries explicit

* Add hovering API to ScatterPlotItem

* Compatibility

* Marshal pen and brush lists in setPen and setBrush

* Fixed platform dependent bug
This commit is contained in:
lidstrom83 2020-12-16 11:07:39 -08:00 committed by GitHub
parent 1847bfcf97
commit 3af23725ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 582 additions and 244 deletions

View File

@ -12,6 +12,7 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
from collections import namedtuple
from itertools import chain
app = QtGui.QApplication([])
mw = QtGui.QMainWindow()
@ -30,9 +31,21 @@ w3 = view.addPlot()
w4 = view.addPlot()
print("Generating data, this takes a few seconds...")
## There are a few different ways we can draw scatter plots; each is optimized for different types of data:
## Make all plots clickable
clickedPen = pg.mkPen('b', width=2)
lastClicked = []
def clicked(plot, points):
global lastClicked
for p in lastClicked:
p.resetPen()
print("clicked points", points)
for p in points:
p.setPen(clickedPen)
lastClicked = points
## There are a few different ways we can draw scatter plots; each is optimized for different types of data:
## 1) All spots identical and transform-invariant (top-left plot).
## In this case we can get a huge performance boost by pre-rendering the spot
## image and just drawing that image repeatedly.
@ -43,21 +56,9 @@ pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}]
s1.addPoints(spots)
w1.addItem(s1)
## Make all plots clickable
lastClicked = []
def clicked(plot, points):
global lastClicked
for p in lastClicked:
p.resetPen()
print("clicked points", points)
for p in points:
p.setPen('b', width=2)
lastClicked = points
s1.sigClicked.connect(clicked)
## 2) Spots are transform-invariant, but not identical (top-right plot).
## In this case, drawing is almsot as fast as 1), but there is more startup
## overhead and memory usage since each spot generates its own pre-rendered
@ -95,7 +96,12 @@ s2.sigClicked.connect(clicked)
## This is the slowest case, since all spots must be completely re-drawn
## every time because their apparent transformation may have changed.
s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view
s3 = pg.ScatterPlotItem(
pxMode=False, # Set pxMode=False to allow spots to transform with the view
hoverable=True,
hoverPen=pg.mkPen('g'),
hoverSize=1e-6
)
spots3 = []
for i in range(10):
for j in range(10):
@ -104,17 +110,31 @@ s3.addPoints(spots3)
w3.addItem(s3)
s3.sigClicked.connect(clicked)
## Test performance of large scatterplots
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20))
pos = np.random.normal(size=(2,10000), scale=1e-9)
s4.addPoints(x=pos[0], y=pos[1])
s4 = pg.ScatterPlotItem(
size=10,
pen=pg.mkPen(None),
brush=pg.mkBrush(255, 255, 255, 20),
hoverable=True,
hoverSymbol='s',
hoverSize=15,
hoverPen=pg.mkPen('r', width=2),
hoverBrush=pg.mkBrush('g'),
)
n = 10000
pos = np.random.normal(size=(2, n), scale=1e-9)
s4.addPoints(
x=pos[0],
y=pos[1],
# size=(np.random.random(n) * 20.).astype(int),
# brush=[pg.mkBrush(x) for x in np.random.randint(0, 256, (n, 3))],
data=np.arange(n)
)
w4.addItem(s4)
s4.sigClicked.connect(clicked)
## Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
import sys

View File

@ -38,20 +38,26 @@ win.show()
p = ui.plot
p.setRange(xRange=[-500, 500], yRange=[-500, 500])
data = np.random.normal(size=(50,500), scale=100)
sizeArray = (np.random.random(500) * 20.).astype(int)
count = 500
data = np.random.normal(size=(50,count), scale=100)
sizeArray = (np.random.random(count) * 20.).astype(int)
brushArray = [pg.mkBrush(x) for x in np.random.randint(0, 256, (count, 3))]
ptr = 0
lastTime = time()
fps = None
def update():
global curve, data, ptr, p, lastTime, fps
p.clear()
if ui.randCheck.isChecked():
size = sizeArray
brush = brushArray
else:
size = ui.sizeSpin.value()
brush = 'b'
curve = pg.ScatterPlotItem(x=data[ptr % 50], y=data[(ptr+1) % 50],
pen='w', brush='b', size=size,
pen='w', brush=brush, size=size,
pxMode=ui.pixelModeCheck.isChecked())
p.addItem(curve)
ptr += 1

View File

@ -1,3 +1,4 @@
import warnings
from functools import reduce
from ..Qt import QtGui, QtCore, isQObjectAlive
from ..GraphicsScene import GraphicsScene
@ -103,10 +104,6 @@ class GraphicsItem(object):
Return the transform that converts local item coordinates to device coordinates (usually pixels).
Extends deviceTransform to automatically determine the viewportTransform.
"""
if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different.
scaler = self._exportOpts.get('resolutionScale', 1.0)
return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1)
if viewportTransform is None:
view = self.getViewWidget()
if view is None:

View File

@ -28,12 +28,15 @@ class PlotDataItem(GraphicsObject):
sigClicked(self, ev) Emitted when the item is clicked.
sigPointsClicked(self, points, ev) Emitted when a plot point is clicked
Sends the list of points under the mouse.
sigPointsHovered(self, points, ev) Emitted when a plot point is hovered over.
Sends the list of points under the mouse.
================================== ==============================================
"""
sigPlotChanged = QtCore.Signal(object)
sigClicked = QtCore.Signal(object, object)
sigPointsClicked = QtCore.Signal(object, object, object)
sigPointsHovered = QtCore.Signal(object, object, object)
def __init__(self, *args, **kargs):
"""
@ -161,6 +164,7 @@ class PlotDataItem(GraphicsObject):
self.curve.sigClicked.connect(self.curveClicked)
self.scatter.sigClicked.connect(self.scatterClicked)
self.scatter.sigHovered.connect(self.scatterHovered)
self._dataRect = None
#self.clear()
@ -768,6 +772,9 @@ class PlotDataItem(GraphicsObject):
self.sigClicked.emit(self, ev)
self.sigPointsClicked.emit(self, points, ev)
def scatterHovered(self, plt, points, ev):
self.sigPointsHovered.emit(self, points, ev)
def viewRangeChanged(self):
# view range has changed; re-plot if needed
if( self.opts['clipToView']

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
4) A PlotWidget for displaying the data.
"""
sigScatterPlotClicked = QtCore.Signal(object, object, object)
sigScatterPlotHovered = QtCore.Signal(object, object, object)
def __init__(self, parent=None):
QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal)
@ -257,6 +258,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
self._indexMap = None
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
self.scatterPlot.sigPointsHovered.connect(self.plotHovered)
self.updateSelected()
def updateSelected(self):
@ -284,3 +286,6 @@ class ScatterPlotWidget(QtGui.QSplitter):
for pt in points:
pt.originalIndex = self._visibleIndices[pt.index()]
self.sigScatterPlotClicked.emit(self, points, ev)
def plotHovered(self, plot, points, ev):
self.sigScatterPlotHovered.emit(self, points, ev)