|
|
|
|
@@ -5,6 +5,7 @@
|
|
|
|
|
#include <QDrag>
|
|
|
|
|
#include <QGraphicsLayout>
|
|
|
|
|
#include <QGraphicsDropShadowEffect>
|
|
|
|
|
#include <QGraphicsItemGroup>
|
|
|
|
|
#include <QGraphicsOpacityEffect>
|
|
|
|
|
#include <QMenu>
|
|
|
|
|
#include <QMimeData>
|
|
|
|
|
@@ -17,6 +18,8 @@
|
|
|
|
|
|
|
|
|
|
#include "tools/cabana/chart/chartswidget.h"
|
|
|
|
|
|
|
|
|
|
// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html)
|
|
|
|
|
const int AXIS_X_TOP_MARGIN = 4;
|
|
|
|
|
static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; }
|
|
|
|
|
|
|
|
|
|
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent) : charts_widget(parent), tip_label(this), QChartView(nullptr, parent) {
|
|
|
|
|
@@ -39,6 +42,7 @@ ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *par
|
|
|
|
|
setRubberBand(QChartView::HorizontalRubberBand);
|
|
|
|
|
setMouseTracking(true);
|
|
|
|
|
setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight);
|
|
|
|
|
signal_value_font.setPointSize(9);
|
|
|
|
|
|
|
|
|
|
QObject::connect(axis_y, &QValueAxis::rangeChanged, [this]() { resetChartCache(); });
|
|
|
|
|
QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, [this]() { resetChartCache(); });
|
|
|
|
|
@@ -119,7 +123,7 @@ void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
|
|
|
|
|
return std::any_of(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
|
|
|
|
return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
|
|
|
|
|
@@ -143,14 +147,14 @@ void ChartView::removeIf(std::function<bool(const SigItem &s)> predicate) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChartView::signalUpdated(const cabana::Signal *sig) {
|
|
|
|
|
if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; })) {
|
|
|
|
|
if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.sig == sig; })) {
|
|
|
|
|
updateTitle();
|
|
|
|
|
updateSeries(sig);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChartView::msgUpdated(MessageId id) {
|
|
|
|
|
if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.msg_id == id; }))
|
|
|
|
|
if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id == id; }))
|
|
|
|
|
updateTitle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -189,10 +193,16 @@ void ChartView::updatePlotArea(int left_pos, bool force) {
|
|
|
|
|
|
|
|
|
|
qreal left, top, right, bottom;
|
|
|
|
|
chart()->layout()->getContentsMargins(&left, &top, &right, &bottom);
|
|
|
|
|
chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), manage_btn_proxy->sceneBoundingRect().bottomLeft()});
|
|
|
|
|
QSizeF legend_size = chart()->legend()->layout()->minimumSize();
|
|
|
|
|
legend_size.setWidth(manage_btn_proxy->sceneBoundingRect().left() - move_icon->sceneBoundingRect().right());
|
|
|
|
|
chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), legend_size});
|
|
|
|
|
|
|
|
|
|
// add top space for signal value
|
|
|
|
|
int adjust_top = chart()->legend()->geometry().height() + QFontMetrics(signal_value_font).height() + 3;
|
|
|
|
|
adjust_top = std::max<int>(adjust_top, manage_btn_proxy->sceneBoundingRect().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin));
|
|
|
|
|
// add right space for x-axis label
|
|
|
|
|
QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2));
|
|
|
|
|
x_label_size += QSizeF{5, 5};
|
|
|
|
|
int adjust_top = chart()->legend()->geometry().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin);
|
|
|
|
|
chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom));
|
|
|
|
|
chart()->layout()->invalidate();
|
|
|
|
|
resetChartCache();
|
|
|
|
|
@@ -229,11 +239,11 @@ void ChartView::updatePlot(double cur, double min, double max) {
|
|
|
|
|
void ChartView::updateSeriesPoints() {
|
|
|
|
|
// Show points when zoomed in enough
|
|
|
|
|
for (auto &s : sigs) {
|
|
|
|
|
auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan);
|
|
|
|
|
auto end = std::lower_bound(begin, s.vals.end(), axis_x->max(), xLessThan);
|
|
|
|
|
auto begin = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
|
|
|
|
|
auto end = std::lower_bound(begin, s.vals.cend(), axis_x->max(), xLessThan);
|
|
|
|
|
if (begin != end) {
|
|
|
|
|
int num_points = std::max<int>((end - begin), 1);
|
|
|
|
|
QPointF right_pt = end == s.vals.end() ? s.vals.back() : *end;
|
|
|
|
|
QPointF right_pt = end == s.vals.cend() ? s.vals.back() : *end;
|
|
|
|
|
double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points;
|
|
|
|
|
|
|
|
|
|
if (series_type == SeriesType::Scatter) {
|
|
|
|
|
@@ -305,8 +315,8 @@ void ChartView::updateAxisY() {
|
|
|
|
|
unit.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan);
|
|
|
|
|
auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan);
|
|
|
|
|
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
|
|
|
|
|
auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan);
|
|
|
|
|
s.min = std::numeric_limits<double>::max();
|
|
|
|
|
s.max = std::numeric_limits<double>::lowest();
|
|
|
|
|
if (can->liveStreaming()) {
|
|
|
|
|
@@ -315,7 +325,7 @@ void ChartView::updateAxisY() {
|
|
|
|
|
if (it->y() > s.max) s.max = it->y();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.begin(), first), std::distance(s.vals.begin(), last));
|
|
|
|
|
auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
|
|
|
|
s.min = min_y;
|
|
|
|
|
s.max = max_y;
|
|
|
|
|
}
|
|
|
|
|
@@ -530,8 +540,8 @@ void ChartView::showTip(double sec) {
|
|
|
|
|
if (s.series->isVisible()) {
|
|
|
|
|
QString value = "--";
|
|
|
|
|
// use reverse iterator to find last item <= sec.
|
|
|
|
|
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()) {
|
|
|
|
|
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x() > x; });
|
|
|
|
|
if (it != s.vals.crend() && it->x() >= axis_x->min()) {
|
|
|
|
|
value = QString::number(it->y());
|
|
|
|
|
s.track_pt = *it;
|
|
|
|
|
x = std::max(x, chart()->mapToPosition(*it).x());
|
|
|
|
|
@@ -640,14 +650,7 @@ void ChartView::drawBackground(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();
|
|
|
|
|
x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right());
|
|
|
|
|
qreal y1 = chart()->plotArea().top() - 2;
|
|
|
|
|
qreal y2 = chart()->plotArea().bottom() + 2;
|
|
|
|
|
painter->setPen(QPen(chart()->titleBrush().color(), 2));
|
|
|
|
|
painter->drawLine(QPointF{x, y1}, QPointF{x, y2});
|
|
|
|
|
|
|
|
|
|
drawTimeline(painter);
|
|
|
|
|
// draw track points
|
|
|
|
|
painter->setPen(Qt::NoPen);
|
|
|
|
|
qreal track_line_x = -1;
|
|
|
|
|
@@ -660,16 +663,17 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (track_line_x > 0) {
|
|
|
|
|
auto plot_area = chart()->plotArea();
|
|
|
|
|
painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
|
|
|
|
|
painter->drawLine(QPointF{track_line_x, y1}, QPointF{track_line_x, y2});
|
|
|
|
|
painter->drawLine(QPointF{track_line_x, plot_area.top()}, QPointF{track_line_x, plot_area.bottom()});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// paint points. OpenGL mode lacks certain features (such as showing points)
|
|
|
|
|
painter->setPen(Qt::NoPen);
|
|
|
|
|
for (auto &s : sigs) {
|
|
|
|
|
if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) {
|
|
|
|
|
auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan);
|
|
|
|
|
auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan);
|
|
|
|
|
auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan);
|
|
|
|
|
auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan);
|
|
|
|
|
painter->setBrush(s.series->color());
|
|
|
|
|
for (auto it = first; it != last; ++it) {
|
|
|
|
|
painter->drawEllipse(chart()->mapToPosition(*it), 4, 4);
|
|
|
|
|
@@ -683,9 +687,8 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
|
|
|
|
|
painter->setPen(Qt::white);
|
|
|
|
|
auto rubber_rect = rubber->geometry().normalized();
|
|
|
|
|
for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) {
|
|
|
|
|
QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 1);
|
|
|
|
|
// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html)
|
|
|
|
|
auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -4, 6, 4);
|
|
|
|
|
QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 2);
|
|
|
|
|
auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN);
|
|
|
|
|
pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2});
|
|
|
|
|
painter->fillRect(r, Qt::gray);
|
|
|
|
|
painter->drawText(r, Qt::AlignCenter, sec);
|
|
|
|
|
@@ -693,6 +696,48 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ChartView::drawTimeline(QPainter *painter) {
|
|
|
|
|
const auto plot_area = chart()->plotArea();
|
|
|
|
|
// draw line
|
|
|
|
|
qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right());
|
|
|
|
|
painter->setPen(QPen(chart()->titleBrush().color(), 2));
|
|
|
|
|
painter->drawLine(QPointF{x, plot_area.top()}, QPointF{x, plot_area.bottom() + 1});
|
|
|
|
|
|
|
|
|
|
// draw current time
|
|
|
|
|
QString time_str = QString::number(cur_sec, 'f', 2);
|
|
|
|
|
QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2);
|
|
|
|
|
QRect time_str_rect(QPoint(x - time_str_size.width() / 2, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size);
|
|
|
|
|
QPainterPath path;
|
|
|
|
|
path.addRoundedRect(time_str_rect, 3, 3);
|
|
|
|
|
painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray);
|
|
|
|
|
painter->setPen(palette().color(QPalette::BrightText));
|
|
|
|
|
painter->setFont(axis_x->labelsFont());
|
|
|
|
|
painter->drawText(time_str_rect, Qt::AlignCenter, time_str);
|
|
|
|
|
|
|
|
|
|
// draw signal value
|
|
|
|
|
auto item_group = qgraphicsitem_cast<QGraphicsItemGroup *>(chart()->legend()->childItems()[0]);
|
|
|
|
|
assert(item_group != nullptr);
|
|
|
|
|
auto legend_markers = item_group->childItems();
|
|
|
|
|
assert(legend_markers.size() == sigs.size());
|
|
|
|
|
|
|
|
|
|
painter->setFont(signal_value_font);
|
|
|
|
|
painter->setPen(chart()->legend()->labelColor());
|
|
|
|
|
int i = 0;
|
|
|
|
|
for (auto &s : sigs) {
|
|
|
|
|
QString value = "--";
|
|
|
|
|
if (s.series->isVisible()) {
|
|
|
|
|
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, [](auto &p, double x) { return p.x() > x; });
|
|
|
|
|
if (it != s.vals.crend() && it->x() >= axis_x->min()) {
|
|
|
|
|
value = s.sig->formatValue(it->y());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
QRectF marker_rect = legend_markers[i++]->sceneBoundingRect();
|
|
|
|
|
QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size());
|
|
|
|
|
QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width());
|
|
|
|
|
painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QXYSeries *ChartView::createSeries(SeriesType type, QColor color) {
|
|
|
|
|
QXYSeries *series = nullptr;
|
|
|
|
|
if (type == SeriesType::Line) {
|
|
|
|
|
|