Skip to content
GitLab
Projets
Groupes
Sujets
Extraits de code
/
Aide
Aide
Support
Forum de la communauté
Raccourcis clavier
?
Proposer une rétroaction
Contribuer à GitLab
Connexion
Activer/désactiver la navigation
Menu
Tristan Cherrier
web_fullstack_lab
Comparer les révisions
6b1d17cf75c2510fb0a6b0ac0ca1c5f3d2b07155...1ebf6154428a4ef82086edeedb75366541b915c2
Masquer les modifications d'espaces
En ligne
Côte à côte
my-app/components/Nuage/Nuage.tsx
→
my-app/
src/
components/Nuage/Nuage.tsx
Voir le fichier @
1ebf6154
...
...
@@ -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épartement
s
*/
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
>
...
...
my-app/src/components/Temporelle/Temporelle.tsx
0 → 100644
Voir le fichier @
1ebf6154
"
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
my-app/src/components/svg/Diagramme_svg.tsx
0 → 100644
Voir le fichier @
1ebf6154
"
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
>
);
}
my-app/src/components/svg/Nuage_svg.tsx
0 → 100644
Voir le fichier @
1ebf6154
"
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
>
);
}
my-app/src/components/svg/Temporelle_svg.tsx
0 → 100644
Voir le fichier @
1ebf6154
"
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
>
);
}
my-app/data/taxes.ts
→
my-app/
src/
data/taxes.ts
Voir le fichier @
1ebf6154
Fichier déplacé
my-app/src/data/years.ts
0 → 100644
Voir le fichier @
1ebf6154
export
const
years
=
[
2018
,
2019
,
2020
,
2021
,
2022
];
my-app/src/providers/QueryProvider.tsx
0 → 100644
Voir le fichier @
1ebf6154
"
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
my-app/src/services/communeService.tsx
0 → 100644
Voir le fichier @
1ebf6154
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
my-app/src/services/taxeStatService.tsx
0 → 100644
Voir le fichier @
1ebf6154
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
my-app/type/CommuneData.ts
→
my-app/
src/
type/CommuneData.ts
Voir le fichier @
1ebf6154
Fichier déplacé
my-app/type/Departement.ts
→
my-app/
src/
type/Departement.ts
Voir le fichier @
1ebf6154
Fichier déplacé
my-app/src/type/Props.ts
0 → 100644
Voir le fichier @
1ebf6154
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
my-app/type/TaxeStats.ts
→
my-app/
src/
type/TaxeStats.ts
Voir le fichier @
1ebf6154
Fichier déplacé
my-app/vitest.config.ts
0 → 100644
Voir le fichier @
1ebf6154
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
,
"
.
"
),
}
},
});
my-app/vitest.setup.ts
0 → 100644
Voir le fichier @
1ebf6154
import
"
@testing-library/jest-dom
"
;
\ No newline at end of file
Précédent
1
2
Suivant