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, qWarning, Signal
00038 from python_qt_binding.QtGui import QColor, QWidget, QHBoxLayout
00039 from rqt_py_common.ini_helper import pack, unpack
00040
00041 try:
00042 from pyqtgraph_data_plot import PyQtGraphDataPlot
00043 except ImportError:
00044 PyQtGraphDataPlot = None
00045
00046 try:
00047 from mat_data_plot import MatDataPlot
00048 except ImportError:
00049 MatDataPlot = None
00050
00051 try:
00052 from qwt_data_plot import QwtDataPlot
00053 except ImportError:
00054 QwtDataPlot = None
00055
00056
00057
00058
00059 class DataPlotException(Exception):
00060 pass
00061
00062 class DataPlot(QWidget):
00063 """A widget for displaying a plot of data
00064
00065 The DataPlot widget displays a plot, on one of several plotting backends,
00066 depending on which backend(s) are available at runtime. It currently
00067 supports PyQtGraph, MatPlot and QwtPlot backends.
00068
00069 The DataPlot widget manages the plot backend internally, and can save
00070 and restore the internal state using `save_settings` and `restore_settings`
00071 functions.
00072
00073 Currently, the user MUST call `restore_settings` before using the widget,
00074 to cause the creation of the enclosed plotting widget.
00075 """
00076
00077 plot_types = [
00078 {
00079 'title': 'PyQtGraph',
00080 'widget_class': PyQtGraphDataPlot,
00081 'description': 'Based on PyQtGraph\n- installer: http://luke.campagnola.me/code/pyqtgraph\n',
00082 'enabled': PyQtGraphDataPlot is not None,
00083 },
00084 {
00085 'title': 'MatPlot',
00086 'widget_class': MatDataPlot,
00087 'description': 'Based on MatPlotLib\n- needs most CPU\n- needs matplotlib >= 1.1.0\n- if using PySide: PySide > 1.1.0\n',
00088 'enabled': MatDataPlot is not None,
00089 },
00090 {
00091 'title': 'QwtPlot',
00092 'widget_class': QwtDataPlot,
00093 'description': 'Based on QwtPlot\n- does not use timestamps\n- uses least CPU\n- needs Python Qwt bindings\n',
00094 'enabled': QwtDataPlot is not None,
00095 },
00096 ]
00097
00098
00099 RED=(255, 0, 0)
00100 GREEN=(0, 255, 0)
00101 BLUE=(0, 0, 255)
00102
00103 SCALE_ALL=1
00104 SCALE_VISIBLE=2
00105 SCALE_EXTEND=4
00106
00107 _colors = [Qt.blue, Qt.red, Qt.cyan, Qt.magenta, Qt.green, Qt.darkYellow, Qt.black, Qt.darkCyan, Qt.darkRed, Qt.gray]
00108
00109 limits_changed = Signal()
00110 _redraw = Signal()
00111 _add_curve = Signal(str, str, 'QColor', bool)
00112
00113 def __init__(self, parent=None):
00114 """Create a new, empty DataPlot
00115
00116 This will raise a RuntimeError if none of the supported plotting
00117 backends can be found
00118 """
00119 super(DataPlot, self).__init__(parent)
00120 self._plot_index = 0
00121 self._color_index = 0
00122 self._markers_on = False
00123 self._autoscroll = True
00124
00125 self._autoscale_x = True
00126 self._autoscale_y = DataPlot.SCALE_ALL
00127
00128
00129 self._data_plot_widget = None
00130 self._curves = {}
00131 self._vline = None
00132 self._redraw.connect(self._do_redraw)
00133
00134 self._layout = QHBoxLayout()
00135 self.setLayout(self._layout)
00136
00137 enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']]
00138 if not enabled_plot_types:
00139 version_info = ' and PySide > 1.1.0' if QT_BINDING == 'pyside' else ''
00140 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)
00141
00142 self._switch_data_plot_widget(self._plot_index)
00143
00144 self.show()
00145
00146 def _switch_data_plot_widget(self, plot_index, markers_on=False):
00147 """Internal method for activating a plotting backend by index"""
00148
00149 if not self.plot_types[plot_index]['enabled']:
00150
00151 for index, plot_type in enumerate(self.plot_types):
00152 if plot_type['enabled']:
00153 plot_index = index
00154 break
00155
00156 self._plot_index = plot_index
00157 self._markers_on = markers_on
00158 selected_plot = self.plot_types[plot_index]
00159
00160 if self._data_plot_widget:
00161 x_limits = self.get_xlim()
00162 y_limits = self.get_ylim()
00163
00164 self._layout.removeWidget(self._data_plot_widget)
00165 self._data_plot_widget.close()
00166 self._data_plot_widget = None
00167 else:
00168 x_limits = [0.0, 10.0]
00169 y_limits = [-0.001, 0.001]
00170
00171 self._data_plot_widget = selected_plot['widget_class'](self)
00172 self._data_plot_widget.limits_changed.connect(self.limits_changed)
00173 self._add_curve.connect(self._data_plot_widget.add_curve)
00174 self._layout.addWidget(self._data_plot_widget)
00175
00176
00177 for curve_id in self._curves:
00178 curve = self._curves[curve_id]
00179 self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00180
00181 if self._vline:
00182 self.vline(*self._vline)
00183
00184 self.set_xlim(x_limits)
00185 self.set_ylim(y_limits)
00186 self.redraw()
00187
00188 def _switch_plot_markers(self, markers_on):
00189 self._markers_on = markers_on
00190 self._data_plot_widget._color_index = 0
00191
00192 for curve_id in self._curves:
00193 self._data_plot_widget.remove_curve(curve_id)
00194 curve = self._curves[curve_id]
00195 self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00196
00197 self.redraw()
00198
00199
00200
00201 def getTitle(self):
00202 """get the title of the current plotting backend"""
00203 return self.plot_types[self._plot_index]['title']
00204
00205 def save_settings(self, plugin_settings, instance_settings):
00206 """Save the settings associated with this widget
00207
00208 Currently, this is just the plot type, but may include more useful
00209 data in the future"""
00210 instance_settings.set_value('plot_type', self._plot_index)
00211 xlim = self.get_xlim()
00212 ylim = self.get_ylim()
00213
00214
00215 xlim = [float(x) for x in xlim]
00216 ylim = [float(y) for y in ylim]
00217 instance_settings.set_value('x_limits', pack(xlim))
00218 instance_settings.set_value('y_limits', pack(ylim))
00219
00220 def restore_settings(self, plugin_settings, instance_settings):
00221 """Restore the settings for this widget
00222
00223 Currently, this just restores the plot type."""
00224 self._switch_data_plot_widget(int(instance_settings.value('plot_type', 0)))
00225 xlim = unpack(instance_settings.value('x_limits', []))
00226 ylim = unpack(instance_settings.value('y_limits', []))
00227 if xlim:
00228
00229
00230 try:
00231 xlim = [float(x) for x in xlim]
00232 self.set_xlim(xlim)
00233 except:
00234 qWarning("Failed to restore X limits")
00235 if ylim:
00236 try:
00237 ylim = [float(y) for y in ylim]
00238 self.set_ylim(ylim)
00239 except:
00240 qWarning("Failed to restore Y limits")
00241
00242
00243 def doSettingsDialog(self):
00244 """Present the user with a dialog for choosing the plot backend
00245
00246 This displays a SimpleSettingsDialog asking the user to choose a
00247 plot type, gets the result, and updates the plot type as necessary
00248
00249 This method is blocking"""
00250
00251 marker_settings = [
00252 {
00253 'title': 'Show Plot Markers',
00254 'description': 'Warning: Displaying markers in rqt_plot may cause\n \t high cpu load, especially using PyQtGraph\n',
00255 'enabled': True,
00256 }]
00257 if self._markers_on:
00258 selected_checkboxes = [0]
00259 else:
00260 selected_checkboxes = []
00261
00262 dialog = SimpleSettingsDialog(title='Plot Options')
00263 dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index)
00264 dialog.add_checkbox_group(title='Plot Markers', options=marker_settings, selected_indexes=selected_checkboxes)
00265 [plot_type, checkboxes] = dialog.get_settings()
00266 if plot_type is not None and plot_type['selected_index'] is not None and self._plot_index != plot_type['selected_index']:
00267 self._switch_data_plot_widget(plot_type['selected_index'], 0 in checkboxes['selected_indexes'])
00268 else:
00269 if checkboxes is not None and self._markers_on != (0 in checkboxes['selected_indexes']):
00270 self._switch_plot_markers(0 in checkboxes['selected_indexes'])
00271
00272
00273
00274 def autoscroll(self, enabled=True):
00275 """Enable or disable autoscrolling of the plot"""
00276 self._autoscroll = enabled
00277
00278 def redraw(self):
00279 self._redraw.emit()
00280
00281 def _do_redraw(self):
00282 """Redraw the underlying plot
00283
00284 This causes the underlying plot to be redrawn. This is usually used
00285 after adding or updating the plot data"""
00286 if self._data_plot_widget:
00287 self._merged_autoscale()
00288 for curve_id in self._curves:
00289 curve = self._curves[curve_id]
00290 self._data_plot_widget.set_values(curve_id, curve['x'], curve['y'])
00291 self._data_plot_widget.redraw()
00292
00293 def _get_curve(self, curve_id):
00294 if curve_id in self._curves:
00295 return self._curves[curve_id]
00296 else:
00297 raise DataPlotException("No curve named %s in this DataPlot" %
00298 ( curve_id) )
00299
00300 def add_curve(self, curve_id, curve_name, data_x, data_y):
00301 """Add a new, named curve to this plot
00302
00303 Add a curve named `curve_name` to the plot, with initial data series
00304 `data_x` and `data_y`.
00305
00306 Future references to this curve should use the provided `curve_id`
00307
00308 Note that the plot is not redraw automatically; call `redraw()` to make
00309 any changes visible to the user.
00310 """
00311 curve_color = QColor(self._colors[self._color_index % len(self._colors)])
00312 self._color_index += 1
00313
00314 self._curves[curve_id] = { 'x': numpy.array(data_x),
00315 'y': numpy.array(data_y),
00316 'name': curve_name,
00317 'color': curve_color}
00318 if self._data_plot_widget:
00319 self._add_curve.emit(curve_id, curve_name, curve_color, self._markers_on)
00320
00321 def remove_curve(self, curve_id):
00322 """Remove the specified curve from this plot"""
00323
00324 if curve_id in self._curves:
00325 del self._curves[curve_id]
00326 if self._data_plot_widget:
00327 self._data_plot_widget.remove_curve(curve_id)
00328
00329 def update_values(self, curve_id, values_x, values_y):
00330 """Append new data to an existing curve
00331
00332 `values_x` and `values_y` will be appended to the existing data for
00333 `curve_id`
00334
00335 Note that the plot is not redraw automatically; call `redraw()` to make
00336 any changes visible to the user.
00337 """
00338 curve = self._get_curve(curve_id)
00339 curve['x'] = numpy.append(curve['x'], values_x)
00340 curve['y'] = numpy.append(curve['y'], values_y)
00341
00342 sort_order = curve['x'].argsort()
00343 curve['x'] = curve['x'][sort_order]
00344 curve['y'] = curve['y'][sort_order]
00345
00346 def clear_values(self, curve_id=None):
00347 """Clear the values for the specified curve, or all curves
00348
00349 This will erase the data series associaed with `curve_id`, or all
00350 curves if `curve_id` is not present or is None
00351
00352 Note that the plot is not redraw automatically; call `redraw()` to make
00353 any changes visible to the user.
00354 """
00355
00356 if curve_id:
00357 curve = self._get_curve(curve_id)
00358 curve['x'] = numpy.array([])
00359 curve['y'] = numpy.array([])
00360 else:
00361 for curve_id in self._curves:
00362 self._curves[curve_id]['x'] = numpy.array([])
00363 self._curves[curve_id]['y'] = numpy.array([])
00364
00365
00366 def vline(self, x, color=RED):
00367 """Draw a vertical line on the plot
00368
00369 Draw a line a position X, with the given color
00370
00371 @param x: position of the vertical line to draw
00372 @param color: optional parameter specifying the color, as tuple of
00373 RGB values from 0 to 255
00374 """
00375 self._vline = (x, color)
00376 if self._data_plot_widget:
00377 self._data_plot_widget.vline(x, color)
00378
00379
00380 def set_autoscale(self, x=None, y=None):
00381 """Change autoscaling of plot axes
00382
00383 if a parameter is not passed, the autoscaling setting for that axis is
00384 not changed
00385
00386 @param x: enable or disable autoscaling for X
00387 @param y: set autoscaling mode for Y
00388 """
00389 if x is not None:
00390 self._autoscale_x = x
00391 if y is not None:
00392 self._autoscale_y = y
00393
00394
00395
00396
00397
00398
00399
00400
00401
00402
00403
00404
00405
00406
00407
00408
00409
00410 def _merged_autoscale(self):
00411 x_limit = [numpy.inf, -numpy.inf]
00412 if self._autoscale_x:
00413 for curve_id in self._curves:
00414 curve = self._curves[curve_id]
00415 if len(curve['x']) > 0:
00416 x_limit[0] = min(x_limit[0], curve['x'].min())
00417 x_limit[1] = max(x_limit[1], curve['x'].max())
00418 elif self._autoscroll:
00419
00420 x_limit = self.get_xlim()
00421 x_width = x_limit[1] - x_limit[0]
00422
00423
00424 x_limit[1] = -numpy.inf
00425
00426
00427 for curve_id in self._curves:
00428 curve = self._curves[curve_id]
00429 if len(curve['x']) > 0:
00430 x_limit[1] = max(x_limit[1], curve['x'].max())
00431
00432
00433 x_limit[0] = x_limit[1] - x_width
00434 else:
00435
00436 x_limit = self.get_xlim()
00437
00438
00439 if numpy.isinf(x_limit[0]):
00440 x_limit[0] = 0.0
00441 if numpy.isinf(x_limit[1]):
00442 x_limit[1] = 1.0
00443
00444
00445 y_limit = [numpy.inf, -numpy.inf]
00446 if self._autoscale_y:
00447
00448
00449 if self._autoscale_y & DataPlot.SCALE_EXTEND:
00450 y_limit = self.get_ylim()
00451 for curve_id in self._curves:
00452 curve = self._curves[curve_id]
00453 start_index = 0
00454 end_index = len(curve['x'])
00455
00456
00457
00458 if self._autoscale_y & DataPlot.SCALE_VISIBLE:
00459
00460 start_index = curve['x'].searchsorted(x_limit[0])
00461
00462 end_index = curve['x'].searchsorted(x_limit[1])
00463
00464
00465
00466 region = curve['y'][start_index:end_index]
00467 if len(region) > 0:
00468 y_limit[0] = min(y_limit[0], region.min())
00469 y_limit[1] = max(y_limit[1], region.max())
00470
00471
00472
00473
00474
00475
00476
00477
00478
00479
00480
00481
00482
00483 else:
00484 y_limit = self.get_ylim()
00485
00486
00487 if numpy.isinf(y_limit[0]):
00488 y_limit[0] = 0.0
00489 if numpy.isinf(y_limit[1]):
00490 y_limit[1] = 1.0
00491
00492 self.set_xlim(x_limit)
00493 self.set_ylim(y_limit)
00494
00495 def get_xlim(self):
00496 """get X limits"""
00497 if self._data_plot_widget:
00498 return self._data_plot_widget.get_xlim()
00499 else:
00500 qWarning("No plot widget; returning default X limits")
00501 return [0.0, 1.0]
00502
00503 def set_xlim(self, limits):
00504 """set X limits"""
00505 if self._data_plot_widget:
00506 self._data_plot_widget.set_xlim(limits)
00507 else:
00508 qWarning("No plot widget; can't set X limits")
00509
00510 def get_ylim(self):
00511 """get Y limits"""
00512 if self._data_plot_widget:
00513 return self._data_plot_widget.get_ylim()
00514 else:
00515 qWarning("No plot widget; returning default Y limits")
00516 return [0.0, 10.0]
00517
00518 def set_ylim(self, limits):
00519 """set Y limits"""
00520 if self._data_plot_widget:
00521 self._data_plot_widget.set_ylim(limits)
00522 else:
00523 qWarning("No plot widget; can't set Y limits")
00524
00525