Skip to content

Commit 9139e82

Browse files
committed
feat: added storage drilldown in analytics
Signed-off-by: Varun Raj <varun@skcript.com>
1 parent 4dd64aa commit 9139e82

File tree

8 files changed

+66
-18
lines changed

8 files changed

+66
-18
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,16 @@ bun run dev
8585

8686
- [x] Manage People
8787
- [x] Smart Merge
88-
- [ ] Manage Albums
88+
- [x] Manage Albums
8989
- [ ] Bulk Delete
9090
- [ ] Bulk Edit
9191
- [ ] Filters
9292
- [x] Potential Albums
93-
- [ ] Statistics
93+
- [x] People in Album
94+
- [x] Missing Location
95+
- [x] Statistics
9496
- [x] EXIF Data
95-
- [ ] Assets Overtime Chart
97+
- [x] Assets Overtime Chart
9698

9799
**Tech Related**
98100

src/components/analytics/exif/EXIFDistribution.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import { Card } from "@/components/ui/card";
22
import PieChart, { IPieChartData } from "@/components/ui/pie-chart";
33
import { getExifDistribution, ISupportedEXIFColumns } from "@/handlers/api/analytics.handler";
44
import React, { useEffect, useState } from "react";
5+
import { ValueType } from "recharts/types/component/DefaultTooltipContent";
56

67
export interface IEXIFDistributionProps {
78
column: ISupportedEXIFColumns;
89
title: string;
910
description: string;
11+
tooltipValueFormatter?: (value?: number | string | undefined | ValueType) => string;
1012
}
1113

1214
export default function EXIFDistribution(
13-
{ column, title, description }: IEXIFDistributionProps
15+
{ column, title, description, tooltipValueFormatter }: IEXIFDistributionProps
1416
) {
1517
const [chartData, setChartData] = useState<IPieChartData[]>([]);
1618
const [loading, setLoading] = useState(false);
@@ -26,13 +28,12 @@ export default function EXIFDistribution(
2628
fetchData();
2729
}, [column]);
2830

29-
3031
return (
3132
<Card
3233
title={title}
3334
description={description}
3435
>
35-
<PieChart data={chartData} loading={loading} errorMessage={errorMessage} />
36+
<PieChart data={chartData} loading={loading} errorMessage={errorMessage} tooltipValueFormatter={tooltipValueFormatter} />
3637
</Card>
3738
);
3839
}

src/components/ui/chart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ const ChartTooltipContent = React.forwardRef<
114114
indicator?: "line" | "dot" | "dashed"
115115
nameKey?: string
116116
labelKey?: string
117+
valueFormatter?: (value: number | string | undefined | ValueType) => string
117118
}
118119
>(
119120
(
@@ -126,6 +127,7 @@ const ChartTooltipContent = React.forwardRef<
126127
hideIndicator = false,
127128
label,
128129
labelFormatter,
130+
valueFormatter = (value) => value?.toLocaleString(),
129131
labelClassName,
130132
formatter,
131133
color,
@@ -243,7 +245,7 @@ const ChartTooltipContent = React.forwardRef<
243245
</div>
244246
{item.value && (
245247
<span className="font-mono font-medium tabular-nums text-foreground">
246-
{item.value.toLocaleString()}
248+
{valueFormatter(item.value)}
247249
</span>
248250
)}
249251
</div>

src/components/ui/pie-chart.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ import { CHART_COLORS, PIE_CONFIG } from "@/config/constants/chart.constant";
22
import React, { useMemo } from "react";
33
import { Label, Pie, PieChart as PieChartRoot } from "recharts";
44
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart";
5-
import { int } from "drizzle-orm/mysql-core";
5+
import { ValueType } from "recharts/types/component/DefaultTooltipContent";
66

7-
export interface IPieChartData { label: string; value: number }
7+
export interface IPieChartData { label: string; value: number; valueLabel?: string }
88

99
interface ChartProps {
1010
data: IPieChartData[];
1111
topLabel?: string;
1212
loading?: boolean;
1313
errorMessage?: string | null;
14+
tooltipValueFormatter?: (value: number | string | undefined | ValueType) => string;
1415
}
15-
export default function PieChart({ data: _data, topLabel, loading, errorMessage }: ChartProps) {
16+
export default function PieChart({ data: _data, topLabel, loading, errorMessage, tooltipValueFormatter }: ChartProps) {
1617
const topItem = _data[0];
1718

1819
const data = useMemo(() => _data.map((item, index) => ({
@@ -45,7 +46,12 @@ export default function PieChart({ data: _data, topLabel, loading, errorMessage
4546
<PieChartRoot accessibilityLayer>
4647
<ChartTooltip
4748
cursor={false}
48-
content={<ChartTooltipContent hideLabel />}
49+
content={<ChartTooltipContent
50+
hideLabel
51+
nameKey="value"
52+
labelKey="valueLabel"
53+
valueFormatter={tooltipValueFormatter}
54+
/>}
4955
/>
5056
<Pie
5157
dataKey="value"
@@ -71,7 +77,7 @@ export default function PieChart({ data: _data, topLabel, loading, errorMessage
7177
y={viewBox.cy}
7278
className="fill-foreground text-3xl font-bold"
7379
>
74-
{topItem.value.toLocaleString()}
80+
{(topItem.valueLabel || topItem.value).toLocaleString()}
7581
</tspan>
7682
<tspan
7783
x={viewBox.cx}

src/handlers/api/analytics.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH, HEATMAP_DATA, LIVE_PHOTO_STAT
22
import API from "@/lib/api";
33

44
export type ISupportedEXIFColumns =
5-
"make" | "model" | "focal-length" | "city" | "state" | "country" | "iso" | "exposureTime" | 'lensModel' | "projectionType";
5+
"make" | "model" | "focal-length" | "city" | "state" | "country" | "iso" | "exposureTime" | 'lensModel' | "projectionType" | "storage";
66

77
export const getExifDistribution = async (column: ISupportedEXIFColumns) => {
88
return API.get(EXIF_DISTRIBUTION_PATH(column));

src/pages/analytics/exif.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,22 @@ import EXIFDistribution, {
77
import {
88
Card,
99
CardContent,
10-
CardDescription,
11-
CardFooter,
1210
CardHeader,
1311
CardTitle,
1412
} from "@/components/ui/card";
1513
import AssetHeatMap from "@/components/analytics/exif/AssetHeatMap";
1614
import { useEffect, useState } from "react";
1715
import { getAssetStatistics, getLivePhotoStatistics } from "@/handlers/api/analytics.handler";
16+
import { humanizeBytes } from "@/helpers/string.helper";
1817

19-
const inter = Inter({ subsets: ["latin"] });
2018

2119
const exifCharts: IEXIFDistributionProps[] = [
20+
{
21+
column: "storage",
22+
title: "Storage",
23+
description: "Distribution of storage",
24+
tooltipValueFormatter: (value) => humanizeBytes(value as number * 1000000),
25+
},
2226
{
2327
column: "make",
2428
title: "Make",
@@ -132,6 +136,7 @@ export default function ExifDataAnalytics() {
132136
column={chart.column}
133137
title={chart.title}
134138
description={chart.description}
139+
tooltipValueFormatter={chart.tooltipValueFormatter}
135140
/>
136141
))}
137142

src/pages/api/analytics/exif/[property].ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2-
import { CHART_COLORS } from "@/config/constants/chart.constant";
32
import { db } from "@/config/db";
43
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
54
import { assets, exif } from "@/schema";
@@ -16,7 +15,7 @@ const columnMap = {
1615
iso: exif.iso,
1716
exposureTime: exif.exposureTime,
1817
lensModel: exif.lensModel,
19-
projectionType: exif.projectionType
18+
projectionType: exif.projectionType,
2019
}
2120

2221
export default async function handler(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { db } from "@/config/db";
2+
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
3+
import { humanizeBytes } from "@/helpers/string.helper";
4+
import { assets, exif, users } from "@/schema";
5+
import { Value } from "@radix-ui/react-select";
6+
import { desc, eq } from "drizzle-orm";
7+
import { sum } from "drizzle-orm";
8+
import { NextApiResponse } from "next";
9+
10+
import { NextApiRequest } from "next";
11+
12+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
13+
const currentUser = await getCurrentUser(req);
14+
if (!currentUser) {
15+
return res.status(401).json({ error: "Unauthorized" });
16+
}
17+
const dbStorage = await db.select({
18+
label: users.name,
19+
value: sum(exif.fileSizeInByte)
20+
})
21+
.from(assets)
22+
.leftJoin(exif, eq(assets.id, exif.assetId))
23+
.leftJoin(users, eq(assets.ownerId, users.id))
24+
.orderBy(desc(sum(exif.fileSizeInByte)))
25+
.groupBy(assets.ownerId, users.name)
26+
27+
const cleanedData = dbStorage.map((item) => ({
28+
label: item.label,
29+
value: Math.round((parseInt(item.value ?? "0") / 1000000)),
30+
valueLabel: humanizeBytes(parseInt(item.value ?? "0")),
31+
}));
32+
res.status(200).json(cleanedData);
33+
}

0 commit comments

Comments
 (0)