Utilisation de cluster de marqueurs grâce à dash-leaflet #65

This commit is contained in:
Colin Maudry
2026-03-14 00:28:02 +01:00
parent f0f9d8cb3d
commit 302d253e6e
4 changed files with 116 additions and 60 deletions
+3 -1
View File
@@ -17,7 +17,9 @@ dependencies = [
"plotly[express]",
"httpx",
"pandas", # utilisé pour la création de certains graphiques
"unidecode"
"unidecode",
"dash-leaflet",
"dash-extensions"
]
[project.optional-dependencies]
+23
View File
@@ -1,4 +1,27 @@
window.dash_clientside = Object.assign({}, window.dash_clientside, {
leaflet: {
pointToLayer: function (feature, latlng, context) {
return L.circleMarker(latlng, {
radius: 5,
fillColor: feature.properties.marker_color,
color: "white",
weight: 1,
opacity: 1,
fillOpacity: 0.8,
}).bindTooltip(feature.properties.tooltip);
},
clusterToLayer: function (feature, latlng, index, context) {
const count = feature.properties.point_count;
const size = count < 100 ? 30 : count < 1000 ? 40 : 50;
const color = "#333"; // Default cluster color
const icon = L.divIcon({
html: `<div style="background-color: ${color}; width: ${size}px; height: ${size}px; border-radius: 50%; display: flex; align-items:center; justify-content:center; color: white; border: 2px solid white; font-weight: bold;">${count}</div>`,
className: "marker-cluster",
iconSize: L.point(size, size),
});
return L.marker(latlng, { icon: icon });
},
},
clientside: {
clean_filters: function (trigger) {
if (!trigger) {
+83 -50
View File
@@ -3,10 +3,13 @@ from typing import Literal
from urllib.error import HTTPError, URLError
import dash_bootstrap_components as dbc
import dash_leaflet as dl
import dash_leaflet.express as dlx
import plotly.express as px
import plotly.graph_objects as go
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
@@ -452,8 +455,8 @@ def get_geographic_maps(dff: pl.DataFrame) -> list | None:
# Ajout des DOM s'ils ont des données
for code in dom_codes:
dom_data = dff.filter(pl.col("acheteur_departement_code") == code)
if dom_data.height > 0:
dff_dom = dff.filter(pl.col("acheteur_departement_code") == code)
if dff_dom.height > 0:
name = f"Département {code}"
if code == "971":
name = "Guadeloupe"
@@ -465,76 +468,106 @@ def get_geographic_maps(dff: pl.DataFrame) -> list | None:
name = "La Réunion"
elif code == "976":
name = "Mayotte"
regions[name] = dom_data
regions[name] = dff_dom
# Region centers for dash-leaflet
region_centers = {
"Métropole": ([46.6, 2.2], 6),
"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], 10),
"Mayotte": ([-12.82, 45.16], 11),
}
# 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():
fig = go.Figure()
# Prepare data for GeoJSON
marker_dicts = []
# Trace Acheteurs
mask_acheteur = region_df.filter(
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()
)
if mask_acheteur.height > 0:
fig.add_trace(
go.Scattergeo(
lat=mask_acheteur["acheteur_latitude"],
lon=mask_acheteur["acheteur_longitude"],
mode="markers",
marker=dict(size=6, color=color_acheteur, opacity=0.5),
name="Acheteurs",
text=mask_acheteur["acheteur_nom"],
)
if mask_acheteur.height > 0:
for row in mask_acheteur.to_dicts():
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,
}
)
# Trace Titulaires
mask_titulaire = region_df.filter(
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()
)
)
if mask_titulaire.height > 0:
fig.add_trace(
go.Scattergeo(
lat=mask_titulaire["titulaire_latitude"],
lon=mask_titulaire["titulaire_longitude"],
mode="markers",
marker=dict(size=6, color=color_titulaire, opacity=0.5),
name="Titulaires",
text=mask_titulaire["titulaire_nom"],
)
for row in mask_titulaire.to_dicts():
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,
}
)
# Configuration spécifique de la vue
geo_config = dict(
projection_type="mercator",
showland=True,
landcolor="lightgray",
showcountries=True,
countrycolor="white",
fitbounds="locations" if name != "Métropole" else False,
resolution=50,
)
geojson_data = dlx.dicts_to_geojson(marker_dicts)
if name == "Métropole":
geo_config["lataxis_range"] = [41, 52]
geo_config["lonaxis_range"] = [-5, 10]
fig.update_layout(
title=name,
geo=geo_config,
margin=dict(l=0, r=0, t=30, b=0),
height=400 if name == "Métropole" else 300,
showlegend=(name == "Métropole"),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
)
# Taille de la colonne : 4 slots (md=12) pour Métropole, 1 slot (md=3) pour DOM
# On suppose une grille de 12 colonnes où 1 card = md=3
center, zoom = region_centers.get(name, ([46.6, 2.2], 6))
col_width = 12 if name == "Métropole" else 3
region_id = name.lower().replace(" ", "-")
cols.append(
dbc.Col(
dcc.Graph(figure=fig, config={"displayModeBar": False}),
[
html.H5(name),
dl.Map(
[
dl.TileLayer(),
dl.GeoJSON(
data=geojson_data,
cluster=True,
zoomToBoundsOnClick=True,
pointToLayer=point_to_layer,
clusterToLayer=cluster_to_layer,
id=f"geojson-{region_id}",
),
],
center=center,
zoom=zoom,
style={
"width": "100%",
"height": "400px" if name == "Métropole" else "300px",
},
id=f"map-{region_id}",
),
],
width=12,
md=col_width,
className="mb-4",
+1 -3
View File
@@ -142,9 +142,7 @@ def udpate_dashboard_cards(
)
if dashboard_acheteur_categorie:
lff = lff.filter(
pl.col("acheteur_categorie").is_in(dashboard_acheteur_categorie)
)
lff = lff.filter(pl.col("acheteur_categorie") == dashboard_acheteur_categorie)
if dashboard_acheteur_departement_code:
lff = lff.filter(