Choix de carte dynamique, même pour les TOM #65

This commit is contained in:
Colin Maudry
2026-03-16 18:06:32 +01:00
parent adc8457abc
commit f531ce7091
2 changed files with 226 additions and 202 deletions
+182 -170
View File
@@ -1,4 +1,3 @@
import json
from typing import Literal
from urllib.error import HTTPError, URLError
@@ -11,57 +10,7 @@ import polars as pl
from dash import dash_table, dcc, html
from dash_extensions.javascript import Namespace
from src.utils import data_schema, df, format_number
def get_map_count_marches(dff: pl.DataFrame) -> go.Figure:
lff: pl.LazyFrame = dff.lazy()
lff = lff.rename({"acheteur_departement_code": "Département"})
lff = (
lff.select(["uid", "Département"])
.drop_nulls()
.group_by("uid")
.agg(pl.col("Département").first())
.group_by("Département")
.len("uid")
)
# Suppression des infos pour les DOM/TOM pour l'instant
lff = lff.filter(~pl.col("Département").str.head(2).is_in(["97", "98"]))
with open("./data/departements-1000m.geojson") as f:
departements = json.load(f)
# Ajout de feature.id
for f in departements["features"]:
f["id"] = f["properties"]["code"]
df_map = lff.collect(engine="streaming")
fig = px.choropleth(
df_map,
geojson=departements,
locations="Département",
color="uid",
color_continuous_scale="Reds",
title="Nombres de marchés attribués par département (lieu d'exécution)",
range_color=(df_map["uid"].min(), df_map["uid"].max()),
labels={"uid": "Marchés attribués"},
scope="europe",
width=900,
height=700,
)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_layout(
mapbox={
"style": "carto-positron",
"center": {"lon": 10, "lat": 10},
"zoom": 1,
"domain": {"x": [0, 1], "y": [0, 1]},
}
)
return fig
from src.utils import data_schema, departements_geojson, df, format_number
def get_yearly_statistics(statistics, today_str) -> html.Div:
@@ -82,11 +31,11 @@ def get_yearly_statistics(statistics, today_str) -> html.Div:
}
)
df = pl.DataFrame(data)
dff = pl.DataFrame(data)
# Create Dash DataTable
table = dash_table.DataTable(
data=df.to_dicts(),
data=dff.to_dicts(),
columns=[
{"name": "Année", "id": "Année"},
{"name": "Marchés et accord-cadres", "id": "Marchés et accord-cadres"},
@@ -429,141 +378,211 @@ def get_geographic_maps(dff: pl.DataFrame) -> list | None:
Génère les cartes géographiques pour la métropole et les DOM-TOM.
"""
# Si les données sont trop importantes on utilise une carte chloropleth
if dff.height > 50000:
return [
dbc.Col(
dcc.Graph(
figure=get_map_count_marches(dff), config={"displayModeBar": False}
),
width=12,
md=12,
className="mb-4",
)
]
regions: dict = {
"Métropole": {
"coordinates": [46.6, 2.2],
"zoom_leaflet": 5,
"zoom_chloropleth": 1,
"name": "Métropole",
},
"971": {
"coordinates": [16.23, -61.55],
"zoom_leaflet": 9,
"zoom_chloropleth": 1,
"name": "Guadeloupe",
},
"972": {
"coordinates": [14.64, -61.02],
"zoom_leaflet": 10,
"zoom_chloropleth": 1,
"name": "Martinique",
},
"973": {
"coordinates": [3.93, -53.12],
"zoom_leaflet": 7,
"zoom_chloropleth": 1,
"name": "Guyane",
},
"974": {
"coordinates": [-21.11, 55.53],
"zoom_leaflet": 9,
"zoom_chloropleth": 1,
"name": "La Réunion",
},
"976": {
"coordinates": [-12.82, 45.16],
"zoom_leaflet": 10,
"zoom_chloropleth": 1,
"name": "Mayotte",
},
}
# Liste des codes départements Outre-Mer
region_codes: list = ["Métropole", "971", "972", "973", "974", "976"]
dom_codes = region_codes[1:]
dom_codes = [code for code in regions.keys() if code != "Métropole"]
print("dom codes", dom_codes)
# Couleurs accessibles (Okabe-Ito)
color_acheteur = "#E69F00" # Orange
color_titulaire = "#56B4E9" # Bleu ciel
regions = {}
# Ajout des DOM s'ils ont des données
for code in region_codes:
if code == "Métropole":
dff_region = dff.filter(
def make_map_data(region_code: str) -> tuple[list, str or None]:
lff: pl.LazyFrame = dff.lazy()
if region_code == "Métropole":
lff = lff.filter(
(
~pl.col("acheteur_departement_code").is_in(dom_codes)
| (~pl.col("titulaire_departement_code").is_in(dom_codes))
)
)
else:
dff_region = dff.filter(
lff = lff.filter(
(pl.col("acheteur_departement_code") == code)
| (pl.col("titulaire_departement_code") == code)
)
if dff_region.height > 0:
if code == "971":
name = "Guadeloupe"
elif code == "972":
name = "Martinique"
elif code == "973":
name = "Guyane"
elif code == "974":
name = "La Réunion"
elif code == "976":
name = "Mayotte"
elif code == "Métropole":
name = "Métropole"
nb_marches = lff.select("uid").group_by("uid").first().collect().height
if nb_marches == 0:
return [], None
dfs = []
if (code == "Métropole" and nb_marches > 20000) or (
code != "Métropole" and nb_marches > 10000
):
_map_type: str = "chloropleth"
lff = lff.rename({"acheteur_departement_code": "Département"})
lff = (
lff.select(["uid", "Département"])
.drop_nulls()
.group_by("uid")
.agg(pl.col("Département").first())
.group_by("Département")
.len("uid")
)
dfs.append(lff.collect())
else:
name = f"Département {code}"
_map_type: str = "clusters"
for org_type in ["acheteur", "titulaire"]:
lff_org = (
lff.select(
"uid",
f"{org_type}_longitude",
f"{org_type}_latitude",
f"{org_type}_nom",
)
.group_by(
f"{org_type}_longitude",
f"{org_type}_latitude",
f"{org_type}_nom",
)
.len("nb_marches")
.filter(
pl.col(f"{org_type}_latitude").is_not_null()
& pl.col(f"{org_type}_longitude").is_not_null()
)
)
regions[name] = dff_region
markers = []
# Region centers for dash-leaflet
region_centers = {
"Métropole": ([46.6, 2.2], 5),
"Guadeloupe": ([16.23, -61.55], 9),
"Martinique": ([14.64, -61.02], 10),
"Guyane": ([3.93, -53.12], 7),
"La Réunion": ([-21.11, 55.53], 9),
"Mayotte": ([-12.82, 45.16], 10),
# Couleurs accessibles (Okabe-Ito)
colors = {
"acheteur": "#E69F00", # orange
"titulaire": "#56B4E9", # bleu ciel
}
for row in lff_org.collect().to_dicts():
markers.append(
{
"lat": row[f"{org_type}_latitude"],
"lon": row[f"{org_type}_longitude"],
"tooltip": f"{row[f'{org_type}_nom']} ({row['nb_marches']} marchés)",
"marker_color": colors[org_type],
}
)
dfs.append(markers)
return dfs, _map_type
cols = []
for code in regions.keys():
regions[code]["data"], map_type = make_map_data(code)
print("region", regions[code]["name"], len(regions[code]["data"][0]), map_type)
if map_type == "chloropleth":
map_graph = make_chloropleth_map(regions[code]) # call chloropleth function
elif map_type == "clusters":
map_graph = make_clusters_map(regions[code])
elif map_type is None:
continue
else:
raise ValueError(f"Map type '{map_type}' not recognised")
col = dbc.Col(
children=[
html.H5(regions[code]["name"]),
map_graph,
],
md=6 if code == "Métropole" else 3,
width=12,
className="mb-4",
)
cols.append(col)
return cols
def make_chloropleth_map(region: dict) -> dcc.Graph:
df_map = region["data"][0]
fig = px.choropleth(
df_map,
geojson=departements_geojson,
locations="Département",
color="uid",
color_continuous_scale="Reds",
title="Nombres de marchés attribués",
range_color=(df_map["uid"].min(), df_map["uid"].max()),
labels={"uid": "Marchés attribués"},
scope="europe",
width=400,
height=400,
)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_layout(
mapbox={
"style": "carto-positron",
"center": {"lon": 10, "lat": 10},
"zoom": 8,
"domain": {"x": [0, 1], "y": [0, 1]},
}
)
graph = dcc.Graph(figure=fig, config={"displayModeBar": False})
return graph
def make_clusters_map(region: dict):
# JavaScript functions for styling
ns = Namespace("dash_clientside", "leaflet")
point_to_layer = ns("pointToLayer")
cluster_to_layer = ns("clusterToLayer")
cols = []
for name, region_df in regions.items():
# Trace Acheteurs
mask_acheteur = (
region_df.select(
"uid", "acheteur_longitude", "acheteur_latitude", "acheteur_nom"
)
.group_by("acheteur_longitude", "acheteur_latitude", "acheteur_nom")
.len("nb_marches")
.filter(
pl.col("acheteur_latitude").is_not_null()
& pl.col("acheteur_longitude").is_not_null()
)
)
name = region["name"]
acheteurs_marker_dicts = []
# Données de la région
region_acheteurs = region["data"][0]
region_titulaires = region["data"][1]
if mask_acheteur.height > 0:
for row in mask_acheteur.to_dicts():
acheteurs_marker_dicts.append(
{
"lat": row["acheteur_latitude"],
"lon": row["acheteur_longitude"],
"tooltip": f"{row['acheteur_nom']} ({row['nb_marches']} marchés)",
"marker_color": color_acheteur,
}
)
# Couleurs
color_acheteur = region_acheteurs[0]["marker_color"]
color_titulaire = region_titulaires[0]["marker_color"]
# Trace Titulaires
mask_titulaire = (
region_df.select(
"uid", "titulaire_longitude", "titulaire_latitude", "titulaire_nom"
)
.group_by("titulaire_longitude", "titulaire_latitude", "titulaire_nom")
.len("nb_marches")
.filter(
pl.col("titulaire_latitude").is_not_null()
& pl.col("titulaire_longitude").is_not_null()
)
)
acheteurs_geojson_data = dlx.dicts_to_geojson(region_acheteurs)
titulaires_geojson_data = dlx.dicts_to_geojson(region_titulaires)
titulaires_marker_dicts = []
if mask_titulaire.height > 0:
for row in mask_titulaire.to_dicts():
titulaires_marker_dicts.append(
{
"lat": row["titulaire_latitude"],
"lon": row["titulaire_longitude"],
"tooltip": f"{row['titulaire_nom']} ({row['nb_marches']} marchés)",
"marker_color": color_titulaire,
}
)
acheteurs_geojson_data = dlx.dicts_to_geojson(acheteurs_marker_dicts)
titulaires_geojson_data = dlx.dicts_to_geojson(titulaires_marker_dicts)
center, zoom = region_centers.get(name, ([46.6, 2.2], 6))
col_width = 6 if name == "Métropole" else 3
center, zoom = region["coordinates"], region["zoom_leaflet"]
region_id = name.lower().replace(" ", "-")
cols.append(
dbc.Col(
[
html.H5(name),
dl.Map(
leaflet_map = dl.Map(
[
dl.TileLayer(),
dl.GeoJSON(
@@ -592,15 +611,8 @@ def get_geographic_maps(dff: pl.DataFrame) -> list | None:
"height": "400px" if name == "Métropole" else "300px",
},
id=f"map-{region_id}",
),
],
width=12,
md=col_width,
className="mb-4",
)
)
return cols
return leaflet_map
def make_column_picker(page: str):
+12
View File
@@ -286,6 +286,17 @@ def get_departements() -> dict:
return data
def get_departements_geojson() -> dict:
with open("./data/departements-1000m.geojson") as f:
geojson = json.load(f)
# Ajout de feature.id
for f in geojson["features"]:
f["id"] = f["properties"]["code"]
return geojson
def get_departement_region(code_postal):
if code_postal > "97000":
code_departement = code_postal[:3]
@@ -774,6 +785,7 @@ df_titulaires_marches: pl.DataFrame = (
)
departements = get_departements()
departements_geojson = get_departements_geojson()
domain_name = (
"test.decp.info" if os.getenv("DEVELOPMENT").lower() == "true" else "decp.info"
)