cabana: added a new series type to chart: step line (#27422)

* add step line series

* create buttons in createToolButtons

* add inline function clearTrackPoints

* do not show tooltip if series is invisible

* use QActionGroup
old-commit-hash: dc4ebeb29c
beeps
Dean Lee 2 years ago committed by GitHub
parent eb77f1c15b
commit db35b147ac
  1. 145
      tools/cabana/chartswidget.cc
  2. 19
      tools/cabana/chartswidget.h
  3. 2
      tools/cabana/settings.cc

@ -1,5 +1,6 @@
#include "tools/cabana/chartswidget.h" #include "tools/cabana/chartswidget.h"
#include <QActionGroup>
#include <QApplication> #include <QApplication>
#include <QCompleter> #include <QCompleter>
#include <QDialogButtonBox> #include <QDialogButtonBox>
@ -183,7 +184,7 @@ void ChartsWidget::settingChanged() {
range_slider->setRange(1, settings.max_cached_minutes * 60); range_slider->setRange(1, settings.max_cached_minutes * 60);
for (auto c : charts) { for (auto c : charts) {
c->setFixedHeight(settings.chart_height); 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::ChartView(QWidget *parent) : QChartView(nullptr, parent) { 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(); QChart *chart = new QChart();
chart->setBackgroundVisible(false); chart->setBackgroundVisible(false);
axis_x = new QValueAxis(this); axis_x = new QValueAxis(this);
@ -325,40 +326,54 @@ ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) {
background->setPen(Qt::NoPen); background->setPen(Qt::NoPen);
background->setZValue(chart->zValue() - 1); 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")); move_icon->setToolTip(tr("Drag and drop to combine charts"));
QToolButton *remove_btn = toolButton("x", tr("Remove Chart")); 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->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); QMenu *menu = new QMenu(this);
line_series_action = menu->addAction(tr("Line"), [this]() { setSeriesType(QAbstractSeries::SeriesTypeLine); }); auto change_series_group = new QActionGroup(menu);
line_series_action->setCheckable(true); change_series_group->setExclusive(true);
line_series_action->setChecked(series_type == QAbstractSeries::SeriesTypeLine); QStringList types{tr("line"), tr("Step Line"), tr("Scatter")};
scatter_series_action = menu->addAction(tr("Scatter"), [this]() { setSeriesType(QAbstractSeries::SeriesTypeScatter); }); for (int i = 0; i < types.size(); ++i) {
scatter_series_action->setCheckable(true); QAction *act = new QAction(types[i], change_series_group);
scatter_series_action->setChecked(series_type == QAbstractSeries::SeriesTypeScatter); act->setData(i);
act->setCheckable(true);
act->setChecked(i == (int)series_type);
menu->addAction(act);
}
menu->addSeparator(); menu->addSeparator();
menu->addAction(tr("Manage series"), this, &ChartView::manageSeries); menu->addAction(tr("Manage series"), this, &ChartView::manageSeries);
QToolButton *manage_btn = toolButton("list", "");
manage_btn->setMenu(menu); manage_btn->setMenu(menu);
manage_btn->setPopupMode(QToolButton::InstantPopup); 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->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(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) { void ChartView::addSeries(const MessageId &msg_id, const Signal *sig) {
@ -476,7 +491,7 @@ void ChartView::updateSeriesPoints() {
int num_points = std::max<int>(end - begin, 1); int num_points = std::max<int>(end - begin, 1);
int pixels_per_point = width() / num_points; 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)); ((QScatterSeries *)s.series)->setMarkerSize(std::clamp(pixels_per_point / 3, 2, 8));
} else { } else {
s.series->setPointsVisible(pixels_per_point > 20); s.series->setPointsVisible(pixels_per_point > 20);
@ -490,7 +505,9 @@ void ChartView::updateSeries(const Signal *sig, const std::vector<Event *> *even
if (!sig || s.sig == sig) { if (!sig || s.sig == sig) {
if (clear) { if (clear) {
s.vals.clear(); s.vals.clear();
s.step_vals.clear();
s.vals.reserve(settings.max_cached_minutes * 60 * 100); // [n]seconds * 100hz 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.last_value_mono_time = 0;
} }
s.series->setColor(getColor(s.sig)); s.series->setColor(getColor(s.sig));
@ -498,6 +515,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector<Event *> *even
struct Chunk { struct Chunk {
std::vector<Event *>::const_iterator first, second; std::vector<Event *>::const_iterator first, second;
QVector<QPointF> vals; QVector<QPointF> vals;
QVector<QPointF> step_vals;
}; };
// split into one minitue chunks // split into one minitue chunks
QVector<Chunk> chunks; QVector<Chunk> chunks;
@ -510,6 +528,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector<Event *> *even
QtConcurrent::blockingMap(chunks, [&](Chunk &chunk) { QtConcurrent::blockingMap(chunks, [&](Chunk &chunk) {
chunk.vals.reserve(60 * 100); // 100 hz chunk.vals.reserve(60 * 100); // 100 hz
chunk.step_vals.reserve(60 * 100 * 2); // 100 hz
double route_start_time = can->routeStartTime(); double route_start_time = can->routeStartTime();
for (auto it = chunk.first; it != chunk.second; ++it) { for (auto it = chunk.first; it != chunk.second; ++it) {
if ((*it)->which == cereal::Event::Which::CAN) { if ((*it)->which == cereal::Event::Which::CAN) {
@ -519,6 +538,10 @@ void ChartView::updateSeries(const Signal *sig, const std::vector<Event *> *even
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig); double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig);
double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds
chunk.vals.push_back({ts, value}); 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<Event *> *even
}); });
for (auto &c : chunks) { for (auto &c : chunks) {
s.vals.append(c.vals); s.vals.append(c.vals);
s.step_vals.append(c.step_vals);
} }
if (events->size()) { if (events->size()) {
s.last_value_mono_time = events->back()->mono_time; 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(); updateAxisY();
@ -607,7 +631,7 @@ qreal ChartView::niceNumber(qreal x, bool ceiling) {
} }
void ChartView::leaveEvent(QEvent *event) { void ChartView::leaveEvent(QEvent *event) {
track_pts.clear(); clearTrackPoints();
scene()->update(); scene()->update();
QChartView::leaveEvent(event); QChartView::leaveEvent(event);
} }
@ -663,26 +687,31 @@ void ChartView::mouseMoveEvent(QMouseEvent *ev) {
auto rubber = findChild<QRubberBand *>(); auto rubber = findChild<QRubberBand *>();
bool is_zooming = rubber && rubber->isVisible(); bool is_zooming = rubber && rubber->isVisible();
const auto plot_area = chart()->plotArea(); const auto plot_area = chart()->plotArea();
track_pts.clear(); clearTrackPoints();
if (!is_zooming && plot_area.contains(ev->pos())) { if (!is_zooming && plot_area.contains(ev->pos())) {
track_pts.resize(sigs.size());
QStringList text_list; QStringList text_list;
const double sec = chart()->mapToValue(ev->pos()).x(); const double sec = chart()->mapToValue(ev->pos()).x();
for (int i = 0; i < sigs.size(); ++i) { qreal x = -1;
QString value = "--"; for (auto &s : sigs) {
if (!s.series->isVisible()) continue;
// use reverse iterator to find last item <= sec. // 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; }); auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; });
if (it != sigs[i].vals.rend() && it->x() >= axis_x->min()) { if (it != s.vals.rend() && it->x() >= axis_x->min()) {
value = QString::number(it->y()); s.track_pt = chart()->mapToPosition(*it);
track_pts[i] = chart()->mapToPosition(*it); x = std::max(x, s.track_pt.x());
} }
text_list.push_back(QString("<span style=\"color:%1;\">■ </span>%2: <b>%3</b>").arg(sigs[i].series->color().name(), sigs[i].sig->name, value)); text_list.push_back(QString("<span style=\"color:%1;\">■ </span>%2: <b>%3</b>")
.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(); }); text_list.push_front(QString::number(chart()->mapToValue({x, 0}).x(), 'f', 3));
auto pt = (max == track_pts.end()) ? ev->pos() : *max; QPointF tooltip_pt(x + 12, plot_area.top() - 20);
text_list.push_front(QString::number(chart()->mapToValue(pt).x(), 'f', 3)); QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), text_list.join("<br />"), this, plot_area.toRect());
QPointF tooltip_pt(pt.x() + 12, plot_area.top() - 20);
QToolTip::showText(mapToGlobal(tooltip_pt.toPoint()), pt.isNull() ? "" : text_list.join("<br />"), this, plot_area.toRect());
scene()->invalidate({}, QGraphicsScene::ForegroundLayer); scene()->invalidate({}, QGraphicsScene::ForegroundLayer);
} else { } else {
QToolTip::hideText(); QToolTip::hideText();
@ -727,6 +756,7 @@ void ChartView::dropEvent(QDropEvent *event) {
} }
void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
// draw time line
qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x(); qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x();
x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right()); x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right());
qreal y1 = chart()->plotArea().top() - 2; 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->setPen(QPen(chart()->titleBrush().color(), 2));
painter->drawLine(QPointF{x, y1}, QPointF{x, y2}); 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(); }); // draw track points
if (max != track_pts.end() && !max->isNull()) { painter->setPen(Qt::NoPen);
painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); qreal track_line_x = -1;
painter->drawLine(QPointF{max->x(), y1}, QPointF{max->x(), y2}); for (auto &s : sigs) {
painter->setPen(Qt::NoPen); if (!s.track_pt.isNull() && s.series->isVisible()) {
for (int i = 0; i < track_pts.size(); ++i) { painter->setBrush(s.series->color().darker(125));
if (!track_pts[i].isNull() && i < sigs.size()) { painter->drawEllipse(s.track_pt, 5.5, 5.5);
painter->setBrush(sigs[i].series->color().darker(125)); track_line_x = std::max(track_line_x, s.track_pt.x());
painter->drawEllipse(track_pts[i], 5.5, 5.5);
}
} }
} }
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) // paint points. OpenGL mode lacks certain features (such as showing points)
painter->setPen(Qt::NoPen); 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; QXYSeries *series = nullptr;
if (type == QAbstractSeries::SeriesTypeLine) { if (type == SeriesType::Line) {
series = new QLineSeries(this); series = new QLineSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle); chart()->legend()->setMarkerShape(QLegend::MarkerShapeRectangle);
} else if (type == SeriesType::StepLine) {
series = new QLineSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeFromSeries);
} else { } else {
series = new QScatterSeries(this); series = new QScatterSeries(this);
chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle); chart()->legend()->setMarkerShape(QLegend::MarkerShapeCircle);
@ -786,9 +821,7 @@ QXYSeries *ChartView::createSeries(QAbstractSeries::SeriesType type, QColor colo
return series; return series;
} }
void ChartView::setSeriesType(QAbstractSeries::SeriesType type) { void ChartView::setSeriesType(SeriesType type) {
line_series_action->setChecked(type == QAbstractSeries::SeriesTypeLine);
scatter_series_action->setChecked(type == QAbstractSeries::SeriesTypeScatter);
if (type != series_type) { if (type != series_type) {
series_type = type; series_type = type;
for (auto &s : sigs) { for (auto &s : sigs) {
@ -797,7 +830,7 @@ void ChartView::setSeriesType(QAbstractSeries::SeriesType type) {
} }
for (auto &s : sigs) { for (auto &s : sigs) {
auto series = createSeries(series_type, getColor(s.sig)); 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; s.series = series;
} }
updateSeriesPoints(); updateSeriesPoints();

@ -20,6 +20,12 @@ using namespace QtCharts;
const int CHART_MIN_WIDTH = 300; const int CHART_MIN_WIDTH = 300;
enum class SeriesType {
Line = 0,
StepLine,
Scatter
};
class ChartView : public QChartView { class ChartView : public QChartView {
Q_OBJECT Q_OBJECT
@ -29,7 +35,7 @@ public:
bool hasSeries(const MessageId &msg_id, const Signal *sig) const; bool hasSeries(const MessageId &msg_id, const Signal *sig) const;
void updateSeries(const Signal *sig = nullptr, const std::vector<Event*> *events = nullptr, bool clear = true); void updateSeries(const Signal *sig = nullptr, const std::vector<Event*> *events = nullptr, bool clear = true);
void updatePlot(double cur, double min, double max); void updatePlot(double cur, double min, double max);
void setSeriesType(QAbstractSeries::SeriesType type); void setSeriesType(SeriesType type);
void updatePlotArea(int left); void updatePlotArea(int left);
struct SigItem { struct SigItem {
@ -37,7 +43,9 @@ public:
const Signal *sig = nullptr; const Signal *sig = nullptr;
QXYSeries *series = nullptr; QXYSeries *series = nullptr;
QVector<QPointF> vals; QVector<QPointF> vals;
QVector<QPointF> step_vals;
uint64_t last_value_mono_time = 0; uint64_t last_value_mono_time = 0;
QPointF track_pt{};
}; };
signals: signals:
@ -57,6 +65,7 @@ private slots:
void signalRemoved(const Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } void signalRemoved(const Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
private: private:
void createToolButtons();
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *ev) override; void mouseMoveEvent(QMouseEvent *ev) override;
@ -70,15 +79,15 @@ private:
void drawForeground(QPainter *painter, const QRectF &rect) override; void drawForeground(QPainter *painter, const QRectF &rect) override;
std::tuple<double, double, int> getNiceAxisNumbers(qreal min, qreal max, int tick_count); std::tuple<double, double, int> getNiceAxisNumbers(qreal min, qreal max, int tick_count);
qreal niceNumber(qreal x, bool ceiling); qreal niceNumber(qreal x, bool ceiling);
QXYSeries *createSeries(QAbstractSeries::SeriesType type, QColor color); QXYSeries *createSeries(SeriesType type, QColor color);
void updateSeriesPoints(); void updateSeriesPoints();
void removeIf(std::function<bool(const SigItem &)> predicate); void removeIf(std::function<bool(const SigItem &)> predicate);
inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; }
int y_label_width = 0; int y_label_width = 0;
int align_to = 0; int align_to = 0;
QValueAxis *axis_x; QValueAxis *axis_x;
QValueAxis *axis_y; QValueAxis *axis_y;
QVector<QPointF> track_pts;
QGraphicsPixmapItem *move_icon; QGraphicsPixmapItem *move_icon;
QGraphicsProxyWidget *close_btn_proxy; QGraphicsProxyWidget *close_btn_proxy;
QGraphicsProxyWidget *manage_btn_proxy; QGraphicsProxyWidget *manage_btn_proxy;
@ -86,9 +95,7 @@ private:
QList<SigItem> sigs; QList<SigItem> sigs;
double cur_sec = 0; double cur_sec = 0;
const QString mime_type = "application/x-cabanachartview"; const QString mime_type = "application/x-cabanachartview";
QAbstractSeries::SeriesType series_type = QAbstractSeries::SeriesTypeLine; SeriesType series_type = SeriesType::Line;
QAction *line_series_action;
QAction *scatter_series_action;
friend class ChartsWidget; friend class ChartsWidget;
}; };

@ -65,7 +65,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
form_layout->addRow(tr("Max Cached Minutes"), cached_minutes); form_layout->addRow(tr("Max Cached Minutes"), cached_minutes);
chart_series_type = new QComboBox(this); 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); chart_series_type->setCurrentIndex(settings.chart_series_type);
form_layout->addRow(tr("Chart Default Series Type"), chart_series_type); form_layout->addRow(tr("Chart Default Series Type"), chart_series_type);

Loading…
Cancel
Save