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 import QT_BINDING
00038 from python_qt_binding.QtCore import Qt, qVersion, qWarning, Signal
00039 from python_qt_binding.QtGui import QColor
00040 from python_qt_binding.QtWidgets import QWidget, QHBoxLayout
00041 from rqt_py_common.ini_helper import pack, unpack
00042
00043 try:
00044 from .pyqtgraph_data_plot import PyQtGraphDataPlot
00045 except ImportError as e:
00046 PyQtGraphDataPlot = None
00047
00048 try:
00049 from .mat_data_plot import MatDataPlot
00050 except ImportError as e:
00051 MatDataPlot = None
00052
00053 try:
00054 from .qwt_data_plot import QwtDataPlot
00055 except ImportError as e:
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 if qVersion().startswith('4.'):
00142 version_info = '1.1.0'
00143 else:
00144
00145 version_info = '1.4.0'
00146 if QT_BINDING == 'pyside':
00147 version_info += ' and PySide %s' % \
00148 ('> 1.1.0' if qVersion().startswith('4.') else '>= 2.0.0')
00149 raise RuntimeError('No usable plot type found. Install at least one of: PyQtGraph, MatPlotLib (at least %s) or Python-Qwt5.' % version_info)
00150
00151 self._switch_data_plot_widget(self._plot_index)
00152
00153 self.show()
00154
00155 def _switch_data_plot_widget(self, plot_index, markers_on=False):
00156 """Internal method for activating a plotting backend by index"""
00157
00158 if not self.plot_types[plot_index]['enabled']:
00159
00160 for index, plot_type in enumerate(self.plot_types):
00161 if plot_type['enabled']:
00162 plot_index = index
00163 break
00164
00165 self._plot_index = plot_index
00166 self._markers_on = markers_on
00167 selected_plot = self.plot_types[plot_index]
00168
00169 if self._data_plot_widget:
00170 x_limits = self.get_xlim()
00171 y_limits = self.get_ylim()
00172
00173 self._layout.removeWidget(self._data_plot_widget)
00174 self._data_plot_widget.close()
00175 self._data_plot_widget = None
00176 else:
00177 x_limits = [0.0, 10.0]
00178 y_limits = [-0.001, 0.001]
00179
00180 self._data_plot_widget = selected_plot['widget_class'](self)
00181 self._data_plot_widget.limits_changed.connect(self.limits_changed)
00182 self._add_curve.connect(self._data_plot_widget.add_curve)
00183 self._layout.addWidget(self._data_plot_widget)
00184
00185
00186 for curve_id in self._curves:
00187 curve = self._curves[curve_id]
00188 self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00189
00190 if self._vline:
00191 self.vline(*self._vline)
00192
00193 self.set_xlim(x_limits)
00194 self.set_ylim(y_limits)
00195 self.redraw()
00196
00197 def _switch_plot_markers(self, markers_on):
00198 self._markers_on = markers_on
00199 self._data_plot_widget._color_index = 0
00200
00201 for curve_id in self._curves:
00202 self._data_plot_widget.remove_curve(curve_id)
00203 curve = self._curves[curve_id]
00204 self._data_plot_widget.add_curve(curve_id, curve['name'], curve['color'], markers_on)
00205
00206 self.redraw()
00207
00208
00209
00210 def getTitle(self):
00211 """get the title of the current plotting backend"""
00212 return self.plot_types[self._plot_index]['title']
00213
00214 def save_settings(self, plugin_settings, instance_settings):
00215 """Save the settings associated with this widget
00216
00217 Currently, this is just the plot type, but may include more useful
00218 data in the future"""
00219 instance_settings.set_value('plot_type', self._plot_index)
00220 xlim = self.get_xlim()
00221 ylim = self.get_ylim()
00222
00223
00224 xlim = [float(x) for x in xlim]
00225 ylim = [float(y) for y in ylim]
00226 instance_settings.set_value('x_limits', pack(xlim))
00227 instance_settings.set_value('y_limits', pack(ylim))
00228
00229 def restore_settings(self, plugin_settings, instance_settings):
00230 """Restore the settings for this widget
00231
00232 Currently, this just restores the plot type."""
00233 self._switch_data_plot_widget(int(instance_settings.value('plot_type', 0)))
00234 xlim = unpack(instance_settings.value('x_limits', []))
00235 ylim = unpack(instance_settings.value('y_limits', []))
00236 if xlim:
00237
00238
00239 try:
00240 xlim = [float(x) for x in xlim]
00241 self.set_xlim(xlim)
00242 except:
00243 qWarning("Failed to restore X limits")
00244 if ylim:
00245 try:
00246 ylim = [float(y) for y in ylim]
00247 self.set_ylim(ylim)
00248 except:
00249 qWarning("Failed to restore Y limits")
00250
00251
00252 def doSettingsDialog(self):
00253 """Present the user with a dialog for choosing the plot backend
00254
00255 This displays a SimpleSettingsDialog asking the user to choose a
00256 plot type, gets the result, and updates the plot type as necessary
00257
00258 This method is blocking"""
00259
00260 marker_settings = [
00261 {
00262 'title': 'Show Plot Markers',
00263 'description': 'Warning: Displaying markers in rqt_plot may cause\n \t high cpu load, especially using PyQtGraph\n',
00264 'enabled': True,
00265 }]
00266 if self._markers_on:
00267 selected_checkboxes = [0]
00268 else:
00269 selected_checkboxes = []
00270
00271 dialog = SimpleSettingsDialog(title='Plot Options')
00272 dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index)
00273 dialog.add_checkbox_group(title='Plot Markers', options=marker_settings, selected_indexes=selected_checkboxes)
00274 [plot_type, checkboxes] = dialog.get_settings()
00275 if plot_type is not None and plot_type['selected_index'] is not None and self._plot_index != plot_type['selected_index']:
00276 self._switch_data_plot_widget(plot_type['selected_index'], 0 in checkboxes['selected_indexes'])
00277 else:
00278 if checkboxes is not None and self._markers_on != (0 in checkboxes['selected_indexes']):
00279 self._switch_plot_markers(0 in checkboxes['selected_indexes'])
00280
00281
00282
00283 def autoscroll(self, enabled=True):
00284 """Enable or disable autoscrolling of the plot"""
00285 self._autoscroll = enabled
00286
00287 def redraw(self):
00288 self._redraw.emit()
00289
00290 def _do_redraw(self):
00291 """Redraw the underlying plot
00292
00293 This causes the underlying plot to be redrawn. This is usually used
00294 after adding or updating the plot data"""
00295 if self._data_plot_widget:
00296 self._merged_autoscale()
00297 for curve_id in self._curves:
00298 curve = self._curves[curve_id]
00299 self._data_plot_widget.set_values(curve_id, curve['x'], curve['y'])
00300 self._data_plot_widget.redraw()
00301
00302 def _get_curve(self, curve_id):
00303 if curve_id in self._curves:
00304 return self._curves[curve_id]
00305 else:
00306 raise DataPlotException("No curve named %s in this DataPlot" %
00307 ( curve_id) )
00308
00309 def add_curve(self, curve_id, curve_name, data_x, data_y):
00310 """Add a new, named curve to this plot
00311
00312 Add a curve named `curve_name` to the plot, with initial data series
00313 `data_x` and `data_y`.
00314
00315 Future references to this curve should use the provided `curve_id`
00316
00317 Note that the plot is not redraw automatically; call `redraw()` to make
00318 any changes visible to the user.
00319 """
00320 curve_color = QColor(self._colors[self._color_index % len(self._colors)])
00321 self._color_index += 1
00322
00323 self._curves[curve_id] = { 'x': numpy.array(data_x),
00324 'y': numpy.array(data_y),
00325 'name': curve_name,
00326 'color': curve_color}
00327 if self._data_plot_widget:
00328 self._add_curve.emit(curve_id, curve_name, curve_color, self._markers_on)
00329
00330 def remove_curve(self, curve_id):
00331 """Remove the specified curve from this plot"""
00332
00333 if curve_id in self._curves:
00334 del self._curves[curve_id]
00335 if self._data_plot_widget:
00336 self._data_plot_widget.remove_curve(curve_id)
00337
00338 def update_values(self, curve_id, values_x, values_y, sort_data=True):
00339 """Append new data to an existing curve
00340
00341 `values_x` and `values_y` will be appended to the existing data for
00342 `curve_id`
00343
00344 Note that the plot is not redraw automatically; call `redraw()` to make
00345 any changes visible to the user.
00346
00347 If `sort_data` is set to False, values won't be sorted by `values_x`
00348 order.
00349 """
00350 curve = self._get_curve(curve_id)
00351 curve['x'] = numpy.append(curve['x'], values_x)
00352 curve['y'] = numpy.append(curve['y'], values_y)
00353
00354 if sort_data:
00355
00356 sort_order = curve['x'].argsort()
00357 curve['x'] = curve['x'][sort_order]
00358 curve['y'] = curve['y'][sort_order]
00359
00360 def clear_values(self, curve_id=None):
00361 """Clear the values for the specified curve, or all curves
00362
00363 This will erase the data series associaed with `curve_id`, or all
00364 curves if `curve_id` is not present or is None
00365
00366 Note that the plot is not redraw automatically; call `redraw()` to make
00367 any changes visible to the user.
00368 """
00369
00370 if curve_id:
00371 curve = self._get_curve(curve_id)
00372 curve['x'] = numpy.array([])
00373 curve['y'] = numpy.array([])
00374 else:
00375 for curve_id in self._curves:
00376 self._curves[curve_id]['x'] = numpy.array([])
00377 self._curves[curve_id]['y'] = numpy.array([])
00378
00379
00380 def vline(self, x, color=RED):
00381 """Draw a vertical line on the plot
00382
00383 Draw a line a position X, with the given color
00384
00385 @param x: position of the vertical line to draw
00386 @param color: optional parameter specifying the color, as tuple of
00387 RGB values from 0 to 255
00388 """
00389 self._vline = (x, color)
00390 if self._data_plot_widget:
00391 self._data_plot_widget.vline(x, color)
00392
00393
00394 def set_autoscale(self, x=None, y=None):
00395 """Change autoscaling of plot axes
00396
00397 if a parameter is not passed, the autoscaling setting for that axis is
00398 not changed
00399
00400 @param x: enable or disable autoscaling for X
00401 @param y: set autoscaling mode for Y
00402 """
00403 if x is not None:
00404 self._autoscale_x = x
00405 if y is not None:
00406 self._autoscale_y = y
00407
00408
00409
00410
00411
00412
00413
00414
00415
00416
00417
00418
00419
00420
00421
00422
00423
00424 def _merged_autoscale(self):
00425 x_limit = [numpy.inf, -numpy.inf]
00426 if self._autoscale_x:
00427 for curve_id in self._curves:
00428 curve = self._curves[curve_id]
00429 if len(curve['x']) > 0:
00430 x_limit[0] = min(x_limit[0], curve['x'].min())
00431 x_limit[1] = max(x_limit[1], curve['x'].max())
00432 elif self._autoscroll:
00433
00434 x_limit = self.get_xlim()
00435 x_width = x_limit[1] - x_limit[0]
00436
00437
00438 x_limit[1] = -numpy.inf
00439
00440
00441 for curve_id in self._curves:
00442 curve = self._curves[curve_id]
00443 if len(curve['x']) > 0:
00444 x_limit[1] = max(x_limit[1], curve['x'].max())
00445
00446
00447 x_limit[0] = x_limit[1] - x_width
00448 else:
00449
00450 x_limit = self.get_xlim()
00451
00452
00453 if numpy.isinf(x_limit[0]):
00454 x_limit[0] = 0.0
00455 if numpy.isinf(x_limit[1]):
00456 x_limit[1] = 1.0
00457
00458
00459 y_limit = [numpy.inf, -numpy.inf]
00460 if self._autoscale_y:
00461
00462
00463 if self._autoscale_y & DataPlot.SCALE_EXTEND:
00464 y_limit = self.get_ylim()
00465 for curve_id in self._curves:
00466 curve = self._curves[curve_id]
00467 start_index = 0
00468 end_index = len(curve['x'])
00469
00470
00471
00472 if self._autoscale_y & DataPlot.SCALE_VISIBLE:
00473
00474 start_index = curve['x'].searchsorted(x_limit[0])
00475
00476 end_index = curve['x'].searchsorted(x_limit[1])
00477
00478
00479
00480 region = curve['y'][start_index:end_index]
00481 if len(region) > 0:
00482 y_limit[0] = min(y_limit[0], region.min())
00483 y_limit[1] = max(y_limit[1], region.max())
00484
00485
00486
00487
00488
00489
00490
00491
00492
00493
00494
00495
00496
00497 else:
00498 y_limit = self.get_ylim()
00499
00500
00501 if numpy.isinf(y_limit[0]):
00502 y_limit[0] = 0.0
00503 if numpy.isinf(y_limit[1]):
00504 y_limit[1] = 1.0
00505
00506 self.set_xlim(x_limit)
00507 self.set_ylim(y_limit)
00508
00509 def get_xlim(self):
00510 """get X limits"""
00511 if self._data_plot_widget:
00512 return self._data_plot_widget.get_xlim()
00513 else:
00514 qWarning("No plot widget; returning default X limits")
00515 return [0.0, 1.0]
00516
00517 def set_xlim(self, limits):
00518 """set X limits"""
00519 if self._data_plot_widget:
00520 self._data_plot_widget.set_xlim(limits)
00521 else:
00522 qWarning("No plot widget; can't set X limits")
00523
00524 def get_ylim(self):
00525 """get Y limits"""
00526 if self._data_plot_widget:
00527 return self._data_plot_widget.get_ylim()
00528 else:
00529 qWarning("No plot widget; returning default Y limits")
00530 return [0.0, 10.0]
00531
00532 def set_ylim(self, limits):
00533 """set Y limits"""
00534 if self._data_plot_widget:
00535 self._data_plot_widget.set_ylim(limits)
00536 else:
00537 qWarning("No plot widget; can't set Y limits")
00538
00539