diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index 7f07490abd..d895191d86 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -1,5 +1,6 @@ #include "tools/cabana/chartswidget.h" +#include #include #include #include @@ -183,7 +184,7 @@ void ChartsWidget::settingChanged() { range_slider->setRange(1, settings.max_cached_minutes * 60); for (auto c : charts) { c->setFixedHeight(settings.chart_height); - c->setSeriesType(settings.chart_series_type == 0 ? QAbstractSeries::SeriesTypeLine : QAbstractSeries::SeriesTypeScatter); + c->setSeriesType((SeriesType)settings.chart_series_type); } } @@ -309,7 +310,7 @@ bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { // ChartView ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { - series_type = settings.chart_series_type == 0 ? QAbstractSeries::SeriesTypeLine : QAbstractSeries::SeriesTypeScatter; + series_type = (SeriesType)settings.chart_series_type; QChart *chart = new QChart(); chart->setBackgroundVisible(false); axis_x = new QValueAxis(this); @@ -325,40 +326,54 @@ ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { background->setPen(Qt::NoPen); background->setZValue(chart->zValue() - 1); - move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart); + setChart(chart); + + createToolButtons(); + setRenderHint(QPainter::Antialiasing); + // TODO: enable zoomIn/seekTo in live streaming mode. + setRubberBand(can->liveStreaming() ? QChartView::NoRubberBand : QChartView::HorizontalRubberBand); + + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); + QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); + QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); + QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); +} + +void ChartView::createToolButtons() { + move_icon = new QGraphicsPixmapItem(utils::icon("grip-horizontal"), chart()); move_icon->setToolTip(tr("Drag and drop to combine charts")); QToolButton *remove_btn = toolButton("x", tr("Remove Chart")); - close_btn_proxy = new QGraphicsProxyWidget(chart); + close_btn_proxy = new QGraphicsProxyWidget(chart()); close_btn_proxy->setWidget(remove_btn); - close_btn_proxy->setZValue(chart->zValue() + 11); + close_btn_proxy->setZValue(chart()->zValue() + 11); - QToolButton *manage_btn = toolButton("list", ""); + // series types QMenu *menu = new QMenu(this); - line_series_action = menu->addAction(tr("Line"), [this]() { setSeriesType(QAbstractSeries::SeriesTypeLine); }); - line_series_action->setCheckable(true); - line_series_action->setChecked(series_type == QAbstractSeries::SeriesTypeLine); - scatter_series_action = menu->addAction(tr("Scatter"), [this]() { setSeriesType(QAbstractSeries::SeriesTypeScatter); }); - scatter_series_action->setCheckable(true); - scatter_series_action->setChecked(series_type == QAbstractSeries::SeriesTypeScatter); + auto change_series_group = new QActionGroup(menu); + change_series_group->setExclusive(true); + QStringList types{tr("line"), tr("Step Line"), tr("Scatter")}; + for (int i = 0; i < types.size(); ++i) { + QAction *act = new QAction(types[i], change_series_group); + act->setData(i); + act->setCheckable(true); + act->setChecked(i == (int)series_type); + menu->addAction(act); + } menu->addSeparator(); menu->addAction(tr("Manage series"), this, &ChartView::manageSeries); + + QToolButton *manage_btn = toolButton("list", ""); manage_btn->setMenu(menu); manage_btn->setPopupMode(QToolButton::InstantPopup); - manage_btn_proxy = new QGraphicsProxyWidget(chart); + manage_btn_proxy = new QGraphicsProxyWidget(chart()); manage_btn_proxy->setWidget(manage_btn); - manage_btn_proxy->setZValue(chart->zValue() + 11); + manage_btn_proxy->setZValue(chart()->zValue() + 11); - setChart(chart); - setRenderHint(QPainter::Antialiasing); - // TODO: enable zoomIn/seekTo in live streaming mode. - setRubberBand(can->liveStreaming() ? QChartView::NoRubberBand : QChartView::HorizontalRubberBand); - - QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated); QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove); + QObject::connect(change_series_group, &QActionGroup::triggered, [this](QAction *action) { + setSeriesType((SeriesType)action->data().toInt()); + }); } void ChartView::addSeries(const MessageId &msg_id, const Signal *sig) { @@ -476,7 +491,7 @@ void ChartView::updateSeriesPoints() { int num_points = std::max(end - begin, 1); int pixels_per_point = width() / num_points; - if (series_type == QAbstractSeries::SeriesTypeScatter) { + if (series_type == SeriesType::Scatter) { ((QScatterSeries *)s.series)->setMarkerSize(std::clamp(pixels_per_point / 3, 2, 8)); } else { s.series->setPointsVisible(pixels_per_point > 20); @@ -490,7 +505,9 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even if (!sig || s.sig == sig) { if (clear) { s.vals.clear(); + s.step_vals.clear(); s.vals.reserve(settings.max_cached_minutes * 60 * 100); // [n]seconds * 100hz + s.step_vals.reserve(settings.max_cached_minutes * 60 * 100 * 2); s.last_value_mono_time = 0; } s.series->setColor(getColor(s.sig)); @@ -498,6 +515,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even struct Chunk { std::vector::const_iterator first, second; QVector vals; + QVector step_vals; }; // split into one minitue chunks QVector chunks; @@ -510,6 +528,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even QtConcurrent::blockingMap(chunks, [&](Chunk &chunk) { chunk.vals.reserve(60 * 100); // 100 hz + chunk.step_vals.reserve(60 * 100 * 2); // 100 hz double route_start_time = can->routeStartTime(); for (auto it = chunk.first; it != chunk.second; ++it) { if ((*it)->which == cereal::Event::Which::CAN) { @@ -519,6 +538,10 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig); double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds chunk.vals.push_back({ts, value}); + if (!chunk.step_vals.empty()) { + chunk.step_vals.push_back({ts, chunk.step_vals.back().y()}); + } + chunk.step_vals.push_back({ts,value}); } } } @@ -526,11 +549,12 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even }); for (auto &c : chunks) { s.vals.append(c.vals); + s.step_vals.append(c.step_vals); } if (events->size()) { s.last_value_mono_time = events->back()->mono_time; } - s.series->replace(s.vals); + s.series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); } } updateAxisY(); @@ -607,7 +631,7 @@ qreal ChartView::niceNumber(qreal x, bool ceiling) { } void ChartView::leaveEvent(QEvent *event) { - track_pts.clear(); + clearTrackPoints(); scene()->update(); QChartView::leaveEvent(event); } @@ -663,26 +687,31 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) { auto rubber = findChild(); bool is_zooming = rubber && rubber->isVisible(); const auto plot_area = chart()->plotArea(); - track_pts.clear(); + clearTrackPoints(); + if (!is_zooming && plot_area.contains(ev->pos())) { - track_pts.resize(sigs.size()); QStringList text_list; const double sec = chart()->mapToValue(ev->pos()).x(); - for (int i = 0; i < sigs.size(); ++i) { - QString value = "--"; + qreal x = -1; + for (auto &s : sigs) { + if (!s.series->isVisible()) continue; + // use reverse iterator to find last item <= sec. - auto it = std::lower_bound(sigs[i].vals.rbegin(), sigs[i].vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); - if (it != sigs[i].vals.rend() && it->x() >= axis_x->min()) { - value = QString::number(it->y()); - track_pts[i] = chart()->mapToPosition(*it); + auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); + if (it != s.vals.rend() && it->x() >= axis_x->min()) { + s.track_pt = chart()->mapToPosition(*it); + x = std::max(x, s.track_pt.x()); } - text_list.push_back(QString("%2: %3").arg(sigs[i].series->color().name(), sigs[i].sig->name, value)); + text_list.push_back(QString("%2: %3") + .arg(s.series->color().name(), s.sig->name, + s.track_pt.isNull() ? "--" : QString::number(s.track_pt.y()))); + } + if (x < 0) { + x = ev->pos().x(); } - auto max = std::max_element(track_pts.begin(), track_pts.end(), [](auto &a, auto &b) { return a.x() < b.x(); }); - auto pt = (max == track_pts.end()) ? ev->pos() : *max; - text_list.push_front(QString::number(chart()->mapToValue(pt).x(), 'f', 3)); - QPointF tooltip_pt(pt.x() + 12, plot_area.top() - 20); - QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), pt.isNull() ? "" : text_list.join("
"), this, plot_area.toRect()); + text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3)); + QPointF tooltip_pt(x + 12, plot_area.top() - 20); + QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), text_list.join("
"), this, plot_area.toRect()); scene()->invalidate({}, QGraphicsScene::ForegroundLayer); } else { QToolTip::hideText(); @@ -727,6 +756,7 @@ void ChartView::dropEvent(QDropEvent *event) { } void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { + // draw time line qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x(); x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right()); qreal y1 = chart()->plotArea().top() - 2; @@ -734,18 +764,20 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { painter->setPen(QPen(chart()->titleBrush().color(), 2)); painter->drawLine(QPointF{x, y1}, QPointF{x, y2}); - auto max = std::max_element(track_pts.begin(), track_pts.end(), [](auto &a, auto &b) { return a.x() < b.x(); }); - if (max != track_pts.end() && !max->isNull()) { - painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{max->x(), y1}, QPointF{max->x(), y2}); - painter->setPen(Qt::NoPen); - for (int i = 0; i < track_pts.size(); ++i) { - if (!track_pts[i].isNull() && i < sigs.size()) { - painter->setBrush(sigs[i].series->color().darker(125)); - painter->drawEllipse(track_pts[i], 5.5, 5.5); - } + // draw track points + painter->setPen(Qt::NoPen); + qreal track_line_x = -1; + for (auto &s : sigs) { + if (!s.track_pt.isNull() && s.series->isVisible()) { + painter->setBrush(s.series->color().darker(125)); + painter->drawEllipse(s.track_pt, 5.5, 5.5); + track_line_x = std::max(track_line_x, s.track_pt.x()); } } + if (track_line_x > 0) { + painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); + painter->drawLine(QPointF{track_line_x, y1}, QPointF{track_line_x, y2}); + } // paint points. OpenGL mode lacks certain features (such as showing points) painter->setPen(Qt::NoPen); @@ -761,11 +793,14 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { } } -QXYSeries *ChartView::createSeries(QAbstractSeries::SeriesType type, QColor color) { +QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { QXYSeries *series = nullptr; - if (type == QAbstractSeries::SeriesTypeLine) { + if (type == SeriesType::Line) { series = new QLineSeries(this); chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); + } else if (type == SeriesType::StepLine) { + series = new QLineSeries(this); + chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries); } else { series = new QScatterSeries(this); chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); @@ -786,9 +821,7 @@ QXYSeries *ChartView::createSeries(QAbstractSeries::SeriesType type, QColor colo return series; } -void ChartView::setSeriesType(QAbstractSeries::SeriesType type) { - line_series_action->setChecked(type == QAbstractSeries::SeriesTypeLine); - scatter_series_action->setChecked(type == QAbstractSeries::SeriesTypeScatter); +void ChartView::setSeriesType(SeriesType type) { if (type != series_type) { series_type = type; for (auto &s : sigs) { @@ -797,7 +830,7 @@ void ChartView::setSeriesType(QAbstractSeries::SeriesType type) { } for (auto &s : sigs) { auto series = createSeries(series_type, getColor(s.sig)); - series->replace(s.vals); + series->replace(series_type == SeriesType::StepLine ? s.step_vals : s.vals); s.series = series; } updateSeriesPoints(); diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index a1dcf32513..76943c1fe9 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -20,6 +20,12 @@ using namespace QtCharts; const int CHART_MIN_WIDTH = 300; +enum class SeriesType { + Line = 0, + StepLine, + Scatter +}; + class ChartView : public QChartView { Q_OBJECT @@ -29,7 +35,7 @@ public: bool hasSeries(const MessageId &msg_id, const Signal *sig) const; void updateSeries(const Signal *sig = nullptr, const std::vector *events = nullptr, bool clear = true); void updatePlot(double cur, double min, double max); - void setSeriesType(QAbstractSeries::SeriesType type); + void setSeriesType(SeriesType type); void updatePlotArea(int left); struct SigItem { @@ -37,7 +43,9 @@ public: const Signal *sig = nullptr; QXYSeries *series = nullptr; QVector vals; + QVector step_vals; uint64_t last_value_mono_time = 0; + QPointF track_pt{}; }; signals: @@ -57,6 +65,7 @@ private slots: void signalRemoved(const Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } private: + void createToolButtons(); void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *ev) override; @@ -70,15 +79,15 @@ private: void drawForeground(QPainter *painter, const QRectF &rect) override; std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); - QXYSeries *createSeries(QAbstractSeries::SeriesType type, QColor color); + QXYSeries *createSeries(SeriesType type, QColor color); void updateSeriesPoints(); void removeIf(std::function predicate); + inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } int y_label_width = 0; int align_to = 0; QValueAxis *axis_x; QValueAxis *axis_y; - QVector track_pts; QGraphicsPixmapItem *move_icon; QGraphicsProxyWidget *close_btn_proxy; QGraphicsProxyWidget *manage_btn_proxy; @@ -86,9 +95,7 @@ private: QList sigs; double cur_sec = 0; const QString mime_type = "application/x-cabanachartview"; - QAbstractSeries::SeriesType series_type = QAbstractSeries::SeriesTypeLine; - QAction *line_series_action; - QAction *scatter_series_action; + SeriesType series_type = SeriesType::Line; friend class ChartsWidget; }; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 6cbd16cabf..23f001efff 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -65,7 +65,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { form_layout->addRow(tr("Max Cached Minutes"), cached_minutes); chart_series_type = new QComboBox(this); - chart_series_type->addItems({tr("Line"), tr("Scatter")}); + chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); chart_series_type->setCurrentIndex(settings.chart_series_type); form_layout->addRow(tr("Chart Default Series Type"), chart_series_type);