diff --git a/src/routes/global/+page.server.ts b/src/routes/global/+page.server.ts index 1416c57..e15eed1 100644 --- a/src/routes/global/+page.server.ts +++ b/src/routes/global/+page.server.ts @@ -202,6 +202,26 @@ export const load: PageServerLoad = async () => { // Net growth = truly new arrivals minus departures const netGrowth7d = newUsers7d - churned7d; + // ── Session depth funnel ────────────────────────────────────────────────── + // For each depth d, count players with >= d completions. + // returnRate at depth d = (players with >= d+1) / (players with >= d). + const depthCounts = new Map(); + for (const r of firstDates) { + const n = r.totalCompletions; + for (let d = 1; d <= n; d++) { + depthCounts.set(d, (depthCounts.get(d) ?? 0) + 1); + } + } + const sessionDepthCards = [2, 3, 4, 5, 7].map((d) => { + const atD = depthCounts.get(d) ?? 0; + const atDplus1 = depthCounts.get(d + 1) ?? 0; + return { + depth: d, + players: atD, + returnRate: atD >= 3 ? Math.round((atDplus1 / atD) * 1000) / 10 : null + }; + }); + // ── Return rate ─────────────────────────────────────────────────────────── // "Return rate": % of all-time unique players who have ever played more than once. const playersWithReturn = firstDates.filter((r) => r.totalCompletions >= 2).length; @@ -329,6 +349,7 @@ export const load: PageServerLoad = async () => { return { todayEst, + sessionDepthCards, stats: { todayCount, totalCount, diff --git a/src/routes/global/+page.svelte b/src/routes/global/+page.svelte index 68b21a6..3dc725a 100644 --- a/src/routes/global/+page.svelte +++ b/src/routes/global/+page.svelte @@ -51,6 +51,7 @@ changePct: number | null; }[]; avgWau: number; + sessionDepthCards: { depth: number; players: number; returnRate: number | null }[]; } let { data }: { data: PageData } = $props(); @@ -68,8 +69,17 @@ newPlayerReturnVelocity, wauWeeks, avgWau, + sessionDepthCards, } = $derived(data); + // Collapsible table state + let returnExpanded = $state(false); + let wauExpanded = $state(false); + let completionsExpanded = $state(false); + let streakExpanded = $state(false); + let ret7dExpanded = $state(false); + let ret30dExpanded = $state(false); + function signed(n: number, unit = ""): string { if (n > 0) return `+${n}${unit}`; if (n < 0) return `${n}${unit}`; @@ -259,6 +269,28 @@ +
+

Survival Curve

+

+ Of players who completed N sessions, what % came back for N+1? +

+
+ {#each sessionDepthCards as card (card.depth)} + + After {card.depth} plays + + {card.returnRate != null ? `${card.returnRate}%` : "N/A"} + + {card.players} players + + {/each} +
+
+

New Player Return Rate - {#each newPlayerReturnSeries as row (row.date)} + {#each (returnExpanded ? newPlayerReturnSeries : newPlayerReturnSeries.slice(0, 3)) as row (row.date)} @@ -375,6 +407,14 @@ + {#if newPlayerReturnSeries.length > 3} + + {/if} {:else}

Not enough data yet. @@ -403,7 +443,7 @@ - {#each wauWeeks as row (row.weekEnd)} + {#each (wauExpanded ? wauWeeks : wauWeeks.slice(0, 3)) as row (row.weekEnd)} {@const barPct = Math.round( (row.wau / maxWau) * 100, )} @@ -444,6 +484,14 @@ + {#if wauWeeks.length > 3} + + {/if}

@@ -462,7 +510,7 @@ - {#each last14Days as row (row.date)} + {#each (completionsExpanded ? last14Days : last14Days.slice(0, 3)) as row (row.date)} {@const barPct = Math.round( (row.count / maxCount) * 100, )} @@ -489,6 +537,14 @@ + {#if last14Days.length > 3} + + {/if}
@@ -512,7 +568,7 @@ - {#each streakChart as row (row.days)} + {#each (streakExpanded ? streakChart : streakChart.slice(0, 3)) as row (row.days)} {@const barPct = Math.round( (row.count / maxStreakCount) * 100, )} @@ -539,6 +595,14 @@ + {#if streakChart.length > 3} + + {/if} {/if}
@@ -580,7 +644,7 @@ - {#each retention7dSeries as row (row.date)} + {#each (ret7dExpanded ? retention7dSeries : retention7dSeries.slice(0, 3)) as row (row.date)} @@ -608,6 +672,14 @@ + {#if retention7dSeries.length > 3} + + {/if} {/if} @@ -640,7 +712,7 @@ - {#each retention30dSeries as row (row.date)} + {#each (ret30dExpanded ? retention30dSeries : retention30dSeries.slice(0, 3)) as row (row.date)} @@ -668,6 +740,14 @@ + {#if retention30dSeries.length > 3} + + {/if} {/if}