mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-06-27 17:02:04 +08:00
Cabana: support for multiple series in chart (#26538)
* customize axis x & y * new function chartView::addSignal * support multiple series in chartView * more * show legend * update changed signals only * fix z value * cleanup * limit trake line in plot area * display signal name in value_text *   * fix axis y * add spaces * cache min max value for axis y * cleanup * better values text * remove force_update
This commit is contained in:
+211
-111
@@ -4,11 +4,9 @@
|
||||
#include <QGraphicsLayout>
|
||||
#include <QRubberBand>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <QtCharts/QLineSeries>
|
||||
#include <QtCharts/QValueAxis>
|
||||
#include <QtConcurrent>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QtConcurrent>
|
||||
|
||||
// ChartsWidget
|
||||
|
||||
@@ -19,7 +17,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
// toolbar
|
||||
QToolBar *toolbar = new QToolBar(tr("Charts"), this);
|
||||
title_label = new QLabel();
|
||||
title_label->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Preferred);
|
||||
title_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
toolbar->addWidget(title_label);
|
||||
toolbar->addWidget(range_label = new QLabel());
|
||||
reset_zoom_btn = toolbar->addAction("⟲");
|
||||
@@ -42,21 +40,10 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
main_layout->addWidget(charts_scroll);
|
||||
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { removeAll(nullptr); });
|
||||
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartsWidget::signalUpdated);
|
||||
QObject::connect(dbc(), &DBCManager::msgRemoved, [this](uint32_t address) {
|
||||
for (auto c : charts.toVector())
|
||||
if (DBCManager::parseId(c->id).second == address) removeChart(c);
|
||||
});
|
||||
QObject::connect(dbc(), &DBCManager::msgUpdated, [this](uint32_t address) {
|
||||
for (auto c : charts) {
|
||||
if (DBCManager::parseId(c->id).second == address) c->updateTitle();
|
||||
}
|
||||
});
|
||||
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(can, &CANMessages::eventsMerged, this, &ChartsWidget::eventsMerged);
|
||||
QObject::connect(can, &CANMessages::updated, this, &ChartsWidget::updateState);
|
||||
QObject::connect(remove_all_btn, &QAction::triggered, [this]() { removeAll(); });
|
||||
QObject::connect(remove_all_btn, &QAction::triggered, this, &ChartsWidget::removeAll);
|
||||
QObject::connect(reset_zoom_btn, &QAction::triggered, this, &ChartsWidget::zoomReset);
|
||||
QObject::connect(dock_btn, &QAction::triggered, [this]() {
|
||||
emit dock(!docking);
|
||||
@@ -81,8 +68,8 @@ void ChartsWidget::zoomIn(double min, double max) {
|
||||
zoomed_range = {min, max};
|
||||
is_zoomed = zoomed_range != display_range;
|
||||
updateToolBar();
|
||||
emit rangeChanged(min, max, is_zoomed);
|
||||
updateState();
|
||||
emit rangeChanged(min, max, is_zoomed);
|
||||
}
|
||||
|
||||
void ChartsWidget::zoomReset() {
|
||||
@@ -108,13 +95,13 @@ void ChartsWidget::updateState() {
|
||||
if (prev_range != display_range) {
|
||||
QFutureSynchronizer<void> future_synchronizer;
|
||||
for (auto c : charts)
|
||||
future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, display_range));
|
||||
future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::setEventsRange, display_range));
|
||||
}
|
||||
}
|
||||
|
||||
const auto &range = is_zoomed ? zoomed_range : display_range;
|
||||
for (auto c : charts) {
|
||||
c->setRange(range.first, range.second);
|
||||
c->setDisplayRange(range.first, range.second);
|
||||
c->updateLineMarker(current_sec);
|
||||
}
|
||||
}
|
||||
@@ -128,49 +115,45 @@ void ChartsWidget::updateToolBar() {
|
||||
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts"));
|
||||
}
|
||||
|
||||
void ChartsWidget::showChart(const QString &id, const Signal *sig, bool show) {
|
||||
auto it = std::find_if(charts.begin(), charts.end(), [=](auto c) { return c->id == id && c->signal == sig; });
|
||||
if (it != charts.end()) {
|
||||
if (!show) removeChart((*it));
|
||||
} else if (show) {
|
||||
auto chart = new ChartView(id, sig, this);
|
||||
chart->updateSeries(display_range);
|
||||
QObject::connect(chart, &ChartView::remove, [=]() { removeChart(chart); });
|
||||
QObject::connect(chart, &ChartView::zoomIn, this, &ChartsWidget::zoomIn);
|
||||
QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset);
|
||||
charts_layout->insertWidget(0, chart);
|
||||
charts.push_back(chart);
|
||||
emit chartOpened(chart->id, chart->signal);
|
||||
ChartView *ChartsWidget::findChart(const QString &id, const Signal *sig) {
|
||||
for (auto c : charts)
|
||||
if (c->hasSeries(id, sig)) return c;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ChartsWidget::showChart(const QString &id, const Signal *sig, bool show, bool merge) {
|
||||
if (!show) {
|
||||
if (ChartView *chart = findChart(id, sig)) {
|
||||
chart->removeSeries(id, sig);
|
||||
}
|
||||
} else {
|
||||
ChartView *chart = merge && charts.size() > 0 ? charts.back() : nullptr;
|
||||
if (!chart) {
|
||||
chart = new ChartView(this);
|
||||
chart->setEventsRange(display_range);
|
||||
QObject::connect(chart, &ChartView::remove, [=]() { removeChart(chart); });
|
||||
QObject::connect(chart, &ChartView::zoomIn, this, &ChartsWidget::zoomIn);
|
||||
QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset);
|
||||
QObject::connect(chart, &ChartView::seriesRemoved, this, &ChartsWidget::chartClosed);
|
||||
charts_layout->insertWidget(0, chart);
|
||||
charts.push_back(chart);
|
||||
}
|
||||
chart->addSeries(id, sig);
|
||||
emit chartOpened(id, sig);
|
||||
updateState();
|
||||
}
|
||||
updateToolBar();
|
||||
}
|
||||
|
||||
bool ChartsWidget::isChartOpened(const QString &id, const Signal *sig) {
|
||||
auto it = std::find_if(charts.begin(), charts.end(), [=](auto c) { return c->id == id && c->signal == sig; });
|
||||
return it != charts.end();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(ChartView *chart) {
|
||||
charts.removeOne(chart);
|
||||
chart->deleteLater();
|
||||
updateToolBar();
|
||||
emit chartClosed(chart->id, chart->signal);
|
||||
}
|
||||
|
||||
void ChartsWidget::removeAll(const Signal *sig) {
|
||||
void ChartsWidget::removeAll() {
|
||||
for (auto c : charts.toVector())
|
||||
if (!sig || c->signal == sig) removeChart(c);
|
||||
}
|
||||
|
||||
void ChartsWidget::signalUpdated(const Signal *sig) {
|
||||
for (auto c : charts) {
|
||||
if (c->signal == sig) {
|
||||
c->updateTitle();
|
||||
c->updateSeries(display_range);
|
||||
c->setRange(display_range.first, display_range.second, true);
|
||||
}
|
||||
}
|
||||
removeChart(c);
|
||||
}
|
||||
|
||||
bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
|
||||
@@ -183,14 +166,14 @@ bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
|
||||
|
||||
// ChartView
|
||||
|
||||
ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
|
||||
: id(id), signal(sig), QChartView(nullptr, parent) {
|
||||
QLineSeries *series = new QLineSeries();
|
||||
ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) {
|
||||
QChart *chart = new QChart();
|
||||
chart->setBackgroundRoundness(0);
|
||||
chart->addSeries(series);
|
||||
chart->createDefaultAxes();
|
||||
chart->legend()->hide();
|
||||
axis_x = new QValueAxis(this);
|
||||
axis_y = new QValueAxis(this);
|
||||
chart->addAxis(axis_x, Qt::AlignBottom);
|
||||
chart->addAxis(axis_y, Qt::AlignLeft);
|
||||
chart->legend()->setShowToolTips(true);
|
||||
chart->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
// top margin for title
|
||||
chart->setMargins({0, 11, 0, 0});
|
||||
@@ -213,33 +196,108 @@ ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
|
||||
remove_btn->setToolTip(tr("Remove Chart"));
|
||||
close_btn_proxy = new QGraphicsProxyWidget(chart);
|
||||
close_btn_proxy->setWidget(remove_btn);
|
||||
close_btn_proxy->setZValue(chart->zValue() + 11);
|
||||
|
||||
setChart(chart);
|
||||
setRenderHint(QPainter::Antialiasing);
|
||||
setRubberBand(QChartView::HorizontalRubberBand);
|
||||
updateFromSettings();
|
||||
updateTitle();
|
||||
|
||||
QTimer *timer = new QTimer(this);
|
||||
timer->setInterval(100);
|
||||
timer->setSingleShot(true);
|
||||
timer->callOnTimeout(this, &ChartView::adjustChartMargins);
|
||||
|
||||
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(&settings, &Settings::changed, this, &ChartView::updateFromSettings);
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { emit remove(id, sig); });
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove);
|
||||
QObject::connect(chart, &QChart::plotAreaChanged, [=](const QRectF &plotArea) {
|
||||
// use a singleshot timer to avoid recursion call.
|
||||
timer->start();
|
||||
});
|
||||
}
|
||||
|
||||
ChartView::~ChartView() {
|
||||
for (auto &s : sigs)
|
||||
emit seriesRemoved(s.msg_id, s.sig);
|
||||
}
|
||||
|
||||
void ChartView::addSeries(const QString &msg_id, const Signal *sig) {
|
||||
QLineSeries *series = new QLineSeries(this);
|
||||
chart()->addSeries(series);
|
||||
series->attachAxis(axis_x);
|
||||
series->attachAxis(axis_y);
|
||||
auto [source, address] = DBCManager::parseId(msg_id);
|
||||
sigs.push_back({.msg_id = msg_id, .address = address, .source = source, .sig = sig, .series = series});
|
||||
updateTitle();
|
||||
updateSeries(sig);
|
||||
}
|
||||
|
||||
void ChartView::removeSeries(const QString &msg_id, const Signal *sig) {
|
||||
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
||||
if (it != sigs.end()) {
|
||||
it = removeSeries(it);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChartView::hasSeries(const QString &msg_id, const Signal *sig) const {
|
||||
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
||||
return it != sigs.end();
|
||||
}
|
||||
|
||||
QList<ChartView::SigItem>::iterator ChartView::removeSeries(const QList<ChartView::SigItem>::iterator &it) {
|
||||
chart()->removeSeries(it->series);
|
||||
it->series->deleteLater();
|
||||
emit seriesRemoved(it->msg_id, it->sig);
|
||||
|
||||
auto ret = sigs.erase(it);
|
||||
if (!sigs.isEmpty()) {
|
||||
updateAxisY();
|
||||
} else {
|
||||
emit remove();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void ChartView::signalUpdated(const Signal *sig) {
|
||||
auto it = std::find_if(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; });
|
||||
if (it != sigs.end()) {
|
||||
updateTitle();
|
||||
// TODO: don't update series if only name changed.
|
||||
updateSeries(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::signalRemoved(const Signal *sig) {
|
||||
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
|
||||
it = (it->sig == sig) ? removeSeries(it) : ++it;
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::msgUpdated(uint32_t address) {
|
||||
auto it = std::find_if(sigs.begin(), sigs.end(), [=](auto &s) { return s.address == address; });
|
||||
if (it != sigs.end())
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
void ChartView::msgRemoved(uint32_t address) {
|
||||
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
|
||||
it = (it->address == address) ? removeSeries(it) : ++it;
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::resizeEvent(QResizeEvent *event) {
|
||||
QChartView::resizeEvent(event);
|
||||
close_btn_proxy->setPos(event->size().width() - close_btn_proxy->size().width() - 11, 8);
|
||||
}
|
||||
|
||||
void ChartView::updateTitle() {
|
||||
chart()->setTitle(tr("<font color=\"gray\" text-align:left>%1 %2</font> <b>%3</b>").arg(dbc()->msg(id)->name).arg(id).arg(signal->name.c_str()));
|
||||
for (auto &s : sigs) {
|
||||
s.series->setName(QString("<b>%1</b> <font color=\"gray\">%2 %3</font>").arg(s.sig->name.c_str()).arg(msgName(s.msg_id)).arg(s.msg_id));
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::updateFromSettings() {
|
||||
@@ -249,9 +307,15 @@ void ChartView::updateFromSettings() {
|
||||
line_marker->setPen(QPen(color, 2));
|
||||
}
|
||||
|
||||
void ChartView::setRange(double min, double max, bool force_update) {
|
||||
auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
|
||||
if (force_update || (min != axis_x->min() || max != axis_x->max())) {
|
||||
void ChartView::setEventsRange(const std::pair<double, double> &range) {
|
||||
if (range != events_range) {
|
||||
events_range = range;
|
||||
updateSeries();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::setDisplayRange(double min, double max) {
|
||||
if (min != axis_x->min() || max != axis_x->max()) {
|
||||
axis_x->setRange(min, max);
|
||||
updateAxisY();
|
||||
}
|
||||
@@ -268,7 +332,6 @@ void ChartView::adjustChartMargins() {
|
||||
}
|
||||
|
||||
void ChartView::updateLineMarker(double current_sec) {
|
||||
auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
|
||||
int x = chart()->plotArea().left() +
|
||||
chart()->plotArea().width() * (current_sec - axis_x->min()) / (axis_x->max() - axis_x->min());
|
||||
if (int(line_marker->line().x1()) != x) {
|
||||
@@ -276,48 +339,72 @@ void ChartView::updateLineMarker(double current_sec) {
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::updateSeries(const std::pair<double, double> range) {
|
||||
void ChartView::updateSeries(const Signal *sig) {
|
||||
auto events = can->events();
|
||||
if (!events) return;
|
||||
|
||||
vals.clear();
|
||||
vals.reserve((range.second - range.first) * 1000); // [n]seconds * 1000hz
|
||||
auto [bus, address] = DBCManager::parseId(id);
|
||||
double route_start_time = can->routeStartTime();
|
||||
Event begin_event(cereal::Event::Which::INIT_DATA, (route_start_time + range.first) * 1e9);
|
||||
auto begin = std::lower_bound(events->begin(), events->end(), &begin_event, Event::lessThan());
|
||||
double end_ns = (route_start_time + range.second) * 1e9;
|
||||
for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) {
|
||||
if ((*it)->which == cereal::Event::Which::CAN) {
|
||||
for (const auto &c : (*it)->event.getCan()) {
|
||||
if (bus == c.getSrc() && address == c.getAddress()) {
|
||||
auto dat = c.getDat();
|
||||
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal);
|
||||
double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds
|
||||
vals.push_back({ts, value});
|
||||
for (int i = 0; i < sigs.size(); ++i) {
|
||||
if (auto &s = sigs[i]; !sig || s.sig == sig) {
|
||||
s.vals.clear();
|
||||
s.vals.reserve((events_range.second - events_range.first) * 1000); // [n]seconds * 1000hz
|
||||
s.min_y = std::numeric_limits<double>::max();
|
||||
s.max_y = std::numeric_limits<double>::lowest();
|
||||
|
||||
double route_start_time = can->routeStartTime();
|
||||
Event begin_event(cereal::Event::Which::INIT_DATA, (route_start_time + events_range.first) * 1e9);
|
||||
auto begin = std::lower_bound(events->begin(), events->end(), &begin_event, Event::lessThan());
|
||||
double end_ns = (route_start_time + events_range.second) * 1e9;
|
||||
|
||||
for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) {
|
||||
if ((*it)->which == cereal::Event::Which::CAN) {
|
||||
for (const auto &c : (*it)->event.getCan()) {
|
||||
if (s.source == c.getSrc() && s.address == c.getAddress()) {
|
||||
auto dat = c.getDat();
|
||||
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig);
|
||||
double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds
|
||||
s.vals.push_back({ts, value});
|
||||
|
||||
if (value < s.min_y) s.min_y = value;
|
||||
if (value > s.max_y) s.max_y = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QLineSeries *series = (QLineSeries *)chart()->series()[i];
|
||||
series->replace(s.vals);
|
||||
}
|
||||
}
|
||||
QLineSeries *series = (QLineSeries *)chart()->series()[0];
|
||||
series->replace(vals);
|
||||
updateAxisY();
|
||||
}
|
||||
|
||||
// auto zoom on yaxis
|
||||
void ChartView::updateAxisY() {
|
||||
const auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
|
||||
const auto axis_y = dynamic_cast<QValueAxis *>(chart()->axisY());
|
||||
auto begin = std::lower_bound(vals.begin(), vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; });
|
||||
if (begin == vals.end())
|
||||
return;
|
||||
|
||||
auto end = std::upper_bound(vals.begin(), vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); });
|
||||
const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); });
|
||||
if (max->y() == min->y()) {
|
||||
axis_y->setRange(min->y() - 1, max->y() + 1);
|
||||
double min_y = std::numeric_limits<double>::max();
|
||||
double max_y = std::numeric_limits<double>::lowest();
|
||||
if (events_range == std::pair{axis_x->min(), axis_x->max()}) {
|
||||
for (auto &s : sigs) {
|
||||
if (s.min_y < min_y) min_y = s.min_y;
|
||||
if (s.max_y > max_y) max_y = s.max_y;
|
||||
}
|
||||
} else {
|
||||
double range = max->y() - min->y();
|
||||
axis_y->setRange(min->y() - range * 0.05, max->y() + range * 0.05);
|
||||
for (auto &s : sigs) {
|
||||
auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; });
|
||||
if (begin == s.vals.end())
|
||||
return;
|
||||
|
||||
auto end = std::upper_bound(s.vals.begin(), s.vals.end(), axis_x->max(), [](double x, auto &p) { return x < p.x(); });
|
||||
const auto [min, max] = std::minmax_element(begin, end, [](auto &p1, auto &p2) { return p1.y() < p2.y(); });
|
||||
if (min->y() < min_y) min_y = min->y();
|
||||
if (max->y() > max_y) max_y = max->y();
|
||||
}
|
||||
}
|
||||
|
||||
if (max_y == min_y) {
|
||||
axis_y->setRange(min_y - 1, max_y + 1);
|
||||
} else {
|
||||
double range = max_y - min_y;
|
||||
axis_y->setRange(min_y - range * 0.05, max_y + range * 0.05);
|
||||
axis_y->applyNiceNumbers();
|
||||
}
|
||||
}
|
||||
@@ -333,7 +420,6 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) {
|
||||
rubber->hide();
|
||||
QRectF rect = rubber->geometry().normalized();
|
||||
rect.translate(-chart()->plotArea().topLeft());
|
||||
const auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
|
||||
double min = axis_x->min() + (rect.left() / chart()->plotArea().width()) * (axis_x->max() - axis_x->min());
|
||||
double max = axis_x->min() + (rect.right() / chart()->plotArea().width()) * (axis_x->max() - axis_x->min());
|
||||
if (rubber->width() <= 0) {
|
||||
@@ -357,26 +443,40 @@ void ChartView::mouseReleaseEvent(QMouseEvent *event) {
|
||||
void ChartView::mouseMoveEvent(QMouseEvent *ev) {
|
||||
auto rubber = findChild<QRubberBand *>();
|
||||
bool is_zooming = rubber && rubber->isVisible();
|
||||
if (!is_zooming) {
|
||||
const auto plot_area = chart()->plotArea();
|
||||
auto axis_x = dynamic_cast<QValueAxis *>(chart()->axisX());
|
||||
const auto plot_area = chart()->plotArea();
|
||||
|
||||
if (!is_zooming && plot_area.contains(ev->pos())) {
|
||||
double x = std::clamp((double)ev->pos().x(), plot_area.left(), plot_area.right() - 1);
|
||||
double sec = axis_x->min() + (axis_x->max() - axis_x->min()) * (x - plot_area.left()) / plot_area.width();
|
||||
auto value = std::upper_bound(vals.begin(), vals.end(), sec, [](double x, auto &p) { return x < p.x(); });
|
||||
if (value != vals.end()) {
|
||||
QPointF pos = chart()->mapToPosition((*value));
|
||||
track_line->setLine(pos.x(), plot_area.top(), pos.x(), plot_area.bottom());
|
||||
track_ellipse->setRect(pos.x() - 5, pos.y() - 5, 10, 10);
|
||||
value_text->setHtml(tr("<div style='background-color:darkGray'><font color='white'>%1, %2)</font></div>")
|
||||
.arg(value->x(), 0, 'f', 3).arg(value->y()));
|
||||
int text_x = pos.x() + 8;
|
||||
if ((text_x + value_text->boundingRect().width()) > plot_area.right()) {
|
||||
text_x = pos.x() - value_text->boundingRect().width() - 8;
|
||||
QStringList text_list;
|
||||
QPointF pos = plot_area.bottomRight();
|
||||
double tm = 0.0;
|
||||
|
||||
for (auto &s : sigs) {
|
||||
auto value = std::upper_bound(s.vals.begin(), s.vals.end(), sec, [](double x, auto &p) { return x < p.x(); });
|
||||
if (value != s.vals.end()) {
|
||||
text_list.push_back(QString(" %1 : %2 ").arg(sigs.size() > 1 ? s.sig->name.c_str() : "Value").arg(value->y()));
|
||||
tm = value->x();
|
||||
auto y_pos = chart()->mapToPosition(*value);
|
||||
if (y_pos.y() < pos.y()) pos = y_pos;
|
||||
}
|
||||
value_text->setPos(text_x, pos.y() - 10);
|
||||
}
|
||||
item_group->setVisible(value != vals.end());
|
||||
|
||||
if (!text_list.isEmpty()) {
|
||||
value_text->setHtml("<div style=\"background-color: darkGray;color: white;\"> Time: " +
|
||||
QString::number(tm, 'f', 3) + " <br />" + text_list.join("<br />") + "</div>");
|
||||
track_line->setLine(pos.x(), plot_area.top(), pos.x(), plot_area.bottom());
|
||||
int text_x = pos.x() + 8;
|
||||
QRectF text_rect = value_text->boundingRect();
|
||||
if ((text_x + text_rect.width()) > plot_area.right()) {
|
||||
text_x = pos.x() - text_rect.width() - 8;
|
||||
}
|
||||
value_text->setPos(text_x, pos.y() - text_rect.height() / 2);
|
||||
track_ellipse->setRect(pos.x() - 5, pos.y() - 5, 10, 10);
|
||||
}
|
||||
item_group->setVisible(!text_list.isEmpty());
|
||||
} else {
|
||||
item_group->setVisible(false);
|
||||
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
|
||||
}
|
||||
QChartView::mouseMoveEvent(ev);
|
||||
|
||||
+39
-13
@@ -8,6 +8,8 @@
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtCharts/QChartView>
|
||||
#include <QtCharts/QLineSeries>
|
||||
#include <QtCharts/QValueAxis>
|
||||
|
||||
#include "tools/cabana/canmessages.h"
|
||||
#include "tools/cabana/dbcmanager.h"
|
||||
@@ -18,35 +20,59 @@ class ChartView : public QChartView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ChartView(const QString &id, const Signal *sig, QWidget *parent = nullptr);
|
||||
void updateSeries(const std::pair<double, double> range);
|
||||
void setRange(double min, double max, bool force_update = false);
|
||||
ChartView(QWidget *parent = nullptr);
|
||||
~ChartView();
|
||||
void addSeries(const QString &msg_id, const Signal *sig);
|
||||
void removeSeries(const QString &msg_id, const Signal *sig);
|
||||
bool hasSeries(const QString &msg_id, const Signal *sig) const;
|
||||
void updateSeries(const Signal *sig = nullptr);
|
||||
void setEventsRange(const std::pair<double, double> &range);
|
||||
void setDisplayRange(double min, double max);
|
||||
void updateLineMarker(double current_sec);
|
||||
void updateFromSettings();
|
||||
void updateTitle();
|
||||
|
||||
QString id;
|
||||
const Signal *signal;
|
||||
struct SigItem {
|
||||
QString msg_id;
|
||||
uint8_t source = 0;
|
||||
uint32_t address = 0;
|
||||
const Signal *sig = nullptr;
|
||||
QLineSeries *series = nullptr;
|
||||
double min_y = 0;
|
||||
double max_y = 0;
|
||||
QVector<QPointF> vals;
|
||||
};
|
||||
|
||||
signals:
|
||||
void seriesRemoved(const QString &id, const Signal *sig);
|
||||
void zoomIn(double min, double max);
|
||||
void zoomReset();
|
||||
void remove(const QString &msg_id, const Signal *sig);
|
||||
void remove();
|
||||
|
||||
private slots:
|
||||
void msgRemoved(uint32_t address);
|
||||
void msgUpdated(uint32_t address);
|
||||
void signalUpdated(const Signal *sig);
|
||||
void signalRemoved(const Signal *sig);
|
||||
|
||||
private:
|
||||
QList<ChartView::SigItem>::iterator removeSeries(const QList<ChartView::SigItem>::iterator &it);
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *ev) override;
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void adjustChartMargins();
|
||||
void updateAxisY();
|
||||
void updateTitle();
|
||||
void updateFromSettings();
|
||||
|
||||
QValueAxis *axis_x;
|
||||
QValueAxis *axis_y;
|
||||
QGraphicsItemGroup *item_group;
|
||||
QGraphicsLineItem *line_marker, *track_line;
|
||||
QGraphicsEllipseItem *track_ellipse;
|
||||
QGraphicsTextItem *value_text;
|
||||
QGraphicsProxyWidget *close_btn_proxy;
|
||||
QVector<QPointF> vals;
|
||||
std::pair<double, double> events_range = {0, 0};
|
||||
QList<SigItem> sigs;
|
||||
};
|
||||
|
||||
class ChartsWidget : public QWidget {
|
||||
@@ -54,9 +80,9 @@ class ChartsWidget : public QWidget {
|
||||
|
||||
public:
|
||||
ChartsWidget(QWidget *parent = nullptr);
|
||||
void showChart(const QString &id, const Signal *sig, bool show);
|
||||
void showChart(const QString &id, const Signal *sig, bool show, bool merge);
|
||||
void removeChart(ChartView *chart);
|
||||
bool isChartOpened(const QString &id, const Signal *sig);
|
||||
inline bool isChartOpened(const QString &id, const Signal *sig) { return findChart(id, sig) != nullptr; }
|
||||
|
||||
signals:
|
||||
void dock(bool floating);
|
||||
@@ -69,10 +95,10 @@ private:
|
||||
void updateState();
|
||||
void zoomIn(double min, double max);
|
||||
void zoomReset();
|
||||
void signalUpdated(const Signal *sig);
|
||||
void updateToolBar();
|
||||
void removeAll(const Signal *sig = nullptr);
|
||||
void removeAll();
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
ChartView *findChart(const QString &id, const Signal *sig);
|
||||
|
||||
QLabel *title_label;
|
||||
QLabel *range_label;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QDoubleValidator>
|
||||
#include <QFormLayout>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QToolBar>
|
||||
@@ -122,7 +123,9 @@ SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(pa
|
||||
save_timer->callOnTimeout(this, &SignalEdit::saveSignal);
|
||||
|
||||
QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked);
|
||||
QObject::connect(plot_btn, &QToolButton::clicked, [this](bool checked) { emit showChart(msg_id, sig, checked); });
|
||||
QObject::connect(plot_btn, &QToolButton::clicked, [this](bool checked) {
|
||||
emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
|
||||
});
|
||||
QObject::connect(seek_btn, &QToolButton::clicked, [this]() { SignalFindDlg(msg_id, sig, this).exec(); });
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, [this]() { emit remove(sig); });
|
||||
QObject::connect(form, &SignalForm::changed, [this]() { save_timer->start(); });
|
||||
@@ -172,7 +175,7 @@ void SignalEdit::saveSignal() {
|
||||
}
|
||||
|
||||
void SignalEdit::setChartOpened(bool opened) {
|
||||
plot_btn->setToolTip(opened ? tr("Close Plot") : tr("Show Plot"));
|
||||
plot_btn->setToolTip(opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened chart"));
|
||||
plot_btn->setChecked(opened);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public:
|
||||
|
||||
signals:
|
||||
void highlight(const Signal *sig);
|
||||
void showChart(const QString &name, const Signal *sig, bool show);
|
||||
void showChart(const QString &name, const Signal *sig, bool show, bool merge);
|
||||
void remove(const Signal *sig);
|
||||
void save(const Signal *sig, const Signal &new_sig);
|
||||
void showFormClicked();
|
||||
|
||||
Reference in New Issue
Block a user