Newer
Older
import { useEffect, useMemo, useState } from "react";
import {taxes} from '@/data/taxes';
import { CommuneData } from "@/type/CommuneData";
import { Departement } from "@/type/Departement";
/**
* TODO :
* - déplacer les années tous
* - déplacer svg
*/
export default function ScatterDepartement() {
const [year, setYear] = useState(2022);
const [taxe, setTaxe] = useState("cves");
const [departements, setDepartements] = useState<Departement[]>([]);
const [selectedDepartement, setSelectedDepartement] = useState<string>("");
const [hovered, setHovered] = useState<string | null>(null);
const [data, setData] = useState<CommuneData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [tooltip, setTooltip] = useState<{
x: number;
y: number;
data: CommuneData;
} | null>(null);
const width = 700;
const height = 420;
const margin = { top: 40, right: 30, bottom: 50, left: 60 };
/**
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
* Récupérer tous les départements
*/
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(`https://localhost/departements`);
if (!res.ok) throw new Error("Erreur API");
const apiData = await res.json();
const formatted: Departement[] = apiData.member.map((d: any) => ({
nom: d.nom
}));
setDepartements(formatted);
if (formatted.length > 0) {
setSelectedDepartement(formatted[0].nom);
}
} catch (err) {
console.error("Erreur fetch", err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
/**
* Récupérer les taux et le volume collectés
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
useEffect(() => {
if (!selectedDepartement) return;
const fetchData = async () => {
setIsLoading(true);
try {
const res = await fetch(
`https://localhost/${taxe}?page=1&departement.nom=${selectedDepartement}&annee=${year}&order[tauxNet]=desc`
);
if (!res.ok) throw new Error("Erreur API data");
const apiData = await res.json();
console.log(apiData);
const members = Array.isArray(apiData.member) ? apiData.member : [];
if (members.length === 0) {
setData([]);
return;
}
const formatted: CommuneData[] = members.map((d: any) => ({
commune: d.nomCommune,
departement: selectedDepartement,
year: d.annee,
taxType: taxe,
taxRate: d.tauxNet,
volume: d.montantReel,
}));
for (let x = 0; x < 5; x++) {
console.log(formatted[x]);
}
setData(formatted);
} catch (err) {
console.error("Erreur fetch data:", err);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [taxe, selectedDepartement, year]);
* Couleurs
*/
const communes = useMemo(() => {
return Array.from(new Set(data.map(d => d.commune)));
}, [data]);
const colorScale = useMemo(() => {
return d3.scaleOrdinal<string>()
.domain(communes)
.range(d3.schemeTableau10);
}, [communes]);
/**
* Scales
const domain = data.length
? d3.extent(data, d => d.taxRate) as [number, number]
: [0, 100]; // valeur par défaut
.nice()
.range([margin.left, width - margin.right]);
const domain = data.length
? d3.extent(data, d => d.volume) as [number, number]
: [0, 1000000]; // valeur par défaut
.nice()
.range([height - margin.bottom, margin.top]);
}, [data]);
if (!xScale || !yScale) return <p>Chargement...</p>;
const xTicks = xScale.ticks(5);
const yTicks = yScale.ticks(5);
return (
<div className="rounded-xl bg-white shadow-sm p-6">
<h2 className="text-2xl font-semibold text-gray-800 mb-1">
Relation taux d'imposition / Volume collecté
value={taxe}
onChange={e => setTaxe(e.target.value)}
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500m"
{taxes.map(t => (
<option key={t.route} value={t.route}>{t.label}</option>
))}
</select>
<select
value={year}
onChange={e => setYear(+e.target.value)}
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
<option value={2018}>2018</option>
<option value={2019}>2019</option>
<option value={2020}>2020</option>
<option value={2021}>2021</option>
<option value={2022}>2022</option>
</select>
<select
value={selectedDepartement}
onChange={e => setSelectedDepartement(e.target.value)}
className="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
{departements.map(dep => (
<option key={dep.nom} value={dep.nom}>
{dep.nom}
</option>
))}
{isLoading && <p className="text-gray-400">Chargement...</p>}
{/* SVG */}
<svg width={width} height={height} className="mx-auto block">
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
{!data.length && !isLoading && (
<text
x={width / 2}
y={height / 2}
textAnchor="middle"
fill="#9ca3af"
fontSize="16"
>
Aucune donnée pour cette sélection
</text>
)}
{/* Label X */}
<text
x={width / 2}
y={height - 10}
textAnchor="middle"
fontSize="12"
fill="#374151"
>
Taux d'imposition (%)
</text>
{/* Label Y */}
<text
transform={`rotate(-90)`}
x={-height / 2}
y={20}
textAnchor="middle"
fontSize="12"
fill="#374151"
>
Volume collecté (€)
</text>
<g key={t}>
<line
x1={margin.left - 6}
x2={margin.left}
y1={yScale(t)}
y2={yScale(t)}
stroke="#6b7280"
/>
<text
x={margin.left - 10}
y={yScale(t) + 4}
textAnchor="end"
fontSize="11"
fill="#374151"
>
{d3.format(".2s")(t)}
</text>
</g>
{xTicks.map(t => (
<g key={t}>
<line
x1={xScale(t)}
x2={xScale(t)}
y1={height - margin.bottom}
y2={height - margin.bottom + 6}
stroke="#6b7280"
/>
<text
x={xScale(t)}
y={height - margin.bottom + 18}
textAnchor="middle"
fontSize="11"
fill="#374151"
>
{t.toFixed(1)}
</text>
</g>
))}
{/* Axes */}
<line
x1={margin.left}
x2={margin.left}
y1={margin.top}
y2={height - margin.bottom}
stroke="#9ca3af"
/>
<line
x1={margin.left}
x2={width - margin.right}
y1={height - margin.bottom}
y2={height - margin.bottom}
stroke="#9ca3af"
/>
{/* Points */}
<circle
key={i}
cx={xScale(d.taxRate)}
cy={yScale(d.volume)}
r={hovered === d.commune ? 8 : 5}
fill={colorScale(d.commune)}
opacity={hovered && hovered !== d.commune ? 0.2 : 0.9}
className="transition-all duration-200 cursor-pointer"
onMouseEnter={(e) => {
setHovered(d.commune);
const rect = (e.target as SVGCircleElement).getBoundingClientRect();
setTooltip({
x: rect.x + rect.width / 2,
y: rect.y,
data: d
});
}}
onMouseLeave={() => {
setHovered(null);
setTooltip(null);
}}
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
{tooltip && (
<div
className="fixed z-50 bg-white shadow-lg rounded-lg px-4 py-2 text-sm border border-gray-200 pointer-events-none"
style={{
left: tooltip.x,
top: tooltip.y - 10,
transform: "translate(-50%, -100%)"
}}
>
<div className="font-semibold text-gray-800">
{tooltip.data.commune}
</div>
<div className="text-gray-600">
Taux : {tooltip.data.taxRate.toFixed(2)} %
</div>
<div className="text-gray-600">
Volume : {d3.format(",")(tooltip.data.volume)} €
</div>
</div>
)}
{/* légende */}
<div className="mt-6 flex flex-wrap gap-4 justify-center">
{communes.map(commune => (
<div
key={commune}
onMouseEnter={() => setHovered(commune)}
onMouseLeave={() => setHovered(null)}
className="flex items-center gap-2 cursor-pointer transition-all"
>
<span
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: colorScale(commune) }}
/>
<span
className={`text-sm ${hovered === commune ? "font-semibold text-gray-800" : "text-gray-500"}`} >
{commune}
</span>
</div>
))}
</div>