mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-06 01:43:32 -04:00
feat: add return rate and retention metrics to global stats
- Overall return rate: % of all-time players who played more than once - New player return rate: 7-day rolling avg of daily first-timer return rates, with velocity vs prior 7 days - 7-day and 30-day retention over time: per-cohort-day retention series Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,11 +26,20 @@
|
||||
churned7d: number;
|
||||
netGrowth7d: number;
|
||||
};
|
||||
retention7dSeries: { date: string; rate: number; cohortSize: number }[];
|
||||
retention30dSeries: { date: string; rate: number; cohortSize: number }[];
|
||||
overallReturnRate: number | null;
|
||||
newPlayerReturnSeries: { date: string; cohort: number; rate: number | null; rollingAvg: number | null }[];
|
||||
newPlayerReturnVelocity: {
|
||||
current7dAvg: number | null;
|
||||
prior7dAvg: number | null;
|
||||
change: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const { stats, last14Days, todayEst, streakChart, growth } = $derived(data);
|
||||
const { stats, last14Days, todayEst, streakChart, growth, retention7dSeries, retention30dSeries, overallReturnRate, newPlayerReturnSeries, newPlayerReturnVelocity } = $derived(data);
|
||||
|
||||
function signed(n: number, unit = ''): string {
|
||||
if (n > 0) return `+${n}${unit}`;
|
||||
@@ -63,6 +72,10 @@
|
||||
label: 'Avg Completions/Player',
|
||||
value: stats.avgCompletionsPerPlayer != null ? stats.avgCompletionsPerPlayer.toFixed(2) : 'N/A',
|
||||
},
|
||||
{
|
||||
label: 'Overall Return Rate',
|
||||
value: overallReturnRate != null ? `${overallReturnRate}%` : 'N/A',
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
@@ -132,6 +145,68 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">New Player Return Rate <span class="text-xs font-normal text-gray-400">(7-day rolling avg)</span></h2>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 mb-6">
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Return Rate (7d avg)</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center text-gray-100">
|
||||
{newPlayerReturnVelocity.current7dAvg != null ? `${newPlayerReturnVelocity.current7dAvg}%` : 'N/A'}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 text-center">new players who came back</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Return Rate Change</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center {newPlayerReturnVelocity.change != null ? trendColor(newPlayerReturnVelocity.change) : 'text-gray-400'}">
|
||||
{newPlayerReturnVelocity.change != null ? signed(newPlayerReturnVelocity.change, 'pp') : 'N/A'}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 text-center">vs prior 7 days</span>
|
||||
</Container>
|
||||
<Container class="w-full p-5 gap-2">
|
||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Prior 7d Avg</span>
|
||||
<span class="text-2xl md:text-3xl font-bold text-center text-gray-100">
|
||||
{newPlayerReturnVelocity.prior7dAvg != null ? `${newPlayerReturnVelocity.prior7dAvg}%` : 'N/A'}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 text-center">days 8–14 ago</span>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
{#if newPlayerReturnSeries.length > 0}
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
||||
<th class="text-left px-4 py-3">Date</th>
|
||||
<th class="text-right px-4 py-3">New Players</th>
|
||||
<th class="text-right px-4 py-3">Day Rate</th>
|
||||
<th class="text-right px-4 py-3">7d Avg</th>
|
||||
<th class="px-4 py-3 w-32"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each newPlayerReturnSeries as row (row.date)}
|
||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400 text-xs">{row.cohort}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400">{row.rate != null ? `${row.rate}%` : '—'}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.rollingAvg != null ? `${row.rollingAvg}%` : '—'}</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="w-full min-w-20">
|
||||
{#if row.rollingAvg != null}
|
||||
<div class="bg-sky-500 h-4 rounded" style="width: {row.rollingAvg}%"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Last 14 Days</h2>
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
@@ -194,5 +269,82 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-100 mb-1">Retention Over Time</h2>
|
||||
<p class="text-gray-400 text-sm mb-6">% of each day's players who returned within the window. Cohorts with fewer than 3 players are excluded.</p>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 7-day retention -->
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-3">7-Day Retention</h3>
|
||||
{#if retention7dSeries.length === 0}
|
||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
||||
<th class="text-left px-4 py-3">Cohort Date</th>
|
||||
<th class="text-right px-4 py-3">n</th>
|
||||
<th class="text-right px-4 py-3">Ret. %</th>
|
||||
<th class="px-4 py-3 w-32"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each retention7dSeries as row (row.date)}
|
||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400 text-xs">{row.cohortSize}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.rate}%</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="w-full min-w-20">
|
||||
<div class="bg-emerald-500 h-4 rounded" style="width: {row.rate}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 30-day retention -->
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-200 mb-3">30-Day Retention</h3>
|
||||
{#if retention30dSeries.length === 0}
|
||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
||||
<th class="text-left px-4 py-3">Cohort Date</th>
|
||||
<th class="text-right px-4 py-3">n</th>
|
||||
<th class="text-right px-4 py-3">Ret. %</th>
|
||||
<th class="px-4 py-3 w-32"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each retention30dSeries as row (row.date)}
|
||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-400 text-xs">{row.cohortSize}</td>
|
||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.rate}%</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="w-full min-w-20">
|
||||
<div class="bg-violet-500 h-4 rounded" style="width: {row.rate}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user