Add double-click to reset chart zoom on all charts
This commit is contained in:
72
index.html
72
index.html
@ -259,8 +259,8 @@ window.WalletManager = WalletManager;
|
||||
<h1 class="text-3xl font-bold tracking-tight text-white"><span id="avg-sats-val">--</span> <span class="text-sm text-gray-500">sats</span></h1>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-lg font-bold" style="color: #FF7A00;">30D AVG</span>
|
||||
<p class="text-xs text-gray-500 font-medium mt-0.5 uppercase tracking-tighter">Daily Acquisition</p>
|
||||
<span class="text-lg font-bold" style="color: #FF7A00;">RUNNING AVG</span>
|
||||
<p class="text-xs text-gray-500 font-medium mt-0.5 uppercase tracking-tighter">Sats per Day</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-52 mt-4" id="chart-container-sats"></div>
|
||||
@ -1602,38 +1602,36 @@ function setupSatsCard(cutoff) {
|
||||
dailyDeltas[d.dateStr] = d.btcVal - prevBalance;
|
||||
});
|
||||
|
||||
/* Fill continuous daily timeline — iterate every day from first to last */
|
||||
const firstUTC = Date.UTC(new Date(sortedDays[0].dateStr).getUTCFullYear(), new Date(sortedDays[0].dateStr).getUTCMonth(), new Date(sortedDays[0].dateStr).getUTCDate());
|
||||
const lastUTC = Date.UTC(new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCFullYear(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCMonth(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCDate());
|
||||
const continuousDays = [];
|
||||
for (let t = firstUTC; t <= lastUTC; t += DAY_MS) {
|
||||
const ds = new Date(t).toISOString().slice(0, 10);
|
||||
continuousDays.push({ ts: t, dateStr: ds, delta: dailyDeltas[ds] || 0 });
|
||||
}
|
||||
/* Compute total sats acquired and days elapsed since first snapshot */
|
||||
const firstUTC = sortedDays.length > 0 ? Date.UTC(new Date(sortedDays[0].dateStr).getUTCFullYear(), new Date(sortedDays[0].dateStr).getUTCMonth(), new Date(sortedDays[0].dateStr).getUTCDate()) : Date.now();
|
||||
const lastUTC = sortedDays.length > 0 ? Date.UTC(new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCFullYear(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCMonth(), new Date(sortedDays[sortedDays.length - 1].dateStr).getUTCDate()) : Date.now();
|
||||
const daysElapsed = Math.max(1, Math.ceil((lastUTC - firstUTC) / DAY_MS));
|
||||
const totalBtc = sortedDays[sortedDays.length - 1]?.btcVal || 0;
|
||||
const totalSats = Math.round(totalBtc * 1e8);
|
||||
const avgSatsPerDay = Math.round(totalSats / daysElapsed);
|
||||
|
||||
/* Exponentially weighted moving average — older days decay so average drops when there are no acquisitions */
|
||||
const filtered = continuousDays.filter(d => d.ts >= cutoff);
|
||||
const halfLife = 7;
|
||||
const decay = Math.pow(0.5, 1 / halfLife);
|
||||
let ewmaVal = filtered[0]?.delta || 0;
|
||||
const emaSats = [];
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
ewmaVal = filtered[i].delta + decay * ewmaVal;
|
||||
emaSats.push([filtered[i].ts, Math.round(ewmaVal * 1e8)]);
|
||||
}
|
||||
|
||||
const latestAvg = emaSats.length > 0 ? emaSats[emaSats.length - 1][1] : 0;
|
||||
document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(Math.round(latestAvg));
|
||||
const options = {
|
||||
chart: { id: 'instance-sats', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false } },
|
||||
series: [{ name: 'Avg Sats/Day', data: emaSats }],
|
||||
/* Build continuous daily series: one data point per day */
|
||||
const filteredSats = [];
|
||||
let runningTotalBtc = 0;
|
||||
for (let t = firstUTC; t <= lastUTC; t += DAY_MS) {
|
||||
const ds = new Date(t).toISOString().slice(0, 10);
|
||||
runningTotalBtc += dailyDeltas[ds] || 0;
|
||||
const daysSoFar = Math.max(1, Math.ceil((t - firstUTC) / DAY_MS) + 1);
|
||||
const runningAvgSats = Math.round((runningTotalBtc * 1e8) / daysSoFar);
|
||||
filteredSats.push([t, runningAvgSats]);
|
||||
}
|
||||
const latestAvg = filteredSats.length > 0 ? filteredSats[filteredSats.length - 1][1] : 0;
|
||||
document.getElementById('avg-sats-val').innerText = new Intl.NumberFormat('en-US').format(latestAvg);
|
||||
const options = {
|
||||
chart: { id: 'instance-sats', type: 'area', height: 200, background: 'transparent', toolbar: { show: false }, sparkline: { enabled: false }, zoom: { enabled: true, type: 'x', autoScaleYaxis: true }, animations: { enabled: false } },
|
||||
series: [{ name: 'Avg Sats/Day', data: filteredSats }],
|
||||
dataLabels: { enabled: false },
|
||||
colors: [orangeBrandColor],
|
||||
stroke: { curve: 'smooth', width: 2 },
|
||||
fill: { type: 'gradient', gradient: { type: 'vertical', shadeIntensity: 0, colorStops: [{ offset: 0, color: orangeBrandColor, opacity: 0.16 }, { offset: 100, color: orangeBrandColor, opacity: 0 }] } },
|
||||
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 } },
|
||||
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: '1day' },
|
||||
yaxis: { opposite: true, labels: { offsetX: -10, style: { colors: '#4B5563', fontSize: '10px', fontWeight: 600 }, formatter: (val) => { if (val >= 1e6) return (val / 1e6).toFixed(0) + 'M'; if (val >= 1e3) return (val / 1e3).toFixed(0) + 'k'; return val; } }, axisBorder: { show: false }, axisTicks: { show: false } },
|
||||
tooltip: { enabled: true, theme: 'dark', shared: false, intersect: false, custom: function({ series, seriesIndex, dataPointIndex, w }) { const rawData = w.config.series[seriesIndex].data[dataPointIndex]; if (!rawData) return ''; const date = new Date(rawData[0]); const dateString = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const sats = rawData[1]; const btcVal = (sats / 1e8).toFixed(6); const priceStr = todayBtcPrice ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(todayBtcPrice) : '—'; const dollarDay = todayBtcPrice ? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format((sats / 1e8) * todayBtcPrice) : '—'; return '<div class="rounded-lg py-2 px-4 text-left"><div class="text-gray-400 text-[10px] font-semibold uppercase tracking-tighter mb-3">' + dateString + '</div><div class="text-orange-400 text-xs font-bold">₿ ' + btcVal + '/day</div><div class="text-white text-sm font-medium mt-1">' + dollarDay + '</div></div>'; } },
|
||||
crosshairs: { show: true, width: 1, position: 'back', stroke: { color: '#4B5563', width: 1, dashArray: 3 } }
|
||||
@ -1705,8 +1703,24 @@ function startTickUpdates(data) {
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Global Event Listeners
|
||||
=================================================================== */
|
||||
Double-click to reset zoom
|
||||
=================================================================== */
|
||||
function resetChartZoom(chartInstance) {
|
||||
if (chartInstance) {
|
||||
chartInstance.updateOptions({
|
||||
xaxis: { min: undefined, max: undefined }
|
||||
}, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('chart-container-btc').addEventListener('dblclick', () => resetChartZoom(window.btcChart));
|
||||
document.getElementById('chart-container-cumul').addEventListener('dblclick', () => resetChartZoom(window.cumulChart));
|
||||
document.getElementById('chart-container-sats').addEventListener('dblclick', () => resetChartZoom(window.satsChart));
|
||||
document.getElementById('chart-container-breakdown').addEventListener('dblclick', () => resetChartZoom(window.breakdownChart));
|
||||
|
||||
/* ===================================================================
|
||||
Global Event Listeners
|
||||
=================================================================== */
|
||||
document.querySelectorAll('.filter-checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', () => updateDashboard());
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user