#define IMGUI_DEFINE_MATH_OPERATORS #include "tools/cabana/ui/chart/chart.h" #include #include #include #include #include #include #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 &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 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("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(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(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(marker_size, fm_height))); x += w + 12; } int adjust_top = (y + row_height) - top_left.y; adjust_top = std::max(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 &events, std::vector &vals, std::vector &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 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::visibleRange(const std::vector &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::max(); double max = std::numeric_limits::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::max(); s.max = std::numeric_limits::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::max()) min = 0; if (max == std::numeric_limits::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 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::takeExtraSignals() { std::vector 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 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::max() ? "--" : utils::toString(s.min); std::string max = s.max == std::numeric_limits::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(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 sat(35, 99); std::uniform_int_distribution 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; }