Files
bibdle/src/lib/components/stats/RetentionLab.svelte
T

83 lines
3.3 KiB
Svelte

<script lang="ts">
import Container from '$lib/components/Container.svelte';
import CohortHeatmap from '$lib/components/charts/CohortHeatmap.svelte';
import MiniLineChart from '$lib/components/charts/MiniLineChart.svelte';
import BarList from '$lib/components/charts/BarList.svelte';
import type { SnapshotPayload, StatsSettings } from '$lib/types/stats';
import { pctOrDash, numOrDash } from '$lib/utils/stats-format';
interface Props {
payload: SnapshotPayload;
settings: StatsSettings;
}
let { payload, settings }: Props = $props();
const mix = $derived(payload.retention.segmentMix);
const colors = ['#60a5fa', '#34d399', '#a78bfa', '#f59e0b', '#fb7185'];
</script>
<section class="flex flex-col gap-3">
<h2 class="text-lg font-bold text-gray-100">3 · Retention Lab</h2>
<div class="grid gap-3 md:grid-cols-2">
<Container class="p-4 w-full items-start text-left">
<p class="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
Overall return rate
</p>
<p class="text-2xl font-bold text-gray-100">
{pctOrDash(payload.kpis.overallReturnRate)}
</p>
<p class="text-xs text-gray-400">
Share of all players who ever played more than once. Target:
{settings.return7_target}%+ within 7 days.
</p>
</Container>
<Container class="p-4 w-full items-start text-left">
<p class="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
Time to second play
</p>
<BarList items={payload.retention.timeToSecondPlay.map((b) => ({ label: b.label, value: b.value }))} />
</Container>
</div>
<Container class="p-4 w-full items-stretch text-left gap-2">
<p class="font-semibold text-gray-100">Cohort retention triangle</p>
<p class="text-xs text-gray-400">
Each row is a starting week; each column is weeks later. Darker = more came back.
Only resolved weeks are filled.
</p>
<CohortHeatmap cohorts={payload.retention.cohorts} weeks={8} />
</Container>
<Container class="p-4 w-full items-stretch text-left gap-2">
<p class="font-semibold text-gray-100">Retention curves</p>
<p class="text-xs text-gray-400">
D1 / D7 / D30 return over time. Latest resolvable cohorts only.
</p>
<MiniLineChart
series={[
{ name: 'D1', color: '#34d399', points: payload.retention.curves.d1 },
{ name: 'D7', color: '#60a5fa', points: payload.retention.curves.d7 },
{ name: 'D30', color: '#a78bfa', points: payload.retention.curves.d30 }
]}
ySuffix="%"
/>
</Container>
<Container class="p-4 w-full items-stretch text-left gap-2">
<p class="font-semibold text-gray-100">Segment mix over time</p>
<p class="text-xs text-gray-400">
Active players split into new, core, returning, at-risk and churned. Total tracked:
{numOrDash(payload.activity.mau)} recently active.
</p>
<MiniLineChart
series={[
{ name: 'New', color: colors[0], points: mix.map((m) => ({ date: m.date, value: m.new })) },
{ name: 'Core', color: colors[1], points: mix.map((m) => ({ date: m.date, value: m.core })) },
{ name: 'Returning', color: colors[2], points: mix.map((m) => ({ date: m.date, value: m.returning })) },
{ name: 'At risk', color: colors[3], points: mix.map((m) => ({ date: m.date, value: m.atRisk })) },
{ name: 'Churned', color: colors[4], points: mix.map((m) => ({ date: m.date, value: m.churned })) }
]}
/>
</Container>
</section>