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