Merge pull request #623 from termim/ScatterPlot

Scatter plot
This commit is contained in:
Luke Campagnola 2018-01-29 18:55:19 -08:00 committed by GitHub
commit 1f9acd1502
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 48 additions and 18 deletions

View File

@ -11,6 +11,7 @@ import initExample
from pyqtgraph.Qt import QtGui, QtCore 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
app = QtGui.QApplication([]) app = QtGui.QApplication([])
mw = QtGui.QMainWindow() mw = QtGui.QMainWindow()
@ -32,8 +33,8 @@ 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: ## 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.
n = 300 n = 300
@ -57,21 +58,41 @@ 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
## image. ## image.
TextSymbol = namedtuple("TextSymbol", "label symbol scale")
def createLabel(label, angle):
symbol = QtGui.QPainterPath()
#symbol.addText(0, 0, QFont("San Serif", 10), label)
f = QtGui.QFont()
f.setPointSize(10)
symbol.addText(0, 0, f, label)
br = symbol.boundingRect()
scale = min(1. / br.width(), 1. / br.height())
tr = QtGui.QTransform()
tr.scale(scale, scale)
tr.rotate(angle)
tr.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.)
return TextSymbol(label, tr.map(symbol), 0.1 / scale)
random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i in range(np.random.randint(1,5))]), np.random.randint(0, 360))
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
pos = np.random.normal(size=(2,n), scale=1e-5) pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)]
s2.addPoints(spots) s2.addPoints(spots)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]]
s2.addPoints(spots)
w2.addItem(s2) w2.addItem(s2)
s2.sigClicked.connect(clicked) s2.sigClicked.connect(clicked)
## 3) Spots are not transform-invariant, not identical (bottom-left). ## 3) Spots are not transform-invariant, not identical (bottom-left).
## 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

View File

@ -126,7 +126,7 @@ class SymbolAtlas(object):
keyi = None keyi = None
sourceRecti = None sourceRecti = None
for i, rec in enumerate(opts): for i, rec in enumerate(opts):
key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
if key == keyi: if key == keyi:
sourceRect[i] = sourceRecti sourceRect[i] = sourceRecti
else: else:
@ -136,6 +136,7 @@ class SymbolAtlas(object):
newRectSrc = QtCore.QRectF() newRectSrc = QtCore.QRectF()
newRectSrc.pen = rec['pen'] newRectSrc.pen = rec['pen']
newRectSrc.brush = rec['brush'] newRectSrc.brush = rec['brush']
newRectSrc.symbol = rec[3]
self.symbolMap[key] = newRectSrc self.symbolMap[key] = newRectSrc
self.atlasValid = False self.atlasValid = False
sourceRect[i] = newRectSrc sourceRect[i] = newRectSrc
@ -151,7 +152,7 @@ class SymbolAtlas(object):
images = [] images = []
for key, sourceRect in self.symbolMap.items(): for key, sourceRect in self.symbolMap.items():
if sourceRect.width() == 0: if sourceRect.width() == 0:
img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush) img = renderSymbol(sourceRect.symbol, key[1], sourceRect.pen, sourceRect.brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False) arr = fn.imageToArray(img, copy=False, transpose=False)
else: else:

View File

@ -1,3 +1,4 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
app = pg.mkQApp() app = pg.mkQApp()
@ -7,9 +8,16 @@ app.processEvents()
def test_scatterplotitem(): def test_scatterplotitem():
plot = pg.PlotWidget() plot = pg.PlotWidget()
# set view range equal to its bounding rect. # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode. # This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect()) plot.setRange(rect=plot.boundingRect())
# test SymbolAtlas accepts custom symbol
s = pg.ScatterPlotItem()
symbol = QtGui.QPainterPath()
symbol.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
s.addPoints([{'pos': [0,0], 'data': 1, 'symbol': symbol}])
for i, pxMode in enumerate([True, False]): for i, pxMode in enumerate([True, False]):
for j, useCache in enumerate([True, False]): for j, useCache in enumerate([True, False]):
s = pg.ScatterPlotItem() s = pg.ScatterPlotItem()
@ -17,14 +25,14 @@ def test_scatterplotitem():
plot.addItem(s) plot.addItem(s)
s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode)
s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
# Test uniform spot updates # Test uniform spot updates
s.setSize(10) s.setSize(10)
s.setBrush('r') s.setBrush('r')
s.setPen('g') s.setPen('g')
s.setSymbol('+') s.setSymbol('+')
app.processEvents() app.processEvents()
# Test list spot updates # Test list spot updates
s.setSize([10] * 6) s.setSize([10] * 6)
s.setBrush([pg.mkBrush('r')] * 6) s.setBrush([pg.mkBrush('r')] * 6)
@ -55,7 +63,7 @@ def test_scatterplotitem():
def test_init_spots(): def test_init_spots():
plot = pg.PlotWidget() plot = pg.PlotWidget()
# set view range equal to its bounding rect. # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode. # This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect()) plot.setRange(rect=plot.boundingRect())
spots = [ spots = [
@ -63,28 +71,28 @@ def test_init_spots():
{'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'}, {'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'},
] ]
s = pg.ScatterPlotItem(spots=spots) s = pg.ScatterPlotItem(spots=spots)
# Check we can display without errors # Check we can display without errors
plot.addItem(s) plot.addItem(s)
app.processEvents() app.processEvents()
plot.clear() plot.clear()
# check data is correct # check data is correct
spots = s.points() spots = s.points()
defPen = pg.mkPen(pg.getConfigOption('foreground')) defPen = pg.mkPen(pg.getConfigOption('foreground'))
assert spots[0].pos().x() == 0 assert spots[0].pos().x() == 0
assert spots[0].pos().y() == 1 assert spots[0].pos().y() == 1
assert spots[0].pen() == defPen assert spots[0].pen() == defPen
assert spots[0].data() is None assert spots[0].data() is None
assert spots[1].pos().x() == 1 assert spots[1].pos().x() == 1
assert spots[1].pos().y() == 2 assert spots[1].pos().y() == 2
assert spots[1].pen() == pg.mkPen(None) assert spots[1].pen() == pg.mkPen(None)
assert spots[1].brush() == pg.mkBrush(None) assert spots[1].brush() == pg.mkBrush(None)
assert spots[1].data() == 'zzz' assert spots[1].data() == 'zzz'
if __name__ == '__main__': if __name__ == '__main__':
test_scatterplotitem() test_scatterplotitem()