From 21d3a30d8b8c78b7aca64259fdef131427b10253 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Fri, 3 Jul 2026 15:55:29 +0200 Subject: [PATCH] =?UTF-8?q?refactor(figures):=20extraite=20build=5Forg=5Fm?= =?UTF-8?q?arkers=20pour=20r=C3=A9utilisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/figures.py | 82 +++++++++++++++++++++++-------------------- tests/test_figures.py | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 39 deletions(-) diff --git a/src/figures.py b/src/figures.py index b5bc04d..a574f9a 100644 --- a/src/figures.py +++ b/src/figures.py @@ -407,6 +407,46 @@ def get_duplicate_matrix() -> dcc.Graph: return dcc.Graph(figure=fig) +ORG_COLORS = { + "acheteur": "#E69F00", # orange + "titulaire": "#56B4E9", # bleu ciel +} + + +def build_org_markers( + lff: pl.LazyFrame, org_type: Literal["acheteur", "titulaire"] +) -> list[dict]: + """Regroupe les marchés par point géographique pour un type d'organisme. + + Renvoie [] (sans exception) si les colonnes longitude/latitude de ce + type sont absentes du LazyFrame (ex: tests/test.parquet). + """ + lon_col = f"{org_type}_longitude" + lat_col = f"{org_type}_latitude" + nom_col = f"{org_type}_nom" + + available = set(lff.collect_schema().names()) + if lon_col not in available or lat_col not in available: + return [] + + lff_org = ( + lff.select("uid", lon_col, lat_col, nom_col) + .group_by(lon_col, lat_col, nom_col) + .len("nb_marches") + .filter(pl.col(lat_col).is_not_null() & pl.col(lon_col).is_not_null()) + ) + + return [ + { + "lat": row[lat_col], + "lon": row[lon_col], + "tooltip": f"{row[nom_col]} ({row['nb_marches']} marchés)", + "marker_color": ORG_COLORS[org_type], + } + for row in lff_org.collect().to_dicts() + ] + + def get_geographic_maps(dff: pl.DataFrame) -> list[dbc.Col] | list: """ Génère les cartes géographiques pour l'hexagone et les DOM-TOM. @@ -489,43 +529,7 @@ def get_geographic_maps(dff: pl.DataFrame) -> list[dbc.Col] | list: else: _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() - ) - ) - - markers = [] - - # 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) + dfs.append(build_org_markers(lff, org_type)) return dfs, _map_type @@ -592,8 +596,8 @@ def make_clusters_map(region: dict) -> dl.Map: region_titulaires = region["data"][1] # Couleurs - color_acheteur = region_acheteurs[0]["marker_color"] - color_titulaire = region_titulaires[0]["marker_color"] + color_acheteur = ORG_COLORS["acheteur"] + color_titulaire = ORG_COLORS["titulaire"] acheteurs_geojson_data = dlx.dicts_to_geojson(region_acheteurs) titulaires_geojson_data = dlx.dicts_to_geojson(region_titulaires) diff --git a/tests/test_figures.py b/tests/test_figures.py index 810a4eb..4a49ca2 100644 --- a/tests/test_figures.py +++ b/tests/test_figures.py @@ -165,3 +165,74 @@ def test_get_considerations_card_content_returns_three_progress_bars(): # Bar 3 : env parmi renseignés (u2 != "Sans objet" -> 1/2 = 50%, vert) assert bar_env.value == 50 assert bar_env.color == "#117733" + + +def test_build_org_markers_groups_and_counts(): + from src.figures import build_org_markers + + lff = pl.LazyFrame( + [ + { + "uid": "u1", + "acheteur_longitude": 2.35, + "acheteur_latitude": 48.85, + "acheteur_nom": "ACHETEUR A", + }, + { + "uid": "u2", + "acheteur_longitude": 2.35, + "acheteur_latitude": 48.85, + "acheteur_nom": "ACHETEUR A", + }, + { + "uid": "u3", + "acheteur_longitude": -1.68, + "acheteur_latitude": 48.11, + "acheteur_nom": "ACHETEUR B", + }, + ] + ) + + markers = build_org_markers(lff, "acheteur") + + assert len(markers) == 2 + marker_a = next(m for m in markers if m["tooltip"].startswith("ACHETEUR A")) + assert marker_a["tooltip"] == "ACHETEUR A (2 marchés)" + assert marker_a["lat"] == 48.85 + assert marker_a["lon"] == 2.35 + assert marker_a["marker_color"] == "#E69F00" + + +def test_build_org_markers_filters_null_coordinates(): + from src.figures import build_org_markers + + lff = pl.LazyFrame( + [ + { + "uid": "u1", + "acheteur_longitude": None, + "acheteur_latitude": None, + "acheteur_nom": "ACHETEUR A", + }, + { + "uid": "u2", + "acheteur_longitude": 2.35, + "acheteur_latitude": 48.85, + "acheteur_nom": "ACHETEUR B", + }, + ] + ) + + markers = build_org_markers(lff, "acheteur") + + assert len(markers) == 1 + assert markers[0]["tooltip"] == "ACHETEUR B (1 marchés)" + + +def test_build_org_markers_missing_columns_returns_empty(): + from src.figures import build_org_markers + + lff = pl.LazyFrame([{"uid": "u1", "acheteur_nom": "ACHETEUR A"}]) + + assert build_org_markers(lff, "acheteur") == [] + assert build_org_markers(lff, "titulaire") == []