Skip to content

Commit c3739af

Browse files
committed
orb charts
1 parent 3e0c1d3 commit c3739af

File tree

8 files changed

+647
-410
lines changed

8 files changed

+647
-410
lines changed

apps/web/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,26 @@
1414
"@radix-ui/react-tooltip": "^1.1.6",
1515
"class-variance-authority": "^0.7.1",
1616
"clsx": "^2.1.1",
17-
"lucide-react": "^0.468.0",
18-
"next": "15.1.1",
17+
"lucide-react": "^0.469.0",
18+
"next": "^15.1.3",
1919
"nuqs": "^2.2.3",
2020
"react": "^19.0.0",
2121
"react-dom": "^19.0.0",
2222
"react-markdown": "^9.0.1",
2323
"recharts": "^2.15.0",
24-
"tailwind-merge": "^2.5.5",
24+
"tailwind-merge": "^2.6.0",
2525
"tailwindcss-animate": "^1.0.7"
2626
},
2727
"devDependencies": {
28-
"@eslint/eslintrc": "^3",
28+
"@eslint/eslintrc": "^3.2.0",
2929
"@tailwindcss/typography": "^0.5.15",
30-
"@types/node": "^20",
31-
"@types/react": "^19",
32-
"@types/react-dom": "^19",
33-
"eslint": "^9",
34-
"eslint-config-next": "15.1.1",
35-
"postcss": "^8",
36-
"tailwindcss": "^3.4.1",
37-
"typescript": "^5"
30+
"@types/node": "^22.10.2",
31+
"@types/react": "^19.0.2",
32+
"@types/react-dom": "^19.0.2",
33+
"eslint": "^9.17.0",
34+
"eslint-config-next": "^15.1.3",
35+
"postcss": "^8.4.49",
36+
"tailwindcss": "^3.4.17",
37+
"typescript": "^5.7.2"
3838
}
3939
}

apps/web/pnpm-lock.yaml

Lines changed: 382 additions & 380 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/src/components/tools/orb/dashboard.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import { useQueryState } from 'nuqs'
44
import { useEffect, useState } from 'react'
55
import { pipe } from '@/lib/tinybird'
66
import MetricCard from '../auth0/metric'
7-
import { SubsChart } from './subs-chart'
8-
9-
interface SubsDataPoint {
10-
day: string
11-
invoices: number
12-
}
7+
import { SubsChart, SubsDataPoint } from './subs-chart'
8+
import { SubsByPlanChart, SubsByPlanDataPoint } from './sub-by-plan-chart'
9+
import { SubsByPlanTsChart, SubsByPlanTsDataPoint } from './sub-by-plan-ts-chart'
1310

1411
export default function OrbDashboard() {
1512
const [token] = useQueryState('token')
@@ -18,6 +15,8 @@ export default function OrbDashboard() {
1815
const [issuedInvoices, setIssuedInvoices] = useState<number>(0)
1916
const [paidInvoices, setPaidInvoices] = useState<number>(0)
2017
const [subsTimeSeriesData, setSubsTimeSeriesData] = useState<SubsDataPoint[]>([])
18+
const [subsByPlanData, setSubsByPlanData] = useState<SubsByPlanDataPoint[]>([])
19+
const [subsByPlanTsData, setSubsByPlanTsData] = useState<SubsByPlanTsDataPoint[]>([])
2120

2221
useEffect(() => {
2322
async function fetchMetrics() {
@@ -29,20 +28,26 @@ export default function OrbDashboard() {
2928
churnedSubsResult,
3029
issuedInvoicesResult,
3130
paidInvoicesResult,
32-
subsTimeSeriesResult
31+
subsTimeSeriesResult,
32+
subsByPlanResult,
33+
subsByPlanTsResult
3334
] = await Promise.all([
3435
pipe(token, 'orb_new_subs'),
3536
pipe(token, 'orb_churn_subs'),
3637
pipe(token, 'orb_invoices_issued'),
3738
pipe(token, 'orb_invoices_paid'),
38-
pipe<{ data: SubsDataPoint[] }>(token, 'orb_subs_ts')
39+
pipe<{ data: SubsDataPoint[] }>(token, 'orb_subs_ts'),
40+
pipe<{ data: SubsByPlanDataPoint[] }>(token, 'orb_subs_created_by_plan'),
41+
pipe<{ data: SubsByPlanTsDataPoint[] }>(token, 'orb_daily_subs_created_by_plan')
3942
])
4043

4144
setNewSubs(newSubsResult.data[0]?.subs || 0)
4245
setChurnedSubs(churnedSubsResult.data[0]?.subs || 0)
4346
setIssuedInvoices(issuedInvoicesResult.data[0]?.invoices || 0)
4447
setPaidInvoices(paidInvoicesResult.data[0]?.invoices || 0)
4548
setSubsTimeSeriesData(subsTimeSeriesResult.data)
49+
setSubsByPlanData(subsByPlanResult.data)
50+
setSubsByPlanTsData(subsByPlanTsResult.data)
4651
} catch (error) {
4752
console.error('Failed to fetch metrics:', error)
4853
}
@@ -51,11 +56,11 @@ export default function OrbDashboard() {
5156
fetchMetrics()
5257
}, [token])
5358

54-
const churnRate = newSubs > 0
55-
? Math.round((churnedSubs / newSubs) * 100)
59+
const churnRate = newSubs > 0
60+
? Math.round((churnedSubs / newSubs) * 100)
5661
: 0
5762

58-
const invoicePaymentRate = issuedInvoices > 0
63+
const invoicePaymentRate = issuedInvoices > 0
5964
? Math.round((paidInvoices / issuedInvoices) * 100)
6065
: 0
6166

@@ -86,8 +91,13 @@ export default function OrbDashboard() {
8691
</div>
8792

8893
{/* Charts Grid */}
89-
<div className="grid gap-4 md:grid-cols-2">
94+
<div className="grid gap-4 grid-cols-1">
9095
<SubsChart data={subsTimeSeriesData} />
96+
97+
</div>
98+
<div className="grid gap-4 md:grid-cols-2">
99+
<SubsByPlanTsChart data={subsByPlanTsData} />
100+
<SubsByPlanChart data={subsByPlanData} />
91101
</div>
92102
</div>
93103
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client"
2+
3+
import { Pie, PieChart } from "recharts"
4+
5+
import {
6+
Card,
7+
CardContent,
8+
CardDescription,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card"
12+
import {
13+
ChartConfig,
14+
ChartContainer,
15+
ChartLegend,
16+
ChartLegendContent,
17+
} from "@/components/ui/chart"
18+
19+
20+
export interface SubsByPlanDataPoint {
21+
plan: string
22+
subs: number
23+
}
24+
25+
export interface SubsByPlanChartData {
26+
data: SubsByPlanDataPoint[]
27+
}
28+
29+
function transformData(data: SubsByPlanDataPoint[]): (SubsByPlanDataPoint & { fill: string })[] {
30+
return data.map((item, index) => ({
31+
...item,
32+
fill: `hsl(var(--chart-${(index % 12) + 1}))` // Using hsl() to properly use the CSS variable
33+
}));
34+
}
35+
36+
function generateChartConfig(data: SubsByPlanDataPoint[]): ChartConfig {
37+
const planNames = Array.from(new Set(data.map(d => d.plan)));
38+
39+
return Object.fromEntries([
40+
['plan', { label: 'Plan' }],
41+
...planNames.map((plan) => [
42+
plan,
43+
{
44+
label: plan,
45+
}
46+
])
47+
]);
48+
}
49+
50+
export function SubsByPlanChart({ data: rawData }: SubsByPlanChartData) {
51+
const chartConfig = generateChartConfig(rawData);
52+
const data = transformData(rawData);
53+
54+
return (
55+
<Card className="flex flex-col">
56+
<CardHeader className="pb-0">
57+
<CardTitle>Subscriptions by Plan</CardTitle>
58+
<CardDescription>Past 30 days</CardDescription>
59+
</CardHeader>
60+
<CardContent className="flex-1 pb-0">
61+
<ChartContainer
62+
config={chartConfig}
63+
className="mx-auto aspect-square max-h-[300px]"
64+
>
65+
<PieChart>
66+
<Pie data={data} dataKey="subs" nameKey="plan" />
67+
<ChartLegend
68+
content={<ChartLegendContent nameKey="plan" />}
69+
className="-translate-y-2 flex-wrap gap-2 [&>*]:basis-1/4 [&>*]:justify-center"
70+
/>
71+
</PieChart>
72+
</ChartContainer>
73+
</CardContent>
74+
</Card>
75+
)
76+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client"
2+
3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card"
10+
import {
11+
ChartConfig,
12+
ChartContainer,
13+
ChartTooltip,
14+
ChartTooltipContent,
15+
} from "@/components/ui/chart"
16+
import { Line, LineChart, XAxis, YAxis } from "recharts"
17+
18+
export interface SubsByPlanTsDataPoint {
19+
day: string
20+
plan: string
21+
subs: number
22+
}
23+
24+
export interface SubsByPlanTsChartData {
25+
data: SubsByPlanTsDataPoint[]
26+
}
27+
28+
export interface TransformedDataPoint {
29+
day: string
30+
[planName: string]: string | number
31+
}
32+
33+
function transformData(rawData: SubsByPlanTsDataPoint[]): TransformedDataPoint[] {
34+
const transformedData: { [key: string]: TransformedDataPoint } = {};
35+
36+
rawData.forEach((item) => {
37+
if (!transformedData[item.day]) {
38+
transformedData[item.day] = { day: item.day };
39+
}
40+
transformedData[item.day][item.plan] = item.subs;
41+
});
42+
43+
return Object.values(transformedData);
44+
}
45+
46+
const chartConfig = {
47+
} satisfies ChartConfig
48+
49+
export function SubsByPlanTsChart({ data: rawData }: SubsByPlanTsChartData) {
50+
const transformedData = transformData(rawData);
51+
const planNames = Array.from(new Set(rawData.map(d => d.plan)));
52+
53+
return (
54+
<Card>
55+
<CardHeader>
56+
<CardTitle>Subscriptions by Plan</CardTitle>
57+
<CardDescription>Active subscriptions over time</CardDescription>
58+
</CardHeader>
59+
<CardContent>
60+
<ChartContainer config={chartConfig}>
61+
<LineChart
62+
accessibilityLayer
63+
data={transformedData}
64+
margin={{
65+
left: 12,
66+
right: 12,
67+
}}
68+
>
69+
<XAxis
70+
dataKey="day"
71+
tickLine={false}
72+
axisLine={false}
73+
tickMargin={8}
74+
// interval="equidistantPreserveStart"
75+
label={{
76+
value: "Day of Month",
77+
position: "bottom",
78+
offset: 0
79+
}}
80+
/>
81+
<YAxis
82+
tickLine={false}
83+
axisLine={false}
84+
tickMargin={8}
85+
label={{
86+
value: "Subscriptions",
87+
angle: -90,
88+
position: "left",
89+
}}
90+
/>
91+
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
92+
{planNames.map((plan) => (
93+
<Line
94+
key={plan}
95+
type="monotone"
96+
dataKey={plan}
97+
name={plan}
98+
strokeWidth={2}
99+
dot={false}
100+
/>
101+
))}
102+
</LineChart>
103+
</ChartContainer>
104+
</CardContent>
105+
</Card>
106+
);
107+
}

apps/web/src/components/tools/orb/subs-chart.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import {
1212
ChartConfig,
1313
ChartTooltipContent
1414
} from "@/components/ui/chart"
15-
import { Line, LineChart, XAxis } from "recharts"
15+
import { Line, LineChart, XAxis, YAxis } from "recharts"
1616

17-
interface SubsDataPoint {
17+
export interface SubsDataPoint {
1818
day: string
1919
invoices: number
2020
}
2121

22-
interface SubsChartProps {
22+
export interface SubsChartProps {
2323
data: SubsDataPoint[]
2424
}
2525

@@ -37,7 +37,7 @@ export function SubsChart({ data }: SubsChartProps) {
3737
<CardTitle>New Subscriptions</CardTitle>
3838
</CardHeader>
3939
<CardContent>
40-
<ChartContainer config={chartConfig}>
40+
<ChartContainer config={chartConfig} className="h-[400px] w-full">
4141
<LineChart
4242
data={data}
4343
margin={{
@@ -50,8 +50,22 @@ export function SubsChart({ data }: SubsChartProps) {
5050
tickLine={false}
5151
axisLine={false}
5252
tickMargin={8}
53-
interval="equidistantPreserveStart"
54-
tickFormatter={(value) => value.split('-')[2]}
53+
// interval="equidistantPreserveStart"
54+
label={{
55+
value: "Day of Month",
56+
position: "bottom",
57+
offset: 0
58+
}}
59+
/>
60+
<YAxis
61+
tickLine={false}
62+
axisLine={false}
63+
tickMargin={8}
64+
label={{
65+
value: "Subscriptions",
66+
angle: -90,
67+
position: "left",
68+
}}
5569
/>
5670
<ChartTooltip
5771
cursor={false}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
TOKEN "read" READ
2+
3+
TAGS "orb"
4+
5+
NODE ep
6+
SQL >
7+
8+
SELECT toDate(toStartOfDay(event_time)) as day, event.subscription.plan.name as plan, count() as subs
9+
FROM orb
10+
where event_type == 'subscription.created' and event_time >= now() - interval 30 day
11+
group by day, plan
12+
order by day, subs asc
13+
14+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
TOKEN "read" READ
2+
3+
TAGS "orb"
4+
5+
NODE ep
6+
SQL >
7+
8+
SELECT event.subscription.plan.name as plan, count() as subs
9+
FROM orb
10+
where event_type == 'subscription.created' and event_time >= now() - interval 30 day
11+
group by plan
12+
order by subs asc
13+
14+

0 commit comments

Comments
 (0)