Scaffold charts & graphs
This commit is contained in:
@@ -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
27
client/pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
106
client/src/components/UserCountGraph.tsx
Normal file
106
client/src/components/UserCountGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal file
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user