IQ.Pilot Release Commit @ 0b96bd5

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-02 16:46:45 -05:00
parent 4fb3da761f
commit 7745f48100
162 changed files with 12835 additions and 9476 deletions

View 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;
}

View 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_;
};

View 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;
}

View 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 *> &currentCharts() { 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;
};

View 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);
}
}
}

View 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;
};

View 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();
}

View 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;
};

View 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);
}

View 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;
};