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();
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user