feat(market): optional price-trend sparkline per portfolio group

Add a per-group "Sparkline" toggle (live-market bases only, like 24h). When on,
the group card draws a faint price-trend line in its lower band — the group's
value tracks the DRGX price, so it plots the market price_history (normalized),
colored green/red by overall direction. Behind the text so amounts stay
readable. The editor's live preview reflects it too.

Persisted as show_sparkline in settings.json; wired through PortfolioEntry +
the editor working state + the pfDrawSparkline() helper shared by card+preview.

Full-node + Lite build clean; ctest 1/1; hygiene clean. Rendering change —
needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 01:15:23 -05:00
parent b0ded528b1
commit b26aff392b
4 changed files with 51 additions and 0 deletions

View File

@@ -330,6 +330,8 @@ bool Settings::load(const std::string& path)
entry.showValue = e["show_value"].get<bool>();
if (e.contains("show_24h") && e["show_24h"].is_boolean())
entry.show24h = e["show_24h"].get<bool>();
if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean())
entry.showSparkline = e["show_sparkline"].get<bool>();
if (!entry.label.empty()) portfolio_entries_.push_back(std::move(entry));
}
}
@@ -487,6 +489,7 @@ bool Settings::save(const std::string& path)
entry["show_drgx"] = e.showDrgx;
entry["show_value"] = e.showValue;
entry["show_24h"] = e.show24h;
entry["show_sparkline"] = e.showSparkline;
j["portfolio_entries"].push_back(std::move(entry));
}
j["font_scale"] = font_scale_;

View File

@@ -89,6 +89,7 @@ public:
bool showDrgx = true; // show the DRGX amount on the card
bool showValue = true; // show the converted/fiat value on the card
bool show24h = false; // show the 24h % change (live-market bases only)
bool showSparkline = false; // show a price-trend sparkline (live-market bases only)
};
// Theme

View File

@@ -110,6 +110,26 @@ static char s_pf_manual_ccy[12] = "USD";
static bool s_pf_show_drgx = true;
static bool s_pf_show_value = true;
static bool s_pf_show_24h = false;
static bool s_pf_show_sparkline = false;
// A group's value trend tracks the DRGX price, so a group sparkline plots the price history
// (normalized) across a rect. Colored by overall direction. Only meaningful for market bases.
static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx,
const std::vector<double>& hist, ImU32 col)
{
int n = (int)hist.size();
if (n < 2) return;
double lo = *std::min_element(hist.begin(), hist.end());
double hi = *std::max_element(hist.begin(), hist.end());
if (hi <= lo) hi = lo + 1e-9;
float w = mx.x - mn.x, h = mx.y - mn.y;
std::vector<ImVec2> pts(n);
for (int i = 0; i < n; i++) {
float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f;
pts[i] = ImVec2(mn.x + t * w, mx.y - (float)((hist[i] - lo) / (hi - lo)) * h);
}
dl->AddPolyline(pts.data(), n, col, ImDrawFlags_None, 1.2f);
}
// Build the right-hand amount strings for a group card / preview from its price-data config.
struct PfDisplay {
@@ -177,6 +197,7 @@ static void PortfolioBeginEdit(App* app, int index)
s_pf_show_drgx = src.showDrgx;
s_pf_show_value = src.showValue;
s_pf_show_24h = src.show24h;
s_pf_show_sparkline = src.showSparkline;
} else {
s_pf_label[0] = '\0';
s_pf_addrs.clear();
@@ -188,6 +209,7 @@ static void PortfolioBeginEdit(App* app, int index)
s_pf_show_drgx = true;
s_pf_show_value = true;
s_pf_show_24h = false;
s_pf_show_sparkline = false;
}
s_pf_search[0] = '\0';
s_pf_type_filter = 0;
@@ -280,6 +302,14 @@ static void RenderPortfolioEditor(App* app)
pdl->AddRectFilled(ImVec2(bx, pMin.y + ppad * 0.6f),
ImVec2(bx + 3.0f, pMax.y - ppad * 0.6f), accent, 2.0f);
}
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1) &&
state.market.price_history.size() >= 2) {
const auto& h = state.market.price_history;
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f);
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
}
float rowY = pMin.y + ppad, nameX = pMin.x + ppad + (accent ? 6.0f : 0.0f);
if (!s_pf_icon.empty()) {
material::project_icons::drawByName(pdl, s_pf_icon,
@@ -418,6 +448,10 @@ static void RenderPortfolioEditor(App* app)
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h);
ImGui::EndDisabled();
// Sparkline on its own line (also live-market only).
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
ImGui::EndDisabled();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -629,6 +663,7 @@ static void RenderPortfolioEditor(App* app)
e.showDrgx = s_pf_show_drgx;
e.showValue = s_pf_show_value;
e.show24h = s_pf_show_24h;
e.showSparkline = s_pf_show_sparkline;
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e;
else entries.push_back(e);
persist(entries);
@@ -1298,6 +1333,17 @@ void RenderMarketTab(App* app)
ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f);
}
// Price-trend sparkline (faint, in the lower band, behind the text) for
// live-market groups. The group's value tracks the DRGX price, so plot its history.
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1) &&
market.price_history.size() >= 2) {
const auto& h = market.price_history;
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
ImVec2 spMin(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + pfCardH * 0.46f);
ImVec2 spMax(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f);
pfDrawSparkline(dl, spMin, spMax, h, spCol);
}
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
float rowY = gcMin.y + pfCardPad;
float gcRight = gcMax.x - pfCardPad;

View File

@@ -1134,6 +1134,7 @@ void I18n::loadBuiltinEnglish()
strings_["portfolio_show"] = "Show:";
strings_["portfolio_show_value"] = "Value";
strings_["portfolio_show_24h"] = "24h";
strings_["portfolio_show_sparkline"] = "Sparkline";
strings_["portfolio_appearance"] = "APPEARANCE";
strings_["portfolio_addresses_hdr"] = "ADDRESSES";
strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6";