__init__.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Copyright (c) 2014, Austin Hendrix
00004 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
00005 # All rights reserved.
00006 #
00007 # Redistribution and use in source and binary forms, with or without
00008 # modification, are permitted provided that the following conditions
00009 # are met:
00010 #
00011 #   * Redistributions of source code must retain the above copyright
00012 #     notice, this list of conditions and the following disclaimer.
00013 #   * Redistributions in binary form must reproduce the above
00014 #     copyright notice, this list of conditions and the following
00015 #     disclaimer in the documentation and/or other materials provided
00016 #     with the distribution.
00017 #   * Neither the name of the TU Darmstadt nor the names of its
00018 #     contributors may be used to endorse or promote products derived
00019 #     from this software without specific prior written permission.
00020 #
00021 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00022 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00023 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00024 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00025 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00026 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00027 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00028 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00029 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00030 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00031 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00032 # POSSIBILITY OF SUCH DAMAGE.
00033 
00034 import numpy
00035 
00036 from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog
00037 from python_qt_binding.QtCore import Qt, qDebug, qWarning, Signal
00038 from python_qt_binding.QtGui import QColor, QWidget, QHBoxLayout
00039 
00040 try:
00041     from pyqtgraph_data_plot import PyQtGraphDataPlot
00042 except ImportError:
00043     qDebug('[DEBUG] rqt_plot.plot: import of PyQtGraphDataPlot failed (trying other backends)')
00044     PyQtGraphDataPlot = None
00045 
00046 try:
00047     from mat_data_plot import MatDataPlot
00048 except ImportError:
00049     qDebug('[DEBUG] rqt_plot.plot: import of MatDataPlot failed (trying other backends)')
00050     MatDataPlot = None
00051 
00052 try:
00053     from qwt_data_plot import QwtDataPlot
00054 except ImportError:
00055     qDebug('[DEBUG] rqt_plot.plot: import of QwtDataPlot failed (trying other backends)')
00056     QwtDataPlot = None
00057 
00058 # separate class for DataPlot exceptions, just so that users can differentiate
00059 # errors from the DataPlot widget from exceptions generated by the underlying
00060 # libraries
00061 class DataPlotException(Exception):
00062     pass
00063 
00064 class DataPlot(QWidget):
00065     """A widget for displaying a plot of data
00066 
00067     The DataPlot widget displays a plot, on one of several plotting backends,
00068     depending on which backend(s) are available at runtime. It currently 
00069     supports PyQtGraph, MatPlot and QwtPlot backends.
00070 
00071     The DataPlot widget manages the plot backend internally, and can save
00072     and restore the internal state using `save_settings` and `restore_settings`
00073     functions.
00074 
00075     Currently, the user MUST call `restore_settings` before using the widget,
00076     to cause the creation of the enclosed plotting widget.
00077     """
00078     # plot types in order of priority
00079     plot_types = [
00080         {
00081             'title': 'PyQtGraph',
00082             'widget_class': PyQtGraphDataPlot,
00083             'description': 'Based on PyQtGraph\n- installer: http://luke.campagnola.me/code/pyqtgraph\n',
00084             'enabled': PyQtGraphDataPlot is not None,
00085         },
00086         {
00087             'title': 'MatPlot',
00088             'widget_class': MatDataPlot,
00089             'description': 'Based on MatPlotLib\n- needs most CPU\n- needs matplotlib >= 1.1.0\n- if using PySide: PySide > 1.1.0\n',
00090             'enabled': MatDataPlot is not None,
00091         },
00092         {
00093             'title': 'QwtPlot',
00094             'widget_class': QwtDataPlot,
00095             'description': 'Based on QwtPlot\n- does not use timestamps\n- uses least CPU\n- needs Python Qwt bindings\n',
00096             'enabled': QwtDataPlot is not None,
00097         },
00098     ]
00099 
00100     # pre-defined colors:
00101     RED=(255, 0, 0)
00102     GREEN=(0, 255, 0)
00103     BLUE=(0, 0, 255)
00104 
00105     SCALE_ALL=1
00106     SCALE_VISIBLE=2
00107     SCALE_EXTEND=4
00108 
00109     _colors = [Qt.blue, Qt.red, Qt.cyan, Qt.magenta, Qt.green, Qt.darkYellow, Qt.black, Qt.darkCyan, Qt.darkRed, Qt.gray]
00110 
00111     limits_changed = Signal()
00112     _redraw = Signal()
00113     _add_curve = Signal(str, str, 'QColor', bool)
00114 
00115     def __init__(self, parent=None):
00116         """Create a new, empty DataPlot
00117 
00118         This will raise a RuntimeError if none of the supported plotting
00119         backends can be found
00120         """
00121         super(DataPlot, self).__init__(parent)
00122         self._plot_index = 0
00123         self._color_index = 0
00124         self._markers_on = False
00125         self._autoscroll = True
00126 
00127         self._autoscale_x = True
00128         self._autoscale_y = DataPlot.SCALE_ALL
00129 
00130         # the backend widget that we're trying to hide/abstract
00131         self._data_plot_widget = None
00132         self._curves = {}
00133         self._vline = None
00134         self._redraw.connect(self._do_redraw)
00135 
00136         self._layout = QHBoxLayout()
00137         self.setLayout(self._layout)
00138 
00139         enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']]
00140         if not enabled_plot_types:
00141             version_info = ' and PySide > 1.1.0' if QT_BINDING == 'pyside' else ''
00142             raise RuntimeError('No usable plot type found. Install at least one of: PyQtGraph, MatPlotLib (at least 1.1.0%s) or Python-Qwt5.' % version_info)
00143 
00144         self._switch_data_plot_widget(self._plot_index)
00145 
00146         self.show()
00147 
00148     def _switch_data_plot_widget(self, plot_index, markers_on=False):
00149         """Internal method for activating a plotting backend by index"""
00150         # check if selected plot type is available
00151         if not self.plot_types[plot_index]['enabled']:
00152             # find other available plot type
00153             for index, plot_type in enumerate(self.plot_types):
00154                 if plot_type['enabled']:
00155                     plot_index = index
00156                     break
00157 
00158         self._plot_index = plot_index
00159         self._markers_on = markers_on
00160         selected_plot = self.plot_types[plot_index]
00161 
00162         if self._data_plot_widget:
00163             x_limits = self.get_xlim()
00164             y_limits = self.get_ylim()
00165 
00166             self._layout.removeWidget(self._data_plot_widget)
00167             self._data_plot_widget.close()
00168             self._data_plot_widget = None
00169         else:
00170             x_limits = [0.0, 10.0]
00171             y_limits = [-0.001, 0.001]
00172 
00173         self._data_plot_widget = selected_plot['widget_class'](self)
00174         self._data_plot_widget.limits_changed.connect(self.limits_changed)
00175         self._add_curve.connect(self._data_plot_widget.add_curve)
00176         self._layout.addWidget(self._data_plot_widget)
00177 
00178         # restore old data
00179         for curve_id in self._curves:
00180             curve = self._curves[curve_id]
00181             self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00182 
00183         if self._vline:
00184             self.vline(*self._vline)
00185 
00186         self.set_xlim(x_limits)
00187         self.set_ylim(y_limits)
00188         self.redraw()
00189 
00190     def _switch_plot_markers(self, markers_on):
00191         self._markers_on = markers_on
00192         self._data_plot_widget._color_index = 0
00193 
00194         for curve_id in self._curves:
00195             self._data_plot_widget.remove_curve(curve_id)
00196             curve = self._curves[curve_id]
00197             self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00198 
00199         self.redraw()
00200 
00201     # interface out to the managing GUI component: get title, save, restore, 
00202     # etc
00203     def getTitle(self):
00204         """get the title of the current plotting backend"""
00205         return self.plot_types[self._plot_index]['title']
00206 
00207     def save_settings(self, plugin_settings, instance_settings):
00208         """Save the settings associated with this widget
00209 
00210         Currently, this is just the plot type, but may include more useful
00211         data in the future"""
00212         instance_settings.set_value('plot_type', self._plot_index)
00213 
00214     def restore_settings(self, plugin_settings, instance_settings):
00215         """Restore the settings for this widget
00216 
00217         Currently, this just restores the plot type."""
00218         self._switch_data_plot_widget(int(instance_settings.value('plot_type', 0)))
00219 
00220     def doSettingsDialog(self):
00221         """Present the user with a dialog for choosing the plot backend
00222 
00223         This displays a SimpleSettingsDialog asking the user to choose a
00224         plot type, gets the result, and updates the plot type as necessary
00225         
00226         This method is blocking"""
00227 
00228         marker_settings = [
00229             {
00230                 'title': 'Show Plot Markers',
00231                 'description': 'Warning: Displaying markers in rqt_plot may cause\n \t high cpu load, especially using PyQtGraph\n',
00232                 'enabled': True,
00233             }]
00234         if self._markers_on:
00235             selected_checkboxes = [0]
00236         else:
00237             selected_checkboxes = []
00238 
00239         dialog = SimpleSettingsDialog(title='Plot Options')
00240         dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index)
00241         dialog.add_checkbox_group(title='Plot Markers', options=marker_settings, selected_indexes=selected_checkboxes)
00242         [plot_type, checkboxes] = dialog.get_settings()
00243         if plot_type is not None and plot_type['selected_index'] is not None and self._plot_index != plot_type['selected_index']:
00244             self._switch_data_plot_widget(plot_type['selected_index'], 0 in checkboxes['selected_indexes'])
00245         else:
00246             if checkboxes is not None and self._markers_on != (0 in checkboxes['selected_indexes']):
00247                 self._switch_plot_markers(0 in checkboxes['selected_indexes'])
00248 
00249     # interface out to the managing DATA component: load data, update data,
00250     # etc
00251     def autoscroll(self, enabled=True):
00252         """Enable or disable autoscrolling of the plot"""
00253         self._autoscroll = enabled
00254 
00255     def redraw(self):
00256         self._redraw.emit()
00257 
00258     def _do_redraw(self):
00259         """Redraw the underlying plot
00260 
00261         This causes the underlying plot to be redrawn. This is usually used
00262         after adding or updating the plot data"""
00263         if self._data_plot_widget:
00264             self._merged_autoscale()
00265             for curve_id in self._curves:
00266                 curve = self._curves[curve_id]
00267                 self._data_plot_widget.set_values(curve_id, curve['x'], curve['y'])
00268             self._data_plot_widget.redraw()
00269 
00270     def _get_curve(self, curve_id):
00271         if curve_id in self._curves:
00272             return self._curves[curve_id]
00273         else:
00274             raise DataPlotException("No curve named %s in this DataPlot" %
00275                     ( curve_id) )
00276 
00277     def add_curve(self, curve_id, curve_name, data_x, data_y):
00278         """Add a new, named curve to this plot
00279 
00280         Add a curve named `curve_name` to the plot, with initial data series
00281         `data_x` and `data_y`.
00282         
00283         Future references to this curve should use the provided `curve_id`
00284 
00285         Note that the plot is not redraw automatically; call `redraw()` to make
00286         any changes visible to the user.
00287         """
00288         curve_color = QColor(self._colors[self._color_index % len(self._colors)])
00289         self._color_index += 1
00290 
00291         self._curves[curve_id] = { 'x': numpy.array(data_x),
00292                                    'y': numpy.array(data_y),
00293                                    'name': curve_name,
00294                                    'color': curve_color}
00295         if self._data_plot_widget:
00296             self._add_curve.emit(curve_id, curve_name, curve_color, self._markers_on)
00297 
00298     def remove_curve(self, curve_id):
00299         """Remove the specified curve from this plot"""
00300         # TODO: do on UI thread with signals
00301         if curve_id in self._curves:
00302             del self._curves[curve_id]
00303         if self._data_plot_widget:
00304             self._data_plot_widget.remove_curve(curve_id)
00305 
00306     def update_values(self, curve_id, values_x, values_y):
00307         """Append new data to an existing curve
00308         
00309         `values_x` and `values_y` will be appended to the existing data for
00310         `curve_id`
00311 
00312         Note that the plot is not redraw automatically; call `redraw()` to make
00313         any changes visible to the user.
00314         """
00315         curve = self._get_curve(curve_id)
00316         curve['x'] = numpy.append(curve['x'], values_x)
00317         curve['y'] = numpy.append(curve['y'], values_y)
00318         # sort resulting data, so we can slice it later
00319         sort_order = curve['x'].argsort()
00320         curve['x'] = curve['x'][sort_order]
00321         curve['y'] = curve['y'][sort_order]
00322 
00323     def clear_values(self, curve_id=None):
00324         """Clear the values for the specified curve, or all curves
00325 
00326         This will erase the data series associaed with `curve_id`, or all
00327         curves if `curve_id` is not present or is None
00328 
00329         Note that the plot is not redraw automatically; call `redraw()` to make
00330         any changes visible to the user.
00331         """
00332         # clear internal curve representation
00333         if curve_id:
00334             curve = self._get_curve(curve_id)
00335             curve['x'] = numpy.array([])
00336             curve['y'] = numpy.array([])
00337         else:
00338             for curve_id in self._curves:
00339                 self._curves[curve_id]['x'] = numpy.array([])
00340                 self._curves[curve_id]['y'] = numpy.array([])
00341 
00342 
00343     def vline(self, x, color=RED):
00344         """Draw a vertical line on the plot
00345 
00346         Draw a line a position X, with the given color
00347         
00348         @param x: position of the vertical line to draw
00349         @param color: optional parameter specifying the color, as tuple of
00350                       RGB values from 0 to 255
00351         """
00352         self._vline = (x, color)
00353         if self._data_plot_widget:
00354             self._data_plot_widget.vline(x, color)
00355 
00356     # autoscaling methods
00357     def set_autoscale(self, x=None, y=None):
00358         """Change autoscaling of plot axes
00359 
00360         if a parameter is not passed, the autoscaling setting for that axis is
00361         not changed
00362 
00363         @param x: enable or disable autoscaling for X
00364         @param y: set autoscaling mode for Y
00365         """
00366         if x is not None:
00367             self._autoscale_x = x
00368         if y is not None:
00369             self._autoscale_y = y
00370 
00371     # autoscaling:  adjusting the plot bounds fit the data
00372     # autoscrollig: move the plot X window to show the most recent data
00373     #
00374     # what order do we do these adjustments in?
00375     #  * assuming the various stages are enabled:
00376     #  * autoscale X to bring all data into view
00377     #   * else, autoscale X to determine which data we're looking at
00378     #  * autoscale Y to fit the data we're viewing
00379     #
00380     # * autoscaling of Y might have several modes:
00381     #  * scale Y to fit the entire dataset
00382     #  * scale Y to fit the current view
00383     #  * increase the Y scale to fit the current view
00384     #
00385     # TODO: incrmenetal autoscaling: only update the autoscaling bounds
00386     #       when new data is added
00387     def _merged_autoscale(self):
00388         x_limit = [numpy.inf, -numpy.inf]
00389         if self._autoscale_x:
00390             for curve_id in self._curves:
00391                 curve = self._curves[curve_id]
00392                 if len(curve['x']) > 0:
00393                     x_limit[0] = min(x_limit[0], curve['x'].min())
00394                     x_limit[1] = max(x_limit[1], curve['x'].max())
00395         elif self._autoscroll:
00396             # get current width of plot
00397             x_limit = self.get_xlim()
00398             x_width = x_limit[1] - x_limit[0]
00399 
00400             # reset the upper x_limit so that we ignore the previous position
00401             x_limit[1] = -numpy.inf
00402             
00403             # get largest X value
00404             for curve_id in self._curves:
00405                 curve = self._curves[curve_id]
00406                 if len(curve['x']) > 0:
00407                     x_limit[1] = max(x_limit[1], curve['x'].max())
00408 
00409             # set lower limit based on width
00410             x_limit[0] = x_limit[1] - x_width
00411         else:
00412             # don't modify limit, or get it from plot
00413             x_limit = self.get_xlim()
00414 
00415         # set sane limits if our limits are infinite
00416         if numpy.isinf(x_limit[0]):
00417             x_limit[0] = 0.0
00418         if numpy.isinf(x_limit[1]):
00419             x_limit[1] = 1.0
00420 
00421 
00422         y_limit = [numpy.inf, -numpy.inf]
00423         if self._autoscale_y:
00424             # if we're extending the y limits, initialize them with the
00425             # current limits
00426             if self._autoscale_y & DataPlot.SCALE_EXTEND:
00427                 y_limit = self.get_ylim()
00428             for curve_id in self._curves:
00429                 curve = self._curves[curve_id]
00430                 start_index = 0
00431                 end_index = len(curve['x'])
00432 
00433                 # if we're scaling based on the visible window, find the
00434                 # start and end indicies of our window
00435                 if self._autoscale_y & DataPlot.SCALE_VISIBLE:
00436                     # indexof x_limit[0] in curves['x']
00437                     start_index = curve['x'].searchsorted(x_limit[0])
00438                     # indexof x_limit[1] in curves['x']
00439                     end_index = curve['x'].searchsorted(x_limit[1])
00440 
00441                 # region here is cheap because it is a numpy view and not a
00442                 # copy of the underlying data
00443                 region = curve['y'][start_index:end_index]
00444                 if len(region) > 0:
00445                     y_limit[0] = min(y_limit[0], region.min())
00446                     y_limit[1] = max(y_limit[1], region.max())
00447 
00448                 # TODO: compute padding around new min and max values
00449                 #       ONLY consider data for new values; not
00450                 #       existing limits, or we'll add padding on top of old
00451                 #       padding in SCALE_EXTEND mode
00452                 # 
00453                 # pad the min/max
00454                 # TODO: invert this padding in get_ylim
00455                 #ymin = limits[0]
00456                 #ymax = limits[1]
00457                 #delta = ymax - ymin if ymax != ymin else 0.1
00458                 #ymin -= .05 * delta
00459                 #ymax += .05 * delta
00460         else:
00461             y_limit = self.get_ylim()
00462 
00463         # set sane limits if our limits are infinite
00464         if numpy.isinf(y_limit[0]):
00465             y_limit[0] = 0.0
00466         if numpy.isinf(y_limit[1]):
00467             y_limit[1] = 1.0
00468 
00469         self.set_xlim(x_limit)
00470         self.set_ylim(y_limit)
00471 
00472     def get_xlim(self):
00473         """get X limits"""
00474         if self._data_plot_widget:
00475             return self._data_plot_widget.get_xlim()
00476         else:
00477             qWarning("No plot widget; returning default X limits")
00478             return [0.0, 1.0]
00479 
00480     def set_xlim(self, limits):
00481         """set X limits"""
00482         if self._data_plot_widget:
00483             self._data_plot_widget.set_xlim(limits)
00484         else:
00485             qWarning("No plot widget; can't set X limits")
00486 
00487     def get_ylim(self):
00488         """get Y limits"""
00489         if self._data_plot_widget:
00490             return self._data_plot_widget.get_ylim()
00491         else:
00492             qWarning("No plot widget; returning default Y limits")
00493             return [0.0, 10.0]
00494 
00495     def set_ylim(self, limits):
00496         """set Y limits"""
00497         if self._data_plot_widget:
00498             self._data_plot_widget.set_ylim(limits)
00499         else:
00500             qWarning("No plot widget; can't set Y limits")
00501 
00502     # signal on y limit changed?


rqt_plot
Author(s): Dorian Scholz
autogenerated on Mon Oct 6 2014 07:15:32