feat(market): card/chart refinements — 8-cell min width, larger labels, flat fill, range-matched change
Portfolio group cards: - Always show the secondary DRGX amount, including on 2-cell cards. - Larger group icon + label (subtitle1); value stays subtitle1 for consistency. - Minimum group width is now 8 cells (default width 8). Price chart: - Area fill is now a flat translucent fill matching the group-card sparklines (dropped the vertical gradient + its primitive-API helper). - Interval buttons sit near the top of the chart area (removed the empty gap above them) with a comfortable gap below before the plot. - The change badge's suffix now matches the SELECTED range (1H->1h, 1D->24h, 1W->7d, 1M->30d) instead of the raw data span (which read 23h / 6d). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,7 +96,7 @@ public:
|
||||
// (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card.
|
||||
int gridCol = -1;
|
||||
int gridRow = -1;
|
||||
int gridW = 6;
|
||||
int gridW = 8; // default group width (cells); minimum is 8
|
||||
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
|
||||
};
|
||||
|
||||
|
||||
@@ -197,27 +197,6 @@ static std::vector<std::pair<std::time_t, double>> pfChartSeries(const MarketInf
|
||||
return out;
|
||||
}
|
||||
|
||||
// Vertical-gradient area fill under a polyline (solid `topCol` at the curve, fading to `botCol` at
|
||||
// `bottomY`). Drawn as one gradient quad per segment via the draw list's primitive API.
|
||||
static void pfFillGradientArea(ImDrawList* dl, const std::vector<ImVec2>& pts, float bottomY,
|
||||
ImU32 topCol, ImU32 botCol)
|
||||
{
|
||||
int n = (int)pts.size();
|
||||
if (n < 2) return;
|
||||
const ImVec2 uv = dl->_Data->TexUvWhitePixel;
|
||||
for (int i = 0; i + 1 < n; i++) {
|
||||
const ImVec2& p0 = pts[i];
|
||||
const ImVec2& p1 = pts[i + 1];
|
||||
dl->PrimReserve(6, 4);
|
||||
unsigned int base = (unsigned int)dl->_VtxCurrentIdx;
|
||||
dl->PrimWriteVtx(p0, uv, topCol);
|
||||
dl->PrimWriteVtx(p1, uv, topCol);
|
||||
dl->PrimWriteVtx(ImVec2(p1.x, bottomY), uv, botCol);
|
||||
dl->PrimWriteVtx(ImVec2(p0.x, bottomY), uv, botCol);
|
||||
dl->PrimWriteIdx((ImDrawIdx)base); dl->PrimWriteIdx((ImDrawIdx)(base + 1)); dl->PrimWriteIdx((ImDrawIdx)(base + 2));
|
||||
dl->PrimWriteIdx((ImDrawIdx)base); dl->PrimWriteIdx((ImDrawIdx)(base + 2)); dl->PrimWriteIdx((ImDrawIdx)(base + 3));
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -929,7 +908,7 @@ void RenderMarketTab(App* app)
|
||||
// this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch.
|
||||
float pfCardGap = 0.0f; // gapless cell grid
|
||||
float pfInset = 3.0f * mktDp; // per-card inset for visual separation
|
||||
const int pfMinW = 4, pfMinH = 2; // minimum card size in cells (~128x64)
|
||||
const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64)
|
||||
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
|
||||
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
|
||||
float pfCellH = pfCellW;
|
||||
@@ -983,26 +962,18 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
// Change over the displayed period (Live falls back to the market's 24h figure).
|
||||
double periodChangePct = market.change_24h;
|
||||
bool periodIsInterval = false;
|
||||
if (s_chart_interval != 0 && s_price_history.size() >= 2 && s_price_history.front() > 0.0) {
|
||||
if (s_chart_interval != 0 && s_price_history.size() >= 2 && s_price_history.front() > 0.0)
|
||||
periodChangePct = (s_price_history.back() - s_price_history.front()) / s_price_history.front() * 100.0;
|
||||
periodIsInterval = true;
|
||||
}
|
||||
bool chartUp = periodChangePct >= 0.0;
|
||||
// Compact label for the displayed span (from the real timestamps), e.g. "24h", "7d", "1.0y".
|
||||
auto pfSpanLabel = [](long sec) -> std::string {
|
||||
char b[32];
|
||||
if (sec < 5400) snprintf(b, sizeof(b), "%ldm", sec / 60);
|
||||
else if (sec < 172800) snprintf(b, sizeof(b), "%ldh", sec / 3600);
|
||||
else if (sec < 1209600) snprintf(b, sizeof(b), "%ldd", sec / 86400);
|
||||
else if (sec < 7776000) snprintf(b, sizeof(b), "%ldw", sec / 604800);
|
||||
else if (sec < 63072000) snprintf(b, sizeof(b), "%ldmo", sec / 2592000);
|
||||
else snprintf(b, sizeof(b), "%.1fy", sec / 31536000.0);
|
||||
return b;
|
||||
};
|
||||
// Suffix = the SELECTED range (not the raw data span), so 1D reads "24h", 1W reads "7d", etc.
|
||||
std::string periodSuffix = "24h";
|
||||
if (periodIsInterval && chartTimes.size() >= 2)
|
||||
periodSuffix = pfSpanLabel((long)(chartTimes.back() - chartTimes.front()));
|
||||
switch (s_chart_interval) {
|
||||
case 1: periodSuffix = "1h"; break; // 1H
|
||||
case 2: periodSuffix = "24h"; break; // 1D
|
||||
case 3: periodSuffix = "7d"; break; // 1W
|
||||
case 4: periodSuffix = "30d"; break; // 1M
|
||||
default: periodSuffix = "24h"; break; // Live (uses the market's 24h change)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
||||
@@ -1140,11 +1111,14 @@ void RenderMarketTab(App* app)
|
||||
|
||||
float chartPad = pad;
|
||||
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
||||
float stripH = pillH + Layout::spacingSm(); // top control/info strip height
|
||||
// Buttons sit near the top of the chart area (minimal gap above), with a comfortable gap
|
||||
// below before the plot so they aren't crowded against the chart.
|
||||
float stripTop = chartMin.y + Layout::spacingXs();
|
||||
float stripH = pillH + Layout::spacingMd();
|
||||
|
||||
// ---- Top strip: interval buttons (left) + 24H volume / market cap + refresh (right) ----
|
||||
{
|
||||
float rowTop = chartMin.y + chartPad;
|
||||
float rowTop = stripTop;
|
||||
float textY = rowTop + (pillH - capFont->LegacySize) * 0.5f;
|
||||
|
||||
// Interval buttons (left). Live = in-session buffer; the rest are historical.
|
||||
@@ -1216,7 +1190,7 @@ void RenderMarketTab(App* app)
|
||||
float labelPadBottom = Layout::spacingXl() + Layout::spacingSm();
|
||||
float plotLeft = chartMin.x + labelPadLeft;
|
||||
float plotRight = chartMax.x - chartPad;
|
||||
float plotTop = chartMin.y + chartPad + stripH;
|
||||
float plotTop = stripTop + stripH;
|
||||
float plotBottom = chartMax.y - labelPadBottom;
|
||||
float plotW = plotRight - plotLeft;
|
||||
float plotH = plotBottom - plotTop;
|
||||
@@ -1264,8 +1238,11 @@ void RenderMarketTab(App* app)
|
||||
points[i] = ImVec2(x, y);
|
||||
}
|
||||
|
||||
// Gradient area fill under the curve (solid near the line, fading to transparent).
|
||||
pfFillGradientArea(dl, points, plotBottom, WithAlpha(dirCol, 65), WithAlpha(dirCol, 0));
|
||||
// Flat translucent area fill under the curve (matches the portfolio group sparklines).
|
||||
for (size_t i = 0; i < n; i++) dl->PathLineTo(points[i]);
|
||||
dl->PathLineTo(ImVec2(points[n - 1].x, plotBottom));
|
||||
dl->PathLineTo(ImVec2(points[0].x, plotBottom));
|
||||
dl->PathFillConcave(WithAlpha(dirCol, 28));
|
||||
|
||||
// Line
|
||||
dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size);
|
||||
@@ -1623,7 +1600,6 @@ void RenderMarketTab(App* app)
|
||||
// below it in the top-right corner; the sparkline fills the body — so it renders even
|
||||
// on a 2-cell card. Tighter padding on short cards.
|
||||
float cardHeight = gcMax.y - gcMin.y;
|
||||
bool cardTall = cardHeight > 112.0f * mktDp;
|
||||
bool cardShort = cardHeight < 90.0f * mktDp;
|
||||
float hpad = cardShort ? Layout::spacingSm() : pad;
|
||||
float hleft = gcMin.x + hpad, hright = gcMax.x - hpad;
|
||||
@@ -1635,7 +1611,7 @@ void RenderMarketTab(App* app)
|
||||
float valLeft = hright;
|
||||
bool showChange = e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0;
|
||||
if (!heroStr.empty()) {
|
||||
ImFont* vf = cardTall ? sub1 : body2;
|
||||
ImFont* vf = sub1;
|
||||
std::string vs = fit(heroStr, vf, hAvailW * 0.66f);
|
||||
float vw = vf->CalcTextSizeA(vf->LegacySize, FLT_MAX, 0, vs.c_str()).x;
|
||||
valLeft = hright - vw;
|
||||
@@ -1651,19 +1627,19 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// Left column: icon + name (top), secondary DRGX below on taller cards.
|
||||
// Left column: icon + name (larger, top), secondary DRGX amount below (always shown).
|
||||
float nameX = hleft;
|
||||
if (!e.icon.empty()) {
|
||||
material::project_icons::drawByName(dl, e.icon.c_str(),
|
||||
ImVec2(nameX + body2->LegacySize * 0.5f, topY + body2->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurfaceMedium(), Type().iconSmall(), body2->LegacySize);
|
||||
nameX += body2->LegacySize + Layout::spacingSm();
|
||||
ImVec2(nameX + sub1->LegacySize * 0.5f, topY + sub1->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurfaceMedium(), Type().iconSmall(), sub1->LegacySize);
|
||||
nameX += sub1->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
float nameMaxW = std::max(24.0f * mktDp, valLeft - Layout::spacingSm() - nameX);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(nameX, topY), OnSurfaceMedium(),
|
||||
fit(e.label, body2, nameMaxW).c_str());
|
||||
float leftBottom = topY + body2->LegacySize;
|
||||
if (!footStr.empty() && cardHeight > 88.0f * mktDp) {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, topY), OnSurfaceMedium(),
|
||||
fit(e.label, sub1, nameMaxW).c_str());
|
||||
float leftBottom = topY + sub1->LegacySize;
|
||||
if (!footStr.empty()) {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(nameX, leftBottom + 1.0f * mktDp),
|
||||
OnSurfaceDisabled(), fit(footStr, capFont, nameMaxW).c_str());
|
||||
leftBottom += capFont->LegacySize + 1.0f * mktDp;
|
||||
@@ -1753,8 +1729,8 @@ void RenderMarketTab(App* app)
|
||||
ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y);
|
||||
drawCard(mn, mx, pfEntriesGeo[i], true);
|
||||
dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);
|
||||
// Live size readout (e.g. "6\xC3\x972") so the reached grid span is visible — the
|
||||
// minimum is pfMinW x pfMinH (4x2).
|
||||
// Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the
|
||||
// minimum is pfMinW x pfMinH (8x2).
|
||||
{
|
||||
char szb[16]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh);
|
||||
ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb);
|
||||
|
||||
Reference in New Issue
Block a user