mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 23:03:45 -04:00
Improve charts
This commit is contained in:
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user