Improve charts

This commit is contained in:
MartinBraquet
2025-10-10 21:31:30 +02:00
parent e756225d8b
commit a2b172ad58
3 changed files with 125 additions and 38 deletions

View File

@@ -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<string, number> = {}
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<string, number>, sortedDates: string[]) {
const out: Record<string, number> = {}
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<any[]>([])
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 (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3"/>
<XAxis dataKey="date" label={{value: "Date", position: "insideBottomRight", offset: -5}}/>
{/*<CartesianGrid strokeDasharray="3 3"/>*/}
<XAxis
dataKey="dateTs"
type="number"
scale="time"
domain={["dataMin", "dataMax"]}
tickFormatter={(ts) => new Date(ts).toISOString().split("T")[0]}
label={{value: "Date", position: "insideBottomRight", offset: -5}}
/>
<YAxis label={{value: "Number of Members", angle: -90, position: "insideLeft"}}/>
<Tooltip
contentStyle={{
@@ -52,14 +114,27 @@ export default function ChartComponent() {
labelStyle={{
color: "rgb(var(--color-primary-900))",
}}
labelFormatter={(value, payload) => (payload && payload[0] && payload[0].payload?.date) || new Date(value as number).toISOString().split("T")[0]}
/>
<Legend/>
<Line
type="monotone"
dataKey="profilesCreations"
name="Total"
stroke="rgb(var(--color-primary-900))"
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="value"
stroke="rgb(var(--color-primary-900))"
dataKey="profilesWithBioCreations"
name="With Bio"
stroke="#9ca3af"
strokeDasharray="4 2"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
);
)
}

View File

@@ -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
}

View File

@@ -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'}
>
<h1 className="text-3xl font-semibold text-center mb-6">Community Growth over Time</h1>
<ChartComponent/>
<ChartMembers/>
</LovePage>
);
}