feat(market): loading spinner while the price chart fetches (pair switches)

Switching exchange pairs (or first load) briefly leaves the chart with no series
while the venue's candles fetch, which showed the bare "No price history
available" empty state. When a chart fetch is in flight — App::isMarketChartLoading()
(the CoinGecko aggregate OR the per-exchange fetch) — draw a spinner + animated
"Loading price history…" in the plot area instead; the "no history" text only
shows when genuinely empty and idle. New i18n key market_chart_loading across all
8 languages. Verified via a forced-state render (spinner + label centered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 09:38:44 -05:00
parent d1ff58c374
commit a107fda9b8
13 changed files with 40 additions and 5 deletions

Binary file not shown.

View File

@@ -534,6 +534,7 @@
"market_btc_price": "BTC PREIS",
"market_cap": "Marktkapitalisierung",
"market_cap_short": "Kap.",
"market_chart_loading": "Preisverlauf wird geladen",
"market_iv_1d": "1T",
"market_iv_1h": "1S",
"market_iv_1m": "1M",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "PRECIO BTC",
"market_cap": "Cap. de Mercado",
"market_cap_short": "Cap.",
"market_chart_loading": "Cargando historial de precios",
"market_iv_1d": "1D",
"market_iv_1h": "1H",
"market_iv_1m": "1M",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "PRIX BTC",
"market_cap": "Capitalisation",
"market_cap_short": "Cap.",
"market_chart_loading": "Chargement de l'historique des prix",
"market_iv_1d": "1J",
"market_iv_1h": "1H",
"market_iv_1m": "1M",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "BTC価格",
"market_cap": "時価総額",
"market_cap_short": "時価総額",
"market_chart_loading": "価格履歴を読み込み中",
"market_iv_1d": "1日",
"market_iv_1h": "1時間",
"market_iv_1m": "1ヶ月",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "BTC 가격",
"market_cap": "시가총액",
"market_cap_short": "시총",
"market_chart_loading": "가격 기록 불러오는 중",
"market_iv_1d": "1일",
"market_iv_1h": "1시간",
"market_iv_1m": "1개월",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "PREÇO BTC",
"market_cap": "Capitalização",
"market_cap_short": "Cap.",
"market_chart_loading": "Carregando histórico de preços",
"market_iv_1d": "1D",
"market_iv_1h": "1H",
"market_iv_1m": "1M",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "ЦЕНА BTC",
"market_cap": "Рыночная капитализация",
"market_cap_short": "Кап.",
"market_chart_loading": "Загрузка истории цен",
"market_iv_1d": "1Д",
"market_iv_1h": "1Ч",
"market_iv_1m": "1М",

View File

@@ -534,6 +534,7 @@
"market_btc_price": "BTC 价格",
"market_cap": "市值",
"market_cap_short": "市值",
"market_chart_loading": "正在加载价格历史",
"market_iv_1d": "1天",
"market_iv_1h": "1时",
"market_iv_1m": "1M",

View File

@@ -2254,6 +2254,12 @@ TRANSLATIONS = {
"ja": "フォルダーをスキャンして他のウォレットを探す",
"ko": "폴더를 스캔하여 다른 지갑 찾기"
},
"market_chart_loading": {
"es": "Cargando historial de precios", "de": "Preisverlauf wird geladen",
"fr": "Chargement de l'historique des prix", "pt": "Carregando histórico de preços",
"ru": "Загрузка истории цен", "zh": "正在加载价格历史",
"ja": "価格履歴を読み込み中", "ko": "가격 기록 불러오는 중"
},
}
def main():

View File

@@ -339,6 +339,9 @@ public:
// Market chart shows the real per-exchange price. Re-fetches on pair change; falls back to the
// CoinGecko aggregate for unmapped venues / failed fetches. Safe to call every frame.
void refreshExchangeChart();
// True while a market price-history fetch is in flight (CoinGecko aggregate OR per-exchange). The
// Market chart shows a loading indicator instead of the empty state during pair switches.
bool isMarketChartLoading() const { return chart_fetch_in_flight_ || exchange_chart_fetch_in_flight_; }
/// @brief Per-category refresh intervals, adjusted by active tab
using RefreshIntervals = services::NetworkRefreshService::Intervals;

View File

@@ -1605,11 +1605,28 @@ static void mktDrawPriceChart(const MktCtx& cx)
}
} 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);
const float dp = Layout::dpiScale();
const float ecx = chartMin.x + availWidth * 0.5f;
const float ecy = (plotTop + plotBottom) * 0.5f;
if (app->isMarketChartLoading()) {
// A fetch is in flight — e.g. just switched exchange pairs. Show a spinner + animated label
// instead of "no history", since the data is on its way.
const float r = 10.0f * dp;
const float t = (float)ImGui::GetTime() * 4.5f; // rotate ~0.7 rev/s
dl->PathArcTo(ImVec2(ecx, ecy - 8.0f * dp), r, t, t + IM_PI * 1.5f, 24);
dl->PathStroke(OnSurfaceMedium(), false, 2.5f * dp);
char lb[80];
snprintf(lb, sizeof(lb), "%s%s", TR("market_chart_loading"), LoadingDots());
ImVec2 ts = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, lb);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(ecx - ts.x * 0.5f, ecy + 10.0f * dp), OnSurfaceMedium(), lb);
} else {
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, ecy - ts.y * 0.5f),
OnSurfaceDisabled(), msg);
}
}
ImGui::SetCursorScreenPos(ImVec2(chartMin.x, chartMin.y + chartH));

View File

@@ -1347,6 +1347,7 @@ void I18n::loadBuiltinEnglish()
strings_["market_attribution"] = "Price data from CoinGecko";
strings_["market_btc_price"] = "BTC PRICE";
strings_["market_no_history"] = "No price history available";
strings_["market_chart_loading"] = "Loading price history";
strings_["market_no_price"] = "No price data";
strings_["market_now"] = "Now";
strings_["market_pct_shielded"] = "%.0f%% Shielded";