Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document rqt_plot backend as public API #236

Merged
merged 6 commits into from
Jun 9, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rqt_bag/src/rqt_bag/timeline_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, popup_name):
self.setLayout(layout)
self.resize(640, 480)
self.setObjectName(popup_name)
self.setWindowTitle(popup_name)

class TimelinePopupMenu(QMenu):
"""
Expand Down
266 changes: 266 additions & 0 deletions rqt_plot/src/rqt_plot/data_plot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
#!/usr/bin/env python

# Copyright (c) 2014, Austin Hendrix
# Copyright (c) 2011, Dorian Scholz, TU Darmstadt
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the TU Darmstadt nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


from qt_gui_py_common.simple_settings_dialog import SimpleSettingsDialog
from python_qt_binding.QtGui import QWidget, QHBoxLayout

try:
from pyqtgraph_data_plot import PyQtGraphDataPlot
except ImportError:
qDebug('[DEBUG] rqt_plot.plot: import of PyQtGraphDataPlot failed (trying other backends)')
PyQtGraphDataPlot = None

try:
from mat_data_plot import MatDataPlot
except ImportError:
qDebug('[DEBUG] rqt_plot.plot: import of MatDataPlot failed (trying other backends)')
MatDataPlot = None

try:
from qwt_data_plot import QwtDataPlot
except ImportError:
qDebug('[DEBUG] rqt_plot.plot: import of QwtDataPlot failed (trying other backends)')
QwtDataPlot = None

# separate class for DataPlot exceptions, just so that users can differentiate
# errors from the DataPlot widget from exceptions generated by the underlying
# libraries
class DataPlotException(Exception):
pass

class DataPlot(QWidget):
"""A widget for displaying a plot of data

The DataPlot widget displays a plot, on one of several plotting backends,
depending on which backend(s) are available at runtime. It currently
supports PyQtGraph, MatPlot and QwtPlot backends.

The DataPlot widget manages the plot backend internally, and can save
and restore the internal state using `save_settings` and `restore_settings`
functions.

Currently, the user MUST call `restore_settings` before using the widget,
to cause the creation of the enclosed plotting widget.
"""
# plot types in order of priority
plot_types = [
{
'title': 'PyQtGraph',
'widget_class': PyQtGraphDataPlot,
'description': 'Based on PyQtGraph\n- installer: http://luke.campagnola.me/code/pyqtgraph',
'enabled': PyQtGraphDataPlot is not None,
},
{
'title': 'MatPlot',
'widget_class': MatDataPlot,
'description': 'Based on MatPlotLib\n- needs most CPU\n- needs matplotlib >= 1.1.0\n- if using PySide: PySide > 1.1.0',
'enabled': MatDataPlot is not None,
},
{
'title': 'QwtPlot',
'widget_class': QwtDataPlot,
'description': 'Based on QwtPlot\n- does not use timestamps\n- uses least CPU\n- needs Python Qwt bindings',
'enabled': QwtDataPlot is not None,
},
]

def __init__(self, parent=None):
"""Create a new, empty DataPlot

This will raise a RuntimeError if none of the supported plotting
backends can be found
"""
super(DataPlot, self).__init__(parent)
self._plot_index = 0
self._autoscroll = True

# the backend widget that we're trying to hide/abstract
self._data_plot_widget = None
self._curves = {}

self._layout = QHBoxLayout()
self.setLayout(self._layout)

enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']]
if not enabled_plot_types:
version_info = ' and PySide > 1.1.0' if QT_BINDING == 'pyside' else ''
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)

self.show()

def _switch_data_plot_widget(self, plot_index):
"""Internal method for activating a plotting backend by index"""
# check if selected plot type is available
if not self.plot_types[plot_index]['enabled']:
# find other available plot type
for index, plot_type in enumerate(self.plot_types):
if plot_type['enabled']:
plot_index = index
break

self._plot_index = plot_index
selected_plot = self.plot_types[plot_index]

print "Creating new plot widget: %s" % ( self.getTitle() )

if self._data_plot_widget:
self._layout.removeWidget(self._data_plot_widget)
self._data_plot_widget.close()
self._data_plot_widget = None

self._data_plot_widget = selected_plot['widget_class'](self)
self._data_plot_widget.autoscroll(self._autoscroll)
self._layout.addWidget(self._data_plot_widget)

# restore old data
for curve_id in self._curves:
curve = self._curves[curve_id]
self._data_plot_widget.add_curve(curve_id, curve['name'],
curve['x'], curve['y'])
self._data_plot_widget.redraw()

# interface out to the managing GUI component: get title, save, restore,
# etc
def getTitle(self):
"""get the title of the current plotting backend"""
return self.plot_types[self._plot_index]['title']

def save_settings(self, plugin_settings, instance_settings):
"""Save the settings associated with this widget

Currently, this is just the plot type, but may include more useful
data in the future"""
instance_settings.set_value('plot_type', self._plot_index)

def restore_settings(self, plugin_settings, instance_settings):
"""Restore the settings for this widget

Currently, this just restores the plot type."""
self._switch_data_plot_widget(int(instance_settings.value('plot_type', 0)))

def doSettingsDialog(self):
"""Present the user with a dialog for choosing the plot backend

This displays a SimpleSettingsDialog asking the user to choose a
plot type, gets the result, and updates the plot type as necessary

This method is blocking"""
dialog = SimpleSettingsDialog(title='Plot Options')
dialog.add_exclusive_option_group(title='Plot Type', options=self.plot_types, selected_index=self._plot_index)
plot_type = dialog.get_settings()[0]
if plot_type is not None and plot_type['selected_index'] is not None and self._plot_index != plot_type['selected_index']:
self._switch_data_plot_widget(plot_type['selected_index'])

# interface out to the managing DATA component: load data, update data,
# etc
def autoscroll(self, enabled=True):
"""Enable or disable autoscrolling of the plot"""
self._autoscroll = enabled
if self._data_plot_widget:
self._data_plot_widget.autoscroll(enabled)

def redraw(self):
"""Redraw the underlying plot

This causes the underlying plot to be redrawn. This is usually used
after adding or updating the plot data"""
if self._data_plot_widget:
self._data_plot_widget.redraw()

def _get_curve(self, curve_id):
if curve_id in self._curves:
return self._curves[curve_id]
else:
raise DataPlotException("No curve named %s in this DataPlot" %
( curve_id) )

def add_curve(self, curve_id, curve_name, data_x, data_y):
"""Add a new, named curve to this plot

Add a curve named `curve_name` to the plot, with initial data series
`data_x` and `data_y`.

Future references to this curve should use the provided `curve_id`

Note that the plot is not redraw automatically; call `redraw()` to make
any changes visible to the user.
"""
self._curves[curve_id] = { 'x': data_x, 'y': data_y, 'name': curve_name }
if self._data_plot_widget:
self._data_plot_widget.add_curve(curve_id, curve_name, data_x, data_y)

def remove_curve(self, curve_id):
"""Remove the specified curve from this plot"""
if curve_id in self._curves:
del self._curves[curve_id]
if self._data_plot_widget:
self._data_plot_widget.remove_curve(curve_id)

def update_values(self, curve_id, values_x, values_y):
"""Append new data to an existing curve

`values_x` and `values_y` will be appended to the existing data for
`curve_id`

Note that the plot is not redraw automatically; call `redraw()` to make
any changes visible to the user.
"""
curve = self._get_curve(curve_id)
curve['x'].extend(values_x)
curve['y'].extend(values_y)
if self._data_plot_widget:
self._data_plot_widget.update_values(curve_id, values_x, values_y)

def clear_values(self, curve_id=None):
"""Clear the values for the specified curve, or all curves

This will erase the data series associaed with `curve_id`, or all
curves if `curve_id` is not present or is None

Note that the plot is not redraw automatically; call `redraw()` to make
any changes visible to the user.
"""
# clear internal curve representation
if curve_id:
curve = self._check_curve_exists(curve_id)
curve['x'] = []
curve['y'] = []
if self._data_plot_widget:
self._data_plot_widget.clear_values(curve_id)
else:
for curve_id in self._curves:
self._curves[curve_id]['x'] = []
self._curves[curve_id]['y'] = []
if self._data_plot_widget:
self._data_plot_widget.clear_values(curve_id)
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ def update_values(self, curve_id, x, y):
ymax = max(ymax, range_y[1])
range_y[1] = ymax

def clear_values(self, curve_id):
data_x, data_y, _, range_y = self._curves[curve_id]
del data_x[:]
del data_y[:]
range_y[0] = None
range_y[1] = None

def redraw(self):
self._canvas.axes.grid(True, color='gray')
# Set axis bounds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def update_values(self, curve_id, x, y):
curve['x'] = numpy.append(curve['x'], x)
curve['y'] = numpy.append(curve['y'], y)

def clear_values(self, curve_id):
curve = self._curves[curve_id]
curve['x'] = numpy.array([])
curve['y'] = numpy.array([])

def autoscroll(self, enabled=True):
self._autoscroll = enabled

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def add_curve(self, curve_id, curve_name, values_x, values_y):
'data': zeros(self._num_value_saved),
'object': curve_object,
}
self.update_values(curve_id, values_x, values_y)

def remove_curve(self, curve_id):
curve_id = str(curve_id)
Expand All @@ -154,6 +155,11 @@ def update_value(self, curve_id, value_x, value_y):
self._curves[curve_id]['data'] = concatenate((self._curves[curve_id]['data'][1:], self._curves[curve_id]['data'][:1]), 1)
self._curves[curve_id]['data'][-1] = float(value_y)

def clear_values(self, curve_id):
curve_id = str(curve_id)
curve = self._curves[curve_id]
curve['data'] = zeros(self._num_value_saved)

def redraw(self):
for curve_id in self._curves.keys():
self._curves[curve_id]['object'].setData(self._time_axis, self._curves[curve_id]['data'][self._data_offset_x: self._data_offset_x + len(self._time_axis)])
Expand Down
Loading