diff --git a/bibdle_logo.svg b/bibdle_logo.svg new file mode 100644 index 0000000..9a0dca1 --- /dev/null +++ b/bibdle_logo.svg @@ -0,0 +1,385 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app.html b/src/app.html index a282adb..ced680e 100644 --- a/src/app.html +++ b/src/app.html @@ -4,6 +4,7 @@ + %sveltekit.head% diff --git a/src/lib/assets/bibdle-logo-square.png b/src/lib/assets/bibdle-logo-square.png new file mode 100644 index 0000000..3a4a6d0 Binary files /dev/null and b/src/lib/assets/bibdle-logo-square.png differ diff --git a/src/routes/global/+page.server.ts b/src/routes/global/+page.server.ts index 1100a7d..1416c57 100644 --- a/src/routes/global/+page.server.ts +++ b/src/routes/global/+page.server.ts @@ -285,11 +285,8 @@ export const load: PageServerLoad = async () => { if (!cohort || cohort.size < 3) continue; // skip tiny cohorts let retained = 0; for (const userId of cohort) { - for (let j = 1; j <= windowDays; j++) { - if (dateUsersMap.get(addDays(dateD, j))?.has(userId)) { - retained++; - break; - } + if (dateUsersMap.get(addDays(dateD, windowDays))?.has(userId)) { + retained++; } } series.push({ @@ -304,6 +301,32 @@ export const load: PageServerLoad = async () => { const retention7dSeries = retentionSeries(7, 30); const retention30dSeries = retentionSeries(30, 30); + // ── Weekly Active Users history (12 weeks) ──────────────────────────────── + + const wauWeeks: { weekStart: string; weekEnd: string; wau: number; changePct: number | null }[] = []; + + for (let w = 0; w < 12; w++) { + const weekEnd = estDateStr(w * 7); + const weekStart = estDateStr(w * 7 + 6); + const users = new Set(); + for (const row of recentCompletions) { + if (row.date >= weekStart && row.date <= weekEnd) { + users.add(row.anonymousId); + } + } + wauWeeks.push({ weekEnd, weekStart, wau: users.size, changePct: null }); + } + + // Change % vs prior week (index i+1 is the older week) + for (let i = 0; i < wauWeeks.length - 1; i++) { + const prev = wauWeeks[i + 1].wau; + if (prev > 0) { + wauWeeks[i].changePct = Math.round(((wauWeeks[i].wau - prev) / prev) * 1000) / 10; + } + } + + const avgWau = Math.round(wauWeeks.reduce((sum, w) => sum + w.wau, 0) / wauWeeks.length); + return { todayEst, stats: { @@ -335,6 +358,8 @@ export const load: PageServerLoad = async () => { current7dAvg: current7dReturnAvg, prior7dAvg: prior7dReturnAvg, change: returnRateChange - } + }, + wauWeeks, + avgWau }; }; diff --git a/src/routes/global/+page.svelte b/src/routes/global/+page.svelte index ac21f93..68b21a6 100644 --- a/src/routes/global/+page.svelte +++ b/src/routes/global/+page.svelte @@ -1,350 +1,676 @@ - Global Stats | Bibdle + Global Stats | Bibdle -
-
+
+
+ + ← Back to Game + - - ← Back to Game - +
+

Global Stats

+

+ EST reference date: {todayEst} +

+
-
-

Global Stats

-

EST reference date: {todayEst}

-
+
+ {#each statCards as card (card.label)} + + {card.label} + {card.value} + + {/each} +
-
- {#each statCards as card (card.label)} - - {card.label} - {card.value} - - {/each} -
+
+

+ Traffic & Growth (7-day windows) +

+
+ + Completions Velocity + {signed(growth.completionsVelocity, "/day")} + vs prior 7 days + + + Completions Accel. + {signed(growth.completionsAcceleration, "/day")} + rate of change of velocity + + + User Velocity + {signed(growth.userVelocity)} + unique players, wk/wk + + + User Acceleration + {signed(growth.userAcceleration)} + rate of change of user velocity + + + New Players (7d) + {String(growth.newUsers7d)} + first-time players + + + Churned (7d) + {String(growth.churned7d)} + played wk prior, not this wk + + + Net Growth (7d) + {signed(growth.netGrowth7d)} + new minus churned + +
+
-
-

Traffic & Growth (7-day windows)

-
- - Completions Velocity - {signed(growth.completionsVelocity, '/day')} - vs prior 7 days - - - Completions Accel. - {signed(growth.completionsAcceleration, '/day')} - rate of change of velocity - - - User Velocity - {signed(growth.userVelocity)} - unique players, wk/wk - - - User Acceleration - {signed(growth.userAcceleration)} - rate of change of user velocity - - - New Players (7d) - {String(growth.newUsers7d)} - first-time players - - - Churned (7d) - {String(growth.churned7d)} - played wk prior, not this wk - - - Net Growth (7d) - {signed(growth.netGrowth7d)} - new minus churned - -
-
+
+

+ New Player Return Rate (7-day rolling avg) +

+
+ + Return Rate (7d avg) + + {newPlayerReturnVelocity.current7dAvg != null + ? `${newPlayerReturnVelocity.current7dAvg}%` + : "N/A"} + + new players who came back + + + Return Rate Change + + {newPlayerReturnVelocity.change != null + ? signed(newPlayerReturnVelocity.change, "pp") + : "N/A"} + + vs prior 7 days + + + Prior 7d Avg + + {newPlayerReturnVelocity.prior7dAvg != null + ? `${newPlayerReturnVelocity.prior7dAvg}%` + : "N/A"} + + days 8–14 ago + +
-
-

New Player Return Rate (7-day rolling avg)

-
- - Return Rate (7d avg) - - {newPlayerReturnVelocity.current7dAvg != null ? `${newPlayerReturnVelocity.current7dAvg}%` : 'N/A'} - - new players who came back - - - Return Rate Change - - {newPlayerReturnVelocity.change != null ? signed(newPlayerReturnVelocity.change, 'pp') : 'N/A'} - - vs prior 7 days - - - Prior 7d Avg - - {newPlayerReturnVelocity.prior7dAvg != null ? `${newPlayerReturnVelocity.prior7dAvg}%` : 'N/A'} - - days 8–14 ago - -
+ {#if newPlayerReturnSeries.length > 0} +
+ + + + + + + + + + + + {#each newPlayerReturnSeries as row (row.date)} + + + + + + + + {/each} + +
DateNew PlayersReturn Rate7d Avg
{row.date}{row.cohort}{row.rate != null + ? `${row.rate}%` + : "—"}{row.rollingAvg != null + ? `${row.rollingAvg}%` + : "—"} +
+ {#if row.rollingAvg != null} +
+ {/if} +
+
+
+ {:else} +

+ Not enough data yet. +

+ {/if} +
- {#if newPlayerReturnSeries.length > 0} -
- - - - - - - - - - - - {#each newPlayerReturnSeries as row (row.date)} - - - - - - - - {/each} - -
DateNew PlayersReturn Rate7d Avg
{row.date}{row.cohort}{row.rate != null ? `${row.rate}%` : '—'}{row.rollingAvg != null ? `${row.rollingAvg}%` : '—'} -
- {#if row.rollingAvg != null} -
- {/if} -
-
-
- {:else} -

Not enough data yet.

- {/if} -
+
+

+ Weekly Active Users +

+

+ Unique players per 7-day window. Most recent week first. Avg + WAU: {avgWau} +

+
+ + + + + + + + + + + {#each wauWeeks as row (row.weekEnd)} + {@const barPct = Math.round( + (row.wau / maxWau) * 100, + )} + + + + + + + {/each} + +
WeekActive UsersWk/Wk Change
{row.weekStart} – {row.weekEnd}{row.wau} + {row.changePct != null + ? signed(row.changePct, "%") + : "—"} + +
+
+
+
+
+
-
-

Last 14 Days — Completions

-
- - - - - - - - - - {#each last14Days as row (row.date)} - {@const barPct = Math.round((row.count / maxCount) * 100)} - - - - - - {/each} - -
DateCompletions
{row.date}{row.count} -
-
-
-
-
-
+
+

+ Last 14 Days — Completions +

+
+ + + + + + + + + + {#each last14Days as row (row.date)} + {@const barPct = Math.round( + (row.count / maxCount) * 100, + )} + + + + + + {/each} + +
DateCompletions
{row.date}{row.count} +
+
+
+
+
+
-
-

Active Streak Distribution

- {#if streakChart.length === 0} -

No active streaks yet.

- {:else} -
- - - - - - - - - - {#each streakChart as row (row.days)} - {@const barPct = Math.round((row.count / maxStreakCount) * 100)} - - - - - - {/each} - -
DaysPlayers
{row.days}{row.count} -
-
-
-
-
- {/if} -
+
+

+ Active Streak Distribution +

+ {#if streakChart.length === 0} +

+ No active streaks yet. +

+ {:else} +
+ + + + + + + + + + {#each streakChart as row (row.days)} + {@const barPct = Math.round( + (row.count / maxStreakCount) * 100, + )} + + + + + + {/each} + +
DaysPlayers
{row.days}{row.count} +
+
+
+
+
+ {/if} +
-
-

Retention Over Time

-

% of each day's players who returned within the window. Cohorts with fewer than 3 players are excluded.

+
+

+ Retention Over Time +

+

+ % of each day's players who played again exactly 7 or 30 days later (regardless of activity in between). Cohorts with fewer than 3 players are excluded. +

-
- -
-

7-Day Retention

- {#if retention7dSeries.length === 0} -

Not enough data yet.

- {:else} -
- - - - - - - - - - - {#each retention7dSeries as row (row.date)} - - - - - - - {/each} - -
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% -
-
-
-
-
- {/if} -
+
+ +
+

+ 7-Day Retention +

+ {#if retention7dSeries.length === 0} +

+ Not enough data yet. +

+ {:else} +
+ + + + + + + + + + + {#each retention7dSeries as row (row.date)} + + + + + + + {/each} + +
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% +
+
+
+
+
+ {/if} +
- -
-

30-Day Retention

- {#if retention30dSeries.length === 0} -

Not enough data yet.

- {:else} -
- - - - - - - - - - - {#each retention30dSeries as row (row.date)} - - - - - - - {/each} - -
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% -
-
-
-
-
- {/if} -
-
-
- -
+ +
+

+ 30-Day Retention +

+ {#if retention30dSeries.length === 0} +

+ Not enough data yet. +

+ {:else} +
+ + + + + + + + + + + {#each retention30dSeries as row (row.date)} + + + + + + + {/each} + +
Cohort DatenRet. %
{row.date}{row.cohortSize}{row.rate}% +
+
+
+
+
+ {/if} +
+
+ +
diff --git a/static/bibdle-logo-circle.png b/static/bibdle-logo-circle.png new file mode 100644 index 0000000..37978a1 Binary files /dev/null and b/static/bibdle-logo-circle.png differ diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..dc7a56f Binary files /dev/null and b/static/favicon.png differ