Source code for qudi.util.widgets.plotting.interactive_curve
# -*- coding: utf-8 -*-
"""
ToDo
Copyright (c) 2022, the qudi developers. See the AUTHORS.md file at the top-level directory of this
distribution and on <https://github.com/Ulm-IQO/qudi-core/>
This file is part of qudi.
Qudi is free software: you can redistribute it and/or modify it under the terms of
the GNU Lesser General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with qudi.
If not, see <https://www.gnu.org/licenses/>.
"""
from PySide2 import QtCore, QtWidgets, QtGui
from typing import Optional, Mapping, Any, Dict, Tuple, Union, List, Sequence
import pyqtgraph as pg
from qudi.util.widgets.scientific_spinbox import ScienDSpinBox
from qudi.util.widgets.separator_lines import VerticalLine
from qudi.util.widgets.plotting.axis import label_nudged_plot_widget
from qudi.util.widgets.plotting.plot_widget import RubberbandZoomSelectionPlotWidget
from qudi.util.units import ScaledFloat
PlotWidget = label_nudged_plot_widget(RubberbandZoomSelectionPlotWidget)
[docs]
class PlotEditorWidget(QtWidgets.QWidget):
"""
"""
sigAutoRangeClicked = QtCore.Signal(bool, bool) # x- and/or y-axis
sigLabelsChanged = QtCore.Signal(object, object)
sigUnitsChanged = QtCore.Signal(object, object)
sigLimitsChanged = QtCore.Signal(object, object)
[docs]
def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None:
super().__init__(parent=parent)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
# Generate labels
x_label = QtWidgets.QLabel('Horizontal Axis:')
x_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
y_label = QtWidgets.QLabel('Vertical Axis:')
y_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
label_label = QtWidgets.QLabel('Label')
label_label.setAlignment(QtCore.Qt.AlignCenter)
unit_label = QtWidgets.QLabel('Units')
unit_label.setAlignment(QtCore.Qt.AlignCenter)
range_label = QtWidgets.QLabel('Range')
range_label.setAlignment(QtCore.Qt.AlignCenter)
# Generate editors
self.x_label_lineEdit = QtWidgets.QLineEdit()
self.x_label_lineEdit.setMinimumWidth(50)
self.x_unit_lineEdit = QtWidgets.QLineEdit()
self.x_unit_lineEdit.setMinimumWidth(50)
self.x_lower_limit_spinBox = ScienDSpinBox()
self.x_lower_limit_spinBox.setMinimumWidth(70)
self.x_upper_limit_spinBox = ScienDSpinBox()
self.x_upper_limit_spinBox.setMinimumWidth(70)
self.x_auto_button = QtWidgets.QPushButton('Auto Range')
self.y_label_lineEdit = QtWidgets.QLineEdit()
self.y_label_lineEdit.setMinimumWidth(50)
self.y_unit_lineEdit = QtWidgets.QLineEdit()
self.y_unit_lineEdit.setMinimumWidth(50)
self.y_lower_limit_spinBox = ScienDSpinBox()
self.y_lower_limit_spinBox.setMinimumWidth(70)
self.y_upper_limit_spinBox = ScienDSpinBox()
self.y_upper_limit_spinBox.setMinimumWidth(70)
self.y_auto_button = QtWidgets.QPushButton('Auto Range')
row = 0
layout.addWidget(label_label, row, 1)
layout.addWidget(unit_label, row, 2)
layout.addWidget(range_label, row, 4, 1, 2)
row += 1
layout.addWidget(x_label, row, 0)
layout.addWidget(self.x_label_lineEdit, row, 1)
layout.addWidget(self.x_unit_lineEdit, row, 2)
layout.addWidget(self.x_lower_limit_spinBox, row, 4)
layout.addWidget(self.x_upper_limit_spinBox, row, 5)
layout.addWidget(self.x_auto_button, row, 6)
row += 1
layout.addWidget(y_label, row, 0)
layout.addWidget(self.y_label_lineEdit, row, 1)
layout.addWidget(self.y_unit_lineEdit, row, 2)
layout.addWidget(self.y_lower_limit_spinBox, row, 4)
layout.addWidget(self.y_upper_limit_spinBox, row, 5)
layout.addWidget(self.y_auto_button, row, 6)
row += 1
layout.addWidget(VerticalLine(), 0, 3, row, 1)
layout.setColumnStretch(1, 1)
layout.setColumnStretch(2, 1)
layout.setColumnStretch(4, 3)
layout.setColumnStretch(5, 3)
self.x_label_lineEdit.editingFinished.connect(self.__x_label_changed)
self.y_label_lineEdit.editingFinished.connect(self.__y_label_changed)
self.x_unit_lineEdit.editingFinished.connect(self.__x_unit_changed)
self.y_unit_lineEdit.editingFinished.connect(self.__y_unit_changed)
self.x_lower_limit_spinBox.editingFinished.connect(self.__x_limits_changed)
self.x_upper_limit_spinBox.editingFinished.connect(self.__x_limits_changed)
self.y_lower_limit_spinBox.editingFinished.connect(self.__y_limits_changed)
self.y_upper_limit_spinBox.editingFinished.connect(self.__y_limits_changed)
self.x_auto_button.clicked.connect(
lambda: self.sigAutoRangeClicked.emit(True, False)
)
self.y_auto_button.clicked.connect(
lambda: self.sigAutoRangeClicked.emit(False, True)
)
self.set_limits((-0.5, 0.5), (-0.5, 0.5))
self.set_units('arb.u.', 'arb.u.')
self.set_labels('X', 'Y')
@property
def labels(self) -> Tuple[str, str]:
return self.x_label_lineEdit.text(), self.y_label_lineEdit.text()
@property
def units(self) -> Tuple[str, str]:
return self.x_unit_lineEdit.text(), self.y_unit_lineEdit.text()
@property
def limits(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
x_min, x_max = sorted([self.x_lower_limit_spinBox.value(),
self.x_upper_limit_spinBox.value()])
y_min, y_max = sorted([self.y_lower_limit_spinBox.value(),
self.y_upper_limit_spinBox.value()])
return (x_min, x_max), (y_min, y_max)
def set_labels(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
if x is not None:
self.x_label_lineEdit.setText(x)
if y is not None:
self.y_label_lineEdit.setText(y)
def set_units(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
if x is not None:
self.x_unit_lineEdit.setText(x)
if y is not None:
self.y_unit_lineEdit.setText(y)
def set_limits(self,
x: Optional[Tuple[float, float]] = None,
y: Optional[Tuple[float, float]] = None
) -> None:
if x is not None:
lower, upper = sorted(x)
self.x_lower_limit_spinBox.setValue(lower)
self.x_upper_limit_spinBox.setValue(upper)
if y is not None:
lower, upper = sorted(y)
self.y_lower_limit_spinBox.setValue(lower)
self.y_upper_limit_spinBox.setValue(upper)
def __x_limits_changed(self) -> None:
lower = self.x_lower_limit_spinBox.value()
upper = self.x_upper_limit_spinBox.value()
if upper < lower:
lower, upper = upper, lower
self.x_lower_limit_spinBox.setValue(lower)
self.x_upper_limit_spinBox.setValue(upper)
self.__swap_limits_focus()
self.sigLimitsChanged.emit((lower, upper), None)
def __y_limits_changed(self) -> None:
lower = self.y_lower_limit_spinBox.value()
upper = self.y_upper_limit_spinBox.value()
if upper < lower:
lower, upper = upper, lower
self.y_lower_limit_spinBox.setValue(lower)
self.y_upper_limit_spinBox.setValue(upper)
self.__swap_limits_focus()
self.sigLimitsChanged.emit(None, self.limits[1])
def __x_label_changed(self) -> None:
self.sigLabelsChanged.emit(self.labels[0], None)
def __y_label_changed(self) -> None:
self.sigLabelsChanged.emit(None, self.labels[1])
def __x_unit_changed(self) -> None:
self.sigUnitsChanged.emit(self.units[0], None)
def __y_unit_changed(self) -> None:
self.sigUnitsChanged.emit(None, self.units[1])
def __swap_limits_focus(self) -> None:
if self.x_lower_limit_spinBox.hasFocus():
self.x_upper_limit_spinBox.setFocus()
elif self.x_upper_limit_spinBox.hasFocus():
self.x_lower_limit_spinBox.setFocus()
elif self.y_lower_limit_spinBox.hasFocus():
self.y_upper_limit_spinBox.setFocus()
elif self.y_upper_limit_spinBox.hasFocus():
self.y_lower_limit_spinBox.setFocus()
[docs]
class PlotLegendIconWidget(QtWidgets.QWidget):
[docs]
def __init__(self, item, parent: Optional[QtWidgets.QWidget] = None) -> None:
super().__init__(parent=parent)
self.setMouseTracking(False)
self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
self.setFixedSize(20, 20)
self._item = item
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
p = QtGui.QPainter(self)
opts = self._item.opts
if opts.get('antialias'):
p.setRenderHint(p.RenderHint.Antialiasing)
if not isinstance(self._item, pg.ScatterPlotItem):
p.setPen(pg.mkPen(opts['pen']))
p.drawLine(0, 11, 20, 11)
if (opts.get('fillLevel', None) is not None and
opts.get('fillBrush', None) is not None):
p.setBrush(pg.mkBrush(opts['fillBrush']))
p.setPen(pg.mkPen(opts['pen']))
p.drawPolygon(QtGui.QPolygonF(
[QtCore.QPointF(2, 18), QtCore.QPointF(18, 2),
QtCore.QPointF(18, 18)]))
symbol = opts.get('symbol', None)
if symbol is not None:
if isinstance(self._item, pg.PlotDataItem):
opts = self._item.scatter.opts
p.translate(10, 10)
pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, opts['size'], pg.mkPen(opts['pen']), pg.mkBrush(opts['brush']))
if isinstance(self._item, pg.BarGraphItem):
p.setBrush(pg.mkBrush(opts['brush']))
p.drawRect(QtCore.QRectF(2, 2, 18, 18))
[docs]
class PlotSelectorWidget(QtWidgets.QWidget):
"""
"""
sigSelectionChanged = QtCore.Signal(dict) # selection
[docs]
def __init__(self, parent: Optional[QtWidgets.QWidget] = None) -> None:
super().__init__(parent=parent)
self._stretch = QtWidgets.QSpacerItem(
0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
)
self._selector_layout = QtWidgets.QGridLayout()
self._selector_layout.addItem(self._stretch, 0, 0, 1, 2)
self._selector_layout.setColumnStretch(0, 1)
self.setLayout(self._selector_layout)
self._selectors = dict()
@property
def selection(self) -> Dict[str, bool]:
return {name: selector.isChecked() for name, (_, selector) in self._selectors.items()}
def set_selection(self, selection: Mapping[str, bool]) -> None:
for name, select in selection.items():
try:
self._selectors[name][1].setChecked(select)
except KeyError:
pass
def add_selector(self,
name: str,
item: Optional[pg.PlotDataItem] = None,
selected: Optional[bool] = False) -> None:
if name in self._selectors:
raise ValueError(f'Selector with name "{name}" already present in plot selector')
selector = self._create_selector(name)
selector.setChecked(selected)
selector.clicked.connect(self._selection_changed)
self._selector_layout.removeItem(self._stretch)
row = len(self._selectors)
if item is None:
self._selector_layout.addWidget(selector, row, 0, 1, 2)
self._selectors[name] = (None, selector)
else:
icon = PlotLegendIconWidget(item)
self._selector_layout.addWidget(icon, row, 0)
self._selector_layout.addWidget(selector, row, 1)
self._selectors[name] = (icon, selector)
self._selector_layout.addItem(self._stretch, row + 1, 0, 1, 2)
def remove_selector(self, name: str) -> None:
if name not in self._selectors:
raise ValueError(f'Selector with name "{name}" not found in plot selector')
self._selector_layout.removeItem(self._stretch)
for sel_name, (icon, selector) in reversed(self._selectors.items()):
self._selector_layout.removeWidget(selector)
if icon is not None:
self._selector_layout.removeWidget(icon)
if sel_name == name:
break
after_remove = False
for row, (sel_name, (icon, selector)) in enumerate(self._selectors.items()):
if after_remove:
if icon is None:
self._selector_layout.addWidget(selector, row - 1, 0, 1, 2)
else:
self._selector_layout.addWidget(icon, row - 1, 0)
self._selector_layout.addWidget(selector, row - 1, 1)
elif sel_name == name:
after_remove = True
icon, selector = self._selectors.pop(name)
selector.clicked.disconnect()
selector.setParent(None)
icon.setParent(None)
self._selector_layout.addItem(self._stretch, len(self._selectors), 0, 1, 2)
def _selection_changed(self) -> None:
self.sigSelectionChanged.emit(self.selection)
@staticmethod
def _create_selector(name: str, color: Optional[Any] = None) -> QtWidgets.QCheckBox:
checkbox = QtWidgets.QCheckBox(name)
if color is not None:
color_str = pg.mkColor(color).name()
checkbox.setStyleSheet('QCheckBox { color: ' + color_str + ' }')
return checkbox
[docs]
class CursorPositionLabel(QtWidgets.QLabel):
"""
"""
[docs]
def __init__(self,
units: Optional[Tuple[str, str]] = None,
parent: Optional[QtWidgets.QWidget] = None
) -> None:
super().__init__(parent=parent)
self._units = ('', '')
self._text_template = ''
self._pos_cache = (0, 0)
if units is None:
units = self._units
self.set_units(*units)
def set_units(self, x: str, y: str) -> None:
units = (x if x else '', y if y else '')
self._update_text_template(units)
self._units = units
self.update_position(self._pos_cache)
def update_position(self, pos: Tuple[float, float]) -> None:
x = ScaledFloat(pos[0])
y = ScaledFloat(pos[1])
self.setText(self._text_template.format(x, y))
self._pos_cache = pos
def _update_text_template(self, units: Tuple[str, str]) -> None:
x_unit, y_unit = units
self._text_template = f'Cursor: ({{:.3r}}{x_unit}, {{:.3r}}{y_unit})'
[docs]
class InteractiveCurvesWidget(QtWidgets.QWidget):
"""
"""
SelectionMode = RubberbandZoomSelectionPlotWidget.SelectionMode
sigPlotParametersChanged = QtCore.Signal()
sigAutoLimitsApplied = QtCore.Signal(bool, bool) # in x- and/or y-direction
[docs]
def __init__(self,
allow_tracking_outside_data: Optional[bool] = False,
max_mouse_pos_update_rate: Optional[float] = None,
selection_bounds: Optional[Sequence[Tuple[Union[None, float], Union[None, float]]]] = None,
selection_pen: Optional[Any] = None,
selection_hover_pen: Optional[Any] = None,
selection_brush: Optional[Any] = None,
selection_hover_brush: Optional[Any] = None,
xy_region_selection_crosshair: Optional[bool] = False,
xy_region_selection_handles: Optional[bool] = True,
**kwargs
) -> None:
super().__init__(**kwargs)
if max_mouse_pos_update_rate is None:
max_mouse_pos_update_rate = 20.
self._plot_widget = PlotWidget(
allow_tracking_outside_data=allow_tracking_outside_data,
max_mouse_pos_update_rate=max_mouse_pos_update_rate,
selection_bounds=selection_bounds,
selection_pen=selection_pen,
selection_hover_pen=selection_hover_pen,
selection_brush=selection_brush,
selection_hover_brush=selection_hover_brush,
xy_region_selection_crosshair=xy_region_selection_crosshair,
xy_region_selection_handles=xy_region_selection_handles,
)
self._plot_legend = self._plot_widget.addLegend()
self._plot_legend.hide()
self._plot_editor = PlotEditorWidget()
self._plot_selector = PlotSelectorWidget()
self._position_label = CursorPositionLabel(units=self._plot_editor.units)
self._plot_editor.layout().setContentsMargins(0, 0, 0, 0)
self._plot_selector.layout().setContentsMargins(0, 0, 0, 0)
layout = QtWidgets.QGridLayout()
layout.addWidget(self._position_label, 0, 0)
layout.addWidget(self._plot_widget, 1, 0)
layout.addWidget(self._plot_editor, 2, 0, 1, 2)
layout.addWidget(self._plot_selector, 1, 1)
layout.setColumnStretch(0, 1)
layout.setRowStretch(1, 1)
self.setLayout(layout)
self._plot_selector.sigSelectionChanged.connect(self._update_plot_selection)
self._plot_editor.sigUnitsChanged.connect(self.__units_changed)
self._plot_editor.sigLabelsChanged.connect(self.__labels_changed)
self._plot_editor.sigLimitsChanged.connect(self.__limits_changed)
self._plot_editor.sigAutoRangeClicked.connect(self.set_auto_range)
self._plot_widget.sigRangeChanged.connect(self.__plot_widget_limits_changed)
self._plot_widget.sigMouseMoved.connect(self._position_label.update_position)
self.__labels_changed(*self.labels)
self.__units_changed(*self.units)
self.set_auto_range(True, True)
# patch attributes of advanced PlotWidget into this widget for easier access and
# auto-completion
self.set_rubberband_zoom_selection_mode = self._plot_widget.set_rubberband_zoom_selection_mode
self.set_region_selection_mode = self._plot_widget.set_region_selection_mode
self.set_marker_selection_mode = self._plot_widget.set_marker_selection_mode
self.set_selection_mutable = self._plot_widget.set_selection_mutable
self.set_selection_bounds = self._plot_widget.set_selection_bounds
self.add_region_selection = self._plot_widget.add_region_selection
self.add_marker_selection = self._plot_widget.add_marker_selection
self.move_region_selection = self._plot_widget.move_region_selection
self.move_marker_selection = self._plot_widget.move_marker_selection
self.clear_marker_selections = self._plot_widget.clear_marker_selections
self.delete_marker_selection = self._plot_widget.delete_marker_selection
self.hide_marker_selections = self._plot_widget.hide_marker_selections
self.show_marker_selections = self._plot_widget.show_marker_selections
self.hide_marker_selection = self._plot_widget.hide_marker_selection
self.show_marker_selection = self._plot_widget.show_marker_selection
self.clear_region_selections = self._plot_widget.clear_region_selections
self.delete_region_selection = self._plot_widget.delete_region_selection
self.hide_region_selections = self._plot_widget.hide_region_selections
self.show_region_selections = self._plot_widget.show_region_selections
self.hide_region_selection = self._plot_widget.hide_region_selection
self.show_region_selection = self._plot_widget.show_region_selection
# Disable bugged pyqtgraph interactive mouse menu options to avoid a myriad of
# user-induced errors.
for action in self._plot_widget.getPlotItem().ctrlMenu.actions():
if action.text() not in ('Alpha', 'Grid', 'Points'):
action.setEnabled(False)
action.setVisible(False)
for axis_ctrl in self._plot_widget.getViewBox().menu.ctrl:
axis_ctrl.autoPanCheck.setEnabled(False)
axis_ctrl.visibleOnlyCheck.setEnabled(False)
axis_ctrl.linkCombo.setEnabled(False)
axis_ctrl.label.setEnabled(False)
axis_ctrl.autoPanCheck.setVisible(False)
axis_ctrl.visibleOnlyCheck.setVisible(False)
axis_ctrl.linkCombo.setVisible(False)
axis_ctrl.label.setVisible(False)
# Keep track of PlotItems plotted
self._plot_items = dict()
self._fit_plot_items = dict()
def _get_valid_generic_name(self, index: Optional[int] = 1) -> str:
name = f'Dataset {index:d}'
if name in self._plot_items:
return self._get_valid_generic_name(index + 1)
return name
def plot(self, name: Optional[str] = None, **kwargs) -> str:
# Delete old plot if present
if name is None:
name = self._get_valid_generic_name()
elif name in self._plot_items:
self.remove_plot(name)
# Add new plot and enable antialias by default if not explicitly set
antialias = kwargs.pop('antialias', True)
item = self._plot_widget.plot(name=name, antialias=antialias, **kwargs)
self._plot_items[name] = item
self._plot_selector.add_selector(name=name, item=item, selected=True)
return name
def remove_plot(self, name: str) -> None:
self.remove_fit_plot(name)
item = self._plot_items.pop(name, None)
if item in self._plot_widget.getViewBox().addedItems:
self._plot_widget.removeItem(item)
try:
self._plot_selector.remove_selector(name)
except ValueError:
pass
def clear(self) -> None:
for name in list(self._plot_items):
self.remove_plot(name)
def clear_fits(self) -> None:
for name in list(self._fit_plot_items):
self.remove_fit_plot(name)
def plot_fit(self, name: str, **kwargs) -> None:
if name not in self._plot_items:
raise ValueError(f'No plot with name "{name}" found to add fit to')
# Delete old plot if present
if name in self._fit_plot_items:
self.remove_fit_plot(name)
# Add new plot and enable antialias by default if not explicitly set
antialias = kwargs.pop('antialias', True)
item = self._plot_widget.plot(name=None, antialias=antialias, **kwargs)
self._fit_plot_items[name] = item
def remove_fit_plot(self, name: str) -> None:
item = self._fit_plot_items.pop(name, None)
if item in self._plot_widget.getViewBox().addedItems:
self._plot_widget.removeItem(item)
def set_data(self, name: str, *args, **kwargs) -> None:
""" See pyqtgraph.PlotDataItem.__init__ for valid arguments """
self._plot_items[name].setData(*args, **kwargs)
def set_fit_data(self, name: str, *args, **kwargs) -> None:
""" See pyqtgraph.PlotDataItem.__init__ for valid arguments """
if name not in self._fit_plot_items:
self.plot_fit(name)
self._fit_plot_items[name].setData(*args, **kwargs)
@property
def plot_names(self) -> List[str]:
return list(self._plot_items)
@property
def plot_selection(self) -> Dict[str, bool]:
return {name: item.isVisible() for name, item in self._plot_items.items()}
def set_plot_selection(self, selection: Mapping[str, bool]) -> None:
self._plot_selector.set_selection(selection)
self._update_plot_selection(selection)
def set_auto_range(self, x: Optional[bool] = None, y: Optional[bool] = None) -> None:
if x is y is None:
return
if x is not None:
self._plot_widget.enableAutoRange(axis='x', enable=x)
if y is not None:
self._plot_widget.enableAutoRange(axis='y', enable=y)
self.sigAutoLimitsApplied.emit(bool(x), bool(y))
def set_labels(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
self._plot_editor.set_labels(x, y)
self.__labels_changed(*self.labels)
def set_units(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
self._plot_editor.set_units(x, y)
self.__units_changed(*self.units)
def set_limits(self,
x: Optional[Tuple[float, float]] = None,
y: Optional[Tuple[float, float]] = None
) -> None:
self._plot_editor.set_limits(x, y)
self.__limits_changed(*self.limits)
# Start of attribute/property wrapping of sub-widgets
@property
def sigMarkerSelectionChanged(self) -> QtCore.Signal:
return self._plot_widget.sigMarkerSelectionChanged
@property
def sigRegionSelectionChanged(self) -> QtCore.Signal:
return self._plot_widget.sigRegionSelectionChanged
@property
def sigMouseMoved(self) -> QtCore.Signal:
return self._plot_widget.sigMouseMoved
@property
def sigMouseDragged(self) -> QtCore.Signal:
return self._plot_widget.sigMouseDragged
@property
def sigMouseClicked(self) -> QtCore.Signal:
return self._plot_widget.sigMouseClicked
@property
def sigZoomAreaApplied(self) -> QtCore.Signal:
return self._plot_widget.sigZoomAreaApplied
@property
def labels(self) -> Tuple[str, str]:
return self._plot_editor.labels
@property
def units(self) -> Tuple[str, str]:
return self._plot_editor.units
@property
def limits(self) -> Tuple[Tuple[float, float], Tuple[float, float]]:
return self._plot_editor.limits
@property
def rubberband_zoom_selection_mode(self) -> SelectionMode:
return self._plot_widget.rubberband_zoom_selection_mode
@property
def marker_selection(self) -> Dict[SelectionMode, List[Union[float, Tuple[float, float]]]]:
return self._plot_widget.marker_selection
@property
def region_selection(self) -> Dict[SelectionMode, List[tuple]]:
return self._plot_widget.region_selection
@property
def region_selection_mode(self) -> SelectionMode:
return self._plot_widget.region_selection_mode
@property
def marker_selection_mode(self) -> SelectionMode:
return self._plot_widget.marker_selection_mode
@property
def selection_mutable(self) -> bool:
return self._plot_widget.selection_mutable
@property
def selection_bounds(self) -> Union[None, List[Union[None, Tuple[float, float]]]]:
return self._plot_widget.selection_bounds
@property
def allow_tracking_outside_data(self) -> bool:
return self._plot_widget.allow_tracking_outside_data
@allow_tracking_outside_data.setter
def allow_tracking_outside_data(self, allow: bool) -> None:
self._plot_widget.allow_tracking_outside_data = bool(allow)
# Start of methods to show/hide sub-widgets
def toggle_plot_selector(self, enable: bool) -> None:
# Sync legend and selector checkboxes
if not self._plot_selector.isVisible() and enable:
self._plot_selector.set_selection(self.plot_selection)
self._plot_selector.setVisible(enable)
self._plot_legend.setVisible(not enable)
def toggle_plot_editor(self, enable: bool) -> None:
self._plot_editor.setVisible(enable)
def toggle_cursor_position(self, enable: bool) -> None:
is_enabled = self._position_label.isVisible()
self._position_label.setVisible(enable)
if is_enabled and not enable:
self._plot_widget.sigMouseMoved.disconnect(self._position_label.update_position)
elif not is_enabled and enable:
self._plot_widget.sigMouseMoved.connect(self._position_label.update_position)
# Start of slots for internal updates
def _update_plot_selection(self, selection: Mapping[str, bool]) -> None:
for name, selected in selection.items():
try:
self._plot_items[name].setVisible(selected)
except KeyError:
pass
else:
try:
self._fit_plot_items[name].setVisible(selected)
except KeyError:
pass
def __units_changed(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
if x is y is None:
return
x_label, y_label = self.labels
if x is not None:
self._plot_widget.setLabel('bottom', x_label, units=x)
if y is not None:
self._plot_widget.setLabel('left', y_label, units=y)
self._position_label.set_units(*self.units)
self.sigPlotParametersChanged.emit()
def __labels_changed(self, x: Optional[str] = None, y: Optional[str] = None) -> None:
if x is y is None:
return
x_unit, y_unit = self.units
if x is not None:
self._plot_widget.setLabel('bottom', x, units=x_unit)
if y is not None:
self._plot_widget.setLabel('left', y, units=y_unit)
self.sigPlotParametersChanged.emit()
def __limits_changed(self,
x: Optional[Tuple[float, float]] = None,
y: Optional[Tuple[float, float]] = None
) -> None:
if x is y is None:
return
if x is not None:
self._plot_widget.enableAutoRange(axis='x', enable=False)
self._plot_widget.setXRange(*x, padding=0)
if y is not None:
self._plot_widget.enableAutoRange(axis='y', enable=False)
self._plot_widget.setYRange(*y, padding=0)
# Signal is emitted once the pyqtgraph plot has actually changed.
# See: self.__plot_widget_limits_changed
def __plot_widget_limits_changed(self,
_,
limits: Tuple[Tuple[float, float], Tuple[float, float]]
) -> None:
self._plot_editor.set_limits(*limits)
self.sigPlotParametersChanged.emit()