Merge pull request #428 from campagnola/datatree-arrays
DataTreeWidget updates
This commit is contained in:
commit
c8c3b7521b
@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# for generating a traceback object to display
|
||||||
|
def some_func1():
|
||||||
|
return some_func2()
|
||||||
|
def some_func2():
|
||||||
|
try:
|
||||||
|
raise Exception()
|
||||||
|
except:
|
||||||
|
import sys
|
||||||
|
return sys.exc_info()[2]
|
||||||
|
|
||||||
|
|
||||||
app = QtGui.QApplication([])
|
app = QtGui.QApplication([])
|
||||||
d = {
|
d = {
|
||||||
'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
|
'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
|
||||||
'dict1': {
|
'a dict': {
|
||||||
'x': 1,
|
'x': 1,
|
||||||
'y': 2,
|
'y': 2,
|
||||||
'z': 'three'
|
'z': 'three'
|
||||||
},
|
},
|
||||||
'array1 (20x20)': np.ones((10,10))
|
'an array': np.random.randint(10, size=(40,10)),
|
||||||
|
'a traceback': some_func1(),
|
||||||
|
'a function': some_func1,
|
||||||
|
'a class': pg.DataTreeWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
tree = pg.DataTreeWidget(data=d)
|
tree = pg.DataTreeWidget(data=d)
|
||||||
|
52
examples/DiffTreeWidget.py
Normal file
52
examples/DiffTreeWidget.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Simple use of DiffTreeWidget to display differences between structures of
|
||||||
|
nested dicts, lists, and arrays.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import initExample ## Add path to library (just for examples; you do not need this)
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtGui
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
app = QtGui.QApplication([])
|
||||||
|
A = {
|
||||||
|
'a list': [1,2,2,4,5,6, {'nested1': 'aaaa', 'nested2': 'bbbbb'}, "seven"],
|
||||||
|
'a dict': {
|
||||||
|
'x': 1,
|
||||||
|
'y': 2,
|
||||||
|
'z': 'three'
|
||||||
|
},
|
||||||
|
'an array': np.random.randint(10, size=(40,10)),
|
||||||
|
#'a traceback': some_func1(),
|
||||||
|
#'a function': some_func1,
|
||||||
|
#'a class': pg.DataTreeWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
B = {
|
||||||
|
'a list': [1,2,3,4,5,5, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"],
|
||||||
|
'a dict': {
|
||||||
|
'x': 2,
|
||||||
|
'y': 2,
|
||||||
|
'z': 'three',
|
||||||
|
'w': 5
|
||||||
|
},
|
||||||
|
'another dict': {1:2, 2:3, 3:4},
|
||||||
|
'an array': np.random.randint(10, size=(40,10)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tree = pg.DiffTreeWidget()
|
||||||
|
tree.setData(A, B)
|
||||||
|
tree.show()
|
||||||
|
tree.setWindowTitle('pyqtgraph example: DiffTreeWidget')
|
||||||
|
tree.resize(1000, 800)
|
||||||
|
|
||||||
|
|
||||||
|
## Start Qt event loop unless running in interactive mode or using pyside.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
|
||||||
|
QtGui.QApplication.instance().exec_()
|
@ -258,6 +258,7 @@ from .widgets.VerticalLabel import *
|
|||||||
from .widgets.FeedbackButton import *
|
from .widgets.FeedbackButton import *
|
||||||
from .widgets.ColorButton import *
|
from .widgets.ColorButton import *
|
||||||
from .widgets.DataTreeWidget import *
|
from .widgets.DataTreeWidget import *
|
||||||
|
from .widgets.DiffTreeWidget import *
|
||||||
from .widgets.GraphicsView import *
|
from .widgets.GraphicsView import *
|
||||||
from .widgets.LayoutWidget import *
|
from .widgets.LayoutWidget import *
|
||||||
from .widgets.TableWidget import *
|
from .widgets.TableWidget import *
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
from ..pgcollections import OrderedDict
|
from ..pgcollections import OrderedDict
|
||||||
|
from .TableWidget import TableWidget
|
||||||
|
from ..python2_3 import asUnicode
|
||||||
import types, traceback
|
import types, traceback
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@ -17,67 +19,106 @@ class DataTreeWidget(QtGui.QTreeWidget):
|
|||||||
Widget for displaying hierarchical python data structures
|
Widget for displaying hierarchical python data structures
|
||||||
(eg, nested dicts, lists, and arrays)
|
(eg, nested dicts, lists, and arrays)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, parent=None, data=None):
|
def __init__(self, parent=None, data=None):
|
||||||
QtGui.QTreeWidget.__init__(self, parent)
|
QtGui.QTreeWidget.__init__(self, parent)
|
||||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||||
self.setData(data)
|
self.setData(data)
|
||||||
self.setColumnCount(3)
|
self.setColumnCount(3)
|
||||||
self.setHeaderLabels(['key / index', 'type', 'value'])
|
self.setHeaderLabels(['key / index', 'type', 'value'])
|
||||||
|
self.setAlternatingRowColors(True)
|
||||||
|
|
||||||
def setData(self, data, hideRoot=False):
|
def setData(self, data, hideRoot=False):
|
||||||
"""data should be a dictionary."""
|
"""data should be a dictionary."""
|
||||||
self.clear()
|
self.clear()
|
||||||
|
self.widgets = []
|
||||||
|
self.nodes = {}
|
||||||
self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot)
|
self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot)
|
||||||
#node = self.mkNode('', data)
|
|
||||||
#while node.childCount() > 0:
|
|
||||||
#c = node.child(0)
|
|
||||||
#node.removeChild(c)
|
|
||||||
#self.invisibleRootItem().addChild(c)
|
|
||||||
self.expandToDepth(3)
|
self.expandToDepth(3)
|
||||||
self.resizeColumnToContents(0)
|
self.resizeColumnToContents(0)
|
||||||
|
|
||||||
def buildTree(self, data, parent, name='', hideRoot=False):
|
def buildTree(self, data, parent, name='', hideRoot=False, path=()):
|
||||||
if hideRoot:
|
if hideRoot:
|
||||||
node = parent
|
node = parent
|
||||||
else:
|
else:
|
||||||
typeStr = type(data).__name__
|
node = QtGui.QTreeWidgetItem([name, "", ""])
|
||||||
if typeStr == 'instance':
|
|
||||||
typeStr += ": " + data.__class__.__name__
|
|
||||||
node = QtGui.QTreeWidgetItem([name, typeStr, ""])
|
|
||||||
parent.addChild(node)
|
parent.addChild(node)
|
||||||
|
|
||||||
if isinstance(data, types.TracebackType): ## convert traceback to a list of strings
|
# record the path to the node so it can be retrieved later
|
||||||
data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data))))
|
# (this is used by DiffTreeWidget)
|
||||||
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
self.nodes[path] = node
|
||||||
data = {
|
|
||||||
'data': data.view(np.ndarray),
|
typeStr, desc, childs, widget = self.parse(data)
|
||||||
'meta': data.infoCopy()
|
node.setText(1, typeStr)
|
||||||
}
|
node.setText(2, desc)
|
||||||
|
|
||||||
|
# Truncate description and add text box if needed
|
||||||
|
if len(desc) > 100:
|
||||||
|
desc = desc[:97] + '...'
|
||||||
|
if widget is None:
|
||||||
|
widget = QtGui.QPlainTextEdit(asUnicode(data))
|
||||||
|
widget.setMaximumHeight(200)
|
||||||
|
widget.setReadOnly(True)
|
||||||
|
|
||||||
|
# Add widget to new subnode
|
||||||
|
if widget is not None:
|
||||||
|
self.widgets.append(widget)
|
||||||
|
subnode = QtGui.QTreeWidgetItem(["", "", ""])
|
||||||
|
node.addChild(subnode)
|
||||||
|
self.setItemWidget(subnode, 0, widget)
|
||||||
|
self.setFirstItemColumnSpanned(subnode, True)
|
||||||
|
|
||||||
|
# recurse to children
|
||||||
|
for key, data in childs.items():
|
||||||
|
self.buildTree(data, node, asUnicode(key), path=path+(key,))
|
||||||
|
|
||||||
|
def parse(self, data):
|
||||||
|
"""
|
||||||
|
Given any python object, return:
|
||||||
|
* type
|
||||||
|
* a short string representation
|
||||||
|
* a dict of sub-objects to be parsed
|
||||||
|
* optional widget to display as sub-node
|
||||||
|
"""
|
||||||
|
# defaults for all objects
|
||||||
|
typeStr = type(data).__name__
|
||||||
|
if typeStr == 'instance':
|
||||||
|
typeStr += ": " + data.__class__.__name__
|
||||||
|
widget = None
|
||||||
|
desc = ""
|
||||||
|
childs = {}
|
||||||
|
|
||||||
|
# type-specific changes
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
for k in data.keys():
|
desc = "length=%d" % len(data)
|
||||||
self.buildTree(data[k], node, str(k))
|
if isinstance(data, OrderedDict):
|
||||||
elif isinstance(data, list) or isinstance(data, tuple):
|
childs = data
|
||||||
for i in range(len(data)):
|
else:
|
||||||
self.buildTree(data[i], node, str(i))
|
childs = OrderedDict(sorted(data.items()))
|
||||||
|
elif isinstance(data, (list, tuple)):
|
||||||
|
desc = "length=%d" % len(data)
|
||||||
|
childs = OrderedDict(enumerate(data))
|
||||||
|
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||||
|
childs = OrderedDict([
|
||||||
|
('data', data.view(np.ndarray)),
|
||||||
|
('meta', data.infoCopy())
|
||||||
|
])
|
||||||
|
elif isinstance(data, np.ndarray):
|
||||||
|
desc = "shape=%s dtype=%s" % (data.shape, data.dtype)
|
||||||
|
table = TableWidget()
|
||||||
|
table.setData(data)
|
||||||
|
table.setMaximumHeight(200)
|
||||||
|
widget = table
|
||||||
|
elif isinstance(data, types.TracebackType): ## convert traceback to a list of strings
|
||||||
|
frames = list(map(str.strip, traceback.format_list(traceback.extract_tb(data))))
|
||||||
|
#childs = OrderedDict([
|
||||||
|
#(i, {'file': child[0], 'line': child[1], 'function': child[2], 'code': child[3]})
|
||||||
|
#for i, child in enumerate(frames)])
|
||||||
|
#childs = OrderedDict([(i, ch) for i,ch in enumerate(frames)])
|
||||||
|
widget = QtGui.QPlainTextEdit(asUnicode('\n'.join(frames)))
|
||||||
|
widget.setMaximumHeight(200)
|
||||||
|
widget.setReadOnly(True)
|
||||||
else:
|
else:
|
||||||
node.setText(2, str(data))
|
desc = asUnicode(data)
|
||||||
|
|
||||||
|
|
||||||
#def mkNode(self, name, v):
|
|
||||||
#if type(v) is list and len(v) > 0 and isinstance(v[0], dict):
|
|
||||||
#inds = map(unicode, range(len(v)))
|
|
||||||
#v = OrderedDict(zip(inds, v))
|
|
||||||
#if isinstance(v, dict):
|
|
||||||
##print "\nadd tree", k, v
|
|
||||||
#node = QtGui.QTreeWidgetItem([name])
|
|
||||||
#for k in v:
|
|
||||||
#newNode = self.mkNode(k, v[k])
|
|
||||||
#node.addChild(newNode)
|
|
||||||
#else:
|
|
||||||
##print "\nadd value", k, str(v)
|
|
||||||
#node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)])
|
|
||||||
#return node
|
|
||||||
|
|
||||||
|
return typeStr, desc, childs, widget
|
||||||
|
|
164
pyqtgraph/widgets/DiffTreeWidget.py
Normal file
164
pyqtgraph/widgets/DiffTreeWidget.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from ..Qt import QtGui, QtCore
|
||||||
|
from ..pgcollections import OrderedDict
|
||||||
|
from .DataTreeWidget import DataTreeWidget
|
||||||
|
from .. import functions as fn
|
||||||
|
import types, traceback
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
__all__ = ['DiffTreeWidget']
|
||||||
|
|
||||||
|
|
||||||
|
class DiffTreeWidget(QtGui.QWidget):
|
||||||
|
"""
|
||||||
|
Widget for displaying differences between hierarchical python data structures
|
||||||
|
(eg, nested dicts, lists, and arrays)
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None, a=None, b=None):
|
||||||
|
QtGui.QWidget.__init__(self, parent)
|
||||||
|
self.layout = QtGui.QHBoxLayout()
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.trees = [DataTreeWidget(self), DataTreeWidget(self)]
|
||||||
|
for t in self.trees:
|
||||||
|
self.layout.addWidget(t)
|
||||||
|
if a is not None:
|
||||||
|
self.setData(a, b)
|
||||||
|
|
||||||
|
def setData(self, a, b):
|
||||||
|
"""
|
||||||
|
Set the data to be compared in this widget.
|
||||||
|
"""
|
||||||
|
self.data = (a, b)
|
||||||
|
self.trees[0].setData(a)
|
||||||
|
self.trees[1].setData(b)
|
||||||
|
|
||||||
|
return self.compare(a, b)
|
||||||
|
|
||||||
|
def compare(self, a, b, path=()):
|
||||||
|
"""
|
||||||
|
Compare data structure *a* to structure *b*.
|
||||||
|
|
||||||
|
Return True if the objects match completely.
|
||||||
|
Otherwise, return a structure that describes the differences:
|
||||||
|
|
||||||
|
{ 'type': bool
|
||||||
|
'len': bool,
|
||||||
|
'str': bool,
|
||||||
|
'shape': bool,
|
||||||
|
'dtype': bool,
|
||||||
|
'mask': array,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
bad = (255, 200, 200)
|
||||||
|
diff = []
|
||||||
|
# generate typestr, desc, childs for each object
|
||||||
|
typeA, descA, childsA, _ = self.trees[0].parse(a)
|
||||||
|
typeB, descB, childsB, _ = self.trees[1].parse(b)
|
||||||
|
|
||||||
|
if typeA != typeB:
|
||||||
|
self.setColor(path, 1, bad)
|
||||||
|
if descA != descB:
|
||||||
|
self.setColor(path, 2, bad)
|
||||||
|
|
||||||
|
if isinstance(a, dict) and isinstance(b, dict):
|
||||||
|
keysA = set(a.keys())
|
||||||
|
keysB = set(b.keys())
|
||||||
|
for key in keysA - keysB:
|
||||||
|
self.setColor(path+(key,), 0, bad, tree=0)
|
||||||
|
for key in keysB - keysA:
|
||||||
|
self.setColor(path+(key,), 0, bad, tree=1)
|
||||||
|
for key in keysA & keysB:
|
||||||
|
self.compare(a[key], b[key], path+(key,))
|
||||||
|
|
||||||
|
elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
|
||||||
|
for i in range(max(len(a), len(b))):
|
||||||
|
if len(a) <= i:
|
||||||
|
self.setColor(path+(i,), 0, bad, tree=1)
|
||||||
|
elif len(b) <= i:
|
||||||
|
self.setColor(path+(i,), 0, bad, tree=0)
|
||||||
|
else:
|
||||||
|
self.compare(a[i], b[i], path+(i,))
|
||||||
|
|
||||||
|
elif isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.shape == b.shape:
|
||||||
|
tableNodes = [tree.nodes[path].child(0) for tree in self.trees]
|
||||||
|
if a.dtype.fields is None and b.dtype.fields is None:
|
||||||
|
eq = self.compareArrays(a, b)
|
||||||
|
if not np.all(eq):
|
||||||
|
for n in tableNodes:
|
||||||
|
n.setBackground(0, fn.mkBrush(bad))
|
||||||
|
#for i in np.argwhere(~eq):
|
||||||
|
|
||||||
|
else:
|
||||||
|
if a.dtype == b.dtype:
|
||||||
|
for i,k in enumerate(a.dtype.fields.keys()):
|
||||||
|
eq = self.compareArrays(a[k], b[k])
|
||||||
|
if not np.all(eq):
|
||||||
|
for n in tableNodes:
|
||||||
|
n.setBackground(0, fn.mkBrush(bad))
|
||||||
|
#for j in np.argwhere(~eq):
|
||||||
|
|
||||||
|
# dict: compare keys, then values where keys match
|
||||||
|
# list:
|
||||||
|
# array: compare elementwise for same shape
|
||||||
|
|
||||||
|
def compareArrays(self, a, b):
|
||||||
|
intnan = -9223372036854775808 # happens when np.nan is cast to int
|
||||||
|
anans = np.isnan(a) | (a == intnan)
|
||||||
|
bnans = np.isnan(b) | (b == intnan)
|
||||||
|
eq = anans == bnans
|
||||||
|
mask = ~anans
|
||||||
|
eq[mask] = np.allclose(a[mask], b[mask])
|
||||||
|
return eq
|
||||||
|
|
||||||
|
def setColor(self, path, column, color, tree=None):
|
||||||
|
brush = fn.mkBrush(color)
|
||||||
|
|
||||||
|
# Color only one tree if specified.
|
||||||
|
if tree is None:
|
||||||
|
trees = self.trees
|
||||||
|
else:
|
||||||
|
trees = [self.trees[tree]]
|
||||||
|
|
||||||
|
for tree in trees:
|
||||||
|
item = tree.nodes[path]
|
||||||
|
item.setBackground(column, brush)
|
||||||
|
|
||||||
|
def _compare(self, a, b):
|
||||||
|
"""
|
||||||
|
Compare data structure *a* to structure *b*.
|
||||||
|
"""
|
||||||
|
# Check test structures are the same
|
||||||
|
assert type(info) is type(expect)
|
||||||
|
if hasattr(info, '__len__'):
|
||||||
|
assert len(info) == len(expect)
|
||||||
|
|
||||||
|
if isinstance(info, dict):
|
||||||
|
for k in info:
|
||||||
|
assert k in expect
|
||||||
|
for k in expect:
|
||||||
|
assert k in info
|
||||||
|
self.compare_results(info[k], expect[k])
|
||||||
|
elif isinstance(info, list):
|
||||||
|
for i in range(len(info)):
|
||||||
|
self.compare_results(info[i], expect[i])
|
||||||
|
elif isinstance(info, np.ndarray):
|
||||||
|
assert info.shape == expect.shape
|
||||||
|
assert info.dtype == expect.dtype
|
||||||
|
if info.dtype.fields is None:
|
||||||
|
intnan = -9223372036854775808 # happens when np.nan is cast to int
|
||||||
|
inans = np.isnan(info) | (info == intnan)
|
||||||
|
enans = np.isnan(expect) | (expect == intnan)
|
||||||
|
assert np.all(inans == enans)
|
||||||
|
mask = ~inans
|
||||||
|
assert np.allclose(info[mask], expect[mask])
|
||||||
|
else:
|
||||||
|
for k in info.dtype.fields.keys():
|
||||||
|
self.compare_results(info[k], expect[k])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
assert info == expect
|
||||||
|
except Exception:
|
||||||
|
raise NotImplementedError("Cannot compare objects of type %s" % type(info))
|
||||||
|
|
@ -146,7 +146,8 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
i += 1
|
i += 1
|
||||||
self.setRow(i, [x for x in fn1(row)])
|
self.setRow(i, [x for x in fn1(row)])
|
||||||
|
|
||||||
if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount():
|
if (self._sorting and self.horizontalHeadersSet and
|
||||||
|
self.horizontalHeader().sortIndicatorSection() >= self.columnCount()):
|
||||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||||
|
|
||||||
def setEditable(self, editable=True):
|
def setEditable(self, editable=True):
|
||||||
@ -216,6 +217,8 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
return self.iterate, list(map(asUnicode, data.dtype.names))
|
return self.iterate, list(map(asUnicode, data.dtype.names))
|
||||||
elif data is None:
|
elif data is None:
|
||||||
return (None,None)
|
return (None,None)
|
||||||
|
elif np.isscalar(data):
|
||||||
|
return self.iterateScalar, None
|
||||||
else:
|
else:
|
||||||
msg = "Don't know how to iterate over data type: {!s}".format(type(data))
|
msg = "Don't know how to iterate over data type: {!s}".format(type(data))
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
@ -230,6 +233,9 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
for x in data:
|
for x in data:
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
|
def iterateScalar(self, data):
|
||||||
|
yield data
|
||||||
|
|
||||||
def appendRow(self, data):
|
def appendRow(self, data):
|
||||||
self.appendData([data])
|
self.appendData([data])
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user