mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
feat: add WAU history table, fix retention metric, add new logos and favicon
- Add 12-week Weekly Active Users table to global stats with WoW change % - Fix 7-day and 30-day retention to measure return on exactly day N (not any day within the window) - Remove "Avg Guesses Today" stat card - Update retention description to clarify exact-day measurement - Add bibdle logos (SVG, square PNG, circle PNG) and new favicon.png - Wire favicon.png as the site favicon via app.html link tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
385
bibdle_logo.svg
Normal file
385
bibdle_logo.svg
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="680"
|
||||||
|
viewBox="0 0 680 520"
|
||||||
|
version="1.1"
|
||||||
|
id="svg41"
|
||||||
|
sodipodi:docname="bibdle_logo.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview41"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="0.91550428"
|
||||||
|
inkscape:cx="321.68064"
|
||||||
|
inkscape:cy="144.18283"
|
||||||
|
inkscape:window-width="1512"
|
||||||
|
inkscape:window-height="921"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="33"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg41">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid41"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="5"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs32">
|
||||||
|
<linearGradient
|
||||||
|
id="bgSq"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="1">
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
stop-color="rgb(110,154,202)"
|
||||||
|
id="stop1" />
|
||||||
|
<stop
|
||||||
|
offset="3.23%"
|
||||||
|
stop-color="rgb(111,155,203)"
|
||||||
|
id="stop2" />
|
||||||
|
<stop
|
||||||
|
offset="6.45%"
|
||||||
|
stop-color="rgb(112,156,203)"
|
||||||
|
id="stop3" />
|
||||||
|
<stop
|
||||||
|
offset="9.68%"
|
||||||
|
stop-color="rgb(114,158,204)"
|
||||||
|
id="stop4" />
|
||||||
|
<stop
|
||||||
|
offset="12.9%"
|
||||||
|
stop-color="rgb(115,159,205)"
|
||||||
|
id="stop5" />
|
||||||
|
<stop
|
||||||
|
offset="16.13%"
|
||||||
|
stop-color="rgb(116,160,205)"
|
||||||
|
id="stop6" />
|
||||||
|
<stop
|
||||||
|
offset="19.35%"
|
||||||
|
stop-color="rgb(118,162,206)"
|
||||||
|
id="stop7" />
|
||||||
|
<stop
|
||||||
|
offset="22.58%"
|
||||||
|
stop-color="rgb(119,163,207)"
|
||||||
|
id="stop8" />
|
||||||
|
<stop
|
||||||
|
offset="25.81%"
|
||||||
|
stop-color="rgb(121,165,208)"
|
||||||
|
id="stop9" />
|
||||||
|
<stop
|
||||||
|
offset="29.03%"
|
||||||
|
stop-color="rgb(123,167,209)"
|
||||||
|
id="stop10" />
|
||||||
|
<stop
|
||||||
|
offset="32.26%"
|
||||||
|
stop-color="rgb(125,168,210)"
|
||||||
|
id="stop11" />
|
||||||
|
<stop
|
||||||
|
offset="35.48%"
|
||||||
|
stop-color="rgb(127,170,211)"
|
||||||
|
id="stop12" />
|
||||||
|
<stop
|
||||||
|
offset="38.71%"
|
||||||
|
stop-color="rgb(130,172,212)"
|
||||||
|
id="stop13" />
|
||||||
|
<stop
|
||||||
|
offset="41.94%"
|
||||||
|
stop-color="rgb(132,175,213)"
|
||||||
|
id="stop14" />
|
||||||
|
<stop
|
||||||
|
offset="45.16%"
|
||||||
|
stop-color="rgb(135,177,214)"
|
||||||
|
id="stop15" />
|
||||||
|
<stop
|
||||||
|
offset="48.39%"
|
||||||
|
stop-color="rgb(138,180,215)"
|
||||||
|
id="stop16" />
|
||||||
|
<stop
|
||||||
|
offset="51.61%"
|
||||||
|
stop-color="rgb(141,182,216)"
|
||||||
|
id="stop17" />
|
||||||
|
<stop
|
||||||
|
offset="54.84%"
|
||||||
|
stop-color="rgb(145,185,218)"
|
||||||
|
id="stop18" />
|
||||||
|
<stop
|
||||||
|
offset="58.06%"
|
||||||
|
stop-color="rgb(149,188,219)"
|
||||||
|
id="stop19" />
|
||||||
|
<stop
|
||||||
|
offset="61.29%"
|
||||||
|
stop-color="rgb(153,191,220)"
|
||||||
|
id="stop20" />
|
||||||
|
<stop
|
||||||
|
offset="64.52%"
|
||||||
|
stop-color="rgb(158,195,222)"
|
||||||
|
id="stop21" />
|
||||||
|
<stop
|
||||||
|
offset="67.74%"
|
||||||
|
stop-color="rgb(163,198,223)"
|
||||||
|
id="stop22" />
|
||||||
|
<stop
|
||||||
|
offset="70.97%"
|
||||||
|
stop-color="rgb(169,202,224)"
|
||||||
|
id="stop23" />
|
||||||
|
<stop
|
||||||
|
offset="74.19%"
|
||||||
|
stop-color="rgb(174,206,226)"
|
||||||
|
id="stop24" />
|
||||||
|
<stop
|
||||||
|
offset="77.42%"
|
||||||
|
stop-color="rgb(181,209,227)"
|
||||||
|
id="stop25" />
|
||||||
|
<stop
|
||||||
|
offset="80.65%"
|
||||||
|
stop-color="rgb(188,213,228)"
|
||||||
|
id="stop26" />
|
||||||
|
<stop
|
||||||
|
offset="83.87%"
|
||||||
|
stop-color="rgb(195,217,229)"
|
||||||
|
id="stop27" />
|
||||||
|
<stop
|
||||||
|
offset="87.1%"
|
||||||
|
stop-color="rgb(203,221,230)"
|
||||||
|
id="stop28" />
|
||||||
|
<stop
|
||||||
|
offset="90.32%"
|
||||||
|
stop-color="rgb(210,225,230)"
|
||||||
|
id="stop29" />
|
||||||
|
<stop
|
||||||
|
offset="93.55%"
|
||||||
|
stop-color="rgb(218,228,229)"
|
||||||
|
id="stop30" />
|
||||||
|
<stop
|
||||||
|
offset="96.77%"
|
||||||
|
stop-color="rgb(224,230,227)"
|
||||||
|
id="stop31" />
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
stop-color="rgb(227,228,223)"
|
||||||
|
id="stop32" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath
|
||||||
|
id="sqClip">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect32" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="ciClip">
|
||||||
|
<circle
|
||||||
|
cx="510"
|
||||||
|
cy="170"
|
||||||
|
r="130"
|
||||||
|
id="circle32" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="sqClip-9">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect32-8" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath1">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect1" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath2">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect2" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath3">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect3" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="sqClip-9-1">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect32-8-7" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath1-1">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect1-8" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath2-7">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect2-8" />
|
||||||
|
</clipPath>
|
||||||
|
<clipPath
|
||||||
|
id="clipPath3-7">
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
id="rect3-7" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<!-- Rounded square (favicon / app icon) -->
|
||||||
|
<rect
|
||||||
|
x="40"
|
||||||
|
y="40"
|
||||||
|
width="260"
|
||||||
|
height="260"
|
||||||
|
rx="44"
|
||||||
|
fill="url(#bgSq)"
|
||||||
|
id="rect33"
|
||||||
|
inkscape:export-filename="../Coding/bibdle/static/favicon.png"
|
||||||
|
inkscape:export-xdpi="23.63077"
|
||||||
|
inkscape:export-ydpi="23.63077" />
|
||||||
|
<!-- Circle (Discord server icon) -->
|
||||||
|
<circle
|
||||||
|
cx="510"
|
||||||
|
cy="170"
|
||||||
|
r="130"
|
||||||
|
fill="url(#bgSq)"
|
||||||
|
id="circle37"
|
||||||
|
inkscape:export-filename="../Coding/bibdle/static/bibdle-logo-circle.png"
|
||||||
|
inkscape:export-xdpi="378.09232"
|
||||||
|
inkscape:export-ydpi="378.09232" />
|
||||||
|
<rect
|
||||||
|
x="152"
|
||||||
|
y="78"
|
||||||
|
width="36"
|
||||||
|
height="184"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9)"
|
||||||
|
id="rect34-6"
|
||||||
|
transform="matrix(0.89748134,0,0,1,357.18847,2.5366858)" />
|
||||||
|
<rect
|
||||||
|
x="128"
|
||||||
|
y="88"
|
||||||
|
width="84"
|
||||||
|
height="26"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9)"
|
||||||
|
id="rect35-5"
|
||||||
|
transform="matrix(1,0,0,0.80796134,339.90604,30.104693)" />
|
||||||
|
<rect
|
||||||
|
x="96"
|
||||||
|
y="140"
|
||||||
|
width="148"
|
||||||
|
height="30"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9)"
|
||||||
|
id="rect36-7"
|
||||||
|
transform="matrix(1,0,0,0.82878095,339.90604,26.507353)" />
|
||||||
|
<rect
|
||||||
|
x="128"
|
||||||
|
y="210"
|
||||||
|
width="84"
|
||||||
|
height="20"
|
||||||
|
rx="4"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9)"
|
||||||
|
transform="rotate(15,330.33996,1512.3487)"
|
||||||
|
id="rect37-6" />
|
||||||
|
<rect
|
||||||
|
x="152"
|
||||||
|
y="78"
|
||||||
|
width="36"
|
||||||
|
height="184"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9-1)"
|
||||||
|
id="rect34-6-4"
|
||||||
|
transform="matrix(0.89748134,0,0,1,17.264653,0.15299483)" />
|
||||||
|
<rect
|
||||||
|
x="128"
|
||||||
|
y="88"
|
||||||
|
width="84"
|
||||||
|
height="26"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9-1)"
|
||||||
|
id="rect35-5-4"
|
||||||
|
transform="matrix(1,0,0,0.80796134,0.28036275,27.721002)" />
|
||||||
|
<rect
|
||||||
|
x="96"
|
||||||
|
y="140"
|
||||||
|
width="148"
|
||||||
|
height="30"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9-1)"
|
||||||
|
id="rect36-7-9"
|
||||||
|
transform="matrix(1,0,0,0.82878095,-0.01777671,24.123662)" />
|
||||||
|
<rect
|
||||||
|
x="128"
|
||||||
|
y="210"
|
||||||
|
width="84"
|
||||||
|
height="20"
|
||||||
|
rx="4"
|
||||||
|
fill="#ffffff"
|
||||||
|
clip-path="url(#sqClip-9-1)"
|
||||||
|
transform="rotate(15,169.4311,220.168)"
|
||||||
|
id="rect37-6-1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.6 KiB |
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
<script src="https://rybbit.snail.city/api/script.js" data-site-id="9abf0e81d024" defer></script>
|
<script src="https://rybbit.snail.city/api/script.js" data-site-id="9abf0e81d024" defer></script>
|
||||||
|
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
|||||||
BIN
src/lib/assets/bibdle-logo-square.png
Normal file
BIN
src/lib/assets/bibdle-logo-square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
@@ -285,11 +285,8 @@ export const load: PageServerLoad = async () => {
|
|||||||
if (!cohort || cohort.size < 3) continue; // skip tiny cohorts
|
if (!cohort || cohort.size < 3) continue; // skip tiny cohorts
|
||||||
let retained = 0;
|
let retained = 0;
|
||||||
for (const userId of cohort) {
|
for (const userId of cohort) {
|
||||||
for (let j = 1; j <= windowDays; j++) {
|
if (dateUsersMap.get(addDays(dateD, windowDays))?.has(userId)) {
|
||||||
if (dateUsersMap.get(addDays(dateD, j))?.has(userId)) {
|
|
||||||
retained++;
|
retained++;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
series.push({
|
series.push({
|
||||||
@@ -304,6 +301,32 @@ export const load: PageServerLoad = async () => {
|
|||||||
const retention7dSeries = retentionSeries(7, 30);
|
const retention7dSeries = retentionSeries(7, 30);
|
||||||
const retention30dSeries = retentionSeries(30, 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<string>();
|
||||||
|
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 {
|
return {
|
||||||
todayEst,
|
todayEst,
|
||||||
stats: {
|
stats: {
|
||||||
@@ -335,6 +358,8 @@ export const load: PageServerLoad = async () => {
|
|||||||
current7dAvg: current7dReturnAvg,
|
current7dAvg: current7dReturnAvg,
|
||||||
prior7dAvg: prior7dReturnAvg,
|
prior7dAvg: prior7dReturnAvg,
|
||||||
change: returnRateChange
|
change: returnRateChange
|
||||||
}
|
},
|
||||||
|
wauWeeks,
|
||||||
|
avgWau
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Container from '$lib/components/Container.svelte';
|
import Container from "$lib/components/Container.svelte";
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
todayCount: number;
|
todayCount: number;
|
||||||
@@ -27,54 +27,86 @@
|
|||||||
netGrowth7d: number;
|
netGrowth7d: number;
|
||||||
};
|
};
|
||||||
retention7dSeries: { date: string; rate: number; cohortSize: number }[];
|
retention7dSeries: { date: string; rate: number; cohortSize: number }[];
|
||||||
retention30dSeries: { date: string; rate: number; cohortSize: number }[];
|
retention30dSeries: {
|
||||||
|
date: string;
|
||||||
|
rate: number;
|
||||||
|
cohortSize: number;
|
||||||
|
}[];
|
||||||
overallReturnRate: number | null;
|
overallReturnRate: number | null;
|
||||||
newPlayerReturnSeries: { date: string; cohort: number; rate: number | null; rollingAvg: number | null }[];
|
newPlayerReturnSeries: {
|
||||||
|
date: string;
|
||||||
|
cohort: number;
|
||||||
|
rate: number | null;
|
||||||
|
rollingAvg: number | null;
|
||||||
|
}[];
|
||||||
newPlayerReturnVelocity: {
|
newPlayerReturnVelocity: {
|
||||||
current7dAvg: number | null;
|
current7dAvg: number | null;
|
||||||
prior7dAvg: number | null;
|
prior7dAvg: number | null;
|
||||||
change: number | null;
|
change: number | null;
|
||||||
};
|
};
|
||||||
|
wauWeeks: {
|
||||||
|
weekStart: string;
|
||||||
|
weekEnd: string;
|
||||||
|
wau: number;
|
||||||
|
changePct: number | null;
|
||||||
|
}[];
|
||||||
|
avgWau: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
const { stats, last14Days, todayEst, streakChart, growth, retention7dSeries, retention30dSeries, overallReturnRate, newPlayerReturnSeries, newPlayerReturnVelocity } = $derived(data);
|
const {
|
||||||
|
stats,
|
||||||
|
last14Days,
|
||||||
|
todayEst,
|
||||||
|
streakChart,
|
||||||
|
growth,
|
||||||
|
retention7dSeries,
|
||||||
|
retention30dSeries,
|
||||||
|
overallReturnRate,
|
||||||
|
newPlayerReturnSeries,
|
||||||
|
newPlayerReturnVelocity,
|
||||||
|
wauWeeks,
|
||||||
|
avgWau,
|
||||||
|
} = $derived(data);
|
||||||
|
|
||||||
function signed(n: number, unit = ''): string {
|
function signed(n: number, unit = ""): string {
|
||||||
if (n > 0) return `+${n}${unit}`;
|
if (n > 0) return `+${n}${unit}`;
|
||||||
if (n < 0) return `${n}${unit}`;
|
if (n < 0) return `${n}${unit}`;
|
||||||
return `0${unit}`;
|
return `0${unit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trendColor(n: number): string {
|
function trendColor(n: number): string {
|
||||||
if (n > 0) return 'text-green-400';
|
if (n > 0) return "text-green-400";
|
||||||
if (n < 0) return 'text-red-400';
|
if (n < 0) return "text-red-400";
|
||||||
return 'text-gray-400';
|
return "text-gray-400";
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxCount = $derived(Math.max(1, ...last14Days.map((d) => d.count)));
|
const maxCount = $derived(Math.max(1, ...last14Days.map((d) => d.count)));
|
||||||
|
|
||||||
const maxStreakCount = $derived(Math.max(1, ...streakChart.map((r) => r.count)));
|
const maxWau = $derived(Math.max(1, ...wauWeeks.map((w) => w.wau)));
|
||||||
|
|
||||||
|
const maxStreakCount = $derived(
|
||||||
|
Math.max(1, ...streakChart.map((r) => r.count)),
|
||||||
|
);
|
||||||
|
|
||||||
const statCards = $derived([
|
const statCards = $derived([
|
||||||
{ label: 'Completions Today', value: String(stats.todayCount) },
|
{ label: "Completions Today", value: String(stats.todayCount) },
|
||||||
{ label: 'All-Time Completions', value: String(stats.totalCount) },
|
{ label: "All-Time Completions", value: String(stats.totalCount) },
|
||||||
{ label: 'Unique Players', value: String(stats.uniquePlayers) },
|
{ label: "Unique Players", value: String(stats.uniquePlayers) },
|
||||||
{ label: 'Players This Week', value: String(stats.weeklyPlayers) },
|
{ label: "Players This Week", value: String(stats.weeklyPlayers) },
|
||||||
{ label: 'Active Streaks', value: String(stats.activeStreaks) },
|
{ label: "Active Streaks", value: String(stats.activeStreaks) },
|
||||||
|
{ label: "Registered Users", value: String(stats.registeredUsers) },
|
||||||
{
|
{
|
||||||
label: 'Avg Guesses Today',
|
label: "Avg Completions/Player",
|
||||||
value: stats.avgGuessesToday != null ? stats.avgGuessesToday.toFixed(2) : 'N/A',
|
value:
|
||||||
},
|
stats.avgCompletionsPerPlayer != null
|
||||||
{ label: 'Registered Users', value: String(stats.registeredUsers) },
|
? stats.avgCompletionsPerPlayer.toFixed(2)
|
||||||
{
|
: "N/A",
|
||||||
label: 'Avg Completions/Player',
|
|
||||||
value: stats.avgCompletionsPerPlayer != null ? stats.avgCompletionsPerPlayer.toFixed(2) : 'N/A',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Overall Return Rate',
|
label: "Overall Return Rate",
|
||||||
value: overallReturnRate != null ? `${overallReturnRate}%` : 'N/A',
|
value: overallReturnRate != null ? `${overallReturnRate}%` : "N/A",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
@@ -83,91 +115,208 @@
|
|||||||
<title>Global Stats | Bibdle</title>
|
<title>Global Stats | Bibdle</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-linear-to-br from-gray-900 via-slate-900 to-gray-900 text-gray-100">
|
<div
|
||||||
|
class="min-h-screen bg-linear-to-br from-gray-900 via-slate-900 to-gray-900 text-gray-100"
|
||||||
|
>
|
||||||
<div class="max-w-6xl mx-auto px-4 py-8">
|
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||||
|
<a
|
||||||
<a href="/" class="inline-flex items-center gap-1 text-gray-400 hover:text-gray-100 text-sm mb-6 transition-colors">
|
href="/"
|
||||||
|
class="inline-flex items-center gap-1 text-gray-400 hover:text-gray-100 text-sm mb-6 transition-colors"
|
||||||
|
>
|
||||||
← Back to Game
|
← Back to Game
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-100">Global Stats</h1>
|
<h1 class="text-3xl font-bold text-gray-100">Global Stats</h1>
|
||||||
<p class="text-gray-400 text-sm mt-1">EST reference date: {todayEst}</p>
|
<p class="text-gray-400 text-sm mt-1">
|
||||||
|
EST reference date: {todayEst}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-10">
|
<section
|
||||||
|
class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-10"
|
||||||
|
>
|
||||||
{#each statCards as card (card.label)}
|
{#each statCards as card (card.label)}
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">{card.label}</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-gray-100">{card.value}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
|
>{card.label}</span
|
||||||
|
>
|
||||||
|
<span class="text-2xl md:text-3xl font-bold text-gray-100"
|
||||||
|
>{card.value}</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-10">
|
<section class="mb-10">
|
||||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Traffic & Growth <span class="text-xs font-normal text-gray-400">(7-day windows)</span></h2>
|
<h2 class="text-lg font-semibold text-gray-100 mb-4">
|
||||||
|
Traffic & Growth <span
|
||||||
|
class="text-xs font-normal text-gray-400"
|
||||||
|
>(7-day windows)</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Completions Velocity</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.completionsVelocity)}">{signed(growth.completionsVelocity, '/day')}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">vs prior 7 days</span>
|
>Completions Velocity</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.completionsVelocity,
|
||||||
|
)}">{signed(growth.completionsVelocity, "/day")}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>vs prior 7 days</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Completions Accel.</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.completionsAcceleration)}">{signed(growth.completionsAcceleration, '/day')}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">rate of change of velocity</span>
|
>Completions Accel.</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.completionsAcceleration,
|
||||||
|
)}"
|
||||||
|
>{signed(growth.completionsAcceleration, "/day")}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>rate of change of velocity</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">User Velocity</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.userVelocity)}">{signed(growth.userVelocity)}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">unique players, wk/wk</span>
|
>User Velocity</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.userVelocity,
|
||||||
|
)}">{signed(growth.userVelocity)}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>unique players, wk/wk</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">User Acceleration</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.userAcceleration)}">{signed(growth.userAcceleration)}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">rate of change of user velocity</span>
|
>User Acceleration</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.userAcceleration,
|
||||||
|
)}">{signed(growth.userAcceleration)}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>rate of change of user velocity</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">New Players (7d)</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.newUsers7d)}">{String(growth.newUsers7d)}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">first-time players</span>
|
>New Players (7d)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.newUsers7d,
|
||||||
|
)}">{String(growth.newUsers7d)}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>first-time players</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Churned (7d)</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(0)}">{String(growth.churned7d)}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">played wk prior, not this wk</span>
|
>Churned (7d)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
0,
|
||||||
|
)}">{String(growth.churned7d)}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>played wk prior, not this wk</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<Container class="w-full p-5 gap-2">
|
||||||
<span class="text-gray-400 text-xs uppercase tracking-wide text-center">Net Growth (7d)</span>
|
<span
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {trendColor(growth.netGrowth7d)}">{signed(growth.netGrowth7d)}</span>
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
<span class="text-xs text-gray-500 text-center">new minus churned</span>
|
>Net Growth (7d)</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-2xl md:text-3xl font-bold text-center {trendColor(
|
||||||
|
growth.netGrowth7d,
|
||||||
|
)}">{signed(growth.netGrowth7d)}</span
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>new minus churned</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-10">
|
<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>
|
<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">
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4 mb-6">
|
||||||
<Container class="w-full p-5 gap-2">
|
<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
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center text-gray-100">
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
{newPlayerReturnVelocity.current7dAvg != null ? `${newPlayerReturnVelocity.current7dAvg}%` : 'N/A'}
|
>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>
|
||||||
<span class="text-xs text-gray-500 text-center">new players who came back</span>
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>new players who came back</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<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
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center {newPlayerReturnVelocity.change != null ? trendColor(newPlayerReturnVelocity.change) : 'text-gray-400'}">
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
{newPlayerReturnVelocity.change != null ? signed(newPlayerReturnVelocity.change, 'pp') : 'N/A'}
|
>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>
|
||||||
<span class="text-xs text-gray-500 text-center">vs prior 7 days</span>
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>vs prior 7 days</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
<Container class="w-full p-5 gap-2">
|
<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
|
||||||
<span class="text-2xl md:text-3xl font-bold text-center text-gray-100">
|
class="text-gray-400 text-xs uppercase tracking-wide text-center"
|
||||||
{newPlayerReturnVelocity.prior7dAvg != null ? `${newPlayerReturnVelocity.prior7dAvg}%` : 'N/A'}
|
>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>
|
||||||
<span class="text-xs text-gray-500 text-center">days 8–14 ago</span>
|
<span class="text-xs text-gray-500 text-center"
|
||||||
|
>days 8–14 ago</span
|
||||||
|
>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -175,25 +324,49 @@
|
|||||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
<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-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">New Players</th
|
||||||
<th class="text-right px-4 py-3">Return Rate</th>
|
>
|
||||||
|
<th class="text-right px-4 py-3">Return Rate</th
|
||||||
|
>
|
||||||
<th class="text-right px-4 py-3">7d Avg</th>
|
<th class="text-right px-4 py-3">7d Avg</th>
|
||||||
<th class="px-4 py-3 w-32"></th>
|
<th class="px-4 py-3 w-32"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each newPlayerReturnSeries as row (row.date)}
|
{#each newPlayerReturnSeries as row (row.date)}
|
||||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
<tr
|
||||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
class="border-t border-white/5 hover:bg-white/5 transition-colors"
|
||||||
<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-gray-300"
|
||||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.rollingAvg != null ? `${row.rollingAvg}%` : '—'}</td>
|
>{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">
|
<td class="px-4 py-3">
|
||||||
<div class="w-full min-w-20">
|
<div class="w-full min-w-20">
|
||||||
{#if row.rollingAvg != null}
|
{#if row.rollingAvg != null}
|
||||||
<div class="bg-sky-500 h-4 rounded" style="width: {row.rollingAvg}%"></div>
|
<div
|
||||||
|
class="bg-sky-500 h-4 rounded"
|
||||||
|
style="width: {row.rollingAvg}%"
|
||||||
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -203,16 +376,86 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
<p class="text-gray-400 text-sm px-4 py-6">
|
||||||
|
Not enough data yet.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Last 14 Days — Completions</h2>
|
<h2 class="text-lg font-semibold text-gray-100 mb-1">
|
||||||
|
Weekly Active Users
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm mb-4">
|
||||||
|
Unique players per 7-day window. Most recent week first. Avg
|
||||||
|
WAU: <span class="text-gray-100 font-medium">{avgWau}</span>
|
||||||
|
</p>
|
||||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
<tr
|
||||||
|
class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide"
|
||||||
|
>
|
||||||
|
<th class="text-left px-4 py-3">Week</th>
|
||||||
|
<th class="text-right px-4 py-3">Active Users</th>
|
||||||
|
<th class="text-right px-4 py-3">Wk/Wk Change</th>
|
||||||
|
<th class="px-4 py-3 w-48"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each wauWeeks as row (row.weekEnd)}
|
||||||
|
{@const barPct = Math.round(
|
||||||
|
(row.wau / maxWau) * 100,
|
||||||
|
)}
|
||||||
|
<tr
|
||||||
|
class="border-t border-white/5 hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 text-gray-300 text-xs"
|
||||||
|
>{row.weekStart} – {row.weekEnd}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="px-4 py-3 text-right text-gray-100 font-medium"
|
||||||
|
>{row.wau}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="px-4 py-3 text-right text-xs font-medium {row.changePct !=
|
||||||
|
null
|
||||||
|
? row.changePct > 0
|
||||||
|
? 'text-green-400'
|
||||||
|
: row.changePct < 0
|
||||||
|
? 'text-red-400'
|
||||||
|
: 'text-gray-400'
|
||||||
|
: 'text-gray-500'}"
|
||||||
|
>
|
||||||
|
{row.changePct != null
|
||||||
|
? signed(row.changePct, "%")
|
||||||
|
: "—"}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="w-full min-w-24">
|
||||||
|
<div
|
||||||
|
class="bg-indigo-500 h-4 rounded"
|
||||||
|
style="width: {barPct}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mt-8">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-100 mb-4">
|
||||||
|
Last 14 Days — Completions
|
||||||
|
</h2>
|
||||||
|
<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-left px-4 py-3">Date</th>
|
||||||
<th class="text-right px-4 py-3">Completions</th>
|
<th class="text-right px-4 py-3">Completions</th>
|
||||||
<th class="px-4 py-3 w-48"></th>
|
<th class="px-4 py-3 w-48"></th>
|
||||||
@@ -220,13 +463,25 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each last14Days as row (row.date)}
|
{#each last14Days as row (row.date)}
|
||||||
{@const barPct = Math.round((row.count / maxCount) * 100)}
|
{@const barPct = Math.round(
|
||||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
(row.count / maxCount) * 100,
|
||||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
)}
|
||||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.count}</td>
|
<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-100 font-medium"
|
||||||
|
>{row.count}</td
|
||||||
|
>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="w-full min-w-24">
|
<div class="w-full min-w-24">
|
||||||
<div class="bg-amber-500 h-4 rounded" style="width: {barPct}%"></div>
|
<div
|
||||||
|
class="bg-amber-500 h-4 rounded"
|
||||||
|
style="width: {barPct}%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -237,14 +492,20 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<h2 class="text-lg font-semibold text-gray-100 mb-4">Active Streak Distribution</h2>
|
<h2 class="text-lg font-semibold text-gray-100 mb-4">
|
||||||
|
Active Streak Distribution
|
||||||
|
</h2>
|
||||||
{#if streakChart.length === 0}
|
{#if streakChart.length === 0}
|
||||||
<p class="text-gray-400 text-sm px-4 py-6">No active streaks yet.</p>
|
<p class="text-gray-400 text-sm px-4 py-6">
|
||||||
|
No active streaks yet.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
<div class="overflow-x-auto rounded-xl border border-white/10">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
<tr
|
||||||
|
class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide"
|
||||||
|
>
|
||||||
<th class="text-left px-4 py-3">Days</th>
|
<th class="text-left px-4 py-3">Days</th>
|
||||||
<th class="text-right px-4 py-3">Players</th>
|
<th class="text-right px-4 py-3">Players</th>
|
||||||
<th class="px-4 py-3 w-48"></th>
|
<th class="px-4 py-3 w-48"></th>
|
||||||
@@ -252,13 +513,25 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each streakChart as row (row.days)}
|
{#each streakChart as row (row.days)}
|
||||||
{@const barPct = Math.round((row.count / maxStreakCount) * 100)}
|
{@const barPct = Math.round(
|
||||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
(row.count / maxStreakCount) * 100,
|
||||||
<td class="px-4 py-3 text-gray-300">{row.days}</td>
|
)}
|
||||||
<td class="px-4 py-3 text-right text-gray-100 font-medium">{row.count}</td>
|
<tr
|
||||||
|
class="border-t border-white/5 hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<td class="px-4 py-3 text-gray-300"
|
||||||
|
>{row.days}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="px-4 py-3 text-right text-gray-100 font-medium"
|
||||||
|
>{row.count}</td
|
||||||
|
>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="w-full min-w-24">
|
<div class="w-full min-w-24">
|
||||||
<div class="bg-blue-500 h-4 rounded" style="width: {barPct}%"></div>
|
<div
|
||||||
|
class="bg-blue-500 h-4 rounded"
|
||||||
|
style="width: {barPct}%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -270,35 +543,64 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8">
|
<section class="mt-8">
|
||||||
<h2 class="text-lg font-semibold text-gray-100 mb-1">Retention Over Time</h2>
|
<h2 class="text-lg font-semibold text-gray-100 mb-1">
|
||||||
<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>
|
Retention Over Time
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm mb-6">
|
||||||
|
% 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.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<!-- 7-day retention -->
|
<!-- 7-day retention -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-base font-semibold text-gray-200 mb-3">7-Day Retention</h3>
|
<h3 class="text-base font-semibold text-gray-200 mb-3">
|
||||||
|
7-Day Retention
|
||||||
|
</h3>
|
||||||
{#if retention7dSeries.length === 0}
|
{#if retention7dSeries.length === 0}
|
||||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
<p class="text-gray-400 text-sm px-4 py-6">
|
||||||
|
Not enough data yet.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
<div
|
||||||
|
class="overflow-x-auto rounded-xl border border-white/10"
|
||||||
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
<tr
|
||||||
<th class="text-left px-4 py-3">Cohort Date</th>
|
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">n</th>
|
||||||
<th class="text-right px-4 py-3">Ret. %</th>
|
<th class="text-right px-4 py-3"
|
||||||
|
>Ret. %</th
|
||||||
|
>
|
||||||
<th class="px-4 py-3 w-32"></th>
|
<th class="px-4 py-3 w-32"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each retention7dSeries as row (row.date)}
|
{#each retention7dSeries as row (row.date)}
|
||||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
<tr
|
||||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
class="border-t border-white/5 hover:bg-white/5 transition-colors"
|
||||||
<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 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">
|
<td class="px-4 py-3">
|
||||||
<div class="w-full min-w-20">
|
<div class="w-full min-w-20">
|
||||||
<div class="bg-emerald-500 h-4 rounded" style="width: {row.rate}%"></div>
|
<div
|
||||||
|
class="bg-emerald-500 h-4 rounded"
|
||||||
|
style="width: {row.rate}%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -311,29 +613,54 @@
|
|||||||
|
|
||||||
<!-- 30-day retention -->
|
<!-- 30-day retention -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-base font-semibold text-gray-200 mb-3">30-Day Retention</h3>
|
<h3 class="text-base font-semibold text-gray-200 mb-3">
|
||||||
|
30-Day Retention
|
||||||
|
</h3>
|
||||||
{#if retention30dSeries.length === 0}
|
{#if retention30dSeries.length === 0}
|
||||||
<p class="text-gray-400 text-sm px-4 py-6">Not enough data yet.</p>
|
<p class="text-gray-400 text-sm px-4 py-6">
|
||||||
|
Not enough data yet.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-x-auto rounded-xl border border-white/10">
|
<div
|
||||||
|
class="overflow-x-auto rounded-xl border border-white/10"
|
||||||
|
>
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-white/5 text-gray-400 text-xs uppercase tracking-wide">
|
<tr
|
||||||
<th class="text-left px-4 py-3">Cohort Date</th>
|
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">n</th>
|
||||||
<th class="text-right px-4 py-3">Ret. %</th>
|
<th class="text-right px-4 py-3"
|
||||||
|
>Ret. %</th
|
||||||
|
>
|
||||||
<th class="px-4 py-3 w-32"></th>
|
<th class="px-4 py-3 w-32"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each retention30dSeries as row (row.date)}
|
{#each retention30dSeries as row (row.date)}
|
||||||
<tr class="border-t border-white/5 hover:bg-white/5 transition-colors">
|
<tr
|
||||||
<td class="px-4 py-3 text-gray-300">{row.date}</td>
|
class="border-t border-white/5 hover:bg-white/5 transition-colors"
|
||||||
<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 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">
|
<td class="px-4 py-3">
|
||||||
<div class="w-full min-w-20">
|
<div class="w-full min-w-20">
|
||||||
<div class="bg-violet-500 h-4 rounded" style="width: {row.rate}%"></div>
|
<div
|
||||||
|
class="bg-violet-500 h-4 rounded"
|
||||||
|
style="width: {row.rate}%"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -345,6 +672,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
static/bibdle-logo-circle.png
Normal file
BIN
static/bibdle-logo-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Reference in New Issue
Block a user