diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index 54cacf56cd..87a0e8abd0 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -68,10 +68,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); main_layout->addWidget(charts_scroll); - align_charts_timer = new QTimer(this); - align_charts_timer->setSingleShot(true); - align_charts_timer->callOnTimeout(this, &ChartsWidget::alignCharts); - // init settings use_dark_theme = QApplication::style()->standardPalette().color(QPalette::WindowText).value() > QApplication::style()->standardPalette().color(QPalette::Background).value(); @@ -125,9 +121,9 @@ void ChartsWidget::zoomReset() { void ChartsWidget::updateState() { if (charts.isEmpty()) return; + const auto events = can->events(); if (can->liveStreaming()) { // appends incoming events to the end of series - const auto events = can->events(); for (auto c : charts) { c->updateSeries(nullptr, events, false); } @@ -135,33 +131,30 @@ void ChartsWidget::updateState() { const double cur_sec = can->currentSec(); if (!is_zoomed) { - double pos = (cur_sec - display_range.first) / max_chart_range; + double pos = (cur_sec - display_range.first) / std::max(1.0, (display_range.second - display_range.first)); if (pos < 0 || pos > 0.8) { - const double min_event_sec = can->events()->empty() ? 0 : (can->events()->front()->mono_time / (double)1e9 - can->routeStartTime()); - display_range.first = std::floor(std::max(min_event_sec, cur_sec - max_chart_range * 0.2)); + display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); } - display_range.second = std::floor(display_range.first + max_chart_range); + double max_event_sec = events->empty() ? 0 : (events->back()->mono_time / 1e9 - can->routeStartTime()); + double max_sec = std::min(std::floor(display_range.first + max_chart_range), max_event_sec); + display_range.first = std::max(0.0, max_sec - max_chart_range); + display_range.second = display_range.first + max_chart_range; } else if (cur_sec < zoomed_range.first || cur_sec >= zoomed_range.second) { // loop in zoommed range can->seekTo(zoomed_range.first); } - setUpdatesEnabled(false); + charts_layout->parentWidget()->setUpdatesEnabled(false); const auto &range = is_zoomed ? zoomed_range : display_range; for (auto c : charts) { c->updatePlot(cur_sec, range.first, range.second); } - setUpdatesEnabled(true); + alignCharts(); + charts_layout->parentWidget()->setUpdatesEnabled(true); } void ChartsWidget::setMaxChartRange(int value) { max_chart_range = settings.chart_range = value; - double current_sec = can->currentSec(); - const double min_event_sec = can->events()->empty() ? 0 : (can->events()->front()->mono_time / (double)1e9 - can->routeStartTime()); - // keep current_sec's pos - double pos = (current_sec - display_range.first) / (display_range.second - display_range.first); - display_range.first = std::floor(std::max(min_event_sec, current_sec - max_chart_range * (1.0 - pos))); - display_range.second = std::floor(display_range.first + max_chart_range); updateToolBar(); updateState(); } @@ -200,7 +193,6 @@ ChartView *ChartsWidget::createChart() { QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset); QObject::connect(chart, &ChartView::seriesRemoved, this, &ChartsWidget::seriesChanged); QObject::connect(chart, &ChartView::seriesAdded, this, &ChartsWidget::seriesChanged); - QObject::connect(chart, &ChartView::axisYUpdated, [this]() { align_charts_timer->start(100); }); charts.push_back(chart); updateLayout(); return chart; @@ -242,6 +234,7 @@ void ChartsWidget::updateLayout() { for (int i = 0; i < charts.size(); ++i) { charts_layout->addWidget(charts[charts.size() - i - 1], i / n, i % n); } + alignCharts(true); } void ChartsWidget::resizeEvent(QResizeEvent *event) { @@ -278,13 +271,16 @@ void ChartsWidget::removeAll() { emit seriesChanged(); } -void ChartsWidget::alignCharts() { +void ChartsWidget::alignCharts(bool force) { int plot_left = 0; for (auto c : charts) { - plot_left = qMax((qreal)plot_left, c->getYAsixLabelWidth()); + plot_left = std::max(plot_left, c->y_label_width); } - for (auto c : charts) { - c->setPlotAreaLeftPosition(plot_left); + plot_left = std::max((plot_left / 10) * 10 + 10, 50); + if (std::exchange(align_to, plot_left) != align_to || force) { + for (auto c : charts) { + c->updatePlotArea(align_to); + } } } @@ -302,15 +298,20 @@ ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { series_type = settings.chart_series_type == 0 ? QAbstractSeries::SeriesTypeLine : QAbstractSeries::SeriesTypeScatter; QChart *chart = new QChart(); - chart->setBackgroundRoundness(0); + chart->setBackgroundVisible(false); axis_x = new QValueAxis(this); axis_y = new QValueAxis(this); + axis_y->setLabelFormat("%.1f"); chart->addAxis(axis_x, Qt::AlignBottom); chart->addAxis(axis_y, Qt::AlignLeft); chart->legend()->layout()->setContentsMargins(0, 0, 40, 0); chart->legend()->setShowToolTips(true); - chart->layout()->setContentsMargins(0, 0, 0, 0); - chart->setMargins({20, 11, 11, 11}); + chart->setMargins({0, 0, 0, 0}); + + background = new QGraphicsRectItem(chart); + background->setBrush(Qt::white); + background->setPen(Qt::NoPen); + background->setZValue(chart->zValue() - 1); QToolButton *remove_btn = new QToolButton(); remove_btn->setIcon(utils::icon("x")); @@ -351,22 +352,6 @@ ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) { QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove); } -qreal ChartView::getYAsixLabelWidth() const { - if (axis_y->max() <= axis_y->min() || axis_y->tickCount() <= 1) { - return 0; - } - QFontMetrics fm(axis_y->labelsFont()); - int n = qMax(int(-qFloor(std::log10((axis_y->max() - axis_y->min()) / (axis_y->tickCount() - 1)))), 0) + 1; - return qMax(fm.width(QString::number(axis_y->min(), 'f', n)), fm.width(QString::number(axis_y->max(), 'f', n))) + 20; -} - -void ChartView::setPlotAreaLeftPosition(int pos) { - if (std::ceil(chart()->plotArea().left()) != pos) { - const float left_margin = chart()->margins().left() + pos - chart()->plotArea().left(); - chart()->setMargins(QMargins(left_margin, 11, 11, 11)); - } -} - void ChartView::addSeries(const QString &msg_id, const Signal *sig) { QXYSeries *series = createSeries(series_type); chart()->addSeries(series); @@ -467,11 +452,21 @@ void ChartView::manageSeries() { void ChartView::resizeEvent(QResizeEvent *event) { QChartView::resizeEvent(event); + updatePlotArea(); int x = event->size().width() - close_btn_proxy->size().width() - 11; close_btn_proxy->setPos(x, 8); manage_btn_proxy->setPos(x - manage_btn_proxy->size().width() - 5, 8); } +void ChartView::updatePlotArea(int left) { + align_to = left > 0 ? left : align_to; + QRect r = rect(); + background->setRect(r); + chart()->legend()->setGeometry(QRect(r.left(), r.top(), r.width(), 45)); + chart()->setPlotArea(QRect(align_to, r.top() + 45, r.width() - align_to - 22, r.height() - 80)); + chart()->layout()->invalidate(); +} + void ChartView::updateTitle() { for (auto &s : sigs) { s.series->setName(QString("%1 %2 %3").arg(s.sig->name.c_str()).arg(msgName(s.msg_id)).arg(s.msg_id)); @@ -536,7 +531,7 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even for (auto it = begin; it != events->end(); ++it) { if ((*it)->which == cereal::Event::Which::CAN) { for (const auto &c : (*it)->event.getCan()) { - if (s.source == c.getSrc() && s.address == c.getAddress()) { + if (s.address == c.getAddress() && s.source == c.getSrc()) { 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 @@ -549,46 +544,47 @@ void ChartView::updateSeries(const Signal *sig, const std::vector *even s.last_value_mono_time = events->back()->mono_time; } s.series->replace(s.vals); - updateAxisY(); } } + updateAxisY(); } // auto zoom on yaxis void ChartView::updateAxisY() { if (sigs.isEmpty()) return; - double min_y = std::numeric_limits::max(); - double max_y = std::numeric_limits::lowest(); + double min = std::numeric_limits::max(); + double max = std::numeric_limits::lowest(); 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; }); - for (auto it = begin; it != s.vals.end() && it->x() <= axis_x->max(); ++it) { - if (it->y() < min_y) min_y = it->y(); - if (it->y() > max_y) max_y = it->y(); + auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; }); + auto last = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->max(), [](auto &p, double x) { return p.x() < x; }); + for (auto it = first; it != last; ++it) { + if (it->y() < min) min = it->y(); + if (it->y() > max) max = it->y(); } } + if (min == std::numeric_limits::max()) min = 0; + if (max == std::numeric_limits::lowest()) max = 0; - if (min_y == std::numeric_limits::max()) min_y = 0; - if (max_y == std::numeric_limits::lowest()) max_y = 0; - if (std::abs(max_y - min_y) < 1e-3) { - applyNiceNumbers(min_y - 1, max_y + 1); - } else { - double range = max_y - min_y; - applyNiceNumbers(min_y - range * 0.05, max_y + range * 0.05); + double delta = std::abs(max - min) < 1e-3 ? 1 : (max - min) * 0.05; + auto [min_y, max_y, tick_count] = getNiceAxisNumbers(min - delta, max + delta, axis_y->tickCount()); + if (min_y != axis_y->min() || max_y != axis_y->max()) { + axis_y->setRange(min_y, max_y); + axis_y->setTickCount(tick_count); + + QFontMetrics fm(axis_y->labelsFont()); + int n = qMax(int(-qFloor(std::log10((max_y - min_y) / (tick_count - 1)))), 0) + 1; + y_label_width = qMax(fm.width(QString::number(min_y, 'f', n)), fm.width(QString::number(max_y, 'f', n))) + 20; // left margin 20 } - emit axisYUpdated(); } -void ChartView::applyNiceNumbers(qreal min, qreal max) { - int tick_count = axis_y->tickCount(); +std::tuple ChartView::getNiceAxisNumbers(qreal min, qreal max, int tick_count) { qreal range = niceNumber((max - min), true); // range with ceiling qreal step = niceNumber(range / (tick_count - 1), false); min = qFloor(min / step); max = qCeil(max / step); tick_count = int(max - min) + 1; - axis_y->setRange(min * step, max * step); - axis_y->setTickCount(tick_count); - axis_y->setLabelFormat("%.1f"); + return {min * step, max * step, tick_count}; } // nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index 8c50993971..f0f9e5cd9c 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,6 +17,8 @@ using namespace QtCharts; +const int CHART_MIN_WIDTH = 300; + class ChartView : public QChartView { Q_OBJECT @@ -29,9 +30,8 @@ public: bool hasSeries(const QString &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 setPlotAreaLeftPosition(int pos); - qreal getYAsixLabelWidth() const; void setSeriesType(QAbstractSeries::SeriesType type); + void updatePlotArea(int left = 0); struct SigItem { QString msg_id; @@ -49,7 +49,6 @@ signals: void zoomIn(double min, double max); void zoomReset(); void remove(); - void axisYUpdated(); private slots: void msgRemoved(uint32_t address); @@ -67,25 +66,30 @@ private: void dropEvent(QDropEvent *event) override; void leaveEvent(QEvent *event) override; void resizeEvent(QResizeEvent *event) override; + QSize sizeHint() const override { return {CHART_MIN_WIDTH, settings.chart_height}; } void updateAxisY(); void updateTitle(); void drawForeground(QPainter *painter, const QRectF &rect) override; - void applyNiceNumbers(qreal min, qreal max); + std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); QXYSeries *createSeries(QAbstractSeries::SeriesType type); void updateSeriesPoints(); + int y_label_width = 50; + int align_to = 0; QValueAxis *axis_x; QValueAxis *axis_y; QVector track_pts; QGraphicsProxyWidget *close_btn_proxy; QGraphicsProxyWidget *manage_btn_proxy; + QGraphicsRectItem *background; 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; + friend class ChartsWidget; }; class ChartsWidget : public QWidget { @@ -107,7 +111,7 @@ signals: private: void resizeEvent(QResizeEvent *event) override; - void alignCharts(); + void alignCharts(bool force = false); void newChart(); ChartView * createChart(); void removeChart(ChartView *chart); @@ -129,7 +133,6 @@ private: QAction *dock_btn; QAction *reset_zoom_btn; QAction *remove_all_btn; - QTimer *align_charts_timer; QGridLayout *charts_layout; QList charts; uint32_t max_chart_range = 0; @@ -141,7 +144,7 @@ private: QAction *columns_cb_action; QComboBox *columns_cb; int column_count = 1; - const int CHART_MIN_WIDTH = 300; + int align_to = 0; }; class SeriesSelector : public QDialog {