[frontend]: Add services heatmap

This commit is contained in:
2025-03-17 22:56:33 +00:00
parent 7f51a55410
commit cf65559435
5 changed files with 84 additions and 44 deletions

View File

@ -8,16 +8,19 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@react-leaflet/core": "^3.0.0",
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.0.9",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"heatmap.js": "^2.0.5",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.heat": "^0.2.0",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-leaflet": "^5.0.0-rc.2", "react-leaflet": "^5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0", "react-leaflet-markercluster": "^5.0.0-rc.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",
"recharts": "^2.15.1", "recharts": "^2.15.1",
@ -3094,6 +3097,11 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/heatmap.js": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/heatmap.js/-/heatmap.js-2.0.5.tgz",
"integrity": "sha512-CG2gYFP5Cv9IQCXEg3ZRxnJDyAilhWnQlAuHYGuWVzv6mFtQelS1bR9iN80IyDmFECbFPbg6I0LR5uAFHgCthw=="
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -3645,6 +3653,11 @@
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/leaflet.heat": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/leaflet.heat/-/leaflet.heat-0.2.0.tgz",
"integrity": "sha512-Cd5PbAA/rX3X3XKxfDoUGi9qp78FyhWYurFg3nsfhntcM/MCNK08pRkf4iEenO1KNqwVPKCmkyktjW3UD+h9bQ=="
},
"node_modules/leaflet.markercluster": { "node_modules/leaflet.markercluster": {
"version": "1.5.3", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",

View File

@ -10,16 +10,19 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@react-leaflet/core": "^3.0.0",
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.0.9",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"heatmap.js": "^2.0.5",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.heat": "^0.2.0",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-leaflet": "^5.0.0-rc.2", "react-leaflet": "^5.0.0",
"react-leaflet-markercluster": "^5.0.0-rc.0", "react-leaflet-markercluster": "^5.0.0-rc.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",
"recharts": "^2.15.1", "recharts": "^2.15.1",

View File

@ -1,11 +1,13 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import ObjectTypeProportionPieChart from "./charts/ObjectTypeProportionPieChart"; import ObjectTypeProportionPieChart from "./charts/ObjectTypeProportionPieChart";
import LoadingOverlay from "./LoadingOverlay.jsx"; import LoadingOverlay from "./LoadingOverlay.jsx";
import HeatmapContainer from "./charts/HeatmapContainer";
const Statistics = () => { const Statistics = () => {
const [transientTypes, setTransientTypes] = useState([]); const [transientTypes, setTransientTypes] = useState([]);
const [trainTypes, setTrainTypes] = useState([]); const [trainTypes, setTrainTypes] = useState([]);
const [trainStatuses, setTrainStatuses] = useState([]); const [trainStatuses, setTrainStatuses] = useState([]);
const [coordinates, setCoordinates] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
@ -20,55 +22,27 @@ const Statistics = () => {
let transientTypes = []; let transientTypes = [];
let trainTypes = []; let trainTypes = [];
let trainStatuses = []; let trainStatuses = [];
let coords = [];
for (const item of transientData) { for (const item of transientData) {
transientTypes.push(item.objectType.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')); transientTypes.push(item.objectType.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2'));
if (item.latitude && item.longitude) {
coords.push([item.latitude, item.longitude]);
}
switch (item.objectType) { if (item.objectType === "IrishRailTrain") {
case "IrishRailTrain": let trainType = item.trainType === "M" ? "Mainline" : item.trainType === "S" ? "Suburban" : item.trainType === "D" ? "DART" : "Unknown";
let trainType; trainTypes.push(trainType);
switch (item.trainType) {
case "M":
trainType = "Mainline";
break;
case "S":
trainType = "Suburban";
break;
case "D":
trainType = "DART";
break;
default:
trainType = "Unknown";
}
trainTypes.push(trainType);
let trainStatus; let trainStatus = item.trainStatus === "R" ? "Running" : item.trainStatus === "T" ? "Terminated" : item.trainStatus === "N" ? "Not yet running" : "Unknown";
switch (item.trainStatus) { trainStatuses.push(trainStatus);
case "R":
trainStatus = "Running";
break;
case "T":
trainStatus = "Terminated";
break;
case "N":
trainStatus = "Not yet running";
break;
default:
trainStatus = "Unknown";
}
trainStatuses.push(trainStatus);
break;
} }
} }
setTransientTypes(transientTypes); setTransientTypes(transientTypes);
setTrainStatuses(trainStatuses); setTrainStatuses(trainStatuses);
setTrainTypes(trainTypes); setTrainTypes(trainTypes);
setCoordinates(coords);
} catch (err) { } catch (err) {
setError("Failed to fetch data"); setError("Failed to fetch data");
} finally { } finally {
@ -98,23 +72,28 @@ const Statistics = () => {
> >
<div <div
className="mx-auto px-4 flex flex-wrap gap-4 pt-[4vh] justify-center"> className="mx-auto px-4 flex flex-wrap gap-4 pt-[4vh] justify-center">
<div className="bg-white shadow-md rounded-lg p-4">
<HeatmapContainer coordinates={coordinates}/>
</div>
<div className="bg-white shadow-md rounded-lg p-4"> <div className="bg-white shadow-md rounded-lg p-4">
<ObjectTypeProportionPieChart <ObjectTypeProportionPieChart
label={`Transport Types`} label={`Live Transport Types`}
dataList={transientTypes} dataList={transientTypes}
/> />
</div> </div>
<div className="bg-white shadow-md rounded-lg p-4"> <div className="bg-white shadow-md rounded-lg p-4">
<ObjectTypeProportionPieChart <ObjectTypeProportionPieChart
label={`Train Types`} label={`Live Train Types`}
dataList={trainTypes} dataList={trainTypes}
/> />
</div> </div>
<div className="bg-white shadow-md rounded-lg p-4"> <div className="bg-white shadow-md rounded-lg p-4">
<ObjectTypeProportionPieChart <ObjectTypeProportionPieChart
label={`Train Statuses`} label={`Live Train Statuses`}
dataList={trainStatuses} dataList={trainStatuses}
/> />
</div> </div>

View File

@ -0,0 +1,45 @@
import React, { useEffect } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet.heat";
const Heatmap = ({ data }) => {
const map = useMap();
useEffect(() => {
if (!map || data.length === 0) return;
const heatmapLayer = L.heatLayer(data, {
radius: 20,
blur: 15,
maxZoom: 17,
}).addTo(map);
return () => {
map.removeLayer(heatmapLayer);
};
}, [map, data]);
return null;
};
const HeatmapContainer = ({ coordinates }) => {
return (
<div className="flex flex-col items-center p-4 bg-white shadow-lg rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold text-center mb-4">Service Density Heatmap</h2>
<MapContainer
center={[53.4494762, -7.5029786]}
zoom={6}
minZoom={4}
maxBounds={[[150, -50], [0, 50]]}
maxBoundsViscosity={0.3}
style={{ height: "400px", width: "400px" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Heatmap data={coordinates} />
</MapContainer>
</div>
);
};
export default HeatmapContainer;

View File

@ -18,7 +18,7 @@ const ObjectTypeProportionPieChart = ({ label, dataList }) => {
return ( return (
<div className="flex flex-col items-center p-4 bg-white shadow-lg rounded-lg w-full max-w-md"> <div className="flex flex-col items-center p-4 bg-white shadow-lg rounded-lg w-full max-w-md">
<h2 className="text-xl font-semibold text-center mb-4">{label}</h2> <h2 className="text-xl font-semibold text-center mb-4">{label}</h2>
<ResponsiveContainer width={350} height={300}> <ResponsiveContainer width={400} height={400}>
<PieChart> <PieChart>
<Pie <Pie
data={chartData} data={chartData}