feat(market): trend sparkline in all portfolio styles + fix candlestick availability
Two coupled Market-tab changes (same file): - Sparkline in every style: render the per-entry trend sparkline in Table (a reserved trend column, aligned under the column header, reserved only when at least one group opts in so the dense grid isn't padded otherwise) and Cards (a filled strip on the right, above the full-width Z/T bar), not just Spotlight — so the per-entry 'show sparkline' setting is honored in all three views. DPI-scale the sparkline stroke (1.2f/1.4f * dp), from the sparkline audit's L1. - Fix the candlestick toggle intermittently missing: the Market tab never triggered the per-exchange OHLC fetch on its own (only a pair-chip / refresh click did), so candlesticks were unavailable on a fresh tab open. Call refreshExchangeChart() every frame (self-throttled + in-flight-guarded — its own doc comment already says 'safe to call every frame'), which also fixes the new pair's candles never loading if you switch pairs mid-fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,16 +164,16 @@ static bool pfProjectSeries(const std::vector<double>& hist, ImVec2 mn, ImVec2 m
|
||||
}
|
||||
|
||||
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 col)
|
||||
const std::vector<double>& hist, ImU32 col, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f);
|
||||
dl->AddPolyline(pts.data(), (int)pts.size(), col, ImDrawFlags_None, 1.2f * dp);
|
||||
}
|
||||
|
||||
// Sparkline with a soft fill under the line — for the card's dedicated bottom strip.
|
||||
static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
const std::vector<double>& hist, ImU32 lineCol)
|
||||
const std::vector<double>& hist, ImU32 lineCol, float dp)
|
||||
{
|
||||
std::vector<ImVec2> pts;
|
||||
if (!pfProjectSeries(hist, mn, mx, pts)) return;
|
||||
@@ -182,7 +182,7 @@ static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
|
||||
dl->PathLineTo(ImVec2(pts[n - 1].x, mx.y));
|
||||
dl->PathLineTo(ImVec2(pts[0].x, mx.y));
|
||||
dl->PathFillConcave(WithAlpha(lineCol, 28));
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f);
|
||||
dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f * dp);
|
||||
}
|
||||
|
||||
|
||||
@@ -974,7 +974,7 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f);
|
||||
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol, dp);
|
||||
}
|
||||
}
|
||||
float rowY = pMin.y + ppad, nameX = pMin.x + ppad;
|
||||
@@ -1094,6 +1094,7 @@ bool PortfolioEditorActive() { return s_pfEdit.open; }
|
||||
static constexpr float kPfChgColW = 62.0f;
|
||||
static constexpr float kPfValColW = 108.0f;
|
||||
static constexpr float kPfDrgxColW = 118.0f;
|
||||
static constexpr float kPfSparkColW = 72.0f; // Table trend column, reserved only when a group shows one
|
||||
|
||||
struct PfTableCols { float labelR, drgxR, valR, chgR; };
|
||||
static PfTableCols pfTableCols(float right, float dp)
|
||||
@@ -1115,7 +1116,7 @@ static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp :
|
||||
static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const config::Settings::PortfolioEntry& e, const WalletState& state,
|
||||
const MarketInfo& market, int style, float dp,
|
||||
ImFont* sub1, ImFont* capFont, bool hov)
|
||||
ImFont* sub1, ImFont* capFont, bool hov, bool tableReserveSpark = false)
|
||||
{
|
||||
// One pass over the wallet's addresses: the shielded/transparent split (used by the Cards Z/T bar)
|
||||
// also yields the total, so we never scan twice.
|
||||
@@ -1193,9 +1194,12 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1;
|
||||
|
||||
if (style == 0) {
|
||||
// ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right).
|
||||
// No sparkline — the trend rides in a small coloured % pill, keeping this a scannable grid.
|
||||
const PfTableCols cols = pfTableCols(right, dp);
|
||||
// ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right), and a
|
||||
// small trend sparkline in a reserved far-right column when any group opts into one. The
|
||||
// column is reserved for ALL rows (tableReserveSpark) so they stay aligned with the header.
|
||||
const float sparkW = tableReserveSpark ? kPfSparkColW * dp : 0.0f;
|
||||
const float colsRight = right - (sparkW > 0.0f ? sparkW + colGap : 0.0f);
|
||||
const PfTableCols cols = pfTableCols(colsRight, dp);
|
||||
const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR;
|
||||
const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols)
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
@@ -1213,6 +1217,8 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f),
|
||||
hasChange?chgCol:OnSurfaceDisabled(), s.c_str());
|
||||
}
|
||||
if (hasSpark && sparkW > 0.0f)
|
||||
pfDrawSparkline(dl, ImVec2(right - sparkW, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp);
|
||||
const float lblMaxW = std::max(24.0f*dp, labelR - x);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str());
|
||||
} else if (style == 1) {
|
||||
@@ -1222,6 +1228,10 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
float iconSz = capFont->LegacySize, x = left;
|
||||
const float topY = rowMin.y + padY + 1.0f*dp;
|
||||
const float botY = rowMax.y - padY - zbarH - 3.0f*dp - capFont->LegacySize;
|
||||
// Optional trend sparkline strip on the right (above the Z/T bar); text right-aligns to its left.
|
||||
const float cSparkW = hasSpark ? std::min(110.0f * dp, (right - left) * 0.34f) : 0.0f;
|
||||
const float cSparkL = right - cSparkW;
|
||||
const float cRight = hasSpark ? (cSparkL - colGap) : right;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+sub1->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz);
|
||||
x += iconSz + Layout::spacingSm();
|
||||
@@ -1229,13 +1239,14 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
// value (top-right, leads via a soft shadow)
|
||||
std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string();
|
||||
float vW = wantValue ? tw(sub1, vTop) : 0.0f;
|
||||
if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(right - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cRight - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str());
|
||||
// label (top-left, muted so the value leads)
|
||||
float labelR = wantValue ? (right - vW - colGap) : right;
|
||||
float labelR = wantValue ? (cRight - vW - colGap) : cRight;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, labelR - x)).c_str());
|
||||
// DRGX (bottom-left) + 24h (bottom-right)
|
||||
if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str());
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(right - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(cRight - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); }
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, topY), ImVec2(right, rowMax.y - padY - zbarH - 2.0f*dp), spark, sparkCol, dp);
|
||||
// shielded/transparent split bar (Cards-only signature; reuses the split computed above)
|
||||
{
|
||||
const data::PortfolioSplit& sp = split;
|
||||
@@ -1289,7 +1300,7 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax,
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str());
|
||||
}
|
||||
}
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol);
|
||||
if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp);
|
||||
}
|
||||
|
||||
if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group
|
||||
@@ -2090,6 +2101,13 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
}
|
||||
|
||||
int style = app->settings() ? app->settings()->getPortfolioStyle() : 0;
|
||||
// Reserve the Table trend column for ALL rows + the header when ANY visible group opts into a
|
||||
// sparkline, so the columns stay aligned regardless of which individual rows draw one.
|
||||
bool anySpark = false;
|
||||
for (int i : vis) {
|
||||
const auto& e = allEntries[i];
|
||||
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { anySpark = true; break; }
|
||||
}
|
||||
float rowH = pfRowHeight(style, mktDp);
|
||||
// Table rows abut like a ledger (thin gap); Cards/Spotlight breathe.
|
||||
float rowGap = pfRowGapFor(style, mktDp);
|
||||
@@ -2118,7 +2136,8 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
const float hLeft = hp.x + Layout::spacingMd();
|
||||
const float hRight = hp.x + rowW - Layout::spacingMd();
|
||||
const PfTableCols hc = pfTableCols(hRight, mktDp);
|
||||
const float hColsRight = hRight - (anySpark ? kPfSparkColW * mktDp + Layout::spacingMd() : 0.0f);
|
||||
const PfTableCols hc = pfTableCols(hColsRight, mktDp);
|
||||
ImFont* ovF = Type().overline();
|
||||
const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f;
|
||||
const ImU32 hcol = OnSurfaceDisabled();
|
||||
@@ -2143,7 +2162,7 @@ static void mktDrawPortfolio(const MktCtx& cx)
|
||||
bool hov = ImGui::IsItemHovered();
|
||||
if (hov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
ImGui::PopID();
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov);
|
||||
pfDrawRow(rdl, rMin, rMax, allEntries[i], state, market, style, mktDp, sub1, capFont, hov, anySpark);
|
||||
if (clicked) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; }
|
||||
if (vi + 1 < (int)vis.size()) ImGui::Dummy(ImVec2(rowW, rowGap));
|
||||
}
|
||||
@@ -2260,6 +2279,10 @@ void RenderMarketTab(App* app)
|
||||
// Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/
|
||||
// month intervals populate promptly when the user opens the Market tab.
|
||||
app->refreshMarketChart();
|
||||
// ...and the SELECTED pair's per-exchange OHLC candles (self-throttled, in-flight-guarded). Without
|
||||
// this, the candle series only loaded on a pair-chip / refresh click, so the candlestick toggle was
|
||||
// missing (and "switch to candlesticks" silently did nothing) when the tab was just opened.
|
||||
app->refreshExchangeChart();
|
||||
const auto& registry = EffectiveRegistry(market);
|
||||
|
||||
// Load persisted exchange/pair on first frame
|
||||
|
||||
Reference in New Issue
Block a user