Convert BTC Treasury Growth chart to stacked multi-wallet area chart

This commit is contained in:
Dione
2026-06-11 08:43:25 +00:00
parent 7a34190956
commit 80a7449688

View File

@ -413,6 +413,7 @@ const wm = new WalletManager();
wm.loadWallets(); wm.loadWallets();
let currentAggregatedSeries = []; let currentAggregatedSeries = [];
let currentWalletSeries = [];
let currentNetHeld = 0; let currentNetHeld = 0;
let currentBuyCost = 0; let currentBuyCost = 0;
let currentBuyAmount = 0; let currentBuyAmount = 0;
@ -1241,9 +1242,7 @@ function getOldestTransactionDate() {
} }
/* Build per-day cumulative BTC series from addressSnapshots. /* Build per-day cumulative BTC series from addressSnapshots.
Forward-fills each wallet's balance for gap days and generates Returns both aggregated total and per-wallet series. */
a data point for every calendar day (like the sats chart) so the
y-axis uses daily steps. */
function buildCumulativeSeries(addresses) { function buildCumulativeSeries(addresses) {
const allDates = {}; const allDates = {};
addresses.forEach(addr => { addresses.forEach(addr => {
@ -1253,7 +1252,7 @@ function buildCumulativeSeries(addresses) {
}); });
}); });
const snapshotDates = Object.keys(allDates).sort(); const snapshotDates = Object.keys(allDates).sort();
if (snapshotDates.length === 0) return []; if (snapshotDates.length === 0) return { total: [], perWallet: [] };
/* Build per-wallet forward-filled balance for every calendar day */ /* Build per-wallet forward-filled balance for every calendar day */
const firstUTC = Date.UTC(new Date(snapshotDates[0]).getUTCFullYear(), new Date(snapshotDates[0]).getUTCMonth(), new Date(snapshotDates[0]).getUTCDate()); const firstUTC = Date.UTC(new Date(snapshotDates[0]).getUTCFullYear(), new Date(snapshotDates[0]).getUTCMonth(), new Date(snapshotDates[0]).getUTCDate());
@ -1271,7 +1270,10 @@ function buildCumulativeSeries(addresses) {
}); });
/* Walk every calendar day, forward-fill each wallet, aggregate */ /* Walk every calendar day, forward-fill each wallet, aggregate */
const result = []; const totalResult = [];
const perWalletResult = {};
addresses.forEach(addr => { perWalletResult[addr] = []; });
for (let t = firstUTC; t <= lastUTC; t += DAY_MS) { for (let t = firstUTC; t <= lastUTC; t += DAY_MS) {
const ds = new Date(t).toISOString().slice(0, 10); const ds = new Date(t).toISOString().slice(0, 10);
let dayTotal = 0; let dayTotal = 0;
@ -1283,11 +1285,12 @@ function buildCumulativeSeries(addresses) {
else break; else break;
} }
dayTotal += balance; dayTotal += balance;
perWalletResult[addr].push([t, balance]);
}); });
result.push([t, dayTotal]); totalResult.push([t, dayTotal]);
} }
return result; return { total: totalResult, perWallet: perWalletResult };
} }
function calculateAggregatedSeries() { function calculateAggregatedSeries() {
@ -1297,10 +1300,13 @@ function calculateAggregatedSeries() {
if (selectedAddr.length === 0) { if (selectedAddr.length === 0) {
currentAggregatedSeries = []; currentAggregatedSeries = [];
currentWalletSeries = [];
return; return;
} }
currentAggregatedSeries = buildCumulativeSeries(selectedAddr); const result = buildCumulativeSeries(selectedAddr);
currentAggregatedSeries = result.total;
currentWalletSeries = result.perWallet;
} }
function calculateCurrentHoldings() { function calculateCurrentHoldings() {
@ -1344,7 +1350,9 @@ function updateDashboard() {
calculateCurrentHoldings(); calculateCurrentHoldings();
if (window.cumulChart) { if (window.cumulChart) {
window.cumulChart.updateSeries([{ data: currentAggregatedSeries }]); window.cumulChart.destroy();
window.cumulChart = null;
refreshAllCharts(currentCutoffMs);
} }
document.getElementById('net-held-val').innerText = currentNetHeld.toFixed(6); document.getElementById('net-held-val').innerText = currentNetHeld.toFixed(6);
@ -1614,8 +1622,67 @@ function updateBtcUI(price, percent) {
} }
function setupCumulCard(cutoff) { function setupCumulCard(cutoff) {
const filteredData = currentAggregatedSeries.filter(d => d[0] >= cutoff); const selectedAddr = getSelectedAddresses();
const options = getBaseChartOptions('instance-cumul', filteredData, orangeBrandColor, false); const wallets = wm.getWallets();
const series = selectedAddr.map(addr => {
const w = wallets.find(x => x.address === addr);
const nickname = w ? (w.nickname || addr.slice(0, 6)) : addr.slice(0, 6);
const ws = currentWalletSeries[addr] || [];
return {
name: nickname,
data: ws.filter(d => d[0] >= cutoff)
};
});
const colors = selectedAddr.map(addr => getColorForWallet(addr));
const gradientStops = colors.map(c => [
{ offset: 0, color: c, opacity: 0.35 },
{ offset: 100, color: c, opacity: 0 }
]);
const filteredTotal = currentAggregatedSeries.filter(d => d[0] >= cutoff);
let yMin = 0, yMax = 0;
if (filteredTotal.length > 0) {
const vals = filteredTotal.map(d => d[1]);
yMin = Math.min(...vals) * 0.95;
yMax = Math.max(...vals) * 1.05;
}
const options = {
chart: { id: 'instance-cumul', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false }, stacked: true },
stackType: 'normal',
series: series,
dataLabels: { enabled: false },
colors: colors,
stroke: { curve: 'smooth', width: 2 },
fill: { type: 'gradient', gradient: { type: 'vertical', shadeIntensity: 0, colorStops: gradientStops } },
grid: { show: false, padding: { left: 0, right: 0, top: 0, bottom: 0 } },
markers: { size: 0, hover: { size: 5 } },
xaxis: { type: 'datetime', labels: { show: true, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, datetimeFormatter: { year: 'yyyy', month: 'MMM yy', day: 'dd MMM' } }, axisBorder: { show: false }, axisTicks: { show: false }, minTickInterval: 86400000 },
yaxis: { opposite: true, min: yMin, max: yMax, labels: { offsetX: -10, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, formatter: (val) => val.toFixed(4) }, axisBorder: { show: false }, axisTicks: { show: false } },
tooltip: { enabled: true, theme: 'dark', shared: true, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) {
const rawData = w.config.series[0].data[dataPointIndex];
if (!rawData) return '';
const date = new Date(rawData[0]);
const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
let html = '<div class="text-center font-medium relative p-1"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter">' + dateString + '</div>';
let total = 0;
w.config.series.forEach((s, i) => {
const pt = s.data[dataPointIndex];
if (pt) {
total += pt[1];
html += '<div class="flex items-center justify-between text-xs mt-1"><div class="flex items-center gap-1.5"><div class="w-2 h-2 rounded-full" style="background:' + colors[i] + ';"></div><span class="text-gray-300">' + s.name + '</span></div><span class="text-white font-bold">' + pt[1].toFixed(6) + '</span></div>';
}
});
html += '<div class="border-t border-gray-600 mt-1 pt-1 flex items-center justify-between text-xs"><span class="text-gray-300">Total</span><span class="text-white font-bold">' + total.toFixed(6) + ' BTC</span></div>';
html += '</div>';
return html;
} },
legend: { show: false },
crosshairs: { show: true, width: 1, position: 'back', stroke: { color: '#4B5563', width: 1, dashArray: 3 } }
};
const chart = new ApexCharts(document.querySelector("#chart-container-cumul"), options); const chart = new ApexCharts(document.querySelector("#chart-container-cumul"), options);
chart.render(); chart.render();
window.cumulChart = chart; window.cumulChart = chart;