From 360c19699e75e22b95fc7d6e97ff9c05114e4d1e Mon Sep 17 00:00:00 2001 From: sai80082 Date: Fri, 13 Sep 2024 20:09:35 +0530 Subject: [PATCH] feat: heatmap based on the assets in past year --- .../analytics/exif/AssetHeatMap.tsx | 140 ++++++++++++++++++ .../analytics/exif/contributiongraph.tsx | 109 -------------- src/config/routes.ts | 4 +- src/handlers/api/analytics.handler.ts | 10 +- src/pages/analytics/exif.tsx | 84 +++++------ .../api/analytics/statistics/heatmap.tsx | 65 ++++++++ .../api/analytics/statistics/livephoto.tsx | 31 ++++ 7 files changed, 283 insertions(+), 160 deletions(-) create mode 100644 src/components/analytics/exif/AssetHeatMap.tsx delete mode 100644 src/components/analytics/exif/contributiongraph.tsx create mode 100644 src/pages/api/analytics/statistics/heatmap.tsx create mode 100644 src/pages/api/analytics/statistics/livephoto.tsx diff --git a/src/components/analytics/exif/AssetHeatMap.tsx b/src/components/analytics/exif/AssetHeatMap.tsx new file mode 100644 index 0000000..b47be3e --- /dev/null +++ b/src/components/analytics/exif/AssetHeatMap.tsx @@ -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([]); + const [loading, setLoading] = useState(false); + const [weeksPerMonth, setWeeksPerMonth] = useState([]); // 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 ( +
+

Past Year

+ {loading ? ( +

Loading...

+ ) : ( +
+ + + + {weeksPerMonth.map((weeks, index) => ( + + ))} + + + + {heatMapData[0]?.map((_, rowIndex) => ( + + {heatMapData.map((column, colIndex) => ( + + ))} + + ))} + +
+ {months[index]} +
+ { + column[rowIndex]?.date ? ( + +
+ ) :
+ } +
+
+ )} +
+ ); +} diff --git a/src/components/analytics/exif/contributiongraph.tsx b/src/components/analytics/exif/contributiongraph.tsx deleted file mode 100644 index 1e9c0a1..0000000 --- a/src/components/analytics/exif/contributiongraph.tsx +++ /dev/null @@ -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 ( -
-

Past Year

-
- {/* Month labels */} - {/*
- {monthLabels.map(({ month, startWeek, span }) => ( -
- {month} -
- ))} -
*/} - - - - - {months.map((month, index) => ( - - ))} - - - - - {contributions.map((dayContributions, dayIndex) => ( - - {dayContributions.map((count, weekIndex) => ( - - ))} - - ))} - -
{month}
-
-
- ); -}; - -export default ContributionGraph; diff --git a/src/config/routes.ts b/src/config/routes.ts index dfd0072..1ecb845 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -38,4 +38,6 @@ export const SEARCH_PLACES_PATH = BASE_PROXY_ENDPOINT + "/search/places"; //Analytics -export const ASSET_STATISTICS = BASE_PROXY_ENDPOINT + "/assets/statistics"; \ No newline at end of file +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"; \ No newline at end of file diff --git a/src/handlers/api/analytics.handler.ts b/src/handlers/api/analytics.handler.ts index 933cdd8..0daf406 100644 --- a/src/handlers/api/analytics.handler.ts +++ b/src/handlers/api/analytics.handler.ts @@ -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); } \ No newline at end of file diff --git a/src/pages/analytics/exif.tsx b/src/pages/analytics/exif.tsx index ed32d6a..64af274 100644 --- a/src/pages/analytics/exif.tsx +++ b/src/pages/analytics/exif.tsx @@ -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 (
+ {["Total", "Images", "Videos"].map((type, i) => ( + + + {type} + + + {loading ? "Loading..." : statistics[type.toLowerCase() as keyof typeof statistics].toLocaleString()} + + + ))} - Total Files + Live Photos - {(() => { - const a = statistics.total; - return a.toLocaleString(); - })()} - - - - - Images - - - {(() => { - const a = statistics.images; - return a.toLocaleString(); - })()} - - - - - Videos - - - {(() => { - const a = statistics.videos; - return a.toLocaleString(); - })()} - - - - - Live - - - {(() => { - const a = 50000; - return a.toLocaleString(); - })()} + {loading ? "Loading..." : livePhotoStatistics.total.toLocaleString()}
- - +
{exifCharts.map((chart) => ( 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, + }); + } +} diff --git a/src/pages/api/analytics/statistics/livephoto.tsx b/src/pages/api/analytics/statistics/livephoto.tsx new file mode 100644 index 0000000..8eb670c --- /dev/null +++ b/src/pages/api/analytics/statistics/livephoto.tsx @@ -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, + }); + } +}