Skip to content

Commit 542c099

Browse files
author
James O'Claire
committed
Add revenue counts to fastest growing apps and app pages
1 parent 05419b3 commit 542c099

File tree

6 files changed

+235
-66
lines changed

6 files changed

+235
-66
lines changed

backend/dbcon/sql/query_growth_apps.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ SELECT
1111
installs_sum_1w,
1212
ratings_sum_1w,
1313
installs_avg_2w,
14+
installs_acceleration,
15+
has_reliable_baseline,
16+
monthly_active_users,
17+
monthly_iap_revenue,
18+
monthly_ad_revenue,
1419
installs_z_score_2w,
1520
installs_sum_4w,
1621
installs_avg_4w,

backend/dbcon/sql/query_single_app.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ SELECT
77
rating,
88
rating_count,
99
installs,
10+
weekly_active_users,
11+
monthly_active_users,
12+
monthly_ad_revenue,
13+
monthly_iap_revenue,
1014
installs_sum_1w,
1115
installs_sum_4w,
1216
ratings_sum_1w,

frontend/src/lib/FastestGrowingAppsTable.svelte

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import Check from 'lucide-svelte/icons/check';
2222
import X from 'lucide-svelte/icons/x';
2323
24-
import { formatNumber } from '$lib/utils/formatNumber';
24+
import { formatNumber, getRevenueBucket } from '$lib/utils/formatNumber';
2525
import ZScoreMeter from '$lib/components/ZScoreMeter.svelte';
2626
2727
type DataTableProps<CompaniesOverviewEntries, TValue> = {
@@ -91,6 +91,21 @@
9191
accessorKey: 'installs_z_score_4w',
9292
isSortable: true
9393
},
94+
{
95+
title: 'Monthly Active Users',
96+
accessorKey: 'monthly_active_users',
97+
isSortable: true
98+
},
99+
{
100+
title: 'Monthly IAP Revenue',
101+
accessorKey: 'monthly_iap_revenue',
102+
isSortable: true
103+
},
104+
{
105+
title: 'Monthly Ad Revenue',
106+
accessorKey: 'monthly_ad_revenue',
107+
isSortable: true
108+
},
94109
// Monetization indicators
95110
{
96111
title: 'IAP',
@@ -327,14 +342,22 @@
327342
size="sm"
328343
showValue={false}
329344
/>
330-
{:else if ['installs', 'rating_count', 'installs_sum_1w', 'ratings_sum_1w', 'installs_avg_2w', 'installs_sum_4w'].includes(cell.column.id)}
345+
{:else if ['installs', 'rating_count', 'installs_sum_1w', 'ratings_sum_1w', 'installs_avg_2w', 'installs_sum_4w', 'monthly_active_users'].includes(cell.column.id)}
331346
<p class="text-xs md:text-sm">
332347
{#if (cell.getValue() ?? 0) === 0}
333348
-
334349
{:else}
335350
{formatNumber(cell.getValue() as number)}
336351
{/if}
337352
</p>
353+
{:else if ['monthly_iap_revenue', 'monthly_ad_revenue'].includes(cell.column.id)}
354+
<p class="text-xs md:text-sm">
355+
{#if Number(cell.getValue() ?? 0) <= 0}
356+
-
357+
{:else}
358+
{getRevenueBucket(Number(cell.getValue()))}
359+
{/if}
360+
</p>
338361
{:else if ['in_app_purchases', 'ad_supported'].includes(cell.column.id)}
339362
<div class="flex justify-center">
340363
{#if cell.getValue()}

frontend/src/lib/RatingInstallsLarge.svelte

Lines changed: 166 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script lang="ts">
22
import TrendingUpIcon from 'lucide-svelte/icons/trending-up';
33
import TrendingDownIcon from 'lucide-svelte/icons/trending-down';
4+
import UsersIcon from 'lucide-svelte/icons/users';
5+
import DollarSignIcon from 'lucide-svelte/icons/dollar-sign';
46
57
import IconDownload from '$lib/svg/IconDownload.svelte';
68
import Star from './Star.svelte';
7-
import { formatNumber } from '$lib/utils/formatNumber';
9+
import { formatNumber, getRevenueBucket } from '$lib/utils/formatNumber';
810
911
import type { AppFullDetail } from '../types';
1012
interface Props {
@@ -15,82 +17,182 @@
1517
1618
let app_install_estimate_min = $derived(app.rating_count * 50);
1719
let app_install_estimate_max = $derived(app.rating_count * 100);
20+
21+
let monthlyActiveUsers = $derived(Number(app.monthly_active_users) || 0);
22+
let monthlyAdRevenue = $derived(Number(app.monthly_ad_revenue) || 0);
23+
let monthlyIapRevenue = $derived(Number(app.monthly_iap_revenue) || 0);
24+
let monthlyTotalRevenue = $derived(monthlyAdRevenue + monthlyIapRevenue);
25+
26+
let monthlyAdRevenueShare = $derived(
27+
monthlyTotalRevenue > 0 ? Math.round((monthlyAdRevenue / monthlyTotalRevenue) * 100) : 0
28+
);
29+
let monthlyIapRevenueShare = $derived(monthlyTotalRevenue > 0 ? 100 - monthlyAdRevenueShare : 0);
1830
</script>
1931

20-
<div class="inline-block">
21-
<div
22-
class="grid grid-cols-2 md:grid-cols-1 p-0 md:p-2 text-primary-900-100 text-xl gap-0 md:gap-4"
23-
>
24-
<div class="items-center">
25-
{#if app.installs && app.installs != 0}
26-
<div class="flex items-center gap-2">
27-
<IconDownload />
28-
<span class="font-medium">{formatNumber(app.installs)}</span> installs
29-
</div>
30-
{:else if app.rating_count != 0}
31-
<div class="flex items-center">
32-
<IconDownload />
33-
<span class="font-medium">
34-
~{formatNumber(app_install_estimate_min)}
35-
- {formatNumber(app_install_estimate_max)}</span
36-
>
37-
</div>
38-
{:else}
39-
<span class="text-primary-600-400">Installs not yet available</span>
40-
{/if}
32+
<div class="inline-block w-full">
33+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 p-3 md:p-4 text-primary-900-100">
34+
<!-- Primary Metrics -->
35+
<div class="space-y-3">
36+
<!-- Installs -->
37+
<div class="flex items-start sm:items-center gap-2">
38+
{#if app.installs && app.installs != 0}
39+
<div class="text-primary-600-400">
40+
<IconDownload />
41+
</div>
42+
<div class="leading-tight">
43+
<span class="font-semibold text-base md:text-lg">{formatNumber(app.installs)}</span>
44+
<span class="text-primary-600-400 text-sm ml-1">installs</span>
45+
</div>
46+
{:else if app.rating_count != 0}
47+
<div class="text-primary-600-400">
48+
<IconDownload />
49+
</div>
50+
<div class="leading-tight">
51+
<span class="font-semibold text-base md:text-lg">
52+
~{formatNumber(app_install_estimate_min)} - {formatNumber(app_install_estimate_max)}
53+
</span>
54+
<span class="text-primary-600-400 text-sm ml-1">est. installs</span>
55+
</div>
56+
{:else}
57+
<div class="text-primary-600-400">
58+
<IconDownload />
59+
</div>
60+
<span class="text-primary-600-400 text-sm">Installs not yet available</span>
61+
{/if}
62+
</div>
63+
64+
<!-- Ratings -->
65+
<div class="flex items-start sm:items-center gap-2">
66+
{#if app.rating_count != 0}
67+
<Star class="w-5 h-5" />
68+
<div class="leading-tight">
69+
<span class="font-semibold text-base md:text-lg">{formatNumber(app.rating_count)}</span>
70+
<span class="text-primary-600-400 text-sm ml-1">ratings</span>
71+
</div>
72+
{:else}
73+
<Star class="w-5 h-5 text-primary-600-400" />
74+
<span class="text-primary-600-400 text-sm">Ratings not yet available</span>
75+
{/if}
76+
</div>
4177
</div>
4278

43-
{#if app.rating_count != 0}
44-
<div class="flex items-center gap-2">
45-
<Star />
46-
<span class="font-medium"> {formatNumber(app.rating_count)}</span> ratings
79+
<!-- Engagement & Revenue Metrics -->
80+
<div class="space-y-3">
81+
<!-- Monthly Active Users -->
82+
<div class="flex items-start sm:items-center gap-2">
83+
{#if monthlyActiveUsers > 0}
84+
<UsersIcon class="w-5 h-5 text-primary-600-400" />
85+
<div class="leading-tight">
86+
<span class="font-semibold text-base md:text-lg"
87+
>{formatNumber(monthlyActiveUsers)}</span
88+
>
89+
<span class="text-primary-600-400 text-sm ml-1">monthly active users</span>
90+
</div>
91+
{:else}
92+
<UsersIcon class="w-5 h-5 text-primary-600-400" />
93+
<span class="text-primary-600-400 text-sm">MAU not available</span>
94+
{/if}
4795
</div>
48-
{:else}
49-
<span class="text-primary-600-400">Ratings not yet available</span>
50-
{/if}
5196

52-
<!-- installs week trend -->
53-
{#if app.installs_sum_1w > 0}
54-
<div class="flex items-center gap-1 text-primary-800-200">
55-
<span class="text-primary-600-400 text-xs md:text-base">
56-
+{formatNumber(app.installs_sum_1w)}
57-
weekly installs
58-
{#if app.installs_z_score_2w > 1}
59-
<div class="flex items-center gap-1 text-success-900-100">
60-
<TrendingUpIcon />
97+
<!-- Monthly Revenue -->
98+
<div class="flex items-start sm:items-center gap-2">
99+
{#if monthlyTotalRevenue > 0}
100+
<DollarSignIcon class="w-5 h-5 text-success-900-100" />
101+
<div class="flex-1 min-w-0 flex flex-col">
102+
<div>
103+
<span class="font-semibold text-base md:text-lg text-success-900-100"
104+
>{getRevenueBucket(monthlyTotalRevenue)}</span
105+
>
106+
<span class="text-primary-600-400 text-sm ml-1">monthly revenue est.</span>
61107
</div>
62-
{:else if app.installs_z_score_2w < -1}
63-
<div class="flex items-center gap-1 text-danger-900-100">
64-
<TrendingDownIcon />
108+
<!-- Revenue split visualization -->
109+
<div class="flex items-center gap-2 mt-1">
110+
<div class="flex-1 flex h-2 rounded-full overflow-hidden bg-primary-200-800">
111+
<div
112+
class="bg-success-800-200"
113+
style="width: {monthlyIapRevenueShare}%"
114+
title="IAP {monthlyIapRevenueShare}%"
115+
></div>
116+
<div
117+
class="bg-primary-800-200"
118+
style="width: {monthlyAdRevenueShare}%"
119+
title="Ad {monthlyAdRevenueShare}%"
120+
></div>
121+
</div>
65122
</div>
66-
{:else}
67-
<div class="flex items-center gap-0 text-primary-600-400 text-xs">
68-
<span>trend steady</span>
123+
<div class="text-xs ml-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 mt-0.5">
124+
<span class="flex items-center gap-1">
125+
<span class="w-2 h-2 rounded-full bg-success-800-200"></span>
126+
<span class="text-success-800-200">IAP {monthlyIapRevenueShare}%</span>
127+
</span>
128+
<span class="text-primary-600-400">·</span>
129+
<span class="flex items-center gap-1">
130+
<span class="w-2 h-2 rounded-full bg-primary-800-200"></span>
131+
<span class="text-primary-800-200">Ad {monthlyAdRevenueShare}%</span>
132+
</span>
69133
</div>
70-
{/if}
71-
</span>
134+
</div>
135+
{:else}
136+
<DollarSignIcon class="w-5 h-5 text-primary-600-400" />
137+
<span class="text-primary-600-400 text-sm">Revenue not available</span>
138+
{/if}
72139
</div>
73-
{/if}
74-
<!-- installs month trend -->
75-
{#if app.installs_sum_4w > 0}
76-
<div class="flex items-center gap-1 text-primary-800-200">
77-
<span class="text-primary-600-400 text-xs md:text-base">
78-
+{formatNumber(app.installs_sum_4w)}
79-
monthly installs
80-
{#if app.installs_z_score_4w > 1}
81-
<div class="flex items-center gap-1 text-success-900-100">
82-
<TrendingUpIcon />
83-
</div>
84-
{:else if app.installs_z_score_4w < -1}
85-
<div class="flex items-center gap-1 text-danger-900-100">
86-
<TrendingDownIcon />
140+
</div>
141+
142+
<!-- Install Trends -->
143+
{#if app.installs_sum_1w > 0 || app.installs_sum_4w > 0}
144+
<div class="col-span-1 md:col-span-2 border-t border-primary-200-800 pt-3 space-y-2">
145+
<div class="text-xs font-medium text-primary-600-400 uppercase tracking-wide">
146+
Install Trends
147+
</div>
148+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
149+
{#if app.installs_sum_1w > 0}
150+
<div
151+
class="flex items-start sm:items-center justify-between gap-2 bg-primary-50-900/20 rounded px-3 py-2"
152+
>
153+
<div class="flex items-center gap-2 min-w-0">
154+
<span class="text-sm text-primary-600-400">Weekly</span>
155+
<span class="font-medium text-sm">+{formatNumber(app.installs_sum_1w)}</span>
156+
</div>
157+
{#if app.installs_z_score_2w > 1}
158+
<div class="flex items-center gap-1 text-success-600">
159+
<TrendingUpIcon class="w-4 h-4" />
160+
<span class="text-xs font-medium">Trending</span>
161+
</div>
162+
{:else if app.installs_z_score_2w < -1}
163+
<div class="flex items-center gap-1 text-danger-600">
164+
<TrendingDownIcon class="w-4 h-4" />
165+
<span class="text-xs font-medium">Declining</span>
166+
</div>
167+
{:else}
168+
<span class="text-xs text-primary-600-400">Steady</span>
169+
{/if}
87170
</div>
88-
{:else}
89-
<div class="flex items-center gap-1 text-primary-600-400 text-xs">
90-
<span>trend steady</span>
171+
{/if}
172+
{#if app.installs_sum_4w > 0}
173+
<div
174+
class="flex items-start sm:items-center justify-between gap-2 bg-primary-50-900/20 rounded px-3 py-2"
175+
>
176+
<div class="flex items-center gap-2 min-w-0">
177+
<span class="text-sm text-primary-600-400">Monthly</span>
178+
<span class="font-medium text-sm">+{formatNumber(app.installs_sum_4w)}</span>
179+
</div>
180+
{#if app.installs_z_score_4w > 1}
181+
<div class="flex items-center gap-1 text-success-600">
182+
<TrendingUpIcon class="w-4 h-4" />
183+
<span class="text-xs font-medium">Trending</span>
184+
</div>
185+
{:else if app.installs_z_score_4w < -1}
186+
<div class="flex items-center gap-1 text-danger-600">
187+
<TrendingDownIcon class="w-4 h-4" />
188+
<span class="text-xs font-medium">Declining</span>
189+
</div>
190+
{:else}
191+
<span class="text-xs text-primary-600-400">Steady</span>
192+
{/if}
91193
</div>
92194
{/if}
93-
</span>
195+
</div>
94196
</div>
95197
{/if}
96198
</div>

frontend/src/lib/utils/formatNumber.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,35 @@ export function formatNumberLocale(num: number): string {
2525
return '';
2626
}
2727
}
28+
29+
/**
30+
* Formats revenue into nearest bucket with $ prefix
31+
* @param value - The revenue value to bucket
32+
* @returns Formatted string with bucket label (e.g., '$>1M') or empty string
33+
*/
34+
export function getRevenueBucket(value: number): string {
35+
if (value <= 0) return '';
36+
37+
const buckets = [
38+
{ value: 10000, label: '$<10K' },
39+
{ value: 50000, label: '$>50K' },
40+
{ value: 100000, label: '$>100K' },
41+
{ value: 200000, label: '$>200K' },
42+
{ value: 500000, label: '$>500K' },
43+
{ value: 1000000, label: '$>1M' },
44+
{ value: 10000000, label: '$>10M' }
45+
];
46+
47+
let closest = buckets[0];
48+
let minDiff = Math.abs(value - closest.value);
49+
50+
for (const bucket of buckets) {
51+
const diff = Math.abs(value - bucket.value);
52+
if (diff < minDiff) {
53+
closest = bucket;
54+
minDiff = diff;
55+
}
56+
}
57+
58+
return closest.label;
59+
}

frontend/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,9 @@ export interface AppFullDetail {
707707
ratings_sum_1w: number;
708708
installs_z_score_2w: number;
709709
installs_z_score_4w: number;
710+
monthly_active_users: number;
711+
monthly_ad_revenue: number;
712+
monthly_iap_revenue: number;
710713
category: string;
711714
free: string;
712715
price: string;

0 commit comments

Comments
 (0)