Merge branch 'develop' into pyqtgraph-core
This commit is contained in:
commit
f613d33c49
|
@ -48,8 +48,8 @@ else:
|
||||||
CONFIG_OPTIONS = {
|
CONFIG_OPTIONS = {
|
||||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||||
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
||||||
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
|
'foreground': 'd', ## default foreground color for axes, labels, etc.
|
||||||
'background': (0, 0, 0), ## default background for GraphicsWidget
|
'background': 'k', ## default background for GraphicsWidget
|
||||||
'antialias': False,
|
'antialias': False,
|
||||||
'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
|
||||||
|
|
|
@ -58,14 +58,15 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
|
||||||
|
|
||||||
|
|
||||||
class Flowchart(Node):
|
class Flowchart(Node):
|
||||||
|
|
||||||
sigFileLoaded = QtCore.Signal(object)
|
sigFileLoaded = QtCore.Signal(object)
|
||||||
sigFileSaved = QtCore.Signal(object)
|
sigFileSaved = QtCore.Signal(object)
|
||||||
|
|
||||||
|
|
||||||
#sigOutputChanged = QtCore.Signal() ## inherited from Node
|
#sigOutputChanged = QtCore.Signal() ## inherited from Node
|
||||||
sigChartLoaded = QtCore.Signal()
|
sigChartLoaded = QtCore.Signal()
|
||||||
sigStateChanged = QtCore.Signal()
|
sigStateChanged = QtCore.Signal() # called when output is expected to have changed
|
||||||
|
sigChartChanged = QtCore.Signal(object, object, object) # called when nodes are added, removed, or renamed.
|
||||||
|
# (self, action, node)
|
||||||
|
|
||||||
def __init__(self, terminals=None, name=None, filePath=None, library=None):
|
def __init__(self, terminals=None, name=None, filePath=None, library=None):
|
||||||
self.library = library or LIBRARY
|
self.library = library or LIBRARY
|
||||||
|
@ -218,6 +219,7 @@ class Flowchart(Node):
|
||||||
node.sigClosed.connect(self.nodeClosed)
|
node.sigClosed.connect(self.nodeClosed)
|
||||||
node.sigRenamed.connect(self.nodeRenamed)
|
node.sigRenamed.connect(self.nodeRenamed)
|
||||||
node.sigOutputChanged.connect(self.nodeOutputChanged)
|
node.sigOutputChanged.connect(self.nodeOutputChanged)
|
||||||
|
self.sigChartChanged.emit(self, 'add', node)
|
||||||
|
|
||||||
def removeNode(self, node):
|
def removeNode(self, node):
|
||||||
node.close()
|
node.close()
|
||||||
|
@ -237,11 +239,13 @@ class Flowchart(Node):
|
||||||
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
|
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
|
self.sigChartChanged.emit(self, 'remove', node)
|
||||||
|
|
||||||
def nodeRenamed(self, node, oldName):
|
def nodeRenamed(self, node, oldName):
|
||||||
del self._nodes[oldName]
|
del self._nodes[oldName]
|
||||||
self._nodes[node.name()] = node
|
self._nodes[node.name()] = node
|
||||||
self.widget().nodeRenamed(node, oldName)
|
self.widget().nodeRenamed(node, oldName)
|
||||||
|
self.sigChartChanged.emit(self, 'rename', node)
|
||||||
|
|
||||||
def arrangeNodes(self):
|
def arrangeNodes(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -4,7 +4,7 @@ import weakref
|
||||||
from ...Qt import QtCore, QtGui
|
from ...Qt import QtCore, QtGui
|
||||||
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
|
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
|
||||||
from ...graphicsItems.PlotCurveItem import PlotCurveItem
|
from ...graphicsItems.PlotCurveItem import PlotCurveItem
|
||||||
from ... import PlotDataItem
|
from ... import PlotDataItem, ComboBox
|
||||||
|
|
||||||
from .common import *
|
from .common import *
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -16,7 +16,9 @@ class PlotWidgetNode(Node):
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}})
|
Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}})
|
||||||
self.plot = None
|
self.plot = None # currently selected plot
|
||||||
|
self.plots = {} # list of available plots user may select from
|
||||||
|
self.ui = None
|
||||||
self.items = {}
|
self.items = {}
|
||||||
|
|
||||||
def disconnected(self, localTerm, remoteTerm):
|
def disconnected(self, localTerm, remoteTerm):
|
||||||
|
@ -26,16 +28,27 @@ class PlotWidgetNode(Node):
|
||||||
|
|
||||||
def setPlot(self, plot):
|
def setPlot(self, plot):
|
||||||
#print "======set plot"
|
#print "======set plot"
|
||||||
|
if plot == self.plot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# clear data from previous plot
|
||||||
|
if self.plot is not None:
|
||||||
|
for vid in list(self.items.keys()):
|
||||||
|
self.plot.removeItem(self.items[vid])
|
||||||
|
del self.items[vid]
|
||||||
|
|
||||||
self.plot = plot
|
self.plot = plot
|
||||||
|
self.updateUi()
|
||||||
|
self.update()
|
||||||
self.sigPlotChanged.emit(self)
|
self.sigPlotChanged.emit(self)
|
||||||
|
|
||||||
def getPlot(self):
|
def getPlot(self):
|
||||||
return self.plot
|
return self.plot
|
||||||
|
|
||||||
def process(self, In, display=True):
|
def process(self, In, display=True):
|
||||||
if display:
|
if display and self.plot is not None:
|
||||||
#self.plot.clearPlots()
|
|
||||||
items = set()
|
items = set()
|
||||||
|
# Add all new input items to selected plot
|
||||||
for name, vals in In.items():
|
for name, vals in In.items():
|
||||||
if vals is None:
|
if vals is None:
|
||||||
continue
|
continue
|
||||||
|
@ -45,14 +58,13 @@ class PlotWidgetNode(Node):
|
||||||
for val in vals:
|
for val in vals:
|
||||||
vid = id(val)
|
vid = id(val)
|
||||||
if vid in self.items and self.items[vid].scene() is self.plot.scene():
|
if vid in self.items and self.items[vid].scene() is self.plot.scene():
|
||||||
|
# Item is already added to the correct scene
|
||||||
|
# possible bug: what if two plots occupy the same scene? (should
|
||||||
|
# rarely be a problem because items are removed from a plot before
|
||||||
|
# switching).
|
||||||
items.add(vid)
|
items.add(vid)
|
||||||
else:
|
else:
|
||||||
#if isinstance(val, PlotCurveItem):
|
# Add the item to the plot, or generate a new item if needed.
|
||||||
#self.plot.addItem(val)
|
|
||||||
#item = val
|
|
||||||
#if isinstance(val, ScatterPlotItem):
|
|
||||||
#self.plot.addItem(val)
|
|
||||||
#item = val
|
|
||||||
if isinstance(val, QtGui.QGraphicsItem):
|
if isinstance(val, QtGui.QGraphicsItem):
|
||||||
self.plot.addItem(val)
|
self.plot.addItem(val)
|
||||||
item = val
|
item = val
|
||||||
|
@ -60,21 +72,47 @@ class PlotWidgetNode(Node):
|
||||||
item = self.plot.plot(val)
|
item = self.plot.plot(val)
|
||||||
self.items[vid] = item
|
self.items[vid] = item
|
||||||
items.add(vid)
|
items.add(vid)
|
||||||
|
|
||||||
|
# Any left-over items that did not appear in the input must be removed
|
||||||
for vid in list(self.items.keys()):
|
for vid in list(self.items.keys()):
|
||||||
if vid not in items:
|
if vid not in items:
|
||||||
#print "remove", self.items[vid]
|
|
||||||
self.plot.removeItem(self.items[vid])
|
self.plot.removeItem(self.items[vid])
|
||||||
del self.items[vid]
|
del self.items[vid]
|
||||||
|
|
||||||
def processBypassed(self, args):
|
def processBypassed(self, args):
|
||||||
|
if self.plot is None:
|
||||||
|
return
|
||||||
for item in list(self.items.values()):
|
for item in list(self.items.values()):
|
||||||
self.plot.removeItem(item)
|
self.plot.removeItem(item)
|
||||||
self.items = {}
|
self.items = {}
|
||||||
|
|
||||||
#def setInput(self, **args):
|
def ctrlWidget(self):
|
||||||
#for k in args:
|
if self.ui is None:
|
||||||
#self.plot.plot(args[k])
|
self.ui = ComboBox()
|
||||||
|
self.ui.currentIndexChanged.connect(self.plotSelected)
|
||||||
|
self.updateUi()
|
||||||
|
return self.ui
|
||||||
|
|
||||||
|
def plotSelected(self, index):
|
||||||
|
self.setPlot(self.ui.value())
|
||||||
|
|
||||||
|
def setPlotList(self, plots):
|
||||||
|
"""
|
||||||
|
Specify the set of plots (PlotWidget or PlotItem) that the user may
|
||||||
|
select from.
|
||||||
|
|
||||||
|
*plots* must be a dictionary of {name: plot} pairs.
|
||||||
|
"""
|
||||||
|
self.plots = plots
|
||||||
|
self.updateUi()
|
||||||
|
|
||||||
|
def updateUi(self):
|
||||||
|
# sets list and automatically preserves previous selection
|
||||||
|
self.ui.setItems(self.plots)
|
||||||
|
try:
|
||||||
|
self.ui.setValue(self.plot)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CanvasNode(Node):
|
class CanvasNode(Node):
|
||||||
|
|
44
functions.py
44
functions.py
|
@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from .python2_3 import asUnicode
|
from .python2_3 import asUnicode
|
||||||
|
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||||
Colors = {
|
Colors = {
|
||||||
'b': (0,0,255,255),
|
'b': QtGui.QColor(0,0,255,255),
|
||||||
'g': (0,255,0,255),
|
'g': QtGui.QColor(0,255,0,255),
|
||||||
'r': (255,0,0,255),
|
'r': QtGui.QColor(255,0,0,255),
|
||||||
'c': (0,255,255,255),
|
'c': QtGui.QColor(0,255,255,255),
|
||||||
'm': (255,0,255,255),
|
'm': QtGui.QColor(255,0,255,255),
|
||||||
'y': (255,255,0,255),
|
'y': QtGui.QColor(255,255,0,255),
|
||||||
'k': (0,0,0,255),
|
'k': QtGui.QColor(0,0,0,255),
|
||||||
'w': (255,255,255,255),
|
'w': QtGui.QColor(255,255,255,255),
|
||||||
|
'd': QtGui.QColor(150,150,150,255),
|
||||||
|
'l': QtGui.QColor(200,200,200,255),
|
||||||
|
's': QtGui.QColor(100,100,150,255),
|
||||||
}
|
}
|
||||||
|
|
||||||
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
|
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
|
||||||
|
@ -168,17 +172,15 @@ def mkColor(*args):
|
||||||
"""
|
"""
|
||||||
err = 'Not sure how to make a color from "%s"' % str(args)
|
err = 'Not sure how to make a color from "%s"' % str(args)
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
if isinstance(args[0], QtGui.QColor):
|
if isinstance(args[0], basestring):
|
||||||
return QtGui.QColor(args[0])
|
|
||||||
elif isinstance(args[0], float):
|
|
||||||
r = g = b = int(args[0] * 255)
|
|
||||||
a = 255
|
|
||||||
elif isinstance(args[0], basestring):
|
|
||||||
c = args[0]
|
c = args[0]
|
||||||
if c[0] == '#':
|
if c[0] == '#':
|
||||||
c = c[1:]
|
c = c[1:]
|
||||||
if len(c) == 1:
|
if len(c) == 1:
|
||||||
(r, g, b, a) = Colors[c]
|
try:
|
||||||
|
return Colors[c]
|
||||||
|
except KeyError:
|
||||||
|
raise Exception('No color named "%s"' % c)
|
||||||
if len(c) == 3:
|
if len(c) == 3:
|
||||||
r = int(c[0]*2, 16)
|
r = int(c[0]*2, 16)
|
||||||
g = int(c[1]*2, 16)
|
g = int(c[1]*2, 16)
|
||||||
|
@ -199,6 +201,11 @@ def mkColor(*args):
|
||||||
g = int(c[2:4], 16)
|
g = int(c[2:4], 16)
|
||||||
b = int(c[4:6], 16)
|
b = int(c[4:6], 16)
|
||||||
a = int(c[6:8], 16)
|
a = int(c[6:8], 16)
|
||||||
|
elif isinstance(args[0], QtGui.QColor):
|
||||||
|
return QtGui.QColor(args[0])
|
||||||
|
elif isinstance(args[0], float):
|
||||||
|
r = g = b = int(args[0] * 255)
|
||||||
|
a = 255
|
||||||
elif hasattr(args[0], '__len__'):
|
elif hasattr(args[0], '__len__'):
|
||||||
if len(args[0]) == 3:
|
if len(args[0]) == 3:
|
||||||
(r, g, b) = args[0]
|
(r, g, b) = args[0]
|
||||||
|
@ -282,7 +289,7 @@ def mkPen(*args, **kargs):
|
||||||
color = args
|
color = args
|
||||||
|
|
||||||
if color is None:
|
if color is None:
|
||||||
color = mkColor(200, 200, 200)
|
color = mkColor('l')
|
||||||
if hsv is not None:
|
if hsv is not None:
|
||||||
color = hsvColor(*hsv)
|
color = hsvColor(*hsv)
|
||||||
else:
|
else:
|
||||||
|
@ -821,7 +828,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||||
minVal, maxVal = levels
|
minVal, maxVal = levels
|
||||||
if minVal == maxVal:
|
if minVal == maxVal:
|
||||||
maxVal += 1e-16
|
maxVal += 1e-16
|
||||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
|
if maxVal == minVal:
|
||||||
|
data = rescaleData(data, 1, minVal, dtype=int)
|
||||||
|
else:
|
||||||
|
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
|
||||||
|
|
||||||
profile()
|
profile()
|
||||||
|
|
||||||
|
|
|
@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget):
|
||||||
if pen == None, the default will be used (see :func:`setConfigOption
|
if pen == None, the default will be used (see :func:`setConfigOption
|
||||||
<pyqtgraph.setConfigOption>`)
|
<pyqtgraph.setConfigOption>`)
|
||||||
"""
|
"""
|
||||||
self._pen = pen
|
|
||||||
self.picture = None
|
self.picture = None
|
||||||
if pen is None:
|
if pen is None:
|
||||||
pen = getConfigOption('foreground')
|
pen = getConfigOption('foreground')
|
||||||
self.labelStyle['color'] = '#' + fn.colorStr(fn.mkPen(pen).color())[:6]
|
self._pen = fn.mkPen(pen)
|
||||||
|
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||||
self.setLabel()
|
self.setLabel()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -458,8 +458,7 @@ class AxisItem(GraphicsWidget):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
||||||
pixelSpacing = size / np.log(size)
|
optimalTickCount = max(2., np.log(size))
|
||||||
optimalTickCount = max(2., size / pixelSpacing)
|
|
||||||
|
|
||||||
## optimal minor tick spacing
|
## optimal minor tick spacing
|
||||||
optimalSpacing = dif / optimalTickCount
|
optimalSpacing = dif / optimalTickCount
|
||||||
|
@ -795,7 +794,7 @@ class AxisItem(GraphicsWidget):
|
||||||
if s is None:
|
if s is None:
|
||||||
rects.append(None)
|
rects.append(None)
|
||||||
else:
|
else:
|
||||||
br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s))
|
br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, asUnicode(s))
|
||||||
## boundingRect is usually just a bit too large
|
## boundingRect is usually just a bit too large
|
||||||
## (but this probably depends on per-font metrics?)
|
## (but this probably depends on per-font metrics?)
|
||||||
br.setHeight(br.height() * 0.8)
|
br.setHeight(br.height() * 0.8)
|
||||||
|
@ -830,7 +829,7 @@ class AxisItem(GraphicsWidget):
|
||||||
vstr = strings[j]
|
vstr = strings[j]
|
||||||
if vstr is None: ## this tick was ignored because it is out of bounds
|
if vstr is None: ## this tick was ignored because it is out of bounds
|
||||||
continue
|
continue
|
||||||
vstr = str(vstr)
|
vstr = asUnicode(vstr)
|
||||||
x = tickPositions[i][j]
|
x = tickPositions[i][j]
|
||||||
#textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
|
#textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
|
||||||
textRect = rects[j]
|
textRect = rects[j]
|
||||||
|
|
|
@ -1,24 +1,70 @@
|
||||||
from ..Qt import QtGui
|
from ..Qt import QtGui
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
|
from .PlotDataItem import PlotDataItem
|
||||||
|
from .PlotCurveItem import PlotCurveItem
|
||||||
|
|
||||||
class FillBetweenItem(QtGui.QGraphicsPathItem):
|
class FillBetweenItem(QtGui.QGraphicsPathItem):
|
||||||
"""
|
"""
|
||||||
GraphicsItem filling the space between two PlotDataItems.
|
GraphicsItem filling the space between two PlotDataItems.
|
||||||
"""
|
"""
|
||||||
def __init__(self, p1, p2, brush=None):
|
def __init__(self, curve1=None, curve2=None, brush=None):
|
||||||
QtGui.QGraphicsPathItem.__init__(self)
|
QtGui.QGraphicsPathItem.__init__(self)
|
||||||
self.p1 = p1
|
self.curves = None
|
||||||
self.p2 = p2
|
if curve1 is not None and curve2 is not None:
|
||||||
p1.sigPlotChanged.connect(self.updatePath)
|
self.setCurves(curve1, curve2)
|
||||||
p2.sigPlotChanged.connect(self.updatePath)
|
elif curve1 is not None or curve2 is not None:
|
||||||
|
raise Exception("Must specify two curves to fill between.")
|
||||||
|
|
||||||
if brush is not None:
|
if brush is not None:
|
||||||
self.setBrush(fn.mkBrush(brush))
|
self.setBrush(fn.mkBrush(brush))
|
||||||
self.setZValue(min(p1.zValue(), p2.zValue())-1)
|
self.updatePath()
|
||||||
|
|
||||||
|
def setCurves(self, curve1, curve2):
|
||||||
|
"""Set the curves to fill between.
|
||||||
|
|
||||||
|
Arguments must be instances of PlotDataItem or PlotCurveItem."""
|
||||||
|
|
||||||
|
if self.curves is not None:
|
||||||
|
for c in self.curves:
|
||||||
|
try:
|
||||||
|
c.sigPlotChanged.disconnect(self.curveChanged)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
curves = [curve1, curve2]
|
||||||
|
for c in curves:
|
||||||
|
if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem):
|
||||||
|
raise TypeError("Curves must be PlotDataItem or PlotCurveItem.")
|
||||||
|
self.curves = curves
|
||||||
|
curve1.sigPlotChanged.connect(self.curveChanged)
|
||||||
|
curve2.sigPlotChanged.connect(self.curveChanged)
|
||||||
|
self.setZValue(min(curve1.zValue(), curve2.zValue())-1)
|
||||||
|
self.curveChanged()
|
||||||
|
|
||||||
|
def setBrush(self, *args, **kwds):
|
||||||
|
"""Change the fill brush. Acceps the same arguments as pg.mkBrush()"""
|
||||||
|
QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds))
|
||||||
|
|
||||||
|
def curveChanged(self):
|
||||||
self.updatePath()
|
self.updatePath()
|
||||||
|
|
||||||
def updatePath(self):
|
def updatePath(self):
|
||||||
p1 = self.p1.curve.path
|
if self.curves is None:
|
||||||
p2 = self.p2.curve.path
|
self.setPath(QtGui.QPainterPath())
|
||||||
|
return
|
||||||
|
paths = []
|
||||||
|
for c in self.curves:
|
||||||
|
if isinstance(c, PlotDataItem):
|
||||||
|
paths.append(c.curve.getPath())
|
||||||
|
elif isinstance(c, PlotCurveItem):
|
||||||
|
paths.append(c.getPath())
|
||||||
|
|
||||||
path = QtGui.QPainterPath()
|
path = QtGui.QPainterPath()
|
||||||
path.addPolygon(p1.toSubpathPolygons()[0] + p2.toReversed().toSubpathPolygons()[0])
|
p1 = paths[0].toSubpathPolygons()
|
||||||
|
p2 = paths[1].toReversed().toSubpathPolygons()
|
||||||
|
if len(p1) == 0 or len(p2) == 0:
|
||||||
|
self.setPath(QtGui.QPainterPath())
|
||||||
|
return
|
||||||
|
|
||||||
|
path.addPolygon(p1[0] + p2[0])
|
||||||
self.setPath(path)
|
self.setPath(path)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import collections
|
import collections
|
||||||
|
@ -287,15 +289,45 @@ class ImageItem(GraphicsObject):
|
||||||
self.render()
|
self.render()
|
||||||
self.qimage.save(fileName, *args)
|
self.qimage.save(fileName, *args)
|
||||||
|
|
||||||
def getHistogram(self, bins=500, step=3):
|
def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
|
||||||
"""Returns x and y arrays containing the histogram values for the current image.
|
"""Returns x and y arrays containing the histogram values for the current image.
|
||||||
The step argument causes pixels to be skipped when computing the histogram to save time.
|
For an explanation of the return format, see numpy.histogram().
|
||||||
|
|
||||||
|
The *step* argument causes pixels to be skipped when computing the histogram to save time.
|
||||||
|
If *step* is 'auto', then a step is chosen such that the analyzed data has
|
||||||
|
dimensions roughly *targetImageSize* for each axis.
|
||||||
|
|
||||||
|
The *bins* argument and any extra keyword arguments are passed to
|
||||||
|
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
||||||
|
chosen based on the image characteristics:
|
||||||
|
|
||||||
|
* Integer images will have approximately *targetHistogramSize* bins,
|
||||||
|
with each bin having an integer width.
|
||||||
|
* All other types will have *targetHistogramSize* bins.
|
||||||
|
|
||||||
This method is also used when automatically computing levels.
|
This method is also used when automatically computing levels.
|
||||||
"""
|
"""
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
return None,None
|
return None,None
|
||||||
stepData = self.image[::step, ::step]
|
if step == 'auto':
|
||||||
hist = np.histogram(stepData, bins=bins)
|
step = (np.ceil(self.image.shape[0] / targetImageSize),
|
||||||
|
np.ceil(self.image.shape[1] / targetImageSize))
|
||||||
|
if np.isscalar(step):
|
||||||
|
step = (step, step)
|
||||||
|
stepData = self.image[::step[0], ::step[1]]
|
||||||
|
|
||||||
|
if bins == 'auto':
|
||||||
|
if stepData.dtype.kind in "ui":
|
||||||
|
mn = stepData.min()
|
||||||
|
mx = stepData.max()
|
||||||
|
step = np.ceil((mx-mn) / 500.)
|
||||||
|
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
||||||
|
else:
|
||||||
|
bins = 500
|
||||||
|
|
||||||
|
kwds['bins'] = bins
|
||||||
|
hist = np.histogram(stepData, **kwds)
|
||||||
|
|
||||||
return hist[1][:-1], hist[0]
|
return hist[1][:-1], hist[0]
|
||||||
|
|
||||||
def setPxMode(self, b):
|
def setPxMode(self, b):
|
||||||
|
|
|
@ -3,7 +3,7 @@ from .LabelItem import LabelItem
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
from ..Point import Point
|
from ..Point import Point
|
||||||
from .ScatterPlotItem import ScatterPlotItem
|
from .ScatterPlotItem import ScatterPlotItem, drawSymbol
|
||||||
from .PlotDataItem import PlotDataItem
|
from .PlotDataItem import PlotDataItem
|
||||||
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
|
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
|
||||||
__all__ = ['LegendItem']
|
__all__ = ['LegendItem']
|
||||||
|
@ -167,7 +167,7 @@ class ItemSample(GraphicsWidget):
|
||||||
size = opts['size']
|
size = opts['size']
|
||||||
|
|
||||||
p.translate(10,10)
|
p.translate(10,10)
|
||||||
path = ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush)
|
path = drawSymbol(p, symbol, size, pen, brush)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -393,16 +393,18 @@ class PlotCurveItem(GraphicsObject):
|
||||||
if self.path is None:
|
if self.path is None:
|
||||||
x,y = self.getData()
|
x,y = self.getData()
|
||||||
if x is None or len(x) == 0 or y is None or len(y) == 0:
|
if x is None or len(x) == 0 or y is None or len(y) == 0:
|
||||||
return QtGui.QPainterPath()
|
self.path = QtGui.QPainterPath()
|
||||||
self.path = self.generatePath(*self.getData())
|
else:
|
||||||
|
self.path = self.generatePath(*self.getData())
|
||||||
self.fillPath = None
|
self.fillPath = None
|
||||||
self._mouseShape = None
|
self._mouseShape = None
|
||||||
|
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
@debug.warnOnException ## raising an exception here causes crash
|
@debug.warnOnException ## raising an exception here causes crash
|
||||||
def paint(self, p, opt, widget):
|
def paint(self, p, opt, widget):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
if self.xData is None:
|
if self.xData is None or len(self.xData) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
|
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
|
||||||
|
|
|
@ -15,7 +15,7 @@ class PlotDataItem(GraphicsObject):
|
||||||
GraphicsItem for displaying plot curves, scatter plots, or both.
|
GraphicsItem for displaying plot curves, scatter plots, or both.
|
||||||
While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or
|
While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or
|
||||||
:class:`ScatterPlotItem <pyqtgraph.ScatterPlotItem>` individually, this class
|
:class:`ScatterPlotItem <pyqtgraph.ScatterPlotItem>` individually, this class
|
||||||
provides a unified interface to both. Inspances of :class:`PlotDataItem` are
|
provides a unified interface to both. Instances of :class:`PlotDataItem` are
|
||||||
usually created by plot() methods such as :func:`pyqtgraph.plot` and
|
usually created by plot() methods such as :func:`pyqtgraph.plot` and
|
||||||
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
|
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
|
||||||
|
|
||||||
|
@ -531,15 +531,17 @@ class PlotDataItem(GraphicsObject):
|
||||||
## downsampling is expensive; delay until after clipping.
|
## downsampling is expensive; delay until after clipping.
|
||||||
|
|
||||||
if self.opts['clipToView']:
|
if self.opts['clipToView']:
|
||||||
# this option presumes that x-values have uniform spacing
|
view = self.getViewBox()
|
||||||
range = self.viewRect()
|
if view is None or not view.autoRangeEnabled()[0]:
|
||||||
if range is not None:
|
# this option presumes that x-values have uniform spacing
|
||||||
dx = float(x[-1]-x[0]) / (len(x)-1)
|
range = self.viewRect()
|
||||||
# clip to visible region extended by downsampling value
|
if range is not None:
|
||||||
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
dx = float(x[-1]-x[0]) / (len(x)-1)
|
||||||
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
|
# clip to visible region extended by downsampling value
|
||||||
x = x[x0:x1]
|
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
|
||||||
y = y[x0:x1]
|
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
|
||||||
|
x = x[x0:x1]
|
||||||
|
y = y[x0:x1]
|
||||||
|
|
||||||
if ds > 1:
|
if ds > 1:
|
||||||
if self.opts['downsampleMethod'] == 'subsample':
|
if self.opts['downsampleMethod'] == 'subsample':
|
||||||
|
|
|
@ -95,7 +95,6 @@ class PlotItem(GraphicsWidget):
|
||||||
|
|
||||||
|
|
||||||
lastFileDir = None
|
lastFileDir = None
|
||||||
managers = {}
|
|
||||||
|
|
||||||
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
|
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
|
||||||
"""
|
"""
|
||||||
|
@ -369,28 +368,6 @@ class PlotItem(GraphicsWidget):
|
||||||
self.scene().removeItem(self.vb)
|
self.scene().removeItem(self.vb)
|
||||||
self.vb = None
|
self.vb = None
|
||||||
|
|
||||||
## causes invalid index errors:
|
|
||||||
#for i in range(self.layout.count()):
|
|
||||||
#self.layout.removeAt(i)
|
|
||||||
|
|
||||||
#for p in self.proxies:
|
|
||||||
#try:
|
|
||||||
#p.setWidget(None)
|
|
||||||
#except RuntimeError:
|
|
||||||
#break
|
|
||||||
#self.scene().removeItem(p)
|
|
||||||
#self.proxies = []
|
|
||||||
|
|
||||||
#self.menuAction.releaseWidget(self.menuAction.defaultWidget())
|
|
||||||
#self.menuAction.setParent(None)
|
|
||||||
#self.menuAction = None
|
|
||||||
|
|
||||||
#if self.manager is not None:
|
|
||||||
#self.manager.sigWidgetListChanged.disconnect(self.updatePlotList)
|
|
||||||
#self.manager.removeWidget(self.name)
|
|
||||||
#else:
|
|
||||||
#print "no manager"
|
|
||||||
|
|
||||||
def registerPlot(self, name): ## for backward compatibility
|
def registerPlot(self, name): ## for backward compatibility
|
||||||
self.vb.register(name)
|
self.vb.register(name)
|
||||||
|
|
||||||
|
|
|
@ -1623,9 +1623,9 @@ class PolyLineROI(ROI):
|
||||||
if pos is None:
|
if pos is None:
|
||||||
pos = [0,0]
|
pos = [0,0]
|
||||||
|
|
||||||
ROI.__init__(self, pos, size=[1,1], **args)
|
|
||||||
self.closed = closed
|
self.closed = closed
|
||||||
self.segments = []
|
self.segments = []
|
||||||
|
ROI.__init__(self, pos, size=[1,1], **args)
|
||||||
|
|
||||||
for p in positions:
|
for p in positions:
|
||||||
self.addFreeHandle(p)
|
self.addFreeHandle(p)
|
||||||
|
@ -1750,6 +1750,10 @@ class PolyLineROI(ROI):
|
||||||
shape[axes[1]] = sliced.shape[axes[1]]
|
shape[axes[1]] = sliced.shape[axes[1]]
|
||||||
return sliced * mask.reshape(shape)
|
return sliced * mask.reshape(shape)
|
||||||
|
|
||||||
|
def setPen(self, *args, **kwds):
|
||||||
|
ROI.setPen(self, *args, **kwds)
|
||||||
|
for seg in self.segments:
|
||||||
|
seg.setPen(*args, **kwds)
|
||||||
|
|
||||||
class LineSegmentROI(ROI):
|
class LineSegmentROI(ROI):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,6 +3,11 @@ from ..Point import Point
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
from .GraphicsItem import GraphicsItem
|
from .GraphicsItem import GraphicsItem
|
||||||
from .GraphicsObject import GraphicsObject
|
from .GraphicsObject import GraphicsObject
|
||||||
|
from itertools import starmap, repeat
|
||||||
|
try:
|
||||||
|
from itertools import imap
|
||||||
|
except ImportError:
|
||||||
|
imap = map
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import weakref
|
import weakref
|
||||||
from .. import getConfigOption
|
from .. import getConfigOption
|
||||||
|
@ -86,11 +91,8 @@ class SymbolAtlas(object):
|
||||||
pm = atlas.getAtlas()
|
pm = atlas.getAtlas()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# symbol key : [x, y, w, h] atlas coordinates
|
# symbol key : QRect(...) coordinates where symbol can be found in atlas.
|
||||||
# note that the coordinate list will always be the same list object as
|
# note that the coordinate list will always be the same list object as
|
||||||
# long as the symbol is in the atlas, but the coordinates may
|
# long as the symbol is in the atlas, but the coordinates may
|
||||||
# change if the atlas is rebuilt.
|
# change if the atlas is rebuilt.
|
||||||
|
@ -101,28 +103,32 @@ class SymbolAtlas(object):
|
||||||
self.atlasData = None # numpy array of atlas image
|
self.atlasData = None # numpy array of atlas image
|
||||||
self.atlas = None # atlas as QPixmap
|
self.atlas = None # atlas as QPixmap
|
||||||
self.atlasValid = False
|
self.atlasValid = False
|
||||||
|
self.max_width=0
|
||||||
|
|
||||||
def getSymbolCoords(self, opts):
|
def getSymbolCoords(self, opts):
|
||||||
"""
|
"""
|
||||||
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
|
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
|
||||||
"""
|
"""
|
||||||
coords = np.empty(len(opts), dtype=object)
|
sourceRect = np.empty(len(opts), dtype=object)
|
||||||
|
keyi = None
|
||||||
|
sourceRecti = None
|
||||||
for i, rec in enumerate(opts):
|
for i, rec in enumerate(opts):
|
||||||
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
|
key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
|
||||||
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
|
if key == keyi:
|
||||||
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
|
sourceRect[i] = sourceRecti
|
||||||
key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color()))
|
else:
|
||||||
if key not in self.symbolMap:
|
try:
|
||||||
newCoords = SymbolAtlas.SymbolCoords()
|
sourceRect[i] = self.symbolMap[key]
|
||||||
self.symbolMap[key] = newCoords
|
except KeyError:
|
||||||
self.atlasValid = False
|
newRectSrc = QtCore.QRectF()
|
||||||
#try:
|
newRectSrc.pen = rec['pen']
|
||||||
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
|
newRectSrc.brush = rec['brush']
|
||||||
#except:
|
self.symbolMap[key] = newRectSrc
|
||||||
#self.buildAtlas() ## otherwise, we need to rebuild
|
self.atlasValid = False
|
||||||
|
sourceRect[i] = newRectSrc
|
||||||
coords[i] = self.symbolMap[key]
|
keyi = key
|
||||||
return coords
|
sourceRecti = newRectSrc
|
||||||
|
return sourceRect
|
||||||
|
|
||||||
def buildAtlas(self):
|
def buildAtlas(self):
|
||||||
# get rendered array for all symbols, keep track of avg/max width
|
# get rendered array for all symbols, keep track of avg/max width
|
||||||
|
@ -130,15 +136,13 @@ class SymbolAtlas(object):
|
||||||
avgWidth = 0.0
|
avgWidth = 0.0
|
||||||
maxWidth = 0
|
maxWidth = 0
|
||||||
images = []
|
images = []
|
||||||
for key, coords in self.symbolMap.items():
|
for key, sourceRect in self.symbolMap.items():
|
||||||
if len(coords) == 0:
|
if sourceRect.width() == 0:
|
||||||
pen = fn.mkPen(color=key[2], width=key[3], style=key[4])
|
img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush)
|
||||||
brush = fn.mkBrush(color=key[5])
|
|
||||||
img = renderSymbol(key[0], key[1], pen, 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:
|
||||||
(x,y,w,h) = self.symbolMap[key]
|
(y,x,h,w) = sourceRect.getRect()
|
||||||
arr = self.atlasData[x:x+w, y:y+w]
|
arr = self.atlasData[x:x+w, y:y+w]
|
||||||
rendered[key] = arr
|
rendered[key] = arr
|
||||||
w = arr.shape[0]
|
w = arr.shape[0]
|
||||||
|
@ -169,17 +173,18 @@ class SymbolAtlas(object):
|
||||||
x = 0
|
x = 0
|
||||||
rowheight = h
|
rowheight = h
|
||||||
self.atlasRows.append([y, rowheight, 0])
|
self.atlasRows.append([y, rowheight, 0])
|
||||||
self.symbolMap[key][:] = x, y, w, h
|
self.symbolMap[key].setRect(y, x, h, w)
|
||||||
x += w
|
x += w
|
||||||
self.atlasRows[-1][2] = x
|
self.atlasRows[-1][2] = x
|
||||||
height = y + rowheight
|
height = y + rowheight
|
||||||
|
|
||||||
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
|
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
|
||||||
for key in symbols:
|
for key in symbols:
|
||||||
x, y, w, h = self.symbolMap[key]
|
y, x, h, w = self.symbolMap[key].getRect()
|
||||||
self.atlasData[x:x+w, y:y+h] = rendered[key]
|
self.atlasData[x:x+w, y:y+h] = rendered[key]
|
||||||
self.atlas = None
|
self.atlas = None
|
||||||
self.atlasValid = True
|
self.atlasValid = True
|
||||||
|
self.max_width = maxWidth
|
||||||
|
|
||||||
def getAtlas(self):
|
def getAtlas(self):
|
||||||
if not self.atlasValid:
|
if not self.atlasValid:
|
||||||
|
@ -223,10 +228,9 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
GraphicsObject.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
|
|
||||||
self.picture = None # QPicture used for rendering when pxmode==False
|
self.picture = None # QPicture used for rendering when pxmode==False
|
||||||
self.fragments = None # fragment specification for pxmode; updated every time the view changes.
|
|
||||||
self.fragmentAtlas = SymbolAtlas()
|
self.fragmentAtlas = SymbolAtlas()
|
||||||
|
|
||||||
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)])
|
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)])
|
||||||
self.bounds = [None, None] ## caches data bounds
|
self.bounds = [None, None] ## caches data bounds
|
||||||
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
|
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
|
||||||
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
|
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
|
||||||
|
@ -237,8 +241,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
'name': None,
|
'name': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setPen(200,200,200, update=False)
|
self.setPen(fn.mkPen(getConfigOption('foreground')), update=False)
|
||||||
self.setBrush(100,100,150, update=False)
|
self.setBrush(fn.mkBrush(100,100,150), update=False)
|
||||||
self.setSymbol('o', update=False)
|
self.setSymbol('o', update=False)
|
||||||
self.setSize(7, update=False)
|
self.setSize(7, update=False)
|
||||||
profiler()
|
profiler()
|
||||||
|
@ -388,6 +392,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
self.setPointData(kargs['data'], dataSet=newData)
|
self.setPointData(kargs['data'], dataSet=newData)
|
||||||
|
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
|
self.informViewBoundsChanged()
|
||||||
self.bounds = [None, None]
|
self.bounds = [None, None]
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
self.updateSpots(newData)
|
self.updateSpots(newData)
|
||||||
|
@ -396,13 +401,11 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
def invalidate(self):
|
def invalidate(self):
|
||||||
## clear any cached drawing state
|
## clear any cached drawing state
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.fragments = None
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def getData(self):
|
def getData(self):
|
||||||
return self.data['x'], self.data['y']
|
return self.data['x'], self.data['y']
|
||||||
|
|
||||||
|
|
||||||
def setPoints(self, *args, **kargs):
|
def setPoints(self, *args, **kargs):
|
||||||
##Deprecated; use setData
|
##Deprecated; use setData
|
||||||
return self.setData(*args, **kargs)
|
return self.setData(*args, **kargs)
|
||||||
|
@ -434,7 +437,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
else:
|
else:
|
||||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||||
|
|
||||||
dataSet['fragCoords'] = None
|
dataSet['sourceRect'] = None
|
||||||
if update:
|
if update:
|
||||||
self.updateSpots(dataSet)
|
self.updateSpots(dataSet)
|
||||||
|
|
||||||
|
@ -459,7 +462,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||||
#self._spotPixmap = None
|
#self._spotPixmap = None
|
||||||
|
|
||||||
dataSet['fragCoords'] = None
|
dataSet['sourceRect'] = None
|
||||||
if update:
|
if update:
|
||||||
self.updateSpots(dataSet)
|
self.updateSpots(dataSet)
|
||||||
|
|
||||||
|
@ -482,7 +485,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
self.opts['symbol'] = symbol
|
self.opts['symbol'] = symbol
|
||||||
self._spotPixmap = None
|
self._spotPixmap = None
|
||||||
|
|
||||||
dataSet['fragCoords'] = None
|
dataSet['sourceRect'] = None
|
||||||
if update:
|
if update:
|
||||||
self.updateSpots(dataSet)
|
self.updateSpots(dataSet)
|
||||||
|
|
||||||
|
@ -505,7 +508,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
self.opts['size'] = size
|
self.opts['size'] = size
|
||||||
self._spotPixmap = None
|
self._spotPixmap = None
|
||||||
|
|
||||||
dataSet['fragCoords'] = None
|
dataSet['sourceRect'] = None
|
||||||
if update:
|
if update:
|
||||||
self.updateSpots(dataSet)
|
self.updateSpots(dataSet)
|
||||||
|
|
||||||
|
@ -537,22 +540,26 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
def updateSpots(self, dataSet=None):
|
def updateSpots(self, dataSet=None):
|
||||||
if dataSet is None:
|
if dataSet is None:
|
||||||
dataSet = self.data
|
dataSet = self.data
|
||||||
self._maxSpotWidth = 0
|
|
||||||
self._maxSpotPxWidth = 0
|
|
||||||
invalidate = False
|
invalidate = False
|
||||||
self.measureSpotSizes(dataSet)
|
|
||||||
if self.opts['pxMode']:
|
if self.opts['pxMode']:
|
||||||
mask = np.equal(dataSet['fragCoords'], None)
|
mask = np.equal(dataSet['sourceRect'], None)
|
||||||
if np.any(mask):
|
if np.any(mask):
|
||||||
invalidate = True
|
invalidate = True
|
||||||
opts = self.getSpotOpts(dataSet[mask])
|
opts = self.getSpotOpts(dataSet[mask])
|
||||||
coords = self.fragmentAtlas.getSymbolCoords(opts)
|
sourceRect = self.fragmentAtlas.getSymbolCoords(opts)
|
||||||
dataSet['fragCoords'][mask] = coords
|
dataSet['sourceRect'][mask] = sourceRect
|
||||||
|
|
||||||
|
self.fragmentAtlas.getAtlas() # generate atlas so source widths are available.
|
||||||
|
|
||||||
|
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2
|
||||||
|
dataSet['targetRect'] = None
|
||||||
|
self._maxSpotPxWidth = self.fragmentAtlas.max_width
|
||||||
|
else:
|
||||||
|
self._maxSpotWidth = 0
|
||||||
|
self._maxSpotPxWidth = 0
|
||||||
|
self.measureSpotSizes(dataSet)
|
||||||
|
|
||||||
#for rec in dataSet:
|
|
||||||
#if rec['fragCoords'] is None:
|
|
||||||
#invalidate = True
|
|
||||||
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
|
||||||
if invalidate:
|
if invalidate:
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
|
|
||||||
|
@ -669,30 +676,43 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
GraphicsObject.viewTransformChanged(self)
|
GraphicsObject.viewTransformChanged(self)
|
||||||
self.bounds = [None, None]
|
self.bounds = [None, None]
|
||||||
self.fragments = None
|
self.data['targetRect'] = None
|
||||||
|
|
||||||
def generateFragments(self):
|
|
||||||
tr = self.deviceTransform()
|
|
||||||
if tr is None:
|
|
||||||
return
|
|
||||||
pts = np.empty((2,len(self.data['x'])))
|
|
||||||
pts[0] = self.data['x']
|
|
||||||
pts[1] = self.data['y']
|
|
||||||
pts = fn.transformCoordinates(tr, pts)
|
|
||||||
self.fragments = []
|
|
||||||
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
|
||||||
## Still won't be able to render correctly, though.
|
|
||||||
for i in xrange(len(self.data)):
|
|
||||||
rec = self.data[i]
|
|
||||||
pos = QtCore.QPointF(pts[0,i], pts[1,i])
|
|
||||||
x,y,w,h = rec['fragCoords']
|
|
||||||
rect = QtCore.QRectF(y, x, h, w)
|
|
||||||
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
|
|
||||||
|
|
||||||
def setExportMode(self, *args, **kwds):
|
def setExportMode(self, *args, **kwds):
|
||||||
GraphicsObject.setExportMode(self, *args, **kwds)
|
GraphicsObject.setExportMode(self, *args, **kwds)
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
def mapPointsToDevice(self, pts):
|
||||||
|
# Map point locations to device
|
||||||
|
tr = self.deviceTransform()
|
||||||
|
if tr is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
#pts = np.empty((2,len(self.data['x'])))
|
||||||
|
#pts[0] = self.data['x']
|
||||||
|
#pts[1] = self.data['y']
|
||||||
|
pts = fn.transformCoordinates(tr, pts)
|
||||||
|
pts -= self.data['width']
|
||||||
|
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
||||||
|
|
||||||
|
return pts
|
||||||
|
|
||||||
|
def getViewMask(self, pts):
|
||||||
|
# Return bool mask indicating all points that are within viewbox
|
||||||
|
# pts is expressed in *device coordiantes*
|
||||||
|
vb = self.getViewBox()
|
||||||
|
if vb is None:
|
||||||
|
return None
|
||||||
|
viewBounds = vb.mapRectToDevice(vb.boundingRect())
|
||||||
|
w = self.data['width']
|
||||||
|
mask = ((pts[0] + w > viewBounds.left()) &
|
||||||
|
(pts[0] - w < viewBounds.right()) &
|
||||||
|
(pts[1] + w > viewBounds.top()) &
|
||||||
|
(pts[1] - w < viewBounds.bottom())) ## remove out of view points
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
@debug.warnOnException ## raising an exception here causes crash
|
@debug.warnOnException ## raising an exception here causes crash
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
|
|
||||||
|
@ -707,29 +727,44 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
|
|
||||||
if self.opts['pxMode'] is True:
|
if self.opts['pxMode'] is True:
|
||||||
atlas = self.fragmentAtlas.getAtlas()
|
|
||||||
#arr = fn.imageToArray(atlas.toImage(), copy=True)
|
|
||||||
#if hasattr(self, 'lastAtlas'):
|
|
||||||
#if np.any(self.lastAtlas != arr):
|
|
||||||
#print "Atlas changed:", arr
|
|
||||||
#self.lastAtlas = arr
|
|
||||||
|
|
||||||
if self.fragments is None:
|
|
||||||
self.updateSpots()
|
|
||||||
self.generateFragments()
|
|
||||||
|
|
||||||
p.resetTransform()
|
p.resetTransform()
|
||||||
|
|
||||||
if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False:
|
# Map point coordinates to device
|
||||||
p.drawPixmapFragments(self.fragments, atlas)
|
pts = np.vstack([self.data['x'], self.data['y']])
|
||||||
|
pts = self.mapPointsToDevice(pts)
|
||||||
|
if pts is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cull points that are outside view
|
||||||
|
viewMask = self.getViewMask(pts)
|
||||||
|
#pts = pts[:,mask]
|
||||||
|
#data = self.data[mask]
|
||||||
|
|
||||||
|
if self.opts['useCache'] and self._exportOpts is False:
|
||||||
|
# Draw symbols from pre-rendered atlas
|
||||||
|
atlas = self.fragmentAtlas.getAtlas()
|
||||||
|
|
||||||
|
# Update targetRects if necessary
|
||||||
|
updateMask = viewMask & np.equal(self.data['targetRect'], None)
|
||||||
|
if np.any(updateMask):
|
||||||
|
updatePts = pts[:,updateMask]
|
||||||
|
width = self.data[updateMask]['width']*2
|
||||||
|
self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
|
||||||
|
|
||||||
|
data = self.data[viewMask]
|
||||||
|
if USE_PYSIDE:
|
||||||
|
list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect']))
|
||||||
|
else:
|
||||||
|
p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas)
|
||||||
else:
|
else:
|
||||||
|
# render each symbol individually
|
||||||
p.setRenderHint(p.Antialiasing, aa)
|
p.setRenderHint(p.Antialiasing, aa)
|
||||||
|
|
||||||
for i in range(len(self.data)):
|
data = self.data[viewMask]
|
||||||
rec = self.data[i]
|
pts = pts[:,viewMask]
|
||||||
frag = self.fragments[i]
|
for i, rec in enumerate(data):
|
||||||
p.resetTransform()
|
p.resetTransform()
|
||||||
p.translate(frag.x, frag.y)
|
p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width'])
|
||||||
drawSymbol(p, *self.getSpotOpts(rec, scale))
|
drawSymbol(p, *self.getSpotOpts(rec, scale))
|
||||||
else:
|
else:
|
||||||
if self.picture is None:
|
if self.picture is None:
|
||||||
|
@ -891,7 +926,7 @@ class SpotItem(object):
|
||||||
self._data['data'] = data
|
self._data['data'] = data
|
||||||
|
|
||||||
def updateItem(self):
|
def updateItem(self):
|
||||||
self._data['fragCoords'] = None
|
self._data['sourceRect'] = None
|
||||||
self._plot.updateSpots(self._data.reshape(1))
|
self._plot.updateSpots(self._data.reshape(1))
|
||||||
self._plot.invalidate()
|
self._plot.invalidate()
|
||||||
|
|
||||||
|
|
23
graphicsItems/tests/ScatterPlotItem.py
Normal file
23
graphicsItems/tests/ScatterPlotItem.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import numpy as np
|
||||||
|
app = pg.mkQApp()
|
||||||
|
plot = pg.plot()
|
||||||
|
app.processEvents()
|
||||||
|
|
||||||
|
# set view range equal to its bounding rect.
|
||||||
|
# This causes plots to look the same regardless of pxMode.
|
||||||
|
plot.setRange(rect=plot.boundingRect())
|
||||||
|
|
||||||
|
|
||||||
|
def test_modes():
|
||||||
|
for i, pxMode in enumerate([True, False]):
|
||||||
|
for j, useCache in enumerate([True, False]):
|
||||||
|
s = pg.ScatterPlotItem()
|
||||||
|
s.opts['useCache'] = useCache
|
||||||
|
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.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_modes()
|
|
@ -198,6 +198,7 @@ class ImageView(QtGui.QWidget):
|
||||||
if not isinstance(img, np.ndarray):
|
if not isinstance(img, np.ndarray):
|
||||||
raise Exception("Image must be specified as ndarray.")
|
raise Exception("Image must be specified as ndarray.")
|
||||||
self.image = img
|
self.image = img
|
||||||
|
self.imageDisp = None
|
||||||
|
|
||||||
if xvals is not None:
|
if xvals is not None:
|
||||||
self.tVals = xvals
|
self.tVals = xvals
|
||||||
|
|
|
@ -1,41 +1,212 @@
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
from ..SignalProxy import SignalProxy
|
from ..SignalProxy import SignalProxy
|
||||||
|
from ..pgcollections import OrderedDict
|
||||||
|
from ..python2_3 import asUnicode
|
||||||
|
|
||||||
class ComboBox(QtGui.QComboBox):
|
class ComboBox(QtGui.QComboBox):
|
||||||
"""Extends QComboBox to add extra functionality.
|
"""Extends QComboBox to add extra functionality.
|
||||||
- updateList() - updates the items in the comboBox while blocking signals, remembers and resets to the previous values if it's still in the list
|
|
||||||
|
* Handles dict mappings -- user selects a text key, and the ComboBox indicates
|
||||||
|
the selected value.
|
||||||
|
* Requires item strings to be unique
|
||||||
|
* Remembers selected value if list is cleared and subsequently repopulated
|
||||||
|
* setItems() replaces the items in the ComboBox and blocks signals if the
|
||||||
|
value ultimately does not change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, parent=None, items=None, default=None):
|
def __init__(self, parent=None, items=None, default=None):
|
||||||
QtGui.QComboBox.__init__(self, parent)
|
QtGui.QComboBox.__init__(self, parent)
|
||||||
|
self.currentIndexChanged.connect(self.indexChanged)
|
||||||
|
self._ignoreIndexChange = False
|
||||||
|
|
||||||
#self.value = default
|
self._chosenText = None
|
||||||
|
self._items = OrderedDict()
|
||||||
|
|
||||||
if items is not None:
|
if items is not None:
|
||||||
self.addItems(items)
|
self.setItems(items)
|
||||||
if default is not None:
|
if default is not None:
|
||||||
self.setValue(default)
|
self.setValue(default)
|
||||||
|
|
||||||
def setValue(self, value):
|
def setValue(self, value):
|
||||||
ind = self.findText(value)
|
"""Set the selected item to the first one having the given value."""
|
||||||
|
text = None
|
||||||
|
for k,v in self._items.items():
|
||||||
|
if v == value:
|
||||||
|
text = k
|
||||||
|
break
|
||||||
|
if text is None:
|
||||||
|
raise ValueError(value)
|
||||||
|
|
||||||
|
self.setText(text)
|
||||||
|
|
||||||
|
def setText(self, text):
|
||||||
|
"""Set the selected item to the first one having the given text."""
|
||||||
|
ind = self.findText(text)
|
||||||
if ind == -1:
|
if ind == -1:
|
||||||
return
|
raise ValueError(text)
|
||||||
#self.value = value
|
#self.value = value
|
||||||
self.setCurrentIndex(ind)
|
self.setCurrentIndex(ind)
|
||||||
|
|
||||||
def updateList(self, items):
|
def value(self):
|
||||||
prevVal = str(self.currentText())
|
"""
|
||||||
try:
|
If items were given as a list of strings, then return the currently
|
||||||
|
selected text. If items were given as a dict, then return the value
|
||||||
|
corresponding to the currently selected key. If the combo list is empty,
|
||||||
|
return None.
|
||||||
|
"""
|
||||||
|
if self.count() == 0:
|
||||||
|
return None
|
||||||
|
text = asUnicode(self.currentText())
|
||||||
|
return self._items[text]
|
||||||
|
|
||||||
|
def ignoreIndexChange(func):
|
||||||
|
# Decorator that prevents updates to self._chosenText
|
||||||
|
def fn(self, *args, **kwds):
|
||||||
|
prev = self._ignoreIndexChange
|
||||||
|
self._ignoreIndexChange = True
|
||||||
|
try:
|
||||||
|
ret = func(self, *args, **kwds)
|
||||||
|
finally:
|
||||||
|
self._ignoreIndexChange = prev
|
||||||
|
return ret
|
||||||
|
return fn
|
||||||
|
|
||||||
|
def blockIfUnchanged(func):
|
||||||
|
# decorator that blocks signal emission during complex operations
|
||||||
|
# and emits currentIndexChanged only if the value has actually
|
||||||
|
# changed at the end.
|
||||||
|
def fn(self, *args, **kwds):
|
||||||
|
prevVal = self.value()
|
||||||
|
blocked = self.signalsBlocked()
|
||||||
self.blockSignals(True)
|
self.blockSignals(True)
|
||||||
|
try:
|
||||||
|
ret = func(self, *args, **kwds)
|
||||||
|
finally:
|
||||||
|
self.blockSignals(blocked)
|
||||||
|
|
||||||
|
# only emit if the value has changed
|
||||||
|
if self.value() != prevVal:
|
||||||
|
self.currentIndexChanged.emit(self.currentIndex())
|
||||||
|
|
||||||
|
return ret
|
||||||
|
return fn
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
@blockIfUnchanged
|
||||||
|
def setItems(self, items):
|
||||||
|
"""
|
||||||
|
*items* may be a list or a dict.
|
||||||
|
If a dict is given, then the keys are used to populate the combo box
|
||||||
|
and the values will be used for both value() and setValue().
|
||||||
|
"""
|
||||||
|
prevVal = self.value()
|
||||||
|
|
||||||
|
self.blockSignals(True)
|
||||||
|
try:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.addItems(items)
|
self.addItems(items)
|
||||||
self.setValue(prevVal)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.blockSignals(False)
|
self.blockSignals(False)
|
||||||
|
|
||||||
if str(self.currentText()) != prevVal:
|
# only emit if we were not able to re-set the original value
|
||||||
|
if self.value() != prevVal:
|
||||||
self.currentIndexChanged.emit(self.currentIndex())
|
self.currentIndexChanged.emit(self.currentIndex())
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return self.items.copy()
|
||||||
|
|
||||||
|
def updateList(self, items):
|
||||||
|
# for backward compatibility
|
||||||
|
return self.setItems(items)
|
||||||
|
|
||||||
|
def indexChanged(self, index):
|
||||||
|
# current index has changed; need to remember new 'chosen text'
|
||||||
|
if self._ignoreIndexChange:
|
||||||
|
return
|
||||||
|
self._chosenText = asUnicode(self.currentText())
|
||||||
|
|
||||||
|
def setCurrentIndex(self, index):
|
||||||
|
QtGui.QComboBox.setCurrentIndex(self, index)
|
||||||
|
|
||||||
|
def itemsChanged(self):
|
||||||
|
# try to set the value to the last one selected, if it is available.
|
||||||
|
if self._chosenText is not None:
|
||||||
|
try:
|
||||||
|
self.setText(self._chosenText)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
def insertItem(self, *args):
|
||||||
|
raise NotImplementedError()
|
||||||
|
#QtGui.QComboBox.insertItem(self, *args)
|
||||||
|
#self.itemsChanged()
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
def insertItems(self, *args):
|
||||||
|
raise NotImplementedError()
|
||||||
|
#QtGui.QComboBox.insertItems(self, *args)
|
||||||
|
#self.itemsChanged()
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
def addItem(self, *args, **kwds):
|
||||||
|
# Need to handle two different function signatures for QComboBox.addItem
|
||||||
|
try:
|
||||||
|
if isinstance(args[0], basestring):
|
||||||
|
text = args[0]
|
||||||
|
if len(args) == 2:
|
||||||
|
value = args[1]
|
||||||
|
else:
|
||||||
|
value = kwds.get('value', text)
|
||||||
|
else:
|
||||||
|
text = args[1]
|
||||||
|
if len(args) == 3:
|
||||||
|
value = args[2]
|
||||||
|
else:
|
||||||
|
value = kwds.get('value', text)
|
||||||
|
|
||||||
|
except IndexError:
|
||||||
|
raise TypeError("First or second argument of addItem must be a string.")
|
||||||
|
|
||||||
|
if text in self._items:
|
||||||
|
raise Exception('ComboBox already has item named "%s".' % text)
|
||||||
|
|
||||||
|
self._items[text] = value
|
||||||
|
QtGui.QComboBox.addItem(self, *args)
|
||||||
|
self.itemsChanged()
|
||||||
|
|
||||||
|
def setItemValue(self, name, value):
|
||||||
|
if name not in self._items:
|
||||||
|
self.addItem(name, value)
|
||||||
|
else:
|
||||||
|
self._items[name] = value
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
@blockIfUnchanged
|
||||||
|
def addItems(self, items):
|
||||||
|
if isinstance(items, list):
|
||||||
|
texts = items
|
||||||
|
items = dict([(x, x) for x in items])
|
||||||
|
elif isinstance(items, dict):
|
||||||
|
texts = list(items.keys())
|
||||||
|
else:
|
||||||
|
raise TypeError("items argument must be list or dict (got %s)." % type(items))
|
||||||
|
|
||||||
|
for t in texts:
|
||||||
|
if t in self._items:
|
||||||
|
raise Exception('ComboBox already has item named "%s".' % t)
|
||||||
|
|
||||||
|
|
||||||
|
for k,v in items.items():
|
||||||
|
self._items[k] = v
|
||||||
|
QtGui.QComboBox.addItems(self, list(texts))
|
||||||
|
|
||||||
|
self.itemsChanged()
|
||||||
|
|
||||||
|
@ignoreIndexChange
|
||||||
|
def clear(self):
|
||||||
|
self._items = OrderedDict()
|
||||||
|
QtGui.QComboBox.clear(self)
|
||||||
|
self.itemsChanged()
|
||||||
|
|
||||||
|
|
44
widgets/tests/test_combobox.py
Normal file
44
widgets/tests/test_combobox.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import pyqtgraph as pg
|
||||||
|
pg.mkQApp()
|
||||||
|
|
||||||
|
def test_combobox():
|
||||||
|
cb = pg.ComboBox()
|
||||||
|
items = {'a': 1, 'b': 2, 'c': 3}
|
||||||
|
cb.setItems(items)
|
||||||
|
cb.setValue(2)
|
||||||
|
assert str(cb.currentText()) == 'b'
|
||||||
|
assert cb.value() == 2
|
||||||
|
|
||||||
|
# Clear item list; value should be None
|
||||||
|
cb.clear()
|
||||||
|
assert cb.value() == None
|
||||||
|
|
||||||
|
# Reset item list; value should be set automatically
|
||||||
|
cb.setItems(items)
|
||||||
|
assert cb.value() == 2
|
||||||
|
|
||||||
|
# Clear item list; repopulate with same names and new values
|
||||||
|
items = {'a': 4, 'b': 5, 'c': 6}
|
||||||
|
cb.clear()
|
||||||
|
cb.setItems(items)
|
||||||
|
assert cb.value() == 5
|
||||||
|
|
||||||
|
# Set list instead of dict
|
||||||
|
cb.setItems(list(items.keys()))
|
||||||
|
assert str(cb.currentText()) == 'b'
|
||||||
|
|
||||||
|
cb.setValue('c')
|
||||||
|
assert cb.value() == str(cb.currentText())
|
||||||
|
assert cb.value() == 'c'
|
||||||
|
|
||||||
|
cb.setItemValue('c', 7)
|
||||||
|
assert cb.value() == 7
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
cb = pg.ComboBox()
|
||||||
|
cb.show()
|
||||||
|
cb.setItems({'': None, 'a': 1, 'b': 2, 'c': 3})
|
||||||
|
def fn(ind):
|
||||||
|
print("New value: %s" % cb.value())
|
||||||
|
cb.currentIndexChanged.connect(fn)
|
Loading…
Reference in New Issue
Block a user