mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-03-03 22:23:53 +08:00
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
This commit is contained in:
@@ -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,31 +326,9 @@ 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);
|
||||
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->setWidget(remove_btn);
|
||||
close_btn_proxy->setZValue(chart->zValue() + 11);
|
||||
|
||||
QToolButton *manage_btn = toolButton("list", "");
|
||||
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);
|
||||
menu->addSeparator();
|
||||
menu->addAction(tr("Manage series"), this, &ChartView::manageSeries);
|
||||
manage_btn->setMenu(menu);
|
||||
manage_btn->setPopupMode(QToolButton::InstantPopup);
|
||||
manage_btn_proxy = new QGraphicsProxyWidget(chart);
|
||||
manage_btn_proxy->setWidget(manage_btn);
|
||||
manage_btn_proxy->setZValue(chart->zValue() + 11);
|
||||
|
||||
setChart(chart);
|
||||
|
||||
createToolButtons();
|
||||
setRenderHint(QPainter::Antialiasing);
|
||||
// TODO: enable zoomIn/seekTo in live streaming mode.
|
||||
setRubberBand(can->liveStreaming() ? QChartView::NoRubberBand : QChartView::HorizontalRubberBand);
|
||||
@@ -358,7 +337,43 @@ ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) {
|
||||
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->setWidget(remove_btn);
|
||||
close_btn_proxy->setZValue(chart()->zValue() + 11);
|
||||
|
||||
// series types
|
||||
QMenu *menu = new QMenu(this);
|
||||
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->setWidget(manage_btn);
|
||||
manage_btn_proxy->setZValue(chart()->zValue() + 11);
|
||||
|
||||
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())));
|
||||
}
|
||||
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());
|
||||
if (x < 0) {
|
||||
x = ev->pos().x();
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user