コード例 #1
0
class DefaultTextLineEdit(LineEdit):
    def __init__(self, parent):
        LineEdit.__init__(self, parent)
        self.editComplete.connect(self._update_name)
        self.nameChanged.connect(self._handle_name_changed)
        self.defaultNameChanged.connect(self._handle_name_changed)

    defaultNameChanged = pyqtSignal(str)
    nameChanged = pyqtSignal(str)

    defaultName = AutoProperty(str)
    name = AutoProperty(str)

    def focusInEvent(self, event):
        self._handle_interaction()
        LineEdit.focusInEvent(self, event)

    def _update_name(self, value):
        self.name = value

    def _handle_name_changed(self):
        self.setText(self.name)
        if not self.name:
            self.setPlaceholderText(self.defaultName)

    def _handle_interaction(self):
        if not self.name:
            self.name = self.defaultName
コード例 #2
0
ファイル: json_text.py プロジェクト: BitBloomTech/pyqttoolkit
class JsonTextModel(QObject):
    def __init__(self, parent):
        QObject.__init__(self, parent)
        self.textChanged.connect(self._set_data)
        self._data = {}
        self.text = '{}'

    textChanged = pyqtSignal(str)
    validationMessageChanged = pyqtSignal(str)

    text = AutoProperty(str)
    validationMessage = AutoProperty(str)

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        self.text = dumps(value,
                          sort_keys=True,
                          indent=2,
                          separators=(',', ': '))

    def _set_data(self, value):
        try:
            self._data = loads(value)
            self.validationMessage = ''
        except JSONDecodeError:
            self.validationMessage = self.tr('Error in JSON')
コード例 #3
0
class InfFloatLineEdit(LineEdit):
    def __init__(self, parent, formatter=None, update_on_enter_pressed=True):
        LineEdit.__init__(self, parent)
        self.editComplete.connect(self._handle_edit_complete)
        self.valueChanged.connect(self._handle_value_changed)
        self._formatter = formatter
        self._update_on_enter_pressed = update_on_enter_pressed

    valueChanged = pyqtSignal(float)
    value = AutoProperty(float)

    def _handle_edit_complete(self):
        try:
            self.value = float(self.text())
        except ValueError:
            self.setText(self._get_text(self.value))

    def _handle_value_changed(self, value):
        self.setText(self._get_text(value))

    def _get_text(self, value):
        if self._formatter is not None:
            return self._formatter(value)
        else:
            return str(value)

    def keyPressEvent(self, event):
        super().keyPressEvent(event)
        if event.key() in [Qt.Key_Enter, Qt.Key_Return
                           ] and self._update_on_enter_pressed:
            self.valueChanged.emit(self.value)
コード例 #4
0
class MetadataSelector(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)

        self._drop_down = MetadataSelectorDropDown(self)
        self._drop_down.setSizePolicy(QSizePolicy.Expanding,
                                      QSizePolicy.Expanding)
        self._add_item = IconButton('plus.svg', self.tr('Add Item'), self,
                                    QSize(20, 20), 2)
        self._add_item.clicked.connect(self.addItemSelected)

        self._layout = QHBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.addWidget(self._drop_down)
        self._layout.addWidget(self._add_item)

        bind(self, self._drop_down, 'selectedId')

    selectedIdChanged = pyqtSignal(str)
    addItemSelected = pyqtSignal()

    selectedId = AutoProperty(str)

    def setModel(self, model):
        self._drop_down.setModel(model)

    def setSelectorEnabled(self, enabled):
        self._drop_down.setEnabled(enabled)
コード例 #5
0
class MetadataSelectorDropDown(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self, parent)

        self._drop_down = ComboBox(self)
        self._drop_down.currentIndexChanged.connect(self._update_current)

        self._layout = QGridLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.addWidget(self._drop_down)

        self.selectedIdChanged.connect(self._handle_selected_id_changed)
        self.selectedId = None

    selectedIdChanged = pyqtSignal(str)

    selectedId = AutoProperty(str)

    def setModel(self, model):
        self._drop_down.setModel(model)
        model.dataChanged.connect(self._handle_selected_id_changed)

    def _update_current(self, index):
        model_index = self._drop_down.model().createIndex(index, 0)
        self.selectedId = self._drop_down.model().data(model_index, DataRole)

    def _handle_selected_id_changed(self):
        model = self._drop_down.model()
        for i in range(model.rowCount()):
            model_index = model.createIndex(i, 0)
            if model.data(model_index, DataRole) == self.selectedId:
                self._drop_down.setCurrentIndex(i)
                return
コード例 #6
0
class CodeTextModel(QObject):
    def __init__(self, parent, validator):
        QObject.__init__(self, parent)
        self.textChanged.connect(self._validate)
        self._validator = validator
        self.text = ''
        self.output = ''

    textChanged = pyqtSignal(str)
    outputChanged = pyqtSignal(str)
    validationMessageChanged = pyqtSignal(str)

    text = AutoProperty(str)
    output = AutoProperty(str)
    validationMessage = AutoProperty(str)

    def _validate(self, value):
        validation_model = self._validator(value)
        self.validationMessage = validation_model.error if validation_model else ''
コード例 #7
0
class BindableLineEdit(LineEdit):
    def __init__(self, parent):
        LineEdit.__init__(self, parent)
        self.editComplete.connect(self._update_value)
        self.valueChanged.connect(self._handle_value_changed)
    
    valueChanged = pyqtSignal(str)

    value = AutoProperty(str)
    
    def _update_value(self, value):
        self.value = value
    
    def _handle_value_changed(self, value):
        self.setText(value)
コード例 #8
0
    class _TypeEdit(LineEdit):
        def __init__(self, parent):
            LineEdit.__init__(self, parent)
            self.setValidator(QDoubleValidator())
            self.editComplete.connect(self._set_value)
            self.valueChanged.connect(self._set_text)

        valueChanged = pyqtSignal(dtype)

        value = AutoProperty(dtype)

        def _set_value(self, text):
            try:
                self.value = dtype(text)
            except (TypeError, ValueError):
                self.value = default_value
                self._set_text(self.value)

        def _set_text(self, value):
            self.setText(str(value))
コード例 #9
0
class BindableIconButton(IconButton):
    def __init__(self,
                 icon,
                 title,
                 parent,
                 size=None,
                 padding=None,
                 color=None,
                 hover_color=None):
        IconButton.__init__(self, icon, title, parent, size, padding, color,
                            hover_color)
        self.setCheckable(True)
        self.clicked.connect(self._handle_clicked)
        self.checkedChanged.connect(self.setChecked)

    checkedChanged = pyqtSignal(bool)
    checked = AutoProperty(bool)

    def _handle_clicked(self):
        self.checked = self.isChecked()
コード例 #10
0
class ListView(QListView):
    def __init__(self, parent):
        QListView.__init__(self, parent)
        self.selectedIndexChanged.connect(self._handle_selected_index_changed)
        self._updating_selection = False

    selectedItemsChanged = pyqtSignal()
    selectedIndexChanged = pyqtSignal(int)

    selectedIndex = AutoProperty(int)

    def selectionChanged(self, current_selection, previous_selection):
        self._updating_selection = True
        result = QListView.selectionChanged(self, current_selection,
                                            previous_selection)
        self.selectedIndex = current_selection.indexes()[0].row(
        ) if current_selection.indexes() else -1
        self.selectedItemsChanged.emit()
        self._updating_selection = False
        return result

    def _handle_selected_index_changed(self, index):
        if self.model() is not None and not self._updating_selection:
            self.setCurrentIndex(self.model().createIndex(index, 0))
コード例 #11
0
class DateTimeEdit(QDateTimeEdit):
    class Limit(Enum):
        min = 0
        max = 1

    def __init__(self, parent, default=Limit.min, calendar_popup=True):
        QDateTimeEdit.__init__(self, parent)
        self.setDisplayFormat('dd-MMM-yyyy hh:mm')
        self.setButtonSymbols(QDateTimeEdit.NoButtons)
        self._min_date = self._max_date = None
        self._start_date = self._end_date = None
        self.isLimit = False
        self._default = default
        self.isLimitChanged.connect(self._handle_is_limit_changed)
        self.dateTimeChanged.connect(self._handle_date_time_changed)
        self.setCalendarPopup(calendar_popup)
        if calendar_popup:
            self.calendarWidget().clicked.connect(
                self._handle_date_changed_from_calendar)

    valueChanged = pyqtSignal(QDateTime)
    isLimitChanged = pyqtSignal(bool)

    isLimit = AutoProperty(bool)

    def focusOutEvent(self, event):
        if self._start_date and self.dateTime() < self._start_date:
            self.setDateTime(self._start_date)
        elif self._min_date and self.dateTime() < self._min_date:
            self.setDateTime(self._min_date)
        elif self._end_date and self.dateTime() > self._end_date:
            self.setDateTime(self._end_date)
        elif self._max_date and self.dateTime() > self._max_date:
            self.setDateTime(self._max_date)
        return QDateTimeEdit.focusOutEvent(self, event)

    def setDateTimeRange(self, start_date, end_date):
        self._start_date = start_date
        self._end_date = end_date

    def setMinimumDateTime(self, min_date):
        self._min_date = min_date
        super().setMinimumDateTime(min_date)

    def setMaximumDateTime(self, max_date):
        self._max_date = max_date
        super().setMaximumDateTime(max_date)

    @auto_property(QDateTime)
    def value(self):
        return QDateTime() if self.isLimit else self.dateTime()

    @value.setter
    def value(self, value):
        value = value or QDateTime()
        if not value.isValid(
        ) and self._start_date is not None and self._end_date is not None:
            date_time = self._start_date if self._default == self.Limit.min else self._end_date
            is_limit = True
        else:
            date_time = value
            is_limit = False
        if self.dateTime() != date_time:
            self.setDateTime(date_time)
        self.isLimit = is_limit
        self.valueChanged.emit(value)

    def reset(self):
        self.value = QDateTime()

    def _handle_is_limit_changed(self, is_limit):
        stylesheet = 'DateTimeEdit {font: italic}' if is_limit else ''
        self.setStyleSheet(stylesheet)

    def _handle_date_time_changed(self, value):
        if value is not None or (isinstance(value, QDateTime)
                                 and value.isValid()):
            self.isLimit = False
        self.valueChanged.emit(self.value)

    def _handle_date_changed_from_calendar(self, value):
        self.value = QDateTime(value, QTime(0, 0))
コード例 #12
0
ファイル: base.py プロジェクト: BitBloomTech/pyqttoolkit
class MatPlotLibBase(QWidget):
    def __init__(self,
                 parent,
                 file_dialog_service,
                 h_margin=(0.8, 0.1),
                 v_margin=(0.5, 0.15),
                 h_axes=[Size.Scaled(1.0)],
                 v_axes=[Size.Scaled(1.0)],
                 nx_default=1,
                 ny_default=1):
        QWidget.__init__(self, parent)
        self._file_dialog_service = file_dialog_service
        self._figure = Figure()
        self._canvas = FigureCanvas(self._figure)
        h = [Size.Fixed(h_margin[0]), *h_axes, Size.Fixed(h_margin[1])]
        v = [Size.Fixed(v_margin[0]), *v_axes, Size.Fixed(v_margin[1])]
        self._divider = Divider(self._figure, (0.0, 0.0, 1.0, 1.0),
                                h,
                                v,
                                aspect=False)
        self._axes = LocatableAxes(self._figure, self._divider.get_position())
        self._axes.set_axes_locator(
            self._divider.new_locator(nx=nx_default, ny=ny_default))
        self._axes.set_zorder(2)
        self._axes.patch.set_visible(False)
        for spine in ['top', 'right']:
            self._axes.spines[spine].set_visible(False)
        self._figure.add_axes(self._axes)

        self._canvas.setParent(self)

        self._layout = QVBoxLayout(self)
        self._layout.setContentsMargins(0, 0, 0, 0)
        self._layout.addWidget(self._canvas)
        self.setLayout(self._layout)

        self._figure.canvas.mpl_connect('scroll_event', self._on_scroll)
        self._xy_extents = None
        self._background_cache = None
        self._decoration_artists = []
        self._is_panning = False

        self._zoom_selector = _RectangleSelector(self._axes,
                                                 self._zoom_selected)
        self._zoom_selector.set_active(False)
        self._x_extent_padding = 0.01
        self._y_extent_padding = 0.01
        self._axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4))
        self._axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4))
        self._active_tools = {}
        self._span = _SpanSeletor(self._axes,
                                  self._handle_span_select,
                                  'horizontal',
                                  rectprops=dict(alpha=0.2,
                                                 facecolor='red',
                                                 edgecolor='k'),
                                  span_stays=True)
        self._span.set_on_select_none(self._handle_span_select_none)
        self.span = self._previous_span = None
        self._span_center_mouse_event = None
        self._span_left_mouse_event = None
        self._span_right_mouse_event = None
        self._figure.canvas.mpl_connect('button_press_event',
                                        self._handle_press)
        self._figure.canvas.mpl_connect('motion_notify_event',
                                        self._handle_move)
        self._figure.canvas.mpl_connect('button_release_event',
                                        self._handle_release)
        self._figure.canvas.mpl_connect('resize_event', self._handle_resize)
        self.activateTool(ToolType.span, self.isActiveDefault(ToolType.span))
        self._pan_event = None
        self._pending_draw = None
        self._pending_artists_draw = None
        self._other_draw_events = []
        self._draw_timer = QTimer(self)
        self._draw_timer.timeout.connect(self._do_draw_events)
        self._draw_timer.start(20)
        self._zoom_skew = None

        self._menu = QMenu(self)
        self._copy_image_action = QAction(self.tr('Copy To Clipboard'), self)
        self._copy_image_action.triggered.connect(self.copyToClipboard)
        self._copy_image_action.setShortcuts(QKeySequence.Copy)
        self._save_image_action = QAction(self.tr('Save As Image'), self)
        self._save_image_action.triggered.connect(self.saveAsImage)
        self._show_table_action = QAction(self.tr('Show Table'), self)
        self._show_table_action.triggered.connect(self.showTable)
        self._menu.addAction(self._copy_image_action)
        self._menu.addAction(self._save_image_action)
        self._menu.addAction(self._show_table_action)
        self.addAction(self._copy_image_action)

        self._table_view = None
        self._single_axis_zoom_enabled = True
        self._cached_label_width_height = None

        if hasattr(type(self), 'dataChanged'):
            self.dataChanged.connect(self._on_data_changed)

        self._options_view = None
        self._secondary_axes = self._secondary_y_extent = self._secondary_x_extent = None
        self._legend = None
        self._draggable_legend = None
        self._setting_axis_limits = False

        self.hasHiddenSeries = False

    enabledToolsChanged = pyqtSignal()
    spanChanged = pyqtSignal(SpanModel)
    hasHiddenSeriesChanged = pyqtSignal(bool)

    span = AutoProperty(SpanModel)
    hasHiddenSeries = AutoProperty(bool)

    def setOptionsView(self, options_view):
        self._options_view = options_view
        self._options_view.setSecondaryYLimitsEnabled(
            self._secondary_y_enabled())
        self._options_view.setSecondaryXLimitsEnabled(
            self._secondary_x_enabled())

        self._options_view.showGridLinesChanged.connect(
            self._update_grid_lines)
        self._options_view.xAxisLowerLimitChanged.connect(
            self._handle_options_view_limit_changed(x_min_changed=True))
        self._options_view.xAxisUpperLimitChanged.connect(
            self._handle_options_view_limit_changed(x_max_changed=True))
        self._options_view.yAxisLowerLimitChanged.connect(
            self._handle_options_view_limit_changed(y_min_changed=True))
        self._options_view.yAxisUpperLimitChanged.connect(
            self._handle_options_view_limit_changed(y_max_changed=True))
        self._options_view.xAxisLimitsChanged.connect(
            self._handle_options_view_limit_changed(x_min_changed=True,
                                                    x_max_changed=True))
        self._options_view.yAxisLimitsChanged.connect(
            self._handle_options_view_limit_changed(y_min_changed=True,
                                                    y_max_changed=True))

        self._options_view.secondaryXAxisLowerLimitChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                x_min_changed=True))
        self._options_view.secondaryXAxisUpperLimitChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                x_max_changed=True))
        self._options_view.secondaryYAxisLowerLimitChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                y_min_changed=True))
        self._options_view.secondaryYAxisUpperLimitChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                y_max_changed=True))
        self._options_view.secondaryXAxisLimitsChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                x_min_changed=True, x_max_changed=True))
        self._options_view.secondaryYAxisLimitsChanged.connect(
            self._handle_options_view_secondary_limit_changed(
                y_min_changed=True, y_max_changed=True))

    def setLegendControl(self, legend_control):
        self._legend_control = legend_control
        self._legend_control.seriesUpdated.connect(self._legend_series_updated)
        self._legend_control.showLegendChanged.connect(self._show_legend)
        self._legend_control.seriesNameChanged.connect(
            self._handle_series_name_changed)
        self._legend_control.showSeriesChanged.connect(
            self._handle_show_series_changed)
        bind(self._legend_control, self, 'hasHiddenSeries', two_way=False)

    def _legend_series_updated(self):
        if self._legend is not None:
            self._show_legend(self._legend_control.showLegend)

    def _show_legend(self, show):
        if self._legend and not show:
            self._legend.remove()
            self._legend = None
            self.draw()
        elif show:
            if self._legend:
                self._legend.remove()
            show_series = self._legend_control.showSeries
            handles = [
                h for h, s in zip(self._legend_control.seriesHandles,
                                  show_series) if s
            ]
            names = [
                n
                for n, s in zip(self._legend_control.seriesNames, show_series)
                if s
            ]
            axes = (self._secondary_axes if self._secondary_axes
                    and self._secondary_axes.get_visible()
                    and self._secondary_axes.get_zorder() >
                    self._axes.get_zorder() else self._axes)
            self._legend = self._create_legend(
                axes,
                handles,
                names,
                markerscale=self._get_legend_markerscale())
            if self._get_legend_text_color() is not None:
                for text in self._legend.texts:
                    text.set_color(self._get_legend_text_color())
            self._draggable_legend = DraggableLegend(self._legend)
            self.draw()

    def _get_legend_markerscale(self):
        return 5

    def _create_legend(self, axes, handles, names, **kwargs):
        return axes.legend(handles, names, **kwargs)

    def _get_legend_text_color(self):
        return None

    def _handle_series_name_changed(self, index, series_name):
        if self._legend is not None and index < len(
                self._legend_control.seriesHandles):
            visible_handles = [
                h for h, s in zip(self._legend_control.seriesHandles,
                                  self._legend_control.showSeries)
                if s and h is not None
            ]
            try:
                legend_index = visible_handles.index(
                    self._legend_control.seriesHandles[index])
            except ValueError:
                return
            if legend_index < len(self._legend.texts):
                self._legend.texts[legend_index].set_text(series_name)
                self.draw()

    def _handle_show_series_changed(self, index, show_series):
        if index < len(self._legend_control.seriesHandles):
            self._set_series_visibility(
                self._legend_control.seriesHandles[index], show_series)
        if self._legend is not None:
            self._show_legend(self._legend_control.showLegend)
        else:
            self.draw()

    def _set_series_visibility(self, handle, visible):
        if not handle:
            return
        if hasattr(handle, 'set_visible'):
            handle.set_visible(visible)
        elif hasattr(handle, 'get_children'):
            for child in handle.get_children():
                self._set_series_visibility(child, visible)

    def _update_grid_lines(self):
        show_grid_lines = False if self._options_view is None else self._options_view.showGridLines
        gridline_color = self._axes.spines['bottom'].get_edgecolor()
        gridline_color = gridline_color[0], gridline_color[1], gridline_color[
            2], 0.5
        kwargs = dict(color=gridline_color,
                      alpha=0.5) if show_grid_lines else {}
        self._axes.grid(show_grid_lines, **kwargs)
        self.draw()

    def _handle_options_view_limit_changed(self,
                                           x_min_changed=False,
                                           x_max_changed=False,
                                           y_min_changed=False,
                                           y_max_changed=False):
        def _():
            if self._options_view is None or self._setting_axis_limits:
                return
            (x_min, x_max), (y_min, y_max) = (new_x_min, new_x_max), (
                new_y_min, new_y_max) = self._get_xy_extents()
            (x_opt_min,
             x_opt_max), (y_opt_min,
                          y_opt_max) = self._get_options_view_xy_extents()
            if x_min_changed:
                new_x_min = x_opt_min
            if x_max_changed:
                new_x_max = x_opt_max
            if y_min_changed:
                new_y_min = y_opt_min
            if y_max_changed:
                new_y_max = y_opt_max
            if [new_x_min, new_x_max, new_y_min, new_y_max
                ] != [x_min, x_max, y_min, y_max]:
                self._xy_extents = (new_x_min, new_x_max), (new_y_min,
                                                            new_y_max)
                self._set_axes_limits()
                self.draw()

        return _

    def _get_options_view_xy_extents(self):
        (x_data_min, x_data_max), (y_data_min,
                                   y_data_max) = self._get_data_xy_extents()
        x_min = x_data_min if np.isnan(
            self._options_view.xAxisLowerLimit
        ) else self._options_view.xAxisLowerLimit
        x_max = x_data_max if np.isnan(
            self._options_view.xAxisUpperLimit
        ) else self._options_view.xAxisUpperLimit
        y_min = y_data_min if np.isnan(
            self._options_view.yAxisLowerLimit
        ) else self._options_view.yAxisLowerLimit
        y_max = y_data_max if np.isnan(
            self._options_view.yAxisUpperLimit
        ) else self._options_view.yAxisUpperLimit
        return (x_min, x_max), (y_min, y_max)

    def _handle_options_view_secondary_limit_changed(self,
                                                     x_min_changed=False,
                                                     x_max_changed=False,
                                                     y_min_changed=False,
                                                     y_max_changed=False):
        def _():
            if self._options_view is None or self._setting_axis_limits:
                return
            updated = False
            (x_opt_min, x_opt_max), (
                y_opt_min,
                y_opt_max) = self._get_options_view_secondary_xy_extents()
            if self._has_secondary_y_extent() and (y_min_changed
                                                   or y_max_changed):
                y_min, y_max = new_y_min, new_y_max = self._get_secondary_y_extent(
                )
                if y_min_changed:
                    new_y_min = y_opt_min
                if y_max_changed:
                    new_y_max = y_opt_max
                if [new_y_min, new_y_max] != [y_min, y_max]:
                    self._secondary_y_extent = (new_y_min, new_y_max)
                    updated = True
            if self._has_secondary_x_extent() and (x_min_changed
                                                   or x_max_changed):
                x_min, x_max = new_x_min, new_x_max = self._get_secondary_x_extent(
                )
                if x_min_changed:
                    new_x_min = x_opt_min
                if x_max_changed:
                    new_x_max = x_opt_max
                if [new_x_min, new_x_max] != [x_min, x_max]:
                    self._secondary_x_extent = (new_x_min, new_x_max)
                    updated = True
            if updated:
                self._set_axes_limits()
                self.draw()

        return _

    def _get_options_view_secondary_xy_extents(self):
        x_data_min, x_data_max = self._get_data_secondary_x_extent()
        y_data_min, y_data_max = self._get_data_secondary_y_extent()
        x_min = x_data_min if np.isnan(
            self._options_view.secondaryXAxisLowerLimit
        ) else self._options_view.secondaryXAxisLowerLimit
        x_max = x_data_max if np.isnan(
            self._options_view.secondaryXAxisUpperLimit
        ) else self._options_view.secondaryXAxisUpperLimit
        y_min = y_data_min if np.isnan(
            self._options_view.secondaryYAxisLowerLimit
        ) else self._options_view.secondaryYAxisLowerLimit
        y_max = y_data_max if np.isnan(
            self._options_view.secondaryYAxisUpperLimit
        ) else self._options_view.secondaryYAxisUpperLimit
        return (x_min, x_max), (y_min, y_max)

    def _on_data_changed(self):
        self._cached_label_width_height = None

    def closeEvent(self, event):
        QWidget.closeEvent(self, event)
        if event.isAccepted():
            self._zoom_selector.onselect = self._span.onselect = self._span._select_none_handler = None

    def set_divider_h_margin(self, h_margin):
        h = [
            Size.Fixed(h_margin[0]),
            Size.Scaled(1.0),
            Size.Fixed(h_margin[1])
        ]
        self._divider.set_horizontal(h)

    def set_divider_v_margin(self, v_margin):
        v = [
            Size.Fixed(v_margin[0]),
            Size.Scaled(1.0),
            Size.Fixed(v_margin[1])
        ]
        self._divider.set_vertical(v)

    @property
    def x_extent_padding(self):
        return self._x_extent_padding

    @x_extent_padding.setter
    def x_extent_padding(self, value):
        self._x_extent_padding = value

    @property
    def y_extent_padding(self):
        return self._y_extent_padding

    @y_extent_padding.setter
    def y_extent_padding(self, value):
        self._y_extent_padding = value

    def _in_interval(self, value, interval):
        return interval[0] <= value <= interval[1]

    def _interval_skew(self, value, interval):
        return (value - interval[0]) / (interval[1] - interval[0])

    def _in_x_scroll_zone(self, event):
        return self._in_interval(event.x, self._axes.bbox.intervalx
                                 ) and event.y <= self._axes.bbox.intervaly[1]

    def _in_y_scroll_zone(self, event):
        return self._in_interval(event.y, self._axes.bbox.intervaly
                                 ) and event.x <= self._axes.bbox.intervalx[1]

    def _on_scroll(self, event):
        if self._secondary_axes is not None:
            self._handle_scroll_secondary(event)
        in_x = self._in_x_scroll_zone(event)
        in_y = self._in_y_scroll_zone(event)
        if in_x or in_y and event.button in ['up', 'down']:
            (x_min, x_max), (y_min, y_max) = self._get_actual_xy_extents()
            if (in_x and self._single_axis_zoom_enabled) or (in_x and in_y):
                skew = self._zoom_skew and self._zoom_skew[0]
                skew = self._interval_skew(
                    event.x,
                    self._axes.bbox.intervalx) if skew is None else skew
                x_min, x_max = self._zoom(x_min, x_max, skew, event.button)
            if (in_y and self._single_axis_zoom_enabled) or (in_x and in_y):
                skew = self._zoom_skew and self._zoom_skew[1]
                skew = self._interval_skew(
                    event.y,
                    self._axes.bbox.intervaly) if skew is None else skew
                y_min, y_max = self._zoom(y_min, y_max, skew, event.button)
            self._xy_extents = (x_min, x_max), (y_min, y_max)
        self._set_axes_limits()
        self.draw()

    def _in_secondary_y_scroll_zone(self, event):
        return self._in_interval(event.y, self._axes.bbox.intervaly) and \
                event.x >= self._axes.bbox.intervalx[1]

    def _in_secondary_x_scroll_zone(self, event):
        return self._in_interval(event.x, self._axes.bbox.intervalx) and \
                event.y >= self._axes.bbox.intervaly[1]

    def _handle_scroll_secondary(self, event):
        if self._has_secondary_y_extent():
            in_secondary_y = self._in_secondary_y_scroll_zone(event)
            if in_secondary_y and event.button in ['up', 'down']:
                self._secondary_y_extent = self._zoom(
                    *self._get_secondary_y_extent(),
                    self._interval_skew(event.y, self._axes.bbox.intervaly),
                    event.button)
        if self._has_secondary_x_extent():
            in_secondary_x = self._in_secondary_x_scroll_zone(event)
            if in_secondary_x and event.button in ['up', 'down']:
                self._secondary_x_extent = self._zoom(
                    *self._get_secondary_x_extent(),
                    self._interval_skew(event.x, self._axes.bbox.intervalx),
                    event.button)

    def _get_zoom_multiplier(self):
        return 20 / 19

    def _zoom(self, min_, max_, skew, direction):
        zoom_multiplier = self._get_zoom_multiplier(
        ) if direction == 'up' else 1 / self._get_zoom_multiplier()
        range_ = max_ - min_
        diff = (range_ * (1 / zoom_multiplier)) - range_
        max_ += diff * (1 - skew)
        min_ -= diff * skew
        return min_, max_

    def _set_axes_limits(self):
        try:
            self._setting_axis_limits = True
            if self._secondary_axes is not None:
                self._set_secondary_axes_limits()
            self._update_ticks()
            (x_min, x_max), (y_min, y_max) = self._get_xy_extents()
            if self._options_view is not None:
                if self._options_view.x_limits:
                    self._options_view.setXLimits(float(x_min), float(x_max))
                if self._options_view.y_limits:
                    self._options_view.setYLimits(float(y_min), float(y_max))
            self._axes.set_xlim(*_safe_limits(x_min, x_max))
            self._axes.set_ylim(*_safe_limits(y_min, y_max))
        finally:
            self._setting_axis_limits = False

    def _set_secondary_axes_limits(self):
        if self._options_view is not None:
            if self._options_view.secondary_y_limits:
                enabled = self._secondary_y_enabled()
                secondary_y_min, secondary_y_max = self._get_secondary_y_extent(
                ) if enabled else (float('nan'), float('nan'))
                self._options_view.setSecondaryYLimitsEnabled(enabled)
                self._options_view.setSecondaryYLimits(float(secondary_y_min),
                                                       float(secondary_y_max))
            if self._options_view.secondary_x_limits:
                enabled = self._secondary_x_enabled()
                secondary_x_min, secondary_x_max = self._get_secondary_x_extent(
                ) if enabled else (float('nan'), float('nan'))
                self._options_view.setSecondaryXLimitsEnabled(enabled)
                self._options_view.setSecondaryXLimits(float(secondary_x_min),
                                                       float(secondary_x_max))
        if self._has_secondary_y_extent():
            self._secondary_axes.set_ylim(*_safe_limits(
                *self._get_secondary_y_extent()))
        if self._has_secondary_x_extent():
            self._secondary_axes.set_xlim(*_safe_limits(
                *self._get_secondary_x_extent()))

    def _secondary_y_enabled(self):
        return True if self._secondary_axes and self._secondary_axes.get_visible(
        ) and self._has_secondary_y_extent() else False

    def _secondary_x_enabled(self):
        return True if self._secondary_axes and self._secondary_axes.get_visible(
        ) and self._has_secondary_x_extent() else False

    def _set_axes_labels(self):
        self._axes.set_xlabel(self.data.xAxisTitle)
        self._axes.set_ylabel(self.data.yAxisTitle)

    def _set_center(self, center):
        if not all(c is not None for c in center):
            center = (0, 0)
        x_extent, y_extent = self._get_xy_extents()
        span = x_extent[1] - x_extent[0], y_extent[1] - y_extent[0]
        x_extent = center[0] - span[0] / 2, center[0] + span[0] / 2
        y_extent = center[1] - span[1] / 2, center[1] + span[1] / 2
        self._xy_extents = x_extent, y_extent

    def _get_xy_extents(self):
        if self.data is None:
            return (0, 0), (0, 0)
        if self._xy_extents is None:
            return self._get_data_xy_extents()
        return self._xy_extents

    def _get_data_xy_extents(self):
        if self.data is None:
            return (0, 0), (0, 0)
        (x_min, x_max), (y_min, y_max) = self.data.get_xy_extents()
        return self._pad_extent(x_min, x_max,
                                self.x_extent_padding), self._pad_extent(
                                    y_min, y_max, self.y_extent_padding)

    def _has_secondary_y_extent(self):
        return hasattr(self.data, 'get_secondary_y_extent')

    def _get_secondary_y_extent(self):
        if self._secondary_y_extent is not None:
            return self._secondary_y_extent
        if self.data is not None:
            return self._get_data_secondary_y_extent()
        return (0, 0)

    def _get_data_secondary_y_extent(self):
        if self.data is None:
            return (0, 0)
        return self._pad_extent(*self.data.get_secondary_y_extent(),
                                self.y_extent_padding)

    def _has_secondary_x_extent(self):
        return hasattr(self.data, 'get_secondary_x_extent')

    def _get_secondary_x_extent(self):
        if self._secondary_x_extent is not None:
            return self._secondary_x_extent
        if self.data is not None:
            return self._get_data_secondary_x_extent()
        return (0, 0)

    def _get_data_secondary_x_extent(self):
        if self.data is None or not hasattr(self.data,
                                            'get_secondary_x_extent'):
            return (0, 0)
        return self._pad_extent(*self.data.get_secondary_x_extent(),
                                self.x_extent_padding)

    def _get_actual_xy_extents(self):
        return self._axes.get_xlim(), self._axes.get_ylim()

    def _pad_extent(self, min_, max_, padding):
        min_, max_ = self._zero_if_nan(min_), self._zero_if_nan(max_)
        range_ = max_ - min_
        return min_ - padding * range_, max_ + padding * range_

    def _zoom_selected(self, start_pos, end_pos):
        x_min, x_max = min(start_pos.xdata,
                           end_pos.xdata), max(start_pos.xdata, end_pos.xdata)
        y_min, y_max = min(start_pos.ydata,
                           end_pos.ydata), max(start_pos.ydata, end_pos.ydata)
        self._xy_extents = (x_min, x_max), (y_min, y_max)
        self._set_axes_limits()
        self.draw()

    def _handle_span_select(self, x_min, x_max):
        x_min, x_max = self._round_to_bin_width(x_min, x_max)
        self._update_span_rect(x_min, x_max)
        self.span = SpanModel(self, x_min, x_max)
        self.draw()

    def _handle_span_select_none(self):
        self.span = None

    def _handle_press(self, event):
        if event.button == 1:
            if self._is_panning:
                self._pan_event = event
            elif self._span.active:
                self._handle_span_press(event)

    def _handle_move(self, event):
        if event.xdata and self._pan_event:
            self._handle_pan_move(event)
        elif event.xdata and any(self._span_events()):
            self._handle_span_move(event)

    def _handle_release(self, event):
        if self._pan_event:
            self._pan_event = None
        elif any(self._span_events()):
            self._handle_span_release(event)

    def _handle_pan_move(self, event):
        from_x, from_y = self._axes.transData.inverted().transform(
            (self._pan_event.x, self._pan_event.y))
        to_x, to_y = self._axes.transData.inverted().transform(
            (event.x, event.y))
        self._pan(from_x - to_x, from_y - to_y)
        self._pan_event = event

    def _pan(self, delta_x, delta_y):
        (x_min, x_max), (y_min, y_max) = self._get_xy_extents()
        self._xy_extents = (x_min + delta_x,
                            x_max + delta_x), (y_min + delta_y,
                                               y_max + delta_y)
        self._set_axes_limits()
        self.draw()

    def _span_events(self):
        return self._span_center_mouse_event, self._span_left_mouse_event, self._span_right_mouse_event

    def _handle_span_press(self, event):
        if not event.xdata:
            return
        span_min, span_max = (self.span.left,
                              self.span.right) if self.span else (0, 0)
        edge_tolerance = self._span_tolerance()
        if abs(span_min - event.xdata) < edge_tolerance:
            self._span.active = False
            self._span_left_mouse_event = event
        elif abs(span_max - event.xdata) < edge_tolerance:
            self._span.active = False
            self._span_right_mouse_event = event
        elif span_min < event.xdata < span_max:
            self._span.active = False
            self._span_center_mouse_event = event

    def _handle_span_move(self, event):
        if not self.span:
            return
        x_min, x_max = self.span.left, self.span.right
        last_event = next(x for x in self._span_events() if x)
        diff_x = event.xdata - last_event.xdata
        if self._span_center_mouse_event is not None:
            self._update_span_rect(x_min + diff_x)
        elif self._span_left_mouse_event is not None:
            self._update_span_rect(x_min + diff_x, x_max)
        elif self._span_right_mouse_event is not None:
            self._update_span_rect(x_min, x_max + diff_x)
        self.draw([self._span.rect])

    def _handle_span_release(self, _event):
        x_min = self._span.rect.get_x()
        x_max = x_min + self._span.rect.get_width()
        x_min, x_max = self._round_to_bin_width(x_min, x_max)
        self._update_span_rect(x_min, x_max)
        self.span = SpanModel(self, x_min, x_max)
        self.draw()
        self._span.active = True
        self._span_center_mouse_event = self._span_left_mouse_event = self._span_right_mouse_event = None

    def _update_span_rect(self, x_min, x_max=None):
        self._span.rect.set_x(x_min)
        self._span.stay_rect.set_x(x_min)
        if x_max:
            self._span.rect.set_width(x_max - x_min)
            self._span.stay_rect.set_width(x_max - x_min)

    def _round_to_bin_width(self, x_min, x_max):
        return x_min, x_max

    def _span_tolerance(self):
        return 5

    def toolEnabled(self, _tool_type):
        return False

    def toolAvailable(self, _tool_type):
        return False

    def activateTool(self, tool_type, active):
        if tool_type == ToolType.zoom:
            self._zoom_selector.set_active(active)
        elif tool_type == ToolType.span:
            if self._span.active and not active:
                self._previous_span = self.span
                self.span = None
                for r in [self._span.rect, self._span.stay_rect]:
                    self._remove_artist(r)
            elif not self._span.active and active:
                self.span = self._previous_span
                for r in [self._span.rect, self._span.stay_rect]:
                    self._add_artist(r)
            self._span.active = active
            self.draw()
        elif tool_type == ToolType.pan:
            self._is_panning = active
        self._active_tools[tool_type] = active

    def toolActive(self, tool_type):
        return self._active_tools.get(tool_type, False)

    def isActiveDefault(self, _tool_type):
        return False

    def _add_artist(self, artist):
        self._axes.add_artist(artist)
        self._decoration_artists.append(artist)

    def _remove_artist(self, artist):
        artist.remove()
        if artist in self._decoration_artists:
            self._decoration_artists.remove(artist)

    def _handle_resize(self, _event):
        self._update_ticks()
        return self.draw()

    def draw(self, artists=None):
        if artists is None:

            def _update():
                for a in self._decoration_artists:
                    a.remove()
                self._canvas.draw()
                self._background_cache = self._canvas.copy_from_bbox(
                    self._figure.bbox)
                for a in self._decoration_artists:
                    self._axes.add_artist(a)
                    self._axes.draw_artist(a)
                self._canvas.update()

            self._pending_draw = _update
        else:

            def _update():
                if self._background_cache is None:
                    raise RuntimeError('Must run draw before drawing artists!')
                self._canvas.restore_region(self._background_cache)
                for a in artists:
                    self._axes.draw_artist(a)
                self._canvas.update()

            self._pending_artists_draw = _update

    def _do_draw_events(self):
        if self._pending_draw is not None:
            self._pending_draw()
            self._pending_draw = None
        if self._pending_artists_draw is not None:
            self._pending_artists_draw()
            self._pending_artists_draw = None
        if self._other_draw_events:
            for draw_event in self._other_draw_events:
                draw_event()
            self._other_draw_events = []

    def addDrawEvent(self, draw_event):
        self._other_draw_events.append(draw_event)

    def resetZoom(self):
        self._secondary_y_extent = self._secondary_x_extent = None
        self._xy_extents = None
        self._set_axes_limits()
        self.draw()

    def _twinx(self, ylabel):
        axes = self._axes.twinx()
        for spine in ['top', 'left']:
            axes.spines[spine].set_visible(False)
        axes.set_ylabel(ylabel)
        axes.set_zorder(1)
        return axes

    @property
    def axes(self):
        return self._axes

    @property
    def secondary_axes(self):
        if self._secondary_axes is None:
            self._set_secondary_axes(self._twinx(''))
        return self._secondary_axes

    def _set_secondary_axes(self, axes):
        self._secondary_axes = axes

    @staticmethod
    def sizeHint():
        """function::sizeHint()
        Override the default sizeHint to ensure the plot has an initial size
        """
        return QSize(600, 400)

    def minimumSizeHint(self):
        """function::sizeHint()
        Override the default sizeHint to ensure the plot does not shrink below minimum size
        """
        return self.sizeHint()

    @staticmethod
    def _zero_if_nan(value):
        return value if not isinstance(value,
                                       float) or not np.isnan(value) else 0

    def canShowTable(self):
        return hasattr(self, 'data') and self.data is not None and hasattr(
            self.data, 'table')

    def contextMenuEvent(self, event):
        self._show_table_action.setEnabled(self.canShowTable())
        self._menu.exec_(event.globalPos())

    def copyToClipboard(self):
        with BytesIO() as buffer:
            self._figure.savefig(buffer,
                                 facecolor=self._figure.get_facecolor())
            QApplication.clipboard().setImage(
                QImage.fromData(buffer.getvalue()))

    def saveAsImage(self):
        filename = self._file_dialog_service.get_save_filename(
            self, self.tr('Portable Network Graphics (*.png)'))
        if filename:
            self._figure.savefig(filename,
                                 facecolor=self._figure.get_facecolor())

    def showTable(self):
        if self.canShowTable():
            self._table_view = TableView(None)
            self._table_view.pasteEnabled = False
            self._table_view.setModel(self.data.table)
            self._table_view.setMinimumSize(800, 600)
            self._table_view.show()

    def _update_ticks(self):
        if not self.data:
            return
        if hasattr(self.data, 'x_labels'):
            step = self.data.x_tick_interval if hasattr(
                self.data, 'x_tick_interval') else None
            x_ticks, x_labels = self._get_labels(self.data.x_labels,
                                                 step,
                                                 horizontal=True)
            self._axes.set_xticks(x_ticks)
            self._axes.set_xticklabels(x_labels)
        if hasattr(self.data, 'y_labels'):
            step = self.data.y_tick_interval if hasattr(
                self.data, 'y_tick_interval') else None
            y_ticks, y_labels = self._get_labels(self.data.y_labels,
                                                 step,
                                                 horizontal=False)
            self._axes.set_yticks(y_ticks)
            self._axes.set_yticklabels(y_labels)

    def _get_labels(self, labels, step, horizontal=True):
        (x0, x1), (y0, y1) = self._get_xy_extents()
        start, end = (int(x0), int(x1)) if horizontal else (int(y0), int(y1))
        visible_points = end - start
        if not (step and step > 0):
            width, height = self._get_label_width_height(labels)
            axes_bbox = self._axes.get_window_extent(
                self._figure.canvas.get_renderer()).transformed(
                    self._figure.dpi_scale_trans.inverted())
            plot_size = (axes_bbox.width if horizontal else
                         axes_bbox.height) * self._figure.dpi
            size = (width if horizontal else height)
            if plot_size == 0 or size == 0:
                n_labels = 16
            else:
                n_labels = int(plot_size / size)
                if n_labels == 0:
                    n_labels = 16
            step = int(visible_points / n_labels) + 1
        else:
            step = int(step)
        indexes = list(range(len(labels)))
        display_labels = list(labels)
        for i in indexes:
            if i % step:
                display_labels[i] = ''
        return indexes, display_labels

    def _get_label_width_height(self, labels):
        if not self._cached_label_width_height:
            font = MatPlotLibFont.default()
            width = 0
            height = 0
            for label in labels:
                next_width, next_height = font.get_size(
                    str(label), matplotlib.rcParams['font.size'],
                    self._figure.dpi)
                width = max(width, next_width)
                height = max(height, next_height)
            self._cached_label_width_height = width, height
        return self._cached_label_width_height

    def _create_new_axes(self, nx=1, ny=1) -> LocatableAxes:
        axes = LocatableAxes(self._figure, self._divider.get_position())
        axes.set_axes_locator(self._divider.new_locator(nx=nx, ny=ny))
        self._figure.add_axes(axes)
        return axes

    @staticmethod
    def _create_secondary_xy_axes(figure,
                                  divider,
                                  nx=1,
                                  ny=1,
                                  visible=False,
                                  z_order=1):
        axes = LocatableAxes(figure, divider.get_position())
        axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny))
        axes.xaxis.tick_top()
        axes.xaxis.set_label_position('top')
        axes.yaxis.tick_right()
        axes.yaxis.set_label_position('right')
        axes.patch.set_visible(visible)
        axes.set_zorder(z_order)
        figure.add_axes(axes)
        axes.ticklabel_format(style='sci', axis='x', scilimits=(-4, 4))
        axes.ticklabel_format(style='sci', axis='y', scilimits=(-4, 4))
        return axes

    @staticmethod
    def _create_shared_axes(figure,
                            divider,
                            shared_axes,
                            nx=1,
                            ny=1,
                            visible=False,
                            z_order=1):
        axes = LocatableAxes(figure,
                             divider.get_position(),
                             sharex=shared_axes,
                             sharey=shared_axes,
                             frameon=False)
        axes.set_axes_locator(divider.new_locator(nx=nx, ny=ny))
        for spine in axes.spines.values():
            spine.set_visible(False)
        for axis in axes.axis.values():
            axis.set_visible(False)
        axes.patch.set_visible(False)
        axes.set_visible(False)
        axes.set_zorder(z_order)
        figure.add_axes(axes)
        return axes
コード例 #13
0
class PlotOptionsView(QWidget):
    def __init__(self, parent, grid_lines=True, x_limits=True, y_limits=True, secondary_x_limits=False, secondary_y_limits=False):
        QWidget.__init__(self, parent)

        edit_width = 60
        self._grid_lines = grid_lines
        self._x_limits = x_limits
        self._y_limits = y_limits
        self._secondary_y_limits = secondary_y_limits
        self._secondary_x_limits = secondary_x_limits

        self.showGridLines = False
        self.xAxisLowerLimit = self.xAxisUpperLimit = self.yAxisLowerLimit = self.yAxisUpperLimit = float('nan')
        self.secondaryXAxisLowerLimit = self.secondaryXAxisUpperLimit = self.secondaryYAxisLowerLimit = self.secondaryYAxisUpperLimit = float('nan')

        self._layout = QGridLayout(self)
        if grid_lines:
            self._show_gridlines = BindableCheckBox(None, self)
            self._layout.addWidget(QLabel(self.tr('Show Grid Lines'), self), 0, 0)
            self._layout.addWidget(self._show_gridlines, 0, 1)
            bind(self, self._show_gridlines, 'showGridLines', 'checked')
        else:
            self._show_gridlines = None

        if x_limits:
            self._x_axis_lower_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._x_axis_upper_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._x_axis_lower_limit.setFixedWidth(edit_width)
            self._x_axis_upper_limit.setFixedWidth(edit_width)
            self._layout.addWidget(QLabel(self.tr('X Axis Limits'), self), 1, 0)
            self._layout.addWidget(self._x_axis_lower_limit, 1, 1)
            self._layout.addWidget(self._x_axis_upper_limit, 1, 2)
            bind(self, self._x_axis_lower_limit, 'xAxisLowerLimit', 'value')
            bind(self, self._x_axis_upper_limit, 'xAxisUpperLimit', 'value')
        else:
            self._x_axis_lower_limit = self._x_axis_upper_limit = None

        if y_limits:
            self._y_axis_lower_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._y_axis_upper_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._y_axis_lower_limit.setFixedWidth(edit_width)
            self._y_axis_upper_limit.setFixedWidth(edit_width)
            self._layout.addWidget(QLabel(self.tr('Y Axis Limits'), self), 2, 0)
            self._layout.addWidget(self._y_axis_lower_limit, 2, 1)
            self._layout.addWidget(self._y_axis_upper_limit, 2, 2)
            bind(self, self._y_axis_lower_limit, 'yAxisLowerLimit', 'value')
            bind(self, self._y_axis_upper_limit, 'yAxisUpperLimit', 'value')
        else:
            self._y_axis_lower_limit = self._y_axis_upper_limit = None
        
        if secondary_y_limits:
            self._secondary_y_axis_lower_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._secondary_y_axis_upper_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._secondary_y_axis_lower_limit.setFixedWidth(edit_width)
            self._secondary_y_axis_upper_limit.setFixedWidth(edit_width)
            self._secondary_y_limits_label = QLabel(self.tr('2nd Y Axis Limits'), self)
            self._layout.addWidget(self._secondary_y_limits_label, 3, 0)
            self._layout.addWidget(self._secondary_y_axis_lower_limit, 3, 1)
            self._layout.addWidget(self._secondary_y_axis_upper_limit, 3, 2)
            bind(self, self._secondary_y_axis_lower_limit, 'secondaryYAxisLowerLimit', 'value')
            bind(self, self._secondary_y_axis_upper_limit, 'secondaryYAxisUpperLimit', 'value')
        else:
            self._secondary_y_axis_lower_limit = self._secondary_y_axis_upper_limit = self._secondary_y_limits_label = None

        if secondary_x_limits:
            self._secondary_x_axis_lower_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._secondary_x_axis_upper_limit = AutoFloatLineEdit(self, allow_inf=False, formatter=self._value_formatter)
            self._secondary_x_axis_lower_limit.setFixedWidth(edit_width)
            self._secondary_x_axis_upper_limit.setFixedWidth(edit_width)
            self._secondary_x_limits_label = QLabel(self.tr('2nd X Axis Limits'), self)
            self._layout.addWidget(self._secondary_x_limits_label, 4, 0)
            self._layout.addWidget(self._secondary_x_axis_lower_limit, 4, 1)
            self._layout.addWidget(self._secondary_x_axis_upper_limit, 4, 2)
            bind(self, self._secondary_x_axis_lower_limit, 'secondaryXAxisLowerLimit', 'value')
            bind(self, self._secondary_x_axis_upper_limit, 'secondaryXAxisUpperLimit', 'value')
        else:
            self._secondary_x_axis_lower_limit = self._secondary_x_axis_upper_limit = self._secondary_x_limits_label = None


    showGridLinesChanged = pyqtSignal(bool)
    xAxisLowerLimitChanged = pyqtSignal(float)
    xAxisUpperLimitChanged = pyqtSignal(float)
    xAxisLimitsChanged = pyqtSignal(float, float)
    yAxisLowerLimitChanged = pyqtSignal(float)
    yAxisUpperLimitChanged = pyqtSignal(float)
    yAxisLimitsChanged = pyqtSignal(float, float)
    secondaryXAxisLowerLimitChanged = pyqtSignal(float)
    secondaryXAxisUpperLimitChanged = pyqtSignal(float)
    secondaryXAxisLimitsChanged = pyqtSignal(float, float)
    secondaryYAxisLowerLimitChanged = pyqtSignal(float)
    secondaryYAxisUpperLimitChanged = pyqtSignal(float)
    secondaryYAxisLimitsChanged = pyqtSignal(float, float)

    showGridLines = AutoProperty(bool)
    xAxisLowerLimit = AutoProperty(float)
    xAxisUpperLimit = AutoProperty(float)
    yAxisLowerLimit = AutoProperty(float)
    yAxisUpperLimit = AutoProperty(float)
    secondaryYAxisLowerLimit = AutoProperty(float)
    secondaryYAxisUpperLimit = AutoProperty(float)
    secondaryXAxisLowerLimit = AutoProperty(float)
    secondaryXAxisUpperLimit = AutoProperty(float)

    def _value_formatter(self, value):
        if isinstance(value, str):
            return value
        if value == np.inf:
            return 'inf'
        if value == -np.inf:
            return '-inf'
        if np.isnan(value):
            return 'Auto'
        value = int(value * 100) / 100
        return '{0:.2g}'.format(value)
    
    @property
    def grid_lines(self):
        return self._grid_lines
    
    def setXLimits(self, x_lower, x_upper):
        blocked = self.blockSignals(True)
        self.xAxisLowerLimit = x_lower
        self._x_axis_lower_limit.value = x_lower
        self.xAxisUpperLimit = x_upper
        self._x_axis_upper_limit.value = x_upper
        self.blockSignals(blocked)
        self.xAxisLimitsChanged.emit(x_lower, x_upper)
    
    def setYLimits(self, y_lower, y_upper):
        blocked = self.blockSignals(True)
        self.yAxisLowerLimit = y_lower
        self._y_axis_lower_limit.value = y_lower
        self.yAxisUpperLimit = y_upper
        self._y_axis_upper_limit.value = y_upper
        self.blockSignals(blocked)
        self.yAxisLimitsChanged.emit(y_lower, y_upper)
    
    def setSecondaryXLimits(self, x_lower, x_upper):
        blocked = self.blockSignals(True)
        self.secondaryXAxisLowerLimit = x_lower
        self._secondary_x_axis_lower_limit.value = x_lower
        self.secondaryXAxisUpperLimit = x_upper
        self._secondary_x_axis_upper_limit.value = x_upper
        self.blockSignals(blocked)
        self.secondaryXAxisLimitsChanged.emit(x_lower, x_upper)
    
    def setSecondaryYLimits(self, y_lower, y_upper):
        blocked = self.blockSignals(True)
        self.secondaryYAxisLowerLimit = y_lower
        self._secondary_y_axis_lower_limit.value = y_lower
        self.secondaryYAxisUpperLimit = y_upper
        self._secondary_y_axis_upper_limit.value = y_upper
        self.blockSignals(blocked)
        self.secondaryYAxisLimitsChanged.emit(y_lower, y_upper)
    
    @property
    def x_limits(self):
        return self._x_limits
    
    @property
    def y_limits(self):
        return self._y_limits

    @property
    def secondary_y_limits(self):
        return self._secondary_y_limits

    @property
    def secondary_x_limits(self):
        return self._secondary_x_limits

    def setSecondaryYLimitsEnabled(self, enabled):
        if self._secondary_y_limits:
            self._secondary_y_axis_lower_limit.setEnabled(enabled)
            self._secondary_y_axis_upper_limit.setEnabled(enabled)
            self._secondary_y_limits_label.setEnabled(enabled)

    def setSecondaryXLimitsEnabled(self, enabled):
        if self._secondary_x_limits:
            self._secondary_x_axis_lower_limit.setEnabled(enabled)
            self._secondary_x_axis_upper_limit.setEnabled(enabled)
            self._secondary_x_limits_label.setEnabled(enabled)