refactor(market): extract price chart into mktDrawPriceChart
Move the ~250-line chart block (interval strip + 24h stats + refresh, plotted curve with grid/area-fill/hi-lo+time labels/hover crosshair+tooltip, empty state) out of RenderMarketTab into mktDrawPriceChart(MktCtx), recomputing cheap locals and reading the precomputed series via the ctx. Drop the now-unused hs/pad locals. Verbatim body. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1542,6 +1542,275 @@ static void mktDrawPriceHero(const MktCtx& cx)
|
|||||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Price chart, drawn inside the hero's shared glass panel: interval strip + 24h stats + refresh,
|
||||||
|
// then the plotted curve (grid, area fill, hi/lo + time labels, hover crosshair/tooltip) or an
|
||||||
|
// empty state. Reads the precomputed series (s_mkt.history / cx.chartTimes).
|
||||||
|
static void mktDrawPriceChart(const MktCtx& cx)
|
||||||
|
{
|
||||||
|
App* app = cx.app;
|
||||||
|
const auto& market = app->getWalletState().market;
|
||||||
|
auto& S = schema::UI();
|
||||||
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
float pad = Layout::cardInnerPadding();
|
||||||
|
float hs = Layout::hScale(availWidth);
|
||||||
|
float mktDp = Layout::dpiScale();
|
||||||
|
ImFont* capFont = Type().caption();
|
||||||
|
ImFont* sub1 = Type().subtitle1();
|
||||||
|
float chartH = cx.chartH;
|
||||||
|
const auto& chartTimes = *cx.chartTimes;
|
||||||
|
std::time_t nowSec = cx.nowSec;
|
||||||
|
bool chartUp = cx.chartUp;
|
||||||
|
char buf[128];
|
||||||
|
|
||||||
|
// The chart series (s_mkt.history) + timestamps (chartTimes) were computed in the
|
||||||
|
// precompute above. Historical intervals come from CoinGecko market_chart; "Live" is the
|
||||||
|
// in-session buffer. Until two points exist, the empty-state below is shown.
|
||||||
|
// The chart shares the combined hero+chart glass panel drawn in PRICE SUMMARY above.
|
||||||
|
ImVec2 chartMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH);
|
||||||
|
|
||||||
|
float chartPad = pad;
|
||||||
|
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
||||||
|
// Buttons sit near the top of the chart area, with a gap below them before the plot.
|
||||||
|
float stripTop = chartMin.y + Layout::spacingXs();
|
||||||
|
|
||||||
|
// ---- Top strip: interval buttons (left) + 24H volume / market cap + refresh (right) ----
|
||||||
|
{
|
||||||
|
float rowTop = stripTop;
|
||||||
|
float textY = rowTop + (pillH - capFont->LegacySize) * 0.5f;
|
||||||
|
|
||||||
|
// Interval buttons (left). Live = in-session buffer; the rest are historical.
|
||||||
|
const struct { const char* lbl; int iv; } kIvs[] = {
|
||||||
|
{ TR("market_iv_live"), 0 }, { "1H", 1 }, { "1D", 2 }, { "1W", 3 }, { "1M", 4 }
|
||||||
|
};
|
||||||
|
float bx = chartMin.x + chartPad;
|
||||||
|
for (int b = 0; b < 5; b++) {
|
||||||
|
float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, kIvs[b].lbl).x;
|
||||||
|
float bw = tw + Layout::spacingSm() * 2.0f;
|
||||||
|
ImVec2 bmn(bx, rowTop), bmx(bx + bw, rowTop + pillH);
|
||||||
|
bool sel = (s_mkt.chartInterval == kIvs[b].iv);
|
||||||
|
bool bhov = material::IsRectHovered(bmn, bmx);
|
||||||
|
ImU32 bg = sel ? WithAlpha(Primary(), 200)
|
||||||
|
: (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18));
|
||||||
|
dl->AddRectFilled(bmn, bmx, bg, 4.0f);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY),
|
||||||
|
sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl);
|
||||||
|
ImGui::SetCursorScreenPos(bmn);
|
||||||
|
ImGui::PushID(9100 + b);
|
||||||
|
if (ImGui::InvisibleButton("##civ", ImVec2(bw, pillH))) {
|
||||||
|
s_mkt.chartInterval = kIvs[b].iv;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::PopID();
|
||||||
|
bx += bw + Layout::spacingXs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh button (far right).
|
||||||
|
float rEdge = chartMax.x - chartPad;
|
||||||
|
ImFont* iconSmall = material::Typography::instance().iconSmall();
|
||||||
|
ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH);
|
||||||
|
bool refreshHov = material::IsRectHovered(rbMin, rbMax);
|
||||||
|
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
||||||
|
ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
||||||
|
dl->AddText(iconSmall, iconSmall->LegacySize,
|
||||||
|
ImVec2(rbMin.x + (pillH - icSz.x) * 0.5f, rbMin.y + (pillH - icSz.y) * 0.5f),
|
||||||
|
refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH);
|
||||||
|
ImGui::SetCursorScreenPos(rbMin);
|
||||||
|
if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(pillH, pillH))) {
|
||||||
|
app->refreshMarketData();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price"));
|
||||||
|
|
||||||
|
// 24H volume + market cap, right-aligned to the left of the refresh button. Skipped
|
||||||
|
// when there's no room (narrow window) so they never overlap the interval buttons.
|
||||||
|
float sxr = rbMin.x - Layout::spacingMd();
|
||||||
|
auto drawStat = [&](const char* label, const std::string& val) {
|
||||||
|
char sb[64]; snprintf(sb, sizeof(sb), "%s %s", label, val.c_str());
|
||||||
|
float w = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, sb).x;
|
||||||
|
if (sxr - w < bx + Layout::spacingMd()) return;
|
||||||
|
sxr -= w;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(sxr, textY), OnSurfaceMedium(), sb);
|
||||||
|
sxr -= Layout::spacingMd();
|
||||||
|
};
|
||||||
|
if (market.price_usd > 0) {
|
||||||
|
drawStat(TR("market_cap_short"), FormatCompactUSD(market.market_cap));
|
||||||
|
drawStat(TR("market_vol_short"), FormatCompactUSD(market.volume_24h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size,
|
||||||
|
S.drawElement("tabs.market", "chart-y-axis-padding").size * hs);
|
||||||
|
// Extra bottom room so the time labels aren't crowded against the card's bottom edge.
|
||||||
|
float labelPadBottom = Layout::spacingXl() + Layout::spacingSm();
|
||||||
|
float plotLeft = chartMin.x + labelPadLeft;
|
||||||
|
float plotRight = chartMax.x - chartPad;
|
||||||
|
float plotTop = stripTop + pillH + Layout::spacingMd(); // gap below the buttons before the plot
|
||||||
|
float plotBottom = chartMax.y - labelPadBottom;
|
||||||
|
float plotW = plotRight - plotLeft;
|
||||||
|
float plotH = plotBottom - plotTop;
|
||||||
|
|
||||||
|
if (s_mkt.history.size() >= 2) {
|
||||||
|
// Compute Y range with padding
|
||||||
|
double yMin = *std::min_element(s_mkt.history.begin(), s_mkt.history.end());
|
||||||
|
double yMax = *std::max_element(s_mkt.history.begin(), s_mkt.history.end());
|
||||||
|
if (yMax <= yMin) { yMax = yMin + 1e-8; }
|
||||||
|
double yRange = yMax - yMin;
|
||||||
|
double yPadding = yRange * 0.12;
|
||||||
|
yMin -= yPadding;
|
||||||
|
yMax += yPadding;
|
||||||
|
|
||||||
|
// Horizontal grid lines (4 lines)
|
||||||
|
for (int g = 0; g <= 4; g++) {
|
||||||
|
float gy = plotTop + plotH * (float)g / 4.0f;
|
||||||
|
dl->AddLine(ImVec2(plotLeft, gy), ImVec2(plotRight, gy),
|
||||||
|
IM_COL32(255, 255, 255, 12), 1.0f);
|
||||||
|
double labelVal = yMax - (yMax - yMin) * (double)g / 4.0;
|
||||||
|
// Adaptive precision (via the shared price formatter) — cleaner than a fixed 6 decimals.
|
||||||
|
snprintf(buf, sizeof(buf), "%s", FormatPrice(labelVal).c_str());
|
||||||
|
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
||||||
|
// smaller than the label width) so it never spills onto the tab background.
|
||||||
|
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
||||||
|
OnSurfaceDisabled(), buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build points
|
||||||
|
size_t n = s_mkt.history.size();
|
||||||
|
std::vector<ImVec2> points(n);
|
||||||
|
|
||||||
|
// Colored by the DISPLAYED period's direction (chartUp), not just 24h.
|
||||||
|
ImU32 dirCol = chartUp ? Success() : Error();
|
||||||
|
ImU32 lineCol = WithAlpha(dirCol, 220);
|
||||||
|
ImU32 dotCol = dirCol;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f;
|
||||||
|
float x = plotLeft + t * plotW;
|
||||||
|
float y = plotBottom - (float)((s_mkt.history[i] - yMin) / (yMax - yMin)) * plotH;
|
||||||
|
points[i] = ImVec2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (no per-point dots — a clean curve).
|
||||||
|
dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size);
|
||||||
|
|
||||||
|
// High/low price labels at the displayed range's extremes (no dot markers).
|
||||||
|
if (n >= 3) {
|
||||||
|
size_t hiIdx = (size_t)(std::max_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin());
|
||||||
|
size_t loIdx = (size_t)(std::min_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin());
|
||||||
|
auto markExtreme = [&](size_t idx, bool high) {
|
||||||
|
ImVec2 p = points[idx];
|
||||||
|
std::string lbl = FormatPrice(s_mkt.history[idx]);
|
||||||
|
ImVec2 ls = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, lbl.c_str());
|
||||||
|
float ly = high ? p.y - ls.y - 5.0f * mktDp : p.y + 5.0f * mktDp;
|
||||||
|
float lx = std::min(std::max(plotLeft, p.x - ls.x * 0.5f), plotRight - ls.x);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(lx, ly), OnSurfaceMedium(), lbl.c_str());
|
||||||
|
};
|
||||||
|
markExtreme(hiIdx, true);
|
||||||
|
markExtreme(loIdx, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-axis time labels. The series is timestamped, so label each tick with the real
|
||||||
|
// "<time> ago" (rightmost = "now") derived from the sample's timestamp.
|
||||||
|
{
|
||||||
|
int nPts = (int)n;
|
||||||
|
int ticks = std::min(4, nPts);
|
||||||
|
for (int tk = 0; tk < ticks; tk++) {
|
||||||
|
float t = (ticks > 1) ? (float)tk / (float)(ticks - 1) : 1.0f;
|
||||||
|
float xpos = plotLeft + t * plotW;
|
||||||
|
int idx = (int)(t * (nPts - 1) + 0.5f);
|
||||||
|
float secAgo = (idx < (int)chartTimes.size()) ? (float)(nowSec - chartTimes[idx])
|
||||||
|
: (float)(nPts - 1 - idx) * 60.0f;
|
||||||
|
char tlbl[32];
|
||||||
|
if (secAgo < 45.0f) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now"));
|
||||||
|
else if (secAgo < 5400.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fm", secAgo / 60.0f);
|
||||||
|
else if (secAgo < 172800.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fh", secAgo / 3600.0f);
|
||||||
|
else if (secAgo < 1209600.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fd", secAgo / 86400.0f);
|
||||||
|
else if (secAgo < 7776000.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fw", secAgo / 604800.0f);
|
||||||
|
else snprintf(tlbl, sizeof(tlbl), "~%.0fmo", secAgo / 2592000.0f);
|
||||||
|
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, tlbl);
|
||||||
|
float lx = (tk == 0) ? plotLeft
|
||||||
|
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||||
|
: xpos - lblSz.x * 0.5f;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hover crosshair + tooltip
|
||||||
|
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
||||||
|
if (mousePos.x >= plotLeft && mousePos.x <= plotRight &&
|
||||||
|
mousePos.y >= plotTop && mousePos.y <= plotBottom + labelPadBottom)
|
||||||
|
{
|
||||||
|
float mx = mousePos.x - plotLeft;
|
||||||
|
float closest_t = mx / plotW;
|
||||||
|
int idx = (int)(closest_t * (n - 1) + 0.5f);
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
if (idx >= (int)n) idx = (int)n - 1;
|
||||||
|
|
||||||
|
float px = points[idx].x;
|
||||||
|
float py = points[idx].y;
|
||||||
|
|
||||||
|
dl->AddLine(ImVec2(px, plotTop), ImVec2(px, plotBottom),
|
||||||
|
IM_COL32(255, 255, 255, 40), 1.0f);
|
||||||
|
dl->AddLine(ImVec2(plotLeft, py), ImVec2(plotRight, py),
|
||||||
|
IM_COL32(255, 255, 255, 40), 1.0f);
|
||||||
|
|
||||||
|
float hoverDotR = std::max(S.drawElement("tabs.market", "chart-hover-dot-min-radius").size, S.drawElement("tabs.market", "chart-hover-dot-radius").size * hs);
|
||||||
|
float hoverRingR = std::max(S.drawElement("tabs.market", "chart-hover-ring-min-radius").size, S.drawElement("tabs.market", "chart-hover-ring-radius").size * hs);
|
||||||
|
dl->AddCircleFilled(ImVec2(px, py), hoverDotR, dotCol);
|
||||||
|
dl->AddCircle(ImVec2(px, py), hoverRingR, IM_COL32(255, 255, 255, 80), 0, 1.5f);
|
||||||
|
|
||||||
|
// Tooltip: real date/time (from the series timestamps) + price at the hovered point.
|
||||||
|
// Sub-day intervals (Live/1H) show the time; day+ intervals show the date.
|
||||||
|
char whenBuf[32] = "";
|
||||||
|
if (idx < (int)chartTimes.size()) {
|
||||||
|
std::time_t tt = chartTimes[idx];
|
||||||
|
std::tm* tmv = std::localtime(&tt);
|
||||||
|
if (tmv) std::strftime(whenBuf, sizeof(whenBuf),
|
||||||
|
s_mkt.chartInterval <= 1 ? "%b %d %H:%M" : "%b %d, %Y", tmv);
|
||||||
|
}
|
||||||
|
if (whenBuf[0])
|
||||||
|
snprintf(buf, sizeof(buf), "%s \xC2\xB7 %s", whenBuf, FormatPrice(s_mkt.history[idx]).c_str());
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_mkt.history[idx]).c_str());
|
||||||
|
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
||||||
|
float tipX = px + 10;
|
||||||
|
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
||||||
|
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
||||||
|
tipX = px - tipSz.x - tipPad * 2 - 10;
|
||||||
|
if (tipY < plotTop) tipY = py + 10;
|
||||||
|
|
||||||
|
ImVec2 tipMin(tipX, tipY);
|
||||||
|
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
||||||
|
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
||||||
|
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Empty state, centered in the plot area (the interval strip + refresh are already drawn).
|
||||||
|
const char* msg = TR("market_no_history");
|
||||||
|
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
|
||||||
|
dl->AddText(sub1, sub1->LegacySize,
|
||||||
|
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, (plotTop + plotBottom) * 0.5f - ts.y * 0.5f),
|
||||||
|
OnSurfaceDisabled(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(chartMin.x, chartMin.y + chartH));
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
|
}
|
||||||
|
|
||||||
void RenderMarketTab(App* app)
|
void RenderMarketTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
@@ -1583,9 +1852,7 @@ void RenderMarketTab(App* app)
|
|||||||
|
|
||||||
// Responsive: scale factors per frame
|
// Responsive: scale factors per frame
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||||
float hs = Layout::hScale(availWidth);
|
|
||||||
float vs = Layout::vScale(marketAvail.y);
|
float vs = Layout::vScale(marketAvail.y);
|
||||||
float pad = Layout::cardInnerPadding();
|
|
||||||
float gap = Layout::cardGap();
|
float gap = Layout::cardGap();
|
||||||
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||||
@@ -1681,254 +1948,7 @@ void RenderMarketTab(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// PRICE CHART — drawn inside the combined hero card's glass panel (above)
|
// PRICE CHART — drawn inside the combined hero card's glass panel (above)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
mktDrawPriceChart(mktc);
|
||||||
// The chart series (s_mkt.history) + timestamps (chartTimes) were computed in the
|
|
||||||
// precompute above. Historical intervals come from CoinGecko market_chart; "Live" is the
|
|
||||||
// in-session buffer. Until two points exist, the empty-state below is shown.
|
|
||||||
// The chart shares the combined hero+chart glass panel drawn in PRICE SUMMARY above.
|
|
||||||
ImVec2 chartMin = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH);
|
|
||||||
|
|
||||||
float chartPad = pad;
|
|
||||||
float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f;
|
|
||||||
// Buttons sit near the top of the chart area, with a gap below them before the plot.
|
|
||||||
float stripTop = chartMin.y + Layout::spacingXs();
|
|
||||||
|
|
||||||
// ---- Top strip: interval buttons (left) + 24H volume / market cap + refresh (right) ----
|
|
||||||
{
|
|
||||||
float rowTop = stripTop;
|
|
||||||
float textY = rowTop + (pillH - capFont->LegacySize) * 0.5f;
|
|
||||||
|
|
||||||
// Interval buttons (left). Live = in-session buffer; the rest are historical.
|
|
||||||
const struct { const char* lbl; int iv; } kIvs[] = {
|
|
||||||
{ TR("market_iv_live"), 0 }, { "1H", 1 }, { "1D", 2 }, { "1W", 3 }, { "1M", 4 }
|
|
||||||
};
|
|
||||||
float bx = chartMin.x + chartPad;
|
|
||||||
for (int b = 0; b < 5; b++) {
|
|
||||||
float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, kIvs[b].lbl).x;
|
|
||||||
float bw = tw + Layout::spacingSm() * 2.0f;
|
|
||||||
ImVec2 bmn(bx, rowTop), bmx(bx + bw, rowTop + pillH);
|
|
||||||
bool sel = (s_mkt.chartInterval == kIvs[b].iv);
|
|
||||||
bool bhov = material::IsRectHovered(bmn, bmx);
|
|
||||||
ImU32 bg = sel ? WithAlpha(Primary(), 200)
|
|
||||||
: (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18));
|
|
||||||
dl->AddRectFilled(bmn, bmx, bg, 4.0f);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY),
|
|
||||||
sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl);
|
|
||||||
ImGui::SetCursorScreenPos(bmn);
|
|
||||||
ImGui::PushID(9100 + b);
|
|
||||||
if (ImGui::InvisibleButton("##civ", ImVec2(bw, pillH))) {
|
|
||||||
s_mkt.chartInterval = kIvs[b].iv;
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::PopID();
|
|
||||||
bx += bw + Layout::spacingXs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh button (far right).
|
|
||||||
float rEdge = chartMax.x - chartPad;
|
|
||||||
ImFont* iconSmall = material::Typography::instance().iconSmall();
|
|
||||||
ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH);
|
|
||||||
bool refreshHov = material::IsRectHovered(rbMin, rbMax);
|
|
||||||
if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
|
|
||||||
ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH);
|
|
||||||
dl->AddText(iconSmall, iconSmall->LegacySize,
|
|
||||||
ImVec2(rbMin.x + (pillH - icSz.x) * 0.5f, rbMin.y + (pillH - icSz.y) * 0.5f),
|
|
||||||
refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH);
|
|
||||||
ImGui::SetCursorScreenPos(rbMin);
|
|
||||||
if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(pillH, pillH))) {
|
|
||||||
app->refreshMarketData();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price"));
|
|
||||||
|
|
||||||
// 24H volume + market cap, right-aligned to the left of the refresh button. Skipped
|
|
||||||
// when there's no room (narrow window) so they never overlap the interval buttons.
|
|
||||||
float sxr = rbMin.x - Layout::spacingMd();
|
|
||||||
auto drawStat = [&](const char* label, const std::string& val) {
|
|
||||||
char sb[64]; snprintf(sb, sizeof(sb), "%s %s", label, val.c_str());
|
|
||||||
float w = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, sb).x;
|
|
||||||
if (sxr - w < bx + Layout::spacingMd()) return;
|
|
||||||
sxr -= w;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(sxr, textY), OnSurfaceMedium(), sb);
|
|
||||||
sxr -= Layout::spacingMd();
|
|
||||||
};
|
|
||||||
if (market.price_usd > 0) {
|
|
||||||
drawStat(TR("market_cap_short"), FormatCompactUSD(market.market_cap));
|
|
||||||
drawStat(TR("market_vol_short"), FormatCompactUSD(market.volume_24h));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size,
|
|
||||||
S.drawElement("tabs.market", "chart-y-axis-padding").size * hs);
|
|
||||||
// Extra bottom room so the time labels aren't crowded against the card's bottom edge.
|
|
||||||
float labelPadBottom = Layout::spacingXl() + Layout::spacingSm();
|
|
||||||
float plotLeft = chartMin.x + labelPadLeft;
|
|
||||||
float plotRight = chartMax.x - chartPad;
|
|
||||||
float plotTop = stripTop + pillH + Layout::spacingMd(); // gap below the buttons before the plot
|
|
||||||
float plotBottom = chartMax.y - labelPadBottom;
|
|
||||||
float plotW = plotRight - plotLeft;
|
|
||||||
float plotH = plotBottom - plotTop;
|
|
||||||
|
|
||||||
if (s_mkt.history.size() >= 2) {
|
|
||||||
// Compute Y range with padding
|
|
||||||
double yMin = *std::min_element(s_mkt.history.begin(), s_mkt.history.end());
|
|
||||||
double yMax = *std::max_element(s_mkt.history.begin(), s_mkt.history.end());
|
|
||||||
if (yMax <= yMin) { yMax = yMin + 1e-8; }
|
|
||||||
double yRange = yMax - yMin;
|
|
||||||
double yPadding = yRange * 0.12;
|
|
||||||
yMin -= yPadding;
|
|
||||||
yMax += yPadding;
|
|
||||||
|
|
||||||
// Horizontal grid lines (4 lines)
|
|
||||||
for (int g = 0; g <= 4; g++) {
|
|
||||||
float gy = plotTop + plotH * (float)g / 4.0f;
|
|
||||||
dl->AddLine(ImVec2(plotLeft, gy), ImVec2(plotRight, gy),
|
|
||||||
IM_COL32(255, 255, 255, 12), 1.0f);
|
|
||||||
double labelVal = yMax - (yMax - yMin) * (double)g / 4.0;
|
|
||||||
// Adaptive precision (via the shared price formatter) — cleaner than a fixed 6 decimals.
|
|
||||||
snprintf(buf, sizeof(buf), "%s", FormatPrice(labelVal).c_str());
|
|
||||||
ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
||||||
// Keep the axis label inside the card even on narrow windows (min-padding may be
|
|
||||||
// smaller than the label width) so it never spills onto the tab background.
|
|
||||||
float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(lblX, gy - labelSz.y * 0.5f),
|
|
||||||
OnSurfaceDisabled(), buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build points
|
|
||||||
size_t n = s_mkt.history.size();
|
|
||||||
std::vector<ImVec2> points(n);
|
|
||||||
|
|
||||||
// Colored by the DISPLAYED period's direction (chartUp), not just 24h.
|
|
||||||
ImU32 dirCol = chartUp ? Success() : Error();
|
|
||||||
ImU32 lineCol = WithAlpha(dirCol, 220);
|
|
||||||
ImU32 dotCol = dirCol;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < n; i++) {
|
|
||||||
float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f;
|
|
||||||
float x = plotLeft + t * plotW;
|
|
||||||
float y = plotBottom - (float)((s_mkt.history[i] - yMin) / (yMax - yMin)) * plotH;
|
|
||||||
points[i] = ImVec2(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (no per-point dots — a clean curve).
|
|
||||||
dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size);
|
|
||||||
|
|
||||||
// High/low price labels at the displayed range's extremes (no dot markers).
|
|
||||||
if (n >= 3) {
|
|
||||||
size_t hiIdx = (size_t)(std::max_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin());
|
|
||||||
size_t loIdx = (size_t)(std::min_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin());
|
|
||||||
auto markExtreme = [&](size_t idx, bool high) {
|
|
||||||
ImVec2 p = points[idx];
|
|
||||||
std::string lbl = FormatPrice(s_mkt.history[idx]);
|
|
||||||
ImVec2 ls = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, lbl.c_str());
|
|
||||||
float ly = high ? p.y - ls.y - 5.0f * mktDp : p.y + 5.0f * mktDp;
|
|
||||||
float lx = std::min(std::max(plotLeft, p.x - ls.x * 0.5f), plotRight - ls.x);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(lx, ly), OnSurfaceMedium(), lbl.c_str());
|
|
||||||
};
|
|
||||||
markExtreme(hiIdx, true);
|
|
||||||
markExtreme(loIdx, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// X-axis time labels. The series is timestamped, so label each tick with the real
|
|
||||||
// "<time> ago" (rightmost = "now") derived from the sample's timestamp.
|
|
||||||
{
|
|
||||||
int nPts = (int)n;
|
|
||||||
int ticks = std::min(4, nPts);
|
|
||||||
for (int tk = 0; tk < ticks; tk++) {
|
|
||||||
float t = (ticks > 1) ? (float)tk / (float)(ticks - 1) : 1.0f;
|
|
||||||
float xpos = plotLeft + t * plotW;
|
|
||||||
int idx = (int)(t * (nPts - 1) + 0.5f);
|
|
||||||
float secAgo = (idx < (int)chartTimes.size()) ? (float)(nowSec - chartTimes[idx])
|
|
||||||
: (float)(nPts - 1 - idx) * 60.0f;
|
|
||||||
char tlbl[32];
|
|
||||||
if (secAgo < 45.0f) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now"));
|
|
||||||
else if (secAgo < 5400.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fm", secAgo / 60.0f);
|
|
||||||
else if (secAgo < 172800.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fh", secAgo / 3600.0f);
|
|
||||||
else if (secAgo < 1209600.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fd", secAgo / 86400.0f);
|
|
||||||
else if (secAgo < 7776000.0f) snprintf(tlbl, sizeof(tlbl), "~%.0fw", secAgo / 604800.0f);
|
|
||||||
else snprintf(tlbl, sizeof(tlbl), "~%.0fmo", secAgo / 2592000.0f);
|
|
||||||
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, tlbl);
|
|
||||||
float lx = (tk == 0) ? plotLeft
|
|
||||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
|
||||||
: xpos - lblSz.x * 0.5f;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(lx, plotBottom + 4), OnSurfaceDisabled(), tlbl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover crosshair + tooltip
|
|
||||||
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
||||||
if (mousePos.x >= plotLeft && mousePos.x <= plotRight &&
|
|
||||||
mousePos.y >= plotTop && mousePos.y <= plotBottom + labelPadBottom)
|
|
||||||
{
|
|
||||||
float mx = mousePos.x - plotLeft;
|
|
||||||
float closest_t = mx / plotW;
|
|
||||||
int idx = (int)(closest_t * (n - 1) + 0.5f);
|
|
||||||
if (idx < 0) idx = 0;
|
|
||||||
if (idx >= (int)n) idx = (int)n - 1;
|
|
||||||
|
|
||||||
float px = points[idx].x;
|
|
||||||
float py = points[idx].y;
|
|
||||||
|
|
||||||
dl->AddLine(ImVec2(px, plotTop), ImVec2(px, plotBottom),
|
|
||||||
IM_COL32(255, 255, 255, 40), 1.0f);
|
|
||||||
dl->AddLine(ImVec2(plotLeft, py), ImVec2(plotRight, py),
|
|
||||||
IM_COL32(255, 255, 255, 40), 1.0f);
|
|
||||||
|
|
||||||
float hoverDotR = std::max(S.drawElement("tabs.market", "chart-hover-dot-min-radius").size, S.drawElement("tabs.market", "chart-hover-dot-radius").size * hs);
|
|
||||||
float hoverRingR = std::max(S.drawElement("tabs.market", "chart-hover-ring-min-radius").size, S.drawElement("tabs.market", "chart-hover-ring-radius").size * hs);
|
|
||||||
dl->AddCircleFilled(ImVec2(px, py), hoverDotR, dotCol);
|
|
||||||
dl->AddCircle(ImVec2(px, py), hoverRingR, IM_COL32(255, 255, 255, 80), 0, 1.5f);
|
|
||||||
|
|
||||||
// Tooltip: real date/time (from the series timestamps) + price at the hovered point.
|
|
||||||
// Sub-day intervals (Live/1H) show the time; day+ intervals show the date.
|
|
||||||
char whenBuf[32] = "";
|
|
||||||
if (idx < (int)chartTimes.size()) {
|
|
||||||
std::time_t tt = chartTimes[idx];
|
|
||||||
std::tm* tmv = std::localtime(&tt);
|
|
||||||
if (tmv) std::strftime(whenBuf, sizeof(whenBuf),
|
|
||||||
s_mkt.chartInterval <= 1 ? "%b %d %H:%M" : "%b %d, %Y", tmv);
|
|
||||||
}
|
|
||||||
if (whenBuf[0])
|
|
||||||
snprintf(buf, sizeof(buf), "%s \xC2\xB7 %s", whenBuf, FormatPrice(s_mkt.history[idx]).c_str());
|
|
||||||
else
|
|
||||||
snprintf(buf, sizeof(buf), "%s", FormatPrice(s_mkt.history[idx]).c_str());
|
|
||||||
ImVec2 tipSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
||||||
float tipPad = Layout::spacingSm() + Layout::spacingXs();
|
|
||||||
float tipX = px + 10;
|
|
||||||
float tipY = py - tipSz.y - tipPad * 2 - 4;
|
|
||||||
if (tipX + tipSz.x + tipPad * 2 > plotRight)
|
|
||||||
tipX = px - tipSz.x - tipPad * 2 - 10;
|
|
||||||
if (tipY < plotTop) tipY = py + 10;
|
|
||||||
|
|
||||||
ImVec2 tipMin(tipX, tipY);
|
|
||||||
ImVec2 tipMax(tipX + tipSz.x + tipPad * 2, tipY + tipSz.y + tipPad * 2);
|
|
||||||
dl->AddRectFilled(tipMin, tipMax, IM_COL32(20, 20, 30, 230), 4.0f);
|
|
||||||
dl->AddRect(tipMin, tipMax, IM_COL32(255, 255, 255, 30), 4.0f, 0, 1.0f);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(tipX + tipPad, tipY + tipPad), dotCol, buf);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Empty state, centered in the plot area (the interval strip + refresh are already drawn).
|
|
||||||
const char* msg = TR("market_no_history");
|
|
||||||
ImVec2 ts = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, msg);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize,
|
|
||||||
ImVec2(chartMin.x + (availWidth - ts.x) * 0.5f, (plotTop + plotBottom) * 0.5f - ts.y * 0.5f),
|
|
||||||
OnSurfaceDisabled(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(chartMin.x, chartMin.y + chartH));
|
|
||||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// PAIR SELECTOR — every trading pair across all exchanges shown as
|
// PAIR SELECTOR — every trading pair across all exchanges shown as
|
||||||
|
|||||||
Reference in New Issue
Block a user