From 8c0064a3230629ab1ff1c5240179922f645b8715 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 11:58:49 -0400 Subject: [PATCH 1/4] DataTreeWidget uses TableWidget to represent arrays --- examples/DataTreeWidget.py | 2 +- pyqtgraph/widgets/DataTreeWidget.py | 34 ++++++++++------------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 8365db2a..2c2b00f9 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -19,7 +19,7 @@ d = { 'y': 2, 'z': 'three' }, - 'array1 (20x20)': np.ones((10,10)) + 'array1 (40x10)': np.random.randint(10, size=(40,10)) } tree = pg.DataTreeWidget(data=d) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index 29e60319..b8215b06 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict +from .TableWidget import TableWidget import types, traceback import numpy as np @@ -29,12 +30,8 @@ class DataTreeWidget(QtGui.QTreeWidget): def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() + self.tables = [] 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.resizeColumnToContents(0) @@ -62,22 +59,15 @@ class DataTreeWidget(QtGui.QTreeWidget): elif isinstance(data, list) or isinstance(data, tuple): for i in range(len(data)): self.buildTree(data[i], node, str(i)) + elif isinstance(data, np.ndarray): + desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) + node.setText(2, desc) + subnode = QtGui.QTreeWidgetItem(["", "", ""]) + node.addChild(subnode) + table = TableWidget() + table.setData(data) + table.setMaximumHeight(200) + self.setItemWidget(subnode, 2, table) + self.tables.append(table) else: node.setText(2, str(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 - From cabd9d6bf2ec61701bdc3ab927d1e9444f09e9df Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 15:21:28 -0400 Subject: [PATCH 2/4] Cleanup, better support for tracebacks in DataTreeWidget --- examples/DataTreeWidget.py | 20 +++++- pyqtgraph/widgets/DataTreeWidget.py | 96 ++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 2c2b00f9..70ac49bd 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui 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([]) d = { - 'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], - 'dict1': { + 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { 'x': 1, 'y': 2, 'z': 'three' }, - 'array1 (40x10)': np.random.randint(10, size=(40,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) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index b8215b06..f960cab9 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -2,6 +2,7 @@ from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict from .TableWidget import TableWidget +from ..python2_3 import asUnicode import types, traceback import numpy as np @@ -18,19 +19,18 @@ class DataTreeWidget(QtGui.QTreeWidget): Widget for displaying hierarchical python data structures (eg, nested dicts, lists, and arrays) """ - - def __init__(self, parent=None, data=None): QtGui.QTreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setData(data) self.setColumnCount(3) self.setHeaderLabels(['key / index', 'type', 'value']) + self.setAlternatingRowColors(True) def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() - self.tables = [] + self.widgets = [] self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) @@ -39,35 +39,75 @@ class DataTreeWidget(QtGui.QTreeWidget): if hideRoot: node = parent else: - typeStr = type(data).__name__ - if typeStr == 'instance': - typeStr += ": " + data.__class__.__name__ - node = QtGui.QTreeWidgetItem([name, typeStr, ""]) + node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) - - if isinstance(data, types.TracebackType): ## convert traceback to a list of strings - data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) - elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): - data = { - 'data': data.view(np.ndarray), - 'meta': data.infoCopy() - } - - if isinstance(data, dict): - for k in data.keys(): - self.buildTree(data[k], node, str(k)) - elif isinstance(data, list) or isinstance(data, tuple): - for i in range(len(data)): - self.buildTree(data[i], node, str(i)) - elif isinstance(data, np.ndarray): - desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) - node.setText(2, desc) + + typeStr, desc, childs, widget = self.parse(data) + node.setText(1, typeStr) + node.setText(2, desc) + if widget is not None: + self.widgets.append(widget) subnode = QtGui.QTreeWidgetItem(["", "", ""]) node.addChild(subnode) + self.setItemWidget(subnode, 0, widget) + self.setFirstItemColumnSpanned(subnode, True) + + for name, data in childs.items(): + self.buildTree(data, node, asUnicode(name)) + + 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): + desc = "length=%d" % len(data) + if isinstance(data, OrderedDict): + childs = data + else: + 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) - self.setItemWidget(subnode, 2, table) - self.tables.append(table) + 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: - node.setText(2, str(data)) + desc = asUnicode(data) + if len(desc) > 100: + desc = desc[:97] + '...' + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + return typeStr, desc, childs, widget + \ No newline at end of file From 4543031ac59eed15adf6a39c82ffb73197a36800 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 17:26:06 -0400 Subject: [PATCH 3/4] Added DiffTreeWidget --- examples/DiffTreeWidget.py | 52 +++++++++ pyqtgraph/__init__.py | 1 + pyqtgraph/widgets/DataTreeWidget.py | 27 +++-- pyqtgraph/widgets/DiffTreeWidget.py | 163 ++++++++++++++++++++++++++++ pyqtgraph/widgets/TableWidget.py | 3 +- 5 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 examples/DiffTreeWidget.py create mode 100644 pyqtgraph/widgets/DiffTreeWidget.py diff --git a/examples/DiffTreeWidget.py b/examples/DiffTreeWidget.py new file mode 100644 index 00000000..fa57a356 --- /dev/null +++ b/examples/DiffTreeWidget.py @@ -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_() \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455..de9fb278 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -240,6 +240,7 @@ from .widgets.VerticalLabel import * from .widgets.FeedbackButton import * from .widgets.ColorButton import * from .widgets.DataTreeWidget import * +from .widgets.DiffTreeWidget import * from .widgets.GraphicsView import * from .widgets.LayoutWidget import * from .widgets.TableWidget import * diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index f960cab9..39cb0d45 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -31,20 +31,35 @@ class DataTreeWidget(QtGui.QTreeWidget): """data should be a dictionary.""" self.clear() self.widgets = [] + self.nodes = {} self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) - def buildTree(self, data, parent, name='', hideRoot=False): + def buildTree(self, data, parent, name='', hideRoot=False, path=()): if hideRoot: node = parent else: node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) + + # record the path to the node so it can be retrieved later + # (this is used by DiffTreeWidget) + self.nodes[path] = node typeStr, desc, childs, widget = self.parse(data) 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(["", "", ""]) @@ -52,8 +67,9 @@ class DataTreeWidget(QtGui.QTreeWidget): self.setItemWidget(subnode, 0, widget) self.setFirstItemColumnSpanned(subnode, True) - for name, data in childs.items(): - self.buildTree(data, node, asUnicode(name)) + # recurse to children + for key, data in childs.items(): + self.buildTree(data, node, asUnicode(key), path=path+(key,)) def parse(self, data): """ @@ -103,11 +119,6 @@ class DataTreeWidget(QtGui.QTreeWidget): widget.setReadOnly(True) else: desc = asUnicode(data) - if len(desc) > 100: - desc = desc[:97] + '...' - widget = QtGui.QPlainTextEdit(asUnicode(data)) - widget.setMaximumHeight(200) - widget.setReadOnly(True) return typeStr, desc, childs, widget \ No newline at end of file diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py new file mode 100644 index 00000000..e3869da8 --- /dev/null +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -0,0 +1,163 @@ +# -*- 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: + for i,k in enumerate(info.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)) + \ No newline at end of file diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 69085a20..db0b6ae2 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -151,7 +151,8 @@ class TableWidget(QtGui.QTableWidget): i += 1 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) def setEditable(self, editable=True): From 9893fc37353a8f1b02637fb4835a9196df52945b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 26 Sep 2014 15:25:05 -0400 Subject: [PATCH 4/4] corrections for 1d arrays --- pyqtgraph/widgets/DiffTreeWidget.py | 13 +++++++------ pyqtgraph/widgets/TableWidget.py | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py index e3869da8..eac29489 100644 --- a/pyqtgraph/widgets/DiffTreeWidget.py +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -91,12 +91,13 @@ class DiffTreeWidget(QtGui.QWidget): #for i in np.argwhere(~eq): else: - for i,k in enumerate(info.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): + 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: diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index db0b6ae2..9e9f2144 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -222,6 +222,8 @@ class TableWidget(QtGui.QTableWidget): return self.iterate, list(map(asUnicode, data.dtype.names)) elif data is None: return (None,None) + elif np.isscalar(data): + return self.iterateScalar, None else: msg = "Don't know how to iterate over data type: {!s}".format(type(data)) raise TypeError(msg) @@ -236,6 +238,9 @@ class TableWidget(QtGui.QTableWidget): for x in data: yield x + def iterateScalar(self, data): + yield data + def appendRow(self, data): self.appendData([data])