......@@ -2,27 +2,26 @@
import { useEffect, useMemo, useState } from "react";
import * as d3 from "d3";
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() {
import { taxes } from '@/src/data/taxes';
import { CommuneData } from "@/src/type/CommuneData";
import { Departement } from "@/src/type/Departement";
import { years } from "@/src/data/years";
import Nuage_svg from "@/src/components/svg/Nuage_svg";
import { fetchDepartements, fetchCommuneStats } from "@/src/services/communeService";
import { useQuery } from "@tanstack/react-query";
export default function Nuage() {
const [year, setYear] = useState(2022);
const [taxe, setTaxe] = useState("cves");
const [departements, setDepartements] = useState<Departement[]>([]);
//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 [data, setData] = useState<CommuneData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [tooltip, setTooltip] = useState<{
x: number;
......@@ -30,7 +29,6 @@ export default function ScatterDepartement() {
data: CommuneData;
} | null>(null);
const width = 700;
const height = 420;
const margin = { top: 40, right: 30, bottom: 50, left: 60 };
......@@ -38,102 +36,39 @@ export default function ScatterDepartement() {
/**
* 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();
}, []);
const { data: departements = [] } = useQuery({
queryKey: ['departements'],
queryFn: fetchDepartements,
staleTime: Infinity, // Les départements ne changent jamais en théorie
});
// Sélectionne le premier département pour le select
useMemo(() => {
if (departements.length > 0 && !selectedDepartement) {
setSelectedDepartement(departements[0].nom);
}
}, [departements, selectedDepartement]);
/**
* Récupérer les taux et le volume collectés
* Récupérer les communes en fonction du départements
*/
const { data = [], isLoading } = useQuery({
queryKey: ['nuageData', taxe, selectedDepartement, year],
queryFn: () => fetchCommuneStats(taxe, selectedDepartement, year),
enabled: !!selectedDepartement, // attente d'un département
placeholderData: (prev) => prev,
staleTime: 1000 * 60 * 2, // Cache de 2 minutes
});
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]);
const communes = useMemo(() => {
return Array.from(new Set(data.map(d => d.commune)));
}, [data]);
/**
* Couleurs
*/
const communes = useMemo(() => {
return Array.from(new Set(data.map(d => d.commune)));
}, [data]);
const colorScale = useMemo(() => {
return d3.scaleOrdinal<string>()
......@@ -147,7 +82,7 @@ export default function ScatterDepartement() {
const xScale = useMemo(() => {
const domain = data.length
? d3.extent(data, d => d.taxRate) as [number, number]
: [0, 100]; // valeur par défaut
: [0, 100];
return d3.scaleLinear()
.domain(domain)
......@@ -158,7 +93,7 @@ export default function ScatterDepartement() {
const yScale = useMemo(() => {
const domain = data.length
? d3.extent(data, d => d.volume) as [number, number]
: [0, 1000000]; // valeur par défaut
: [0, 1000000];
return d3.scaleLinear()
.domain(domain)
......@@ -181,7 +116,7 @@ export default function ScatterDepartement() {
Relation taux d'imposition / Volume collecté
</h2>
{/* CONTROLS */}
{/* Options */}
<div className="mb-6 flex flex-wrap gap-4">
<select
......@@ -199,11 +134,7 @@ export default function ScatterDepartement() {
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>
{years.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<select
......@@ -223,139 +154,17 @@ export default function ScatterDepartement() {
{isLoading && <p className="text-gray-400">Chargement...</p>}
{/* SVG */}
<svg width={width} height={height} className="mx-auto block">
{!data.length && !isLoading && (
<text
x={width / 2}
y={height / 2}
textAnchor="middle"
fill="#9ca3af"
fontSize="16"
>
Aucune donnée pour cette sélection
</text>
)}
<Nuage_svg
data={data}
width={width}
height={height}
margin={margin}
hovered={hovered}
setHovered={setHovered}
setTooltip={setTooltip}
/>
{/* 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>
{yTicks.map(t => (
<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 */}
{data.map((d, i) => (
<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);
}}
/>
))}
</svg>
{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"
......@@ -377,25 +186,54 @@ export default function ScatterDepartement() {
</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>
{/* Légende */}
<div className="mt-8 border-t pt-6">
<div className="flex flex-wrap gap-x-4 gap-y-2 justify-center transition-all">
{(isExpanded ? communes : communes.slice(0, 30)).map(commune => (
<div
key={commune}
onMouseEnter={() => setHovered(commune)}
onMouseLeave={() => setHovered(null)}
className="flex items-center gap-2 cursor-pointer group"
style={{ width: 'fit-content' }} // Empêche certains comportements de saut
>
<span
className={`w-3 h-3 rounded-full transition-transform ${hovered === commune ? "scale-125" : "scale-100"}`}
style={{ backgroundColor: colorScale(commune) }}
/>
<span
className={`text-sm transition-colors ${hovered === commune
? "text-blue-600 font-medium" // On évite le bold extrême pour le saut de ligne
: "text-gray-500"
}`}
>
{commune}
</span>
</div>
))}
</div>
{/* Bouton Voir plus / Voir moins */}
{communes.length > 30 && (
<div className="flex justify-center mt-4">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800 transition-colors"
>
{isExpanded ? (
<>
Voir moins
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" /></svg>
</>
) : (
<>
Voir les {communes.length - 30} autres villes
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</>
)}
</button>
</div>
))}
)}
</div>
</div>
......
"use client";
import { useMemo, useState } from "react";
import { useQueries } from "@tanstack/react-query";
import * as d3 from "d3";
import { taxes } from '@/src/data/taxes';
import { taxeStats } from "@/src/type/TaxeStats";
import { years as allYears } from "@/src/data/years";
import Temporelle_svg from "../svg/Temporelle_svg";
import { fetchTaxeData } from "@/src/services/taxeStatService";
export default function Temporelle() {
const [taxe, setTaxe] = useState("cves");
const [minYear, setMinYear] = useState<number>(2018);
const [maxYear, setMaxYear] = useState<number>(2022);
const [activeRegion, setActiveRegion] = useState<string | null>(null);
const yearsToFetch = useMemo(() => d3.range(minYear, maxYear + 1), [minYear, maxYear]);
const results = useQueries({
queries: yearsToFetch.map(year => ({
queryKey: ['taxeYear', taxe, year],
queryFn: () => fetchTaxeData(taxe, year, "taux"),
staleTime: 1000 * 60 * 2, // 2 minutes
}))
});
// React-Query (cache)
const data = useMemo(() => {
return results.map(r => r.data).filter(Boolean).flat() as taxeStats[];
}, [results]);
const isLoading = results.some(r => r.isLoading);
// Régions
const regions = useMemo(() => {
return Array.from(new Set(data.map(d => d.region)));
}, [data]);
// Filtres
const startYears = useMemo(() => allYears.filter(y => y <= maxYear), [maxYear]);
const endYears = useMemo(() => allYears.filter(y => y >= minYear), [minYear]);
const colorScale = useMemo(() => {
return d3.scaleOrdinal<string>().domain(regions).range(d3.schemeTableau10);
}, [regions]);
return (
<section className="w-full max-w-4xl">
{isLoading ? (
<div className="h-96 w-full bg-gray-100 animate-pulse rounded-xl" />
) : (
<div className="rounded-xl bg-white text-gray-700 shadow-sm p-6">
<div className="flex flex-wrap items-center gap-4 mb-6">
<h2 className="text-lg font-bold">Taux moyen par région</h2>
<div className="flex items-center gap-2">
<label className="text-sm">Taxe</label>
<select
value={taxe}
onChange={e => setTaxe(e.target.value)}
className="border rounded px-2 py-1"
>
{taxes.map(t => <option key={t.route} value={t.route}>{t.label}</option>)}
</select>
</div>
<div className="flex items-center gap-2">
<label className="text-sm">De</label>
<select
value={minYear}
onChange={e => setMinYear(Number(e.target.value))}
className="border rounded px-2 py-1"
>
{startYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<label className="text-sm">à</label>
<select
value={maxYear}
onChange={e => setMaxYear(Number(e.target.value))}
className="border rounded px-2 py-1"
>
{endYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
</div>
<Temporelle_svg
data={data}
minYear={minYear}
maxYear={maxYear}
activeRegion={activeRegion}
setActiveRegion={setActiveRegion}
/>
{/* Légende */}
<div className="mt-6 flex flex-wrap gap-4 justify-center">
{regions.map(region => (
<div
key={region}
className="flex items-center gap-2 cursor-pointer transition-opacity"
onMouseEnter={() => setActiveRegion(region)}
onMouseLeave={() => setActiveRegion(null)}
style={{ opacity: activeRegion === null || activeRegion === region ? 1 : 0.3 }}
>
<span className="w-3 h-3 rounded-sm" style={{ backgroundColor: colorScale(region) }} />
<span className="text-sm">{region}</span>
</div>
))}
</div>
</div>
)}
</section>
);
}
\ No newline at end of file
"use client";
import { pie, arc, PieArcDatum } from "d3-shape";
import { useMemo } from "react";
import { taxeStats } from "@/src/type/TaxeStats";
import { DiagrammeProps } from "@/src/type/Props";
const COLORS = ["#2563eb", "#16a34a", "#ea580c", "#7c3aed", "#06b6d4", "#f43f5e"];
export default function Digramme_svg({
data,
hoveredRegion,
setHoveredRegion
}: DiagrammeProps) {
const totalSum = useMemo(
() => data.reduce((acc, curr) => acc + curr.value, 0),
[data]
);
const pieGenerator = pie<taxeStats>()
.value(d => d.value)
.sort(null);
const arcGenerator = arc<PieArcDatum<taxeStats>>()
.innerRadius(120)
.outerRadius(240)
.cornerRadius(4)
.padAngle(0.006);
const arcs = useMemo(() => pieGenerator(data), [data]);
return (
<div className="flex flex-col items-center">
{/* === SVG === */}
<svg width={350} height={350} viewBox="-240 -240 480 480">
{arcs.map((d, i) => {
const isActive =
hoveredRegion === null ||
hoveredRegion.region === d.data.region;
return (
<path
key={`arc-${d.data.region}`}
d={arcGenerator(d)!}
fill={COLORS[i % COLORS.length]}
opacity={isActive ? 1 : 0.3}
onMouseEnter={() => setHoveredRegion(d.data)}
onMouseLeave={() => setHoveredRegion(null)}
className="transition-all duration-300 cursor-pointer"
/>
);
})}
{/* Centre */}
{hoveredRegion && totalSum > 0 && (
<text textAnchor="middle" className="text-lg fill-gray-700">
<tspan className="font-bold text-xl" x="0" dy="0">{hoveredRegion.region}</tspan>
<tspan x="0" dy="1.2em">
{Math.round(hoveredRegion.value).toLocaleString()}
</tspan>
<tspan x="0" dy="1.2em">
{((hoveredRegion.value / totalSum) * 100).toFixed(1)}%
</tspan>
</text>
)}
</svg>
<div className="md:col-span-3 mt-4">
<div className="mt-6 flex flex-wrap gap-4 justify-center custom-scrollbar">
{arcs.map((d, i) => {
const percentage = totalSum > 0 ? ((d.data.value / totalSum) * 100).toFixed(1) : 0;
const isActive = hoveredRegion === null || hoveredRegion.region === d.data.region;
return (
<div
key={`legend-${d.data.region}`}
onMouseEnter={() => setHoveredRegion(d.data)}
onMouseLeave={() => setHoveredRegion(null)}
className={` flex items-center gap-2 p-2 rounded-md border transition-all ${isActive
? "bg-gray-50 shadow-sm scale-[1.02]"
: "border-transparent opacity-50 grayscale-[0.5]"
}`}>
<span
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: COLORS[i % COLORS.length] }}
/>
<div className="flex flex-col min-w-0">
<span className="text-xs font-bold text-gray-800 truncate">
{d.data.region}
</span>
<div className="flex items-center gap-2">
<span className="text-[10px] text-gray-600 font-semibold">{percentage}%</span>
<span className="text-[10px] text-gray-400 truncate">
{Math.round(d.data.value).toLocaleString()}
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}
"use client";
import * as d3 from "d3";
import { NuageProps } from "@/src/type/Props";
export default function Nuage_svg({
data,
width,
height,
margin,
hovered,
setHovered,
setTooltip
}: NuageProps) {
const communes = Array.from(new Set(data.map(d => d.commune)));
const colorScale = d3.scaleOrdinal<string>()
.domain(communes)
.range(d3.schemeTableau10);
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.taxRate) as [number, number] || [0, 100])
.nice()
.range([margin.left, width - margin.right]);
const yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.volume) as [number, number] || [0, 100000])
.nice()
.range([height - margin.bottom, margin.top]);
const xTicks = xScale.ticks(5);
const yTicks = yScale.ticks(5);
return (
<svg width={width} height={height} className="mx-auto block">
{/* 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"
/>
{/* Y ticks */}
{yTicks.map(t => (
<text
key={t}
x={margin.left - 10}
y={yScale(t)}
textAnchor="end"
alignmentBaseline="middle"
fontSize="11"
>
{d3.format(".2s")(t)}
</text>
))}
{/* X ticks */}
{xTicks.map(t => (
<text
key={t}
x={xScale(t)}
y={height - margin.bottom + 18}
textAnchor="middle"
fontSize="11"
>
{t.toFixed(1)}
</text>
))}
{/* Points */}
{data.map((d, i) => (
<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);
}}
/>
))}
</svg>
);
}
"use client";
import * as d3 from "d3";
import { useMemo } from "react";
import { taxeStats } from "@/src/type/TaxeStats";
import { TemporelleProps } from "@/src/type/Props";
export default function Temporelle_svg({
data,
minYear,
maxYear,
activeRegion,
setActiveRegion,
}: TemporelleProps) {
const width = 1100;
const height = 700;
const margin = { top: 60, right: 50, bottom: 60, left: 80 };
const yearsRange = useMemo(
() => d3.range(minYear, maxYear + 1),
[minYear, maxYear]
);
const groupedData = useMemo(
() => d3.group(data, d => d.region),
[data]
);
const regions = useMemo(
() => Array.from(new Set(data.map(d => d.region))),
[data]
);
const xScale = useMemo(() => {
return d3.scalePoint<number>()
.domain(yearsRange)
.range([margin.left, width - margin.right]);
}, [yearsRange]);
const yMax = d3.max(data, d => d.value) ?? 0;
const yScale = useMemo(() => {
return d3.scaleLinear()
.domain([0, yMax + 1])
.nice()
.range([height - margin.bottom, margin.top]);
}, [yMax]);
const yTicks = yScale.ticks(5);
const colorScale = useMemo(() => {
return d3.scaleOrdinal<string>()
.domain(regions)
.range(d3.schemeTableau10);
}, [regions]);
const line = d3.line<any>()
.x(d => xScale(d.year)!)
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
return (
<div className="w-full rounded-xl bg-white shadow-sm p-6">
<div className="w-full aspect-[16/10]">
<svg
viewBox={`0 0 ${width} ${height}`}
className="w-full h-full"
>
{/* Grid */}
{/* Axe Y */}
{yTicks.map(tick => (
<g key={`y-axis-${tick}`}>
{/* ligne */}
<line
x1={margin.left}
x2={width - margin.right}
y1={yScale(tick)}
y2={yScale(tick)}
stroke="#eee"
/>
{/* Le texte de la métrique */}
<text
x={margin.left - 10}
y={yScale(tick)}
dy="0.32em"
textAnchor="end"
fontSize="16"
fill="#666"
>
{tick}
</text>
</g>
))}
{/*Axe X */}
{yearsRange.map(year => (
<text
key={`x-axis-${year}`}
x={xScale(year)}
y={height - margin.bottom + 25}
textAnchor="middle"
fontSize="16"
fill="#666"
>
{year}
</text>
))}
{/* Lignes */}
{[...groupedData.entries()].map(([region, values]) => {
const isActive =
activeRegion === null || activeRegion === region;
return (
<path
key={region}
d={line(values)!}
fill="none"
stroke={colorScale(region)}
strokeWidth={isActive ? 3 : 1.5}
opacity={isActive ? 1 : 0.2}
onMouseEnter={() => setActiveRegion(region)}
onMouseLeave={() => setActiveRegion(null)}
className="transition-all duration-200"
/>
);
})}
{/* Points */}
{[...groupedData.entries()].flatMap(([region, values]) =>
values.map(d => {
const isActive =
activeRegion === null || activeRegion === region;
return (
<circle
key={`${region}-${d.year}`}
cx={xScale(d.year)}
cy={yScale(d.value)}
r={isActive ? 4 : 3}
fill={colorScale(region)}
opacity={isActive ? 1 : 0.3}
/>
);
})
)}
</svg>
</div>
</div>
);
}
export const years = [2018, 2019, 2020, 2021, 2022];
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, ReactNode } from "react";
export default function QueryProvider({ children }: { children: ReactNode }) {
// On utilise un useState pour s'assurer que le QueryClient
// n'est créé qu'une seule fois côté client
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
\ No newline at end of file
import { CommuneData } from "@/src/type/CommuneData";
import { Departement } from "@/src/type/Departement";
export const fetchDepartements = async (): Promise<Departement[]> => {
const res = await fetch(`https://localhost/departements`);
if (!res.ok) throw new Error("Erreur départements");
const apiData = await res.json();
return apiData.member.map((d: any) => ({ nom: d.nom }));
};
export const fetchCommuneStats = async (
taxe: string,
dep: string,
year: number
): Promise<CommuneData[]> => {
const res = await fetch(
`https://localhost/${taxe}?departement.nom=${dep}&annee=${year}&order[tauxNet]=desc&pagination=false`
);
if (!res.ok) throw new Error("Erreur stats communes");
const apiData = await res.json();
return (apiData.member || []).map((d: any) => ({
commune: d.nomCommune,
departement: dep,
year: d.annee,
taxType: taxe,
taxRate: d.tauxNet,
volume: d.montantReel,
}));
};
\ No newline at end of file
import { taxeStats } from "@/src/type/TaxeStats";
export const fetchTaxeData = async (taxe: string, year: number, metric: string): Promise<taxeStats[]> => {
const response = await fetch(
`https://localhost/${taxe}/stats?annee=${year}&groupBy=region&metric=${metric}`
);
if (!response.ok) {
throw new Error(`Erreur API pour l'année ${year}`);
}
const apiData = await response.json();
// On transforme le résultat pour qu'il corresponde à ton interface taxeStats
return apiData.member.map((d: any) => ({
region: d.label,
year: year,
value: d.value,
}));
};
\ No newline at end of file
import { CommuneData } from "@/src/type/CommuneData";
import { taxeStats } from "./TaxeStats";
export type NuageProps = {
data: CommuneData[];
width: number;
height: number;
margin: { top: number; right: number; bottom: number; left: number };
hovered: string | null;
setHovered: (c: string | null) => void;
setTooltip: (t: any) => void;
};
export type DiagrammeProps = {
data: taxeStats[];
hoveredRegion: taxeStats | null;
setHoveredRegion: (d: taxeStats | null) => void;
};
export type TemporelleProps = {
data: taxeStats[];
minYear: number;
maxYear: number;
activeRegion: string | null;
setActiveRegion: (region: string | null) => void;
};
\ No newline at end of file
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: "./vitest.setup.ts",
include: ["**/*.test.{ts,tsx}"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
}
},
});
import "@testing-library/jest-dom";
\ No newline at end of file