class ChecklistWindow(QWidget): def __init__(self, aircraft_id): super(ChecklistWindow, self).__init__() # Set properties of the window self.setWindowTitle("BTO and BPO Checklist") self.resize(500, 700) self.move(200,100) self.checklist_state = 0 # Relative path for the default BPO and BTO checklist BPO_checklist_file = os.path.join(rospkg.RosPack().get_path('yonah_rqt'), 'src/yonah_rqt', 'BPO_checklist.csv') BTO_checklist_file = os.path.join(rospkg.RosPack().get_path('yonah_rqt'), 'src/yonah_rqt', 'BTO_checklist.csv') # Check whether checklist is present, if not print a error message to terminal try: # If checklist is present, parse it and pass it to its respective variable self.BPO_checklist = self.excel_parser(BPO_checklist_file) self.BTO_checklist = self.excel_parser(BTO_checklist_file) except: rospy.logerr("Checklist files are missing or named wrongly. Please follow the original directory and naming") exit() # Create the layout self.main_layout = QVBoxLayout() self.buttons_layout = QHBoxLayout() self.tree_widget_layout = QHBoxLayout() # Create the widgets self.create_widget() self.has_message_opened = 0 # Add the widgets into the layouts self.main_layout.addLayout(self.tree_widget_layout) self.main_layout.addLayout(self.buttons_layout) self.setLayout(self.main_layout) # Create the main layout of widget def create_widget(self): # Create tree structure self.create_tree() # Declare buttons and connect each of them to a function self.load_button = QPushButton('Load') self.ok_button = QPushButton('OK') self.cancel_button = QPushButton('Cancel') self.load_button.pressed.connect(self.load_clicked) self.ok_button.pressed.connect(self.ok_clicked) self.cancel_button.pressed.connect(self.cancel_clicked) # Add buttons into the layout self.buttons_layout.addWidget(self.load_button) self.buttons_layout.addWidget(self.cancel_button) self.buttons_layout.addWidget(self.ok_button) self.tree_widget_layout.addWidget(self.tree_widget) # Create the tree layout of widget inside of the main layout def create_tree(self): # Set up the main tree widget self.tree_widget = QTreeWidget() self.tree_widget.setColumnCount(2) self.tree_widget.setColumnWidth(0, 250) self.tree_widget.setHeaderLabels(['Parts', 'Status']) self.item = QTreeWidgetItem() # Create the BPO section self.BPO_header = QTreeWidgetItem(self.tree_widget) self.BPO_header.setFlags(self.BPO_header.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable) self.BPO_header.setText(0, 'BPO Checklist') self.BPO_header.setExpanded(True) self.create_item(self.BPO_header, self.BPO_checklist) # Adds the list of items into the section # Create the BTO section self.BTO_header = QTreeWidgetItem(self.tree_widget) self.BTO_header.setFlags(self.BTO_header.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable) self.BTO_header.setText(0, 'BTO Checklist') self.BTO_header.setExpanded(True) self.create_item(self.BTO_header, self.BTO_checklist) # Adds the list of items into the section # Populate the tree layout with items def create_item(self, parent, list): section_header = [] # List of the section headers for i in range (len(list)): if (list[i][1] == '' and list[i][0] != ''): section_header.append(list[i][0]) k = 0 # Iterate through the different sections for j in range (len(section_header)): # Child refers to the sections (mechanical, avionics, etc) child = QTreeWidgetItem(parent) child.setFlags(child.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable) child.setText(0, section_header[j]) while k < len(list): if list[k][0] in section_header: # When the while loop encounters the first title, continue the loop if (list[k][0] == section_header[0]): k += 1 continue # When the while loop encounters the next titles, break the loop so that the value of j increases k += 1 break # when the list contains empty cells, skip the cell and continue elif list[k][0] == '': k += 1 continue # Add the list items to the treewidgetitem # Grandchild refers to the items under each section (wing nuts, tail nuts, etc) grandchild = QTreeWidgetItem(child) grandchild.setText(0, list[k][0]) grandchild.setText(1, list[k][1]) grandchild.setCheckState(0, Qt.Unchecked) # Set all checkbox to its unchecked state k += 1 # Read the excel sheet and parse it as an array def excel_parser(self, file_name): with open(file_name, 'r') as file: checklist = [] reader = csv.reader(file) for row in reader: checklist.append(row) return checklist # Determines what happens when load button is clicked def load_clicked(self): # Use QFileDialog to open the system's file browser filenames = QFileDialog.getOpenFileNames( self, self.tr('Load from Files'), '.', self.tr('csv files {.csv} (*.csv)')) # Iterate through the file names selected for filename in filenames[0]: # If the file names has the word BPO or BTO in it, remove current widget, add the loaded one if (filename.find('BPO') != -1): self.BPO_checklist = self.excel_parser(filename) self.remove_widget() self.create_widget() elif (filename.find('BTO') != -1): self.BTO_checklist = self.excel_parser(filename) self.remove_widget() self.create_widget() else: rospy.logerr('rqt: Checklist name must contain BPO or BTO') self.close() # Close all the main_layout def remove_widget(self): self.main_layout.removeWidget(self.tree_widget) self.tree_widget.deleteLater() self.buttons_layout.removeWidget(self.ok_button) self.buttons_layout.removeWidget(self.cancel_button) self.buttons_layout.removeWidget(self.load_button) self.ok_button.deleteLater() self.cancel_button.deleteLater() self.load_button.deleteLater() # Declare what will happen when ok button is clicked def ok_clicked(self): if self.BPO_header.checkState(0) != 2 or self.BTO_header.checkState(0) != 2: # User clicks ok without checking all self.dialog_window("Some items in the checklist are still unchecked", "Do you still want to continue?", True) else: self.checklist_state = 1 self.close() # Declare what will happen when cancel button is clicked def cancel_clicked(self): if self.BPO_header.checkState(0) != 0 or self.BTO_header.checkState(0) != 0: # User clicks cancel with some boxes checked self.dialog_window('Some of your items are checked. Cancelling will uncheck all your items', 'Do you still want to continue?', False) else: self.BTO_header.setCheckState(0, Qt.Unchecked) self.BPO_header.setCheckState(0, Qt.Unchecked) self.close() # Create a pop up window when user pre-emptively cancelled or clicked ok without completing the checklist def dialog_window(self, message, detail, check): self.message = QMessageBox() self.has_message_opened = 1 self.message.setIcon(QMessageBox.Warning) self.message.setText(message) self.message.setInformativeText(detail) self.message.setWindowTitle("Items are unchecked") self.message.setStandardButtons(QMessageBox.Yes | QMessageBox.No) self.message.show() if check == True: # Check == True means it is from ok_clicked self.message.buttonClicked.connect(self.message_action) else: self.message.buttonClicked.connect(self.message_action_uncheck) # Determines what happens after dialog_window pops up from ok_button def message_action(self, i): if i.text() == '&Yes': self.checklist_state = 1 self.close() else: self.message.close() # Determines what happens after dialog_window pops up from cancel_button def message_action_uncheck(self, i): self.response = i.text() if self.response == '&Yes': self.checklist_state = 1 self.BTO_header.setCheckState(0, Qt.Unchecked) self.BPO_header.setCheckState(0, Qt.Unchecked) self.close() else: self.message.close() # Shutdown function def shutdown(self): self.close() if self.has_message_opened == 1: self.message.close()
class DataPlot(QWidget): """A widget for displaying a plot of data The DataPlot widget displays a plot, on one of several plotting backends, depending on which backend(s) are available at runtime. It currently supports PyQtGraph, MatPlot and QwtPlot backends. The DataPlot widget manages the plot backend internally, and can save and restore the internal state using `save_settings` and `restore_settings` functions. Currently, the user MUST call `restore_settings` before using the widget, to cause the creation of the enclosed plotting widget. """ # plot types in order of priority plot_types = [ { 'title': 'PyQtGraph', 'widget_class': PyQtGraphDataPlot, 'description': 'Based on PyQtGraph\n- installer: http://luke.campagnola.me/code/pyqtgraph\n', 'enabled': PyQtGraphDataPlot is not None, }, { 'title': 'MatPlot', 'widget_class': MatDataPlot, 'description': 'Based on MatPlotLib\n- needs most CPU\n- needs matplotlib >= 1.1.0\n- if using ' 'PySide: PySide > 1.1.0\n', 'enabled': MatDataPlot is not None, }, { 'title': 'QwtPlot', 'widget_class': QwtDataPlot, 'description': 'Based on QwtPlot\n- does not use timestamps\n- uses least CPU\n- needs Python ' 'Qwt bindings\n', 'enabled': QwtDataPlot is not None, }, ] # pre-defined colors: RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) SCALE_ALL = 1 SCALE_VISIBLE = 2 SCALE_EXTEND = 4 _colors = [ Qt.blue, Qt.red, Qt.cyan, Qt.magenta, Qt.green, Qt.darkYellow, Qt.black, Qt.darkCyan, Qt.darkRed, Qt.gray ] limits_changed = Signal() _redraw = Signal() _add_curve = Signal(str, str, 'QColor', bool, bool) def __init__(self, parent=None): """Create a new, empty DataPlot This will raise a RuntimeError if none of the supported plotting backends can be found """ super(DataPlot, self).__init__(parent) self.x_width = 5.0 self._plot_index = 0 self._color_index = 0 self._markers_on = False self._autoscroll = True self._autoscale_x = False self._autoscale_y = DataPlot.SCALE_ALL # the backend widget that we're trying to hide/abstract self._data_plot_widget = None self._curves = {} self._vline = None self._redraw.connect(self._do_redraw) self._layout = QHBoxLayout() self.setLayout(self._layout) enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']] if not enabled_plot_types: if qVersion().startswith('4.'): version_info = '1.1.0' else: # minimum matplotlib version for Qt 5 version_info = '1.4.0' if QT_BINDING == 'pyside': version_info += ' and PySide %s' % \ ('> 1.1.0' if qVersion().startswith('4.') else '>= 2.0.0') raise RuntimeError( 'No usable plot type found. Install at least one of: PyQtGraph, MatPlotLib ' '(at least %s) or Python-Qwt5.' % version_info) self._switch_data_plot_widget(self._plot_index) self.show() def set_x_width(self, width): self.x_width = width def _switch_data_plot_widget(self, plot_index, markers_on=False): """Internal method for activating a plotting backend by index""" # check if selected plot type is available if not self.plot_types[plot_index]['enabled']: # find other available plot type for index, plot_type in enumerate(self.plot_types): if plot_type['enabled']: plot_index = index break self._plot_index = plot_index self._markers_on = markers_on selected_plot = self.plot_types[plot_index] print("Selected plot: {}".format(selected_plot['title'])) if self._data_plot_widget: x_limits = self.get_xlim() y_limits = self.get_ylim() self._layout.removeWidget(self._data_plot_widget) self._data_plot_widget.close() self._data_plot_widget = None else: x_limits = [0.0, 10.0] y_limits = [-0.001, 0.001] self._data_plot_widget = selected_plot['widget_class'](self) self._data_plot_widget.limits_changed.connect(self.limits_changed) self._add_curve.connect(self._data_plot_widget.add_curve) self._layout.addWidget(self._data_plot_widget) # restore old data for curve_id in self._curves: curve = self._curves[curve_id] self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on) if self._vline: self.vline(*self._vline) self.set_xlim(x_limits) self.set_ylim(y_limits) self.redraw() def _switch_plot_markers(self, markers_on): self._markers_on = markers_on self._data_plot_widget._color_index = 0 for curve_id in self._curves: self._data_plot_widget.remove_curve(curve_id) curve = self._curves[curve_id] self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on, curve['dashed']) self.redraw() # interface out to the managing GUI component: get title, save, restore, # etc def getTitle(self): """get the title of the current plotting backend""" return self.plot_types[self._plot_index]['title'] def save_settings(self, plugin_settings, instance_settings): """Save the settings associated with this widget Currently, this is just the plot type, but may include more useful data in the future""" instance_settings.set_value('plot_type', self._plot_index) xlim = self.get_xlim() ylim = self.get_ylim() # convert limits to normal arrays of floats; some backends return numpy # arrays xlim = [float(x) for x in xlim] ylim = [float(y) for y in ylim] instance_settings.set_value('x_limits', pack(xlim)) instance_settings.set_value('y_limits', pack(ylim)) def restore_settings(self, plugin_settings, instance_settings): """Restore the settings for this widget Currently, this just restores the plot type.""" self._switch_data_plot_widget( int(instance_settings.value('plot_type', 0))) xlim = unpack(instance_settings.value('x_limits', [])) ylim = unpack(instance_settings.value('y_limits', [])) if xlim: # convert limits to an array of floats; they're often lists of # strings try: xlim = [float(x) for x in xlim] self.set_xlim(xlim) except: qWarning("Failed to restore X limits") if ylim: try: ylim = [float(y) for y in ylim] self.set_ylim(ylim) except: qWarning("Failed to restore Y limits") def doSettingsDialog(self): """Present the user with a dialog for choosing the plot backend This displays a SimpleSettingsDialog asking the user to choose a plot type, gets the result, and updates the plot type as necessary This method is blocking""" marker_settings = [{ 'title': 'Show Plot Markers', 'description': 'Warning: Displaying markers in rqt_plot may cause\n \t high cpu load, ' 'especially using PyQtGraph\n', 'enabled': True, }] if self._markers_on: selected_checkboxes = [0] else: selected_checkboxes = [] dialog = SimpleSettingsDialog(title='Plot Options') dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index) dialog.add_checkbox_group(title='Plot Markers', options=marker_settings, selected_indexes=selected_checkboxes) [plot_type, checkboxes] = dialog.get_settings() if plot_type is not None and \ plot_type['selected_index'] is not None and \ self._plot_index != plot_type['selected_index']: self._switch_data_plot_widget(plot_type['selected_index'], 0 in checkboxes['selected_indexes']) else: if checkboxes is not None and self._markers_on != ( 0 in checkboxes['selected_indexes']): self._switch_plot_markers(0 in checkboxes['selected_indexes']) # interface out to the managing DATA component: load data, update data, # etc def autoscroll(self, enabled=True): """Enable or disable autoscrolling of the plot""" self._autoscroll = enabled def redraw(self): self._redraw.emit() def _do_redraw(self): """Redraw the underlying plot This causes the underlying plot to be redrawn. This is usually used after adding or updating the plot data""" if self._data_plot_widget: self._merged_autoscale() for curve_id in self._curves: curve = self._curves[curve_id] try: self._data_plot_widget.set_values(curve_id, curve['x'], curve['y']) except KeyError: # skip curve which has been removed in the mean time pass self._data_plot_widget.redraw() def _get_curve(self, curve_id): if curve_id in self._curves: return self._curves[curve_id] else: raise DataPlotException("No curve named %s in this DataPlot" % (curve_id)) def add_curve(self, curve_id, curve_name, data_x, data_y, curve_color=None, dashed=False): """Add a new, named curve to this plot Add a curve named `curve_name` to the plot, with initial data series `data_x` and `data_y`. Future references to this curve should use the provided `curve_id` Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. """ if curve_color is None: curve_color = QColor(self._colors[self._color_index % len(self._colors)]) self._color_index += 1 self._curves[curve_id] = { 'x': numpy.array(data_x), 'y': numpy.array(data_y), 'name': curve_name, 'color': curve_color, 'dashed': dashed } if self._data_plot_widget: self._add_curve.emit(curve_id, curve_name, curve_color, self._markers_on, dashed) def remove_curve(self, curve_id): """Remove the specified curve from this plot""" # TODO: do on UI thread with signals if curve_id in self._curves: del self._curves[curve_id] if self._data_plot_widget: self._data_plot_widget.remove_curve(curve_id) def update_values(self, curve_id, values_x, values_y, sort_data=True): """Append new data to an existing curve `values_x` and `values_y` will be appended to the existing data for `curve_id` Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. If `sort_data` is set to False, values won't be sorted by `values_x` order. """ curve = self._get_curve(curve_id) curve['x'] = numpy.append(curve['x'], values_x) curve['y'] = numpy.append(curve['y'], values_y) if sort_data: # sort resulting data, so we can slice it later sort_order = curve['x'].argsort() curve['x'] = curve['x'][sort_order] curve['y'] = curve['y'][sort_order] def clear_values(self, curve_id=None): """Clear the values for the specified curve, or all curves This will erase the data series associaed with `curve_id`, or all curves if `curve_id` is not present or is None Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. """ # clear internal curve representation if curve_id: curve = self._get_curve(curve_id) curve['x'] = numpy.array([]) curve['y'] = numpy.array([]) else: for curve_id in self._curves: self._curves[curve_id]['x'] = numpy.array([]) self._curves[curve_id]['y'] = numpy.array([]) def vline(self, x, color=RED): """Draw a vertical line on the plot Draw a line a position X, with the given color @param x: position of the vertical line to draw @param color: optional parameter specifying the color, as tuple of RGB values from 0 to 255 """ self._vline = (x, color) if self._data_plot_widget: self._data_plot_widget.vline(x, color) # autoscaling methods def set_autoscale(self, x=None, y=None): """Change autoscaling of plot axes if a parameter is not passed, the autoscaling setting for that axis is not changed @param x: enable or disable autoscaling for X @param y: set autoscaling mode for Y """ if x is not None: self._autoscale_x = x if y is not None: self._autoscale_y = y # autoscaling: adjusting the plot bounds fit the data # autoscrollig: move the plot X window to show the most recent data # # what order do we do these adjustments in? # * assuming the various stages are enabled: # * autoscale X to bring all data into view # * else, autoscale X to determine which data we're looking at # * autoscale Y to fit the data we're viewing # # * autoscaling of Y might have several modes: # * scale Y to fit the entire dataset # * scale Y to fit the current view # * increase the Y scale to fit the current view # # TODO: incrmenetal autoscaling: only update the autoscaling bounds # when new data is added def _merged_autoscale(self): x_limit = [numpy.inf, -numpy.inf] if self._autoscale_x: for curve_id in self._curves: curve = self._curves[curve_id] if len(curve['x']) > 0: x_limit[0] = min(x_limit[0], curve['x'].min()) x_limit[1] = max(x_limit[1], curve['x'].max()) elif self._autoscroll: # get current width of plot x_limit = self.get_xlim() # x_width = x_limit[1] - x_limit[0] x_width = self.x_width # reset the upper x_limit so that we ignore the previous position x_limit[1] = -numpy.inf # get largest X value for curve_id in self._curves: curve = self._curves[curve_id] if len(curve['x']) > 0: x_limit[1] = max(x_limit[1], curve['x'].max()) # set lower limit based on width x_limit[0] = x_limit[1] - x_width else: # don't modify limit, or get it from plot x_limit = self.get_xlim() # set sane limits if our limits are infinite if numpy.isinf(x_limit[0]): x_limit[0] = 0.0 if numpy.isinf(x_limit[1]): x_limit[1] = 1.0 y_limit = [numpy.inf, -numpy.inf] if self._autoscale_y: # if we're extending the y limits, initialize them with the # current limits if self._autoscale_y & DataPlot.SCALE_EXTEND: y_limit = self.get_ylim() for curve_id in self._curves: curve = self._curves[curve_id] start_index = 0 end_index = len(curve['x']) # if we're scaling based on the visible window, find the # start and end indicies of our window if self._autoscale_y & DataPlot.SCALE_VISIBLE: # indexof x_limit[0] in curves['x'] start_index = curve['x'].searchsorted(x_limit[0]) # indexof x_limit[1] in curves['x'] end_index = curve['x'].searchsorted(x_limit[1]) # region here is cheap because it is a numpy view and not a # copy of the underlying data region = curve['y'][start_index:end_index] if len(region) > 0: y_limit[0] = min(y_limit[0], region.min()) y_limit[1] = max(y_limit[1], region.max()) # TODO: compute padding around new min and max values # ONLY consider data for new values; not # existing limits, or we'll add padding on top of old # padding in SCALE_EXTEND mode # # pad the min/max # TODO: invert this padding in get_ylim # ymin = limits[0] # ymax = limits[1] # delta = ymax - ymin if ymax != ymin else 0.1 # ymin -= .05 * delta # ymax += .05 * delta else: y_limit = self.get_ylim() # set sane limits if our limits are infinite if numpy.isinf(y_limit[0]): y_limit[0] = 0.0 if numpy.isinf(y_limit[1]): y_limit[1] = 1.0 self.set_xlim(x_limit) self.set_ylim(y_limit) def get_xlim(self): """get X limits""" if self._data_plot_widget: return self._data_plot_widget.get_xlim() else: qWarning("No plot widget; returning default X limits") return [0.0, 1.0] def set_xlim(self, limits): """set X limits""" if self._data_plot_widget: self._data_plot_widget.set_xlim(limits) else: qWarning("No plot widget; can't set X limits") def get_ylim(self): """get Y limits""" if self._data_plot_widget: return self._data_plot_widget.get_ylim() else: qWarning("No plot widget; returning default Y limits") return [0.0, 10.0] def set_ylim(self, limits): """set Y limits""" if self._data_plot_widget: self._data_plot_widget.set_ylim(limits) else: qWarning("No plot widget; can't set Y limits")
class DataPlot(QWidget): """A widget for displaying a plot of data The DataPlot widget displays a plot, on one of several plotting backends, depending on which backend(s) are available at runtime. It currently supports PyQtGraph, MatPlot and QwtPlot backends. The DataPlot widget manages the plot backend internally, and can save and restore the internal state using `save_settings` and `restore_settings` functions. Currently, the user MUST call `restore_settings` before using the widget, to cause the creation of the enclosed plotting widget. """ # plot types in order of priority plot_types = [ { 'title': 'PyQtGraph', 'widget_class': PyQtGraphDataPlot, 'description': 'Based on PyQtGraph\n- installer: http://luke.campagnola.me/code/pyqtgraph\n', 'enabled': PyQtGraphDataPlot is not None, }, { 'title': 'MatPlot', 'widget_class': MatDataPlot, 'description': 'Based on MatPlotLib\n- needs most CPU\n- needs matplotlib >= 1.1.0\n- if using PySide: PySide > 1.1.0\n', 'enabled': MatDataPlot is not None, }, { 'title': 'QwtPlot', 'widget_class': QwtDataPlot, 'description': 'Based on QwtPlot\n- does not use timestamps\n- uses least CPU\n- needs Python Qwt bindings\n', 'enabled': QwtDataPlot is not None, }, ] # pre-defined colors: RED=(255, 0, 0) GREEN=(0, 255, 0) BLUE=(0, 0, 255) SCALE_ALL=1 SCALE_VISIBLE=2 SCALE_EXTEND=4 _colors = [Qt.blue, Qt.red, Qt.cyan, Qt.magenta, Qt.green, Qt.darkYellow, Qt.black, Qt.darkCyan, Qt.darkRed, Qt.gray] limits_changed = Signal() _redraw = Signal() _add_curve = Signal(str, str, 'QColor', bool) def __init__(self, parent=None): """Create a new, empty DataPlot This will raise a RuntimeError if none of the supported plotting backends can be found """ super(DataPlot, self).__init__(parent) self._plot_index = 0 self._color_index = 0 self._markers_on = False self._autoscroll = True self._autoscale_x = True self._autoscale_y = DataPlot.SCALE_ALL # the backend widget that we're trying to hide/abstract self._data_plot_widget = None self._curves = {} self._vline = None self._redraw.connect(self._do_redraw) self._layout = QHBoxLayout() self.setLayout(self._layout) enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']] if not enabled_plot_types: if qVersion().startswith('4.'): version_info = '1.1.0' else: # minimum matplotlib version for Qt 5 version_info = '1.4.0' if QT_BINDING == 'pyside': version_info += ' and PySide %s' % \ ('> 1.1.0' if qVersion().startswith('4.') else '>= 2.0.0') raise RuntimeError('No usable plot type found. Install at least one of: PyQtGraph, MatPlotLib (at least %s) or Python-Qwt5.' % version_info) self._switch_data_plot_widget(self._plot_index) self.show() def _switch_data_plot_widget(self, plot_index, markers_on=False): """Internal method for activating a plotting backend by index""" # check if selected plot type is available if not self.plot_types[plot_index]['enabled']: # find other available plot type for index, plot_type in enumerate(self.plot_types): if plot_type['enabled']: plot_index = index break self._plot_index = plot_index self._markers_on = markers_on selected_plot = self.plot_types[plot_index] if self._data_plot_widget: x_limits = self.get_xlim() y_limits = self.get_ylim() self._layout.removeWidget(self._data_plot_widget) self._data_plot_widget.close() self._data_plot_widget = None else: x_limits = [0.0, 10.0] y_limits = [-0.001, 0.001] self._data_plot_widget = selected_plot['widget_class'](self) self._data_plot_widget.limits_changed.connect(self.limits_changed) self._add_curve.connect(self._data_plot_widget.add_curve) self._layout.addWidget(self._data_plot_widget) # restore old data for curve_id in self._curves: curve = self._curves[curve_id] self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on) if self._vline: self.vline(*self._vline) self.set_xlim(x_limits) self.set_ylim(y_limits) self.redraw() def _switch_plot_markers(self, markers_on): self._markers_on = markers_on self._data_plot_widget._color_index = 0 for curve_id in self._curves: self._data_plot_widget.remove_curve(curve_id) curve = self._curves[curve_id] self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on) self.redraw() # interface out to the managing GUI component: get title, save, restore, # etc def getTitle(self): """get the title of the current plotting backend""" return self.plot_types[self._plot_index]['title'] def save_settings(self, plugin_settings, instance_settings): """Save the settings associated with this widget Currently, this is just the plot type, but may include more useful data in the future""" instance_settings.set_value('plot_type', self._plot_index) xlim = self.get_xlim() ylim = self.get_ylim() # convert limits to normal arrays of floats; some backends return numpy # arrays xlim = [float(x) for x in xlim] ylim = [float(y) for y in ylim] instance_settings.set_value('x_limits', pack(xlim)) instance_settings.set_value('y_limits', pack(ylim)) def restore_settings(self, plugin_settings, instance_settings): """Restore the settings for this widget Currently, this just restores the plot type.""" self._switch_data_plot_widget(int(instance_settings.value('plot_type', 0))) xlim = unpack(instance_settings.value('x_limits', [])) ylim = unpack(instance_settings.value('y_limits', [])) if xlim: # convert limits to an array of floats; they're often lists of # strings try: xlim = [float(x) for x in xlim] self.set_xlim(xlim) except: qWarning("Failed to restore X limits") if ylim: try: ylim = [float(y) for y in ylim] self.set_ylim(ylim) except: qWarning("Failed to restore Y limits") def doSettingsDialog(self): """Present the user with a dialog for choosing the plot backend This displays a SimpleSettingsDialog asking the user to choose a plot type, gets the result, and updates the plot type as necessary This method is blocking""" marker_settings = [ { 'title': 'Show Plot Markers', 'description': 'Warning: Displaying markers in rqt_plot may cause\n \t high cpu load, especially using PyQtGraph\n', 'enabled': True, }] if self._markers_on: selected_checkboxes = [0] else: selected_checkboxes = [] dialog = SimpleSettingsDialog(title='Plot Options') dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index) dialog.add_checkbox_group(title='Plot Markers', options=marker_settings, selected_indexes=selected_checkboxes) [plot_type, checkboxes] = dialog.get_settings() if plot_type is not None and plot_type['selected_index'] is not None and self._plot_index != plot_type['selected_index']: self._switch_data_plot_widget(plot_type['selected_index'], 0 in checkboxes['selected_indexes']) else: if checkboxes is not None and self._markers_on != (0 in checkboxes['selected_indexes']): self._switch_plot_markers(0 in checkboxes['selected_indexes']) # interface out to the managing DATA component: load data, update data, # etc def autoscroll(self, enabled=True): """Enable or disable autoscrolling of the plot""" self._autoscroll = enabled def redraw(self): self._redraw.emit() def _do_redraw(self): """Redraw the underlying plot This causes the underlying plot to be redrawn. This is usually used after adding or updating the plot data""" if self._data_plot_widget: self._merged_autoscale() for curve_id in self._curves: curve = self._curves[curve_id] self._data_plot_widget.set_values(curve_id, curve['x'], curve['y']) self._data_plot_widget.redraw() def _get_curve(self, curve_id): if curve_id in self._curves: return self._curves[curve_id] else: raise DataPlotException("No curve named %s in this DataPlot" % ( curve_id) ) def add_curve(self, curve_id, curve_name, data_x, data_y): """Add a new, named curve to this plot Add a curve named `curve_name` to the plot, with initial data series `data_x` and `data_y`. Future references to this curve should use the provided `curve_id` Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. """ curve_color = QColor(self._colors[self._color_index % len(self._colors)]) self._color_index += 1 self._curves[curve_id] = { 'x': numpy.array(data_x), 'y': numpy.array(data_y), 'name': curve_name, 'color': curve_color} if self._data_plot_widget: self._add_curve.emit(curve_id, curve_name, curve_color, self._markers_on) def remove_curve(self, curve_id): """Remove the specified curve from this plot""" # TODO: do on UI thread with signals if curve_id in self._curves: del self._curves[curve_id] if self._data_plot_widget: self._data_plot_widget.remove_curve(curve_id) def update_values(self, curve_id, values_x, values_y): """Append new data to an existing curve `values_x` and `values_y` will be appended to the existing data for `curve_id` Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. """ curve = self._get_curve(curve_id) curve['x'] = numpy.append(curve['x'], values_x) curve['y'] = numpy.append(curve['y'], values_y) # sort resulting data, so we can slice it later sort_order = curve['x'].argsort() curve['x'] = curve['x'][sort_order] curve['y'] = curve['y'][sort_order] def clear_values(self, curve_id=None): """Clear the values for the specified curve, or all curves This will erase the data series associaed with `curve_id`, or all curves if `curve_id` is not present or is None Note that the plot is not redraw automatically; call `redraw()` to make any changes visible to the user. """ # clear internal curve representation if curve_id: curve = self._get_curve(curve_id) curve['x'] = numpy.array([]) curve['y'] = numpy.array([]) else: for curve_id in self._curves: self._curves[curve_id]['x'] = numpy.array([]) self._curves[curve_id]['y'] = numpy.array([]) def vline(self, x, color=RED): """Draw a vertical line on the plot Draw a line a position X, with the given color @param x: position of the vertical line to draw @param color: optional parameter specifying the color, as tuple of RGB values from 0 to 255 """ self._vline = (x, color) if self._data_plot_widget: self._data_plot_widget.vline(x, color) # autoscaling methods def set_autoscale(self, x=None, y=None): """Change autoscaling of plot axes if a parameter is not passed, the autoscaling setting for that axis is not changed @param x: enable or disable autoscaling for X @param y: set autoscaling mode for Y """ if x is not None: self._autoscale_x = x if y is not None: self._autoscale_y = y # autoscaling: adjusting the plot bounds fit the data # autoscrollig: move the plot X window to show the most recent data # # what order do we do these adjustments in? # * assuming the various stages are enabled: # * autoscale X to bring all data into view # * else, autoscale X to determine which data we're looking at # * autoscale Y to fit the data we're viewing # # * autoscaling of Y might have several modes: # * scale Y to fit the entire dataset # * scale Y to fit the current view # * increase the Y scale to fit the current view # # TODO: incrmenetal autoscaling: only update the autoscaling bounds # when new data is added def _merged_autoscale(self): x_limit = [numpy.inf, -numpy.inf] if self._autoscale_x: for curve_id in self._curves: curve = self._curves[curve_id] if len(curve['x']) > 0: x_limit[0] = min(x_limit[0], curve['x'].min()) x_limit[1] = max(x_limit[1], curve['x'].max()) elif self._autoscroll: # get current width of plot x_limit = self.get_xlim() x_width = x_limit[1] - x_limit[0] # reset the upper x_limit so that we ignore the previous position x_limit[1] = -numpy.inf # get largest X value for curve_id in self._curves: curve = self._curves[curve_id] if len(curve['x']) > 0: x_limit[1] = max(x_limit[1], curve['x'].max()) # set lower limit based on width x_limit[0] = x_limit[1] - x_width else: # don't modify limit, or get it from plot x_limit = self.get_xlim() # set sane limits if our limits are infinite if numpy.isinf(x_limit[0]): x_limit[0] = 0.0 if numpy.isinf(x_limit[1]): x_limit[1] = 1.0 y_limit = [numpy.inf, -numpy.inf] if self._autoscale_y: # if we're extending the y limits, initialize them with the # current limits if self._autoscale_y & DataPlot.SCALE_EXTEND: y_limit = self.get_ylim() for curve_id in self._curves: curve = self._curves[curve_id] start_index = 0 end_index = len(curve['x']) # if we're scaling based on the visible window, find the # start and end indicies of our window if self._autoscale_y & DataPlot.SCALE_VISIBLE: # indexof x_limit[0] in curves['x'] start_index = curve['x'].searchsorted(x_limit[0]) # indexof x_limit[1] in curves['x'] end_index = curve['x'].searchsorted(x_limit[1]) # region here is cheap because it is a numpy view and not a # copy of the underlying data region = curve['y'][start_index:end_index] if len(region) > 0: y_limit[0] = min(y_limit[0], region.min()) y_limit[1] = max(y_limit[1], region.max()) # TODO: compute padding around new min and max values # ONLY consider data for new values; not # existing limits, or we'll add padding on top of old # padding in SCALE_EXTEND mode # # pad the min/max # TODO: invert this padding in get_ylim #ymin = limits[0] #ymax = limits[1] #delta = ymax - ymin if ymax != ymin else 0.1 #ymin -= .05 * delta #ymax += .05 * delta else: y_limit = self.get_ylim() # set sane limits if our limits are infinite if numpy.isinf(y_limit[0]): y_limit[0] = 0.0 if numpy.isinf(y_limit[1]): y_limit[1] = 1.0 self.set_xlim(x_limit) self.set_ylim(y_limit) def get_xlim(self): """get X limits""" if self._data_plot_widget: return self._data_plot_widget.get_xlim() else: qWarning("No plot widget; returning default X limits") return [0.0, 1.0] def set_xlim(self, limits): """set X limits""" if self._data_plot_widget: self._data_plot_widget.set_xlim(limits) else: qWarning("No plot widget; can't set X limits") def get_ylim(self): """get Y limits""" if self._data_plot_widget: return self._data_plot_widget.get_ylim() else: qWarning("No plot widget; returning default Y limits") return [0.0, 10.0] def set_ylim(self, limits): """set Y limits""" if self._data_plot_widget: self._data_plot_widget.set_ylim(limits) else: qWarning("No plot widget; can't set Y limits")