Merge branch 'develop' into pyqtgraph-core

This commit is contained in:
Luke Campagnola 2014-01-24 10:50:50 -05:00
commit f613d33c49
17 changed files with 586 additions and 198 deletions

View File

@ -48,8 +48,8 @@ else:
CONFIG_OPTIONS = {
'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
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
'background': (0, 0, 0), ## default background for GraphicsWidget
'foreground': 'd', ## default foreground color for axes, labels, etc.
'background': 'k', ## default background for GraphicsWidget
'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available

View File

@ -58,14 +58,15 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
class Flowchart(Node):
sigFileLoaded = QtCore.Signal(object)
sigFileSaved = QtCore.Signal(object)
#sigOutputChanged = QtCore.Signal() ## inherited from Node
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):
self.library = library or LIBRARY
@ -218,6 +219,7 @@ class Flowchart(Node):
node.sigClosed.connect(self.nodeClosed)
node.sigRenamed.connect(self.nodeRenamed)
node.sigOutputChanged.connect(self.nodeOutputChanged)
self.sigChartChanged.emit(self, 'add', node)
def removeNode(self, node):
node.close()
@ -237,11 +239,13 @@ class Flowchart(Node):
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
except TypeError:
pass
self.sigChartChanged.emit(self, 'remove', node)
def nodeRenamed(self, node, oldName):
del self._nodes[oldName]
self._nodes[node.name()] = node
self.widget().nodeRenamed(node, oldName)
self.sigChartChanged.emit(self, 'rename', node)
def arrangeNodes(self):
pass

View File

@ -4,7 +4,7 @@ import weakref
from ...Qt import QtCore, QtGui
from ...graphicsItems.ScatterPlotItem import ScatterPlotItem
from ...graphicsItems.PlotCurveItem import PlotCurveItem
from ... import PlotDataItem
from ... import PlotDataItem, ComboBox
from .common import *
import numpy as np
@ -16,7 +16,9 @@ class PlotWidgetNode(Node):
def __init__(self, name):
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 = {}
def disconnected(self, localTerm, remoteTerm):
@ -26,16 +28,27 @@ class PlotWidgetNode(Node):
def setPlot(self, 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.updateUi()
self.update()
self.sigPlotChanged.emit(self)
def getPlot(self):
return self.plot
def process(self, In, display=True):
if display:
#self.plot.clearPlots()
if display and self.plot is not None:
items = set()
# Add all new input items to selected plot
for name, vals in In.items():
if vals is None:
continue
@ -45,14 +58,13 @@ class PlotWidgetNode(Node):
for val in vals:
vid = id(val)
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)
else:
#if isinstance(val, PlotCurveItem):
#self.plot.addItem(val)
#item = val
#if isinstance(val, ScatterPlotItem):
#self.plot.addItem(val)
#item = val
# Add the item to the plot, or generate a new item if needed.
if isinstance(val, QtGui.QGraphicsItem):
self.plot.addItem(val)
item = val
@ -60,22 +72,48 @@ class PlotWidgetNode(Node):
item = self.plot.plot(val)
self.items[vid] = item
items.add(vid)
# Any left-over items that did not appear in the input must be removed
for vid in list(self.items.keys()):
if vid not in items:
#print "remove", self.items[vid]
self.plot.removeItem(self.items[vid])
del self.items[vid]
def processBypassed(self, args):
if self.plot is None:
return
for item in list(self.items.values()):
self.plot.removeItem(item)
self.items = {}
#def setInput(self, **args):
#for k in args:
#self.plot.plot(args[k])
def ctrlWidget(self):
if self.ui is None:
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):
"""Connection to a Canvas widget."""

View File

@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
from __future__ import division
from .python2_3 import asUnicode
from .Qt import QtGui, QtCore, USE_PYSIDE
Colors = {
'b': (0,0,255,255),
'g': (0,255,0,255),
'r': (255,0,0,255),
'c': (0,255,255,255),
'm': (255,0,255,255),
'y': (255,255,0,255),
'k': (0,0,0,255),
'w': (255,255,255,255),
'b': QtGui.QColor(0,0,255,255),
'g': QtGui.QColor(0,255,0,255),
'r': QtGui.QColor(255,0,0,255),
'c': QtGui.QColor(0,255,255,255),
'm': QtGui.QColor(255,0,255,255),
'y': QtGui.QColor(255,255,0,255),
'k': QtGui.QColor(0,0,0,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')
@ -168,17 +172,15 @@ def mkColor(*args):
"""
err = 'Not sure how to make a color from "%s"' % str(args)
if len(args) == 1:
if 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 isinstance(args[0], basestring):
if isinstance(args[0], basestring):
c = args[0]
if c[0] == '#':
c = 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:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
@ -199,6 +201,11 @@ def mkColor(*args):
g = int(c[2:4], 16)
b = int(c[4:6], 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__'):
if len(args[0]) == 3:
(r, g, b) = args[0]
@ -282,7 +289,7 @@ def mkPen(*args, **kargs):
color = args
if color is None:
color = mkColor(200, 200, 200)
color = mkColor('l')
if hsv is not None:
color = hsvColor(*hsv)
else:
@ -821,7 +828,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels
if minVal == maxVal:
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()

View File

@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget):
if pen == None, the default will be used (see :func:`setConfigOption
<pyqtgraph.setConfigOption>`)
"""
self._pen = pen
self.picture = None
if pen is None:
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.update()
@ -458,8 +458,7 @@ class AxisItem(GraphicsWidget):
return []
## decide optimal minor tick spacing in pixels (this is just aesthetics)
pixelSpacing = size / np.log(size)
optimalTickCount = max(2., size / pixelSpacing)
optimalTickCount = max(2., np.log(size))
## optimal minor tick spacing
optimalSpacing = dif / optimalTickCount
@ -795,7 +794,7 @@ class AxisItem(GraphicsWidget):
if s is None:
rects.append(None)
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
## (but this probably depends on per-font metrics?)
br.setHeight(br.height() * 0.8)
@ -830,7 +829,7 @@ class AxisItem(GraphicsWidget):
vstr = strings[j]
if vstr is None: ## this tick was ignored because it is out of bounds
continue
vstr = str(vstr)
vstr = asUnicode(vstr)
x = tickPositions[i][j]
#textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
textRect = rects[j]

View File

@ -1,24 +1,70 @@
from ..Qt import QtGui
from .. import functions as fn
from .PlotDataItem import PlotDataItem
from .PlotCurveItem import PlotCurveItem
class FillBetweenItem(QtGui.QGraphicsPathItem):
"""
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)
self.p1 = p1
self.p2 = p2
p1.sigPlotChanged.connect(self.updatePath)
p2.sigPlotChanged.connect(self.updatePath)
self.curves = None
if curve1 is not None and curve2 is not None:
self.setCurves(curve1, curve2)
elif curve1 is not None or curve2 is not None:
raise Exception("Must specify two curves to fill between.")
if brush is not None:
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()
def updatePath(self):
p1 = self.p1.curve.path
p2 = self.p2.curve.path
if self.curves is None:
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.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)

View File

@ -1,3 +1,5 @@
from __future__ import division
from ..Qt import QtGui, QtCore
import numpy as np
import collections
@ -287,15 +289,45 @@ class ImageItem(GraphicsObject):
self.render()
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.
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.
"""
if self.image is None:
return None,None
stepData = self.image[::step, ::step]
hist = np.histogram(stepData, bins=bins)
if step == 'auto':
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]
def setPxMode(self, b):

View File

@ -3,7 +3,7 @@ from .LabelItem import LabelItem
from ..Qt import QtGui, QtCore
from .. import functions as fn
from ..Point import Point
from .ScatterPlotItem import ScatterPlotItem
from .ScatterPlotItem import ScatterPlotItem, drawSymbol
from .PlotDataItem import PlotDataItem
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
__all__ = ['LegendItem']
@ -167,7 +167,7 @@ class ItemSample(GraphicsWidget):
size = opts['size']
p.translate(10,10)
path = ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush)
path = drawSymbol(p, symbol, size, pen, brush)

View File

@ -393,16 +393,18 @@ class PlotCurveItem(GraphicsObject):
if self.path is None:
x,y = self.getData()
if x is None or len(x) == 0 or y is None or len(y) == 0:
return QtGui.QPainterPath()
self.path = self.generatePath(*self.getData())
self.path = QtGui.QPainterPath()
else:
self.path = self.generatePath(*self.getData())
self.fillPath = None
self._mouseShape = None
return self.path
@debug.warnOnException ## raising an exception here causes crash
def paint(self, p, opt, widget):
profiler = debug.Profiler()
if self.xData is None:
if self.xData is None or len(self.xData) == 0:
return
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):

View File

@ -15,7 +15,7 @@ class PlotDataItem(GraphicsObject):
GraphicsItem for displaying plot curves, scatter plots, or both.
While it is possible to use :class:`PlotCurveItem <pyqtgraph.PlotCurveItem>` or
: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
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
@ -531,15 +531,17 @@ class PlotDataItem(GraphicsObject):
## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
# clip to visible region extended by downsampling value
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
x = x[x0:x1]
y = y[x0:x1]
view = self.getViewBox()
if view is None or not view.autoRangeEnabled()[0]:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
# clip to visible region extended by downsampling value
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
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 self.opts['downsampleMethod'] == 'subsample':

View File

@ -95,7 +95,6 @@ class PlotItem(GraphicsWidget):
lastFileDir = None
managers = {}
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.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
self.vb.register(name)

View File

@ -1623,9 +1623,9 @@ class PolyLineROI(ROI):
if pos is None:
pos = [0,0]
ROI.__init__(self, pos, size=[1,1], **args)
self.closed = closed
self.segments = []
ROI.__init__(self, pos, size=[1,1], **args)
for p in positions:
self.addFreeHandle(p)
@ -1750,6 +1750,10 @@ class PolyLineROI(ROI):
shape[axes[1]] = sliced.shape[axes[1]]
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):
"""

View File

@ -3,6 +3,11 @@ from ..Point import Point
from .. import functions as fn
from .GraphicsItem import GraphicsItem
from .GraphicsObject import GraphicsObject
from itertools import starmap, repeat
try:
from itertools import imap
except ImportError:
imap = map
import numpy as np
import weakref
from .. import getConfigOption
@ -86,11 +91,8 @@ class SymbolAtlas(object):
pm = atlas.getAtlas()
"""
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
pass
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
# long as the symbol is in the atlas, but the coordinates may
# change if the atlas is rebuilt.
@ -101,28 +103,32 @@ class SymbolAtlas(object):
self.atlasData = None # numpy array of atlas image
self.atlas = None # atlas as QPixmap
self.atlasValid = False
self.max_width=0
def getSymbolCoords(self, opts):
"""
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):
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
key = (symbol, size, fn.colorTuple(pen.color()), pen.widthF(), pen.style(), fn.colorTuple(brush.color()))
if key not in self.symbolMap:
newCoords = SymbolAtlas.SymbolCoords()
self.symbolMap[key] = newCoords
self.atlasValid = False
#try:
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
#except:
#self.buildAtlas() ## otherwise, we need to rebuild
coords[i] = self.symbolMap[key]
return coords
key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
if key == keyi:
sourceRect[i] = sourceRecti
else:
try:
sourceRect[i] = self.symbolMap[key]
except KeyError:
newRectSrc = QtCore.QRectF()
newRectSrc.pen = rec['pen']
newRectSrc.brush = rec['brush']
self.symbolMap[key] = newRectSrc
self.atlasValid = False
sourceRect[i] = newRectSrc
keyi = key
sourceRecti = newRectSrc
return sourceRect
def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width
@ -130,15 +136,13 @@ class SymbolAtlas(object):
avgWidth = 0.0
maxWidth = 0
images = []
for key, coords in self.symbolMap.items():
if len(coords) == 0:
pen = fn.mkPen(color=key[2], width=key[3], style=key[4])
brush = fn.mkBrush(color=key[5])
img = renderSymbol(key[0], key[1], pen, brush)
for key, sourceRect in self.symbolMap.items():
if sourceRect.width() == 0:
img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False)
else:
(x,y,w,h) = self.symbolMap[key]
(y,x,h,w) = sourceRect.getRect()
arr = self.atlasData[x:x+w, y:y+w]
rendered[key] = arr
w = arr.shape[0]
@ -169,17 +173,18 @@ class SymbolAtlas(object):
x = 0
rowheight = h
self.atlasRows.append([y, rowheight, 0])
self.symbolMap[key][:] = x, y, w, h
self.symbolMap[key].setRect(y, x, h, w)
x += w
self.atlasRows[-1][2] = x
height = y + rowheight
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
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.atlas = None
self.atlasValid = True
self.max_width = maxWidth
def getAtlas(self):
if not self.atlasValid:
@ -223,10 +228,9 @@ class ScatterPlotItem(GraphicsObject):
GraphicsObject.__init__(self)
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.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._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
@ -237,8 +241,8 @@ class ScatterPlotItem(GraphicsObject):
'name': None,
}
self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False)
self.setPen(fn.mkPen(getConfigOption('foreground')), update=False)
self.setBrush(fn.mkBrush(100,100,150), update=False)
self.setSymbol('o', update=False)
self.setSize(7, update=False)
profiler()
@ -388,6 +392,7 @@ class ScatterPlotItem(GraphicsObject):
self.setPointData(kargs['data'], dataSet=newData)
self.prepareGeometryChange()
self.informViewBoundsChanged()
self.bounds = [None, None]
self.invalidate()
self.updateSpots(newData)
@ -396,12 +401,10 @@ class ScatterPlotItem(GraphicsObject):
def invalidate(self):
## clear any cached drawing state
self.picture = None
self.fragments = None
self.update()
def getData(self):
return self.data['x'], self.data['y']
return self.data['x'], self.data['y']
def setPoints(self, *args, **kargs):
##Deprecated; use setData
@ -434,7 +437,7 @@ class ScatterPlotItem(GraphicsObject):
else:
self.opts['pen'] = fn.mkPen(*args, **kargs)
dataSet['fragCoords'] = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
@ -459,7 +462,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['brush'] = fn.mkBrush(*args, **kargs)
#self._spotPixmap = None
dataSet['fragCoords'] = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
@ -482,7 +485,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['symbol'] = symbol
self._spotPixmap = None
dataSet['fragCoords'] = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
@ -505,7 +508,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size
self._spotPixmap = None
dataSet['fragCoords'] = None
dataSet['sourceRect'] = None
if update:
self.updateSpots(dataSet)
@ -537,22 +540,26 @@ class ScatterPlotItem(GraphicsObject):
def updateSpots(self, dataSet=None):
if dataSet is None:
dataSet = self.data
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
invalidate = False
self.measureSpotSizes(dataSet)
if self.opts['pxMode']:
mask = np.equal(dataSet['fragCoords'], None)
mask = np.equal(dataSet['sourceRect'], None)
if np.any(mask):
invalidate = True
opts = self.getSpotOpts(dataSet[mask])
coords = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['fragCoords'][mask] = coords
sourceRect = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['sourceRect'][mask] = sourceRect
#for rec in dataSet:
#if rec['fragCoords'] is None:
#invalidate = True
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
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)
if invalidate:
self.invalidate()
@ -669,29 +676,42 @@ class ScatterPlotItem(GraphicsObject):
self.prepareGeometryChange()
GraphicsObject.viewTransformChanged(self)
self.bounds = [None, None]
self.fragments = 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))
self.data['targetRect'] = None
def setExportMode(self, *args, **kwds):
GraphicsObject.setExportMode(self, *args, **kwds)
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
def paint(self, p, *args):
@ -707,29 +727,44 @@ class ScatterPlotItem(GraphicsObject):
scale = 1.0
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()
if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False:
p.drawPixmapFragments(self.fragments, atlas)
else:
p.setRenderHint(p.Antialiasing, aa)
# Map point coordinates to device
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()
for i in range(len(self.data)):
rec = self.data[i]
frag = self.fragments[i]
# 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:
# render each symbol individually
p.setRenderHint(p.Antialiasing, aa)
data = self.data[viewMask]
pts = pts[:,viewMask]
for i, rec in enumerate(data):
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))
else:
if self.picture is None:
@ -891,7 +926,7 @@ class SpotItem(object):
self._data['data'] = data
def updateItem(self):
self._data['fragCoords'] = None
self._data['sourceRect'] = None
self._plot.updateSpots(self._data.reshape(1))
self._plot.invalidate()

View 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()

View File

@ -198,6 +198,7 @@ class ImageView(QtGui.QWidget):
if not isinstance(img, np.ndarray):
raise Exception("Image must be specified as ndarray.")
self.image = img
self.imageDisp = None
if xvals is not None:
self.tVals = xvals

View File

@ -1,41 +1,212 @@
from ..Qt import QtGui, QtCore
from ..SignalProxy import SignalProxy
from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode
class ComboBox(QtGui.QComboBox):
"""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):
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:
self.addItems(items)
self.setItems(items)
if default is not None:
self.setValue(default)
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:
return
raise ValueError(text)
#self.value = value
self.setCurrentIndex(ind)
def updateList(self, items):
prevVal = str(self.currentText())
try:
self.setCurrentIndex(ind)
def value(self):
"""
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)
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.addItems(items)
self.setValue(prevVal)
finally:
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())
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()

View 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)