00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
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
00059
00060
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
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
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
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
00151 if not self.plot_types[plot_index]['enabled']:
00152
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
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
00202
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
00250
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
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
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
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
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
00372
00373
00374
00375
00376
00377
00378
00379
00380
00381
00382
00383
00384
00385
00386
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
00397 x_limit = self.get_xlim()
00398 x_width = x_limit[1] - x_limit[0]
00399
00400
00401 x_limit[1] = -numpy.inf
00402
00403
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
00410 x_limit[0] = x_limit[1] - x_width
00411 else:
00412
00413 x_limit = self.get_xlim()
00414
00415
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
00425
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
00434
00435 if self._autoscale_y & DataPlot.SCALE_VISIBLE:
00436
00437 start_index = curve['x'].searchsorted(x_limit[0])
00438
00439 end_index = curve['x'].searchsorted(x_limit[1])
00440
00441
00442
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
00449
00450
00451
00452
00453
00454
00455
00456
00457
00458
00459
00460 else:
00461 y_limit = self.get_ylim()
00462
00463
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