diff --git a/src/config/settings.cpp b/src/config/settings.cpp index 4fb7268..1be5efc 100644 --- a/src/config/settings.cpp +++ b/src/config/settings.cpp @@ -332,6 +332,8 @@ bool Settings::load(const std::string& path) entry.show24h = e["show_24h"].get(); if (e.contains("show_sparkline") && e["show_sparkline"].is_boolean()) entry.showSparkline = e["show_sparkline"].get(); + if (e.contains("sparkline_interval") && e["sparkline_interval"].is_number_integer()) + entry.sparklineInterval = e["sparkline_interval"].get(); if (e.contains("grid_col") && e["grid_col"].is_number_integer()) entry.gridCol = e["grid_col"].get(); if (e.contains("grid_row") && e["grid_row"].is_number_integer()) @@ -498,6 +500,7 @@ bool Settings::save(const std::string& path) entry["show_value"] = e.showValue; entry["show_24h"] = e.show24h; entry["show_sparkline"] = e.showSparkline; + entry["sparkline_interval"] = e.sparklineInterval; entry["grid_col"] = e.gridCol; entry["grid_row"] = e.gridRow; entry["grid_w"] = e.gridW; diff --git a/src/config/settings.h b/src/config/settings.h index be21f99..529105b 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -90,11 +90,12 @@ public: bool showValue = true; // show the converted/fiat value on the card bool show24h = false; // show the 24h % change (live-market bases only) bool showSparkline = false; // show a price-trend sparkline (live-market bases only) - // Dashboard grid placement on the Market portfolio. col/row in grid cells (-1 = auto-place), - // w/h in cells (span). Set when the user drags/resizes a card. + int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history) + // Dashboard grid placement on the Market portfolio. col/row in square grid cells + // (-1 = auto-place), w/h in cells (span; minimum 2x1). Set when the user drags/resizes. int gridCol = -1; int gridRow = -1; - int gridW = 1; + int gridW = 2; int gridH = 1; }; diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index a9934ed..9d00a46 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -111,6 +111,23 @@ static bool s_pf_show_drgx = true; static bool s_pf_show_value = true; static bool s_pf_show_24h = false; static bool s_pf_show_sparkline = false; +static int s_pf_spark_interval = 0; // 0=min 1=hour 2=day 3=week 4=month + +// price_history is ~1 sample/minute; resample it to the group's chosen interval by averaging +// each block of K minute-samples into one point. +static std::vector pfResampleHistory(const std::vector& hist, int interval) +{ + static const int kMins[5] = {1, 60, 1440, 10080, 43200}; + int k = kMins[(interval >= 0 && interval < 5) ? interval : 0]; + if (k <= 1) return hist; + std::vector out; + for (size_t i = 0; i < hist.size(); i += (size_t)k) { + double sum = 0.0; size_t cnt = 0; + for (size_t j = i; j < hist.size() && j < i + (size_t)k; ++j) { sum += hist[j]; ++cnt; } + if (cnt) out.push_back(sum / (double)cnt); + } + return out; +} // 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. @@ -208,6 +225,7 @@ static void PortfolioBeginEdit(App* app, int index) s_pf_show_value = src.showValue; s_pf_show_24h = src.show24h; s_pf_show_sparkline = src.showSparkline; + s_pf_spark_interval = src.sparklineInterval; } else { s_pf_label[0] = '\0'; s_pf_addrs.clear(); @@ -220,6 +238,7 @@ static void PortfolioBeginEdit(App* app, int index) s_pf_show_value = true; s_pf_show_24h = false; s_pf_show_sparkline = false; + s_pf_spark_interval = 0; } s_pf_search[0] = '\0'; s_pf_type_filter = 0; @@ -312,13 +331,14 @@ static void RenderPortfolioEditor(App* app) pdl->AddRectFilled(ImVec2(bx, pMin.y + ppad * 0.6f), ImVec2(bx + 3.0f, pMax.y - ppad * 0.6f), accent, 2.0f); } - if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1) && - state.market.price_history.size() >= 2) { - const auto& h = state.market.price_history; - ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80); - ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f); - ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f); - pfDrawSparkline(pdl, spMin, spMax, h, spCol); + if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) { + std::vector h = pfResampleHistory(state.market.price_history, s_pf_spark_interval); + if (h.size() >= 2) { + ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80); + ImVec2 spMin(pMin.x + ppad + (accent ? 6.0f : 0.0f), pMin.y + ph * 0.46f); + ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f); + pfDrawSparkline(pdl, spMin, spMax, h, spCol); + } } float rowY = pMin.y + ppad, nameX = pMin.x + ppad + (accent ? 6.0f : 0.0f); if (!s_pf_icon.empty()) { @@ -458,9 +478,17 @@ static void RenderPortfolioEditor(App* app) ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h); ImGui::EndDisabled(); - // Sparkline on its own line (also live-market only). + // Sparkline on its own line (also live-market only) + its interval. ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline); + ImGui::SameLine(); + ImGui::BeginDisabled(!s_pf_show_sparkline); + const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"), + TR("portfolio_spark_day"), TR("portfolio_spark_week"), + TR("portfolio_spark_month") }; + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::Combo("##pfSparkInterval", &s_pf_spark_interval, spItems, 5); + ImGui::EndDisabled(); ImGui::EndDisabled(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); @@ -662,7 +690,10 @@ static void RenderPortfolioEditor(App* app) ImGui::BeginDisabled(!canSave); if (ImGui::Button(TR("portfolio_save"))) { auto entries = settings->getPortfolioEntries(); + // Start from the existing entry when editing so its grid placement/size (and any + // other fields not on this form) are preserved — editing must not reset the card. config::Settings::PortfolioEntry e; + if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) e = entries[s_pf_editing]; e.label = s_pf_label; e.addresses = s_pf_addrs; e.icon = s_pf_icon; @@ -674,6 +705,7 @@ static void RenderPortfolioEditor(App* app) e.showValue = s_pf_show_value; e.show24h = s_pf_show_24h; e.showSparkline = s_pf_show_sparkline; + e.sparklineInterval = s_pf_spark_interval; if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e; else entries.push_back(e); persist(entries); @@ -767,12 +799,11 @@ void RenderMarketTab(App* app) int pfN = (int)pfEntriesGeo.size(); float pfCardGap = Layout::spacingSm(); float pfCardPad = Layout::spacingMd(); // inner padding of each group card - // Dashboard grid: responsive column count; a 1x1 cell is one card. Cards can be dragged to a - // new cell and corner-resized to span multiple cells. - int pfCols = std::max(1, std::min(4, (int)(availWidth / (240.0f * mktDp) + 0.5f))); + // Dashboard grid of SQUARE cells. Column count is responsive; min 2 columns so a card can meet + // its 2x1 minimum size. Cards drag to a new cell and corner-resize to span multiple cells. + int pfCols = std::max(2, std::min(8, (int)(availWidth / (110.0f * mktDp) + 0.5f))); float pfCellW = (availWidth - (pfCols - 1) * pfCardGap) / (float)pfCols; - // A 1x1 cell: name row (sub1) + a fiat row (caption) + padding; the amounts sit top-right. - float pfCellH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize; + float pfCellH = pfCellW; // square cells; the smallest card is 2x1 // Resolve each group's grid cell: honor a stored placement when it fits, else auto-place // row-major into the first free cells. Deterministic each frame. std::vector pfLayout((size_t)std::max(0, pfN)); @@ -790,7 +821,7 @@ void RenderMarketTab(App* app) std::vector placed((size_t)pfN, 0); for (int i = 0; i < pfN; i++) { const auto& e = pfEntriesGeo[i]; - int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); + int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) { pfLayout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1; } @@ -798,7 +829,7 @@ void RenderMarketTab(App* app) for (int i = 0; i < pfN; i++) { if (placed[i]) continue; const auto& e = pfEntriesGeo[i]; - int w = std::max(1, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); + int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); int fc = 0, fr = 0; bool found = false; for (int r = 0; !found && r < 4096; r++) for (int c = 0; c + w <= pfCols; c++) @@ -1378,12 +1409,13 @@ void RenderMarketTab(App* app) dl->AddRectFilled(ImVec2(bx, gcMin.y + pfCardPad * 0.6f), ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f); } - if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1) && - market.price_history.size() >= 2) { - const auto& hh = market.price_history; - ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80); - pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f), - ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol); + if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { + std::vector hh = pfResampleHistory(market.price_history, e.sparklineInterval); + if (hh.size() >= 2) { + ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80); + pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f), + ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol); + } } float rowY = gcMin.y + pfCardPad, gcRight = gcMax.x - pfCardPad; PfDisplay disp = pfBuildDisplay(e, bal, market); @@ -1484,7 +1516,7 @@ void RenderMarketTab(App* app) if (s_pf_resize >= 0 && s_pf_resize < pfN) { int i = s_pf_resize; ImVec2 mn = cellMin(pfLayout[i]); - int nw = std::max(1, std::min(pfCols - pfLayout[i].col, + int nw = std::max(2, std::min(pfCols - pfLayout[i].col, (int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f))); int nh = std::max(1, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f)); ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 36b9ff7..117c36e 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1135,6 +1135,11 @@ void I18n::loadBuiltinEnglish() strings_["portfolio_show_value"] = "Value"; strings_["portfolio_show_24h"] = "24h"; strings_["portfolio_show_sparkline"] = "Sparkline"; + strings_["portfolio_spark_min"] = "Minute"; + strings_["portfolio_spark_hour"] = "Hour"; + strings_["portfolio_spark_day"] = "Day"; + strings_["portfolio_spark_week"] = "Week"; + strings_["portfolio_spark_month"] = "Month"; strings_["portfolio_appearance"] = "APPEARANCE"; strings_["portfolio_addresses_hdr"] = "ADDRESSES"; strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6";