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:
parent
1847bfcf97
commit
3af23725ca
@ -12,6 +12,7 @@ from pyqtgraph.Qt import QtGui, QtCore
|
|||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
app = QtGui.QApplication([])
|
app = QtGui.QApplication([])
|
||||||
mw = QtGui.QMainWindow()
|
mw = QtGui.QMainWindow()
|
||||||
@ -30,9 +31,21 @@ w3 = view.addPlot()
|
|||||||
w4 = view.addPlot()
|
w4 = view.addPlot()
|
||||||
print("Generating data, this takes a few seconds...")
|
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).
|
## 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
|
## In this case we can get a huge performance boost by pre-rendering the spot
|
||||||
## image and just drawing that image repeatedly.
|
## 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}]
|
spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}]
|
||||||
s1.addPoints(spots)
|
s1.addPoints(spots)
|
||||||
w1.addItem(s1)
|
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)
|
s1.sigClicked.connect(clicked)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 2) Spots are transform-invariant, but not identical (top-right plot).
|
## 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
|
## 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
|
## 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
|
## This is the slowest case, since all spots must be completely re-drawn
|
||||||
## every time because their apparent transformation may have changed.
|
## 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 = []
|
spots3 = []
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
for j in range(10):
|
for j in range(10):
|
||||||
@ -104,17 +110,31 @@ s3.addPoints(spots3)
|
|||||||
w3.addItem(s3)
|
w3.addItem(s3)
|
||||||
s3.sigClicked.connect(clicked)
|
s3.sigClicked.connect(clicked)
|
||||||
|
|
||||||
|
|
||||||
## Test performance of large scatterplots
|
## Test performance of large scatterplots
|
||||||
|
|
||||||
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20))
|
s4 = pg.ScatterPlotItem(
|
||||||
pos = np.random.normal(size=(2,10000), scale=1e-9)
|
size=10,
|
||||||
s4.addPoints(x=pos[0], y=pos[1])
|
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)
|
w4.addItem(s4)
|
||||||
s4.sigClicked.connect(clicked)
|
s4.sigClicked.connect(clicked)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Start Qt event loop unless running in interactive mode.
|
## Start Qt event loop unless running in interactive mode.
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
|
@ -38,20 +38,26 @@ win.show()
|
|||||||
p = ui.plot
|
p = ui.plot
|
||||||
p.setRange(xRange=[-500, 500], yRange=[-500, 500])
|
p.setRange(xRange=[-500, 500], yRange=[-500, 500])
|
||||||
|
|
||||||
data = np.random.normal(size=(50,500), scale=100)
|
count = 500
|
||||||
sizeArray = (np.random.random(500) * 20.).astype(int)
|
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
|
ptr = 0
|
||||||
lastTime = time()
|
lastTime = time()
|
||||||
fps = None
|
fps = None
|
||||||
|
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
global curve, data, ptr, p, lastTime, fps
|
global curve, data, ptr, p, lastTime, fps
|
||||||
p.clear()
|
p.clear()
|
||||||
if ui.randCheck.isChecked():
|
if ui.randCheck.isChecked():
|
||||||
size = sizeArray
|
size = sizeArray
|
||||||
|
brush = brushArray
|
||||||
else:
|
else:
|
||||||
size = ui.sizeSpin.value()
|
size = ui.sizeSpin.value()
|
||||||
curve = pg.ScatterPlotItem(x=data[ptr%50], y=data[(ptr+1)%50],
|
brush = 'b'
|
||||||
pen='w', brush='b', size=size,
|
curve = pg.ScatterPlotItem(x=data[ptr % 50], y=data[(ptr+1) % 50],
|
||||||
|
pen='w', brush=brush, size=size,
|
||||||
pxMode=ui.pixelModeCheck.isChecked())
|
pxMode=ui.pixelModeCheck.isChecked())
|
||||||
p.addItem(curve)
|
p.addItem(curve)
|
||||||
ptr += 1
|
ptr += 1
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import warnings
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from ..Qt import QtGui, QtCore, isQObjectAlive
|
from ..Qt import QtGui, QtCore, isQObjectAlive
|
||||||
from ..GraphicsScene import GraphicsScene
|
from ..GraphicsScene import GraphicsScene
|
||||||
@ -103,10 +104,6 @@ class GraphicsItem(object):
|
|||||||
Return the transform that converts local item coordinates to device coordinates (usually pixels).
|
Return the transform that converts local item coordinates to device coordinates (usually pixels).
|
||||||
Extends deviceTransform to automatically determine the viewportTransform.
|
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:
|
if viewportTransform is None:
|
||||||
view = self.getViewWidget()
|
view = self.getViewWidget()
|
||||||
if view is None:
|
if view is None:
|
||||||
|
@ -28,12 +28,15 @@ class PlotDataItem(GraphicsObject):
|
|||||||
sigClicked(self, ev) Emitted when the item is clicked.
|
sigClicked(self, ev) Emitted when the item is clicked.
|
||||||
sigPointsClicked(self, points, ev) Emitted when a plot point is clicked
|
sigPointsClicked(self, points, ev) Emitted when a plot point is clicked
|
||||||
Sends the list of points under the mouse.
|
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)
|
sigPlotChanged = QtCore.Signal(object)
|
||||||
sigClicked = QtCore.Signal(object, object)
|
sigClicked = QtCore.Signal(object, object)
|
||||||
sigPointsClicked = QtCore.Signal(object, object, object)
|
sigPointsClicked = QtCore.Signal(object, object, object)
|
||||||
|
sigPointsHovered = QtCore.Signal(object, object, object)
|
||||||
|
|
||||||
def __init__(self, *args, **kargs):
|
def __init__(self, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
@ -161,6 +164,7 @@ class PlotDataItem(GraphicsObject):
|
|||||||
|
|
||||||
self.curve.sigClicked.connect(self.curveClicked)
|
self.curve.sigClicked.connect(self.curveClicked)
|
||||||
self.scatter.sigClicked.connect(self.scatterClicked)
|
self.scatter.sigClicked.connect(self.scatterClicked)
|
||||||
|
self.scatter.sigHovered.connect(self.scatterHovered)
|
||||||
|
|
||||||
self._dataRect = None
|
self._dataRect = None
|
||||||
#self.clear()
|
#self.clear()
|
||||||
@ -768,6 +772,9 @@ class PlotDataItem(GraphicsObject):
|
|||||||
self.sigClicked.emit(self, ev)
|
self.sigClicked.emit(self, ev)
|
||||||
self.sigPointsClicked.emit(self, points, ev)
|
self.sigPointsClicked.emit(self, points, ev)
|
||||||
|
|
||||||
|
def scatterHovered(self, plt, points, ev):
|
||||||
|
self.sigPointsHovered.emit(self, points, ev)
|
||||||
|
|
||||||
def viewRangeChanged(self):
|
def viewRangeChanged(self):
|
||||||
# view range has changed; re-plot if needed
|
# view range has changed; re-plot if needed
|
||||||
if( self.opts['clipToView']
|
if( self.opts['clipToView']
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,8 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
4) A PlotWidget for displaying the data.
|
4) A PlotWidget for displaying the data.
|
||||||
"""
|
"""
|
||||||
sigScatterPlotClicked = QtCore.Signal(object, object, object)
|
sigScatterPlotClicked = QtCore.Signal(object, object, object)
|
||||||
|
sigScatterPlotHovered = QtCore.Signal(object, object, object)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal)
|
QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal)
|
||||||
self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical)
|
self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical)
|
||||||
@ -257,6 +258,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
self._indexMap = None
|
self._indexMap = None
|
||||||
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style)
|
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style)
|
||||||
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
|
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
|
||||||
|
self.scatterPlot.sigPointsHovered.connect(self.plotHovered)
|
||||||
self.updateSelected()
|
self.updateSelected()
|
||||||
|
|
||||||
def updateSelected(self):
|
def updateSelected(self):
|
||||||
@ -284,3 +286,6 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
for pt in points:
|
for pt in points:
|
||||||
pt.originalIndex = self._visibleIndices[pt.index()]
|
pt.originalIndex = self._visibleIndices[pt.index()]
|
||||||
self.sigScatterPlotClicked.emit(self, points, ev)
|
self.sigScatterPlotClicked.emit(self, points, ev)
|
||||||
|
|
||||||
|
def plotHovered(self, plot, points, ev):
|
||||||
|
self.sigScatterPlotHovered.emit(self, points, ev)
|
||||||
|
Loading…
Reference in New Issue
Block a user