forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
790
iqpilot/tools/cabana/ui/chart/chart.cc
Normal file
@@ -0,0 +1,790 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
|
||||
#include "tools/cabana/core/settings.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
const int AXIS_X_TOP_MARGIN = 4;
|
||||
const int X_TICK_COUNT = 5;
|
||||
const double MIN_ZOOM_SECONDS = 0.01;
|
||||
const double EPSILON = 1e-6;
|
||||
constexpr ImVec4 LAYOUT_MARGINS{8, 6, 8, 6};
|
||||
static inline bool xLessThan(const ImPlotPoint &p, double x) { return p.x < (x - EPSILON); }
|
||||
static inline bool isNull(const ImPlotPoint &p) { return p.x == 0 && p.y == 0; }
|
||||
|
||||
static std::string formatNumber(double value, int precision) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.*f", precision, value);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
static int axisPrecision(double range, int tick_count, int min_precision) {
|
||||
return std::max(int(-std::floor(std::log10(range / (tick_count - 1)))), min_precision);
|
||||
}
|
||||
|
||||
static void addTextEllipsis(ImDrawList *dl, ImFont *font, ImU32 col, const ImVec2 &pos, float max_x, const std::string &text) {
|
||||
const float size = ImGui::GetFontSize();
|
||||
ImGui::PushFont(font, 0.0f);
|
||||
ImGui::RenderTextEllipsis(dl, pos, ImVec2(max_x, pos.y + size), max_x, text.c_str(), nullptr, nullptr);
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
ChartView::ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent)
|
||||
: x_min_(x_range.first), x_max_(x_range.second), charts_widget_(parent) {
|
||||
series_type_ = (SeriesType)settings.chart_series_type;
|
||||
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); }));
|
||||
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); }));
|
||||
}
|
||||
|
||||
void ChartView::drawMenuActions() {
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
float label_width = ImGui::CalcTextSize("Manage Signals").x;
|
||||
for (const char *type : SERIES_TYPE_NAMES) label_width = std::max(label_width, ImGui::CalcTextSize(type).x);
|
||||
for (int i = 0; i < (int)std::size(SERIES_TYPE_NAMES); ++i) {
|
||||
if (radioMenuItem(SERIES_TYPE_NAMES[i], i == (int)series_type_, indent + label_width + indent)) {
|
||||
setSeriesType((SeriesType)i);
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::Indent(indent);
|
||||
if (ImGui::MenuItem("Manage Signals")) manageSignals();
|
||||
if (ImGui::MenuItem("Split Chart", nullptr, false, sigs_.size() > 1)) charts_widget_->splitChart(this);
|
||||
ImGui::Unindent(indent);
|
||||
}
|
||||
|
||||
|
||||
void ChartView::createToolButtons() {
|
||||
ImGui::SetCursorScreenPos(layout_.close_btn_rect.Min);
|
||||
bool close_clicked = toolButton("close_btn", icon::X, "Remove Chart");
|
||||
|
||||
ImGui::SetCursorScreenPos(layout_.manage_btn_rect.Min);
|
||||
if (toolButton("manage_btn", icon::LIST, "")) ImGui::OpenPopup("manage_menu");
|
||||
if (ImGui::BeginPopup("manage_menu")) {
|
||||
drawMenuActions();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (close_clicked) charts_widget_->removeChart(this);
|
||||
}
|
||||
|
||||
void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) {
|
||||
if (hasSignal(msg_id, sig)) return;
|
||||
|
||||
sigs_.push_back({.msg_id = msg_id, .sig = sig, .color = uniqueColor(sig->color)});
|
||||
updateSeries(sig);
|
||||
charts_widget_->seriesChanged();
|
||||
}
|
||||
|
||||
bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const {
|
||||
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) {
|
||||
int prev_size = sigs_.size();
|
||||
sigs_.erase(std::remove_if(sigs_.begin(), sigs_.end(), predicate), sigs_.end());
|
||||
if (sigs_.empty()) {
|
||||
charts_widget_->removeChart(this);
|
||||
} else if (sigs_.size() != prev_size) {
|
||||
charts_widget_->seriesChanged();
|
||||
updateAxisY();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::signalUpdated(const cabana::Signal *sig) {
|
||||
auto it = std::find_if(sigs_.begin(), sigs_.end(), [sig](auto &s) { return s.sig == sig; });
|
||||
if (it != sigs_.end()) {
|
||||
if (!(it->color == sig->color)) {
|
||||
it->color = uniqueColor(sig->color, sig);
|
||||
}
|
||||
updateSeries(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::manageSignals() {
|
||||
auto dlg = std::make_unique<SignalSelector>("Manage Chart");
|
||||
for (auto &s : sigs_) {
|
||||
dlg->addSelected(s.msg_id, s.sig);
|
||||
}
|
||||
|
||||
charts_widget_->execSignalSelector(std::move(dlg), this, [this](SignalSelector &selector) {
|
||||
const auto &items = selector.selectedItems();
|
||||
for (const auto &s : items) {
|
||||
addSignal(s.msg_id, s.sig);
|
||||
}
|
||||
removeIf([&](auto &s) {
|
||||
return std::none_of(items.cbegin(), items.cend(), [&](auto &it) { return s.msg_id == it.msg_id && s.sig == it.sig; });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ChartView::updateLayout() {
|
||||
const ImVec2 grip = ImGui::CalcTextSize(icon::GRIP_HORIZONTAL);
|
||||
const ImVec2 top_left = layout_.rect.Min + ImVec2(LAYOUT_MARGINS.x, LAYOUT_MARGINS.y);
|
||||
layout_.move_icon_rect = ImRect(top_left, top_left + grip);
|
||||
const ImVec2 pad = ImGui::GetStyle().FramePadding * 2;
|
||||
const ImVec2 close_size = ImGui::CalcTextSize(icon::X) + pad;
|
||||
const ImVec2 manage_size = ImGui::CalcTextSize(icon::LIST) + pad;
|
||||
const ImVec2 close_min(layout_.rect.Max.x - LAYOUT_MARGINS.z - close_size.x, top_left.y);
|
||||
layout_.close_btn_rect = ImRect(close_min, close_min + close_size);
|
||||
const ImVec2 manage_min(close_min.x - manage_size.x - ImGui::GetStyle().ItemSpacing.x, top_left.y);
|
||||
layout_.manage_btn_rect = ImRect(manage_min, manage_min + manage_size);
|
||||
|
||||
ImFont *bold = boldFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float fm_height = ImGui::GetTextLineHeight();
|
||||
const int marker_size = markerSize();
|
||||
const int row_height = std::max<int>(marker_size, fm_height) + fm_height + 3;
|
||||
const int legend_left = layout_.move_icon_rect.Max.x + LAYOUT_MARGINS.x;
|
||||
const int legend_right = std::max<int>(layout_.manage_btn_rect.Min.x - LAYOUT_MARGINS.z, legend_left + 10);
|
||||
|
||||
|
||||
layout_.legend_rects.clear();
|
||||
int x = legend_left, y = top_left.y;
|
||||
for (auto &s : sigs_) {
|
||||
int w = marker_size + 5 + bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x +
|
||||
ImGui::CalcTextSize(msgLabel(s.msg_id).c_str()).x;
|
||||
w = std::min(w, legend_right - legend_left);
|
||||
if (x + w > legend_right && x > legend_left) {
|
||||
x = legend_left;
|
||||
y += row_height;
|
||||
}
|
||||
layout_.legend_rects.emplace_back(ImVec2(x, y), ImVec2(x + w, y + std::max<int>(marker_size, fm_height)));
|
||||
x += w + 12;
|
||||
}
|
||||
|
||||
|
||||
int adjust_top = (y + row_height) - top_left.y;
|
||||
adjust_top = std::max<int>(adjust_top, layout_.manage_btn_rect.Max.y - layout_.rect.Min.y + LAYOUT_MARGINS.y);
|
||||
layout_.header_bottom = layout_.rect.Min.y + adjust_top + LAYOUT_MARGINS.y;
|
||||
}
|
||||
|
||||
void ChartView::updatePlot(double cur, double min, double max) {
|
||||
cur_sec_ = cur;
|
||||
if (min != x_min_ || max != x_max_) {
|
||||
x_min_ = min;
|
||||
x_max_ = max;
|
||||
updateAxisY();
|
||||
if (tooltip_x_ >= 0) {
|
||||
showTip(secondsAtPoint({(float)tooltip_x_, 0}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals) {
|
||||
vals.reserve(vals.size() + events.size());
|
||||
step_vals.reserve(step_vals.size() + events.size() * 2);
|
||||
|
||||
double value = 0;
|
||||
for (const CanEvent *e : events) {
|
||||
if (sig->getValue(e->dat, e->size, &value)) {
|
||||
const double ts = can->toSeconds(e->mono_time);
|
||||
vals.emplace_back(ts, value);
|
||||
if (!step_vals.empty())
|
||||
step_vals.emplace_back(ts, step_vals.back().y);
|
||||
step_vals.emplace_back(ts, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap *msg_new_events) {
|
||||
for (auto &s : sigs_) {
|
||||
if (!sig || s.sig == sig) {
|
||||
if (!msg_new_events) {
|
||||
s.vals.clear();
|
||||
s.step_vals.clear();
|
||||
}
|
||||
auto events = msg_new_events ? msg_new_events : &can->eventsMap();
|
||||
auto it = events->find(s.msg_id);
|
||||
if (it == events->end() || it->second.empty()) continue;
|
||||
|
||||
if (s.vals.empty() || can->toSeconds(it->second.back()->mono_time) > s.vals.back().x) {
|
||||
appendCanEvents(s.sig, it->second, s.vals, s.step_vals);
|
||||
} else {
|
||||
std::vector<ImPlotPoint> vals, step_vals;
|
||||
appendCanEvents(s.sig, it->second, vals, step_vals);
|
||||
if (vals.empty()) continue;
|
||||
s.vals.insert(std::lower_bound(s.vals.begin(), s.vals.end(), vals.front().x, xLessThan),
|
||||
vals.begin(), vals.end());
|
||||
s.step_vals.insert(std::lower_bound(s.step_vals.begin(), s.step_vals.end(), step_vals.front().x, xLessThan),
|
||||
step_vals.begin(), step_vals.end());
|
||||
}
|
||||
|
||||
if (!can->liveStreaming()) {
|
||||
s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y; });
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAxisY();
|
||||
}
|
||||
|
||||
std::pair<ChartView::PointIter, ChartView::PointIter> ChartView::visibleRange(const std::vector<ImPlotPoint> &points) const {
|
||||
auto first = std::lower_bound(points.cbegin(), points.cend(), x_min_, xLessThan);
|
||||
auto last = std::lower_bound(first, points.cend(), x_max_, xLessThan);
|
||||
return {first, last};
|
||||
}
|
||||
|
||||
const ImPlotPoint *ChartView::lastPointBefore(const SigItem &s, double sec) const {
|
||||
auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x > x + EPSILON; });
|
||||
return it != s.vals.crend() && it->x >= x_min_ ? &*it : nullptr;
|
||||
}
|
||||
|
||||
void ChartView::updateAxisY() {
|
||||
if (sigs_.empty()) return;
|
||||
|
||||
double min = std::numeric_limits<double>::max();
|
||||
double max = std::numeric_limits<double>::lowest();
|
||||
std::string unit = sigs_[0].sig->unit;
|
||||
|
||||
for (auto &s : sigs_) {
|
||||
if (!s.visible) continue;
|
||||
|
||||
|
||||
if (unit != s.sig->unit) {
|
||||
unit.clear();
|
||||
}
|
||||
|
||||
auto [first, last] = visibleRange(s.vals);
|
||||
s.min = std::numeric_limits<double>::max();
|
||||
s.max = std::numeric_limits<double>::lowest();
|
||||
if (can->liveStreaming()) {
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (it->y < s.min) s.min = it->y;
|
||||
if (it->y > s.max) s.max = it->y;
|
||||
}
|
||||
} else {
|
||||
std::tie(s.min, s.max) = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last));
|
||||
}
|
||||
min = std::min(min, s.min);
|
||||
max = std::max(max, s.max);
|
||||
}
|
||||
if (min == std::numeric_limits<double>::max()) min = 0;
|
||||
if (max == std::numeric_limits<double>::lowest()) max = 0;
|
||||
|
||||
y_unit_ = unit;
|
||||
|
||||
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, 3);
|
||||
if (min_y != y_min_ || max_y != y_max_) {
|
||||
y_min_ = min_y;
|
||||
y_max_ = max_y;
|
||||
y_tick_count_ = tick_count;
|
||||
y_precision_ = axisPrecision(max_y - min_y, tick_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
std::tuple<double, double, int> ChartView::getNiceAxisNumbers(double min, double max, int tick_count) {
|
||||
double range = niceNumber((max - min), true);
|
||||
double step = niceNumber(range / (tick_count - 1), false);
|
||||
min = std::floor(min / step);
|
||||
max = std::ceil(max / step);
|
||||
tick_count = int(max - min) + 1;
|
||||
return {min * step, max * step, tick_count};
|
||||
}
|
||||
|
||||
int ChartView::xAxisPrecision() const {
|
||||
return axisPrecision(x_max_ - x_min_, X_TICK_COUNT, 2);
|
||||
}
|
||||
|
||||
|
||||
double ChartView::niceNumber(double x, bool ceiling) {
|
||||
double z = std::pow(10, std::floor(std::log10(x)));
|
||||
double q = x / z;
|
||||
if (ceiling) {
|
||||
if (q <= 1.0) q = 1;
|
||||
else if (q <= 2.0) q = 2;
|
||||
else if (q <= 5.0) q = 5;
|
||||
else q = 10;
|
||||
} else {
|
||||
if (q < 1.5) q = 1;
|
||||
else if (q < 3.0) q = 2;
|
||||
else if (q < 7.0) q = 5;
|
||||
else q = 10;
|
||||
}
|
||||
return q * z;
|
||||
}
|
||||
|
||||
void ChartView::drawContextMenu() {
|
||||
if (drawing_ghost_) return;
|
||||
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) &&
|
||||
!ImGui::IsAnyItemActive()) {
|
||||
ImGui::OpenPopup("context_menu");
|
||||
}
|
||||
context_menu_id_ = ImGui::GetID("context_menu");
|
||||
if (ImGui::BeginPopup("context_menu")) {
|
||||
drawMenuActions();
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
ImGui::Indent(indent);
|
||||
ImGui::Separator();
|
||||
|
||||
if (can->timeRange().has_value()) {
|
||||
const std::string undo_text = std::string(icon::ARROW_COUNTERCLOCKWISE) + " Undo Zoom";
|
||||
const std::string redo_text = std::string(icon::ARROW_CLOCKWISE) + " Redo Zoom";
|
||||
if (ImGui::MenuItem(undo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canUndo())) charts_widget_->zoom_undo_stack_.undo();
|
||||
if (ImGui::MenuItem(redo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canRedo())) charts_widget_->zoom_undo_stack_.redo();
|
||||
ImGui::Separator();
|
||||
}
|
||||
if (ImGui::MenuItem("Close")) charts_widget_->removeChart(this);
|
||||
ImGui::Unindent(indent);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMousePress() {
|
||||
if (drawing_ghost_) return;
|
||||
const ImVec2 pos = ImGui::GetMousePos();
|
||||
|
||||
const bool widget_pressed = ImGui::IsMouseClicked(ImGuiMouseButton_Left) && layout_.rect.Contains(pos) &&
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
||||
!layout_.close_btn_rect.Contains(pos) && !layout_.manage_btn_rect.Contains(pos);
|
||||
if (!widget_pressed) return;
|
||||
press_pos_ = pos;
|
||||
if (layout_.move_icon_rect.Contains(pos)) return;
|
||||
|
||||
if (ImGui::GetIO().KeyShift) {
|
||||
|
||||
resume_after_scrub_ = !can->isPaused();
|
||||
if (resume_after_scrub_) {
|
||||
can->pause(true);
|
||||
}
|
||||
mouse_mode_ = MouseMode::Scrub;
|
||||
} else if (layout_.plot_area.Contains(pos)) {
|
||||
mouse_mode_ = MouseMode::Rubber;
|
||||
rubber_rect_ = ImRect();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMouseMove() {
|
||||
if (drawing_ghost_) return;
|
||||
const ImVec2 pos = ImGui::GetMousePos();
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
|
||||
if (delta.x == 0 && delta.y == 0) return;
|
||||
|
||||
if (mouse_mode_ == MouseMode::None && !layout_.rect.Contains(pos)) return;
|
||||
|
||||
if (mouse_mode_ == MouseMode::Scrub && ImGui::GetIO().KeyShift) {
|
||||
if (layout_.plot_area.Contains(pos)) {
|
||||
can->seekTo(std::clamp(secondsAtPoint(pos), can->minSeconds(), can->maxSeconds()));
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse_mode_ == MouseMode::Rubber) {
|
||||
|
||||
float left = std::clamp(std::min(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
float right = std::clamp(std::max(press_pos_.x, pos.x), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
rubber_rect_ = ImRect(ImVec2(left, layout_.plot_area.Min.y), ImVec2(right, layout_.plot_area.Max.y));
|
||||
}
|
||||
|
||||
clearTrackPoints();
|
||||
if (mouse_mode_ != MouseMode::Rubber && layout_.plot_area.Contains(pos) && (layout_.plot_hovered || mouse_mode_ != MouseMode::None) &&
|
||||
ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||
charts_widget_->showValueTip(secondsAtPoint(pos));
|
||||
} else if (tip_label_.isVisible()) {
|
||||
charts_widget_->showValueTip(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::handleMouseRelease() {
|
||||
if (drawing_ghost_) return;
|
||||
const bool left_released = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
const bool right_released = ImGui::IsMouseReleased(ImGuiMouseButton_Right) && layout_.rect.Contains(ImGui::GetMousePos());
|
||||
if (!left_released && !right_released) return;
|
||||
if (left_released && mouse_mode_ == MouseMode::Rubber) {
|
||||
mouse_mode_ = MouseMode::None;
|
||||
|
||||
double min = std::clamp(secondsAtPoint(rubber_rect_.Min), can->minSeconds(), can->maxSeconds());
|
||||
double max = std::clamp(secondsAtPoint(rubber_rect_.Max), can->minSeconds(), can->maxSeconds());
|
||||
if (rubber_rect_.GetWidth() <= 0) {
|
||||
|
||||
can->seekTo(std::clamp(secondsAtPoint(press_pos_), can->minSeconds(), can->maxSeconds()));
|
||||
} else if (rubber_rect_.GetWidth() > 10 && (max - min) > MIN_ZOOM_SECONDS) {
|
||||
charts_widget_->zoom_undo_stack_.push(new ZoomCommand({min, max}));
|
||||
}
|
||||
rubber_rect_ = ImRect();
|
||||
} else if (right_released && !ImGui::IsPopupOpen(context_menu_id_, ImGuiPopupFlags_None)) {
|
||||
charts_widget_->zoom_undo_stack_.undo();
|
||||
}
|
||||
|
||||
if (mouse_mode_ == MouseMode::Scrub) {
|
||||
mouse_mode_ = MouseMode::None;
|
||||
if (resume_after_scrub_) {
|
||||
can->pause(false);
|
||||
resume_after_scrub_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::takeSignalsFrom(ChartView *source) {
|
||||
for (auto &s : source->sigs_) {
|
||||
sigs_.push_back(std::move(s));
|
||||
sigs_.back().color = uniqueColor(sigs_.back().color, sigs_.back().sig);
|
||||
}
|
||||
source->sigs_.clear();
|
||||
updateAxisY();
|
||||
charts_widget_->removeChart(source);
|
||||
}
|
||||
|
||||
std::vector<ChartView::SigItem> ChartView::takeExtraSignals() {
|
||||
std::vector<SigItem> extra;
|
||||
for (auto it = sigs_.begin() + 1; it != sigs_.end(); ++it) {
|
||||
it->color = it->sig->color;
|
||||
extra.push_back(std::move(*it));
|
||||
}
|
||||
sigs_.resize(1);
|
||||
updateAxisY();
|
||||
return extra;
|
||||
}
|
||||
|
||||
void ChartView::adoptSignal(SigItem s) {
|
||||
sigs_.push_back(std::move(s));
|
||||
updateAxisY();
|
||||
}
|
||||
|
||||
void ChartView::showTip(double sec) {
|
||||
ImRect tip_area(ImVec2(layout_.rect.Min.x, layout_.plot_area.Min.y), ImVec2(layout_.rect.Max.x, layout_.plot_area.Max.y));
|
||||
ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||
visible_rect.ClipWith(tip_area);
|
||||
if (visible_rect.GetWidth() <= 0 || visible_rect.GetHeight() <= 0) {
|
||||
tip_label_.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip_x_ = xPos(sec);
|
||||
float x = -1;
|
||||
std::vector<TipLine> text_list;
|
||||
for (auto &s : sigs_) {
|
||||
if (s.visible) {
|
||||
std::string value = "--";
|
||||
if (const ImPlotPoint *pt = lastPointBefore(s, sec)) {
|
||||
value = s.sig->formatValue(pt->y, false);
|
||||
s.track_pt = *pt;
|
||||
x = std::max(x, xPos(pt->x));
|
||||
}
|
||||
std::string name = sigs_.size() > 1 ? s.sig->name + ": " : "";
|
||||
std::string min = s.min == std::numeric_limits<double>::max() ? "--" : utils::toString(s.min);
|
||||
std::string max = s.max == std::numeric_limits<double>::lowest() ? "--" : utils::toString(s.max);
|
||||
text_list.push_back({.has_marker = true, .marker = toImU32(s.color), .name = name, .bold = value, .rest = " (" + min + ", " + max + ")"});
|
||||
}
|
||||
}
|
||||
if (x < 0) {
|
||||
x = tooltip_x_;
|
||||
}
|
||||
ImVec2 pt(x, layout_.plot_area.Min.y);
|
||||
text_list.insert(text_list.begin(), TipLine{.name = formatNumber(secondsAtPoint({x, 0}), 3)});
|
||||
tip_label_.showText(pt, text_list, visible_rect);
|
||||
}
|
||||
|
||||
void ChartView::hideTip() {
|
||||
clearTrackPoints();
|
||||
tooltip_x_ = -1;
|
||||
tip_label_.hide();
|
||||
}
|
||||
|
||||
void ChartView::draw(float width) {
|
||||
ImGui::PushID(this);
|
||||
width = std::max(width, (float)CHART_MIN_WIDTH);
|
||||
layout_.plot_hovered = false;
|
||||
|
||||
const ImVec2 tile_pos = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 tile_size(width, (float)settings.chart_height);
|
||||
layout_.rect = ImRect(tile_pos, tile_pos + tile_size);
|
||||
if (ImGui::BeginChild("chart", tile_size, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
|
||||
updateLayout();
|
||||
paint();
|
||||
drawContextMenu();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
const ImRect visible_rect = charts_widget_->chartVisibleRect(this);
|
||||
if (!drawing_ghost_ && visible_rect.GetWidth() > 0 && visible_rect.GetHeight() > 0) tip_label_.draw();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ChartView::drawGhost(float width) {
|
||||
|
||||
drawing_ghost_ = true;
|
||||
const Layout saved = layout_;
|
||||
draw(width);
|
||||
layout_ = saved;
|
||||
drawing_ghost_ = false;
|
||||
}
|
||||
|
||||
void ChartView::paint() {
|
||||
drawStaticLayer();
|
||||
|
||||
if (can_drop_) {
|
||||
ImGui::GetWindowDrawList()->AddRect(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_Header), 0.0f, 0, 4.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawStaticLayer() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
painter->AddRectFilled(layout_.rect.Min, layout_.rect.Max, ImGui::GetColorU32(ImGuiCol_ChildBg));
|
||||
ImGui::SetCursorScreenPos(layout_.move_icon_rect.Min);
|
||||
ImGui::InvisibleButton("grip", layout_.move_icon_rect.GetSize());
|
||||
if (ImGui::IsItemActivated()) charts_widget_->startChartDrag(this, ImGui::GetMousePos());
|
||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
painter->AddText(layout_.move_icon_rect.Min, ImGui::GetColorU32(ImGuiCol_Text), icon::GRIP_HORIZONTAL);
|
||||
createToolButtons();
|
||||
drawLegend();
|
||||
drawSignalValue();
|
||||
drawAxes();
|
||||
}
|
||||
|
||||
void ChartView::drawAxes() {
|
||||
ImGui::SetCursorScreenPos(ImVec2(layout_.rect.Min.x, layout_.header_bottom));
|
||||
const float plot_h = std::max(layout_.rect.Max.y - layout_.header_bottom - LAYOUT_MARGINS.w, 10.0f);
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_PlotPadding, ImVec2(LAYOUT_MARGINS.x, AXIS_X_TOP_MARGIN));
|
||||
ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0));
|
||||
ImPlot::PushStyleColor(ImPlotCol_FrameBg, ImVec4(0, 0, 0, 0));
|
||||
|
||||
|
||||
const bool dark = isDarkTheme();
|
||||
ImVec4 grid_color;
|
||||
if (dark) {
|
||||
grid_color = colorRgb(DarkTheme::light.r, DarkTheme::light.g, DarkTheme::light.b);
|
||||
} else {
|
||||
grid_color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
|
||||
grid_color.w = 50.0f / 255.0f;
|
||||
}
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisGrid, grid_color);
|
||||
ImPlot::PushStyleColor(ImPlotCol_PlotBorder, grid_color);
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisTick, ImVec4(0, 0, 0, 0));
|
||||
ImPlot::PushStyleColor(ImPlotCol_AxisText, ImGui::GetStyleColorVec4(ImGuiCol_Text));
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_MajorTickLen, ImVec2(0, 0));
|
||||
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_MajorGridSize, dark ? ImVec2(2.0f, 2.0f) : ImVec2(1.0f, 1.0f));
|
||||
const ImPlotFlags flags = ImPlotFlags_NoTitle | ImPlotFlags_NoLegend | ImPlotFlags_NoMenus | ImPlotFlags_NoMouseText |
|
||||
ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs | ImPlotFlags_NoFrame;
|
||||
const ImPlotAxisFlags axis_flags = ImPlotAxisFlags_NoMenus | ImPlotAxisFlags_NoHighlight | ImPlotAxisFlags_NoSideSwitch | ImPlotAxisFlags_Lock;
|
||||
|
||||
const float x_label_width = ImGui::CalcTextSize(formatNumber(x_max_, xAxisPrecision()).c_str()).x + 5;
|
||||
if (ImPlot::BeginPlot("##plot", ImVec2(layout_.rect.GetWidth() - x_label_width / 2, plot_h), flags)) {
|
||||
ImPlot::SetupAxis(ImAxis_X1, nullptr, axis_flags);
|
||||
ImPlot::SetupAxis(ImAxis_Y1, y_unit_.empty() ? nullptr : y_unit_.c_str(), axis_flags);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, x_min_, x_max_, ImPlotCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, y_min_, y_max_, ImPlotCond_Always);
|
||||
|
||||
ImPlot::SetupAxisFormat(ImAxis_Y1, ("%." + std::to_string(y_precision_) + "f").c_str());
|
||||
ImPlot::SetupAxisTicks(ImAxis_Y1, y_min_, y_max_, y_tick_count_);
|
||||
ImPlot::SetupAxisFormat(ImAxis_X1, ("%." + std::to_string(xAxisPrecision()) + "f").c_str());
|
||||
ImPlot::SetupAxisTicks(ImAxis_X1, x_min_, x_max_, X_TICK_COUNT);
|
||||
ImPlot::SetupFinish();
|
||||
|
||||
layout_.plot_area = ImRect(ImPlot::GetPlotPos(), ImPlot::GetPlotPos() + ImPlot::GetPlotSize());
|
||||
|
||||
layout_.plot_hovered = layout_.plot_area.Contains(ImGui::GetMousePos()) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||
drawSeries();
|
||||
handleMousePress();
|
||||
handleMouseMove();
|
||||
handleMouseRelease();
|
||||
drawForeground();
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
ImPlot::PopStyleColor(6);
|
||||
ImPlot::PopStyleVar(3);
|
||||
}
|
||||
|
||||
void ChartView::drawLegend() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
const ImU32 title_color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
const ImU32 msg_color = withAlpha(title_color, 180);
|
||||
ImFont *bold = boldFont();
|
||||
ImFont *normal = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float marker_size = markerSize();
|
||||
|
||||
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||
const auto &s = sigs_[i];
|
||||
const ImRect &r = layout_.legend_rects[i];
|
||||
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetCursorScreenPos(r.Min);
|
||||
if (ImGui::InvisibleButton("legend", ImVec2(std::max(r.GetWidth(), 1.0f), std::max(r.GetHeight(), 1.0f))) &&
|
||||
mouse_mode_ == MouseMode::None && sigs_.size() > 1) {
|
||||
sigs_[i].visible = !sigs_[i].visible;
|
||||
updateAxisY();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
if (series_type_ == SeriesType::Scatter) {
|
||||
painter->AddCircleFilled(r.Min + ImVec2(marker_size / 2.0f, 2.0f + marker_size / 2.0f), marker_size / 2.0f, toImU32(s.color));
|
||||
} else {
|
||||
drawColorMarker(painter, r.Min, toImU32(s.color));
|
||||
}
|
||||
|
||||
float x = r.Min.x + marker_size + 5;
|
||||
const float text_y = r.GetCenter().y - font_size / 2.0f;
|
||||
addTextEllipsis(painter, bold, title_color, ImVec2(x, text_y), r.Max.x, s.sig->name);
|
||||
float name_w = std::min(bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, s.sig->name.c_str()).x, r.Max.x - x);
|
||||
x += name_w;
|
||||
const std::string msg = msgLabel(s.msg_id);
|
||||
addTextEllipsis(painter, normal, msg_color, ImVec2(x, text_y), r.Max.x, msg);
|
||||
if (!s.visible) {
|
||||
const float y = r.GetCenter().y;
|
||||
painter->AddLine(ImVec2(r.Min.x + marker_size + 5, y), ImVec2(std::min(x + ImGui::CalcTextSize(msg.c_str()).x, r.Max.x), y), title_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawSeries() {
|
||||
for (int i = 0; i < sigs_.size(); ++i) {
|
||||
auto &s = sigs_[i];
|
||||
if (!s.visible) continue;
|
||||
|
||||
|
||||
auto [first, last] = visibleRange(s.vals);
|
||||
int num_points = std::max<int>(last - first, 1);
|
||||
double pixels_per_point = 0;
|
||||
if (first != last) {
|
||||
const ImPlotPoint &right_pt = last == s.vals.cend() ? s.vals.back() : *last;
|
||||
pixels_per_point = (xPos(right_pt.x) - xPos(first->x)) / num_points;
|
||||
}
|
||||
|
||||
const std::string label = "##sig" + std::to_string(i);
|
||||
ImPlotSpec spec;
|
||||
spec.LineColor = toImVec4(s.color);
|
||||
spec.Stride = sizeof(ImPlotPoint);
|
||||
if (series_type_ == SeriesType::Scatter) {
|
||||
float radius = std::clamp(pixels_per_point / 2.0, 2.0, 8.0) / 2.0;
|
||||
spec.Marker = ImPlotMarker_Circle;
|
||||
spec.MarkerSize = radius;
|
||||
if (first != last) ImPlot::PlotScatter(label.c_str(), &first->x, &first->y, last - first, spec);
|
||||
} else {
|
||||
const auto &points = series_type_ == SeriesType::StepLine ? s.step_vals : s.vals;
|
||||
|
||||
auto [begin, end] = visibleRange(points);
|
||||
if (begin != points.cbegin()) --begin;
|
||||
if (end != points.cend()) ++end;
|
||||
if (begin == end) continue;
|
||||
|
||||
spec.LineWeight = 2;
|
||||
ImPlot::PlotLine(label.c_str(), &begin->x, &begin->y, end - begin, spec);
|
||||
|
||||
|
||||
if ((num_points == 1 || pixels_per_point > 20) && first != last) {
|
||||
ImPlotSpec dots;
|
||||
dots.LineColor = toImVec4(s.color);
|
||||
dots.Stride = sizeof(ImPlotPoint);
|
||||
dots.Marker = ImPlotMarker_Circle;
|
||||
dots.MarkerSize = 4;
|
||||
ImPlot::PlotScatter((label + "_pts").c_str(), &first->x, &first->y, last - first, dots);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartView::drawForeground() {
|
||||
drawTimeline();
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
ImPlot::PushPlotClipRect();
|
||||
float track_line_x = -1;
|
||||
for (auto &s : sigs_) {
|
||||
if (!isNull(s.track_pt) && s.visible) {
|
||||
ImVec2 pos(xPos(s.track_pt.x), yPos(s.track_pt.y));
|
||||
painter->AddCircleFilled(pos, 5.5f, toImU32(s.color.darker(125)));
|
||||
track_line_x = std::max(track_line_x, pos.x);
|
||||
}
|
||||
}
|
||||
if (track_line_x > 0) {
|
||||
const ImU32 dark_gray = IM_COL32(0x80, 0x80, 0x80, 0xff);
|
||||
for (float y = layout_.plot_area.Min.y; y < layout_.plot_area.Max.y; y += 8) {
|
||||
painter->AddLine(ImVec2(track_line_x, y), ImVec2(track_line_x, std::min(y + 4, layout_.plot_area.Max.y)), dark_gray, 1.0f);
|
||||
}
|
||||
}
|
||||
ImPlot::PopPlotClipRect();
|
||||
|
||||
drawRubberBandTimeRange();
|
||||
}
|
||||
|
||||
void ChartView::drawRubberBandTimeRange() {
|
||||
if (rubber_rect_.GetWidth() <= 1) return;
|
||||
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
|
||||
const ImU32 highlight = withAlpha(ImGui::GetColorU32(ImGuiCol_Header), 255);
|
||||
painter->AddRectFilled(rubber_rect_.Min, rubber_rect_.Max, withAlpha(highlight, 50));
|
||||
painter->AddRect(rubber_rect_.Min, rubber_rect_.Max, highlight);
|
||||
|
||||
|
||||
const ImU32 white = IM_COL32_WHITE;
|
||||
const ImU32 gray = IM_COL32(0xa0, 0xa0, 0xa4, 0xff);
|
||||
painter = ImGui::GetWindowDrawList();
|
||||
painter->PushClipRect(layout_.rect.Min, layout_.rect.Max);
|
||||
for (const auto &pt : {rubber_rect_.GetBL(), rubber_rect_.GetBR()}) {
|
||||
std::string sec = formatNumber(secondsAtPoint(pt), 2);
|
||||
ImVec2 size = ImGui::CalcTextSize(sec.c_str()) + ImVec2(12, AXIS_X_TOP_MARGIN * 2);
|
||||
ImVec2 top_left = pt.x == rubber_rect_.Min.x ? ImVec2(pt.x - size.x, pt.y + 2) : ImVec2(pt.x, pt.y + 2);
|
||||
painter->AddRectFilled(top_left, top_left + size, gray);
|
||||
painter->AddText(top_left + ImVec2(6, AXIS_X_TOP_MARGIN), white, sec.c_str());
|
||||
}
|
||||
painter->PopClipRect();
|
||||
}
|
||||
|
||||
void ChartView::drawTimeline() {
|
||||
ImDrawList *painter = ImPlot::GetPlotDrawList();
|
||||
float x = std::clamp(xPos(cur_sec_), layout_.plot_area.Min.x, layout_.plot_area.Max.x);
|
||||
painter->AddLine(ImVec2(x, layout_.plot_area.Min.y - 1.0f), ImVec2(x, layout_.plot_area.Max.y + 1.0f), ImGui::GetColorU32(ImGuiCol_Text), 1.0f);
|
||||
|
||||
std::string time_str = formatNumber(cur_sec_, 2);
|
||||
ImVec2 time_str_size = ImGui::CalcTextSize(time_str.c_str()) + ImVec2(8, 2);
|
||||
ImVec2 time_str_pos(x - time_str_size.x / 2.0f, layout_.plot_area.Max.y + AXIS_X_TOP_MARGIN);
|
||||
const bool dark = isDarkTheme();
|
||||
painter->AddRectFilled(time_str_pos, time_str_pos + time_str_size, dark ? IM_COL32(0x80, 0x80, 0x80, 0xff) : IM_COL32(0xa0, 0xa0, 0xa4, 0xff), 3.0f);
|
||||
painter->AddText(time_str_pos + ImVec2(4, 1), IM_COL32_WHITE, time_str.c_str());
|
||||
}
|
||||
|
||||
void ChartView::drawSignalValue() {
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
for (int i = 0; i < sigs_.size() && i < layout_.legend_rects.size(); ++i) {
|
||||
const auto &s = sigs_[i];
|
||||
const ImPlotPoint *pt = lastPointBefore(s, cur_sec_);
|
||||
std::string value = pt ? s.sig->formatValue(pt->y) : "--";
|
||||
const ImVec2 value_min = layout_.legend_rects[i].GetBL() - ImVec2(0, 1);
|
||||
ImRect value_rect(value_min, value_min + layout_.legend_rects[i].GetSize());
|
||||
float w = ImGui::CalcTextSize(value.c_str()).x;
|
||||
if (w <= value_rect.GetWidth()) {
|
||||
painter->AddText(ImVec2(value_rect.GetCenter().x - w / 2, value_rect.Min.y), color, value.c_str());
|
||||
} else {
|
||||
addTextEllipsis(painter, ImGui::GetFont(), color, value_rect.Min, value_rect.Max.x, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CabanaColor ChartView::uniqueColor(CabanaColor color, const cabana::Signal *exclude) const {
|
||||
for (auto &s : sigs_) {
|
||||
if (s.sig != exclude && std::abs(color.hsv().hue - s.color.hsv().hue) < 0.1) {
|
||||
|
||||
auto last_color = sigs_.back().color;
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
std::uniform_int_distribution<int> sat(35, 99);
|
||||
std::uniform_int_distribution<int> val(85, 99);
|
||||
color = CabanaColor::fromHsv(std::fmod(last_color.hsv().hue + 60 / 360.0, 1.0),
|
||||
sat(rng) / 100.0,
|
||||
val(rng) / 100.0,
|
||||
color.a / 255.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
142
iqpilot/tools/cabana/ui/chart/chart.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "implot.h"
|
||||
|
||||
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
enum class SeriesType {
|
||||
Line = 0,
|
||||
StepLine,
|
||||
Scatter
|
||||
};
|
||||
inline constexpr const char *SERIES_TYPE_NAMES[] = {"Line", "Step Line", "Scatter"};
|
||||
|
||||
|
||||
inline std::string msgLabel(const MessageId &id) { return " " + msgName(id) + " " + id.toString(); }
|
||||
|
||||
class ChartsWidget;
|
||||
class ChartView {
|
||||
public:
|
||||
struct SigItem {
|
||||
MessageId msg_id;
|
||||
const cabana::Signal *sig = nullptr;
|
||||
CabanaColor color;
|
||||
bool visible = true;
|
||||
std::vector<ImPlotPoint> vals;
|
||||
std::vector<ImPlotPoint> step_vals;
|
||||
ImPlotPoint track_pt{};
|
||||
SegmentTree segment_tree;
|
||||
double min = 0;
|
||||
double max = 0;
|
||||
};
|
||||
|
||||
ChartView(const std::pair<double, double> &x_range, ChartsWidget *parent);
|
||||
void addSignal(const MessageId &msg_id, const cabana::Signal *sig);
|
||||
bool hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const;
|
||||
void updateSeries(const cabana::Signal *sig = nullptr, const MessageEventsMap *msg_new_events = nullptr);
|
||||
void updatePlot(double cur, double min, double max);
|
||||
void setSeriesType(SeriesType type) { series_type_ = type; }
|
||||
void showTip(double sec);
|
||||
void hideTip();
|
||||
void draw(float width);
|
||||
void drawGhost(float width);
|
||||
void removeIf(std::function<bool(const SigItem &)> predicate);
|
||||
void takeSignalsFrom(ChartView *source);
|
||||
|
||||
std::vector<SigItem> takeExtraSignals();
|
||||
void adoptSignal(SigItem s);
|
||||
void setDropHighlight(bool highlight) { can_drop_ = highlight; }
|
||||
const std::vector<SigItem> &signals() const { return sigs_; }
|
||||
const ImRect &rect() const { return layout_.rect; }
|
||||
bool plotHovered() const { return layout_.plot_hovered; }
|
||||
double secondsAtPoint(const ImVec2 &pt) const {
|
||||
return x_min_ + (pt.x - layout_.plot_area.Min.x) * (x_max_ - x_min_) / std::max(layout_.plot_area.GetWidth(), 1.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
using PointIter = std::vector<ImPlotPoint>::const_iterator;
|
||||
|
||||
void signalUpdated(const cabana::Signal *sig);
|
||||
void manageSignals();
|
||||
void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); }
|
||||
void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
|
||||
|
||||
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||
std::vector<ImPlotPoint> &vals, std::vector<ImPlotPoint> &step_vals);
|
||||
void createToolButtons();
|
||||
void drawContextMenu();
|
||||
void handleMousePress();
|
||||
void handleMouseMove();
|
||||
void handleMouseRelease();
|
||||
void updateLayout();
|
||||
void updateAxisY();
|
||||
void paint();
|
||||
void drawStaticLayer();
|
||||
void drawAxes();
|
||||
void drawLegend();
|
||||
void drawSeries();
|
||||
void drawForeground();
|
||||
void drawSignalValue();
|
||||
void drawTimeline();
|
||||
void drawRubberBandTimeRange();
|
||||
void drawMenuActions();
|
||||
int xAxisPrecision() const;
|
||||
std::tuple<double, double, int> getNiceAxisNumbers(double min, double max, int tick_count);
|
||||
double niceNumber(double x, bool ceiling);
|
||||
CabanaColor uniqueColor(CabanaColor color, const cabana::Signal *exclude = nullptr) const;
|
||||
|
||||
const ImPlotPoint *lastPointBefore(const SigItem &s, double sec) const;
|
||||
|
||||
std::pair<PointIter, PointIter> visibleRange(const std::vector<ImPlotPoint> &points) const;
|
||||
inline void clearTrackPoints() { for (auto &s : sigs_) s.track_pt = {}; }
|
||||
inline float xPos(double sec) const { return layout_.plot_area.Min.x + (sec - x_min_) / (x_max_ - x_min_) * layout_.plot_area.GetWidth(); }
|
||||
inline float yPos(double val) const { return layout_.plot_area.Max.y - (val - y_min_) / (y_max_ - y_min_) * layout_.plot_area.GetHeight(); }
|
||||
|
||||
|
||||
struct Layout {
|
||||
ImRect rect;
|
||||
ImRect plot_area;
|
||||
ImRect move_icon_rect;
|
||||
ImRect close_btn_rect;
|
||||
ImRect manage_btn_rect;
|
||||
std::vector<ImRect> legend_rects;
|
||||
float header_bottom = 0;
|
||||
bool plot_hovered = false;
|
||||
} layout_;
|
||||
|
||||
double x_min_;
|
||||
double x_max_;
|
||||
double y_min_ = 0;
|
||||
double y_max_ = 1;
|
||||
int y_tick_count_ = 3;
|
||||
int y_precision_ = 0;
|
||||
std::string y_unit_;
|
||||
|
||||
enum class MouseMode { None, Rubber, Scrub };
|
||||
MouseMode mouse_mode_ = MouseMode::None;
|
||||
ImVec2 press_pos_;
|
||||
ImRect rubber_rect_;
|
||||
bool resume_after_scrub_ = false;
|
||||
bool drawing_ghost_ = false;
|
||||
ImGuiID context_menu_id_ = 0;
|
||||
|
||||
TipLabel tip_label_;
|
||||
std::vector<SigItem> sigs_;
|
||||
double cur_sec_ = 0;
|
||||
SeriesType series_type_ = SeriesType::Line;
|
||||
bool can_drop_ = false;
|
||||
double tooltip_x_ = -1;
|
||||
ChartsWidget *charts_widget_;
|
||||
Connections connections_;
|
||||
};
|
||||
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
674
iqpilot/tools/cabana/ui/chart/chartswidget.cc
Normal file
@@ -0,0 +1,674 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
const int MAX_COLUMN_COUNT = 4;
|
||||
const int CHART_SPACING = 4;
|
||||
const int START_DRAG_DISTANCE = 10;
|
||||
const float LAYOUT_HORIZONTAL_SPACING = 6.0f;
|
||||
const float MIN_RANGE_SLIDER_WIDTH = 40.0f;
|
||||
|
||||
bool LogSlider::draw(const char *label, float width) {
|
||||
return fusionSliderInt(label, &pos_, min_, max_, width);
|
||||
}
|
||||
|
||||
ChartsWidget::ChartsWidget() {
|
||||
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||
|
||||
tabbar_.setAutoHide(true);
|
||||
tabbar_.setUsesScrollButtons(true);
|
||||
tabbar_.setTabsClosable(true);
|
||||
|
||||
column_count_ = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
|
||||
max_chart_range_ = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60);
|
||||
display_range_ = std::make_pair(can->minSeconds(), can->minSeconds() + max_chart_range_);
|
||||
range_slider_.setValue(max_chart_range_);
|
||||
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); }));
|
||||
connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { updateState(); }));
|
||||
connections_.push_back(can->seeking.connect([this](double) { updateState(); }));
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { updateState(); }));
|
||||
connections_.push_back(settings.changed.connect([this]() { settingChanged(); }));
|
||||
connections_.push_back(seriesChanged.connect([this]() { updateTabBar(); }));
|
||||
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { removeTab(index); }));
|
||||
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Close Other Tabs")) {
|
||||
tabbar_.moveTab(index, 0);
|
||||
tabbar_.setCurrentIndex(0);
|
||||
while (tabbar_.count() > 1) removeTab(1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}));
|
||||
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
|
||||
if (index != -1) updateLayout();
|
||||
}));
|
||||
|
||||
setIsDocked(true);
|
||||
newTab();
|
||||
}
|
||||
|
||||
ChartsWidget::~ChartsWidget() = default;
|
||||
|
||||
std::string ChartsWidget::whatsThis() const {
|
||||
return R"(
|
||||
<b>Chart View</b><br />
|
||||
<b>Click</b>: Click to seek to a corresponding time.<br />
|
||||
<b>Drag</b>: Zoom into the chart.<br />
|
||||
<b>Shift + Drag</b>: Scrub through the chart to view values.<br />
|
||||
<b>Right Mouse</b>: Open the context menu.<br />
|
||||
)";
|
||||
}
|
||||
|
||||
void ChartsWidget::newTab() {
|
||||
static int tab_unique_id = 0;
|
||||
int idx = tabbar_.addTab("");
|
||||
tabbar_.setTabData(idx, tab_unique_id++);
|
||||
tabbar_.setCurrentIndex(idx);
|
||||
updateTabBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeTab(int index) {
|
||||
int id = tabbar_.tabData(index);
|
||||
for (auto &c : std::vector<ChartView *>(tab_charts_[id])) {
|
||||
removeChart(c);
|
||||
}
|
||||
tab_charts_.erase(id);
|
||||
tabbar_.removeTab(index);
|
||||
updateTabBar();
|
||||
}
|
||||
|
||||
void ChartsWidget::updateTabBar() {
|
||||
for (int i = 0; i < tabbar_.count(); ++i) {
|
||||
const auto &charts_in_tab = tab_charts_[tabbar_.tabData(i)];
|
||||
tabbar_.setTabText(i, "Tab " + std::to_string(i + 1) + " (" + std::to_string((int)charts_in_tab.size()) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::eventsMerged(const MessageEventsMap &new_events) {
|
||||
std::vector<std::future<void>> futures;
|
||||
for (auto &c : charts_) {
|
||||
futures.push_back(ThreadPool::instance().run([c = c.get(), &new_events]() { c->updateSeries(nullptr, &new_events); }));
|
||||
}
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
|
||||
void ChartsWidget::zoomReset() {
|
||||
can->setTimeRange(std::nullopt);
|
||||
zoom_undo_stack_.clear();
|
||||
}
|
||||
|
||||
ImRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
||||
ImRect r = chart->rect();
|
||||
r.ClipWith(charts_scroll_viewport_);
|
||||
return r;
|
||||
}
|
||||
|
||||
void ChartsWidget::showValueTip(double sec) {
|
||||
if (chartDragActive()) sec = -1;
|
||||
showTip(sec);
|
||||
if (sec < 0 && !value_tip_visible_) return;
|
||||
|
||||
value_tip_visible_ = sec >= 0;
|
||||
for (auto c : currentCharts()) {
|
||||
value_tip_visible_ ? c->showTip(sec) : c->hideTip();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::updateState() {
|
||||
if (charts_.empty()) return;
|
||||
|
||||
const auto &time_range = can->timeRange();
|
||||
const double cur_sec = can->currentSec();
|
||||
if (!time_range.has_value()) {
|
||||
double pos = (cur_sec - display_range_.first) / std::max<float>(1.0, max_chart_range_);
|
||||
if (pos < 0 || pos > 0.8) {
|
||||
display_range_.first = std::max(can->minSeconds(), cur_sec - max_chart_range_ * 0.1);
|
||||
}
|
||||
double max_sec = std::min(display_range_.first + max_chart_range_, can->maxSeconds());
|
||||
display_range_.first = std::max(can->minSeconds(), max_sec - max_chart_range_);
|
||||
display_range_.second = display_range_.first + max_chart_range_;
|
||||
}
|
||||
|
||||
const auto &range = time_range ? *time_range : display_range_;
|
||||
for (auto &c : charts_) {
|
||||
c->updatePlot(cur_sec, range.first, range.second);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::setMaxChartRange(int value) {
|
||||
max_chart_range_ = settings.chart_range = value;
|
||||
updateState();
|
||||
}
|
||||
|
||||
void ChartsWidget::setIsDocked(bool docked) {
|
||||
is_docked_ = docked;
|
||||
if (!docked) float_window_init_ = true;
|
||||
}
|
||||
|
||||
void ChartsWidget::drawToolBar() {
|
||||
beginToolbar();
|
||||
float slider_width = 150.0f;
|
||||
const bool is_zoomed = can->timeRange().has_value();
|
||||
|
||||
|
||||
std::vector<ToolbarItem> items;
|
||||
items.push_back({toolbarButtonWidth(icon::PLUS_SQUARE), [this]() {
|
||||
if (toolButton("new_plot_btn", icon::PLUS_SQUARE, "New Chart")) newChart();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(icon::WINDOW_STACK), [this]() {
|
||||
if (toolButton("new_tab_btn", icon::WINDOW_STACK, "New Tab")) newTab();
|
||||
}});
|
||||
const std::string title_label = "Charts: " + std::to_string(charts_.size());
|
||||
items.push_back({ImGui::CalcTextSize(title_label.c_str()).x + LAYOUT_HORIZONTAL_SPACING, [&title_label]() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(title_label.c_str());
|
||||
ImGui::SameLine(0.0f, LAYOUT_HORIZONTAL_SPACING);
|
||||
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
||||
}});
|
||||
|
||||
const int type_count = (int)std::size(SERIES_TYPE_NAMES);
|
||||
const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)];
|
||||
items.push_back({menuButtonWidth(chart_type_text), [this, &chart_type_text]() {
|
||||
menuButton("chart_type", chart_type_text, "chart_type_menu");
|
||||
if (ImGui::BeginPopup("chart_type_menu")) {
|
||||
for (int i = 0; i < type_count; ++i) {
|
||||
if (ImGui::MenuItem(SERIES_TYPE_NAMES[i])) {
|
||||
settings.chart_series_type = i;
|
||||
settingChanged();
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}});
|
||||
|
||||
const std::string columns_action_text = "Columns: " + std::to_string(column_count_);
|
||||
if (columns_action_visible_) {
|
||||
items.push_back({menuButtonWidth(columns_action_text), [this, &columns_action_text]() {
|
||||
menuButton("columns", columns_action_text, "columns_menu");
|
||||
if (ImGui::BeginPopup("columns_menu")) {
|
||||
for (int i = 0; i < MAX_COLUMN_COUNT; ++i) {
|
||||
if (ImGui::MenuItem(std::to_string(i + 1).c_str())) setColumnCount(i + 1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
|
||||
const size_t spacer_index = items.size();
|
||||
size_t slider_index = (size_t)-1;
|
||||
const std::string range_lb = is_zoomed ? std::string() : utils::formatSeconds(max_chart_range_);
|
||||
std::string reset_zoom_text;
|
||||
if (!is_zoomed) {
|
||||
items.push_back({ImGui::CalcTextSize(range_lb.c_str()).x, [&range_lb]() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(range_lb.c_str());
|
||||
}});
|
||||
slider_index = items.size();
|
||||
items.push_back({slider_width, [this, &slider_width]() {
|
||||
if (range_slider_.draw("##range_slider", slider_width)) setMaxChartRange(range_slider_.value());
|
||||
ImGui::SetItemTooltip("Set the chart range");
|
||||
}});
|
||||
} else {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%.2f-%.2f", can->timeRange()->first, can->timeRange()->second);
|
||||
reset_zoom_text = buf;
|
||||
items.push_back({toolbarButtonWidth(icon::ARROW_COUNTERCLOCKWISE), [this]() {
|
||||
ImGui::BeginDisabled(!zoom_undo_stack_.canUndo());
|
||||
if (toolButton("undo_zoom", icon::ARROW_COUNTERCLOCKWISE, "Undo Zoom")) zoom_undo_stack_.undo();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(icon::ARROW_CLOCKWISE), [this]() {
|
||||
ImGui::BeginDisabled(!zoom_undo_stack_.canRedo());
|
||||
if (toolButton("redo_zoom", icon::ARROW_CLOCKWISE, "Redo Zoom")) zoom_undo_stack_.redo();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
items.push_back({toolbarButtonWidth(std::string(icon::ZOOM_OUT) + " " + reset_zoom_text), [this, &reset_zoom_text]() {
|
||||
if (toolButton("reset_zoom_btn", icon::ZOOM_OUT, "Reset Zoom", reset_zoom_text.c_str())) zoomReset();
|
||||
}});
|
||||
}
|
||||
items.push_back({toolbarButtonWidth(icon::X_SQUARE), [this]() {
|
||||
ImGui::BeginDisabled(charts_.empty());
|
||||
if (toolButton("remove_all_btn", icon::X_SQUARE, "Remove all charts")) removeAll();
|
||||
ImGui::EndDisabled();
|
||||
}});
|
||||
const char *dock_btn_icon = is_docked_ ? icon::ARROW_UP_RIGHT_SQUARE : icon::ARROW_DOWN_LEFT_SQUARE;
|
||||
items.push_back({toolbarButtonWidth(dock_btn_icon), [this, dock_btn_icon]() {
|
||||
if (toolButton("dock_btn", dock_btn_icon, is_docked_ ? "Float the charts window" : "Dock the charts window")) toggleChartsDocking();
|
||||
}});
|
||||
|
||||
|
||||
if (slider_index != (size_t)-1) {
|
||||
const float shrink = std::min(slider_width - MIN_RANGE_SLIDER_WIDTH, toolbarWidth(items, spacer_index) - ImGui::GetContentRegionAvail().x);
|
||||
if (shrink > 0.0f) {
|
||||
slider_width -= shrink;
|
||||
items[slider_index].width = slider_width;
|
||||
}
|
||||
}
|
||||
drawToolbar(items, spacer_index);
|
||||
endToolbar();
|
||||
}
|
||||
|
||||
void ChartsWidget::settingChanged() {
|
||||
if (range_slider_.maximum() != settings.max_cached_minutes * 60) {
|
||||
range_slider_.setRange(1, settings.max_cached_minutes * 60);
|
||||
}
|
||||
for (auto &c : charts_) {
|
||||
c->setSeriesType((SeriesType)settings.chart_series_type);
|
||||
}
|
||||
}
|
||||
|
||||
ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) {
|
||||
for (auto &c : charts_)
|
||||
if (c->hasSignal(id, sig)) return c.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ChartView *ChartsWidget::createChart(int pos) {
|
||||
auto chart = std::make_unique<ChartView>(can->timeRange().value_or(display_range_), this);
|
||||
ChartView *ptr = chart.get();
|
||||
pos = std::clamp(pos, 0, (int)charts_.size());
|
||||
charts_.insert(charts_.begin() + pos, std::move(chart));
|
||||
currentCharts().insert(currentCharts().begin() + pos, ptr);
|
||||
updateLayout();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) {
|
||||
ChartView *chart = findChart(id, sig);
|
||||
if (show && !chart) {
|
||||
chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart();
|
||||
chart->addSignal(id, sig);
|
||||
updateState();
|
||||
} else if (!show && chart) {
|
||||
chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; });
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||
if (src_chart->signals().size() > 1) {
|
||||
auto it = std::find_if(charts_.begin(), charts_.end(), [src_chart](auto &c) { return c.get() == src_chart; });
|
||||
const int pos = it - charts_.begin() + 1;
|
||||
for (auto &s : src_chart->takeExtraSignals()) {
|
||||
createChart(pos)->adoptSignal(std::move(s));
|
||||
}
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ChartsWidget::serializeChartIds() const {
|
||||
std::vector<std::string> chart_ids;
|
||||
for (auto &c : charts_) {
|
||||
std::string ids;
|
||||
for (const auto &s : c->signals()) {
|
||||
if (!ids.empty()) ids += ',';
|
||||
ids += s.msg_id.toString() + "|" + s.sig->name;
|
||||
}
|
||||
chart_ids.push_back(ids);
|
||||
}
|
||||
std::reverse(chart_ids.begin(), chart_ids.end());
|
||||
return chart_ids;
|
||||
}
|
||||
|
||||
void ChartsWidget::restoreChartsFromIds(const std::vector<std::string> &chart_ids) {
|
||||
for (const auto &chart_id : chart_ids) {
|
||||
int index = 0;
|
||||
for (const auto &part : utils::split(chart_id, ',')) {
|
||||
const size_t sep = part.find('|');
|
||||
if (sep == std::string::npos) continue;
|
||||
MessageId msg_id = MessageId::fromString(part.substr(0, sep));
|
||||
if (auto *msg = dbc()->msg(msg_id))
|
||||
if (auto *sig = msg->sig(part.substr(sep + 1)))
|
||||
showChart(msg_id, sig, true, index++ > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::setColumnCount(int n) {
|
||||
n = std::clamp(n, 1, MAX_COLUMN_COUNT);
|
||||
if (column_count_ != n) {
|
||||
column_count_ = settings.chart_column_count = n;
|
||||
updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::updateLayout() {
|
||||
|
||||
const float container_width = charts_container_.geometry().GetWidth();
|
||||
if (container_width <= 0) return;
|
||||
|
||||
int n = MAX_COLUMN_COUNT;
|
||||
for (; n > 1; --n) {
|
||||
if ((n * CHART_MIN_WIDTH + (n - 1) * CHART_SPACING) < container_width) break;
|
||||
}
|
||||
|
||||
columns_action_visible_ = n > 1;
|
||||
current_column_count_ = std::min(column_count_, n);
|
||||
}
|
||||
|
||||
void ChartsWidget::startChartDrag(ChartView *chart, const ImVec2 &global_pos) {
|
||||
stopAutoScroll();
|
||||
drag_ = {.source = chart, .press_pos = global_pos};
|
||||
showValueTip(-1);
|
||||
|
||||
drag_preview_size_ = ImVec2(CHART_MIN_WIDTH, (float)settings.chart_height);
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartMove(const ImVec2 &global_pos) {
|
||||
if (!drag_.active) {
|
||||
ImVec2 d = global_pos - drag_.press_pos;
|
||||
if (std::abs(d.x) + std::abs(d.y) < START_DRAG_DISTANCE) return;
|
||||
drag_.active = true;
|
||||
drag_preview_visible_ = true;
|
||||
}
|
||||
drag_preview_pos_ = global_pos + ImVec2(5, 5);
|
||||
|
||||
|
||||
int tab = tabbar_.tabAt(global_pos);
|
||||
if (tab >= 0 && tab != tabbar_.currentIndex()) {
|
||||
tabbar_.setCurrentIndex(tab);
|
||||
}
|
||||
|
||||
ChartView *target = nullptr;
|
||||
for (auto c : currentCharts()) {
|
||||
if (c != drag_.source && c->rect().Contains(global_pos)) {
|
||||
target = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::exchange(drop_target_, target) != target) {
|
||||
for (auto &c : charts_) c->setDropHighlight(c.get() == target);
|
||||
}
|
||||
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||
bool on_background = !target && in_viewport && !charts_container_.childAt(global_pos);
|
||||
charts_container_.setDropIndicator(on_background ? global_pos : ImVec2());
|
||||
|
||||
if (in_viewport) {
|
||||
startAutoScroll(global_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::cancelChartDrag() {
|
||||
drag_ = {};
|
||||
stopAutoScroll();
|
||||
drag_preview_visible_ = false;
|
||||
charts_container_.setDropIndicator({});
|
||||
if (auto target = std::exchange(drop_target_, nullptr)) target->setDropHighlight(false);
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartRelease(const ImVec2 &global_pos) {
|
||||
ChartView *source = drag_.source;
|
||||
bool active = drag_.active;
|
||||
ChartView *target = drop_target_;
|
||||
cancelChartDrag();
|
||||
if (!active) return;
|
||||
|
||||
bool in_viewport = charts_scroll_viewport_.Contains(global_pos);
|
||||
if (target) {
|
||||
|
||||
target->takeSignalsFrom(source);
|
||||
} else if (in_viewport && !charts_container_.childAt(global_pos)) {
|
||||
|
||||
auto w = charts_container_.getDropAfter(global_pos);
|
||||
if (w != source) {
|
||||
for (auto &[_, list] : tab_charts_) {
|
||||
list.erase(std::remove(list.begin(), list.end(), source), list.end());
|
||||
}
|
||||
auto &cur = currentCharts();
|
||||
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
|
||||
cur.insert(cur.begin() + to, source);
|
||||
updateLayout();
|
||||
updateTabBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::drawDragPreview() {
|
||||
if (!drag_preview_visible_ || !drag_.source) return;
|
||||
|
||||
|
||||
ImGui::SetNextWindowPos(drag_preview_pos_);
|
||||
ImGui::SetNextWindowSize(drag_preview_size_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking;
|
||||
if (ImGui::Begin("##chart_drag_ghost", nullptr, flags)) {
|
||||
drag_.source->drawGhost(drag_preview_size_.x);
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
}
|
||||
|
||||
void ChartsWidget::startAutoScroll(const ImVec2 &global_pos) {
|
||||
auto_scroll_pos_ = global_pos;
|
||||
if (!auto_scroll_timer_active_) auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||
auto_scroll_timer_active_ = true;
|
||||
}
|
||||
|
||||
void ChartsWidget::stopAutoScroll() {
|
||||
auto_scroll_timer_active_ = false;
|
||||
auto_scroll_count_ = 0;
|
||||
}
|
||||
|
||||
void ChartsWidget::doAutoScroll() {
|
||||
if (!charts_scroll_) return;
|
||||
const int page_step = charts_scroll_viewport_.GetHeight();
|
||||
if (auto_scroll_count_ < page_step) {
|
||||
++auto_scroll_count_;
|
||||
}
|
||||
|
||||
int value = charts_scroll_->Scroll.y;
|
||||
ImVec2 pos = auto_scroll_pos_;
|
||||
ImRect area = charts_scroll_viewport_;
|
||||
|
||||
int new_value = value;
|
||||
if (pos.y - area.Min.y < settings.chart_height / 2) {
|
||||
new_value = value - auto_scroll_count_;
|
||||
} else if (area.Max.y - pos.y < settings.chart_height / 2) {
|
||||
new_value = value + auto_scroll_count_;
|
||||
}
|
||||
new_value = std::clamp<int>(new_value, 0, charts_scroll_->ScrollMax.y);
|
||||
if (new_value != value) ImGui::SetScrollY(charts_scroll_, new_value);
|
||||
if (value == new_value) {
|
||||
stopAutoScroll();
|
||||
} else if (chartDragActive()) {
|
||||
|
||||
dragChartMove(auto_scroll_pos_);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::newChart() {
|
||||
execSignalSelector(std::make_unique<SignalSelector>("New Chart"), nullptr, [this](SignalSelector &dlg) {
|
||||
const auto &items = dlg.selectedItems();
|
||||
if (!items.empty()) {
|
||||
auto c = createChart();
|
||||
for (const auto &it : items) {
|
||||
c->addSignal(it.msg_id, it.sig);
|
||||
}
|
||||
updateState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ChartsWidget::execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted) {
|
||||
signal_selector_ = std::move(dlg);
|
||||
signal_selector_owner_ = owner;
|
||||
signal_selector_accepted_ = std::move(accepted);
|
||||
signal_selector_->open();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(ChartView *chart) {
|
||||
if (drag_.source == chart) cancelChartDrag();
|
||||
if (drop_target_ == chart) drop_target_ = nullptr;
|
||||
if (signal_selector_owner_ == chart) {
|
||||
signal_selector_owner_ = nullptr;
|
||||
signal_selector_accepted_ = nullptr;
|
||||
}
|
||||
auto it = std::find_if(charts_.begin(), charts_.end(), [chart](auto &c) { return c.get() == chart; });
|
||||
if (it != charts_.end()) {
|
||||
deleted_charts_.push_back(std::move(*it));
|
||||
charts_.erase(it);
|
||||
}
|
||||
for (auto &[_, list] : tab_charts_) {
|
||||
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
|
||||
}
|
||||
updateLayout();
|
||||
seriesChanged();
|
||||
}
|
||||
|
||||
void ChartsWidget::removeAll() {
|
||||
while (tabbar_.count() > 1) {
|
||||
tabbar_.removeTab(1);
|
||||
}
|
||||
std::vector<ChartView *> all;
|
||||
for (auto &c : charts_) all.push_back(c.get());
|
||||
for (auto c : all) removeChart(c);
|
||||
tab_charts_.clear();
|
||||
zoomReset();
|
||||
}
|
||||
|
||||
void ChartsWidget::handleEvents() {
|
||||
|
||||
if (ImGui::IsMouseClicked(3) && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
|
||||
zoom_undo_stack_.undo();
|
||||
}
|
||||
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) {
|
||||
if (chartDragActive()) cancelChartDrag();
|
||||
showValueTip(-1);
|
||||
}
|
||||
|
||||
|
||||
if (chartDragActive()) {
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||
dragChartMove(ImGui::GetMousePos());
|
||||
} else {
|
||||
dragChartRelease(ImGui::GetMousePos());
|
||||
}
|
||||
}
|
||||
|
||||
if (!value_tip_visible_) return;
|
||||
|
||||
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
if (!any_plot_hovered_ &&
|
||||
(delta.x != 0 || delta.y != 0 || !ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))) {
|
||||
showValueTip(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::draw() {
|
||||
deleted_charts_.clear();
|
||||
|
||||
|
||||
if (float_window_init_ && !is_docked_) {
|
||||
float_window_init_ = false;
|
||||
const ImGuiViewport *viewport = ImGui::GetMainViewport();
|
||||
const ImVec2 size(viewport->WorkSize.x * 0.6f, viewport->WorkSize.y * 0.6f);
|
||||
ImGui::SetWindowSize(size);
|
||||
ImGui::SetWindowPos(viewport->WorkPos + (viewport->WorkSize - size) * 0.5f);
|
||||
}
|
||||
ImGui::PushID(this);
|
||||
if (auto_scroll_timer_active_ && ImGui::GetTime() >= auto_scroll_timer_next_) {
|
||||
auto_scroll_timer_next_ = ImGui::GetTime() + 0.05;
|
||||
doAutoScroll();
|
||||
}
|
||||
|
||||
|
||||
handleEvents();
|
||||
|
||||
drawToolBar();
|
||||
tabbar_.draw();
|
||||
|
||||
any_plot_hovered_ = false;
|
||||
if (ImGui::BeginChild("charts_scroll", ImVec2(0, 0), ImGuiChildFlags_None, 0)) {
|
||||
charts_scroll_ = ImGui::GetCurrentWindow();
|
||||
charts_scroll_viewport_ = charts_scroll_->InnerRect;
|
||||
charts_container_.draw();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
drawDragPreview();
|
||||
|
||||
if (signal_selector_ && !signal_selector_->draw()) {
|
||||
auto dlg = std::move(signal_selector_);
|
||||
auto accepted = std::move(signal_selector_accepted_);
|
||||
signal_selector_owner_ = nullptr;
|
||||
if (dlg->accepted() && accepted) accepted(*dlg);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
void ChartsContainer::draw() {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
geometry_ = ImRect(start, start + ImVec2(window->InnerRect.GetWidth(), 0));
|
||||
charts_widget_->updateLayout();
|
||||
|
||||
const int n = std::max(charts_widget_->current_column_count_, 1);
|
||||
const float spacing = CHART_SPACING;
|
||||
const float width = (geometry_.GetWidth() - (n - 1) * spacing) / n;
|
||||
const ImVec2 origin = ImGui::GetCursorScreenPos() + ImVec2(0, CHART_SPACING);
|
||||
auto current_charts = charts_widget_->currentCharts();
|
||||
float bottom = origin.y;
|
||||
const bool aligned = ImPlot::BeginAlignedPlots("charts_align", true);
|
||||
for (int i = 0; i < current_charts.size(); ++i) {
|
||||
ImVec2 pos = origin + ImVec2((i % n) * (width + spacing), (i / n) * (settings.chart_height + spacing));
|
||||
ImGui::SetCursorScreenPos(pos);
|
||||
current_charts[i]->draw(width);
|
||||
bottom = std::max(bottom, pos.y + settings.chart_height);
|
||||
if (current_charts[i]->plotHovered()) charts_widget_->any_plot_hovered_ = true;
|
||||
}
|
||||
if (aligned) ImPlot::EndAlignedPlots();
|
||||
ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom));
|
||||
ImGui::Dummy(ImVec2(geometry_.GetWidth(), CHART_SPACING));
|
||||
geometry_.Max.y = bottom + CHART_SPACING;
|
||||
drawDropIndicator();
|
||||
}
|
||||
|
||||
void ChartsContainer::drawDropIndicator() {
|
||||
if (!(drop_indicator_pos_.x == 0 && drop_indicator_pos_.y == 0) && !childAt(drop_indicator_pos_)) {
|
||||
ImRect r = geometry_;
|
||||
r.Max.y = r.Min.y + CHART_SPACING;
|
||||
if (auto insert_after = getDropAfter(drop_indicator_pos_)) {
|
||||
float h = r.GetHeight();
|
||||
r.Min.y = insert_after->rect().Max.y;
|
||||
r.Max.y = r.Min.y + h;
|
||||
}
|
||||
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_Header));
|
||||
}
|
||||
}
|
||||
|
||||
ChartView *ChartsContainer::getDropAfter(const ImVec2 &pos) const {
|
||||
const auto &charts = charts_widget_->currentCharts();
|
||||
auto it = std::find_if(charts.crbegin(), charts.crend(), [&pos](auto c) {
|
||||
const ImRect &area = c->rect();
|
||||
return pos.x >= area.Min.x && pos.x <= area.Max.x && pos.y >= area.Max.y;
|
||||
});
|
||||
return it == charts.crend() ? nullptr : *it;
|
||||
}
|
||||
|
||||
ChartView *ChartsContainer::childAt(const ImVec2 &pos) const {
|
||||
for (auto c : charts_widget_->currentCharts()) {
|
||||
if (c->rect().Contains(pos)) return c;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
168
iqpilot/tools/cabana/ui/chart/chartswidget.h
Normal file
@@ -0,0 +1,168 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/ui/chart/signalselector.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
const int CHART_MIN_WIDTH = 300;
|
||||
|
||||
|
||||
class LogSlider {
|
||||
public:
|
||||
LogSlider(double factor) : scale_(factor) {}
|
||||
|
||||
void setRange(double min, double max) {
|
||||
scale_.setRange(min, max);
|
||||
min_ = min;
|
||||
max_ = max;
|
||||
setValue(pos_);
|
||||
}
|
||||
int value() const { return scale_.value(pos_, minimum(), maximum()); }
|
||||
void setValue(int v) { pos_ = scale_.position(v, minimum(), maximum()); }
|
||||
int minimum() const { return min_; }
|
||||
int maximum() const { return max_; }
|
||||
bool draw(const char *label, float width);
|
||||
|
||||
private:
|
||||
LogScale scale_;
|
||||
int min_ = 0;
|
||||
int max_ = 1;
|
||||
int pos_ = 0;
|
||||
};
|
||||
|
||||
class ChartView;
|
||||
class ChartsWidget;
|
||||
|
||||
class ChartsContainer {
|
||||
public:
|
||||
ChartsContainer(ChartsWidget *parent) : charts_widget_(parent) {}
|
||||
void setDropIndicator(const ImVec2 &pt) { drop_indicator_pos_ = pt; }
|
||||
void draw();
|
||||
ChartView *getDropAfter(const ImVec2 &pos) const;
|
||||
ChartView *childAt(const ImVec2 &pos) const;
|
||||
const ImRect &geometry() const { return geometry_; }
|
||||
|
||||
private:
|
||||
void drawDropIndicator();
|
||||
|
||||
ImRect geometry_;
|
||||
ChartsWidget *charts_widget_;
|
||||
ImVec2 drop_indicator_pos_;
|
||||
};
|
||||
|
||||
class ChartsWidget {
|
||||
public:
|
||||
ChartsWidget();
|
||||
~ChartsWidget();
|
||||
void draw();
|
||||
void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge);
|
||||
inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; }
|
||||
std::vector<std::string> serializeChartIds() const;
|
||||
void restoreChartsFromIds(const std::vector<std::string> &chart_ids);
|
||||
std::string whatsThis() const;
|
||||
|
||||
void setColumnCount(int n);
|
||||
void removeAll();
|
||||
void setIsDocked(bool dock);
|
||||
|
||||
Observable<> toggleChartsDocking;
|
||||
Observable<> seriesChanged;
|
||||
Observable<double> showTip;
|
||||
|
||||
private:
|
||||
void handleEvents();
|
||||
void newChart();
|
||||
ChartView *createChart(int pos = 0);
|
||||
void removeChart(ChartView *chart);
|
||||
void splitChart(ChartView *chart);
|
||||
ImRect chartVisibleRect(ChartView *chart);
|
||||
void eventsMerged(const MessageEventsMap &new_events);
|
||||
void updateState();
|
||||
void zoomReset();
|
||||
void startChartDrag(ChartView *chart, const ImVec2 &global_pos);
|
||||
void dragChartMove(const ImVec2 &global_pos);
|
||||
void dragChartRelease(const ImVec2 &global_pos);
|
||||
void cancelChartDrag();
|
||||
bool chartDragActive() const { return drag_.source != nullptr; }
|
||||
void startAutoScroll(const ImVec2 &global_pos);
|
||||
void stopAutoScroll();
|
||||
void doAutoScroll();
|
||||
void drawToolBar();
|
||||
void updateTabBar();
|
||||
void setMaxChartRange(int value);
|
||||
void updateLayout();
|
||||
void settingChanged();
|
||||
void showValueTip(double sec);
|
||||
void newTab();
|
||||
void removeTab(int index);
|
||||
inline std::vector<ChartView *> ¤tCharts() { return tab_charts_[tabbar_.tabData(tabbar_.currentIndex())]; }
|
||||
ChartView *findChart(const MessageId &id, const cabana::Signal *sig);
|
||||
|
||||
void execSignalSelector(std::unique_ptr<SignalSelector> dlg, ChartView *owner, std::function<void(SignalSelector &)> accepted);
|
||||
void drawDragPreview();
|
||||
|
||||
LogSlider range_slider_{1000};
|
||||
bool is_docked_ = true;
|
||||
bool float_window_init_ = false;
|
||||
|
||||
UndoStack zoom_undo_stack_;
|
||||
|
||||
std::vector<std::unique_ptr<ChartView>> charts_;
|
||||
std::unordered_map<int, std::vector<ChartView *>> tab_charts_;
|
||||
TabBar tabbar_;
|
||||
ChartsContainer charts_container_{this};
|
||||
ImGuiWindow *charts_scroll_ = nullptr;
|
||||
ImRect charts_scroll_viewport_;
|
||||
int max_chart_range_ = 0;
|
||||
std::pair<double, double> display_range_;
|
||||
bool columns_action_visible_ = false;
|
||||
int column_count_ = 1;
|
||||
int current_column_count_ = 0;
|
||||
struct ChartDrag {
|
||||
ChartView *source = nullptr;
|
||||
ImVec2 press_pos;
|
||||
bool active = false;
|
||||
} drag_;
|
||||
|
||||
ImVec2 drag_preview_pos_;
|
||||
ImVec2 drag_preview_size_;
|
||||
bool drag_preview_visible_ = false;
|
||||
ChartView *drop_target_ = nullptr;
|
||||
int auto_scroll_count_ = 0;
|
||||
ImVec2 auto_scroll_pos_;
|
||||
bool auto_scroll_timer_active_ = false;
|
||||
double auto_scroll_timer_next_ = 0;
|
||||
bool value_tip_visible_ = false;
|
||||
bool any_plot_hovered_ = false;
|
||||
std::vector<std::unique_ptr<ChartView>> deleted_charts_;
|
||||
std::unique_ptr<SignalSelector> signal_selector_;
|
||||
ChartView *signal_selector_owner_ = nullptr;
|
||||
std::function<void(SignalSelector &)> signal_selector_accepted_;
|
||||
Connections connections_;
|
||||
friend class ChartView;
|
||||
friend class ChartsContainer;
|
||||
};
|
||||
|
||||
class ZoomCommand : public UndoCommand {
|
||||
public:
|
||||
ZoomCommand(std::pair<double, double> range) : range(range) {
|
||||
prev_range = can->timeRange();
|
||||
}
|
||||
void undo() override { can->setTimeRange(prev_range); }
|
||||
void redo() override { can->setTimeRange(range); }
|
||||
std::optional<std::pair<double, double>> prev_range, range;
|
||||
};
|
||||
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
155
iqpilot/tools/cabana/ui/chart/signalselector.cc
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "tools/cabana/ui/chart/signalselector.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
#include "tools/cabana/ui/chart/chart.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
SignalSelector::SignalSelector(std::string title) : title_(std::move(title)) {
|
||||
for (const auto &[id, _] : can->lastMessages()) {
|
||||
if (auto m = dbc()->msg(id)) {
|
||||
msgs_combo_.push_back({m->name + " (" + id.toString() + ")", id});
|
||||
}
|
||||
}
|
||||
std::sort(msgs_combo_.begin(), msgs_combo_.end(), [](auto &a, auto &b) { return a.text < b.text; });
|
||||
}
|
||||
|
||||
bool SignalSelector::draw() {
|
||||
if (!open_) return false;
|
||||
const std::string popup_id = title_ + "###SignalSelector";
|
||||
if (!show_) {
|
||||
ImGui::OpenPopup(popup_id.c_str());
|
||||
show_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(700.0f, 450.0f));
|
||||
if (!ImGui::BeginPopupModal(popup_id.c_str(), nullptr, ImGuiWindowFlags_NoSavedSettings)) {
|
||||
open_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const float btn_w = ImGui::GetFrameHeight() + 8.0f;
|
||||
const float column_w = (ImGui::GetContentRegionAvail().x - btn_w - ImGui::GetStyle().ItemSpacing.x * 2) / 2;
|
||||
|
||||
const float lists_h = ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() * 3;
|
||||
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted("Available Signals");
|
||||
|
||||
const char *preview = msgs_combo_index_ >= 0 ? msgs_combo_[msgs_combo_index_].text.c_str() : "Select a msg...";
|
||||
ImGui::SetNextItemWidth(column_w);
|
||||
if (ImGui::BeginCombo("##msgs_combo", preview)) {
|
||||
if (ImGui::IsWindowAppearing()) {
|
||||
msgs_combo_filter_.clear();
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
}
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
inputText("##msgs_filter", &msgs_combo_filter_, "Select a msg...");
|
||||
for (int i = 0; i < (int)msgs_combo_.size(); ++i) {
|
||||
if (!msgs_combo_filter_.empty() && !utils::containsCI(msgs_combo_[i].text, msgs_combo_filter_)) continue;
|
||||
if (ImGui::Selectable(msgs_combo_[i].text.c_str(), i == msgs_combo_index_)) {
|
||||
msgs_combo_index_ = i;
|
||||
updateAvailableList(i);
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
bool add_dbl = false;
|
||||
drawList("##available_list", available_list_, &available_row_, false, &add_dbl, ImVec2(column_w, lists_h));
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::Dummy(ImVec2(btn_w, (lists_h + ImGui::GetFrameHeightWithSpacing() * 2) / 2 - ImGui::GetFrameHeight()));
|
||||
ImGui::BeginDisabled(available_row_ == -1);
|
||||
bool add_clicked = ImGui::Button(icon::CHEVRON_RIGHT, ImVec2(btn_w, 0));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::BeginDisabled(selected_row_ == -1);
|
||||
bool remove_clicked = ImGui::Button(icon::CHEVRON_LEFT, ImVec2(btn_w, 0));
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted("Selected Signals");
|
||||
bool remove_dbl = false;
|
||||
drawList("##selected_list", selected_list_, &selected_row_, true, &remove_dbl, ImVec2(column_w, lists_h + ImGui::GetFrameHeightWithSpacing()));
|
||||
bool rejected = false;
|
||||
dialogButtons("OK", &accepted_, &rejected);
|
||||
const bool done = accepted_ || rejected;
|
||||
ImGui::EndGroup();
|
||||
|
||||
if ((add_dbl || add_clicked) && available_row_ >= 0 && available_row_ < (int)available_list_.size()) {
|
||||
add(available_row_);
|
||||
} else if ((remove_dbl || remove_clicked) && selected_row_ >= 0 && selected_row_ < (int)selected_list_.size()) {
|
||||
remove(selected_row_);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
open_ = false;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
return open_;
|
||||
}
|
||||
|
||||
void SignalSelector::drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size) {
|
||||
if (!ImGui::BeginListBox(id, size)) return;
|
||||
for (int i = 0; i < (int)list.size(); ++i) {
|
||||
const auto &item = list[i];
|
||||
ImGui::PushID(i);
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
if (ImGui::Selectable("##item", i == *current_row)) *current_row = i;
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
||||
*current_row = i;
|
||||
*double_clicked = true;
|
||||
}
|
||||
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
float x = pos.x + 5;
|
||||
drawColorMarker(dl, ImVec2(x, pos.y), toImU32(item.sig->color));
|
||||
x += markerSize() + 4;
|
||||
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_Text), item.sig->name.c_str());
|
||||
if (show_msg_name) {
|
||||
x += ImGui::CalcTextSize(item.sig->name.c_str()).x;
|
||||
dl->AddText(ImVec2(x, pos.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), msgLabel(item.msg_id).c_str());
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndListBox();
|
||||
}
|
||||
|
||||
void SignalSelector::add(int row) {
|
||||
const auto &item = available_list_[row];
|
||||
selected_list_.emplace_back(item.msg_id, item.sig);
|
||||
available_list_.erase(available_list_.begin() + row);
|
||||
available_row_ = -1;
|
||||
}
|
||||
|
||||
void SignalSelector::remove(int row) {
|
||||
const auto &item = selected_list_[row];
|
||||
if (msgs_combo_index_ >= 0 && item.msg_id == msgs_combo_[msgs_combo_index_].id) {
|
||||
available_list_.emplace_back(item.msg_id, item.sig);
|
||||
}
|
||||
selected_list_.erase(selected_list_.begin() + row);
|
||||
selected_row_ = -1;
|
||||
}
|
||||
|
||||
void SignalSelector::updateAvailableList(int index) {
|
||||
if (index == -1) return;
|
||||
available_list_.clear();
|
||||
available_row_ = -1;
|
||||
MessageId msg_id = msgs_combo_[index].id;
|
||||
for (auto s : dbc()->msg(msg_id)->getSignals()) {
|
||||
bool is_selected = std::any_of(selected_list_.begin(), selected_list_.end(),
|
||||
[sig = s, &msg_id](auto &it) { return it.msg_id == msg_id && it.sig == sig; });
|
||||
if (!is_selected) {
|
||||
available_list_.emplace_back(msg_id, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
46
iqpilot/tools/cabana/ui/chart/signalselector.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
|
||||
|
||||
class SignalSelector {
|
||||
public:
|
||||
struct ListItem {
|
||||
ListItem(const MessageId &msg_id, const cabana::Signal *sig) : msg_id(msg_id), sig(sig) {}
|
||||
MessageId msg_id;
|
||||
const cabana::Signal *sig;
|
||||
};
|
||||
|
||||
SignalSelector(std::string title);
|
||||
const std::vector<ListItem> &selectedItems() const { return selected_list_; }
|
||||
inline void addSelected(const MessageId &id, const cabana::Signal *sig) { selected_list_.emplace_back(id, sig); }
|
||||
void open() { open_ = true; show_ = false; accepted_ = false; }
|
||||
bool draw();
|
||||
bool accepted() const { return accepted_; }
|
||||
|
||||
private:
|
||||
void updateAvailableList(int index);
|
||||
void add(int row);
|
||||
void remove(int row);
|
||||
void drawList(const char *id, std::vector<ListItem> &list, int *current_row, bool show_msg_name, bool *double_clicked, const ImVec2 &size);
|
||||
|
||||
struct ComboItem {
|
||||
std::string text;
|
||||
MessageId id;
|
||||
};
|
||||
std::string title_;
|
||||
std::vector<ComboItem> msgs_combo_;
|
||||
int msgs_combo_index_ = -1;
|
||||
std::string msgs_combo_filter_;
|
||||
std::vector<ListItem> available_list_;
|
||||
std::vector<ListItem> selected_list_;
|
||||
int available_row_ = -1;
|
||||
int selected_row_ = -1;
|
||||
bool accepted_ = false;
|
||||
bool open_ = false;
|
||||
bool show_ = false;
|
||||
};
|
||||
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
177
iqpilot/tools/cabana/ui/chart/sparkline.cc
Normal file
@@ -0,0 +1,177 @@
|
||||
#include "tools/cabana/ui/chart/sparkline.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz,
|
||||
double window_end) {
|
||||
if (first == last || sz.x <= 0 || sz.y <= 0) {
|
||||
render_points_.clear();
|
||||
size = {};
|
||||
return;
|
||||
}
|
||||
|
||||
points_.clear();
|
||||
min_val = std::numeric_limits<double>::max();
|
||||
max_val = std::numeric_limits<double>::lowest();
|
||||
points_.reserve(std::distance(first, last));
|
||||
|
||||
|
||||
|
||||
|
||||
const double window_start = window_end - range;
|
||||
double value = 0.0;
|
||||
for (auto it = first; it != last; ++it) {
|
||||
if (sig->getValue((*it)->dat, (*it)->size, &value)) {
|
||||
double x = can->toSeconds((*it)->mono_time) - window_start;
|
||||
|
||||
|
||||
if (x >= 0.0) {
|
||||
min_val = std::min(min_val, value);
|
||||
max_val = std::max(max_val, value);
|
||||
}
|
||||
points_.push_back({x, value});
|
||||
}
|
||||
}
|
||||
if (min_val > max_val) {
|
||||
for (const auto &p : points_) {
|
||||
min_val = std::min(min_val, p.y);
|
||||
max_val = std::max(max_val, p.y);
|
||||
}
|
||||
}
|
||||
|
||||
if (points_.empty()) {
|
||||
render_points_.clear();
|
||||
size = {};
|
||||
return;
|
||||
}
|
||||
|
||||
freq_ = points_.size() / std::max(points_.back().x - points_.front().x, 1.0);
|
||||
render(sig->color, range, sz, window_end);
|
||||
}
|
||||
|
||||
void Sparkline::render(const CabanaColor &color, int range, ImVec2 sz, double window_end) {
|
||||
bool is_flat_line = min_val == max_val;
|
||||
if (is_flat_line) {
|
||||
min_val -= 1.0;
|
||||
max_val += 1.0;
|
||||
}
|
||||
|
||||
const double xscale = (sz.x - 1) / (double)range;
|
||||
const double yscale = (sz.y - 3) / (max_val - min_val);
|
||||
const double span = points_.back().x - points_.front().x;
|
||||
bool draw_individual_points = (span * xscale / points_.size()) > 8.0;
|
||||
|
||||
|
||||
render_points_.reserve(points_.size());
|
||||
render_points_.clear();
|
||||
if (draw_individual_points) {
|
||||
for (const auto &p : points_) {
|
||||
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - p.y) * yscale);
|
||||
}
|
||||
} else if (is_flat_line) {
|
||||
double y = sz.y / 2.0;
|
||||
render_points_.emplace_back(points_.front().x * xscale, y);
|
||||
render_points_.emplace_back(points_.back().x * xscale, y);
|
||||
} else {
|
||||
double prev_y = points_.front().y;
|
||||
render_points_.emplace_back(points_.front().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
bool in_flat = false;
|
||||
|
||||
for (size_t i = 1; i < points_.size(); ++i) {
|
||||
const auto &p = points_[i];
|
||||
double y = p.y;
|
||||
if (std::abs(y - prev_y) < 1e-6) {
|
||||
in_flat = true;
|
||||
} else {
|
||||
if (in_flat) render_points_.emplace_back(points_[i - 1].x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
render_points_.emplace_back(p.x * xscale, 1.0 + (max_val - y) * yscale);
|
||||
in_flat = false;
|
||||
}
|
||||
prev_y = y;
|
||||
}
|
||||
if (in_flat) render_points_.emplace_back(points_.back().x * xscale, 1.0 + (max_val - prev_y) * yscale);
|
||||
}
|
||||
|
||||
size = sz;
|
||||
CabanaColor line_color = color;
|
||||
if (!isDarkTheme()) {
|
||||
auto [h, s, v] = color.hsv();
|
||||
line_color = CabanaColor::fromHsv(h, std::min(1.0f, s * 2.0f), v * 0.7f, color.a / 255.0f);
|
||||
}
|
||||
color_ = toImU32(line_color);
|
||||
draw_individual_points_ = draw_individual_points;
|
||||
window_end_ = window_end;
|
||||
xscale_ = xscale;
|
||||
}
|
||||
|
||||
void Sparkline::draw(ImDrawList *draw_list, ImVec2 pos) const {
|
||||
if (render_points_.empty()) return;
|
||||
|
||||
|
||||
|
||||
|
||||
const float shift = std::clamp((float)((can->currentSec() - window_end_) * xscale_), 0.0f, size.x);
|
||||
|
||||
|
||||
const float px = 1.0f / std::max(1.0f, ImGui::GetIO().DisplayFramebufferScale.x);
|
||||
auto snap = [&](float x) { return std::floor(x / px) * px; };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ImVec2 offset(pos.x - shift, pos.y);
|
||||
const double k = offset.x - (window_end_ * xscale_ - (size.x - 1));
|
||||
offset.x += snap(k) - k;
|
||||
auto point_at = [&](const ImVec2 &p) { return ImVec2(offset.x + p.x, offset.y + p.y); };
|
||||
|
||||
draw_list->PushClipRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), true);
|
||||
|
||||
|
||||
auto draw_point = [&](const ImVec2 &p) { draw_list->AddRectFilled(ImVec2(p.x - 1.5f, p.y - 1.5f), ImVec2(p.x + 1.5f, p.y + 1.5f), color_); };
|
||||
|
||||
if (draw_individual_points_) {
|
||||
for (const auto &p : render_points_) {
|
||||
draw_list->PathLineTo(point_at(p));
|
||||
draw_point(point_at(p));
|
||||
}
|
||||
draw_list->PathStroke(color_, ImDrawFlags_None, 1.5f);
|
||||
} else {
|
||||
|
||||
|
||||
std::vector<ImVec2> pts;
|
||||
pts.reserve(render_points_.size());
|
||||
float col = -1e9f;
|
||||
for (const auto &p : render_points_) {
|
||||
ImVec2 sp = point_at(p);
|
||||
float c = snap(sp.x);
|
||||
if (c != col) {
|
||||
pts.push_back(sp);
|
||||
col = c;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
auto steep = [&](size_t i) { return std::abs(pts[i + 1].y - pts[i].y) > 2.0f * std::abs(pts[i + 1].x - pts[i].x) + px; };
|
||||
const ImDrawListFlags saved = draw_list->Flags;
|
||||
size_t i = 0;
|
||||
while (i + 1 < pts.size()) {
|
||||
const bool is_steep = steep(i);
|
||||
size_t j = i + 1;
|
||||
while (j + 1 < pts.size() && steep(j) == is_steep) ++j;
|
||||
draw_list->Flags = is_steep ? (saved & ~ImDrawListFlags_AntiAliasedLines) : saved;
|
||||
for (size_t n = i; n <= j; ++n) draw_list->PathLineTo(pts[n]);
|
||||
draw_list->PathStroke(color_, ImDrawFlags_None, 1.0f);
|
||||
i = j;
|
||||
}
|
||||
draw_list->Flags = saved;
|
||||
draw_point(point_at(render_points_.back()));
|
||||
}
|
||||
draw_list->PopClipRect();
|
||||
}
|
||||
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
34
iqpilot/tools/cabana/ui/chart/sparkline.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
class Sparkline {
|
||||
public:
|
||||
void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, ImVec2 sz, double window_end);
|
||||
inline double freq() const { return freq_; }
|
||||
bool isEmpty() const { return render_points_.empty(); }
|
||||
|
||||
void draw(ImDrawList *draw_list, ImVec2 pos) const;
|
||||
|
||||
ImVec2 size = {};
|
||||
double min_val = 0;
|
||||
double max_val = 0;
|
||||
|
||||
private:
|
||||
struct Point {
|
||||
double x, y;
|
||||
};
|
||||
void render(const CabanaColor &color, int range, ImVec2 sz, double window_end);
|
||||
|
||||
std::vector<Point> points_;
|
||||
std::vector<ImVec2> render_points_;
|
||||
ImU32 color_ = 0;
|
||||
double window_end_ = 0;
|
||||
double xscale_ = 0;
|
||||
bool draw_individual_points_ = false;
|
||||
double freq_ = 0;
|
||||
};
|
||||
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
69
iqpilot/tools/cabana/ui/chart/tiplabel.cc
Normal file
@@ -0,0 +1,69 @@
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#include "tools/cabana/ui/chart/tiplabel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
|
||||
ImVec2 TipLabel::layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const {
|
||||
ImFont *bold = boldFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float line_height = ImGui::GetTextLineHeight();
|
||||
ImVec2 size(0, 0);
|
||||
float y = origin.y;
|
||||
for (const auto &line : text_) {
|
||||
float x = origin.x;
|
||||
if (line.has_marker) {
|
||||
if (p) drawColorMarker(p, ImVec2(x, y), line.marker);
|
||||
x += markerSize() + 4;
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.name.c_str());
|
||||
x += ImGui::CalcTextSize(line.name.c_str()).x;
|
||||
if (!line.bold.empty()) {
|
||||
if (p) p->AddText(bold, font_size, ImVec2(x, y), fg, line.bold.c_str());
|
||||
x += bold->CalcTextSizeA(font_size, FLT_MAX, 0.0f, line.bold.c_str()).x;
|
||||
}
|
||||
if (p) p->AddText(ImVec2(x, y), fg, line.rest.c_str());
|
||||
x += ImGui::CalcTextSize(line.rest.c_str()).x;
|
||||
size.x = std::max(size.x, x - origin.x);
|
||||
y += line_height;
|
||||
}
|
||||
size.y = y - origin.y;
|
||||
return size;
|
||||
}
|
||||
|
||||
ImVec2 TipLabel::sizeHint() const {
|
||||
return layoutLines(nullptr, ImVec2(0, 0), 0) + ImVec2(MARGIN * 2, MARGIN * 2);
|
||||
}
|
||||
|
||||
void TipLabel::showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect) {
|
||||
text_ = text;
|
||||
if (!text_.empty()) {
|
||||
ImVec2 extra(1, 1);
|
||||
size_ = sizeHint() + extra;
|
||||
ImVec2 tip_pos(pt.x + 8, rect.Min.y + 2);
|
||||
if (tip_pos.x + size_.x >= rect.Max.x) {
|
||||
tip_pos.x = pt.x - size_.x - 8;
|
||||
}
|
||||
if (rect.Contains(ImRect(tip_pos, tip_pos + size_))) {
|
||||
pos_ = tip_pos;
|
||||
visible_ = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
visible_ = false;
|
||||
}
|
||||
|
||||
void TipLabel::draw() {
|
||||
if (!visible_) return;
|
||||
|
||||
ImDrawList *p = ImGui::GetForegroundDrawList();
|
||||
const bool dark = isDarkTheme();
|
||||
const ImU32 bg = dark ? ImGui::GetColorU32(ImGuiCol_PopupBg) : ImGui::GetColorU32(ImGuiCol_ChildBg);
|
||||
const ImU32 fg = dark ? ImGui::GetColorU32(ImGuiCol_Text) : IM_COL32(0x40, 0x40, 0x44, 0xff);
|
||||
|
||||
p->AddRectFilled(pos_, pos_ + size_, bg);
|
||||
p->AddRect(pos_, pos_ + size_, ImGui::GetColorU32(ImGuiCol_Border));
|
||||
layoutLines(p, pos_ + ImVec2(MARGIN, MARGIN), fg);
|
||||
}
|
||||
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
35
iqpilot/tools/cabana/ui/chart/tiplabel.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
|
||||
struct TipLine {
|
||||
bool has_marker = false;
|
||||
ImU32 marker = 0;
|
||||
std::string name;
|
||||
std::string bold;
|
||||
std::string rest;
|
||||
};
|
||||
|
||||
class TipLabel {
|
||||
public:
|
||||
void showText(const ImVec2 &pt, const std::vector<TipLine> &text, const ImRect &rect);
|
||||
void hide() { visible_ = false; }
|
||||
bool isVisible() const { return visible_; }
|
||||
void draw();
|
||||
|
||||
private:
|
||||
|
||||
ImVec2 layoutLines(ImDrawList *p, const ImVec2 &origin, ImU32 fg) const;
|
||||
ImVec2 sizeHint() const;
|
||||
|
||||
static constexpr float MARGIN = 2.0f;
|
||||
std::vector<TipLine> text_;
|
||||
ImVec2 pos_;
|
||||
ImVec2 size_;
|
||||
bool visible_ = false;
|
||||
};
|
||||
Reference in New Issue
Block a user