feat: heatmap based on the assets in past year

This commit is contained in:
sai80082 committed 2024-09-13 20:09:35 +05:30
1 parent 7df379336d
commit 360c19699e
7 files changed
+283 -160

No files matched your search

@@ -0,0 +1,140 @@
import React, { useEffect, useState } from "react";
import { getHeatMapData } from "@/handlers/api/analytics.handler";
type HeatMapEntry = {
date: string;
count: number;
};
export default function AssetHeatMap() {
const [heatMapData, setHeatMapData] = useState<HeatMapEntry[][]>([]);
const [loading, setLoading] = useState(false);
const [weeksPerMonth, setWeeksPerMonth] = useState<number[]>([]); // Store weeks per month
const fetchHeatMapData = async () => {
setLoading(true);
try {
const data = await getHeatMapData();
setHeatMapData(formatHeatMapData(data));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchHeatMapData();
}, []);
const currentDate = new Date();
const months: string[] = [];
// Build an array of the last 12 months
for (let i = 0; i < 12; i++) {
months.push(currentDate.toLocaleString("default", { month: "short" }));
currentDate.setMonth(currentDate.getMonth() - 1);
}
months.reverse();
// Flatten heatMapData to calculate min and max counts
const flattenedData = heatMapData.flat();
const minCount = Math.min(...flattenedData.map((entry) => entry.count));
const maxCount = Math.max(...flattenedData.map((entry) => entry.count));
// Check if a week contains a date with the first day of the month
const hasFirstDayOfMonth = (arr: HeatMapEntry[]) => {
return arr.some((entry) => new Date(entry.date).getDate() === 1);
};
const getColor = (count: number) => {
if (count === -1) return "";
// Calculate thresholds based on the data range
const range = maxCount - minCount;
const threshold1 = minCount + range * 0.2;
const threshold2 = minCount + range * 0.4;
const threshold3 = minCount + range * 0.6;
const threshold4 = minCount + range * 0.8;
if (count === 0) return "bg-zinc-800";
if (count <= threshold1) return "bg-green-200";
if (count <= threshold2) return "bg-green-400";
if (count <= threshold3) return "bg-green-500";
if (count <= threshold4) return "bg-green-600";
return "bg-green-800";
};
const formatHeatMapData = (data: HeatMapEntry[]) => {
const chunks = [];
const weeksPerMonthTemp: number[] = [];
let previousMonthIndex = 0;
for (let i = 0; i < data.length; i += 7) {
const thisWeek = data.slice(i, i + 7);
chunks.push(thisWeek);
if (hasFirstDayOfMonth(thisWeek)) {
const changeWeek = (i - previousMonthIndex) / 7;
previousMonthIndex = i;
weeksPerMonthTemp.push(changeWeek);
}
}
weeksPerMonthTemp.reverse();
setWeeksPerMonth(weeksPerMonthTemp);
return chunks;
};
return (
<div className="p-4 w-full">
<h2 className="text-xl font-bold mb-4">Past Year</h2>
{loading ? (
<p>Loading...</p>
) : (
<div className="overflow-x-auto">
<table className="table-auto border-separate border-spacing-1 w-full">
<thead>
<tr>
{weeksPerMonth.map((weeks, index) => (
<th
key={index}
className="text-center"
colSpan={weeks}
>
{months[index]}
</th>
))}
</tr>
</thead>
<tbody>
{heatMapData[0]?.map((_, rowIndex) => (
<tr key={rowIndex}>
{heatMapData.map((column, colIndex) => (
<td
key={colIndex}
className={`h-6 ${getColor(column[rowIndex]?.count ?? -1)} rounded-lg`}
style={{ width: `calc(${100 / 60}%)` }}
>
{
column[rowIndex]?.date ? (
<a
href={`http://localhost:2283/search?query=%7B%22takenAfter%22%3A%22${column[rowIndex]?.date ?? "N/A"}T00%3A00%3A00.000Z%22%2C%22takenBefore%22%3A%22${column[rowIndex]?.date ?? "N/A"}T23%3A59%3A59.999Z%22%7D`}
className="block h-full w-full"
target="_blank"
>
<div
className="h-full w-full"
title={`Date: ${column[rowIndex]?.date ?? "N/A"}, Count: ${column[rowIndex]?.count ?? 0}`}
/>
</a>) : <div
className="h-full w-full"
/>
}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
@@ -1,109 +0,0 @@
import React, { useEffect, useState } from "react";
import { getAssetStatistics } from "@/handlers/api/analytics.handler";
// List of month labels
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const weeksPerMonth = [4, 4, 4, 4, 5, 4, 4, 4, 5, 4, 4, 5];
// Function to map weeks to months
const getMonthLabels = () => {
// Approximate number of weeks per month
const monthLabels = [];
let cumulativeWeeks = 0;
for (let i = 0; i < months.length; i++) {
monthLabels.push({
month: months[i],
startWeek: cumulativeWeeks,
span: weeksPerMonth[i],
});
cumulativeWeeks += weeksPerMonth[i];
}
return monthLabels;
};
// Function to generate the last 365 days of contribution data
const generateContributions = () => {
const contributions: number[][] = Array.from({ length: 7 }, () => Array(52).fill(0));
for (let week = 0; week < 52; week++) {
for (let day = 0; day < 7; day++) {
// Generate a random number of contributions for each day (0 to 5 for example)
contributions[day][week] = Math.floor(Math.random() * 6);
}
}
return contributions;
};
// Function to determine the background color based on the contribution count
const getColor = (count: number) => {
if (count === 0) return 'bg-zinc-900';
if (count === 1) return 'bg-green-200';
if (count === 2) return 'bg-green-400';
if (count === 3) return 'bg-green-500';
if (count === 4) return 'bg-green-600';
return 'bg-green-800';
};
const ContributionGraph: React.FC = () => {
const contributions = generateContributions();
const [statistics, setStatistics] = useState({});
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true); // Set loading to true when starting to fetch data
return getAssetStatistics()
.then((data) => {
setStatistics(data); // Update statistics with the returned data
})
.finally(() => setLoading(false)); // Stop loading after data is fetched
};
const monthLabels = getMonthLabels();
return (
<div className="p-4 w-full">
<h2 className="text-xl font-bold mb-4">Past Year</h2>
<div className="overflow-x-auto">
{/* Month labels */}
{/* <div className="flex mb-1">
{monthLabels.map(({ month, startWeek, span }) => (
<div
key={month}
style={{ width: `calc(${span / 52 * 100}%)`, marginLeft: `calc(${startWeek / 52 * 100}%)` }}
className="text-center font-bold text-sm"
>
{month}
</div>
))}
</div> */}
<table className="table-auto border-separate border-spacing-1 w-full">
<thead>
<tr>
{months.map((month, index) => (
<th key={index} colSpan={weeksPerMonth[index]}>{month}</th>
))}
</tr>
</thead>
<tbody>
{contributions.map((dayContributions, dayIndex) => (
<tr key={dayIndex}>
{dayContributions.map((count, weekIndex) => (
<td
key={weekIndex}
className={`h-6 ${getColor(count)} rounded-lg`}
style={{ width: `calc(${100 / 52}%)` }}
title={`${count} Images`}
></td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default ContributionGraph;
+3 -1
View File
@@ -38,4 +38,6 @@ export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places";
//Analytics
export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics";
export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics";
export const LIVE_PHOTO_STATISTICS = BASE_API_ENDPOINT + "/analytics/statistics/livephoto";
export const HEATMAP_DATA = BASE_API_ENDPOINT + "/analytics/statistics/heatmap";
+9 -1
View File
@@ -1,4 +1,4 @@
import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH } from "@/config/routes";
import { ASSET_STATISTICS, EXIF_DISTRIBUTION_PATH, HEATMAP_DATA, LIVE_PHOTO_STATISTICS } from "@/config/routes";
import API from "@/lib/api";
export type ISupportedEXIFColumns =
@@ -10,4 +10,12 @@ export const getExifDistribution = async (column: ISupportedEXIFColumns) => {
export const getAssetStatistics = async () => {
return API.get(ASSET_STATISTICS);
}
export const getLivePhotoStatistics = async () => {
return API.get(LIVE_PHOTO_STATISTICS);
}
export const getHeatMapData = async () => {
return API.get(HEATMAP_DATA);
}
+35 -49
View File
@@ -12,9 +12,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import ContributionGraph from "@/components/analytics/exif/contributiongraph";
import AssetHeatMap from "@/components/analytics/exif/AssetHeatMap";
import { useEffect, useState } from "react";
import { getAssetStatistics } from "@/handlers/api/analytics.handler";
import { getAssetStatistics, getLivePhotoStatistics } from "@/handlers/api/analytics.handler";
const inter = Inter({ subsets: ["latin"] });
@@ -73,72 +73,58 @@ const exifCharts: IEXIFDistributionProps[] = [
export default function ExifDataAnalytics() {
const [statistics, setStatistics] = useState({ images: 0, videos: 0, total: 0 });
const [loading, setLoading] = useState(false);
const [livePhotoStatistics, setLivePhotoStatistics] = useState({ total: 0 });
const fetchLivePhotoStatistics = async () => {
setLoading(true);
try {
const data = await getLivePhotoStatistics();
const livePhotoData = Array.isArray(data) && data.length ? data[0].value : 0;
setLivePhotoStatistics({ total: livePhotoData });
} finally {
setLoading(false);
}
};
const fetchStatisticsData = async () => {
setLoading(true); // Set loading to true when starting to fetch data
return getAssetStatistics()
.then((data) => {
console.log("asdasdadasd")
setStatistics(data); // Update statistics with the returned data
})
.finally(() => setLoading(false)); // Stop loading after data is fetched
setLoading(true);
try {
const data = await getAssetStatistics();
setStatistics(data);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatisticsData();
fetchLivePhotoStatistics();
}, []);
return (
<PageLayout>
<Header leftComponent="Exif Data" />
<div className="grid grid-cols-4 gap-4">
{["Total", "Images", "Videos"].map((type, i) => (
<Card key={i}>
<CardHeader>
<CardTitle className="text-center text-lg font-mono">{type}</CardTitle>
</CardHeader>
<CardContent className="text-center text-lg font-mono">
{loading ? "Loading..." : statistics[type.toLowerCase() as keyof typeof statistics].toLocaleString()}
</CardContent>
</Card>
))}
<Card>
<CardHeader>
<CardTitle className="text-center text-lg font-mono">Total Files</CardTitle>
<CardTitle className="text-center text-lg font-mono">Live Photos</CardTitle>
</CardHeader>
<CardContent className="text-center text-lg font-mono">
{(() => {
const a = statistics.total;
return a.toLocaleString();
})()}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-center text-lg font-mono">Images</CardTitle>
</CardHeader>
<CardContent className="text-center text-lg font-mono">
{(() => {
const a = statistics.images;
return a.toLocaleString();
})()}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-center text-lg font-mono">Videos</CardTitle>
</CardHeader>
<CardContent className="text-center text-lg font-mono">
{(() => {
const a = statistics.videos;
return a.toLocaleString();
})()}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-center text-lg font-mono">Live</CardTitle>
</CardHeader>
<CardContent className="text-center text-lg font-mono">
{(() => {
const a = 50000;
return a.toLocaleString();
})()}
{loading ? "Loading..." : livePhotoStatistics.total.toLocaleString()}
</CardContent>
</Card>
</div>
<ContributionGraph />
<AssetHeatMap />
<div className="grid grid-cols-3 gap-4">
{exifCharts.map((chart) => (
<EXIFDistribution
@@ -0,0 +1,65 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { assets, exif } from "@/schema";
import { and, count, desc, eq, isNotNull, ne, sql, } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
// Helper function to format date as YYYY-MM-DD
function formatDate(date: Date) {
return date.toISOString().split('T')[0];
}
function fillMissingDates(data: any[]) {
const today = new Date();
// Set the start date as one year ago, excluding the current month in the previous year
const oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth() + 1, 1); // First day of the next month last year
const existingDates = new Set(data.map(item => item.date));
const filledData = [];
// Iterate from today to one year ago, but skip the current month from last year
for (let d = new Date(today); d >= oneYearAgo; d.setDate(d.getDate() - 1)) {
const dateStr = formatDate(d);
// Skip dates in the current month of the previous year
if (d.getFullYear() === today.getFullYear() - 1 && d.getMonth() === today.getMonth()) {
continue;
}
if (existingDates.has(dateStr)) {
filledData.push(data.find(item => item.date === dateStr));
} else {
filledData.push({ date: dateStr, count: 0 });
}
}
// Sort the filled data in descending order
filledData.sort((a, b) => a.date.localeCompare(b.date));
return filledData;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser(req);
const dataFromDB = await db.select({
date: sql`DATE(${assets.fileCreatedAt})`.as('date'),
count: count(),
})
.from(assets)
.where(eq(assets.ownerId, currentUser.id))
.groupBy(sql`DATE(${assets.fileCreatedAt})`)
.orderBy(sql`DATE(${assets.fileCreatedAt}) DESC`)
const updatedData = fillMissingDates(dataFromDB);
return res.status(200).json(updatedData);
} catch (error: any) {
res.status(500).json({
error: error?.message,
});
}
}
@@ -0,0 +1,31 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { CHART_COLORS } from "@/config/constants/chart.constant";
import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { assets, exif } from "@/schema";
import { and, count, desc, eq, isNotNull, ne } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const currentUser = await getCurrentUser(req);
const dataFromDB = await db.select({
value: count(),
})
.from(assets)
.where(and(
eq(assets.ownerId, currentUser.id),
isNotNull(assets.livePhotoVideoId))
);
return res.status(200).json(dataFromDB);
} catch (error: any) {
res.status(500).json({
error: error?.message,
});
}
}