Commit 194eba1e authored by Luke Campagnola's avatar Luke Campagnola
Browse files

merge from inp

parents 4c887c8f 63c3b36a
......@@ -566,8 +566,8 @@ def transformCoordinates(tr, coords, transpose=False):
def solve3DTransform(points1, points2):
"""
Find a 3D transformation matrix that maps points1 onto points2
points must be specified as a list of 4 Vectors.
Find a 3D transformation matrix that maps points1 onto points2.
Points must be specified as a list of 4 Vectors.
"""
if not HAVE_SCIPY:
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
......@@ -583,8 +583,8 @@ def solve3DTransform(points1, points2):
def solveBilinearTransform(points1, points2):
"""
Find a bilinear transformation matrix (2x4) that maps points1 onto points2
points must be specified as a list of 4 Vector, Point, QPointF, etc.
Find a bilinear transformation matrix (2x4) that maps points1 onto points2.
Points must be specified as a list of 4 Vector, Point, QPointF, etc.
To use this matrix to map a point [x,y]::
......@@ -951,8 +951,15 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
ch = ctypes.c_char.from_buffer(imgData, 0)
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
else:
addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
#addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
addr = ctypes.c_char.from_buffer(imgData, 0)
try:
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
except TypeError:
addr = ctypes.addressof(addr)
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData
return img
#try:
......@@ -1043,7 +1050,7 @@ def colorToAlpha(data, color):
def arrayToQPath(x, y, connect='all'):
"""Convert an array of x,y coordinats to QPath as efficiently as possible.
"""Convert an array of x,y coordinats to QPainterPath as efficiently as possible.
The *connect* argument may be 'all', indicating that each point should be
connected to the next; 'pairs', indicating that each pair of points
should be connected, or an array of int32 values (0 or 1) indicating
......
......@@ -12,6 +12,10 @@ class ArrowItem(QtGui.QGraphicsPathItem):
def __init__(self, **opts):
"""
Arrows can be initialized with any keyword arguments accepted by
the setStyle() method.
"""
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
if 'size' in opts:
opts['headLen'] = opts['size']
......@@ -40,6 +44,32 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.moveBy(*self.opts['pos'])
def setStyle(self, **opts):
"""
Changes the appearance of the arrow.
All arguments are optional:
================= =================================================
Keyword Arguments
angle Orientation of the arrow in degrees. Default is
0; arrow pointing to the left.
headLen Length of the arrow head, from tip to base.
default=20
headWidth Width of the arrow head at its base.
tipAngle Angle of the tip of the arrow in degrees. Smaller
values make a 'sharper' arrow. If tipAngle is
specified, ot overrides headWidth. default=25
baseAngle Angle of the base of the arrow head. Default is
0, which means that the base of the arrow head
is perpendicular to the arrow shaft.
tailLen Length of the arrow tail, measured from the base
of the arrow head to the tip of the tail. If
this value is None, no tail will be drawn.
default=None
tailWidth Width of the tail. default=3
pen The pen used to draw the outline of the arrow.
brush The brush used to fill the arrow.
================= =================================================
"""
self.opts = opts
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
......
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from .GraphicsObject import GraphicsObject
__all__ = ['ErrorBarItem']
class ErrorBarItem(GraphicsObject):
def __init__(self, **opts):
"""
Valid keyword options are:
x, y, height, width, top, bottom, left, right, beam, pen
x and y must be numpy arrays specifying the coordinates of data points.
height, width, top, bottom, left, right, and beam may be numpy arrays,
single values, or None to disable. All values should be positive.
If height is specified, it overrides top and bottom.
If width is specified, it overrides left and right.
"""
GraphicsObject.__init__(self)
self.opts = dict(
x=None,
y=None,
height=None,
width=None,
top=None,
bottom=None,
left=None,
right=None,
beam=None,
pen=None
)
self.setOpts(**opts)
def setOpts(self, **opts):
self.opts.update(opts)
self.path = None
self.update()
self.informViewBoundsChanged()
def drawPath(self):
p = QtGui.QPainterPath()
x, y = self.opts['x'], self.opts['y']
if x is None or y is None:
return
beam = self.opts['beam']
height, top, bottom = self.opts['height'], self.opts['top'], self.opts['bottom']
if height is not None or top is not None or bottom is not None:
## draw vertical error bars
if height is not None:
y1 = y - height/2.
y2 = y + height/2.
else:
if bottom is None:
y1 = y
else:
y1 = y - bottom
if top is None:
y2 = y
else:
y2 = y + top
for i in range(len(x)):
p.moveTo(x[i], y1[i])
p.lineTo(x[i], y2[i])
if beam is not None and beam > 0:
x1 = x - beam/2.
x2 = x + beam/2.
if height is not None or top is not None:
for i in range(len(x)):
p.moveTo(x1[i], y2[i])
p.lineTo(x2[i], y2[i])
if height is not None or bottom is not None:
for i in range(len(x)):
p.moveTo(x1[i], y1[i])
p.lineTo(x2[i], y1[i])
width, right, left = self.opts['width'], self.opts['right'], self.opts['left']
if width is not None or right is not None or left is not None:
## draw vertical error bars
if width is not None:
x1 = x - width/2.
x2 = x + width/2.
else:
if left is None:
x1 = x
else:
x1 = x - left
if right is None:
x2 = x
else:
x2 = x + right
for i in range(len(x)):
p.moveTo(x1[i], y[i])
p.lineTo(x2[i], y[i])
if beam is not None and beam > 0:
y1 = y - beam/2.
y2 = y + beam/2.
if width is not None or right is not None:
for i in range(len(x)):
p.moveTo(x2[i], y1[i])
p.lineTo(x2[i], y2[i])
if width is not None or left is not None:
for i in range(len(x)):
p.moveTo(x1[i], y1[i])
p.lineTo(x1[i], y2[i])
self.path = p
self.prepareGeometryChange()
def paint(self, p, *args):
if self.path is None:
self.drawPath()
pen = self.opts['pen']
if pen is None:
pen = pg.getConfigOption('foreground')
p.setPen(pg.mkPen(pen))
p.drawPath(self.path)
def boundingRect(self):
if self.path is None:
self.drawPath()
return self.path.boundingRect()
\ No newline at end of file
from .. import functions as fn
from GraphicsObject import GraphicsObject
from ScatterPlotItem import ScatterPlotItem
from .GraphicsObject import GraphicsObject
from .ScatterPlotItem import ScatterPlotItem
import pyqtgraph as pg
import numpy as np
......@@ -8,8 +8,9 @@ __all__ = ['GraphItem']
class GraphItem(GraphicsObject):
"""A GraphItem displays graph information (as in 'graph theory', not 'graphics') as
a set of nodes connected by lines.
"""A GraphItem displays graph information as
a set of nodes connected by lines (as in 'graph theory', not 'graphics').
Useful for drawing networks, trees, etc.
"""
def __init__(self, **kwds):
......@@ -28,19 +29,24 @@ class GraphItem(GraphicsObject):
============ =========================================================
Arguments
pos (N,2) array of the positions of each node in the graph
pos (N,2) array of the positions of each node in the graph.
adj (M,2) array of connection data. Each row contains indexes
of two nodes that are connected.
pen The pen to use when drawing lines between connected
nodes. May be one of:
* QPen
* a single argument to pass to pg.mkPen
* a record array of length M
with fields (red, green, blue, alpha, width).
with fields (red, green, blue, alpha, width). Note
that using this option may have a significant performance
cost.
* None (to disable connection drawing)
* 'default' to use the default foreground color.
symbolPen The pen used for drawing nodes.
**opts All other keyword arguments are given to ScatterPlotItem
``**opts`` All other keyword arguments are given to
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
to affect the appearance of nodes (symbol, size, brush,
etc.)
============ =========================================================
......
......@@ -204,7 +204,8 @@ class GraphicsItem(object):
return tuple(map(Point, self._pixelVectorCache[1])) ## return a *copy*
## check global cache
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
#key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
key = (dt.m11(), dt.m21(), dt.m12(), dt.m22())
pv = self._pixelVectorGlobalCache.get(key, None)
if direction is None and pv is not None:
self._pixelVectorCache = [dt, pv]
......
......@@ -93,7 +93,7 @@ class PlotCurveItem(GraphicsObject):
(x, y) = self.getData()
if x is None or len(x) == 0:
return (0, 0)
return (None, None)
if ax == 0:
d = x
......@@ -102,20 +102,106 @@ class PlotCurveItem(GraphicsObject):
d = y
d2 = x
## If an orthogonal range is specified, mask the data now
if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
d2 = d2[mask]
## Get min/max (or percentiles) of the requested data range
if frac >= 1.0:
b = (d.min(), d.max())
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
else:
b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
## adjust for fill level
if ax == 1 and self.opts['fillLevel'] is not None:
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
## Add pen width only if it is non-cosmetic.
pen = self.opts['pen']
spen = self.opts['shadowPen']
if not pen.isCosmetic():
b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072)
if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
self._boundsCache[ax] = [(frac, orthoRange), b]
return b
def pixelPadding(self):
pen = self.opts['pen']
spen = self.opts['shadowPen']
w = 0
if pen.isCosmetic():
w += pen.widthF()*0.7072
if spen is not None and spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
w = max(w, spen.widthF()*0.7072)
return w
def boundingRect(self):
if self._boundingRect is None:
(xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1)
if xmn is None:
return QtCore.QRectF()
px = py = 0.0
pxPad = self.pixelPadding()
if pxPad > 0:
# determine length of pixel in local x, y directions
px, py = self.pixelVectors()
px = 0 if px is None else px.length()
py = 0 if py is None else py.length()
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
#px += self._maxSpotWidth * 0.5
#py += self._maxSpotWidth * 0.5
self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
return self._boundingRect
def viewTransformChanged(self):
self.invalidateBounds()
self.prepareGeometryChange()
#def boundingRect(self):
#if self._boundingRect is None:
#(x, y) = self.getData()
#if x is None or y is None or len(x) == 0 or len(y) == 0:
#return QtCore.QRectF()
#if self.opts['shadowPen'] is not None:
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
#else:
#lineWidth = (self.opts['pen'].width()+1)
#pixels = self.pixelVectors()
#if pixels == (None, None):
#pixels = [Point(0,0), Point(0,0)]
#xmin = x.min()
#xmax = x.max()
#ymin = y.min()
#ymax = y.max()
#if self.opts['fillLevel'] is not None:
#ymin = min(ymin, self.opts['fillLevel'])
#ymax = max(ymax, self.opts['fillLevel'])
#xmin -= pixels[0].x() * lineWidth
#xmax += pixels[0].x() * lineWidth
#ymin -= abs(pixels[1].y()) * lineWidth
#ymax += abs(pixels[1].y()) * lineWidth
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
#return self._boundingRect
def invalidateBounds(self):
self._boundingRect = None
......@@ -280,40 +366,6 @@ class PlotCurveItem(GraphicsObject):
return QtGui.QPainterPath()
return self.path
def boundingRect(self):
if self._boundingRect is None:
(x, y) = self.getData()
if x is None or y is None or len(x) == 0 or len(y) == 0:
return QtCore.QRectF()
if self.opts['shadowPen'] is not None:
lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else:
lineWidth = (self.opts['pen'].width()+1)
pixels = self.pixelVectors()
if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)]
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['fillLevel'] is not None:
ymin = min(ymin, self.opts['fillLevel'])
ymax = max(ymax, self.opts['fillLevel'])
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
return self._boundingRect
def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
if self.xData is None:
......
......@@ -471,33 +471,57 @@ class PlotDataItem(GraphicsObject):
and max)
=============== =============================================================
"""
if frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
(x, y) = self.getData()
if x is None or len(x) == 0:
return None
range = [None, None]
if self.curve.isVisible():
range = self.curve.dataBounds(ax, frac, orthoRange)
elif self.scatter.isVisible():
r2 = self.scatter.dataBounds(ax, frac, orthoRange)
range = [
r2[0] if range[0] is None else (range[0] if r2[0] is None else min(r2[0], range[0])),
r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1]))
]
return range
#if frac <= 0.0:
#raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
#(x, y) = self.getData()
#if x is None or len(x) == 0:
#return None
if ax == 0:
d = x
d2 = y
elif ax == 1:
d = y
d2 = x
#if ax == 0:
#d = x
#d2 = y
#elif ax == 1:
#d = y
#d2 = x
if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
#d2 = d2[mask]
#if orthoRange is not None:
#mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
#d = d[mask]
##d2 = d2[mask]
if len(d) > 0:
if frac >= 1.0:
return (np.min(d), np.max(d))
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
else:
return None
#if len(d) > 0:
#if frac >= 1.0:
#return (np.min(d), np.max(d))
#else:
#return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
#else:
#return None
def pixelPadding(self):
"""
Return the size in pixels that this item may draw beyond the values returned by dataBounds().
This method is called by ViewBox when auto-scaling.
"""
pad = 0
if self.curve.isVisible():
pad = max(pad, self.curve.pixelPadding())
elif self.scatter.isVisible():
pad = max(pad, self.scatter.pixelPadding())
return pad
def clear(self):
#for i in self.curves+self.scatters:
......
......@@ -60,7 +60,7 @@ def renderSymbol(symbol, size, pen, brush, device=None):
#return SymbolPixmapCache[key]
## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.width()), 1)
penPxWidth = max(np.ceil(pen.widthF()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0)
p = QtGui.QPainter(image)
......@@ -115,7 +115,7 @@ class SymbolAtlas(object):
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.width(), pen.style(), fn.colorTuple(brush.color()))
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
......@@ -472,8 +472,8 @@ class ScatterPlotItem(GraphicsObject):
if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol
if kargs['mask'] is not None:
symbols = symbols[kargs['mask']]
if mask is not None:
symbols = symbols[mask]
if len(symbols) != len(dataSet):
raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet)))
dataSet['symbol'] = symbols
......@@ -589,13 +589,13 @@ class ScatterPlotItem(GraphicsObject):
width = 0
pxWidth = 0
if self.opts['pxMode']:
pxWidth = size + pen.width()
pxWidth = size + pen.widthF()
else:
width = size
if pen.isCosmetic():
pxWidth += pen.width()
pxWidth += pen.widthF()
else:
width += pen.width()
width += pen.widthF()
self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None]
......@@ -629,31 +629,15 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask]
if frac >= 1.0:
## increase size of bounds based on spot size and pen width
#px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis
px = self.pixelVectors()[ax]
if px is None:
px = 0
else:
px = px.length()
minIndex = np.argmin(d)
maxIndex = np.argmax(d)
minVal = d[minIndex]
maxVal = d[maxIndex]
spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072)
return self.bounds[ax]
elif frac <= 0.0: