feat(market): 2-cell min group height; declutter + polish the price chart

- Portfolio group minimum height is now 2 cells (was 3). Added a 2-cell card tier
  that also drops the sparkline (not just the footer) so a ~58px card keeps the
  header + hero value legible.
- Price chart: stop drawing a dot on every point — for a dense historical series
  (>40 points) just the clean line + fill, and always mark the latest point
  (current price) with a dot + ring.
- Y-axis labels use the shared adaptive price formatter (e.g. $0.0143) instead of
  a fixed 6 decimals, so they're cleaner and narrower.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 15:45:10 -05:00
parent 5aa3cc4d51
commit d76c109063
2 changed files with 20 additions and 12 deletions

View File

@@ -97,7 +97,7 @@ public:
int gridCol = -1;
int gridRow = -1;
int gridW = 6;
int gridH = 3; // minimum group height (cells); card design adapts below ~4
int gridH = 3; // default group height (cells); minimum is 2, card design adapts
};
// Theme

View File

@@ -891,7 +891,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 = 3; // minimum card size in cells (~128x96)
const int pfMinW = 4, pfMinH = 2; // minimum card size in cells (~128x64)
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
float pfCellH = pfCellW;
@@ -1168,7 +1168,8 @@ void RenderMarketTab(App* app)
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;
snprintf(buf, sizeof(buf), "$%.6f", labelVal);
// 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.
@@ -1208,11 +1209,14 @@ void RenderMarketTab(App* app)
// Line
dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size);
// Dots
// Dots: only for a sparse series — a dense historical curve reads better as a clean line.
// Always mark the latest point (current price) with a dot + ring.
float dotR = std::max(S.drawElement("tabs.market", "chart-dot-min-radius").size, S.drawElement("tabs.market", "chart-dot-radius").size * hs);
for (size_t i = 0; i < n; i++) {
dl->AddCircleFilled(points[i], dotR, dotCol);
if (n <= 40) {
for (size_t i = 0; i < n; i++) dl->AddCircleFilled(points[i], dotR, dotCol);
}
dl->AddCircleFilled(points[n - 1], dotR + 1.0f * mktDp, dotCol);
dl->AddCircle(points[n - 1], dotR + 3.5f * mktDp, WithAlpha(dotCol, 130), 0, 1.4f * mktDp);
// X-axis time labels. Each point spans one interval unit (minute/hour/day/week/month);
// convert points-before-now to a "~<time> ago" label (rightmost = "now"). Approximate — "~".
@@ -1550,14 +1554,18 @@ void RenderMarketTab(App* app)
dl->AddText(body2, body2->LegacySize, ImVec2(nameX, hy), OnSurfaceMedium(),
fit(e.label, body2, nameMaxW).c_str());
// Short cards (near the 3-cell minimum) drop the redundant DRGX footer and use the
// compact hero font so the value keeps room; taller cards show the full layout.
bool cardTall = (gcMax.y - gcMin.y) > 112.0f * mktDp;
// Card layout adapts to height (cells are ~32px): tall cards get the full stat tile;
// shorter cards drop the redundant DRGX footer; the shortest (2-cell) also drop the
// sparkline so the header + hero value stay legible.
float cardHeight = gcMax.y - gcMin.y;
bool cardTall = cardHeight > 112.0f * mktDp; // footer + large hero
bool showFooter = !footStr.empty() && cardTall;
bool showSpark = e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)
&& cardHeight > 76.0f * mktDp;
// Sparkline strip along the bottom.
float sparkH = 0.0f;
if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) {
if (showSpark) {
std::vector<double> hh = pfSparklineSeries(market, e.sparklineInterval);
if (hh.size() >= 2) {
sparkH = std::max(18.0f * mktDp, (gcMax.y - gcMin.y) * 0.24f);
@@ -1657,8 +1665,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\x973") so the reached grid span is visible — the
// minimum is pfMinW x pfMinH (4x3).
// Live size readout (e.g. "6\xC3\x972") so the reached grid span is visible — the
// minimum is pfMinW x pfMinH (4x2).
{
char szb[16]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh);
ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb);