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 <QActionGroup>
#include <QApplication>
#include <QCompleter>
#include <QDialogButtonBox>
@ -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<int>(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<Event *> *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<Event *> *even
struct Chunk {
std::vector<Event *>::const_iterator first, second;
QVector<QPointF> vals;
QVector<QPointF> step_vals;
};
// split into one minitue chunks
QVector<Chunk> chunks;
@ -510,6 +528,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector<Event *> *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<Event *> *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<Event *> *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<QRubberBand *>();
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("<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(); });
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("<br />"), 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("<br />"), 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();

@ -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<Event*> *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<QPointF> vals;
QVector<QPointF> 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<double, double, int> 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<bool(const SigItem &)> 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<QPointF> track_pts;
QGraphicsPixmapItem *move_icon;
QGraphicsProxyWidget *close_btn_proxy;
QGraphicsProxyWidget *manage_btn_proxy;
@ -86,9 +95,7 @@ private:
QList<SigItem> 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;
};

@ -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);

Loading…
Cancel
Save