Merged changes from acq4
Added style options to flowchart connection lines SVG export bug - correctly handle coordinate corrections for groups with mixed elements Updates to ScatterPlotWidget, DataFilterWidget, and ColorMapWidget Added exit() function for working around PyQt exit crashes Bidirectional pseudoScatter for beeswarm plots Added several examples Added BarGraphItem Fixed GraphItem antialiasing Added parentChanged and viewChanged hooks to GraphicsItem Made LabelItem a subclass of GraphicsWidgetAnchor Documented planned features for PlotDataItem (these should be fixed before next release) ScaleBar complete rewrite Re-fixed crash bug in ScatterPlotItem fixed scatterplotitem antialiasing
This commit is contained in:
commit
a67667b1ca
@ -97,6 +97,6 @@ Miscellaneous Functions
|
|||||||
|
|
||||||
.. autofunction:: pyqtgraph.systemInfo
|
.. autofunction:: pyqtgraph.systemInfo
|
||||||
|
|
||||||
|
.. autofunction:: pyqtgraph.exit
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ import pyqtgraph as pg
|
|||||||
from pyqtgraph.Qt import QtCore, QtGui
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
# Enable antialiasing for prettier plots
|
||||||
|
pg.setConfigOptions(antialias=True)
|
||||||
|
|
||||||
w = pg.GraphicsWindow()
|
w = pg.GraphicsWindow()
|
||||||
w.setWindowTitle('pyqtgraph example: GraphItem')
|
w.setWindowTitle('pyqtgraph example: GraphItem')
|
||||||
v = w.addViewBox()
|
v = w.addViewBox()
|
||||||
|
@ -21,7 +21,8 @@ win = pg.GraphicsWindow(title="Basic plotting examples")
|
|||||||
win.resize(1000,600)
|
win.resize(1000,600)
|
||||||
win.setWindowTitle('pyqtgraph example: Plotting')
|
win.setWindowTitle('pyqtgraph example: Plotting')
|
||||||
|
|
||||||
|
# Enable antialiasing for prettier plots
|
||||||
|
pg.setConfigOptions(antialias=True)
|
||||||
|
|
||||||
p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100))
|
p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100))
|
||||||
|
|
||||||
|
31
examples/ScaleBar.py
Normal file
31
examples/ScaleBar.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Demonstrates ScaleBar
|
||||||
|
"""
|
||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
pg.mkQApp()
|
||||||
|
win = pg.GraphicsWindow()
|
||||||
|
win.setWindowTitle('pyqtgraph example: ScaleBar')
|
||||||
|
|
||||||
|
vb = win.addViewBox()
|
||||||
|
vb.setAspectLocked()
|
||||||
|
|
||||||
|
img = pg.ImageItem()
|
||||||
|
img.setImage(np.random.normal(size=(100,100)))
|
||||||
|
img.scale(0.01, 0.01)
|
||||||
|
vb.addItem(img)
|
||||||
|
|
||||||
|
scale = pg.ScaleBar(size=0.1)
|
||||||
|
scale.setParentItem(vb)
|
||||||
|
scale.anchor((1, 1), (1, 1), offset=(-20, -20))
|
||||||
|
|
||||||
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
QtGui.QApplication.instance().exec_()
|
52
examples/ScatterPlotWidget.py
Normal file
52
examples/ScatterPlotWidget.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
pg.mkQApp()
|
||||||
|
|
||||||
|
spw = pg.ScatterPlotWidget()
|
||||||
|
spw.show()
|
||||||
|
|
||||||
|
data = np.array([
|
||||||
|
(1, 1, 3, 4, 'x'),
|
||||||
|
(2, 3, 3, 7, 'y'),
|
||||||
|
(3, 2, 5, 2, 'z'),
|
||||||
|
(4, 4, 6, 9, 'z'),
|
||||||
|
(5, 3, 6, 7, 'x'),
|
||||||
|
(6, 5, 4, 6, 'x'),
|
||||||
|
(7, 5, 8, 2, 'z'),
|
||||||
|
(8, 1, 2, 4, 'x'),
|
||||||
|
(9, 2, 3, 7, 'z'),
|
||||||
|
(0, 6, 0, 2, 'z'),
|
||||||
|
(1, 3, 1, 2, 'z'),
|
||||||
|
(2, 5, 4, 6, 'y'),
|
||||||
|
(3, 4, 8, 1, 'y'),
|
||||||
|
(4, 7, 6, 8, 'z'),
|
||||||
|
(5, 8, 7, 4, 'y'),
|
||||||
|
(6, 1, 2, 3, 'y'),
|
||||||
|
(7, 5, 3, 9, 'z'),
|
||||||
|
(8, 9, 3, 1, 'x'),
|
||||||
|
(9, 2, 6, 2, 'z'),
|
||||||
|
(0, 3, 4, 6, 'x'),
|
||||||
|
(1, 5, 9, 3, 'y'),
|
||||||
|
], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')])
|
||||||
|
|
||||||
|
spw.setFields([
|
||||||
|
('col1', {'units': 'm'}),
|
||||||
|
('col2', {'units': 'm'}),
|
||||||
|
('col3', {}),
|
||||||
|
('col4', {}),
|
||||||
|
('col5', {'mode': 'enum', 'values': ['x', 'y', 'z']}),
|
||||||
|
])
|
||||||
|
|
||||||
|
spw.setData(data)
|
||||||
|
|
||||||
|
|
||||||
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
QtGui.QApplication.instance().exec_()
|
@ -3,9 +3,12 @@ import initExample ## Add path to library (just for examples; you do not need th
|
|||||||
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
|
||||||
pg.plot(np.random.normal(size=100000), title="Simplest possible plotting example")
|
plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example")
|
||||||
|
plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]])
|
||||||
## Start Qt event loop unless running in interactive mode or using pyside.
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene())
|
||||||
|
ex.export('/home/luke/tmp/test.svg')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'):
|
if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
38
examples/beeswarm.py
Normal file
38
examples/beeswarm.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Example beeswarm / bar chart
|
||||||
|
"""
|
||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
win = pg.plot()
|
||||||
|
win.setWindowTitle('pyqtgraph example: beeswarm')
|
||||||
|
|
||||||
|
data = np.random.normal(size=(4,20))
|
||||||
|
data[0] += 5
|
||||||
|
data[1] += 7
|
||||||
|
data[2] += 5
|
||||||
|
data[3] = 10 + data[3] * 2
|
||||||
|
|
||||||
|
## Make bar graph
|
||||||
|
#bar = pg.BarGraphItem(x=range(4), height=data.mean(axis=1), width=0.5, brush=0.4)
|
||||||
|
#win.addItem(bar)
|
||||||
|
|
||||||
|
## add scatter plots on top
|
||||||
|
for i in range(4):
|
||||||
|
xvals = pg.pseudoScatter(data[i], spacing=0.4, bidir=True) * 0.2
|
||||||
|
win.plot(x=xvals+i, y=data[i], pen=None, symbol='o', symbolBrush=pg.intColor(i,6,maxValue=128))
|
||||||
|
|
||||||
|
## Make error bars
|
||||||
|
err = pg.ErrorBarItem(x=np.arange(4), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2})
|
||||||
|
win.addItem(err)
|
||||||
|
|
||||||
|
|
||||||
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
QtGui.QApplication.instance().exec_()
|
@ -54,6 +54,7 @@ CONFIG_OPTIONS = {
|
|||||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||||
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
||||||
'weaveDebug': False, ## Print full error message if weave compile fails
|
'weaveDebug': False, ## Print full error message if weave compile fails
|
||||||
|
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
|
||||||
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
|
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,9 +192,20 @@ from .SignalProxy import *
|
|||||||
from .colormap import *
|
from .colormap import *
|
||||||
from .ptime import time
|
from .ptime import time
|
||||||
|
|
||||||
|
##############################################################
|
||||||
|
## PyQt and PySide both are prone to crashing on exit.
|
||||||
|
## There are two general approaches to dealing with this:
|
||||||
|
## 1. Install atexit handlers that assist in tearing down to avoid crashes.
|
||||||
|
## This helps, but is never perfect.
|
||||||
|
## 2. Terminate the process before python starts tearing down
|
||||||
|
## This is potentially dangerous
|
||||||
|
|
||||||
|
## Attempts to work around exit crashes:
|
||||||
import atexit
|
import atexit
|
||||||
def cleanup():
|
def cleanup():
|
||||||
|
if not getConfigOption('exitCleanup'):
|
||||||
|
return
|
||||||
|
|
||||||
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
|
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
|
||||||
|
|
||||||
## Workaround for Qt exit crash:
|
## Workaround for Qt exit crash:
|
||||||
@ -213,6 +225,38 @@ def cleanup():
|
|||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
## Optional function for exiting immediately (with some manual teardown)
|
||||||
|
def exit():
|
||||||
|
"""
|
||||||
|
Causes python to exit without garbage-collecting any objects, and thus avoids
|
||||||
|
calling object destructor methods. This is a sledgehammer workaround for
|
||||||
|
a variety of bugs in PyQt and Pyside that cause crashes on exit.
|
||||||
|
|
||||||
|
This function does the following in an attempt to 'safely' terminate
|
||||||
|
the process:
|
||||||
|
|
||||||
|
* Invoke atexit callbacks
|
||||||
|
* Close all open file handles
|
||||||
|
* os._exit()
|
||||||
|
|
||||||
|
Note: there is some potential for causing damage with this function if you
|
||||||
|
are using objects that _require_ their destructors to be called (for example,
|
||||||
|
to properly terminate log files, disconnect from devices, etc). Situations
|
||||||
|
like this are probably quite rare, but use at your own risk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
## first disable our own cleanup function; won't be needing it.
|
||||||
|
setConfigOptions(exitCleanup=False)
|
||||||
|
|
||||||
|
## invoke atexit callbacks
|
||||||
|
atexit._run_exitfuncs()
|
||||||
|
|
||||||
|
## close file handles
|
||||||
|
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
|
||||||
|
|
||||||
|
os._exit(os.EX_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Convenience functions for command-line use
|
## Convenience functions for command-line use
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget):
|
|||||||
|
|
||||||
|
|
||||||
def execMulti(self, nextLine):
|
def execMulti(self, nextLine):
|
||||||
self.stdout.write(nextLine+"\n")
|
#self.stdout.write(nextLine+"\n")
|
||||||
if nextLine.strip() != '':
|
if nextLine.strip() != '':
|
||||||
self.multiline += "\n" + nextLine
|
self.multiline += "\n" + nextLine
|
||||||
return
|
return
|
||||||
|
@ -304,7 +304,36 @@ def _generateItemSvg(item, nodes=None, root=None):
|
|||||||
|
|
||||||
def correctCoordinates(node, item):
|
def correctCoordinates(node, item):
|
||||||
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
|
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
|
||||||
|
## Each item is represented by a single top-level group with one or more groups inside.
|
||||||
|
## Each inner group contains one or more drawing primitives, possibly of different types.
|
||||||
groups = node.getElementsByTagName('g')
|
groups = node.getElementsByTagName('g')
|
||||||
|
|
||||||
|
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
|
||||||
|
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
|
||||||
|
groups2 = []
|
||||||
|
for grp in groups:
|
||||||
|
subGroups = [grp.cloneNode(deep=False)]
|
||||||
|
textGroup = None
|
||||||
|
for ch in grp.childNodes[:]:
|
||||||
|
if isinstance(ch, xml.Element):
|
||||||
|
if textGroup is None:
|
||||||
|
textGroup = ch.tagName == 'text'
|
||||||
|
if ch.tagName == 'text':
|
||||||
|
if textGroup is False:
|
||||||
|
subGroups.append(grp.cloneNode(deep=False))
|
||||||
|
textGroup = True
|
||||||
|
else:
|
||||||
|
if textGroup is True:
|
||||||
|
subGroups.append(grp.cloneNode(deep=False))
|
||||||
|
textGroup = False
|
||||||
|
subGroups[-1].appendChild(ch)
|
||||||
|
groups2.extend(subGroups)
|
||||||
|
for sg in subGroups:
|
||||||
|
node.insertBefore(sg, grp)
|
||||||
|
node.removeChild(grp)
|
||||||
|
groups = groups2
|
||||||
|
|
||||||
|
|
||||||
for grp in groups:
|
for grp in groups:
|
||||||
matrix = grp.getAttribute('transform')
|
matrix = grp.getAttribute('transform')
|
||||||
match = re.match(r'matrix\((.*)\)', matrix)
|
match = re.match(r'matrix\((.*)\)', matrix)
|
||||||
@ -375,7 +404,6 @@ def correctCoordinates(node, item):
|
|||||||
if removeTransform:
|
if removeTransform:
|
||||||
grp.removeAttribute('transform')
|
grp.removeAttribute('transform')
|
||||||
|
|
||||||
|
|
||||||
def itemTransform(item, root):
|
def itemTransform(item, root):
|
||||||
## Return the transformation mapping item to root
|
## Return the transformation mapping item to root
|
||||||
## (actually to parent coordinate system of root)
|
## (actually to parent coordinate system of root)
|
||||||
|
@ -523,6 +523,15 @@ class ConnectionItem(GraphicsObject):
|
|||||||
self.hovered = False
|
self.hovered = False
|
||||||
self.path = None
|
self.path = None
|
||||||
self.shapePath = None
|
self.shapePath = None
|
||||||
|
self.style = {
|
||||||
|
'shape': 'line',
|
||||||
|
'color': (100, 100, 250),
|
||||||
|
'width': 1.0,
|
||||||
|
'hoverColor': (150, 150, 250),
|
||||||
|
'hoverWidth': 1.0,
|
||||||
|
'selectedColor': (200, 200, 0),
|
||||||
|
'selectedWidth': 3.0,
|
||||||
|
}
|
||||||
#self.line = QtGui.QGraphicsLineItem(self)
|
#self.line = QtGui.QGraphicsLineItem(self)
|
||||||
self.source.getViewBox().addItem(self)
|
self.source.getViewBox().addItem(self)
|
||||||
self.updateLine()
|
self.updateLine()
|
||||||
@ -537,6 +546,13 @@ class ConnectionItem(GraphicsObject):
|
|||||||
self.target = target
|
self.target = target
|
||||||
self.updateLine()
|
self.updateLine()
|
||||||
|
|
||||||
|
def setStyle(self, **kwds):
|
||||||
|
self.style.update(kwds)
|
||||||
|
if 'shape' in kwds:
|
||||||
|
self.updateLine()
|
||||||
|
else:
|
||||||
|
self.update()
|
||||||
|
|
||||||
def updateLine(self):
|
def updateLine(self):
|
||||||
start = Point(self.source.connectPoint())
|
start = Point(self.source.connectPoint())
|
||||||
if isinstance(self.target, TerminalGraphicsItem):
|
if isinstance(self.target, TerminalGraphicsItem):
|
||||||
@ -547,19 +563,20 @@ class ConnectionItem(GraphicsObject):
|
|||||||
return
|
return
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
|
|
||||||
self.path = QtGui.QPainterPath()
|
self.path = self.generatePath(start, stop)
|
||||||
self.path.moveTo(start)
|
|
||||||
self.path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
|
|
||||||
self.shapePath = None
|
self.shapePath = None
|
||||||
#self.resetTransform()
|
|
||||||
#ang = (stop-start).angle(Point(0, 1))
|
|
||||||
#if ang is None:
|
|
||||||
#ang = 0
|
|
||||||
#self.rotate(ang)
|
|
||||||
#self.setPos(start)
|
|
||||||
#self.length = (start-stop).length()
|
|
||||||
self.update()
|
self.update()
|
||||||
#self.line.setLine(start.x(), start.y(), stop.x(), stop.y())
|
|
||||||
|
def generatePath(self, start, stop):
|
||||||
|
path = QtGui.QPainterPath()
|
||||||
|
path.moveTo(start)
|
||||||
|
if self.style['shape'] == 'line':
|
||||||
|
path.lineTo(stop)
|
||||||
|
elif self.style['shape'] == 'cubic':
|
||||||
|
path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape'])
|
||||||
|
return path
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
def keyPressEvent(self, ev):
|
||||||
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
|
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
|
||||||
@ -609,12 +626,12 @@ class ConnectionItem(GraphicsObject):
|
|||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
if self.isSelected():
|
if self.isSelected():
|
||||||
p.setPen(fn.mkPen(200, 200, 0, width=3))
|
p.setPen(fn.mkPen(self.style['selectedColor'], width=self.style['selectedWidth']))
|
||||||
else:
|
else:
|
||||||
if self.hovered:
|
if self.hovered:
|
||||||
p.setPen(fn.mkPen(150, 150, 250, width=1))
|
p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth']))
|
||||||
else:
|
else:
|
||||||
p.setPen(fn.mkPen(100, 100, 250, width=1))
|
p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
|
||||||
|
|
||||||
#p.drawLine(0, 0, 0, self.length)
|
#p.drawLine(0, 0, 0, self.length)
|
||||||
|
|
||||||
|
@ -1930,9 +1930,9 @@ def invertQTransform(tr):
|
|||||||
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
||||||
|
|
||||||
|
|
||||||
def pseudoScatter(data, spacing=None, shuffle=True):
|
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
||||||
"""
|
"""
|
||||||
Used for examining the distribution of values in a set.
|
Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots.
|
||||||
|
|
||||||
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot
|
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot
|
||||||
will not have overlapping points (it will look similar to a histogram).
|
will not have overlapping points (it will look similar to a histogram).
|
||||||
@ -1959,23 +1959,41 @@ def pseudoScatter(data, spacing=None, shuffle=True):
|
|||||||
xmask = dx < s2 # exclude anything too far away
|
xmask = dx < s2 # exclude anything too far away
|
||||||
|
|
||||||
if xmask.sum() > 0:
|
if xmask.sum() > 0:
|
||||||
dx = dx[xmask]
|
if bidir:
|
||||||
dy = (s2 - dx)**0.5
|
dirs = [-1, 1]
|
||||||
|
else:
|
||||||
|
dirs = [1]
|
||||||
|
yopts = []
|
||||||
|
for direction in dirs:
|
||||||
|
y = 0
|
||||||
|
dx2 = dx[xmask]
|
||||||
|
dy = (s2 - dx2)**0.5
|
||||||
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
|
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
|
||||||
limits[0] = y0[xmask] - dy
|
limits[0] = y0[xmask] - dy
|
||||||
limits[1] = y0[xmask] + dy
|
limits[1] = y0[xmask] + dy
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# ignore anything below this y-value
|
# ignore anything below this y-value
|
||||||
|
if direction > 0:
|
||||||
mask = limits[1] >= y
|
mask = limits[1] >= y
|
||||||
limits = limits[:,mask]
|
else:
|
||||||
|
mask = limits[0] <= y
|
||||||
|
|
||||||
|
limits2 = limits[:,mask]
|
||||||
|
|
||||||
# are we inside an excluded region?
|
# are we inside an excluded region?
|
||||||
mask = (limits[0] < y) & (limits[1] > y)
|
mask = (limits2[0] < y) & (limits2[1] > y)
|
||||||
if mask.sum() == 0:
|
if mask.sum() == 0:
|
||||||
break
|
break
|
||||||
y = limits[:,mask].max()
|
|
||||||
|
|
||||||
|
if direction > 0:
|
||||||
|
y = limits2[:,mask].max()
|
||||||
|
else:
|
||||||
|
y = limits2[:,mask].min()
|
||||||
|
yopts.append(y)
|
||||||
|
if bidir:
|
||||||
|
y = yopts[0] if -yopts[0] < yopts[1] else yopts[1]
|
||||||
|
else:
|
||||||
|
y = yopts[0]
|
||||||
yvals[i] = y
|
yvals[i] = y
|
||||||
|
|
||||||
return yvals[np.argsort(inds)] ## un-shuffle values before returning
|
return yvals[np.argsort(inds)] ## un-shuffle values before returning
|
||||||
|
149
pyqtgraph/graphicsItems/BarGraphItem.py
Normal file
149
pyqtgraph/graphicsItems/BarGraphItem.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
|
from .GraphicsObject import GraphicsObject
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
__all__ = ['BarGraphItem']
|
||||||
|
|
||||||
|
class BarGraphItem(GraphicsObject):
|
||||||
|
def __init__(self, **opts):
|
||||||
|
"""
|
||||||
|
Valid keyword options are:
|
||||||
|
x, x0, x1, y, y0, y1, width, height, pen, brush
|
||||||
|
|
||||||
|
x specifies the x-position of the center of the bar.
|
||||||
|
x0, x1 specify left and right edges of the bar, respectively.
|
||||||
|
width specifies distance from x0 to x1.
|
||||||
|
You may specify any combination:
|
||||||
|
|
||||||
|
x, width
|
||||||
|
x0, width
|
||||||
|
x1, width
|
||||||
|
x0, x1
|
||||||
|
|
||||||
|
Likewise y, y0, y1, and height.
|
||||||
|
If only height is specified, then y0 will be set to 0
|
||||||
|
|
||||||
|
Example uses:
|
||||||
|
|
||||||
|
BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
GraphicsObject.__init__(self)
|
||||||
|
self.opts = dict(
|
||||||
|
x=None,
|
||||||
|
y=None,
|
||||||
|
x0=None,
|
||||||
|
y0=None,
|
||||||
|
x1=None,
|
||||||
|
y1=None,
|
||||||
|
height=None,
|
||||||
|
width=None,
|
||||||
|
pen=None,
|
||||||
|
brush=None,
|
||||||
|
pens=None,
|
||||||
|
brushes=None,
|
||||||
|
)
|
||||||
|
self.setOpts(**opts)
|
||||||
|
|
||||||
|
def setOpts(self, **opts):
|
||||||
|
self.opts.update(opts)
|
||||||
|
self.picture = None
|
||||||
|
self.update()
|
||||||
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
|
def drawPicture(self):
|
||||||
|
self.picture = QtGui.QPicture()
|
||||||
|
p = QtGui.QPainter(self.picture)
|
||||||
|
|
||||||
|
pen = self.opts['pen']
|
||||||
|
pens = self.opts['pens']
|
||||||
|
|
||||||
|
if pen is None and pens is None:
|
||||||
|
pen = pg.getConfigOption('foreground')
|
||||||
|
|
||||||
|
brush = self.opts['brush']
|
||||||
|
brushes = self.opts['brushes']
|
||||||
|
if brush is None and brushes is None:
|
||||||
|
brush = (128, 128, 128)
|
||||||
|
|
||||||
|
def asarray(x):
|
||||||
|
if x is None or np.isscalar(x) or isinstance(x, np.ndarray):
|
||||||
|
return x
|
||||||
|
return np.array(x)
|
||||||
|
|
||||||
|
|
||||||
|
x = asarray(self.opts.get('x'))
|
||||||
|
x0 = asarray(self.opts.get('x0'))
|
||||||
|
x1 = asarray(self.opts.get('x1'))
|
||||||
|
width = asarray(self.opts.get('width'))
|
||||||
|
|
||||||
|
if x0 is None:
|
||||||
|
if width is None:
|
||||||
|
raise Exception('must specify either x0 or width')
|
||||||
|
if x1 is not None:
|
||||||
|
x0 = x1 - width
|
||||||
|
elif x is not None:
|
||||||
|
x0 = x - width/2.
|
||||||
|
else:
|
||||||
|
raise Exception('must specify at least one of x, x0, or x1')
|
||||||
|
if width is None:
|
||||||
|
if x1 is None:
|
||||||
|
raise Exception('must specify either x1 or width')
|
||||||
|
width = x1 - x0
|
||||||
|
|
||||||
|
y = asarray(self.opts.get('y'))
|
||||||
|
y0 = asarray(self.opts.get('y0'))
|
||||||
|
y1 = asarray(self.opts.get('y1'))
|
||||||
|
height = asarray(self.opts.get('height'))
|
||||||
|
|
||||||
|
if y0 is None:
|
||||||
|
if height is None:
|
||||||
|
y0 = 0
|
||||||
|
elif y1 is not None:
|
||||||
|
y0 = y1 - height
|
||||||
|
elif y is not None:
|
||||||
|
y0 = y - height/2.
|
||||||
|
else:
|
||||||
|
y0 = 0
|
||||||
|
if height is None:
|
||||||
|
if y1 is None:
|
||||||
|
raise Exception('must specify either y1 or height')
|
||||||
|
height = y1 - y0
|
||||||
|
|
||||||
|
p.setPen(pg.mkPen(pen))
|
||||||
|
p.setBrush(pg.mkBrush(brush))
|
||||||
|
for i in range(len(x0)):
|
||||||
|
if pens is not None:
|
||||||
|
p.setPen(pg.mkPen(pens[i]))
|
||||||
|
if brushes is not None:
|
||||||
|
p.setBrush(pg.mkBrush(brushes[i]))
|
||||||
|
|
||||||
|
if np.isscalar(y0):
|
||||||
|
y = y0
|
||||||
|
else:
|
||||||
|
y = y0[i]
|
||||||
|
if np.isscalar(width):
|
||||||
|
w = width
|
||||||
|
else:
|
||||||
|
w = width[i]
|
||||||
|
|
||||||
|
p.drawRect(QtCore.QRectF(x0[i], y, w, height[i]))
|
||||||
|
|
||||||
|
|
||||||
|
p.end()
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
|
||||||
|
|
||||||
|
def paint(self, p, *args):
|
||||||
|
if self.picture is None:
|
||||||
|
self.drawPicture()
|
||||||
|
self.picture.play(p)
|
||||||
|
|
||||||
|
def boundingRect(self):
|
||||||
|
if self.picture is None:
|
||||||
|
self.drawPicture()
|
||||||
|
return QtCore.QRectF(self.picture.boundingRect())
|
||||||
|
|
||||||
|
|
@ -103,6 +103,8 @@ class GraphItem(GraphicsObject):
|
|||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
if self.picture == None:
|
if self.picture == None:
|
||||||
self.generatePicture()
|
self.generatePicture()
|
||||||
|
if pg.getConfigOption('antialias') is True:
|
||||||
|
p.setRenderHint(p.Antialiasing)
|
||||||
self.picture.play(p)
|
self.picture.play(p)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
|
@ -446,6 +446,14 @@ class GraphicsItem(object):
|
|||||||
#print " --> ", ch2.scene()
|
#print " --> ", ch2.scene()
|
||||||
#self.setChildScene(ch2)
|
#self.setChildScene(ch2)
|
||||||
|
|
||||||
|
def parentChanged(self):
|
||||||
|
"""Called when the item's parent has changed.
|
||||||
|
This method handles connecting / disconnecting from ViewBox signals
|
||||||
|
to make sure viewRangeChanged works properly. It should generally be
|
||||||
|
extended, not overridden."""
|
||||||
|
self._updateView()
|
||||||
|
|
||||||
|
|
||||||
def _updateView(self):
|
def _updateView(self):
|
||||||
## called to see whether this item has a new view to connect to
|
## called to see whether this item has a new view to connect to
|
||||||
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
|
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
|
||||||
@ -496,6 +504,12 @@ class GraphicsItem(object):
|
|||||||
## inform children that their view might have changed
|
## inform children that their view might have changed
|
||||||
self._replaceView(oldView)
|
self._replaceView(oldView)
|
||||||
|
|
||||||
|
self.viewChanged(view, oldView)
|
||||||
|
|
||||||
|
def viewChanged(self, view, oldView):
|
||||||
|
"""Called when this item's view has changed
|
||||||
|
(ie, the item has been added to or removed from a ViewBox)"""
|
||||||
|
pass
|
||||||
|
|
||||||
def _replaceView(self, oldView, item=None):
|
def _replaceView(self, oldView, item=None):
|
||||||
if item is None:
|
if item is None:
|
||||||
|
@ -19,7 +19,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
|
|||||||
def itemChange(self, change, value):
|
def itemChange(self, change, value):
|
||||||
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
||||||
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
||||||
self._updateView()
|
self.parentChanged()
|
||||||
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
|
@ -5,7 +5,9 @@ from ..Point import Point
|
|||||||
class GraphicsWidgetAnchor(object):
|
class GraphicsWidgetAnchor(object):
|
||||||
"""
|
"""
|
||||||
Class used to allow GraphicsWidgets to anchor to a specific position on their
|
Class used to allow GraphicsWidgets to anchor to a specific position on their
|
||||||
parent.
|
parent. The item will be automatically repositioned if the parent is resized.
|
||||||
|
This is used, for example, to anchor a LegendItem to a corner of its parent
|
||||||
|
PlotItem.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@ from pyqtgraph.Qt import QtGui, QtCore
|
|||||||
import pyqtgraph.functions as fn
|
import pyqtgraph.functions as fn
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from .GraphicsWidget import GraphicsWidget
|
from .GraphicsWidget import GraphicsWidget
|
||||||
|
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['LabelItem']
|
__all__ = ['LabelItem']
|
||||||
|
|
||||||
class LabelItem(GraphicsWidget):
|
class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||||
"""
|
"""
|
||||||
GraphicsWidget displaying text.
|
GraphicsWidget displaying text.
|
||||||
Used mainly as axis labels, titles, etc.
|
Used mainly as axis labels, titles, etc.
|
||||||
@ -17,6 +18,7 @@ class LabelItem(GraphicsWidget):
|
|||||||
|
|
||||||
def __init__(self, text=' ', parent=None, angle=0, **args):
|
def __init__(self, text=' ', parent=None, angle=0, **args):
|
||||||
GraphicsWidget.__init__(self, parent)
|
GraphicsWidget.__init__(self, parent)
|
||||||
|
GraphicsWidgetAnchor.__init__(self)
|
||||||
self.item = QtGui.QGraphicsTextItem(self)
|
self.item = QtGui.QGraphicsTextItem(self)
|
||||||
self.opts = {
|
self.opts = {
|
||||||
'color': None,
|
'color': None,
|
||||||
|
@ -84,13 +84,24 @@ class PlotDataItem(GraphicsObject):
|
|||||||
|
|
||||||
**Optimization keyword arguments:**
|
**Optimization keyword arguments:**
|
||||||
|
|
||||||
========== =====================================================================
|
============ =====================================================================
|
||||||
antialias (bool) By default, antialiasing is disabled to improve performance.
|
antialias (bool) By default, antialiasing is disabled to improve performance.
|
||||||
Note that in some cases (in particluar, when pxMode=True), points
|
Note that in some cases (in particluar, when pxMode=True), points
|
||||||
will be rendered antialiased even if this is set to False.
|
will be rendered antialiased even if this is set to False.
|
||||||
|
decimate (int) Sub-sample data by selecting every nth sample before plotting
|
||||||
|
onlyVisible (bool) If True, only plot data that is visible within the X range of
|
||||||
|
the containing ViewBox. This can improve performance when plotting
|
||||||
|
very large data sets where only a fraction of the data is visible
|
||||||
|
at any time.
|
||||||
|
autoResample (bool) If True, resample the data before plotting to avoid plotting
|
||||||
|
multiple line segments per pixel. This can improve performance when
|
||||||
|
viewing very high-density data, but increases the initial overhead
|
||||||
|
and memory usage.
|
||||||
|
sampleRate (float) The sample rate of the data along the X axis (for data with
|
||||||
|
a fixed sample rate). Providing this value improves performance of
|
||||||
|
the *onlyVisible* and *autoResample* options.
|
||||||
identical *deprecated*
|
identical *deprecated*
|
||||||
decimate (int) sub-sample data by selecting every nth sample before plotting
|
============ =====================================================================
|
||||||
========== =====================================================================
|
|
||||||
|
|
||||||
**Meta-info keyword arguments:**
|
**Meta-info keyword arguments:**
|
||||||
|
|
||||||
|
@ -1,50 +1,104 @@
|
|||||||
from pyqtgraph.Qt import QtGui, QtCore
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
from .UIGraphicsItem import *
|
from .GraphicsObject import *
|
||||||
|
from .GraphicsWidgetAnchor import *
|
||||||
|
from .TextItem import TextItem
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph.functions as fn
|
import pyqtgraph.functions as fn
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
__all__ = ['ScaleBar']
|
__all__ = ['ScaleBar']
|
||||||
class ScaleBar(UIGraphicsItem):
|
|
||||||
|
class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
|
||||||
"""
|
"""
|
||||||
Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
|
Displays a rectangular bar to indicate the relative scale of objects on the view.
|
||||||
"""
|
"""
|
||||||
def __init__(self, size, width=5, color=(100, 100, 255)):
|
def __init__(self, size, width=5, brush=None, pen=None, suffix='m'):
|
||||||
UIGraphicsItem.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
|
GraphicsWidgetAnchor.__init__(self)
|
||||||
|
self.setFlag(self.ItemHasNoContents)
|
||||||
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
||||||
|
|
||||||
self.brush = fn.mkBrush(color)
|
if brush is None:
|
||||||
self.pen = fn.mkPen((0,0,0))
|
brush = pg.getConfigOption('foreground')
|
||||||
|
self.brush = fn.mkBrush(brush)
|
||||||
|
self.pen = fn.mkPen(pen)
|
||||||
self._width = width
|
self._width = width
|
||||||
self.size = size
|
self.size = size
|
||||||
|
|
||||||
def paint(self, p, opt, widget):
|
self.bar = QtGui.QGraphicsRectItem()
|
||||||
UIGraphicsItem.paint(self, p, opt, widget)
|
self.bar.setPen(self.pen)
|
||||||
|
self.bar.setBrush(self.brush)
|
||||||
|
self.bar.setParentItem(self)
|
||||||
|
|
||||||
rect = self.boundingRect()
|
self.text = TextItem(text=fn.siFormat(size, suffix=suffix), anchor=(0.5,1))
|
||||||
unit = self.pixelSize()
|
self.text.setParentItem(self)
|
||||||
y = rect.top() + (rect.bottom()-rect.top()) * 0.02
|
|
||||||
y1 = y + unit[1]*self._width
|
|
||||||
x = rect.right() + (rect.left()-rect.right()) * 0.02
|
|
||||||
x1 = x - self.size
|
|
||||||
|
|
||||||
p.setPen(self.pen)
|
def parentChanged(self):
|
||||||
p.setBrush(self.brush)
|
view = self.parentItem()
|
||||||
rect = QtCore.QRectF(
|
if view is None:
|
||||||
QtCore.QPointF(x1, y1),
|
return
|
||||||
QtCore.QPointF(x, y)
|
view.sigRangeChanged.connect(self.updateBar)
|
||||||
)
|
self.updateBar()
|
||||||
p.translate(x1, y1)
|
|
||||||
p.scale(rect.width(), rect.height())
|
|
||||||
p.drawRect(0, 0, 1, 1)
|
|
||||||
|
|
||||||
alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
|
|
||||||
p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
|
|
||||||
for i in range(1, 10):
|
|
||||||
#x2 = x + (x1-x) * 0.1 * i
|
|
||||||
x2 = 0.1 * i
|
|
||||||
p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
|
|
||||||
|
|
||||||
|
|
||||||
def setSize(self, s):
|
def updateBar(self):
|
||||||
self.size = s
|
view = self.parentItem()
|
||||||
|
if view is None:
|
||||||
|
return
|
||||||
|
p1 = view.mapFromViewToItem(self, QtCore.QPointF(0,0))
|
||||||
|
p2 = view.mapFromViewToItem(self, QtCore.QPointF(self.size,0))
|
||||||
|
w = (p2-p1).x()
|
||||||
|
self.bar.setRect(QtCore.QRectF(-w, 0, w, self._width))
|
||||||
|
self.text.setPos(-w/2., 0)
|
||||||
|
|
||||||
|
def boundingRect(self):
|
||||||
|
return QtCore.QRectF()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#class ScaleBar(UIGraphicsItem):
|
||||||
|
#"""
|
||||||
|
#Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
|
||||||
|
#"""
|
||||||
|
#def __init__(self, size, width=5, color=(100, 100, 255)):
|
||||||
|
#UIGraphicsItem.__init__(self)
|
||||||
|
#self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
||||||
|
|
||||||
|
#self.brush = fn.mkBrush(color)
|
||||||
|
#self.pen = fn.mkPen((0,0,0))
|
||||||
|
#self._width = width
|
||||||
|
#self.size = size
|
||||||
|
|
||||||
|
#def paint(self, p, opt, widget):
|
||||||
|
#UIGraphicsItem.paint(self, p, opt, widget)
|
||||||
|
|
||||||
|
#rect = self.boundingRect()
|
||||||
|
#unit = self.pixelSize()
|
||||||
|
#y = rect.top() + (rect.bottom()-rect.top()) * 0.02
|
||||||
|
#y1 = y + unit[1]*self._width
|
||||||
|
#x = rect.right() + (rect.left()-rect.right()) * 0.02
|
||||||
|
#x1 = x - self.size
|
||||||
|
|
||||||
|
#p.setPen(self.pen)
|
||||||
|
#p.setBrush(self.brush)
|
||||||
|
#rect = QtCore.QRectF(
|
||||||
|
#QtCore.QPointF(x1, y1),
|
||||||
|
#QtCore.QPointF(x, y)
|
||||||
|
#)
|
||||||
|
#p.translate(x1, y1)
|
||||||
|
#p.scale(rect.width(), rect.height())
|
||||||
|
#p.drawRect(0, 0, 1, 1)
|
||||||
|
|
||||||
|
#alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
|
||||||
|
#p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
|
||||||
|
#for i in range(1, 10):
|
||||||
|
##x2 = x + (x1-x) * 0.1 * i
|
||||||
|
#x2 = 0.1 * i
|
||||||
|
#p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
|
||||||
|
|
||||||
|
|
||||||
|
#def setSize(self, s):
|
||||||
|
#self.size = s
|
||||||
|
|
||||||
|
@ -677,15 +677,12 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
pts[1] = self.data['y']
|
pts[1] = self.data['y']
|
||||||
pts = fn.transformCoordinates(tr, pts)
|
pts = fn.transformCoordinates(tr, pts)
|
||||||
self.fragments = []
|
self.fragments = []
|
||||||
pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault.
|
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
||||||
## Still won't be able to render correctly, though.
|
## Still won't be able to render correctly, though.
|
||||||
for i in xrange(len(self.data)):
|
for i in xrange(len(self.data)):
|
||||||
rec = self.data[i]
|
rec = self.data[i]
|
||||||
pos = QtCore.QPointF(pts[0,i], pts[1,i])
|
pos = QtCore.QPointF(pts[0,i], pts[1,i])
|
||||||
x,y,w,h = rec['fragCoords']
|
x,y,w,h = rec['fragCoords']
|
||||||
if abs(w) > 10000 or abs(h) > 10000:
|
|
||||||
print self.data
|
|
||||||
raise Exception("fragment corrupt")
|
|
||||||
rect = QtCore.QRectF(y, x, h, w)
|
rect = QtCore.QRectF(y, x, h, w)
|
||||||
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
||||||
|
|
||||||
@ -743,6 +740,7 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
drawSymbol(p2, *self.getSpotOpts(rec, scale))
|
drawSymbol(p2, *self.getSpotOpts(rec, scale))
|
||||||
p2.end()
|
p2.end()
|
||||||
|
|
||||||
|
p.setRenderHint(p.Antialiasing, aa)
|
||||||
self.picture.play(p)
|
self.picture.play(p)
|
||||||
|
|
||||||
def points(self):
|
def points(self):
|
||||||
|
@ -72,7 +72,8 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
|||||||
(see *values* option).
|
(see *values* option).
|
||||||
units String indicating the units of the data for this field.
|
units String indicating the units of the data for this field.
|
||||||
values List of unique values for which the user may assign a
|
values List of unique values for which the user may assign a
|
||||||
color when mode=='enum'.
|
color when mode=='enum'. Optionally may specify a dict
|
||||||
|
instead {value: name}.
|
||||||
============== ============================================================
|
============== ============================================================
|
||||||
"""
|
"""
|
||||||
self.fields = OrderedDict(fields)
|
self.fields = OrderedDict(fields)
|
||||||
@ -168,7 +169,16 @@ class EnumColorMapItem(ptree.types.GroupParameter):
|
|||||||
def __init__(self, name, opts):
|
def __init__(self, name, opts):
|
||||||
self.fieldName = name
|
self.fieldName = name
|
||||||
vals = opts.get('values', [])
|
vals = opts.get('values', [])
|
||||||
|
if isinstance(vals, list):
|
||||||
|
vals = OrderedDict([(v,str(v)) for v in vals])
|
||||||
childs = [{'name': v, 'type': 'color'} for v in vals]
|
childs = [{'name': v, 'type': 'color'} for v in vals]
|
||||||
|
|
||||||
|
childs = []
|
||||||
|
for val,vname in vals.items():
|
||||||
|
ch = ptree.Parameter.create(name=vname, type='color')
|
||||||
|
ch.maskValue = val
|
||||||
|
childs.append(ch)
|
||||||
|
|
||||||
ptree.types.GroupParameter.__init__(self,
|
ptree.types.GroupParameter.__init__(self,
|
||||||
name=name, autoIncrementName=True, removable=True, renamable=True,
|
name=name, autoIncrementName=True, removable=True, renamable=True,
|
||||||
children=[
|
children=[
|
||||||
@ -191,8 +201,7 @@ class EnumColorMapItem(ptree.types.GroupParameter):
|
|||||||
colors[:] = default
|
colors[:] = default
|
||||||
|
|
||||||
for v in self.param('Values'):
|
for v in self.param('Values'):
|
||||||
n = v.name()
|
mask = data == v.maskValue
|
||||||
mask = data == n
|
|
||||||
c = np.array(fn.colorTuple(v.value())) / 255.
|
c = np.array(fn.colorTuple(v.value())) / 255.
|
||||||
colors[mask] = c
|
colors[mask] = c
|
||||||
#scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
|
#scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
|
||||||
|
@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore
|
|||||||
import pyqtgraph.parametertree as ptree
|
import pyqtgraph.parametertree as ptree
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pyqtgraph.pgcollections import OrderedDict
|
from pyqtgraph.pgcollections import OrderedDict
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
__all__ = ['DataFilterWidget']
|
__all__ = ['DataFilterWidget']
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ class DataFilterWidget(ptree.ParameterTree):
|
|||||||
|
|
||||||
self.setFields = self.params.setFields
|
self.setFields = self.params.setFields
|
||||||
self.filterData = self.params.filterData
|
self.filterData = self.params.filterData
|
||||||
|
self.describe = self.params.describe
|
||||||
|
|
||||||
def filterChanged(self):
|
def filterChanged(self):
|
||||||
self.sigFilterChanged.emit(self)
|
self.sigFilterChanged.emit(self)
|
||||||
@ -70,7 +72,7 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
|||||||
for fp in self:
|
for fp in self:
|
||||||
if fp.value() is False:
|
if fp.value() is False:
|
||||||
continue
|
continue
|
||||||
mask &= fp.generateMask(data)
|
mask &= fp.generateMask(data, mask.copy())
|
||||||
#key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
|
#key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
|
||||||
|
|
||||||
#vals = data[key]
|
#vals = data[key]
|
||||||
@ -78,10 +80,20 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
|||||||
#mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
|
#mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
"""Return a list of strings describing the currently enabled filters."""
|
||||||
|
desc = []
|
||||||
|
for fp in self:
|
||||||
|
if fp.value() is False:
|
||||||
|
continue
|
||||||
|
desc.append(fp.describe())
|
||||||
|
return desc
|
||||||
|
|
||||||
class RangeFilterItem(ptree.types.SimpleParameter):
|
class RangeFilterItem(ptree.types.SimpleParameter):
|
||||||
def __init__(self, name, opts):
|
def __init__(self, name, opts):
|
||||||
self.fieldName = name
|
self.fieldName = name
|
||||||
units = opts.get('units', '')
|
units = opts.get('units', '')
|
||||||
|
self.units = units
|
||||||
ptree.types.SimpleParameter.__init__(self,
|
ptree.types.SimpleParameter.__init__(self,
|
||||||
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
|
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
|
||||||
children=[
|
children=[
|
||||||
@ -90,26 +102,49 @@ class RangeFilterItem(ptree.types.SimpleParameter):
|
|||||||
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
|
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
|
||||||
])
|
])
|
||||||
|
|
||||||
def generateMask(self, data):
|
def generateMask(self, data, mask):
|
||||||
vals = data[self.fieldName]
|
vals = data[self.fieldName][mask]
|
||||||
return (vals >= mn) & (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
|
mask[mask] = (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
|
||||||
|
return mask
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
return "%s < %s < %s" % (pg.siFormat(self['Min'], suffix=self.units), self.fieldName, pg.siFormat(self['Max'], suffix=self.units))
|
||||||
|
|
||||||
class EnumFilterItem(ptree.types.SimpleParameter):
|
class EnumFilterItem(ptree.types.SimpleParameter):
|
||||||
def __init__(self, name, opts):
|
def __init__(self, name, opts):
|
||||||
self.fieldName = name
|
self.fieldName = name
|
||||||
vals = opts.get('values', [])
|
vals = opts.get('values', [])
|
||||||
childs = [{'name': v, 'type': 'bool', 'value': True} for v in vals]
|
childs = []
|
||||||
|
if isinstance(vals, list):
|
||||||
|
vals = OrderedDict([(v,str(v)) for v in vals])
|
||||||
|
for val,vname in vals.items():
|
||||||
|
ch = ptree.Parameter.create(name=vname, type='bool', value=True)
|
||||||
|
ch.maskValue = val
|
||||||
|
childs.append(ch)
|
||||||
|
ch = ptree.Parameter.create(name='(other)', type='bool', value=True)
|
||||||
|
ch.maskValue = '__other__'
|
||||||
|
childs.append(ch)
|
||||||
|
|
||||||
ptree.types.SimpleParameter.__init__(self,
|
ptree.types.SimpleParameter.__init__(self,
|
||||||
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
|
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
|
||||||
children=childs)
|
children=childs)
|
||||||
|
|
||||||
def generateMask(self, data):
|
def generateMask(self, data, startMask):
|
||||||
vals = data[self.fieldName]
|
vals = data[self.fieldName][startMask]
|
||||||
mask = np.ones(len(data), dtype=bool)
|
mask = np.ones(len(vals), dtype=bool)
|
||||||
|
otherMask = np.ones(len(vals), dtype=bool)
|
||||||
for c in self:
|
for c in self:
|
||||||
if c.value() is True:
|
key = c.maskValue
|
||||||
continue
|
if key == '__other__':
|
||||||
key = c.name()
|
m = ~otherMask
|
||||||
mask &= vals != key
|
else:
|
||||||
return mask
|
m = vals != key
|
||||||
|
otherMask &= m
|
||||||
|
if c.value() is False:
|
||||||
|
mask &= m
|
||||||
|
startMask[startMask] = mask
|
||||||
|
return startMask
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
vals = [ch.name() for ch in self if ch.value() is True]
|
||||||
|
return "%s: %s" % (self.fieldName, ', '.join(vals))
|
@ -6,6 +6,7 @@ import pyqtgraph.parametertree as ptree
|
|||||||
import pyqtgraph.functions as fn
|
import pyqtgraph.functions as fn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pyqtgraph.pgcollections import OrderedDict
|
from pyqtgraph.pgcollections import OrderedDict
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
__all__ = ['ScatterPlotWidget']
|
__all__ = ['ScatterPlotWidget']
|
||||||
|
|
||||||
@ -47,14 +48,22 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
self.ctrlPanel.addWidget(self.ptree)
|
self.ctrlPanel.addWidget(self.ptree)
|
||||||
self.addWidget(self.plot)
|
self.addWidget(self.plot)
|
||||||
|
|
||||||
|
bg = pg.mkColor(pg.getConfigOption('background'))
|
||||||
|
bg.setAlpha(150)
|
||||||
|
self.filterText = pg.TextItem(border=pg.getConfigOption('foreground'), color=bg)
|
||||||
|
self.filterText.setPos(60,20)
|
||||||
|
self.filterText.setParentItem(self.plot.plotItem)
|
||||||
|
|
||||||
self.data = None
|
self.data = None
|
||||||
|
self.mouseOverField = None
|
||||||
|
self.scatterPlot = None
|
||||||
self.style = dict(pen=None, symbol='o')
|
self.style = dict(pen=None, symbol='o')
|
||||||
|
|
||||||
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
|
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
|
||||||
self.filter.sigFilterChanged.connect(self.filterChanged)
|
self.filter.sigFilterChanged.connect(self.filterChanged)
|
||||||
self.colorMap.sigColorMapChanged.connect(self.updatePlot)
|
self.colorMap.sigColorMapChanged.connect(self.updatePlot)
|
||||||
|
|
||||||
def setFields(self, fields):
|
def setFields(self, fields, mouseOverField=None):
|
||||||
"""
|
"""
|
||||||
Set the list of field names/units to be processed.
|
Set the list of field names/units to be processed.
|
||||||
|
|
||||||
@ -62,6 +71,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
:func:`ColorMapWidget.setFields <pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields>`
|
:func:`ColorMapWidget.setFields <pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields>`
|
||||||
"""
|
"""
|
||||||
self.fields = OrderedDict(fields)
|
self.fields = OrderedDict(fields)
|
||||||
|
self.mouseOverField = mouseOverField
|
||||||
self.fieldList.clear()
|
self.fieldList.clear()
|
||||||
for f,opts in fields:
|
for f,opts in fields:
|
||||||
item = QtGui.QListWidgetItem(f)
|
item = QtGui.QListWidgetItem(f)
|
||||||
@ -94,6 +104,13 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
def filterChanged(self, f):
|
def filterChanged(self, f):
|
||||||
self.filtered = None
|
self.filtered = None
|
||||||
self.updatePlot()
|
self.updatePlot()
|
||||||
|
desc = self.filter.describe()
|
||||||
|
if len(desc) == 0:
|
||||||
|
self.filterText.setVisible(False)
|
||||||
|
else:
|
||||||
|
self.filterText.setText('\n'.join(desc))
|
||||||
|
self.filterText.setVisible(True)
|
||||||
|
|
||||||
|
|
||||||
def updatePlot(self):
|
def updatePlot(self):
|
||||||
self.plot.clear()
|
self.plot.clear()
|
||||||
@ -122,64 +139,73 @@ class ScatterPlotWidget(QtGui.QSplitter):
|
|||||||
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
|
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
x = data[sel[0]]
|
#x = data[sel[0]]
|
||||||
#if x.dtype.kind == 'f':
|
#y = None
|
||||||
#mask = ~np.isnan(x)
|
xy = [data[sel[0]], None]
|
||||||
#else:
|
|
||||||
#mask = np.ones(len(x), dtype=bool)
|
|
||||||
#x = x[mask]
|
|
||||||
#style['symbolBrush'] = colors[mask]
|
|
||||||
y = None
|
|
||||||
elif len(sel) == 2:
|
elif len(sel) == 2:
|
||||||
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
|
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
|
||||||
if len(data) == 0:
|
if len(data) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
xydata = []
|
xy = [data[sel[0]], data[sel[1]]]
|
||||||
for ax in [0,1]:
|
#xydata = []
|
||||||
d = data[sel[ax]]
|
#for ax in [0,1]:
|
||||||
## scatter catecorical values just a bit so they show up better in the scatter plot.
|
#d = data[sel[ax]]
|
||||||
#if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
|
### scatter catecorical values just a bit so they show up better in the scatter plot.
|
||||||
#d += np.random.normal(size=len(cells), scale=0.1)
|
##if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
|
||||||
xydata.append(d)
|
##d += np.random.normal(size=len(cells), scale=0.1)
|
||||||
x,y = xydata
|
|
||||||
#mask = np.ones(len(x), dtype=bool)
|
#xydata.append(d)
|
||||||
#if x.dtype.kind == 'f':
|
#x,y = xydata
|
||||||
#mask |= ~np.isnan(x)
|
|
||||||
#if y.dtype.kind == 'f':
|
|
||||||
#mask |= ~np.isnan(y)
|
|
||||||
#x = x[mask]
|
|
||||||
#y = y[mask]
|
|
||||||
#style['symbolBrush'] = colors[mask]
|
|
||||||
|
|
||||||
## convert enum-type fields to float, set axis labels
|
## convert enum-type fields to float, set axis labels
|
||||||
xy = [x,y]
|
enum = [False, False]
|
||||||
for i in [0,1]:
|
for i in [0,1]:
|
||||||
axis = self.plot.getAxis(['bottom', 'left'][i])
|
axis = self.plot.getAxis(['bottom', 'left'][i])
|
||||||
if xy[i] is not None and xy[i].dtype.kind in ('S', 'O'):
|
if xy[i] is not None and (self.fields[sel[i]].get('mode', None) == 'enum' or xy[i].dtype.kind in ('S', 'O')):
|
||||||
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
|
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
|
||||||
xy[i] = np.array([vals.index(x) if x in vals else None for x in xy[i]], dtype=float)
|
xy[i] = np.array([vals.index(x) if x in vals else len(vals) for x in xy[i]], dtype=float)
|
||||||
axis.setTicks([list(enumerate(vals))])
|
axis.setTicks([list(enumerate(vals))])
|
||||||
|
enum[i] = True
|
||||||
else:
|
else:
|
||||||
axis.setTicks(None) # reset to automatic ticking
|
axis.setTicks(None) # reset to automatic ticking
|
||||||
x,y = xy
|
|
||||||
|
|
||||||
## mask out any nan values
|
## mask out any nan values
|
||||||
mask = np.ones(len(x), dtype=bool)
|
mask = np.ones(len(xy[0]), dtype=bool)
|
||||||
if x.dtype.kind == 'f':
|
if xy[0].dtype.kind == 'f':
|
||||||
mask &= ~np.isnan(x)
|
mask &= ~np.isnan(xy[0])
|
||||||
if y is not None and y.dtype.kind == 'f':
|
if xy[1] is not None and xy[1].dtype.kind == 'f':
|
||||||
mask &= ~np.isnan(y)
|
mask &= ~np.isnan(xy[1])
|
||||||
x = x[mask]
|
|
||||||
|
xy[0] = xy[0][mask]
|
||||||
style['symbolBrush'] = colors[mask]
|
style['symbolBrush'] = colors[mask]
|
||||||
|
|
||||||
## Scatter y-values for a histogram-like appearance
|
## Scatter y-values for a histogram-like appearance
|
||||||
if y is None:
|
if xy[1] is None:
|
||||||
y = fn.pseudoScatter(x)
|
## column scatter plot
|
||||||
|
xy[1] = fn.pseudoScatter(xy[0])
|
||||||
else:
|
else:
|
||||||
y = y[mask]
|
## beeswarm plots
|
||||||
|
xy[1] = xy[1][mask]
|
||||||
|
for ax in [0,1]:
|
||||||
|
if not enum[ax]:
|
||||||
|
continue
|
||||||
|
for i in range(int(xy[ax].max())+1):
|
||||||
|
keymask = xy[ax] == i
|
||||||
|
scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True)
|
||||||
|
scatter *= 0.2 / np.abs(scatter).max()
|
||||||
|
xy[ax][keymask] += scatter
|
||||||
|
|
||||||
|
if self.scatterPlot is not None:
|
||||||
|
try:
|
||||||
|
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style)
|
||||||
|
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
|
||||||
|
|
||||||
|
|
||||||
self.plot.plot(x, y, **style)
|
def plotClicked(self, plot, points):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user