pyqtgraph/pyqtgraph/widgets/ScatterPlotWidget.py

292 lines
11 KiB
Python
Raw Permalink Normal View History

2013-12-22 07:18:37 +00:00
from ..Qt import QtGui, QtCore
2013-02-10 19:10:30 +00:00
from .PlotWidget import PlotWidget
from .DataFilterWidget import DataFilterParameter
from .ColorMapWidget import ColorMapParameter
2013-12-22 07:18:37 +00:00
from .. import parametertree as ptree
from .. import functions as fn
from .. import getConfigOption
from ..graphicsItems.TextItem import TextItem
2013-02-10 19:10:30 +00:00
import numpy as np
from collections import OrderedDict
2013-02-10 19:10:30 +00:00
__all__ = ['ScatterPlotWidget']
class ScatterPlotWidget(QtGui.QSplitter):
"""
This is a high-level widget for exploring relationships in tabular data.
Given a multi-column record array, the widget displays a scatter plot of a
specific subset of the data. Includes controls for selecting the columns to
plot, filtering data, and determining symbol color and shape.
2013-02-10 19:10:30 +00:00
The widget consists of four components:
1) A list of column names from which the user may select 1 or 2 columns
to plot. If one column is selected, the data for that column will be
plotted in a histogram-like manner by using :func:`pseudoScatter()
<pyqtgraph.pseudoScatter>`. If two columns are selected, then the
scatter plot will be generated with x determined by the first column
that was selected and y by the second.
2) A DataFilter that allows the user to select a subset of the data by
specifying multiple selection criteria.
3) A ColorMap that allows the user to determine how points are colored by
specifying multiple criteria.
4) A PlotWidget for displaying the data.
"""
sigScatterPlotClicked = QtCore.Signal(object, object, object)
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
2020-12-16 19:07:39 +00:00
sigScatterPlotHovered = QtCore.Signal(object, object, object)
2013-02-10 19:10:30 +00:00
def __init__(self, parent=None):
QtGui.QSplitter.__init__(self, QtCore.Qt.Orientation.Horizontal)
self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Orientation.Vertical)
2013-02-10 19:10:30 +00:00
self.addWidget(self.ctrlPanel)
self.fieldList = QtGui.QListWidget()
self.fieldList.setSelectionMode(self.fieldList.SelectionMode.ExtendedSelection)
2013-02-10 19:10:30 +00:00
self.ptree = ptree.ParameterTree(showHeader=False)
self.filter = DataFilterParameter()
self.colorMap = ColorMapParameter()
self.params = ptree.Parameter.create(name='params', type='group', children=[self.filter, self.colorMap])
self.ptree.setParameters(self.params, showTop=False)
self.plot = PlotWidget()
self.ctrlPanel.addWidget(self.fieldList)
self.ctrlPanel.addWidget(self.ptree)
self.addWidget(self.plot)
2018-04-25 18:10:39 +00:00
fg = fn.mkColor(getConfigOption('foreground'))
fg.setAlpha(150)
self.filterText = TextItem(border=getConfigOption('foreground'), color=fg)
2013-03-26 17:46:26 +00:00
self.filterText.setPos(60,20)
self.filterText.setParentItem(self.plot.plotItem)
2013-02-10 19:10:30 +00:00
self.data = None
self.indices = None
2013-03-19 20:04:46 +00:00
self.mouseOverField = None
self.scatterPlot = None
self.selectionScatter = None
self.selectedIndices = []
2013-02-10 19:10:30 +00:00
self.style = dict(pen=None, symbol='o')
self._visibleXY = None # currently plotted points
self._visibleData = None # currently plotted records
self._visibleIndices = None
self._indexMap = None
2013-02-10 19:10:30 +00:00
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.filter.sigFilterChanged.connect(self.filterChanged)
self.colorMap.sigColorMapChanged.connect(self.updatePlot)
2013-03-19 20:04:46 +00:00
def setFields(self, fields, mouseOverField=None):
2013-02-10 19:10:30 +00:00
"""
Set the list of field names/units to be processed.
2013-02-10 22:45:16 +00:00
The format of *fields* is the same as used by
:func:`ColorMapWidget.setFields <pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields>`
2013-02-10 19:10:30 +00:00
"""
self.fields = OrderedDict(fields)
2013-03-19 20:04:46 +00:00
self.mouseOverField = mouseOverField
2013-02-10 19:10:30 +00:00
self.fieldList.clear()
for f,opts in fields:
item = QtGui.QListWidgetItem(f)
item.opts = opts
item = self.fieldList.addItem(item)
self.filter.setFields(fields)
self.colorMap.setFields(fields)
def setSelectedFields(self, *fields):
self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged)
try:
self.fieldList.clearSelection()
for f in fields:
i = list(self.fields.keys()).index(f)
item = self.fieldList.item(i)
item.setSelected(True)
finally:
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.fieldSelectionChanged()
2013-02-10 19:10:30 +00:00
def setData(self, data):
"""
Set the data to be processed and displayed.
Argument must be a numpy record array.
"""
self.data = data
self.indices = np.arange(len(data))
2013-02-10 19:10:30 +00:00
self.filtered = None
self.filteredIndices = None
2013-02-10 19:10:30 +00:00
self.updatePlot()
def setSelectedIndices(self, inds):
"""Mark the specified indices as selected.
Must be a sequence of integers that index into the array given in setData().
"""
self.selectedIndices = inds
self.updateSelected()
def setSelectedPoints(self, points):
"""Mark the specified points as selected.
Must be a list of points as generated by the sigScatterPlotClicked signal.
"""
self.setSelectedIndices([pt.originalIndex for pt in points])
2013-02-10 19:10:30 +00:00
def fieldSelectionChanged(self):
sel = self.fieldList.selectedItems()
if len(sel) > 2:
self.fieldList.blockSignals(True)
try:
for item in sel[1:-1]:
item.setSelected(False)
finally:
self.fieldList.blockSignals(False)
self.updatePlot()
def filterChanged(self, f):
self.filtered = None
self.updatePlot()
2013-03-26 17:46:26 +00:00
desc = self.filter.describe()
if len(desc) == 0:
self.filterText.setVisible(False)
else:
self.filterText.setText('\n'.join(desc))
self.filterText.setVisible(True)
2013-02-10 19:10:30 +00:00
def updatePlot(self):
self.plot.clear()
if self.data is None or len(self.data) == 0:
2013-02-10 19:10:30 +00:00
return
if self.filtered is None:
mask = self.filter.generateMask(self.data)
self.filtered = self.data[mask]
self.filteredIndices = self.indices[mask]
2013-02-10 19:10:30 +00:00
data = self.filtered
if len(data) == 0:
return
colors = np.array([fn.mkBrush(*x) for x in self.colorMap.map(data)])
style = self.style.copy()
## Look up selected columns and units
sel = list([str(item.text()) for item in self.fieldList.selectedItems()])
units = list([item.opts.get('units', '') for item in self.fieldList.selectedItems()])
if len(sel) == 0:
self.plot.setTitle('')
return
if len(sel) == 1:
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
if len(data) == 0:
return
2013-03-26 17:46:26 +00:00
#x = data[sel[0]]
#y = None
xy = [data[sel[0]], None]
2013-02-10 19:10:30 +00:00
elif len(sel) == 2:
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
if len(data) == 0:
return
2013-03-26 17:46:26 +00:00
xy = [data[sel[0]], data[sel[1]]]
#xydata = []
#for ax in [0,1]:
#d = data[sel[ax]]
### scatter catecorical values just a bit so they show up better in the scatter plot.
##if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
##d += np.random.normal(size=len(cells), scale=0.1)
#xydata.append(d)
#x,y = xydata
2013-02-10 19:10:30 +00:00
## convert enum-type fields to float, set axis labels
2013-03-26 17:46:26 +00:00
enum = [False, False]
2013-02-10 19:10:30 +00:00
for i in [0,1]:
axis = self.plot.getAxis(['bottom', 'left'][i])
2013-03-26 17:46:26 +00:00
if xy[i] is not None and (self.fields[sel[i]].get('mode', None) == 'enum' or xy[i].dtype.kind in ('S', 'O')):
2013-02-10 19:10:30 +00:00
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
2013-03-19 20:04:46 +00:00
xy[i] = np.array([vals.index(x) if x in vals else len(vals) for x in xy[i]], dtype=float)
2013-02-10 19:10:30 +00:00
axis.setTicks([list(enumerate(vals))])
2013-03-26 17:46:26 +00:00
enum[i] = True
2013-02-10 19:10:30 +00:00
else:
axis.setTicks(None) # reset to automatic ticking
## mask out any nan values
2013-03-26 17:46:26 +00:00
mask = np.ones(len(xy[0]), dtype=bool)
if xy[0].dtype.kind == 'f':
mask &= np.isfinite(xy[0])
2013-03-26 17:46:26 +00:00
if xy[1] is not None and xy[1].dtype.kind == 'f':
mask &= np.isfinite(xy[1])
2013-03-26 17:46:26 +00:00
xy[0] = xy[0][mask]
2013-02-10 19:10:30 +00:00
style['symbolBrush'] = colors[mask]
data = data[mask]
indices = self.filteredIndices[mask]
2013-02-10 19:10:30 +00:00
## Scatter y-values for a histogram-like appearance
2013-03-26 17:46:26 +00:00
if xy[1] is None:
## column scatter plot
xy[1] = fn.pseudoScatter(xy[0])
2013-02-10 19:10:30 +00:00
else:
2013-03-26 17:46:26 +00:00
## beeswarm plots
xy[1] = xy[1][mask]
for ax in [0,1]:
if not enum[ax]:
continue
2013-07-03 15:20:49 +00:00
imax = int(xy[ax].max()) if len(xy[ax]) > 0 else 0
for i in range(imax+1):
2013-03-26 17:46:26 +00:00
keymask = xy[ax] == i
2013-12-22 07:18:37 +00:00
scatter = fn.pseudoScatter(xy[1-ax][keymask], bidir=True)
2013-07-03 15:20:49 +00:00
if len(scatter) == 0:
continue
smax = np.abs(scatter).max()
if smax != 0:
scatter *= 0.2 / smax
2013-03-26 17:46:26 +00:00
xy[ax][keymask] += scatter
2013-03-19 20:04:46 +00:00
if self.scatterPlot is not None:
try:
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
except:
pass
2013-02-10 19:10:30 +00:00
self._visibleXY = xy
self._visibleData = data
self._visibleIndices = indices
self._indexMap = None
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
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
2020-12-16 19:07:39 +00:00
self.scatterPlot.sigPointsHovered.connect(self.plotHovered)
self.updateSelected()
2013-03-19 20:04:46 +00:00
def updateSelected(self):
if self._visibleXY is None:
return
# map from global index to visible index
indMap = self._getIndexMap()
inds = [indMap[i] for i in self.selectedIndices if i in indMap]
x,y = self._visibleXY[0][inds], self._visibleXY[1][inds]
2013-03-19 20:04:46 +00:00
if self.selectionScatter is not None:
self.plot.plotItem.removeItem(self.selectionScatter)
if len(x) == 0:
return
self.selectionScatter = self.plot.plot(x, y, pen=None, symbol='s', symbolSize=12, symbolBrush=None, symbolPen='y')
def _getIndexMap(self):
# mapping from original data index to visible point index
if self._indexMap is None:
self._indexMap = {j:i for i,j in enumerate(self._visibleIndices)}
return self._indexMap
def plotClicked(self, plot, points, ev):
# Tag each point with its index into the original dataset
for pt in points:
pt.originalIndex = self._visibleIndices[pt.index()]
self.sigScatterPlotClicked.emit(self, points, ev)
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
2020-12-16 19:07:39 +00:00
def plotHovered(self, plot, points, ev):
self.sigScatterPlotHovered.emit(self, points, ev)