Utilisation de cluster de marqueurs grâce à dash-leaflet #65
This commit is contained in:
+3
-1
@@ -17,7 +17,9 @@ dependencies = [
|
|||||||
"plotly[express]",
|
"plotly[express]",
|
||||||
"httpx",
|
"httpx",
|
||||||
"pandas", # utilisé pour la création de certains graphiques
|
"pandas", # utilisé pour la création de certains graphiques
|
||||||
"unidecode"
|
"unidecode",
|
||||||
|
"dash-leaflet",
|
||||||
|
"dash-extensions"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -1,4 +1,27 @@
|
|||||||
window.dash_clientside = Object.assign({}, window.dash_clientside, {
|
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: {
|
clientside: {
|
||||||
clean_filters: function (trigger) {
|
clean_filters: function (trigger) {
|
||||||
if (!trigger) {
|
if (!trigger) {
|
||||||
|
|||||||
+89
-56
@@ -3,10 +3,13 @@ from typing import Literal
|
|||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
|
|
||||||
import dash_bootstrap_components as dbc
|
import dash_bootstrap_components as dbc
|
||||||
|
import dash_leaflet as dl
|
||||||
|
import dash_leaflet.express as dlx
|
||||||
import plotly.express as px
|
import plotly.express as px
|
||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
import polars as pl
|
import polars as pl
|
||||||
from dash import dash_table, dcc, html
|
from dash import dash_table, dcc, html
|
||||||
|
from dash_extensions.javascript import Namespace
|
||||||
|
|
||||||
from src.utils import data_schema, df, format_number
|
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
|
# Ajout des DOM s'ils ont des données
|
||||||
for code in dom_codes:
|
for code in dom_codes:
|
||||||
dom_data = dff.filter(pl.col("acheteur_departement_code") == code)
|
dff_dom = dff.filter(pl.col("acheteur_departement_code") == code)
|
||||||
if dom_data.height > 0:
|
if dff_dom.height > 0:
|
||||||
name = f"Département {code}"
|
name = f"Département {code}"
|
||||||
if code == "971":
|
if code == "971":
|
||||||
name = "Guadeloupe"
|
name = "Guadeloupe"
|
||||||
@@ -465,76 +468,106 @@ def get_geographic_maps(dff: pl.DataFrame) -> list | None:
|
|||||||
name = "La Réunion"
|
name = "La Réunion"
|
||||||
elif code == "976":
|
elif code == "976":
|
||||||
name = "Mayotte"
|
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 = []
|
cols = []
|
||||||
for name, region_df in regions.items():
|
for name, region_df in regions.items():
|
||||||
fig = go.Figure()
|
# Prepare data for GeoJSON
|
||||||
|
marker_dicts = []
|
||||||
|
|
||||||
# Trace Acheteurs
|
# Trace Acheteurs
|
||||||
mask_acheteur = region_df.filter(
|
mask_acheteur = (
|
||||||
pl.col("acheteur_latitude").is_not_null()
|
region_df.select(
|
||||||
& pl.col("acheteur_longitude").is_not_null()
|
"uid", "acheteur_longitude", "acheteur_latitude", "acheteur_nom"
|
||||||
)
|
|
||||||
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"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
.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:
|
||||||
|
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
|
# Trace Titulaires
|
||||||
mask_titulaire = region_df.filter(
|
mask_titulaire = (
|
||||||
pl.col("titulaire_latitude").is_not_null()
|
region_df.select(
|
||||||
& pl.col("titulaire_longitude").is_not_null()
|
"uid", "titulaire_longitude", "titulaire_latitude", "titulaire_nom"
|
||||||
)
|
)
|
||||||
if mask_titulaire.height > 0:
|
.group_by("titulaire_longitude", "titulaire_latitude", "titulaire_nom")
|
||||||
fig.add_trace(
|
.len("nb_marches")
|
||||||
go.Scattergeo(
|
.filter(
|
||||||
lat=mask_titulaire["titulaire_latitude"],
|
pl.col("titulaire_latitude").is_not_null()
|
||||||
lon=mask_titulaire["titulaire_longitude"],
|
& pl.col("titulaire_longitude").is_not_null()
|
||||||
mode="markers",
|
|
||||||
marker=dict(size=6, color=color_titulaire, opacity=0.5),
|
|
||||||
name="Titulaires",
|
|
||||||
text=mask_titulaire["titulaire_nom"],
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if name == "Métropole":
|
if mask_titulaire.height > 0:
|
||||||
geo_config["lataxis_range"] = [41, 52]
|
for row in mask_titulaire.to_dicts():
|
||||||
geo_config["lonaxis_range"] = [-5, 10]
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
fig.update_layout(
|
geojson_data = dlx.dicts_to_geojson(marker_dicts)
|
||||||
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
|
center, zoom = region_centers.get(name, ([46.6, 2.2], 6))
|
||||||
# On suppose une grille de 12 colonnes où 1 card = md=3
|
|
||||||
col_width = 12 if name == "Métropole" else 3
|
col_width = 12 if name == "Métropole" else 3
|
||||||
|
region_id = name.lower().replace(" ", "-")
|
||||||
cols.append(
|
cols.append(
|
||||||
dbc.Col(
|
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,
|
width=12,
|
||||||
md=col_width,
|
md=col_width,
|
||||||
className="mb-4",
|
className="mb-4",
|
||||||
|
|||||||
@@ -142,9 +142,7 @@ def udpate_dashboard_cards(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if dashboard_acheteur_categorie:
|
if dashboard_acheteur_categorie:
|
||||||
lff = lff.filter(
|
lff = lff.filter(pl.col("acheteur_categorie") == dashboard_acheteur_categorie)
|
||||||
pl.col("acheteur_categorie").is_in(dashboard_acheteur_categorie)
|
|
||||||
)
|
|
||||||
|
|
||||||
if dashboard_acheteur_departement_code:
|
if dashboard_acheteur_departement_code:
|
||||||
lff = lff.filter(
|
lff = lff.filter(
|
||||||
|
|||||||
Reference in New Issue
Block a user