/** * @file StatsPanel.cpp * @brief Statistics table implementation. */ #include "StatsPanel.h" #include "App.h" #include "imgui.h" #include "implot.h" #include namespace StreamHubClient { void RenderStatsPanel(App& app) { const auto& sources = app.sources(); ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollX | ImGuiTableFlags_SizingFixedFit; if (!ImGui::BeginTable("##statstbl", 9, flags)) { return; } ImGui::TableSetupScrollFreeze(1, 1); ImGui::TableSetupColumn("Source"); ImGui::TableSetupColumn("State"); ImGui::TableSetupColumn("Cycles Rx"); ImGui::TableSetupColumn("Lost"); ImGui::TableSetupColumn("Rate Hz"); ImGui::TableSetupColumn("Frags/cyc"); ImGui::TableSetupColumn("Bytes/cyc"); ImGui::TableSetupColumn("Cycle ms (avg±std)"); ImGui::TableSetupColumn("Cycle ms (min/max)"); ImGui::TableHeadersRow(); for (const auto& src : sources) { const SourceStats& st = src.stats; ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextUnformatted(src.id.c_str()); /* State cell with color */ ImGui::TableNextColumn(); ImVec4 stColor = (st.state == "connected") ? ImVec4(0.2f,0.9f,0.3f,1.f) : ImVec4(0.9f,0.3f,0.3f,1.f); ImGui::TextColored(stColor, "%s", st.state.c_str()); ImGui::TableNextColumn(); ImGui::Text("%llu", (unsigned long long)st.totalReceived); ImGui::TableNextColumn(); ImGui::Text("%llu", (unsigned long long)st.totalLost); ImGui::TableNextColumn(); ImGui::Text("%.2f ± %.2f", st.rateHz, st.rateStdHz); ImGui::TableNextColumn(); ImGui::Text("%.1f", st.fragsPerCycle); ImGui::TableNextColumn(); ImGui::Text("%.0f", st.bytesPerCycle); ImGui::TableNextColumn(); ImGui::Text("%.3f ± %.3f", st.cycleAvgMs, st.cycleStdMs); ImGui::TableNextColumn(); ImGui::Text("%.3f / %.3f", st.cycleMinMs, st.cycleMaxMs); } ImGui::EndTable(); /* ── Cycle-time histograms (20 bins, mirrors web UI) ────────────── */ for (const auto& src : sources) { const SourceStats& st = src.stats; if (st.cycleHistMax <= st.cycleHistMin) { continue; } char title[160]; snprintf(title, sizeof(title), "Cycle time %s##hist_%s", src.id.c_str(), src.id.c_str()); if (ImPlot::BeginPlot(title, ImVec2(-1.f, 140.f), ImPlotFlags_NoMouseText | ImPlotFlags_NoMenus)) { const double binW = (st.cycleHistMax - st.cycleHistMin) / 20.0; double xs[20]; for (int b = 0; b < 20; b++) { xs[b] = st.cycleHistMin + (b + 0.5) * binW; } ImPlot::SetupAxes("cycle ms", "count", ImPlotAxisFlags_AutoFit, ImPlotAxisFlags_AutoFit); ImPlot::PlotBars("##bars", xs, st.cycleHist, 20, binW * 0.9); ImPlot::EndPlot(); } } ImGui::Spacing(); if (ImGui::Button("Refresh")) { app.ws().sendText(BuildGetStats()); } } } /* namespace StreamHubClient */