Skip to content

Commit d3ae103

Browse files
committed
feat: borrowing heat map chart
1 parent c8b31ef commit d3ae103

File tree

9 files changed

+288
-37
lines changed

9 files changed

+288
-37
lines changed

app/(protected)/admin/dashboard/page.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { MonthlyRevenueChart } from '@/components/dashboard/MonthlyRevenueChart'
1111
import { MostBorrowedBookChart } from '@/components/dashboard/MostBorrowedBookChart'
1212
import { MontlyBorrowChart } from '@/components/dashboard/MontlyBorrowChart'
1313
import { TopMembershipChart } from '@/components/dashboard/TopMembershipChart'
14-
import { getAnalysis } from '@/lib/api/analysis'
14+
import { getAnalysis, getBorrowingHeatmapAnalysis } from '@/lib/api/analysis'
1515
import { SITE_NAME } from '@/lib/consts'
1616
import type { Metadata } from 'next'
1717
import { LibrarySelector } from '@/components/dashboard/LibrarySelector'
@@ -22,6 +22,8 @@ import { format, subMonths, parse, startOfDay, endOfDay } from 'date-fns'
2222
import { getListLibraries } from '@/lib/api/library'
2323
import { DateRange } from 'react-day-picker'
2424
import { cookies } from 'next/headers'
25+
import { Suspense } from 'react'
26+
import { BorrowHeatmapChart } from '@/components/dashboard/BorrowHeatmapChart'
2527

2628
export const metadata: Metadata = {
2729
title: `Dashboard · ${SITE_NAME}`,
@@ -65,7 +67,7 @@ export default async function DashboardPage({
6567
redirect('?' + sp.toString(), RedirectType.replace)
6668
}
6769

68-
const [res, libsRes] = await Promise.all([
70+
const [res, libsRes, heatmapRes] = await Promise.all([
6971
getAnalysis({
7072
skip,
7173
limit,
@@ -74,6 +76,11 @@ export default async function DashboardPage({
7476
library_id: libID!,
7577
}),
7678
getListLibraries({ limit: 5 }),
79+
getBorrowingHeatmapAnalysis({
80+
library_id: libID!,
81+
start: startOfDay(parse(from, 'dd-MM-yyyy', new Date())).toJSON(),
82+
end: endOfDay(parse(to, 'dd-MM-yyyy', new Date())).toJSON(),
83+
}),
7784
])
7885

7986
if ('error' in res) {
@@ -144,6 +151,11 @@ export default async function DashboardPage({
144151
<TopMembershipChart data={res.data.membership} />
145152
<MontlyBorrowChart data={res.data.borrowing} />
146153
<MonthlyRevenueChart data={res.data.revenue} />
154+
{'error' in heatmapRes ? (
155+
<div>{heatmapRes.error}</div>
156+
) : (
157+
<BorrowHeatmapChart data={heatmapRes.data} />
158+
)}
147159
</div>
148160
</div>
149161
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use client'
2+
3+
import {
4+
Scatter,
5+
ScatterChart,
6+
XAxis,
7+
YAxis,
8+
ZAxis,
9+
CartesianGrid,
10+
} from 'recharts'
11+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
12+
import {
13+
ChartConfig,
14+
ChartContainer,
15+
ChartTooltip,
16+
} from '@/components/ui/chart'
17+
18+
const chartConfig = {
19+
count: {
20+
label: 'Borrowings',
21+
color: 'var(--chart-1)',
22+
},
23+
} satisfies ChartConfig
24+
25+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
26+
27+
export const BorrowHeatmapChart: React.FC<{
28+
data: {
29+
day_of_week: number
30+
hour_of_day: number
31+
count: number
32+
}[]
33+
}> = ({ data }) => {
34+
const chartData = data.map(({ day_of_week, hour_of_day, count }) => ({
35+
x: hour_of_day,
36+
y: day_of_week,
37+
z: count * 100, // Scale up for bubble size
38+
count,
39+
day: dayNames[day_of_week],
40+
hour: `${hour_of_day}:00`,
41+
}))
42+
43+
console.log(data)
44+
45+
return (
46+
<Card>
47+
<CardHeader>
48+
<CardTitle>Borrowing Activity Heatmap</CardTitle>
49+
</CardHeader>
50+
<CardContent>
51+
<ChartContainer config={chartConfig}>
52+
<ScatterChart
53+
width={800}
54+
height={400}
55+
data={chartData}
56+
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
57+
>
58+
<CartesianGrid />
59+
<XAxis
60+
type="number"
61+
dataKey="x"
62+
domain={[0, 23]}
63+
tickFormatter={(value) => `${value}:00`}
64+
label={{
65+
value: 'Hour of Day',
66+
position: 'insideBottom',
67+
offset: -10,
68+
}}
69+
/>
70+
<YAxis
71+
type="number"
72+
dataKey="y"
73+
domain={[0, 6]}
74+
tickFormatter={(value) => dayNames[value]}
75+
label={{
76+
value: 'Day of Week',
77+
angle: -90,
78+
position: 'insideLeft',
79+
}}
80+
/>
81+
<ZAxis type="number" dataKey="z" range={[50, 400]} />
82+
<ChartTooltip
83+
cursor={{ strokeDasharray: '3 3' }}
84+
content={({ active, payload }) => {
85+
if (active && payload && payload.length) {
86+
const data = payload[0].payload
87+
return (
88+
<div className="rounded-lg border bg-background p-2 shadow-sm">
89+
<div className="grid grid-cols-2 gap-2">
90+
<span className="font-medium">{data.day}</span>
91+
<span className="font-medium">{data.hour}</span>
92+
<span className="text-muted-foreground">
93+
Borrowings:
94+
</span>
95+
<span className="font-medium">{data.count}</span>
96+
</div>
97+
</div>
98+
)
99+
}
100+
return null
101+
}}
102+
/>
103+
<Scatter dataKey="z" fill="var(--color-count)" />
104+
</ScatterChart>
105+
</ChartContainer>
106+
</CardContent>
107+
</Card>
108+
)
109+
}

components/dashboard/LibrarySelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const LibrarySelector: React.FC<{
1717
onChangeAction: (libID: string) => void
1818
}> = ({ libs, lib, onChangeAction: onChange }) => {
1919
return (
20-
<Select value={lib} onValueChange={onChange}>
20+
<Select value={lib} onValueChange={onChange} disabled>
2121
<SelectTrigger className="w-1/2 justify-self-end">
2222
<SelectValue placeholder="Select a library" />
2323
</SelectTrigger>

components/dashboard/MonthlyRevenueChart.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ export function MonthlyRevenueChart({ data }: { data: Analysis['revenue'] }) {
7474
tickMargin={8}
7575
tickFormatter={(value) => formatDate(value)}
7676
/>
77-
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
77+
<ChartTooltip
78+
cursor={true}
79+
content={<ChartTooltipContent />}
80+
labelFormatter={(value) => formatDate(value)}
81+
/>
7882
<defs>
7983
<linearGradient id="fillSubscription" x1="0" y1="0" x2="0" y2="1">
8084
<stop
@@ -123,9 +127,6 @@ export function MonthlyRevenueChart({ data }: { data: Analysis['revenue'] }) {
123127
<CardFooter>
124128
<div className="flex w-full items-start gap-2 text-sm">
125129
<div className="grid gap-2">
126-
{/* <div className="flex items-center gap-2 font-medium leading-none">
127-
Trending up by 5.2% this month <TrendingUp className="size-4" />
128-
</div> */}
129130
<div className="flex items-center gap-2 leading-none text-muted-foreground">
130131
Showing total revenue from subscriptions and fines
131132
</div>

components/dashboard/MontlyBorrowChart.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ import { useMemo } from 'react'
2424
import { format, parse } from 'date-fns'
2525

2626
const chartConfig = {
27-
count: {
28-
label: 'Borrowing',
27+
total_borrow: {
28+
label: 'Borrow',
2929
color: 'var(--chart-1)',
3030
},
31+
total_return: {
32+
label: 'Return',
33+
color: 'var(--chart-2)',
34+
},
3135
} satisfies ChartConfig
3236

3337
export function MontlyBorrowChart({ data }: { data: Analysis['borrowing'] }) {
@@ -71,16 +75,29 @@ export function MontlyBorrowChart({ data }: { data: Analysis['borrowing'] }) {
7175
tickFormatter={(value) => formatDate(value)}
7276
/>
7377
<ChartTooltip
74-
cursor={false}
75-
content={<ChartTooltipContent hideLabel />}
78+
cursor={true}
79+
content={<ChartTooltipContent />}
80+
labelFormatter={(value) => formatDate(value)}
81+
/>
82+
<Line
83+
dataKey="total_borrow"
84+
type="natural"
85+
stroke="var(--color-total_borrow)"
86+
strokeWidth={2}
87+
dot={{
88+
fill: 'var(--color-total_borrow)',
89+
}}
90+
activeDot={{
91+
r: 6,
92+
}}
7693
/>
7794
<Line
78-
dataKey="count"
95+
dataKey="total_return"
7996
type="natural"
80-
stroke="var(--color-count)"
97+
stroke="var(--color-total_return)"
8198
strokeWidth={2}
8299
dot={{
83-
fill: 'var(--color-count)',
100+
fill: 'var(--color-total_return)',
84101
}}
85102
activeDot={{
86103
r: 6,
@@ -90,9 +107,6 @@ export function MontlyBorrowChart({ data }: { data: Analysis['borrowing'] }) {
90107
</ChartContainer>
91108
</CardContent>
92109
<CardFooter className="flex-col items-start gap-2 text-sm">
93-
{/* <div className="flex gap-2 font-medium leading-none">
94-
Trending up by 5.2% this month <TrendingUp className="size-4" />
95-
</div> */}
96110
<div className="leading-none text-muted-foreground">
97111
Showing total borrows
98112
</div>

components/dashboard/MostBorrowedBookChart.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@ import {
1818
ChartTooltipContent,
1919
} from '@/components/ui/chart'
2020
import { Analysis } from '@/lib/types/analysis'
21-
import { useSearchParams } from 'next/navigation'
21+
import { useRouter, useSearchParams } from 'next/navigation'
2222
import { useMemo } from 'react'
2323
import { format, parse } from 'date-fns'
2424

2525
export function MostBorrowedBookChart({ data }: { data: Analysis['book'] }) {
26+
const router = useRouter()
27+
2628
const chartConfig = data.reduce((acc, { title }, index) => {
2729
acc[title] = {
2830
label: title,
2931
color: `var(--chart-${index + 1})`,
3032
}
3133
return acc
3234
}, {} as ChartConfig)
33-
const chartData = data.map(({ title, count }) => ({
34-
browser: title,
35+
const chartData = data.map(({ id, title, count }) => ({
36+
title: title,
3537
total: count,
38+
id: id,
3639
fill: chartConfig[title].color,
3740
}))
3841

@@ -68,7 +71,7 @@ export function MostBorrowedBookChart({ data }: { data: Analysis['book'] }) {
6871
}}
6972
>
7073
<YAxis
71-
dataKey="browser"
74+
dataKey="title"
7275
type="category"
7376
tickLine={false}
7477
tickMargin={10}
@@ -78,18 +81,18 @@ export function MostBorrowedBookChart({ data }: { data: Analysis['book'] }) {
7881
}
7982
/>
8083
<XAxis dataKey="total" type="number" hide />
81-
<ChartTooltip
82-
cursor={false}
83-
content={<ChartTooltipContent hideLabel />}
84+
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
85+
<Bar
86+
dataKey="total"
87+
layout="vertical"
88+
radius={5}
89+
cursor="pointer"
90+
onClick={({ id }) => router.push(`/admin/books/${id}`)}
8491
/>
85-
<Bar dataKey="total" layout="vertical" radius={5} />
8692
</BarChart>
8793
</ChartContainer>
8894
</CardContent>
8995
<CardFooter className="flex-col items-start gap-2 text-sm">
90-
{/* <div className="flex gap-2 font-medium leading-none">
91-
Trending up by 5.2% this month <TrendingUp className="size-4" />
92-
</div> */}
9396
<div className="leading-none text-muted-foreground">
9497
Showing top most borrowed books
9598
</div>

components/dashboard/TopMembershipChart.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import {
1818
ChartTooltipContent,
1919
} from '@/components/ui/chart'
2020
import { Analysis } from '@/lib/types/analysis'
21-
import { useSearchParams } from 'next/navigation'
21+
import { useRouter, useSearchParams } from 'next/navigation'
2222
import { useMemo } from 'react'
2323
import { format, parse } from 'date-fns'
2424

2525
export function TopMembershipChart({ data }: { data: Analysis['membership'] }) {
26+
const router = useRouter()
27+
2628
const chartConfig = data.reduce((acc, { name }, index) => {
2729
acc[name] = {
2830
label: name,
@@ -31,10 +33,11 @@ export function TopMembershipChart({ data }: { data: Analysis['membership'] }) {
3133
return acc
3234
}, {} as ChartConfig)
3335

34-
const chartData = data.map(({ name, count }) => ({
35-
browser: name,
36+
const chartData = data.map(({ id, name, count }) => ({
37+
name: name,
3638
visitors: count,
3739
fill: chartConfig[name].color,
40+
id: id,
3841
}))
3942

4043
const params = useSearchParams()
@@ -65,14 +68,20 @@ export function TopMembershipChart({ data }: { data: Analysis['membership'] }) {
6568
>
6669
<PieChart>
6770
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
68-
<Pie data={chartData} dataKey="visitors" label nameKey="browser" />
71+
<Pie
72+
data={chartData}
73+
dataKey="visitors"
74+
label
75+
nameKey="name"
76+
cursor="pointer"
77+
onClick={({ name }) =>
78+
router.push(`/admin/memberships?name=${name}`)
79+
}
80+
/>
6981
</PieChart>
7082
</ChartContainer>
7183
</CardContent>
7284
<CardFooter className="flex-col items-start gap-2 text-sm">
73-
{/* <div className="flex items-center gap-2 font-medium leading-none">
74-
Trending up by 5.2% this month <TrendingUp className="size-4" />
75-
</div> */}
7685
<div className="leading-none text-muted-foreground">
7786
Showing top purchased membership
7887
</div>

0 commit comments

Comments
 (0)