Esempio n. 1
0
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)
Esempio n. 2
0
 def edit_parameters(self):
     self.parent.plotChanged = True
     NavigationToolbar2QT.edit_parameters(self)
Esempio n. 3
0
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)