Scaffold charts & graphs

This commit is contained in:
2024-08-01 13:55:40 +08:00
parent d6212f6c77
commit fc8ccb0eea
5 changed files with 253 additions and 20 deletions

View File

@@ -13,11 +13,13 @@
"@internationalized/date": "^3.5.5",
"@nextui-org/react": "^2.4.2",
"axios": "^1.7.2",
"chart.js": "^4.4.3",
"dayjs": "^1.11.12",
"formik": "^2.4.6",
"framer-motion": "^11.2.10",
"openai": "^4.53.2",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.23.1",

27
client/pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ dependencies:
axios:
specifier: ^1.7.2
version: 1.7.2
chart.js:
specifier: ^4.4.3
version: 4.4.3
dayjs:
specifier: ^1.11.12
version: 1.11.12
@@ -29,6 +32,9 @@ dependencies:
react:
specifier: ^18.2.0
version: 18.3.1
react-chartjs-2:
specifier: ^5.2.0
version: 5.2.0(chart.js@4.4.3)(react@18.3.1)
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
@@ -689,6 +695,10 @@ packages:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
/@kurkle/color@0.3.2:
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
dev: false
/@nextui-org/accordion@2.0.35(@nextui-org/system@2.2.2)(@nextui-org/theme@2.2.6)(framer-motion@11.2.10)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-42T8DAgpICKORry5h1UCgAQ71QJ3dCzvqrnnJQco3LICeIER2JT/wEdpxHUVT893MkL6z6CFsJmWNfFJPk59kA==}
peerDependencies:
@@ -3648,6 +3658,13 @@ packages:
supports-color: 7.2.0
dev: true
/chart.js@4.4.3:
resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==}
engines: {pnpm: '>=8'}
dependencies:
'@kurkle/color': 0.3.2
dev: false
/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@@ -4747,6 +4764,16 @@ packages:
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
/react-chartjs-2@5.2.0(chart.js@4.4.3)(react@18.3.1):
resolution: {integrity: sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
chart.js: 4.4.3
react: 18.3.1
dev: false
/react-dom@18.3.1(react@18.3.1):
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:

View File

@@ -0,0 +1,106 @@
import { useEffect, useState } from "react";
import instance from "../security/http";
import config from "../config";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ChartOptions,
} from "chart.js";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
export default function UserCountGraph() {
const [userInformationCollection, setUserInformationCollection] = useState<
any[]
>([]);
const [chartData, setChartData] = useState<any>(null);
const retrieveUserInformationCollection = () => {
instance.get(`${config.serverAddress}/users/all`).then((values) => {
setUserInformationCollection(values.data);
});
};
useEffect(() => {
retrieveUserInformationCollection();
}, []);
useEffect(() => {
if (userInformationCollection.length > 0) {
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
const daysCount = new Array(31).fill(0); // 记录过去一个月每天的用户注册数量
userInformationCollection.forEach((user) => {
const createdAt = new Date(user.createdAt);
if (createdAt > lastMonth) {
const dayIndex = Math.floor(
(createdAt.getTime() - lastMonth.getTime()) / (1000 * 60 * 60 * 24)
);
daysCount[dayIndex]++;
}
});
const labels = Array.from({ length: 31 }, (_, i) => {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - (30 - i));
return currentDate.toISOString().split("T")[0]; // 返回日期字符串 YYYY-MM-DD
});
setChartData({
labels: labels,
datasets: [
{
label: "User Onboard",
data: daysCount,
fill: false,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
},
],
});
}
}, [userInformationCollection]);
// 定义图表选项
const options: ChartOptions<"line"> = {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: Math.max(
10,
Math.ceil(Math.max(...(chartData?.datasets[0]?.data || [10])))
), // 动态设置最大值最低为10
ticks: {
stepSize: 1, // 设置步长为1
},
},
},
};
return (
<div>
{chartData ? (
<Line data={chartData} options={options} />
) : (
<p>Loading...</p>
)}
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { useEffect, useState } from "react";
import instance from "../security/http";
import config from "../config";
import { Pie } from "react-chartjs-2";
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
// Register Chart.js components
ChartJS.register(ArcElement, Tooltip, Legend);
export default function UserTownCouncilDistributionChart() {
const [userInformationCollection, setUserInformationCollection] = useState<
any[]
>([]);
const [townCouncilInformation, setTownCouncilInformation] = useState<
string[]
>([]);
const [chartData, setChartData] = useState<any>(null);
const retrieveInformation = () => {
instance.get(`${config.serverAddress}/users/all`).then((values) => {
setUserInformationCollection(values.data);
});
instance
.get(`${config.serverAddress}/users/town-councils-metadata`)
.then((values) => {
setTownCouncilInformation(JSON.parse(values.data).townCouncils);
});
};
const generateChartData = () => {
const townCouncilCounts: { [key: string]: number } = {};
userInformationCollection.forEach((user: any) => {
const townCouncil = user.townCouncil || "Unknown";
if (townCouncilCounts[townCouncil]) {
townCouncilCounts[townCouncil]++;
} else {
townCouncilCounts[townCouncil] = 1;
}
});
const labels = Object.keys(townCouncilCounts);
const data = Object.values(townCouncilCounts);
const chartData = {
labels,
datasets: [
{
data,
backgroundColor: labels.map(
(label, index) => `hsl(${index * (360 / labels.length)}, 70%, 50%)`
),
},
],
};
setChartData(chartData);
};
useEffect(() => {
retrieveInformation();
}, []);
useEffect(() => {
if (
userInformationCollection.length > 0 &&
townCouncilInformation.length > 0
) {
generateChartData();
}
}, [userInformationCollection, townCouncilInformation]);
const options = {
responsive: true,
plugins: {
legend: {
position: "left" as const,
labels: {
font: {
size: 20,
},
color: "black",
},
},
tooltip: {
callbacks: {
label: function (tooltipItem: any) {
const dataset = tooltipItem.dataset;
const dataIndex = tooltipItem.dataIndex;
const value = dataset.data[dataIndex];
return `${chartData.labels[dataIndex]}: ${value}`;
},
},
},
},
};
return (
<div>
{chartData ? (
<Pie data={chartData} options={options} />
) : (
<p>Loading...</p>
)}
</div>
);
}

View File

@@ -6,6 +6,8 @@ import UserProfilePicture from "../components/UserProfilePicture";
import { Button, Card } from "@nextui-org/react";
import { PencilSquareIcon } from "../icons";
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
import UserCountGraph from "../components/UserCountGraph";
import UserTownCouncilDistributionChart from "../components/UserTownCouncilDistributionChart";
export default function AdministratorSpringboard() {
const navigate = useNavigate();
@@ -75,28 +77,18 @@ export default function AdministratorSpringboard() {
<Card className="w-full bg-primary-500 p-8 text-white rounded-r-none">
<div className="flex flex-col gap-4">
<p className="font-bold text-4xl">Statistics Overview</p>
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
<div className="w-60 flex flex-col justify-between">
<div className="flex flex-col">
<p className="text-2xl">User Count</p>
<p className="opacity-70">(past 30 days)</p>
</div>
<p className="text-lg">
Total: <span className="font-bold">2139</span> users
</p>
</div>
<div className="w-full h-full bg-white rounded-xl">
{/* TODO: Graph */}
<p className="text-black">GRAPH HERE</p>
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
<p className="text-3xl">Users Onboard</p>
<div className="w-full p-4 bg-white rounded-xl">
<UserCountGraph />
</div>
</div>
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
<div className="w-60 flex flex-col justify-between">
<p className="text-2xl">Population Distribution</p>
</div>
<div className="w-full h-full bg-white rounded-xl">
{/* TODO: Graph */}
<p className="text-black">GRAPH HERE</p>
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
<p className="text-3xl">Population Distribution</p>
<div className="w-full bg-white rounded-xl">
<div className="w-[600px] mx-auto">
<UserTownCouncilDistributionChart />
</div>
</div>
</div>
</div>