Files
Compass/web/components/widgets/charts.tsx
Martin Braquet ba9b3cfb06 Add pretty formatting (#29)
* Test

* Add pretty formatting

* Fix Tests

* Fix Tests

* Fix Tests

* Fix

* Add pretty formatting fix

* Fix

* Test

* Fix tests

* Clean typeckech

* Add prettier check

* Fix api tsconfig

* Fix api tsconfig

* Fix tsconfig

* Fix

* Fix

* Prettier
2026-02-20 17:32:27 +01:00

174 lines
5.4 KiB
TypeScript

import {useEffect, useState} from 'react'
import {Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'
import {useT} from 'web/lib/locale'
import {getCompletedProfilesCreations, getProfilesCreations} from 'web/lib/supabase/users'
// 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[]>([])
const [chartHeight, setChartHeight] = useState<number>(400)
const t = useT()
useEffect(() => {
// Set responsive chart height: 300px on small widths, 400px otherwise
function applyHeight() {
if (typeof window !== 'undefined') {
const isSmall = window.innerWidth < 420
setChartHeight(isSmall ? 320 : 400)
}
}
applyHeight()
window.addEventListener('resize', applyHeight)
return () => window.removeEventListener('resize', applyHeight)
}, [])
useEffect(() => {
async function load() {
const [allProfiles, complatedProfiles] = await Promise.all([
getProfilesCreations(),
getCompletedProfilesCreations(),
])
const countsAll = buildCounts(allProfiles)
const countsCompleted = buildCounts(complatedProfiles)
// Build a full daily date range from min to max date for equidistant time axis
const allDates = Object.keys(countsAll)
const completedDates = Object.keys(countsCompleted)
const minDateStr = [...allDates, ...completedDates].sort((a, b) => a.localeCompare(b))[0]
const maxDateStr = [...allDates, ...completedDates].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 cumCompleted = cumulativeFromCounts(countsCompleted, dates)
const merged = dates.map((date) => ({
date,
dateTs: new Date(date + 'T00:00:00.000Z').getTime(),
profilesCreations: cumAll[date] || 0,
profilesCompletedCreations: cumCompleted[date] || 0,
}))
setData(merged)
}
void load()
}, [])
// One LineChart with two Line series sharing the same data array
return (
<div>
<ResponsiveContainer width="100%" height={chartHeight}>
<LineChart data={data} margin={{top: 24, right: 16, bottom: 24, left: -20}}>
<text
x="50%"
y="24"
textAnchor="middle"
dominantBaseline="middle"
style={{
fontSize: '16px',
fontWeight: 600,
fill: 'rgb(var(--color-primary-900))',
}}
>
{t('stats.number_members', 'Number of Members')}
</text>
{/*<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: t('charts.date', 'Date'),
position: 'insideBottomRight',
offset: -5,
}}
/>
<YAxis />
<Tooltip
contentStyle={{
backgroundColor: 'rgb(var(--color-canvas-100))',
border: 'none',
borderRadius: '8px',
color: 'rgb(var(--color-primary-900))',
}}
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={t('stats.total', 'Total')}
stroke="rgb(var(--color-primary-900))"
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="profilesCompletedCreations"
name={t('stats.with_bio', 'Completed')}
stroke="#9ca3af"
strokeDasharray="4 2"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
)
}