Convert BTC Treasury Growth chart to stacked multi-wallet area chart
This commit is contained in:
89
index.html
89
index.html
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user