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();
let currentAggregatedSeries = [];
let currentWalletSeries = [];
let currentNetHeld = 0;
let currentBuyCost = 0;
let currentBuyAmount = 0;
@ -1241,9 +1242,7 @@ function getOldestTransactionDate() {
}
/* Build per-day cumulative BTC series from addressSnapshots.
Forward-fills each wallet's balance for gap days and generates
a data point for every calendar day (like the sats chart) so the
y-axis uses daily steps. */
Returns both aggregated total and per-wallet series. */
function buildCumulativeSeries(addresses) {
const allDates = {};
addresses.forEach(addr => {
@ -1253,7 +1252,7 @@ function buildCumulativeSeries(addresses) {
});
});
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 */
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 */
const result = [];
const totalResult = [];
const perWalletResult = {};
addresses.forEach(addr => { perWalletResult[addr] = []; });
for (let t = firstUTC; t <= lastUTC; t += DAY_MS) {
const ds = new Date(t).toISOString().slice(0, 10);
let dayTotal = 0;
@ -1283,11 +1285,12 @@ function buildCumulativeSeries(addresses) {
else break;
}
dayTotal += balance;
perWalletResult[addr].push([t, balance]);
});
result.push([t, dayTotal]);
totalResult.push([t, dayTotal]);
}
return result;
return { total: totalResult, perWallet: perWalletResult };
}
function calculateAggregatedSeries() {
@ -1297,10 +1300,13 @@ function calculateAggregatedSeries() {
if (selectedAddr.length === 0) {
currentAggregatedSeries = [];
currentWalletSeries = [];
return;
}
currentAggregatedSeries = buildCumulativeSeries(selectedAddr);
const result = buildCumulativeSeries(selectedAddr);
currentAggregatedSeries = result.total;
currentWalletSeries = result.perWallet;
}
function calculateCurrentHoldings() {
@ -1344,7 +1350,9 @@ function updateDashboard() {
calculateCurrentHoldings();
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);
@ -1614,8 +1622,67 @@ function updateBtcUI(price, percent) {
}
function setupCumulCard(cutoff) {
const filteredData = currentAggregatedSeries.filter(d => d[0] >= cutoff);
const options = getBaseChartOptions('instance-cumul', filteredData, orangeBrandColor, false);
const selectedAddr = getSelectedAddresses();
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);
chart.render();
window.cumulChart = chart;