Commit ad7b5f0a authored by Luke Campagnola's avatar Luke Campagnola
Browse files

- Default foreground / background colors can now be set using pyqtgraph.setConfigOption()

- Added pyqtgraph.systemInfo() for bug reporting
- GraphicsLayout does a better job of avoiding occupied cells when using automatic placement
- Fixed sizing issues with LabelItem
- Updated GraphicsLayout example
parent debe847f
## Do all Qt imports from here to allow easier PyQt / PySide compatibility
#from PySide import QtGui, QtCore, QtOpenGL, QtSvg
from PyQt4 import QtGui, QtCore
try:
from PyQt4 import QtSvg
except ImportError:
pass
try:
from PyQt4 import QtOpenGL
except ImportError:
pass
USE_PYSIDE = False ## If False, import PyQt4. If True, import PySide
if USE_PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
VERSION_INFO = 'PySide ' + PySide.__version__
else:
from PyQt4 import QtGui, QtCore
try:
from PyQt4 import QtSvg
except ImportError:
pass
try:
from PyQt4 import QtOpenGL
except ImportError:
pass
if not hasattr(QtCore, 'Signal'):
QtCore.Signal = QtCore.pyqtSignal
VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
# -*- coding: utf-8 -*-
REVISION = '621'
### import all the goodies and add some helper functions for easy CLI use
## 'Qt' is a local module; it is intended mainly to cover up the differences
## between PyQt4 and PySide.
from .Qt import QtGui
from .Qt import QtGui
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
#if QtGui.QApplication.instance() is None:
......@@ -30,13 +32,15 @@ else:
useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff.
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
'foregroundColor': (200,200,200),
'backgroundColor': (0,0,0),
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
'background': (0, 0, 0), ## default background for GraphicsWidget
'antialias': False,
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
}
def setConfigOption(opt, value):
CONFIG_OPTIONS[opt] = value
......@@ -44,6 +48,16 @@ def getConfigOption(opt):
return CONFIG_OPTIONS[opt]
def systemInfo():
print "sys.platform:", sys.platform
print "sys.version:", sys.version
from .Qt import VERSION_INFO
print "qt bindings:", VERSION_INFO
print "pyqtgraph:", REVISION
print "config:"
import pprint
pprint.pprint(CONFIG_OPTIONS)
## Rename orphaned .pyc files. This is *probably* safe :)
def renamePyc(startDir):
......
......@@ -4,41 +4,73 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import user
import numpy as np
app = QtGui.QApplication([])
view = pg.GraphicsView()
l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255))
l = pg.GraphicsLayout(border=(100,100,100))
view.setCentralItem(l)
view.show()
view.resize(800,600)
## Title at top
text = """
This example demonstrates the use of GraphicsLayout to arrange items in a grid.<br>
The items added to the layout must be subclasses of QGraphicsWidget (this includes <br>
PlotItem, ViewBox, LabelItem, and GrphicsLayout itself).
"""
l.addLabel(text, col=1, colspan=4)
l.nextRow()
## Put vertical label on left side
l.addLabel('Long Vertical Label', angle=-90, rowspan=3)
## Add 3 plots into the first row (automatic position)
p1 = l.addPlot()
p2 = l.addPlot()
p3 = l.addPlot()
p1 = l.addPlot(title="Plot 1")
p2 = l.addPlot(title="Plot 2")
vb = l.addViewBox(lockAspect=True)
img = pg.ImageItem(np.random.normal(size=(100,100)))
vb.addItem(img)
vb.autoRange()
## Add a viewbox into the second row (automatic position)
## Add a sub-layout into the second row (automatic position)
## The added item should avoid the first column, which is already filled
l.nextRow()
vb = l.addViewBox(colspan=3)
l2 = l.addLayout(colspan=3, border=(50,0,0))
l2.setContentsMargins(10, 10, 10, 10)
l2.addLabel("Sub-layout: this layout demonstrates the use of shared axes and axis labels", colspan=3)
l2.nextRow()
l2.addLabel('Vertical Axis Label', angle=-90, rowspan=2)
p21 = l2.addPlot()
p22 = l2.addPlot()
l2.nextRow()
p23 = l2.addPlot()
p24 = l2.addPlot()
l2.nextRow()
l2.addLabel("HorizontalAxisLabel", col=1, colspan=2)
## Add 2 more plots into the third row (manual position)
p4 = l.addPlot(row=2, col=0)
p5 = l.addPlot(row=2, col=1, colspan=2)
## hide axes on some plots
p21.hideAxis('bottom')
p22.hideAxis('bottom')
p22.hideAxis('left')
p24.hideAxis('left')
p21.hideButtons()
p22.hideButtons()
p23.hideButtons()
p24.hideButtons()
## Add 2 more plots into the third row (manual position)
p4 = l.addPlot(row=3, col=1)
p5 = l.addPlot(row=3, col=2, colspan=2)
## show some content
## show some content in the plots
p1.plot([1,3,2,4,3,5])
p2.plot([1,3,2,4,3,5])
p3.plot([1,3,2,4,3,5])
p4.plot([1,3,2,4,3,5])
p5.plot([1,3,2,4,3,5])
b = QtGui.QGraphicsRectItem(0, 0, 1, 1)
b.setPen(pg.mkPen(255,255,0))
vb.addItem(b)
vb.setRange(QtCore.QRectF(-1, -1, 3, 3))
## Start Qt event loop unless running in interactive mode.
......
......@@ -4,6 +4,7 @@ from pyqtgraph.Point import Point
import pyqtgraph.debug as debug
import weakref
import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget
__all__ = ['AxisItem']
......@@ -65,8 +66,6 @@ class AxisItem(GraphicsWidget):
self.setRange(0, 1)
if pen is None:
pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
self.setPen(pen)
self._linkedView = None
......@@ -189,8 +188,18 @@ class AxisItem(GraphicsWidget):
self.setMaximumWidth(w)
self.setMinimumWidth(w)
def pen(self):
if self._pen is None:
return fn.mkPen(pg.getConfigOption('foreground'))
return self._pen
def setPen(self, pen):
self.pen = pen
"""
Set the pen used for drawing text, axes, ticks, and grid lines.
if pen == None, the default will be used (see :func:`setConfigOption
<pyqtgraph.setConfigOption>`)
"""
self._pen = pen
self.picture = None
self.update()
......@@ -370,8 +379,6 @@ class AxisItem(GraphicsWidget):
"""
minVal, maxVal = sorted((minVal, maxVal))
if self.logMode:
return self.logTickValues(minVal, maxVal, size)
ticks = []
tickLevels = self.tickSpacing(minVal, maxVal, size)
......@@ -391,18 +398,33 @@ class AxisItem(GraphicsWidget):
values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values)
allValues = np.concatenate([allValues, values])
ticks.append((spacing, values))
if self.logMode:
return self.logTickValues(minVal, maxVal, size, ticks)
return ticks
def logTickValues(self, minVal, maxVal, size):
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
major = list(range(v1+1, v2))
minor = []
for v in range(v1, v2):
minor.extend(v + np.log10(np.arange(1, 10)))
minor = [x for x in minor if x>minVal and x<maxVal]
return [(1.0, major), (None, minor)]
def logTickValues(self, minVal, maxVal, size, stdTicks):
## start with the tick spacing given by tickValues().
## Any level whose spacing is < 1 needs to be converted to log scale
ticks = []
for (spacing, t) in stdTicks:
if spacing >= 1.0:
ticks.append((spacing, t))
if len(ticks) < 3:
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
#major = list(range(v1+1, v2))
minor = []
for v in range(v1, v2):
minor.extend(v + np.log10(np.arange(1, 10)))
minor = [x for x in minor if x>minVal and x<maxVal]
ticks.append((None, minor))
return ticks
def tickStrings(self, values, scale, spacing):
"""Return the strings that should be placed next to ticks. This method is called
......@@ -477,7 +499,7 @@ class AxisItem(GraphicsWidget):
#print tickStart, tickStop, span
## draw long line along axis
p.setPen(self.pen)
p.setPen(self.pen())
p.drawLine(*span)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
......@@ -542,7 +564,11 @@ class AxisItem(GraphicsWidget):
p2[axis] = tickStop
if self.grid is False:
p2[axis] += tickLength*tickDir
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
tickPen = self.pen()
color = tickPen.color()
color.setAlpha(lineAlpha)
tickPen.setColor(color)
p.setPen(tickPen)
p.drawLine(Point(p1), Point(p2))
tickPositions[i].append(x)
prof.mark('draw ticks')
......@@ -594,7 +620,7 @@ class AxisItem(GraphicsWidget):
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150)))
p.setPen(self.pen())
p.drawText(rect, textFlags, vstr)
prof.mark('draw text')
prof.finish()
......
......@@ -175,7 +175,7 @@ class TickSliderItem(GraphicsWidget):
def resizeEvent(self, ev):
wlen = max(40, self.widgetLength())
self.setLength(wlen-self.tickSize)
self.setLength(wlen-self.tickSize-2)
self.setOrientation(self.orientation)
#bounds = self.scene().itemsBoundingRect()
#bounds.setLeft(min(-self.tickSize*0.5, bounds.left()))
......@@ -186,7 +186,7 @@ class TickSliderItem(GraphicsWidget):
def setLength(self, newLen):
#private
for t, x in list(self.ticks.items()):
t.setPos(x * newLen, t.pos().y())
t.setPos(x * newLen + 1, t.pos().y())
self.length = float(newLen)
#def mousePressEvent(self, ev):
......@@ -491,8 +491,8 @@ class GradientEditorItem(TickSliderItem):
def setLength(self, newLen):
#private (but maybe public)
TickSliderItem.setLength(self, newLen)
self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize)
self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize)
self.backgroundRect.setRect(1, -self.rectSize, newLen, self.rectSize)
self.gradRect.setRect(1, -self.rectSize, newLen, self.rectSize)
self.updateGradient()
def currentColorChanged(self, color):
......
......@@ -21,21 +21,29 @@ class GraphicsLayout(GraphicsWidget):
self.border = border
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
self.items = {}
self.rows = {}
self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item
self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item
self.currentRow = 0
self.currentCol = 0
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
#def resizeEvent(self, ev):
#ret = GraphicsWidget.resizeEvent(self, ev)
#print self.pos(), self.mapToDevice(self.rect().topLeft())
#return ret
def nextRow(self):
"""Advance to next row for automatic item placement"""
self.currentRow += 1
self.currentCol = 0
self.currentCol = -1
self.nextColumn()
def nextColumn(self, colspan=1):
"""Advance to next column, while returning the current column number
def nextColumn(self):
"""Advance to next available column
(generally only for internal use--called by addItem)"""
self.currentCol += colspan
return self.currentCol-colspan
self.currentCol += 1
while self.getItem(self.currentRow, self.currentCol) is not None:
self.currentCol += 1
def nextCol(self, *args, **kargs):
"""Alias of nextColumn"""
......@@ -66,6 +74,8 @@ class GraphicsLayout(GraphicsWidget):
Create a LabelItem with *text* and place it in the next available cell (or in the cell specified)
All extra keyword arguments are passed to :func:`LabelItem.__init__ <pyqtgraph.LabelItem.__init__>`
Returns the created item.
To create a vertical label, use *angle*=-90
"""
text = LabelItem(text, **kargs)
self.addItem(text, row, col, rowspan, colspan)
......@@ -89,18 +99,24 @@ class GraphicsLayout(GraphicsWidget):
if row is None:
row = self.currentRow
if col is None:
col = self.nextCol(colspan)
col = self.currentCol
if row not in self.rows:
self.rows[row] = {}
self.rows[row][col] = item
self.items[item] = (row, col)
self.items[item] = []
for i in range(rowspan):
for j in range(colspan):
row2 = row + i
col2 = col + j
if row2 not in self.rows:
self.rows[row2] = {}
self.rows[row2][col2] = item
self.items[item].append((row2, col2))
self.layout.addItem(item, row, col, rowspan, colspan)
self.nextColumn()
def getItem(self, row, col):
"""Return the item in (*row*, *col*)"""
return self.row[row][col]
"""Return the item in (*row*, *col*). If the cell is empty, return None."""
return self.rows.get(row, {}).get(col, None)
def boundingRect(self):
return self.rect()
......@@ -124,9 +140,10 @@ class GraphicsLayout(GraphicsWidget):
ind = self.itemIndex(item)
self.layout.removeAt(ind)
self.scene().removeItem(item)
r,c = self.items[item]
for r,c in self.items[item]:
del self.rows[r][c]
del self.items[item]
del self.rows[r][c]
self.update()
def clear(self):
......
......@@ -50,7 +50,7 @@ class HistogramLUTItem(GraphicsWidget):
self.layout.setSpacing(0)
self.vb = ViewBox()
self.vb.setMaximumWidth(152)
self.vb.setMinimumWidth(52)
self.vb.setMinimumWidth(45)
self.vb.setMouseEnabled(x=False, y=True)
self.gradient = GradientEditorItem()
self.gradient.setOrientation('right')
......
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget
......@@ -18,14 +19,13 @@ class LabelItem(GraphicsWidget):
GraphicsWidget.__init__(self, parent)
self.item = QtGui.QGraphicsTextItem(self)
self.opts = {
'color': 'CCC',
'color': None,
'justify': 'center'
}
self.opts.update(args)
self.sizeHint = {}
self._sizeHint = {}
self.setText(text)
self.setAngle(angle)
def setAttr(self, attr, value):
"""Set default text properties. See setText() for accepted parameters."""
......@@ -44,15 +44,17 @@ class LabelItem(GraphicsWidget):
==================== ==============================
"""
self.text = text
opts = self.opts.copy()
opts = self.opts
for k in args:
opts[k] = args[k]
optlist = []
if 'color' in opts:
if isinstance(opts['color'], QtGui.QColor):
opts['color'] = fn.colorStr(opts['color'])[:6]
optlist.append('color: #' + opts['color'])
color = self.opts['color']
if color is None:
color = pg.getConfigOption('foreground')
color = fn.mkColor(color)
optlist.append('color: #' + fn.colorStr(color)[:6])
if 'size' in opts:
optlist.append('font-size: ' + opts['size'])
if 'bold' in opts and opts['bold'] in [True, False]:
......@@ -64,7 +66,7 @@ class LabelItem(GraphicsWidget):
self.item.setHtml(full)
self.updateMin()
self.resizeEvent(None)
self.update()
self.updateGeometry()
def resizeEvent(self, ev):
#c1 = self.boundingRect().center()
......@@ -72,16 +74,35 @@ class LabelItem(GraphicsWidget):
#dif = c1 - c2
#self.item.moveBy(dif.x(), dif.y())
#print c1, c2, dif, self.item.pos()
self.item.setPos(0,0)
bounds = self.itemRect()
left = self.mapFromItem(self.item, QtCore.QPointF(0,0)) - self.mapFromItem(self.item, QtCore.QPointF(1,0))
rect = self.rect()
if self.opts['justify'] == 'left':
self.item.setPos(0,0)
if left.x() != 0:
bounds.moveLeft(rect.left())
if left.y() < 0:
bounds.moveTop(rect.top())
elif left.y() > 0:
bounds.moveBottom(rect.bottom())
elif self.opts['justify'] == 'center':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width()/2. - bounds.width()/2., 0)
bounds.moveCenter(rect.center())
#bounds = self.itemRect()
#self.item.setPos(self.width()/2. - bounds.width()/2., 0)
elif self.opts['justify'] == 'right':
bounds = self.item.mapRectToParent(self.item.boundingRect())
self.item.setPos(self.width() - bounds.width(), 0)
#if self.width() > 0:
#self.item.setTextWidth(self.width())
if left.x() != 0:
bounds.moveRight(rect.right())
if left.y() < 0:
bounds.moveBottom(rect.bottom())
elif left.y() > 0:
bounds.moveTop(rect.top())
#bounds = self.itemRect()
#self.item.setPos(self.width() - bounds.width(), 0)
self.item.setPos(bounds.topLeft() - self.itemRect().topLeft())
self.updateMin()
def setAngle(self, angle):
self.angle = angle
......@@ -89,27 +110,31 @@ class LabelItem(GraphicsWidget):
self.item.rotate(angle)
self.updateMin()
def updateMin(self):
bounds = self.item.mapRectToParent(self.item.boundingRect())
bounds = self.itemRect()
self.setMinimumWidth(bounds.width())
self.setMinimumHeight(bounds.height())
self.sizeHint = {
self._sizeHint = {
QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()),
QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()),
QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2),
QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this?
}
self.update()
self.updateGeometry()
def sizeHint(self, hint, constraint):
if hint not in self.sizeHint:
if hint not in self._sizeHint:
return QtCore.QSizeF(0, 0)
return QtCore.QSizeF(*self.sizeHint[hint])
return QtCore.QSizeF(*self._sizeHint[hint])
def itemRect(self):
return self.item.mapRectToParent(self.item.boundingRect())
#def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.rect())
#p.drawRect(self.item.boundingRect())
#p.setPen(fn.mkPen('g'))
#p.drawRect(self.itemRect())
......@@ -105,6 +105,8 @@ class ViewBox(GraphicsWidget):
'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,
'enableMenu': enableMenu,
'wheelScaleFactor': -1.0 / 8.0,
'background': None,
}
......@@ -118,11 +120,17 @@ class ViewBox(GraphicsWidget):
self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses
## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
## this is a workaround for a Qt + OpenGL but that causes improper clipping
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
self.childGroup = ChildGroup(self)
self.childGroup.sigItemsChanged.connect(self.itemsChanged)
self.background = QtGui.QGraphicsRectItem(self.rect())
self.background.setParentItem(self)
self.background.setZValue(-1e6)
self.background.setPen(fn.mkPen(None))
self.updateBackground()
#self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
# this also enables capture of keyPressEvents.
......@@ -286,6 +294,7 @@ class ViewBox(GraphicsWidget):
#self.updateAutoRange()
self.updateMatrix()
self.sigStateChanged.emit(self)
self.background.setRect(self.rect())
#self.linkedXChanged()
#self.linkedYChanged()
......@@ -1155,6 +1164,16 @@ class ViewBox(GraphicsWidget):
#self.scene().render(p)
#p.end()
def updateBackground(self):
bg = self.state['background']
#print self, bg
if bg is None:
self.background.hide()
else:
self.background.show()
self.background.setBrush(fn.mkBrush(bg))
def updateViewLists(self):
def cmpViews(a, b):
wins = 100 * cmp(a.window() is self.window(), b.window() is self.window())
......