diff --git a/.gitignore b/.gitignore index b34a663d8a7dd4dccfda4a2ceba5cee39de7c567..f776f2d6383e8338da856c647e6741b069d36fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /data/raw/*.txt -/data/raw/*.csv -/data/processed/*.csv \ No newline at end of file +/data/raw/*.csv \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index a547bf36d8d11a4f89c59c144f24795749086dd1..d9011fa206ef7f0410afe725360c1e70714781c5 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +/data/processed/* diff --git a/backend/data/water_scores_2024_filtered.csv b/backend/data/water_scores_2024_filtered.csv new file mode 100644 index 0000000000000000000000000000000000000000..1b7b9cb4b28d9fb33ba90e00ab9586212fcc0875 --- /dev/null +++ b/backend/data/water_scores_2024_filtered.csv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2326a32a95b66002324a181324b8aec7f4bf8106b54ff15dfed1906d7d3eea +size 21067232 diff --git a/backend/index.js b/backend/index.js index 87a4295cf8687ecfe951b409e8bb01033fa7f131..89d9ac4f976e481a49cf230a97ec13b607bdbeec 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,3 +1,5 @@ +const { generateDepartmentAveragesAndColors } = require('./indicateurs.js'); + const express = require("express"); const cors = require("cors"); @@ -6,31 +8,69 @@ app.use(cors()); const PORT = 5000; -// Fonction pour générer des couleurs aléatoires par département -const generateFakeIndicators = () => { - const departments = Array.from({ length: 95 }, (_, i) => (i + 1).toString().padStart(2, "0")); - const colors = [ - "#E69F00", // Jaune doré - "#56B4E9", // Bleu clair - "#009E73", // Vert foncé - "#F0E442", // Jaune vif - "#D55E00", // Orange vif - "#CC79A7" // Rose fuchsia - ]; - - // Ajoute la corse - departments.push("2A", "2B"); - - return departments.reduce((acc, dept) => { - acc[dept] = colors[Math.floor(Math.random() * colors.length)]; - return acc; - }, {}); -}; - -app.get("/api/indicateurs", (req, res) => { - res.json(generateFakeIndicators()); + + +app.get("/api/indicateurs", async (req, res) => { + try { + const { averages, colors } = await generateDepartmentAveragesAndColors("score_global"); + res.json({ + averages, + colors + }); + } catch (error) { + res.status(500).send('Erreur lors du traitement du fichier CSV'); + } +}); + +app.get("/api/indicateursPH", async (req, res) => { + try { + const { averages, colors } = await generateDepartmentAveragesAndColors("score_pH"); + res.json({ + averages, + colors + }); + } catch (error) { + res.status(500).send('Erreur lors du traitement du fichier CSV'); + } +}); + +app.get("/api/indicateursChlore", async (req, res) => { + try { + const { averages, colors } = await generateDepartmentAveragesAndColors("score_Chlore"); + res.json({ + averages, + colors + }); + } catch (error) { + res.status(500).send('Erreur lors du traitement du fichier CSV'); + } +}); + +app.get("/api/indicateursNitritesNitrates", async (req, res) => { + try { + const { averages, colors } = await generateDepartmentAveragesAndColors("score_Nitrites_Nitrates"); + res.json({ + averages, + colors + }); + } catch (error) { + res.status(500).send('Erreur lors du traitement du fichier CSV'); + } }); +app.get("/api/indicateursMetauxLourds", async (req, res) => { + try { + const { averages, colors } = await generateDepartmentAveragesAndColors("score_Metaux_Lourds"); + res.json({ + averages, + colors + }); + } catch (error) { + res.status(500).send('Erreur lors du traitement du fichier CSV'); + } +}); + + app.listen(PORT, () => { console.log(`Serveur lancé sur http://localhost:${PORT}`); }); diff --git a/backend/indicateurs.js b/backend/indicateurs.js new file mode 100644 index 0000000000000000000000000000000000000000..dd157fa2c6391e8b3cff1141a3657644e7644df8 --- /dev/null +++ b/backend/indicateurs.js @@ -0,0 +1,64 @@ + +const fs = require("fs"); +const csv = require("csv-parser"); + +// Fonction pour générer la couleur en fonction du score global +const getColorForScore = (score) => { + if (score >= 0.8) return "#4CAF50"; // Vert moyen (très bonne qualité) + if (score >= 0.5) return "#FFEB3B"; // Jaune (acceptable, à surveiller) + if (score >= 0.2) return "#FF9800"; // Orange (médiocre) + return "#F44336"; // Rouge (très pollué) +}; + +// Fonction pour lire le CSV et calculer les scores moyens par département +const generateDepartmentAveragesAndColors = (scoreType) => { + return new Promise((resolve, reject) => { + const departmentScores = {}; + const departmentCounts = {}; + + fs.createReadStream('data/water_scores_2024_filtered.csv') + .pipe(csv()) + .on('data', (row) => { + let dept = row.cddept_x.replace('.0', ''); // Supprimer ".0" si présent + + // Traitement spécial pour la Corse (02A et 02B) + if (dept.startsWith('02A') || dept.startsWith('02B')) { + // Supprimer le "0" initial pour obtenir "2A" ou "2B" + dept = dept.substring(1); // Résultat : "2A" ou "2B" + } else { + // Formater les autres codes pour qu'ils aient 2 chiffres + dept = dept.padStart(2, '0'); + } + + const scoreGlobal = parseFloat(row[scoreType]); + + if (!departmentScores[dept]) { + departmentScores[dept] = 0; + departmentCounts[dept] = 0; + } + + departmentScores[dept] += scoreGlobal; + departmentCounts[dept]++; + }) + .on('end', () => { + const results = { + averages: {}, + colors: {} + }; + + for (const dept in departmentScores) { + const avgScore = departmentScores[dept] / departmentCounts[dept]; + results.averages[dept] = avgScore.toFixed(2); // Moyenne avec 2 décimales + results.colors[dept] = getColorForScore(avgScore); + } + + console.log('CSV fichier traité'); + resolve(results); + }) + .on('error', (err) => { + reject(err); + }); + }); +}; + +module.exports = { generateDepartmentAveragesAndColors }; \ No newline at end of file diff --git a/data/processed/water_scores_2024.csv b/data/processed/water_scores_2024.csv new file mode 100644 index 0000000000000000000000000000000000000000..4bdf8ac40bd95dc35927c644ecec7e25a67641e2 --- /dev/null +++ b/data/processed/water_scores_2024.csv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b02156d17680402e09debc60617efe8018aae5cd84a511a78d24257c8f1d2b8 +size 72152990 diff --git a/data/processed/water_scores_2024_filtered.csv b/data/processed/water_scores_2024_filtered.csv new file mode 100644 index 0000000000000000000000000000000000000000..1b7b9cb4b28d9fb33ba90e00ab9586212fcc0875 --- /dev/null +++ b/data/processed/water_scores_2024_filtered.csv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d2326a32a95b66002324a181324b8aec7f4bf8106b54ff15dfed1906d7d3eea +size 21067232 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c586098e381a28815c8daf5749c3fb1d71197376..416446a7fc9f92c44116924b091ef7a898b7ea32 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,9 +1,22 @@ +import { useState } from "react"; import FranceMap from './components/FranceMap'; import MetricsComponent from "./components/MetricsComponent"; import TitleWithSearchComponent from "./components/UpBar"; import CheckboxGroup from "./components/CheckBoxGroup"; // Importer le groupe de checkboxes const App = () => { + // État pour stocker le paramètre sélectionné + const [selectedParam, setSelectedParam] = useState("indicateurs"); + + // Fonction pour gérer le changement de la case sélectionnée + const handleCheckboxChange = (param) => { + console.log("Changement dans App, nouveau paramètre sélectionné :", param); // Log du changement + setSelectedParam(param); // Mise à jour de l'état avec le paramètre sélectionné + }; + + + console.log("Paramètre sélectionné :", selectedParam); + return ( <div> <TitleWithSearchComponent /> @@ -11,16 +24,22 @@ const App = () => { {/* Partie gauche - Paramètres */} <div style={{ flex: 1, height: '100%', padding: '20px' }}> <h2>Paramètres</h2> - <CheckboxGroup /> {/* Utiliser le groupe de checkboxes */} + {/* Passer selectedParam et handleCheckboxChange à CheckboxGroup */} + <CheckboxGroup + onCheckboxChange={handleCheckboxChange} // Ici on passe handleCheckboxChange correctement + selectedParam={selectedParam} // Et on passe également selectedParam si nécessaire + /> + </div> {/* Partie droite - Carte */} - <div style={{ flex: 6, minWidth: '320px', height: '100%' }}> - <FranceMap /> + <div style={{ flex: 5, minWidth: '320px', height: '100%' }}> + {/* Passer selectedParam à FranceMap */} + <FranceMap typeIndic={selectedParam} /> </div> {/* Partie droite - Metrics */} - <div style={{ flex: 3, minWidth: '300px', height: '100%' }}> + <div style={{ flex: 2, minWidth: '300px', height: '100%' }}> <MetricsComponent /> </div> </div> @@ -28,4 +47,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/CheckBoxGroup.jsx b/frontend/src/components/CheckBoxGroup.jsx index d33d79ff97bc6b067e207e923138e3b587d11d64..647f1303b1c941bb329db08e857c158e53945927 100644 --- a/frontend/src/components/CheckBoxGroup.jsx +++ b/frontend/src/components/CheckBoxGroup.jsx @@ -1,52 +1,57 @@ -// CheckboxGroup.js import { useState } from "react"; +import PropTypes from 'prop-types'; import CheckboxWithLabel from "./checkBox"; -const CheckboxGroup = () => { - // On utilise ici une seule valeur pour l'état, car seulement une case peut être sélectionnée - const [selectedParam, setSelectedParam] = useState(null); - - // Fonction pour gérer le changement de la case sélectionnée - const handleCheckboxChange = (param) => { - setSelectedParam(param); // Mise à jour de l'état avec le paramètre sélectionné - }; - - return ( - <div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> - <CheckboxWithLabel - label="Tout" - isChecked={selectedParam === "all"} // Vérifie si cette case est sélectionnée - onChange={() => handleCheckboxChange("all")} - name="all" - /> - <CheckboxWithLabel - label="pH" - isChecked={selectedParam === "pH"} - onChange={() => handleCheckboxChange("pH")} - name="pH" - /> +const CheckboxGroup = ({ onCheckboxChange }) => { + // État pour stocker la checkbox cochée + const [selectedParam, setSelectedParam] = useState(null); - <CheckboxWithLabel - label="Chlore" - isChecked={selectedParam === "chlore"} - onChange={() => handleCheckboxChange("chlore")} - name="chlore" - /> - <CheckboxWithLabel - label="Nitrates & Nitrites" - isChecked={selectedParam === "nit"} - onChange={() => handleCheckboxChange("nit")} - name="nitrates_nitrites" - /> - <CheckboxWithLabel - label="Métaux lourds" - isChecked={selectedParam === "metaux"} - onChange={() => handleCheckboxChange("metaux")} - name="metaux_lourds" - /> - </div> - ); + // Fonction pour gérer le changement de la case sélectionnée + const handleCheckboxChange = (param) => { + console.log("Changement de paramètre dans CheckboxGroup :", param); // Log de changement + setSelectedParam(param); // Met à jour l'état local + onCheckboxChange(param); // Notifie le parent de la nouvelle valeur }; + + + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}> + <CheckboxWithLabel + label="Tout" + isChecked={selectedParam === "indicateurs"} // Vérifie si cette case est sélectionnée + onChange={() => handleCheckboxChange("indicateurs")} + name="indicateurs" + /> + <CheckboxWithLabel + label="pH" + isChecked={selectedParam === "indicateursPH"} + onChange={() => handleCheckboxChange("indicateursPH")} + name="indicateursPH" + /> + <CheckboxWithLabel + label="Chlore" + isChecked={selectedParam === "indicateursChlore"} + onChange={() => handleCheckboxChange("indicateursChlore")} + name="indicateursChlore" + /> + <CheckboxWithLabel + label="Nitrates & Nitrites" + isChecked={selectedParam === "indicateursNitritesNitrates"} + onChange={() => handleCheckboxChange("indicateursNitritesNitrates")} + name="indicateursNitritesNitrates" + /> + <CheckboxWithLabel + label="Métaux lourds" + isChecked={selectedParam === "indicateursMetauxLourds"} + onChange={() => handleCheckboxChange("indicateursMetauxLourds")} + name="indicateursMetauxLourds" + /> + </div> + ); +}; +CheckboxGroup.propTypes = { + onCheckboxChange: PropTypes.func.isRequired, +}; -export default CheckboxGroup; +export default CheckboxGroup; \ No newline at end of file diff --git a/frontend/src/components/FranceMap.jsx b/frontend/src/components/FranceMap.jsx index cd239d6a9723118ca392cdf37e63c62911dd6a43..80ed9b7927c9eb64376ee1f84b6f1d8d8fb9889d 100644 --- a/frontend/src/components/FranceMap.jsx +++ b/frontend/src/components/FranceMap.jsx @@ -1,10 +1,20 @@ import { useEffect, useState, useRef } from "react"; +import PropTypes from "prop-types"; import axios from "axios"; import * as d3 from "d3"; -const FranceMap = () => { +const FranceMap = ({ typeIndic = "indicateurs" }) => { + + if (typeof typeIndic !== "string") { + console.error("typeIndic doit être une chaîne de caractères, reçu :", typeIndic); + typeIndic = "indicateurs"; // Valeur par défaut + } + + console.log("typeIndic reçu :", typeIndic); + const [geoData, setGeoData] = useState(null); const [deptColors, setDeptColors] = useState({}); + const [deptAverages, setDeptAverages] = useState({}); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [selectedDept, setSelectedDept] = useState(null); const svgRef = useRef(); @@ -12,29 +22,33 @@ const FranceMap = () => { // Charger les données GeoJSON useEffect(() => { + console.log("url : ", "http://localhost:5000/api/" + typeIndic); axios - .get("/France_dep.geojson") // Assure-toi que ton fichier est accessible + .get("/France_dep.geojson") .then((res) => { setGeoData(res.data.features); }) .catch((err) => console.error("Erreur lors du chargement du GeoJSON :", err)); // Charger les couleurs depuis l'API backend - axios.get("http://localhost:5000/api/indicateurs") + axios.get("http://localhost:5000/api/" + typeIndic) .then((res) => { - console.log("Couleurs reçues du backend :", res.data); // Debug ici - setDeptColors(res.data); + console.log("Couleurs reçues du backend :", res.data.colors); + setDeptColors(res.data.colors); + console.log("Moyennes reçues du backend :", res.data.averages); + setDeptAverages(res.data.averages); + }) .catch((err) => console.error("Erreur lors du chargement des couleurs :", err)); - }, []); + }, [typeIndic]); // Calculer la taille du conteneur parent et mettre à jour les dimensions useEffect(() => { const updateDimensions = () => { const { clientWidth, clientHeight } = containerRef.current; setDimensions({ - width: clientWidth * 0.8, - height: clientHeight * 0.8, + width: clientWidth * 0.75, + height: clientHeight * 0.75, }); }; @@ -82,7 +96,7 @@ const FranceMap = () => { .attr("opacity", (dept) => dept.properties.code === d.properties.code || dept === selectedDept ? 1 : 0.25); tooltip.style("display", "block") - .html(`<strong>${d.properties.nom}</strong>`) + .html(`<strong>${d.properties.nom} (${d.properties.code})</strong><br>Score moyen : ${deptAverages[d.properties.code] || "N/A"}`) .style("left", event.pageX + 3 + "px") .style("top", event.pageY + 3 + "px"); }) @@ -105,7 +119,7 @@ const FranceMap = () => { } return deptColors[d.properties.code] || "lightgray"; }); - }, [geoData, deptColors, dimensions, selectedDept]); + }, [geoData, deptColors, dimensions, selectedDept, deptAverages]); return ( <div ref={containerRef} style={{ position: "relative", width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center" }}> @@ -115,5 +129,8 @@ const FranceMap = () => { </div> ); }; +FranceMap.propTypes = { + typeIndic: PropTypes.string, +}; export default FranceMap; diff --git a/frontend/src/components/UpBar.jsx b/frontend/src/components/UpBar.jsx index 34a027ac9cea2b440ea09a626eeaf684306f55f2..77b7e63811d949320c64ffb5af3246c8b4d48834 100644 --- a/frontend/src/components/UpBar.jsx +++ b/frontend/src/components/UpBar.jsx @@ -16,42 +16,6 @@ const TitleWithSearchComponent = () => { fontSize: '1.8rem', fontWeight: 'bold', }}>Dashboard France</h1> - - {/* Barre de recherche */} - <div style={{ - display: 'flex', - alignItems: 'center', - backgroundColor: 'white', - borderRadius: '25px', - padding: '5px 15px', - width: '350px', - boxShadow: '0 2px 5px rgba(0, 0, 0, 0.1)' - }}> - <input - type="text" - placeholder="Rechercher..." - style={{ - border: 'none', - outline: 'none', - flex: 1, - padding: '8px', - fontSize: '1rem', - borderRadius: '25px', - marginRight: '10px' - }} - /> - <button style={{ - backgroundColor: '#1c49ba', - border: 'none', - color: 'white', - padding: '8px 15px', - fontSize: '1rem', - borderRadius: '25px', - cursor: 'pointer' - }}> - Rechercher - </button> - </div> </div> ); };