class Interact(QtGui.QMainWindow): def __init__(self, data, app, title=None, sortkey=None, axisequal=False, parent=None, **kwargs): self.app = app QtGui.QMainWindow.__init__(self, parent) if title is not None: self.setWindowTitle(title) else: self.setWindowTitle(', '.join(d[1] for d in data)) if sortkey is not None: self.sortkey = sortkey else: self.sortkey = kwargs.get('key', lambda x: x.lower()) self.grid = QtGui.QGridLayout() self.frame = QtGui.QWidget() self.dpi = 100 self.fig = Figure(tight_layout=True) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.frame) self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) self.canvas.mpl_connect('key_press_event', self.canvas_key_press) self.axes = self.fig.add_subplot(111) self.axes2 = self.axes.twinx() self.fig.delaxes(self.axes2) self.xlim = None self.ylim = None self.xlogscale = 'linear' self.ylogscale = 'linear' self.axisequal = axisequal self.margins = 0 self.mpl_toolbar = NavigationToolbar(self.canvas, self.frame) self.pickers = None self.vbox = QtGui.QVBoxLayout() self.vbox.addWidget(self.mpl_toolbar) self.props_editor = PropertyEditor(self) self.datas = [] for d in data: self.add_data(*d) self.vbox.addLayout(self.grid) self.set_layout() def set_layout(self): self.vbox.addWidget(self.canvas) self.frame.setLayout(self.vbox) self.setCentralWidget(self.frame) for data in self.datas: self.setTabOrder(data.menu, data.scale_box) self.setTabOrder(data.scale_box, data.xmenu) self.setTabOrder(data.xmenu, data.xscale_box) if len(self.datas) >= 2: for d1, d2 in zip(self.datas[:-1], self.datas[1:]): self.setTabOrder(d1.menu, d2.menu) self.draw() def add_data(self, obj, name, kwargs=None): kwargs = kwargs or {} kwargs['name'] = kwargs.get('name', name) or 'data' self.datas.append(DataObj(self, obj, **kwargs)) data = self.datas[-1] self.row = self.grid.rowCount() self.column = 0 def axisequal(): self.axisequal = not self.axisequal self.draw() def add_widget(w, axis=None): self.grid.addWidget(w, self.row, self.column) data.widgets.append(w) self.connect(w, SIGNAL('duplicate()'), data.duplicate) self.connect(w, SIGNAL('remove()'), data.remove) self.connect(w, SIGNAL('closed()'), data.close) self.connect(w, SIGNAL('axisequal()'), axisequal) self.connect(w, SIGNAL('relabel()'), data.change_label) self.connect(w, SIGNAL('edit_props()'), data.edit_props) self.connect(w, SIGNAL('sync()'), data.sync) self.connect(w, SIGNAL('twin()'), data.toggle_twin) self.connect(w, SIGNAL('xlim()'), self.set_xlim) self.connect(w, SIGNAL('ylim()'), self.set_ylim) if axis: self.connect(w, SIGNAL('sync_axis()'), lambda axes=[axis]: data.sync(axes)) self.column += 1 add_widget(data.label) add_widget(data.menu, 'y') add_widget(data.scale_label) add_widget(data.scale_box, 'y') add_widget(data.xlabel) add_widget(data.xmenu, 'x') add_widget(data.xscale_label) add_widget(data.xscale_box, 'x') def warn(self, message): self.warnings = [message] self.draw_warnings() self.canvas.draw() def remove_data(self, data): if len(self.datas) < 2: return self.warn("Can't delete last row") index = self.datas.index(data) self.datas.pop(index) for widget in data.widgets: self.grid.removeWidget(widget) widget.deleteLater() if self.props_editor.dataobj is data: self.props_editor.close() self.set_layout() self.draw() self.datas[index - 1].menu.setFocus() self.datas[index - 1].menu.lineEdit().selectAll() def get_scale(self, textbox, completer): completer.close_popup() text = text_type(textbox.text()) try: return eval(text, CONSTANTS.copy()) except Exception as e: self.warnings.append('Error setting scale: ' + text_type(e)) return 1.0 def get_key(self, menu): key = text_type(menu.itemText(menu.currentIndex())) text = text_type(menu.lineEdit().text()) if key != text: self.warnings.append( 'Plotted key (%s) does not match typed key (%s)' % (key, text)) return key @staticmethod def cla(axes): tight, xmargin, ymargin = axes._tight, axes._xmargin, axes._ymargin axes.clear() axes._tight, axes._xmargin, axes._ymargin = tight, xmargin, ymargin def clear_pickers(self): if self.pickers is not None: [p.disable() for p in self.pickers] self.pickers = None def plot(self, axes, data, xname, xscale, yname, yscale, i, label): if xname in data.obj: x = data.obj[xname][..., i] * xscale y = data.obj[yname][..., i] * yscale def plot(y): if xname in data.obj: return axes.plot(x, y, label=label) else: return axes.plot(y, label=label) try: lines = plot(y) except ValueError: lines = plot(y.T) if isinstance(i, slice): i = 0 for line in lines: if hasattr(data, 'cdata'): line.set_color(data.cmap(data.norm(data.cdata[i]))) self.handles[(label,)] = line elif 'color' not in data.props and 'linestyle' not in data.props: style, color = next(self.styles) line.set_color(color) line.set_linestyle(style) self.handles[(label, color, style)] = line elif 'color' not in data.props: line.set_color(color_cycle[i % len(color_cycle)]) self.handles[ (label, line.get_color(), data.props['linestyle'])] = line else: self.handles[ (label, data.props.get('color', line.get_color()), data.props.get('linestyle', line.get_linestyle()))] = line for key, value in data.props.items(): getattr(line, 'set_' + key, lambda _: None)(value) return len(lines) def draw(self): self.mpl_toolbar.home = self.draw twin = any(d.twin for d in self.datas) self.clear_pickers() self.fig.clear() self.axes = self.fig.add_subplot(111) color_data = next((d for d in self.datas if hasattr(d, 'cdata')), None) if color_data and not twin: self.mappable = mpl.cm.ScalarMappable(norm=color_data.norm, cmap=color_data.cmap) self.mappable.set_array(color_data.cdata) self.colorbar = self.fig.colorbar( self.mappable, ax=self.axes, fraction=0.1, pad=0.02) elif twin: self.axes2 = self.axes.twinx() for ax in self.axes, self.axes2: ax._tight = bool(self.margins) if self.margins: ax.margins(self.margins) xlabel = [] ylabel = [] xlabel2 = [] ylabel2 = [] self.warnings = [] self.handles = OrderedDict() self.styles = cycle(product(linestyle_cycle, color_cycle)) for d in self.datas: if d.twin: axes, x, y = self.axes2, xlabel2, ylabel2 else: axes, x, y = self.axes, xlabel, ylabel scale = self.get_scale(d.scale_box, d.scale_compl) xscale = self.get_scale(d.xscale_box, d.xscale_compl) text = self.get_key(d.menu) xtext = self.get_key(d.xmenu) args = axes, d, xtext, xscale, text, scale if isiterable(d.labels): for i, label in enumerate(d.labels): self.plot(*args + (i, label)) else: n = self.plot(*args + (slice(None), d.labels)) if n > 1: d.labels = ['%s %d' % (d.labels, i) for i in range(n)] return self.draw() axes.set_xlabel('') if xtext: x.append(xtext + ' (' + d.name + ')') y.append(text + ' (' + d.name + ')') self.axes.set_xlabel('\n'.join(xlabel)) self.axes.set_ylabel('\n'.join(ylabel)) self.draw_warnings() self.axes2.set_xlabel('\n'.join(xlabel2)) self.axes2.set_ylabel('\n'.join(ylabel2)) self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) self.axes.set_xscale(self.xlogscale) self.axes.set_yscale(self.ylogscale) for ax in self.axes, self.axes2: ax.set_aspect('equal' if self.axisequal else 'auto', 'box-forced') legend = self.axes.legend(self.handles.values(), [k[0] for k in self.handles.keys()]) legend.draggable(True) self.pickers = [picker(ax) for ax in [self.axes, self.axes2]] self.canvas.draw() def draw_warnings(self): self.axes.text(0.05, 0.05, '\n'.join(self.warnings), transform=self.axes.transAxes, color='red') def canvas_key_press(self, event): key_press_handler(event, self.canvas, self.mpl_toolbar) if event.key == 'ctrl+q': self._close() elif event.key in mpl.rcParams['keymap.home']: self.xlim = self.ylim = None self.draw() elif event.key == 'ctrl+x': self.set_xlim(draw=False) elif event.key == 'ctrl+y': self.set_ylim(draw=False) elif event.key == 'ctrl+l': self.draw() self.xlogscale = self.axes.get_xscale() self.ylogscale = self.axes.get_yscale() def edit_parameters(self): xlim = self.axes.get_xlim() ylim = self.axes.get_ylim() self.mpl_toolbar.edit_parameters() if xlim != self.axes.get_xlim(): self.xlim = self.axes.get_xlim() if ylim != self.axes.get_ylim(): self.ylim = self.axes.get_ylim() self.xlogscale = self.axes.get_xscale() self.ylogscale = self.axes.get_yscale() def _margins(self): self.margins = 0 if self.margins else 0.05 self.draw() def _options(self): self.edit_parameters() def _close(self): self.app.references.discard(self) self.window().close() def _input_lim(self, axis, default): default = text_type(default) if re.match(r'^\(.*\)$', default) or re.match(r'^\[.*\]$', default): default = default[1:-1] text, ok = QtGui.QInputDialog.getText( self, 'Set axis limits', '{} limits:'.format(axis), QtGui.QLineEdit.Normal, default) if ok: try: return eval(text_type(text), CONSTANTS.copy()) except Exception: return None else: return None def set_xlim(self, draw=True): self.xlim = self._input_lim( 'x', self.xlim or self.axes.get_xlim()) if draw: self.draw() def set_ylim(self, draw=True): self.ylim = self._input_lim( 'y', self.ylim or self.axes.get_ylim()) if draw: self.draw() control_actions = { QtCore.Qt.Key_M: _margins, QtCore.Qt.Key_O: _options, QtCore.Qt.Key_Q: _close, } @staticmethod def data_dict(d): kwargs = OrderedDict(( ('name', d.name), ('xname', text_type(d.xmenu.lineEdit().text())), ('xscale', text_type(d.xscale_box.text())), ('yname', text_type(d.menu.lineEdit().text())), ('yscale', text_type(d.scale_box.text())), ('props', d.props), ('labels', d.labels), )) for key in 'xscale', 'yscale': try: kwargs[key] = ast.literal_eval(kwargs[key]) except ValueError: pass else: if float(kwargs[key]) == 1.0: del kwargs[key] if not kwargs['props']: del kwargs['props'] return kwargs def data_dicts(self): return "\n".join(text_type(self.dict_repr(self.data_dict(d))) for d in self.datas) @classmethod def dict_repr(cls, d, top=True): if isinstance(d, dict): return ('{}' if top else 'dict({})').format(', '.join( ['{}={}'.format(k, cls.dict_repr(v, False)) for k, v in d.items()])) elif isinstance(d, string_types): return repr(str(d)) return repr(d) def event(self, event): if (event.type() == QtCore.QEvent.KeyPress and event.modifiers() & CONTROL_MODIFIER and event.key() in self.control_actions): self.control_actions[event.key()](self) return True # Create duplicate of entire GUI with Ctrl+Shift+N elif (event.type() == QtCore.QEvent.KeyPress and event.modifiers() & CONTROL_MODIFIER and event.modifiers() & QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_N): create(*[[d.obj, d.name, self.data_dict(d)] for d in self.datas]) return True # Print dictionaries of keys and scales for all data with Ctrl+Shift+P elif (event.type() == QtCore.QEvent.KeyPress and event.modifiers() & CONTROL_MODIFIER and event.modifiers() & QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_P): print(self.data_dicts()) sys.stdout.flush() return True return super(Interact, self).event(event)
def edit_parameters(self): self.parent.plotChanged = True NavigationToolbar2QT.edit_parameters(self)
class Interact(QtGui.QMainWindow): def __init__(self, data, app, title=None, sortkey=None, axisequal=False, parent=None, **kwargs): self.app = app QtGui.QMainWindow.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) self.setWindowTitle(title or ', '.join(d[1] for d in data)) if sortkey is not None: self.sortkey = sortkey else: self.sortkey = kwargs.get('key', lambda x: x.lower()) self.grid = QtGui.QGridLayout() self.frame = QtGui.QWidget() self.dpi = 100 self.fig = Figure(tight_layout=True) self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.frame) self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus) self.canvas.mpl_connect('key_press_event', self.canvas_key_press) self.axes = self.fig.add_subplot(111) self.axes2 = self.axes.twinx() self.fig.delaxes(self.axes2) self.xlim = None self.ylim = None self.xlogscale = 'linear' self.ylogscale = 'linear' self.axisequal = axisequal self.margins = 0 self.mpl_toolbar = NavigationToolbar(self.canvas, self.frame) self.pickers = None self.vbox = QtGui.QVBoxLayout() self.vbox.addWidget(self.mpl_toolbar) self.props_editor = PropertyEditor(self) self.datas = [] for d in data: self.add_data(*d) self.vbox.addLayout(self.grid) self.set_layout() def set_layout(self): self.vbox.addWidget(self.canvas) self.frame.setLayout(self.vbox) self.setCentralWidget(self.frame) for data in self.datas: self.setTabOrder(data.menu, data.scale_box) self.setTabOrder(data.scale_box, data.xmenu) self.setTabOrder(data.xmenu, data.xscale_box) if len(self.datas) >= 2: for d1, d2 in zip(self.datas[:-1], self.datas[1:]): self.setTabOrder(d1.menu, d2.menu) self.draw() def add_data(self, obj, name, kwargs=None): kwargs = kwargs or {} kwargs['name'] = kwargs.get('name', name) or 'data' self.datas.append(DataObj(self, obj, **kwargs)) data = self.datas[-1] self.row = self.grid.rowCount() self.column = 0 def axisequal(): self.axisequal = not self.axisequal self.draw() def add_widget(w, axis=None): self.grid.addWidget(w, self.row, self.column) data.widgets.append(w) self.connect(w, SIGNAL('duplicate()'), data.duplicate) self.connect(w, SIGNAL('remove()'), data.remove) self.connect(w, SIGNAL('closed()'), data.close) self.connect(w, SIGNAL('axisequal()'), axisequal) self.connect(w, SIGNAL('relabel()'), data.change_label) self.connect(w, SIGNAL('edit_props()'), data.edit_props) self.connect(w, SIGNAL('sync()'), data.sync) self.connect(w, SIGNAL('twin()'), data.toggle_twin) self.connect(w, SIGNAL('xlim()'), self.set_xlim) self.connect(w, SIGNAL('ylim()'), self.set_ylim) if axis: self.connect(w, SIGNAL('sync_axis()'), lambda axes=[axis]: data.sync(axes)) self.column += 1 add_widget(data.label) add_widget(data.menu, 'y') add_widget(data.scale_label) add_widget(data.scale_box, 'y') add_widget(data.xlabel) add_widget(data.xmenu, 'x') add_widget(data.xscale_label) add_widget(data.xscale_box, 'x') def warn(self, message): self.warnings = {message} self.draw_warnings() self.canvas.draw() def remove_data(self, data): if len(self.datas) < 2: return self.warn("Can't delete last row") index = self.datas.index(data) self.datas.pop(index) for widget in data.widgets: self.grid.removeWidget(widget) widget.deleteLater() if self.props_editor.dataobj is data: self.props_editor.close() self.set_layout() self.draw() self.datas[index - 1].menu.setFocus() self.datas[index - 1].menu.lineEdit().selectAll() def get_scale(self, textbox, completer): completer.close_popup() text = text_type(textbox.text()) try: return eval(text, CONSTANTS.copy()) except Exception as e: self.warnings.add('Error setting scale: ' + text_type(e)) return 1.0 def get_key(self, menu): key = text_type(menu.itemText(menu.currentIndex())) text = text_type(menu.lineEdit().text()) if key != text: self.warnings.add( 'Plotted key (%s) does not match typed key (%s)' % (key, text)) return key @staticmethod def cla(axes): tight, xmargin, ymargin = axes._tight, axes._xmargin, axes._ymargin axes.clear() axes._tight, axes._xmargin, axes._ymargin = tight, xmargin, ymargin def clear_pickers(self): if self.pickers: [p.disable() for p in self.pickers] self.pickers = None def plot(self, axes, data): xscale = self.get_scale(data.xscale_box, data.xscale_compl) yscale = self.get_scale(data.scale_box, data.scale_compl) xname = self.get_key(data.xmenu) yname = self.get_key(data.menu) if xname in data.obj: x = data.obj[xname] * xscale y = data.obj[yname] * yscale if xname in data.obj and x.shape[0] in y.shape: xaxis = y.shape.index(x.shape[0]) lines = axes.plot(x, np.rollaxis(y, xaxis)) else: if xname in data.obj: self.warnings.add( '{} {} and {} {} have incompatible dimensions'.format( xname, x.shape, yname, y.shape)) lines = axes.plot(y) if not isiterable(data.labels): if len(lines) > 1: data.labels = [ '%s %d' % (data.labels, i) for i in range(len(lines)) ] else: data.labels = [data.labels] for i, (line, label) in enumerate(zip(lines, data.labels)): line.set_label(label) if hasattr(data, 'cdata'): line.set_color(data.cmap(data.norm(data.cdata[i]))) self.handles[(label, )] = line elif 'color' not in data.props and 'linestyle' not in data.props: style, color = next(self.styles) line.set_color(color) line.set_linestyle(style) self.handles[(label, color, style)] = line elif 'color' not in data.props: line.set_color(color_cycle[i % len(color_cycle)]) self.handles[(label, line.get_color(), data.props['linestyle'])] = line else: self.handles[(label, data.props.get('color', line.get_color()), data.props.get('linestyle', line.get_linestyle()))] = line for key, value in data.props.items(): getattr(line, 'set_' + key, lambda _: None)(value) return len(lines) def draw(self): self.mpl_toolbar.home = self.draw twin = any(d.twin for d in self.datas) self.clear_pickers() self.fig.clear() self.axes = self.fig.add_subplot(111) color_data = next((d for d in self.datas if hasattr(d, 'cdata')), None) if color_data and not twin: self.mappable = mpl.cm.ScalarMappable(norm=color_data.norm, cmap=color_data.cmap) self.mappable.set_array(color_data.cdata) self.colorbar = self.fig.colorbar(self.mappable, ax=self.axes, fraction=0.1, pad=0.02) elif twin: self.axes2 = self.axes.twinx() for ax in self.axes, self.axes2: ax._tight = bool(self.margins) if self.margins: ax.margins(self.margins) xlabel = [] ylabel = [] xlabel2 = [] ylabel2 = [] self.warnings = set() self.handles = OrderedDict() self.styles = cycle(product(linestyle_cycle, color_cycle)) for d in self.datas: if d.twin: axes, x, y = self.axes2, xlabel2, ylabel2 else: axes, x, y = self.axes, xlabel, ylabel self.plot(axes, d) text = self.get_key(d.menu) xtext = self.get_key(d.xmenu) if xtext: x.append(xtext + ' (' + d.name + ')') y.append(text + ' (' + d.name + ')') self.axes.set_xlabel('\n'.join(xlabel)) self.axes.set_ylabel('\n'.join(ylabel)) self.draw_warnings() self.axes2.set_xlabel('\n'.join(xlabel2)) self.axes2.set_ylabel('\n'.join(ylabel2)) self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) self.axes.set_xscale(self.xlogscale) self.axes.set_yscale(self.ylogscale) for ax in self.axes, self.axes2: ax.set_aspect('equal' if self.axisequal else 'auto', 'box-forced') legend = self.axes.legend(self.handles.values(), [k[0] for k in self.handles.keys()]) legend.draggable(True) self.pickers = [picker(ax) for ax in [self.axes, self.axes2]] self.canvas.draw() def draw_warnings(self): self.axes.text(0.05, 0.05, '\n'.join(self.warnings), transform=self.axes.transAxes, color='red') def canvas_key_press(self, event): key_press_handler(event, self.canvas, self.mpl_toolbar) if event.key == 'ctrl+q': self._close() elif event.key in mpl.rcParams['keymap.home']: self.xlim = self.ylim = None self.draw() elif event.key == 'ctrl+x': self.set_xlim(draw=False) elif event.key == 'ctrl+y': self.set_ylim(draw=False) elif event.key == 'ctrl+l': self.draw() self.xlogscale = self.axes.get_xscale() self.ylogscale = self.axes.get_yscale() def edit_parameters(self): xlim = self.axes.get_xlim() ylim = self.axes.get_ylim() self.mpl_toolbar.edit_parameters() if xlim != self.axes.get_xlim(): self.xlim = self.axes.get_xlim() if ylim != self.axes.get_ylim(): self.ylim = self.axes.get_ylim() self.xlogscale = self.axes.get_xscale() self.ylogscale = self.axes.get_yscale() def _margins(self): self.margins = 0 if self.margins else 0.05 self.draw() def _close(self): self.app.references.discard(self) self.window().close() def _input_lim(self, axis, default): default = text_type(default) if re.match(r'^\(.*\)$', default) or re.match(r'^\[.*\]$', default): default = default[1:-1] text, ok = QtGui.QInputDialog.getText(self, 'Set axis limits', '{} limits:'.format(axis), QtGui.QLineEdit.Normal, default) if ok: try: return eval(text_type(text), CONSTANTS.copy()) except Exception: return None else: return None def set_xlim(self, draw=True): self.xlim = self._input_lim('x', self.xlim or self.axes.get_xlim()) if draw: self.draw() def set_ylim(self, draw=True): self.ylim = self._input_lim('y', self.ylim or self.axes.get_ylim()) if draw: self.draw() @staticmethod def data_dict(d): kwargs = OrderedDict(( ('name', d.name), ('xname', text_type(d.xmenu.lineEdit().text())), ('xscale', text_type(d.xscale_box.text())), ('yname', text_type(d.menu.lineEdit().text())), ('yscale', text_type(d.scale_box.text())), ('props', d.props), ('labels', d.labels), )) for key in 'xscale', 'yscale': try: kwargs[key] = ast.literal_eval(kwargs[key]) except ValueError: pass else: if float(kwargs[key]) == 1.0: del kwargs[key] if not kwargs['props']: del kwargs['props'] return kwargs def data_dicts(self): return "\n".join( text_type(dict_repr(self.data_dict(d))) for d in self.datas) def event(self, event): control_actions = { QtCore.Qt.Key_M: self._margins, QtCore.Qt.Key_O: self.edit_parameters, QtCore.Qt.Key_Q: self._close, } if (event.type() == QtCore.QEvent.KeyPress and event.modifiers() == CONTROL_MODIFIER and event.key() in control_actions): control_actions[event.key()]() return True # Create duplicate of entire GUI with Ctrl+Shift+N elif (event.type() == QtCore.QEvent.KeyPress and event.modifiers() == CONTROL_MODIFIER | QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_N): create(*[[d.obj, d.name, self.data_dict(d)] for d in self.datas]) return True # Print dictionaries of keys and scales for all data with Ctrl+Shift+P elif (event.type() == QtCore.QEvent.KeyPress and event.modifiers() == CONTROL_MODIFIER | QtCore.Qt.ShiftModifier and event.key() == QtCore.Qt.Key_P): print(self.data_dicts()) sys.stdout.flush() return True return super(Interact, self).event(event)