From 6bdd9c55bc2d1073c835d0e876994ddea21ee9c0 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 11:39:28 +0200 Subject: [PATCH 1/6] style: Improve numbers formatting --- app/pages/statistics.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/pages/statistics.tsx b/app/pages/statistics.tsx index 84757d010..287557c9e 100644 --- a/app/pages/statistics.tsx +++ b/app/pages/statistics.tsx @@ -282,12 +282,20 @@ const BaseStatsCard = ({ Last 30 days daily average:{" "} - {trend.lastMonthDailyAverage.toFixed(2)} + + {trend.lastMonthDailyAverage >= 10 + ? formatInteger(trend.lastMonthDailyAverage) + : trend.lastMonthDailyAverage.toFixed(2)} +
Last 90 days daily average:{" "} - {trend.previousThreeMonthsDailyAverage.toFixed(2)} + + {trend.previousThreeMonthsDailyAverage >= 10 + ? formatInteger(trend.previousThreeMonthsDailyAverage) + : trend.previousThreeMonthsDailyAverage.toFixed(2)} + } @@ -453,7 +461,7 @@ const Bar = ({ }; const StatsCard = ( - props: PageProps["charts"] & { + props: StatProps & { title: (total: number) => string; subtitle: (total: number, avgMonthlyCount: number) => string; } From d96f9e00f0cfd4c1d23a8795ba3faaf379483e1d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 12:14:31 +0200 Subject: [PATCH 2/6] feat: Add basic most popular charts section --- app/pages/statistics.tsx | 184 ++++++++++++++++++++++++++------------- 1 file changed, 123 insertions(+), 61 deletions(-) diff --git a/app/pages/statistics.tsx b/app/pages/statistics.tsx index 287557c9e..280c5bba8 100644 --- a/app/pages/statistics.tsx +++ b/app/pages/statistics.tsx @@ -2,7 +2,6 @@ import { Box, Card, Tooltip, Typography } from "@mui/material"; import { max, rollups, sum } from "d3-array"; import { formatLocale } from "d3-format"; -import { timeFormat } from "d3-time-format"; import { motion } from "framer-motion"; import uniq from "lodash/uniq"; import { GetServerSideProps } from "next"; @@ -13,6 +12,8 @@ import { BANNER_MARGIN_TOP } from "@/components/presence"; import prisma from "@/db/client"; import { Serialized, deserializeProps, serializeProps } from "@/db/serialize"; import { useFlag } from "@/flags"; +import { Locale } from "@/locales/locales"; +import { useLocale } from "@/src"; type StatProps = { countByDay: { day: Date; count: number }[]; @@ -23,17 +24,51 @@ type StatProps = { }; type PageProps = { - charts: StatProps; + charts: StatProps & { + mostPopular: { + key: string; + createdDate: Date; + viewCount: number; + }[]; + }; views: StatProps; }; export const getServerSideProps: GetServerSideProps = async () => { const [ + mostPopularCharts, chartCountByDay, chartTrendAverages, viewCountByDay, viewTrendAverages, ] = await Promise.all([ + prisma.config + .findMany({ + select: { + key: true, + created_at: true, + _count: { + select: { + views: true, + }, + }, + }, + orderBy: { + views: { + _count: "desc", + }, + }, + take: 10, + }) + .then((rows) => + rows.map((row) => { + return { + key: row.key, + createdDate: row.created_at, + viewCount: row._count.views, + }; + }) + ), prisma.$queryRaw` SELECT DATE_TRUNC('day', created_at) AS day, @@ -137,9 +172,11 @@ export const getServerSideProps: GetServerSideProps = async () => { }; }), ]); + return { props: serializeProps({ charts: { + mostPopular: mostPopularCharts, countByDay: chartCountByDay, trendAverages: chartTrendAverages, }, @@ -153,6 +190,7 @@ export const getServerSideProps: GetServerSideProps = async () => { const Statistics = (props: Serialized) => { const { charts, views } = deserializeProps(props); + return ( ) => {

Statistics

{charts.countByDay.length > 0 && ( `Visualize users created ${formatInteger(total)} charts in total` } @@ -186,7 +225,8 @@ const Statistics = (props: Serialized) => { )} {views.countByDay.length > 0 && ( `Charts were viewed ${formatInteger(total)} times in total` } @@ -195,6 +235,17 @@ const Statistics = (props: Serialized) => { } /> )} + {charts.mostPopular.length > 0 && ( + [ + chart.key, + { count: chart.viewCount, label: chart.key }, + ])} + columnName="Chart" + /> + )}
@@ -203,41 +254,46 @@ const Statistics = (props: Serialized) => { export default Statistics; -const formatShortMonth = timeFormat("%b"); -const formatYearMonth = timeFormat("%Y-%m"); +const formatYearMonth = (date: Date, { locale }: { locale: Locale }) => { + const year = date.getFullYear(); + const month = date.toLocaleDateString(locale, { month: "short" }); + return `${year} ${month}`; +}; const groupByYearMonth = ( - countByDay: PageProps[keyof PageProps]["countByDay"] + countByDay: PageProps[keyof PageProps]["countByDay"], + { locale }: { locale: Locale } ) => { const countByDate = rollups( countByDay, (v) => ({ count: sum(v, (d) => d.count), - date: v[0].day, - monthStr: formatShortMonth(v[0].day), + date: v[v.length - 1].day, + label: formatYearMonth(v[0].day, { locale }), }), - (d) => formatYearMonth(d.day) + (d) => formatYearMonth(d.day, { locale }) ).reverse(); const allYearMonthStrings = uniq( countByDate.map(([yearMonthStr]) => yearMonthStr) ); - const start = countByDate[0][1].date; - const end = countByDate[countByDate.length - 1][1].date; + const start = new Date(countByDate[0][1].date); + const end = new Date(countByDate[countByDate.length - 1][1].date); if (start.getTime() !== end.getTime()) { for (let date = start; date <= end; date.setMonth(date.getMonth() + 1)) { - if (!allYearMonthStrings.includes(formatYearMonth(date))) { + const formattedDate = formatYearMonth(date, { locale }); + if (!allYearMonthStrings.includes(formattedDate)) { countByDate.push([ - formatYearMonth(date), + formattedDate, { count: 0, - date, - monthStr: formatShortMonth(date), + date: new Date(date), + label: formattedDate, }, ]); } } } - countByDate.sort(([a], [b]) => b.localeCompare(a)); + countByDate.sort(([_a, a], [_b, b]) => b.date.getTime() - a.date.getTime()); return countByDate; }; @@ -245,12 +301,14 @@ const BaseStatsCard = ({ title, subtitle, data, + columnName = "Date", trend, }: { title: string; subtitle: string; - data: [string, { count: number; date: Date; monthStr: string }][]; - trend: { + data: [string, { count: number; label: string }][]; + columnName?: string; + trend?: { direction: "up" | "down"; lastMonthDailyAverage: number; previousThreeMonthsDailyAverage: number; @@ -274,36 +332,40 @@ const BaseStatsCard = ({ {title} - - - {trend.direction === "up" ? "Upward trend" : "Downward trend"} - - - Last 30 days daily average:{" "} - - {trend.lastMonthDailyAverage >= 10 - ? formatInteger(trend.lastMonthDailyAverage) - : trend.lastMonthDailyAverage.toFixed(2)} - - -
- - Last 90 days daily average:{" "} - - {trend.previousThreeMonthsDailyAverage >= 10 - ? formatInteger(trend.previousThreeMonthsDailyAverage) - : trend.previousThreeMonthsDailyAverage.toFixed(2)} - - - - } - > - - {trend.direction === "up" ? "📈" : "📉"} - -
+ {trend ? ( + + + {trend.direction === "up" + ? "Upward trend" + : "Downward trend"} + + + Last 30 days daily average:{" "} + + {trend.lastMonthDailyAverage >= 10 + ? formatInteger(trend.lastMonthDailyAverage) + : trend.lastMonthDailyAverage.toFixed(2)} + + +
+ + Last 90 days daily average:{" "} + + {trend.previousThreeMonthsDailyAverage >= 10 + ? formatInteger(trend.previousThreeMonthsDailyAverage) + : trend.previousThreeMonthsDailyAverage.toFixed(2)} + + + + } + > + + {trend.direction === "up" ? "📈" : "📉"} + +
+ ) : null} {subtitle} @@ -341,7 +403,7 @@ const BaseStatsCard = ({ variant="caption" sx={{ position: "sticky", top: 0, fontWeight: "bold" }} > - Date + {columnName} - {data.map(([dateStr, datum]) => ( - + {data.map(([key, datum]) => ( + ))} @@ -381,12 +443,11 @@ const BaseStatsCard = ({ }; const Bar = ({ - dateStr, - monthStr, + label, count, maxCount, }: ComponentProps["data"][number][1] & { - dateStr: string; + label: string; maxCount: number; }) => { const easterEgg = useFlag("easter-eggs"); @@ -401,7 +462,7 @@ const Bar = ({ }} > - {monthStr} {dateStr.split("-")[0]} + {label} { const { title, subtitle, countByDay, trendAverages } = props; + const locale = useLocale(); const { countByYearMonth, total } = useMemo(() => { return { - countByYearMonth: groupByYearMonth(countByDay), + countByYearMonth: groupByYearMonth(countByDay, { locale }), total: sum(countByDay, (d) => d.count) ?? 0, }; - }, [countByDay]); + }, [countByDay, locale]); const avgMonthlyCount = Math.round(total / countByYearMonth.length); const { lastMonthDailyAverage, previousThreeMonthsDailyAverage } = trendAverages; From 6a18330f6018d202e9b3bc8944a89dd7f96864d8 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 12:40:02 +0200 Subject: [PATCH 3/6] feat: Add most popular charts in the last 30 days --- app/pages/statistics.tsx | 49 +++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/app/pages/statistics.tsx b/app/pages/statistics.tsx index 280c5bba8..b5743d7e5 100644 --- a/app/pages/statistics.tsx +++ b/app/pages/statistics.tsx @@ -25,18 +25,23 @@ type StatProps = { type PageProps = { charts: StatProps & { - mostPopular: { + mostPopularAllTime: { key: string; createdDate: Date; viewCount: number; }[]; + mostPopularThisMonth: { + key: string; + viewCount: number; + }[]; }; views: StatProps; }; export const getServerSideProps: GetServerSideProps = async () => { const [ - mostPopularCharts, + mostPopularAllTimeCharts, + mostPopularThisMonthCharts, chartCountByDay, chartTrendAverages, viewCountByDay, @@ -69,6 +74,25 @@ export const getServerSideProps: GetServerSideProps = async () => { }; }) ), + prisma.$queryRaw` + SELECT config_key AS key, COUNT(*) AS view_count + FROM config_view + WHERE viewed_at > CURRENT_DATE - INTERVAL '30 days' + GROUP BY config_key + ORDER BY view_count DESC + LIMIT 10; + `.then((rows) => { + return ( + rows as { + key: string; + view_count: BigInt; + }[] + ).map((row) => ({ + key: row.key, + // superjson conversion breaks when we use default BigInt + viewCount: Number(row.view_count), + })); + }), prisma.$queryRaw` SELECT DATE_TRUNC('day', created_at) AS day, @@ -176,7 +200,8 @@ export const getServerSideProps: GetServerSideProps = async () => { return { props: serializeProps({ charts: { - mostPopular: mostPopularCharts, + mostPopularAllTime: mostPopularAllTimeCharts, + mostPopularThisMonth: mostPopularThisMonthCharts, countByDay: chartCountByDay, trendAverages: chartTrendAverages, }, @@ -190,6 +215,7 @@ export const getServerSideProps: GetServerSideProps = async () => { const Statistics = (props: Serialized) => { const { charts, views } = deserializeProps(props); + console.log("month", charts.mostPopularThisMonth); return ( @@ -235,11 +261,22 @@ const Statistics = (props: Serialized) => { } /> )} - {charts.mostPopular.length > 0 && ( + {charts.mostPopularAllTime.length > 0 && ( + [ + chart.key, + { count: chart.viewCount, label: chart.key }, + ])} + columnName="Chart" + /> + )} + {charts.mostPopularThisMonth.length > 0 && ( [ + data={charts.mostPopularThisMonth.map((chart) => [ chart.key, { count: chart.viewCount, label: chart.key }, ])} From 016391fce1cb8d98c35f5895e18ecaee290768f1 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 12:44:29 +0200 Subject: [PATCH 4/6] feat: Make it possible to preview most popular charts --- app/pages/statistics.tsx | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/app/pages/statistics.tsx b/app/pages/statistics.tsx index b5743d7e5..e86b686fb 100644 --- a/app/pages/statistics.tsx +++ b/app/pages/statistics.tsx @@ -1,11 +1,11 @@ /* eslint-disable visualize-admin/no-large-sx */ -import { Box, Card, Tooltip, Typography } from "@mui/material"; +import { Box, Card, Link, Tooltip, Typography } from "@mui/material"; import { max, rollups, sum } from "d3-array"; import { formatLocale } from "d3-format"; import { motion } from "framer-motion"; import uniq from "lodash/uniq"; import { GetServerSideProps } from "next"; -import { ComponentProps, useMemo } from "react"; +import { ComponentProps, ReactNode, useMemo } from "react"; import { AppLayout } from "@/components/layout"; import { BANNER_MARGIN_TOP } from "@/components/presence"; @@ -215,7 +215,7 @@ export const getServerSideProps: GetServerSideProps = async () => { const Statistics = (props: Serialized) => { const { charts, views } = deserializeProps(props); - console.log("month", charts.mostPopularThisMonth); + const locale = useLocale(); return ( @@ -267,7 +267,18 @@ const Statistics = (props: Serialized) => { subtitle="Top 10 charts by view count" data={charts.mostPopularAllTime.map((chart) => [ chart.key, - { count: chart.viewCount, label: chart.key }, + { + count: chart.viewCount, + label: ( + + {chart.key} + + ), + }, ])} columnName="Chart" /> @@ -278,7 +289,18 @@ const Statistics = (props: Serialized) => { subtitle="Top 10 charts by view count" data={charts.mostPopularThisMonth.map((chart) => [ chart.key, - { count: chart.viewCount, label: chart.key }, + { + count: chart.viewCount, + label: ( + + {chart.key} + + ), + }, ])} columnName="Chart" /> @@ -343,7 +365,7 @@ const BaseStatsCard = ({ }: { title: string; subtitle: string; - data: [string, { count: number; label: string }][]; + data: [string, { count: number; label: ReactNode }][]; columnName?: string; trend?: { direction: "up" | "down"; @@ -484,7 +506,7 @@ const Bar = ({ count, maxCount, }: ComponentProps["data"][number][1] & { - label: string; + label: ReactNode; maxCount: number; }) => { const easterEgg = useFlag("easter-eggs"); From 2cfe96824e3958fb86dba5fc5e5e7c1497feaff6 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 12:55:13 +0200 Subject: [PATCH 5/6] refactor: Extract ChartLink --- app/pages/statistics.tsx | 53 +++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/app/pages/statistics.tsx b/app/pages/statistics.tsx index e86b686fb..f853fe5fa 100644 --- a/app/pages/statistics.tsx +++ b/app/pages/statistics.tsx @@ -12,6 +12,7 @@ import { BANNER_MARGIN_TOP } from "@/components/presence"; import prisma from "@/db/client"; import { Serialized, deserializeProps, serializeProps } from "@/db/serialize"; import { useFlag } from "@/flags"; +import { Icon } from "@/icons"; import { Locale } from "@/locales/locales"; import { useLocale } from "@/src"; @@ -63,7 +64,7 @@ export const getServerSideProps: GetServerSideProps = async () => { _count: "desc", }, }, - take: 10, + take: 25, }) .then((rows) => rows.map((row) => { @@ -80,7 +81,7 @@ export const getServerSideProps: GetServerSideProps = async () => { WHERE viewed_at > CURRENT_DATE - INTERVAL '30 days' GROUP BY config_key ORDER BY view_count DESC - LIMIT 10; + LIMIT 25; `.then((rows) => { return ( rows as { @@ -264,20 +265,12 @@ const Statistics = (props: Serialized) => { {charts.mostPopularAllTime.length > 0 && ( [ chart.key, { count: chart.viewCount, - label: ( - - {chart.key} - - ), + label: , }, ])} columnName="Chart" @@ -286,20 +279,12 @@ const Statistics = (props: Serialized) => { {charts.mostPopularThisMonth.length > 0 && ( [ chart.key, { count: chart.viewCount, - label: ( - - {chart.key} - - ), + label: , }, ])} columnName="Chart" @@ -311,6 +296,30 @@ const Statistics = (props: Serialized) => { ); }; +const ChartLink = ({ + locale, + chartKey, +}: { + locale: Locale; + chartKey: string; +}) => { + return ( + + {chartKey} + + + ); +}; + export default Statistics; const formatYearMonth = (date: Date, { locale }: { locale: Locale }) => { From f7388ef629f6b5f335b0a4028a212e852f9c03ce Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 4 Oct 2024 13:06:24 +0200 Subject: [PATCH 6/6] docs: Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc49a2601..1c7b7f1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ You can also check the # Unreleased +- Features + - Added most recent charts (all time and last 30 days) to the the Statistics + page - Fixes - Fixed inconsistent text style behavior in title and description fields