feat(market): line/candle toggle + ~50% taller chart

- Add a chart-style toggle (line vs candlestick) next to the interval buttons,
  shown only when the selected range has per-exchange candles (the aggregate /
  Live view is line-only). It flips s_mkt.chartStyle; candles draw when OHLC
  exists AND the user hasn't switched to the line. Icon reflects the current
  style; tooltip says what a click switches to. Two i18n keys across 8 languages.
- Make the price chart ~50% taller: scale the height floor / desired / viewport
  cap (110->165, x1.5 desired, 0.22->0.33 of available) for a roomier plot.

Verified via a forced-OHLC render: the taller chart, the toggle button, and
candles with a correct week/day x-axis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:54:08 -05:00
parent a767702ff6
commit 76708e9191
12 changed files with 58 additions and 2 deletions

Binary file not shown.

View File

@@ -548,6 +548,8 @@
"market_price_loading": "Preisdaten werden geladen...",
"market_price_unavailable": "Preisdaten nicht verfügbar",
"market_refresh_price": "Preisdaten aktualisieren",
"market_style_candle": "Zu Kerzenchart wechseln",
"market_style_line": "Zum Liniendiagramm wechseln",
"market_trade_on": "Handeln auf %s",
"market_updated": "\\xc2\\xb7 Aktualisiert %s",
"market_vol_short": "Vol.",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "Cargando datos de precio...",
"market_price_unavailable": "Datos de precio no disponibles",
"market_refresh_price": "Actualizar datos de precio",
"market_style_candle": "Cambiar a velas",
"market_style_line": "Cambiar a gráfico de líneas",
"market_trade_on": "Operar en %s",
"market_updated": "\\xc2\\xb7 Actualizado %s",
"market_vol_short": "Vol",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "Chargement des données de prix...",
"market_price_unavailable": "Données de prix indisponibles",
"market_refresh_price": "Actualiser les données de prix",
"market_style_candle": "Passer aux chandeliers",
"market_style_line": "Passer au graphique en ligne",
"market_trade_on": "Échanger sur %s",
"market_updated": "\\xc2\\xb7 Mis à jour %s",
"market_vol_short": "Vol",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "価格データを読み込み中...",
"market_price_unavailable": "価格データが利用できません",
"market_refresh_price": "価格データを更新",
"market_style_candle": "ローソク足に切り替え",
"market_style_line": "折れ線チャートに切り替え",
"market_trade_on": "%s で取引",
"market_updated": "\\xc2\\xb7 更新: %s",
"market_vol_short": "出来高",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "가격 데이터를 불러오는 중...",
"market_price_unavailable": "가격 데이터를 사용할 수 없습니다",
"market_refresh_price": "가격 데이터 새로고침",
"market_style_candle": "캔들차트로 전환",
"market_style_line": "선형 차트로 전환",
"market_trade_on": "%s에서 거래",
"market_updated": "\\xc2\\xb7 업데이트됨 %s",
"market_vol_short": "거래량",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "Carregando dados de preço...",
"market_price_unavailable": "Dados de preço indisponíveis",
"market_refresh_price": "Atualizar dados de preço",
"market_style_candle": "Mudar para velas",
"market_style_line": "Mudar para gráfico de linhas",
"market_trade_on": "Negociar no %s",
"market_updated": "\\xc2\\xb7 Atualizado %s",
"market_vol_short": "Vol",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "Загрузка данных о ценах...",
"market_price_unavailable": "Данные о ценах недоступны",
"market_refresh_price": "Обновить данные о ценах",
"market_style_candle": "Переключить на свечи",
"market_style_line": "Переключить на линейный график",
"market_trade_on": "Торговать на %s",
"market_updated": "\\xc2\\xb7 Обновлено %s",
"market_vol_short": "Объём",

View File

@@ -548,6 +548,8 @@
"market_price_loading": "正在加载价格数据...",
"market_price_unavailable": "价格数据不可用",
"market_refresh_price": "刷新价格数据",
"market_style_candle": "切换到蜡烛图",
"market_style_line": "切换到折线图",
"market_trade_on": "在 %s 交易",
"market_updated": "\\xc2\\xb7 已更新 %s",
"market_vol_short": "成交量",

View File

@@ -2260,6 +2260,18 @@ TRANSLATIONS = {
"ru": "Загрузка истории цен", "zh": "正在加载价格历史",
"ja": "価格履歴を読み込み中", "ko": "가격 기록 불러오는 중"
},
"market_style_line": {
"es": "Cambiar a gráfico de líneas", "de": "Zum Liniendiagramm wechseln",
"fr": "Passer au graphique en ligne", "pt": "Mudar para gráfico de linhas",
"ru": "Переключить на линейный график", "zh": "切换到折线图",
"ja": "折れ線チャートに切り替え", "ko": "선형 차트로 전환"
},
"market_style_candle": {
"es": "Cambiar a velas", "de": "Zu Kerzenchart wechseln",
"fr": "Passer aux chandeliers", "pt": "Mudar para velas",
"ru": "Переключить на свечи", "zh": "切换到蜡烛图",
"ja": "ローソク足に切り替え", "ko": "캔들차트로 전환"
},
}
def main():

View File

@@ -44,6 +44,7 @@ struct MarketViewState {
int pairIdx = 0;
bool stateLoaded = false;
int chartInterval = 4; // main chart range: 0=Live 1=1H 2=1D 3=1W 4=1M (default 1M)
int chartStyle = 1; // 0 = line, 1 = candlestick (only applies when per-exchange OHLC exists)
};
static MarketViewState s_mkt;
@@ -1412,6 +1413,29 @@ static void mktDrawPriceChart(const MktCtx& cx)
bx += bw + Layout::spacingXs();
}
// Line/candle toggle — only when the selected range has per-exchange candles (the aggregate /
// Live view is line-only, so the toggle is hidden there).
if (cx.chartCandles && cx.chartCandles->size() >= 2) {
bx += Layout::spacingSm();
ImFont* icoF = material::Typography::instance().iconSmall();
const bool isCandle = (s_mkt.chartStyle == 1);
const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART;
ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH);
bool thov = material::IsRectHovered(tmn, tmx);
if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); }
ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon);
dl->AddText(icoF, icoF->LegacySize,
ImVec2(tmn.x + (pillH - tiSz.x) * 0.5f, tmn.y + (pillH - tiSz.y) * 0.5f),
thov ? OnSurface() : OnSurfaceMedium(), styleIcon);
ImGui::SetCursorScreenPos(tmn);
if (ImGui::InvisibleButton("##ChartStyle", ImVec2(pillH, pillH)))
s_mkt.chartStyle = isCandle ? 0 : 1;
if (ImGui::IsItemHovered())
material::Tooltip("%s", TR(isCandle ? "market_style_line" : "market_style_candle"));
bx += pillH;
}
// Refresh button (far right).
float rEdge = chartMax.x - chartPad;
ImFont* iconSmall = material::Typography::instance().iconSmall();
@@ -1465,7 +1489,8 @@ static void mktDrawPriceChart(const MktCtx& cx)
float plotH = plotBottom - plotTop;
const auto& candles = *cx.chartCandles;
const bool hasCandles = candles.size() >= 2; // per-exchange OHLC -> candlesticks, else a line
// Candlesticks when per-exchange OHLC exists AND the user hasn't switched to the line view.
const bool hasCandles = candles.size() >= 2 && s_mkt.chartStyle == 1;
if (hasCandles || s_mkt.history.size() >= 2) {
// Compute Y range with padding — candles span low..high, the line spans close..close.
double yMin, yMax;
@@ -1941,7 +1966,8 @@ void RenderMarketTab(App* app)
float mktDp = Layout::dpiScale();
// -- Compact price chart: a modest responsive height, no longer stretched to fill the tab. --
float chartH = std::max(110.0f * vs, std::min(chartElem.height * vs, marketAvail.y * 0.22f));
// ~50% taller than before (floor / desired / viewport-cap all scaled) — a roomier price chart.
float chartH = std::max(165.0f * vs, std::min(chartElem.height * 1.5f * vs, marketAvail.y * 0.33f));
// -- Hero header: size to the ACTUAL content (price row + separator gap + stats row) so the
// chart starts right after "24H VOLUME"/"Market Cap" instead of below a reserved empty band. --

View File

@@ -1348,6 +1348,8 @@ void I18n::loadBuiltinEnglish()
strings_["market_btc_price"] = "BTC PRICE";
strings_["market_no_history"] = "No price history available";
strings_["market_chart_loading"] = "Loading price history";
strings_["market_style_line"] = "Switch to line chart";
strings_["market_style_candle"] = "Switch to candlesticks";
strings_["market_no_price"] = "No price data";
strings_["market_now"] = "Now";
strings_["market_pct_shielded"] = "%.0f%% Shielded";