From a2b172ad588aa2a61c68b9a89109c2f1896961b7 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Fri, 10 Oct 2025 21:31:30 +0200 Subject: [PATCH] Improve charts --- web/components/widgets/charts.tsx | 145 ++++++++++++++++++++++-------- web/lib/supabase/users.ts | 14 ++- web/pages/charts.tsx | 4 +- 3 files changed, 125 insertions(+), 38 deletions(-) diff --git a/web/components/widgets/charts.tsx b/web/components/widgets/charts.tsx index 4aad2569..978a8c76 100644 --- a/web/components/widgets/charts.tsx +++ b/web/components/widgets/charts.tsx @@ -1,46 +1,108 @@ import {useEffect, useState} from "react"; -import {CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; -import {getUserCreations} from "web/lib/supabase/users"; +import {Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts"; +import {getProfilesCreations, getProfilesWithBioCreations} from "web/lib/supabase/users"; -export default function ChartComponent() { - const [data, setData] = useState([]); +// Helper to convert rows into date -> count map +function buildCounts(rows: any[]) { + const counts: Record = {} + for (const r of rows) { + const date = new Date(r.created_time).toISOString().split('T')[0] + counts[date] = (counts[date] || 0) + 1 + } + return counts +} + +// Helper to turn count map into cumulative by sorted date array +function cumulativeFromCounts(counts: Record, sortedDates: string[]) { + const out: Record = {} + let prev = 0 + for (const d of sortedDates) { + const v = counts[d] || 0 + prev += v + out[d] = prev + } + return out +} + + +export default function ChartMembers() { + const [data, setData] = useState([]) useEffect(() => { - async function loadData() { - // Load some data from the backend API or Supabase - const data = await getUserCreations() - const counts: { [date: string]: number } = {} - data.forEach((d) => { - const date = new Date(d.created_time).toISOString().split('T')[0] - counts[date] = (counts[date] || 0) + 1 - }) - const json: any = Object.entries(counts).map(([date, value]) => ({date, value})) - let prev = 0 - for (const e of json) { - e.value += prev - prev = e.value - } - json.sort((a: any, b: any) => a.date.localeCompare(b.date)) + async function load() { + const [allProfiles, bioProfiles] = await Promise.all([ + getProfilesCreations(), + getProfilesWithBioCreations(), + ]) - // Example static data - // const json: any = [ - // { date: '2023-01-01', value: 400 }, - // { date: '2023-02-01', value: 300 }, - // { date: '2023-03-01', value: 500 }, - // { date: '2023-04-01', value: 200 }, - // { date: '2023-05-01', value: 600 }, - // ] - setData(json); + const countsAll = buildCounts(allProfiles) + const countsBio = buildCounts(bioProfiles) + + // Build a full daily date range from min to max date for equidistant time axis + const allDates = Object.keys(countsAll) + const bioDates = Object.keys(countsBio) + const minDateStr = [ + ...allDates, + ...bioDates, + ].sort((a, b) => a.localeCompare(b))[0] + const maxDateStr = [ + ...allDates, + ...bioDates, + ].sort((a, b) => b.localeCompare(a))[0] + + function toISODate(d: Date) { + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) + .toISOString() + .split('T')[0] + } + + function addDays(d: Date, days: number) { + const nd = new Date(d) + nd.setUTCDate(nd.getUTCDate() + days) + return nd + } + + function buildDailyRange(startStr: string, endStr: string) { + const out: string[] = [] + const start = new Date(startStr + 'T00:00:00.000Z') + const end = new Date(endStr + 'T00:00:00.000Z') + for (let d = start; d <= end; d = addDays(d, 1)) { + out.push(toISODate(d)) + } + return out + } + + const dates = buildDailyRange(minDateStr, maxDateStr) + + const cumAll = cumulativeFromCounts(countsAll, dates) + const cumBio = cumulativeFromCounts(countsBio, dates) + + const merged = dates.map((date) => ({ + date, + dateTs: new Date(date + 'T00:00:00.000Z').getTime(), + profilesCreations: cumAll[date] || 0, + profilesWithBioCreations: cumBio[date] || 0, + })) + + setData(merged) } - loadData(); - }, []); + void load() + }, []) + // One LineChart with two Line series sharing the same data array return ( - - + {/**/} + new Date(ts).toISOString().split("T")[0]} + label={{value: "Date", position: "insideBottomRight", offset: -5}} + /> (payload && payload[0] && payload[0].payload?.date) || new Date(value as number).toISOString().split("T")[0]} + /> + + - ); + ) } diff --git a/web/lib/supabase/users.ts b/web/lib/supabase/users.ts index c9b8fdc1..7b7de738 100644 --- a/web/lib/supabase/users.ts +++ b/web/lib/supabase/users.ts @@ -3,6 +3,7 @@ import {run} from 'common/supabase/utils' import {APIError, api} from 'web/lib/api' import {unauthedApi} from 'common/util/api' import type {DisplayUser} from 'common/api/user-types' +import {MIN_BIO_LENGTH} from "common/constants"; export type {DisplayUser} @@ -56,11 +57,22 @@ export async function getDisplayUsers(userIds: string[]) { return data as unknown as DisplayUser[] } -export async function getUserCreations() { +export async function getProfilesCreations() { const {data} = await run( db.from('profiles') .select(`id, created_time`) .order('created_time') ) return data +} + +export async function getProfilesWithBioCreations() { + const {data} = await run( + db + .from('profiles') + .select(`id, created_time`) + .gt('bio_length', MIN_BIO_LENGTH) + .order('created_time') + ) + return data } \ No newline at end of file diff --git a/web/pages/charts.tsx b/web/pages/charts.tsx index 07240940..25d637b6 100644 --- a/web/pages/charts.tsx +++ b/web/pages/charts.tsx @@ -1,5 +1,5 @@ import {LovePage} from "web/components/love-page"; -import ChartComponent from "web/components/widgets/charts"; +import ChartMembers from "web/components/widgets/charts"; export default function Charts() { return ( @@ -7,7 +7,7 @@ export default function Charts() { trackPageView={'charts'} >

Community Growth over Time

- + ); }