diff --git a/package.xml b/package.xml index d5b20d9..a444632 100644 --- a/package.xml +++ b/package.xml @@ -17,9 +17,6 @@ Mabel Zhang Scott K Logan - qt-base-dev - libqtwidgets - ament_index_python python_qt_binding python3-matplotlib diff --git a/src/rqt_plot/data_plot/__init__.py b/src/rqt_plot/data_plot/__init__.py index 387dd44..4959041 100644 --- a/src/rqt_plot/data_plot/__init__.py +++ b/src/rqt_plot/data_plot/__init__.py @@ -29,7 +29,6 @@ import numpy -from python_qt_binding import QT_BINDING from python_qt_binding.QtCore import qWarning, Signal from python_qt_binding.QtGui import QColor, QColorConstants from python_qt_binding.QtWidgets import QHBoxLayout, QWidget @@ -46,11 +45,6 @@ except ImportError: MatDataPlot = None -try: - from .qwt_data_plot import QwtDataPlot -except ImportError: - 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 @@ -66,7 +60,7 @@ class DataPlot(QWidget): 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. + supports PyQtGraph and MatPlot backends. The DataPlot widget manages the plot backend internally, and can save and restore the internal state using `save_settings` and `restore_settings` @@ -93,14 +87,6 @@ class DataPlot(QWidget): 'PySide: PySide > 1.1.0\n', '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\n', - 'enabled': QwtDataPlot is not None, - }, ] # pre-defined colors: @@ -148,13 +134,9 @@ def __init__(self, parent=None): enabled_plot_types = [pt for pt in self.plot_types if pt['enabled']] if not enabled_plot_types: - # minimum matplotlib version for Qt 5 - version_info = '1.4.0' - if QT_BINDING == 'pyside': - version_info += ' and PySide 2.0.0' raise RuntimeError( - 'No usable plot type found. Install at least one of: PyQtGraph, MatPlotLib ' - f'(at least {version_info}) or Python-Qwt5.') + 'No usable plot type found. ' + 'Install at least one of: PyQtGraph, MatPlotLib.') self._switch_data_plot_widget(self._plot_index) diff --git a/src/rqt_plot/data_plot/mat_data_plot.py b/src/rqt_plot/data_plot/mat_data_plot.py index c8adbfb..8df322f 100644 --- a/src/rqt_plot/data_plot/mat_data_plot.py +++ b/src/rqt_plot/data_plot/mat_data_plot.py @@ -29,42 +29,18 @@ import operator import matplotlib -from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -from matplotlib.figure import Figure - -from python_qt_binding import QT_BINDING, QT_BINDING_VERSION, QtWidgets - -try: - from pkg_resources import parse_version -except ImportError: - import re - - def parse_version(s): - return [int(x) for x in re.sub(r'(\.0+)*$', '', s).split('.')] - -if QT_BINDING == 'pyside': - qt_binding_version = QT_BINDING_VERSION.replace('~', '-') - if parse_version(qt_binding_version) <= parse_version('1.1.2'): - raise ImportError('A PySide version newer than 1.1.0 is required.') - -from python_qt_binding.QtCore import Signal -from python_qt_binding.QtGui import QColorConstants -from python_qt_binding.QtWidgets import QVBoxLayout, QWidget - -if QT_BINDING == 'pyside': - if parse_version(matplotlib.__version__) < parse_version('2.1.0'): - raise ImportError('A newer matplotlib is required (at least 2.1.0 for PySide 2)') -if parse_version(matplotlib.__version__) < parse_version('1.4.0'): - raise ImportError('A newer matplotlib is required (at least 1.4.0 for Qt 5)') -try: - matplotlib.use('Qt5Agg') - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -except ImportError: - # work around bug in dateutil - import sys - import thread - sys.modules['_thread'] = thread - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +matplotlib.use('QtAgg') + +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas # noqa: E402 +from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar # noqa: E402,E501 +from matplotlib.figure import Figure # noqa: E402 + +from packaging.version import Version # noqa: E402 + +from python_qt_binding import QtWidgets # noqa: E402 +from python_qt_binding.QtCore import Signal # noqa: E402 +from python_qt_binding.QtGui import QColorConstants # noqa: E402 +from python_qt_binding.QtWidgets import QVBoxLayout, QWidget # noqa: E402 class MatDataPlot(QWidget): @@ -101,7 +77,7 @@ def safe_tight_layout(self): return self.figure.tight_layout() except ValueError: - if parse_version(matplotlib.__version__) >= parse_version('2.2.3'): + if Version(matplotlib.__version__) >= Version('2.2.3'): raise limits_changed = Signal() diff --git a/src/rqt_plot/data_plot/pyqtgraph_data_plot.py b/src/rqt_plot/data_plot/pyqtgraph_data_plot.py index 72cc72e..0800788 100644 --- a/src/rqt_plot/data_plot/pyqtgraph_data_plot.py +++ b/src/rqt_plot/data_plot/pyqtgraph_data_plot.py @@ -32,26 +32,6 @@ from python_qt_binding.QtGui import QColor, QColorConstants from python_qt_binding.QtWidgets import QVBoxLayout, QWidget -try: - from pkg_resources import parse_version -except ImportError: - import re - - def parse_version(s): - return [int(x) for x in re.sub(r'(\.0+)*$', '', s).split('.')] - -try: - from pyqtgraph import __version__ as pyqtgraph_version -except RuntimeError: - # pyqtgraph < 1.0 using Qt4 failing on 16.04 because kinetic uses Qt5. - # This raises RuntimeError('the PyQt4.QtCore and PyQt5.QtCore modules both - # wrap the QObject class') - import pkg_resources - pyqtgraph_version = pkg_resources.get_distribution('pyqtgraph').version - -if parse_version(pyqtgraph_version) < parse_version('0.10.0'): - raise ImportError('A newer PyQtGraph version is required (at least 0.10 for Qt 5)') - class PyQtGraphDataPlot(QWidget): diff --git a/src/rqt_plot/data_plot/qwt_data_plot.py b/src/rqt_plot/data_plot/qwt_data_plot.py deleted file mode 100644 index 38c1bfe..0000000 --- a/src/rqt_plot/data_plot/qwt_data_plot.py +++ /dev/null @@ -1,253 +0,0 @@ -# Copyright (c) 2011, Dorian Scholz, TU Darmstadt -# -# 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. - -# -*- coding: utf-8 -*- -import math -import sys - -from python_qt_binding.QtCore import QEvent, QPointF, QSize, Qt, qWarning, Signal -from python_qt_binding.QtGui import QBrush, QColorConstants, QPen, QVector2D -import Qwt - - -# create real QwtDataPlot class -class QwtDataPlot(Qwt.QwtPlot): - mouseCoordinatesChanged = Signal(QPointF) - _num_value_saved = 1000 - _num_values_ploted = 1000 - - limits_changed = Signal() - - def __init__(self, *args): - super().__init__(*args) - self.setCanvasBackground(QColorConstants.White) - self.insertLegend(Qwt.QwtLegend(), Qwt.QwtPlot.BottomLegend) - - self._curves = {} - - # TODO: rejigger these internal data structures so that they're easier - # to work with, and easier to use with set_xlim and set_ylim - # this may also entail rejiggering the _time_axis so that it's - # actually time axis data, or just isn't required any more - # new data structure - self._x_limits = [0.0, 10.0] - self._y_limits = [0.0, 10.0] - - # old data structures - self._last_canvas_x = 0 - self._last_canvas_y = 0 - self._pressed_canvas_y = 0 - self._pressed_canvas_x = 0 - self._last_click_coordinates = None - - marker_axis_y = Qwt.QwtPlotMarker() - marker_axis_y.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop) - marker_axis_y.setLineStyle(Qwt.QwtPlotMarker.HLine) - marker_axis_y.setYValue(0.0) - marker_axis_y.attach(self) - - self._picker = Qwt.QwtPlotPicker( - Qwt.QwtPlot.xBottom, Qwt.QwtPlot.yLeft, Qwt.QwtPicker.PolygonSelection, - Qwt.QwtPlotPicker.PolygonRubberBand, Qwt.QwtPicker.AlwaysOn, self.canvas() - ) - self._picker.setRubberBandPen(QPen(QColorConstants.Blue)) - self._picker.setTrackerPen(QPen(QColorConstants.Blue)) - - # Initialize data - self.rescale() - # self.move_canvas(0, 0) - self.canvas().setMouseTracking(True) - self.canvas().installEventFilter(self) - - def eventFilter(self, _, event): - if event.type() == QEvent.MouseButtonRelease: - x = self.invTransform(Qwt.QwtPlot.xBottom, event.pos().x()) - y = self.invTransform(Qwt.QwtPlot.yLeft, event.pos().y()) - self._last_click_coordinates = QPointF(x, y) - self.limits_changed.emit() - elif event.type() == QEvent.MouseMove: - x = self.invTransform(Qwt.QwtPlot.xBottom, event.pos().x()) - y = self.invTransform(Qwt.QwtPlot.yLeft, event.pos().y()) - coords = QPointF(x, y) - if self._picker.isActive() and self._last_click_coordinates is not None: - toolTip = 'origin x: %.5f, y: %.5f' % ( - self._last_click_coordinates.x(), self._last_click_coordinates.y()) - delta = coords - self._last_click_coordinates - toolTip += '\ndelta x: %.5f, y: %.5f\nlength: %.5f' % ( - delta.x(), delta.y(), QVector2D(delta).length()) - else: - toolTip = 'buttons\nleft: measure\nmiddle: move\nright: zoom x/y\nwheel: zoom y' - self.setToolTip(toolTip) - self.mouseCoordinatesChanged.emit(coords) - return False - - def log(self, level, message): - # self.emit(SIGNAL('logMessage'), level, message) - pass - - def resizeEvent(self, event): - Qwt.QwtPlot.resizeEvent(self, event) - self.rescale() - - def add_curve(self, curve_id, curve_name, curve_color=QColorConstants.Blue, markers_on=False): - curve_id = str(curve_id) - if curve_id in self._curves: - return - curve_object = Qwt.QwtPlotCurve(curve_name) - curve_object.attach(self) - curve_object.setPen(curve_color) - if markers_on: - curve_object.setSymbol( - Qwt.QwtSymbol( - Qwt.QwtSymbol.Ellipse, QBrush(curve_color), QPen(Qt.black), QSize(4, 4))) - self._curves[curve_id] = curve_object - - def remove_curve(self, curve_id): - curve_id = str(curve_id) - if curve_id in self._curves: - self._curves[curve_id].hide() - self._curves[curve_id].attach(None) - del self._curves[curve_id] - - def set_values(self, curve_id, data_x, data_y): - curve = self._curves[curve_id] - curve.setData(data_x, data_y) - - def redraw(self): - self.replot() - - # ---------------------------------------------- - # begin qwtplot internal methods - # ---------------------------------------------- - def rescale(self): - self.setAxisScale(Qwt.QwtPlot.yLeft, - self._y_limits[0], - self._y_limits[1]) - self.setAxisScale(Qwt.QwtPlot.xBottom, - self._x_limits[0], - self._x_limits[1]) - - self._canvas_display_height = self._y_limits[1] - self._y_limits[0] - self._canvas_display_width = self._x_limits[1] - self._x_limits[0] - self.redraw() - - def rescale_axis_x(self, delta__x): - """Add delta_x to the length of the x axis.""" - x_width = self._x_limits[1] - self._x_limits[0] - x_width += delta__x - self._x_limits[1] = x_width + self._x_limits[0] - self.rescale() - - def scale_axis_y(self, max_value): - """Set the y axis height to max_value, about the current center.""" - canvas_display_height = max_value - canvas_offset_y = (self._y_limits[1] + self._y_limits[0]) / 2.0 - y_lower_limit = canvas_offset_y - (canvas_display_height / 2) - y_upper_limit = canvas_offset_y + (canvas_display_height / 2) - self._y_limits = [y_lower_limit, y_upper_limit] - self.rescale() - - def move_canvas(self, delta_x, delta_y): - """Move the canvas by delta_x and delta_y in SCREEN COORDINATES.""" - canvas_offset_x = delta_x * self._canvas_display_width / self.canvas().width() - canvas_offset_y = delta_y * self._canvas_display_height / self.canvas().height() - self._x_limits = [limit + canvas_offset_x for limit in self._x_limits] - self._y_limits = [limit + canvas_offset_y for limit in self._y_limits] - self.rescale() - - def mousePressEvent(self, event): - self._last_canvas_x = event.x() - self.canvas().x() - self._last_canvas_y = event.y() - self.canvas().y() - self._pressed_canvas_y = event.y() - self.canvas().y() - - def mouseMoveEvent(self, event): - canvas_x = event.x() - self.canvas().x() - canvas_y = event.y() - self.canvas().y() - if event.buttons() & Qt.MiddleButton: # middle button moves the canvas - delta_x = self._last_canvas_x - canvas_x - delta_y = canvas_y - self._last_canvas_y - self.move_canvas(delta_x, delta_y) - elif event.buttons() & Qt.RightButton: # right button zooms - zoom_factor = max(-0.6, min(0.6, (self._last_canvas_y - canvas_y) / 20.0 / 2.0)) - delta_y = (self.canvas().height() / 2.0) - self._pressed_canvas_y - self.move_canvas(0, zoom_factor * delta_y * 1.0225) - self.scale_axis_y( - max(0.005, - self._canvas_display_height - (zoom_factor * self._canvas_display_height))) - self.rescale_axis_x(self._last_canvas_x - canvas_x) - self._last_canvas_x = canvas_x - self._last_canvas_y = canvas_y - - def wheelEvent(self, event): # mouse wheel zooms the y-axis - # y position of pointer in graph coordinates - canvas_y = event.y() - self.canvas().y() - - try: - delta = event.angleDelta().y() - except AttributeError: - delta = event.delta() - zoom_factor = max(-0.6, min(0.6, (delta / 120) / 6.0)) - delta_y = (self.canvas().height() / 2.0) - canvas_y - self.move_canvas(0, zoom_factor * delta_y * 1.0225) - - self.scale_axis_y( - max(0.0005, self._canvas_display_height - zoom_factor * self._canvas_display_height)) - self.limits_changed.emit() - - def vline(self, x, color): - qWarning('QwtDataPlot.vline is not implemented yet') - - def set_xlim(self, limits): - self.setAxisScale(Qwt.QwtPlot.xBottom, limits[0], limits[1]) - self._x_limits = limits - - def set_ylim(self, limits): - self.setAxisScale(Qwt.QwtPlot.yLeft, limits[0], limits[1]) - self._y_limits = limits - - def get_xlim(self): - return self._x_limits - - def get_ylim(self): - return self._y_limits - - -if __name__ == '__main__': - from python_qt_binding.QtGui import QApplication - - app = QApplication(sys.argv) - plot = QwtDataPlot() - plot.resize(700, 500) - plot.show() - plot.add_curve(0, '(x/500)^2') - plot.add_curve(1, 'sin(x / 20) * 500') - for i in range(plot._num_value_saved): - plot.update_value(0, (i / 500.0) * (i / 5.0)) - plot.update_value(1, math.sin(i / 20.0) * 500) - - sys.exit(app.exec()) diff --git a/src/rqt_plot/plot_widget.py b/src/rqt_plot/plot_widget.py index c7238ea..8eaf4a8 100644 --- a/src/rqt_plot/plot_widget.py +++ b/src/rqt_plot/plot_widget.py @@ -31,15 +31,9 @@ import time from ament_index_python.resources import get_resource -from packaging.version import Version from python_qt_binding import loadUi -from python_qt_binding import QT_BINDING_VERSION from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot -if Version(QT_BINDING_VERSION) >= Version('6.0.0'): - from python_qt_binding.QtGui import QAction -else: - from python_qt_binding.QtWidgets import QAction -from python_qt_binding.QtGui import QIcon +from python_qt_binding.QtGui import QAction, QIcon from python_qt_binding.QtWidgets import QMenu, QWidget from rosidl_parser.definition import AbstractGenericString