import numpy as np
from .. import functions as fn
from .. import getConfigOption
from .. import Qt
from ..Qt import QtCore, QtGui
from .GraphicsObject import GraphicsObject
__all__ = ['BarGraphItem']
[docs]
class BarGraphItem(GraphicsObject):
[docs]
def __init__(self, **opts):
"""
Valid keyword options are:
x, x0, x1, y, y0, y1, width, height, pen, brush
x specifies the x-position of the center of the bar.
x0, x1 specify left and right edges of the bar, respectively.
width specifies distance from x0 to x1.
You may specify any combination:
x, width
x0, width
x1, width
x0, x1
Likewise y, y0, y1, and height.
If only height is specified, then y0 will be set to 0
Example uses:
BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5)
"""
GraphicsObject.__init__(self)
self.opts = dict(
x=None,
y=None,
x0=None,
y0=None,
x1=None,
y1=None,
name=None,
height=None,
width=None,
pen=None,
brush=None,
pens=None,
brushes=None,
)
if 'pen' not in opts:
opts['pen'] = getConfigOption('foreground')
if 'brush' not in opts:
opts['brush'] = (128, 128, 128)
# the first call to _updateColors() will thus always be an update
self._rectarray = Qt.internals.PrimitiveArray(QtCore.QRectF, 4)
self._shape = None
self.picture = None
self.setOpts(**opts)
def setOpts(self, **opts):
self.opts.update(opts)
self.picture = None
self._shape = None
self._prepareData()
self._updateColors(opts)
self.prepareGeometryChange()
self.update()
self.informViewBoundsChanged()
def _updatePenWidth(self, pen):
no_pen = pen is None or pen.style() == QtCore.Qt.PenStyle.NoPen
if no_pen:
return
idx = pen.isCosmetic()
self._penWidth[idx] = max(self._penWidth[idx], pen.widthF())
def _updateColors(self, opts):
# the logic here is to permit the user to update only data
# without updating pens/brushes
# update only if fresh pen/pens supplied
if 'pen' in opts or 'pens' in opts:
self._penWidth = [0, 0]
if self.opts['pens'] is None:
# pens not configured, use single pen
pen = self.opts['pen']
pen = fn.mkPen(pen)
self._updatePenWidth(pen)
self._sharedPen = pen
self._pens = None
else:
# pens configured, ignore single pen (if any)
pens = []
for pen in self.opts['pens']:
if not isinstance(pen, QtGui.QPen):
pen = fn.mkPen(pen)
pens.append(pen)
self._updatePenWidth(pen)
self._sharedPen = None
self._pens = pens
# update only if fresh brush/brushes supplied
if 'brush' in opts or 'brushes' in opts:
if self.opts['brushes'] is None:
# brushes not configured, use single brush
brush = self.opts['brush']
self._sharedBrush = fn.mkBrush(brush)
self._brushes = None
else:
# brushes configured, ignore single brush (if any)
brushes = []
for brush in self.opts['brushes']:
if not isinstance(brush, QtGui.QBrush):
brush = fn.mkBrush(brush)
brushes.append(brush)
self._sharedBrush = None
self._brushes = brushes
self._singleColor = (
self._sharedPen is not None and
self._sharedBrush is not None
)
def _getNormalizedCoords(self):
def asarray(x):
if x is None or np.isscalar(x) or isinstance(x, np.ndarray):
return x
return np.array(x)
x = asarray(self.opts.get('x'))
x0 = asarray(self.opts.get('x0'))
x1 = asarray(self.opts.get('x1'))
width = asarray(self.opts.get('width'))
if x0 is None:
if width is None:
raise Exception('must specify either x0 or width')
if x1 is not None:
x0 = x1 - width
elif x is not None:
x0 = x - width/2.
else:
raise Exception('must specify at least one of x, x0, or x1')
if width is None:
if x1 is None:
raise Exception('must specify either x1 or width')
width = x1 - x0
y = asarray(self.opts.get('y'))
y0 = asarray(self.opts.get('y0'))
y1 = asarray(self.opts.get('y1'))
height = asarray(self.opts.get('height'))
if y0 is None:
if height is None:
y0 = 0
elif y1 is not None:
y0 = y1 - height
elif y is not None:
y0 = y - height/2.
else:
y0 = 0
if height is None:
if y1 is None:
raise Exception('must specify either y1 or height')
height = y1 - y0
# ensure x0 < x1 and y0 < y1
t0, t1 = x0, x0 + width
x0 = np.minimum(t0, t1, dtype=np.float64)
x1 = np.maximum(t0, t1, dtype=np.float64)
t0, t1 = y0, y0 + height
y0 = np.minimum(t0, t1, dtype=np.float64)
y1 = np.maximum(t0, t1, dtype=np.float64)
# here, all of x0, y0, x1, y1 are numpy objects,
# BUT could possibly be numpy scalars
return x0, y0, x1, y1
def _prepareData(self):
x0, y0, x1, y1 = self._getNormalizedCoords()
if x0.size == 0 or y0.size == 0:
self._dataBounds = (None, None), (None, None)
self._rectarray.resize(0)
return
xmn, xmx = np.min(x0), np.max(x1)
ymn, ymx = np.min(y0), np.max(y1)
self._dataBounds = (xmn, xmx), (ymn, ymx)
self._rectarray.resize(max(x0.size, y0.size))
memory = self._rectarray.ndarray()
memory[:, 0] = x0
memory[:, 1] = y0
memory[:, 2] = x1 - x0
memory[:, 3] = y1 - y0
def _render(self, painter):
multi_pen = self._pens is not None
multi_brush = self._brushes is not None
no_pen = (
not multi_pen
and self._sharedPen.style() == QtCore.Qt.PenStyle.NoPen
)
rects = self._rectarray.instances()
if no_pen and multi_brush:
for idx, rect in enumerate(rects):
painter.fillRect(rect, self._brushes[idx])
else:
if not multi_pen:
painter.setPen(self._sharedPen)
if not multi_brush:
painter.setBrush(self._sharedBrush)
for idx, rect in enumerate(rects):
if multi_pen:
painter.setPen(self._pens[idx])
if multi_brush:
painter.setBrush(self._brushes[idx])
painter.drawRect(rect)
def drawPicture(self):
self.picture = QtGui.QPicture()
painter = QtGui.QPainter(self.picture)
self._render(painter)
painter.end()
def paint(self, p, *args):
if self._singleColor:
p.setPen(self._sharedPen)
p.setBrush(self._sharedBrush)
drawargs = self._rectarray.drawargs()
p.drawRects(*drawargs)
else:
if self.picture is None:
self.drawPicture()
self.picture.play(p)
def shape(self):
if self._shape is None:
shape = QtGui.QPainterPath()
rects = self._rectarray.instances()
for rect in rects:
shape.addRect(rect)
self._shape = shape
return self._shape
def implements(self, interface=None):
ints = ['plotData']
if interface is None:
return ints
return interface in ints
def name(self):
return self.opts.get('name', None)
def getData(self):
return self.opts.get('x'), self.opts.get('height')
def dataBounds(self, ax, frac=1.0, orthoRange=None):
# _penWidth is available after _updateColors()
pw = self._penWidth[0] * 0.5
# _dataBounds is available after _prepareData()
bounds = self._dataBounds[ax]
if bounds[0] is None or bounds[1] is None:
return None, None
return (bounds[0] - pw, bounds[1] + pw)
def pixelPadding(self):
# _penWidth is available after _updateColors()
pw = (self._penWidth[1] or 1) * 0.5
return pw
def boundingRect(self):
xmn, xmx = self.dataBounds(ax=0)
if xmn is None or xmx is None:
return QtCore.QRectF()
ymn, ymx = self.dataBounds(ax=1)
if ymn is None or ymx is None:
return QtCore.QRectF()
px = py = 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
return QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)