From 8b69fee31c87a44fb0be4ed238d6e345d1144847 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 3 Jul 2026 15:59:47 +0200 Subject: [PATCH] feat(figures): ajouter get_org_location_map (carte cluster organisme + contrepartie) --- src/figures.py | 62 +++++++++++++++++++++++++++++++++++++++++++ tests/test_figures.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/src/figures.py b/src/figures.py index a574f9a..0d78084 100644 --- a/src/figures.py +++ b/src/figures.py @@ -637,6 +637,68 @@ def make_clusters_map(region: dict) -> dl.Map: return leaflet_map +def get_org_location_map( + dff: pl.DataFrame, + home_type: Literal["acheteur", "titulaire"], + map_id: str, +) -> dl.Map: + """Carte cluster (dash-leaflet) d'un organisme et de sa contrepartie. + + Affiche les marchés de `home_type` (fiche /acheteur ou /titulaire + consultée) ainsi que ceux de son type complémentaire, clusterisés et + colorés comme sur /observatoire. Cadrage automatique (fitBounds) sur + l'ensemble des points ; repli sur une vue France fixe si aucun point + n'est disponible. + """ + lff = dff.lazy() + markers_by_type = { + "acheteur": build_org_markers(lff, "acheteur"), + "titulaire": build_org_markers(lff, "titulaire"), + } + + ns = Namespace("dash_clientside", "leaflet") + point_to_layer = ns("pointToLayer") + cluster_to_layer = ns("clusterToLayer") + + layers: list = [dl.TileLayer()] + all_points: list[tuple[float, float]] = [] + # Ordre fixe (titulaire puis acheteur) pour que l'organisme consulté + # soit toujours peint au-dessus de sa contrepartie, comme sur /observatoire. + for org_type in ("titulaire", "acheteur"): + markers = markers_by_type[org_type] + if not markers: + continue + all_points.extend((m["lat"], m["lon"]) for m in markers) + layers.append( + dl.GeoJSON( + data=dlx.dicts_to_geojson(markers), + cluster=True, + zoomToBoundsOnClick=True, + pointToLayer=point_to_layer, + clusterToLayer=cluster_to_layer, + id=f"{map_id}-{org_type}", + options={"fillColor": ORG_COLORS[org_type]}, + ) + ) + + map_kwargs: dict = {} + if all_points: + lats = [lat for lat, _ in all_points] + lons = [lon for _, lon in all_points] + map_kwargs["bounds"] = [[min(lats), min(lons)], [max(lats), max(lons)]] + map_kwargs["boundsOptions"] = {"padding": [30, 30], "maxZoom": 12} + else: + map_kwargs["center"] = [46.6, 2.2] + map_kwargs["zoom"] = 5 + + return dl.Map( + layers, + style={"width": "100%", "height": "300px"}, + id=map_id, + **map_kwargs, + ) + + def get_distance_histogram(lff: pl.LazyFrame) -> dcc.Graph: if "titulaire_distance" not in lff.collect_schema().names(): dff = pl.DataFrame({"titulaire_distance": pl.Series([], dtype=pl.Float64)}) diff --git a/tests/test_figures.py b/tests/test_figures.py index 4a49ca2..c8c83f4 100644 --- a/tests/test_figures.py +++ b/tests/test_figures.py @@ -236,3 +236,47 @@ def test_build_org_markers_missing_columns_returns_empty(): assert build_org_markers(lff, "acheteur") == [] assert build_org_markers(lff, "titulaire") == [] + + +def test_get_org_location_map_bounds_cover_home_and_counterpart(): + import dash_leaflet as dl + + from src.figures import get_org_location_map + + dff = pl.DataFrame( + [ + { + "uid": "u1", + "acheteur_longitude": 2.35, + "acheteur_latitude": 48.85, + "acheteur_nom": "ACHETEUR A", + "titulaire_longitude": -1.68, + "titulaire_latitude": 48.11, + "titulaire_nom": "TITULAIRE A", + } + ] + ) + + leaflet_map = get_org_location_map(dff, "acheteur", "test-map") + + assert isinstance(leaflet_map, dl.Map) + assert leaflet_map.bounds == [[48.11, -1.68], [48.85, 2.35]] + + geojson_layers = [c for c in leaflet_map.children if isinstance(c, dl.GeoJSON)] + assert {layer.id for layer in geojson_layers} == { + "test-map-acheteur", + "test-map-titulaire", + } + + +def test_get_org_location_map_defaults_to_france_view_without_coordinates(): + from src.figures import get_org_location_map + + dff = pl.DataFrame( + [{"uid": "u1", "acheteur_nom": "ACHETEUR A", "titulaire_nom": "TITULAIRE A"}] + ) + + leaflet_map = get_org_location_map(dff, "acheteur", "test-map") + + assert leaflet_map.center == [46.6, 2.2] + assert leaflet_map.zoom == 5