From d48c6db21c5d053f002a03ba0f26542980097ccc Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 16:52:09 -0500 Subject: [PATCH] feat(market): range-based chart intervals; redesign group cards (value top-right + body sparkline) - Chart interval buttons are now time RANGES, not bucket sizes: 1H = last hour, 1D = last 24h (5-min intraday), 1W = last 7 days, 1M = last 30 days (daily). pfChartSeries filters the timestamped series to the window; X-axis "ago" labels and the period-change span now derive from the real timestamps. - Portfolio group cards redesigned: name (+ secondary DRGX) on the left; the value with its % change stacked in the top-right corner; the sparkline fills the card body. The sparkline now renders on 2-cell cards too (down to ~8px of body), with tighter padding on short cards. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 167 +++++++++++++++------------------- 1 file changed, 73 insertions(+), 94 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index f5b0e3f..b1aa947 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -169,46 +169,25 @@ static std::vector pfSparklineSeries(const MarketInfo& m, int interval) // 0=Live (in-session minute buffer) 1=hour 2=day 3=week 4=month. Session-scoped. static int s_chart_interval = 2; // default: Day (historical) -// Approx seconds represented by one plotted point at the given chart interval, for X-axis labels. -static float pfChartSecPerPoint(int interval) { - switch (interval) { - case 1: return 3600.0f; // hour - case 2: return 86400.0f; // day - case 3: return 604800.0f; // week - case 4: return 2592000.0f; // month (~30d) - default: return 60.0f; // minute (live buffer) - } -} - -// Timestamped variant of pfBucketBySeconds: each bucket keeps its last sample's timestamp so the -// chart can label the X axis / hover with real dates. Input oldest->newest; output preserves order. -static std::vector> pfBucketBySecondsTimed( - const std::vector>& series, long windowSec) -{ - std::vector> out; - if (series.empty() || windowSec <= 0) return out; - long curBucket = 0; double sum = 0.0; int cnt = 0; std::time_t lastTs = 0; - for (const auto& s : series) { - long b = (long)(s.first / windowSec); - if (cnt > 0 && b != curBucket) { out.push_back({ lastTs, sum / cnt }); sum = 0.0; cnt = 0; } - curBucket = b; sum += s.second; ++cnt; lastTs = s.first; - } - if (cnt > 0) out.push_back({ lastTs, sum / cnt }); - return out; -} - -// Timestamped price series backing the main chart for the selected interval (Live/1H/1D/1W/1M). -// Historical intervals bucket the CoinGecko series; Live (or an un-fetched historical interval) -// synthesizes minute timestamps for the in-session buffer, ending at `now`. +// Timestamped price series backing the main chart for the selected RANGE (Live/1H/1D/1W/1M). +// The interval buttons select a time window ending at `now`: 1H = last hour, 1D = last 24h, +// 1W = last 7 days, 1M = last 30 days. 1H/1D come from the 5-minute intraday series; 1W/1M from +// the daily series. Live (or an un-fetched/empty range) uses the in-session minute buffer. static std::vector> pfChartSeries(const MarketInfo& m, int interval, std::time_t now) { const long kDay = 86400; + auto lastWindow = [now](const std::vector>& src, long rangeSec) { + std::vector> out; + std::time_t cutoff = now - (std::time_t)rangeSec; + for (const auto& s : src) if (s.first >= cutoff) out.push_back(s); + return out; + }; switch (interval) { - case 1: { auto v = pfBucketBySecondsTimed(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } - case 2: { auto v = pfBucketBySecondsTimed(m.price_chart_daily, kDay); if (v.size() >= 2) return v; break; } - case 3: { auto v = pfBucketBySecondsTimed(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } - case 4: { auto v = pfBucketBySecondsTimed(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; } + case 1: { auto v = lastWindow(m.price_chart_intraday, 3600); if (v.size() >= 2) return v; break; } // 1H + case 2: { auto v = lastWindow(m.price_chart_intraday, kDay); if (v.size() >= 2) return v; break; } // 1D + case 3: { auto v = lastWindow(m.price_chart_daily, 7 * kDay); if (v.size() >= 2) return v; break; } // 1W + case 4: { auto v = lastWindow(m.price_chart_daily, 30 * kDay); if (v.size() >= 2) return v; break; } // 1M default: break; } std::vector> out; @@ -914,7 +893,6 @@ void RenderMarketTab(App* app) ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); - ImFont* h4 = Type().h4(); ImFont* body2 = Type().body2(); char buf[128]; @@ -996,9 +974,10 @@ void RenderMarketTab(App* app) // Chart series for the selected interval (timestamped), shared by the hero change badge and // the chart block below. Computed once here so the badge can report the displayed period's change. + std::time_t nowSec = std::time(nullptr); std::vector chartTimes; s_price_history.clear(); - for (const auto& pr : pfChartSeries(market, s_chart_interval, std::time(nullptr))) { + for (const auto& pr : pfChartSeries(market, s_chart_interval, nowSec)) { s_price_history.push_back(pr.second); chartTimes.push_back(pr.first); } @@ -1317,17 +1296,17 @@ void RenderMarketTab(App* app) markExtreme(loIdx, false); } - // X-axis time labels. Each point spans one interval unit (minute/hour/day/week/month); - // convert points-before-now to a "~