diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c176e17..0937479 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,16 +8,19 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@react-leaflet/core": "^3.0.0", "@tailwindcss/vite": "^4.0.9", "autoprefixer": "^10.4.20", + "heatmap.js": "^2.0.5", "js-cookie": "^3.0.5", "leaflet": "^1.9.4", + "leaflet.heat": "^0.2.0", "leaflet.markercluster": "^1.5.3", "postcss": "^8.5.3", "react": "^19.0.0", "react-dom": "^19.0.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-router-dom": "^7.3.0", "recharts": "^2.15.1", @@ -3094,6 +3097,11 @@ "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": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3645,6 +3653,11 @@ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", "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": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 700ce04..cfcf2cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,16 +10,19 @@ "preview": "vite preview" }, "dependencies": { + "@react-leaflet/core": "^3.0.0", "@tailwindcss/vite": "^4.0.9", "autoprefixer": "^10.4.20", + "heatmap.js": "^2.0.5", "js-cookie": "^3.0.5", "leaflet": "^1.9.4", + "leaflet.heat": "^0.2.0", "leaflet.markercluster": "^1.5.3", "postcss": "^8.5.3", "react": "^19.0.0", "react-dom": "^19.0.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-router-dom": "^7.3.0", "recharts": "^2.15.1", diff --git a/frontend/src/components/Statistics.jsx b/frontend/src/components/Statistics.jsx index 668dce8..cc70f82 100644 --- a/frontend/src/components/Statistics.jsx +++ b/frontend/src/components/Statistics.jsx @@ -1,11 +1,13 @@ import React, { useState, useEffect } from "react"; import ObjectTypeProportionPieChart from "./charts/ObjectTypeProportionPieChart"; import LoadingOverlay from "./LoadingOverlay.jsx"; +import HeatmapContainer from "./charts/HeatmapContainer"; const Statistics = () => { const [transientTypes, setTransientTypes] = useState([]); const [trainTypes, setTrainTypes] = useState([]); const [trainStatuses, setTrainStatuses] = useState([]); + const [coordinates, setCoordinates] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -20,55 +22,27 @@ const Statistics = () => { let transientTypes = []; let trainTypes = []; let trainStatuses = []; + let coords = []; 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')); + if (item.latitude && item.longitude) { + coords.push([item.latitude, item.longitude]); + } - switch (item.objectType) { - case "IrishRailTrain": - let 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); + if (item.objectType === "IrishRailTrain") { + let trainType = item.trainType === "M" ? "Mainline" : item.trainType === "S" ? "Suburban" : item.trainType === "D" ? "DART" : "Unknown"; + trainTypes.push(trainType); - let trainStatus; - switch (item.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; + let trainStatus = item.trainStatus === "R" ? "Running" : item.trainStatus === "T" ? "Terminated" : item.trainStatus === "N" ? "Not yet running" : "Unknown"; + trainStatuses.push(trainStatus); } } setTransientTypes(transientTypes); setTrainStatuses(trainStatuses); setTrainTypes(trainTypes); - + setCoordinates(coords); } catch (err) { setError("Failed to fetch data"); } finally { @@ -98,23 +72,28 @@ const Statistics = () => { >
+ +
+ +
+
diff --git a/frontend/src/components/charts/HeatmapContainer.jsx b/frontend/src/components/charts/HeatmapContainer.jsx new file mode 100644 index 0000000..c2f5196 --- /dev/null +++ b/frontend/src/components/charts/HeatmapContainer.jsx @@ -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 ( +
+

Service Density Heatmap

+ + + + +
+ ); +}; + +export default HeatmapContainer; diff --git a/frontend/src/components/charts/ObjectTypeProportionPieChart.jsx b/frontend/src/components/charts/ObjectTypeProportionPieChart.jsx index a5514db..c837c00 100644 --- a/frontend/src/components/charts/ObjectTypeProportionPieChart.jsx +++ b/frontend/src/components/charts/ObjectTypeProportionPieChart.jsx @@ -18,7 +18,7 @@ const ObjectTypeProportionPieChart = ({ label, dataList }) => { return (

{label}

- +