diff --git a/src/figures.py b/src/figures.py index 767a579..87faba4 100644 --- a/src/figures.py +++ b/src/figures.py @@ -738,13 +738,18 @@ CONSIDERATIONS_COLUMNS = { def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int]]: """Part des marchés (uid distincts) ayant au moins une considération. - Renvoie {"sociales": (count, pct), "environnementales": (count, pct)}. + Renvoie pour chaque clé (sociales, environnementales) : + - (count, pct) : marchés avec au moins une considération (regex) + - (count, pct) suffixé _renseignees : marchés avec une valeur non nulle Dénominateur = tous les uid distincts. Colonne absente -> (0, 0). """ names = lff.collect_schema().names() present = {key: col for key, col in CONSIDERATIONS_COLUMNS.items() if col in names} - stats = {key: (0, 0) for key in CONSIDERATIONS_COLUMNS} + stats: dict[str, tuple[int, int]] = {} + for key in CONSIDERATIONS_COLUMNS: + stats[key] = (0, 0) + stats[f"{key}_renseignees"] = (0, 0) if not present: return stats @@ -761,26 +766,37 @@ def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int] return stats for key, col in present.items(): - count = agg.filter(pl.col(col).str.contains(CONSIDERATIONS_REGEX)).height - pct = round(100 * count / total) - stats[key] = (count, pct) + count_pos = agg.filter(pl.col(col).str.contains(CONSIDERATIONS_REGEX)).height + count_ren = agg.filter(pl.col(col).is_not_null()).height + stats[key] = (count_pos, round(100 * count_pos / total)) + stats[f"{key}_renseignees"] = (count_ren, round(100 * count_ren / total)) return stats +# (clé, libellé, couleur principale, couleur renseignée) CONSIDERATIONS_DISPLAY = [ - ("sociales", "Sociales", "#CC6677"), - ("environnementales", "Environnementales", "#117733"), + ("sociales", "Sociales", "#CC6677", "#E5B2BB"), + ("environnementales", "Environnementales", "#117733", "#88BB99"), ] +def _progress_bar(pct: int, color: str) -> dbc.Progress: + return dbc.Progress( + dbc.Progress( + value=pct, label=f"{pct} %", bar=True, color=color, style={"color": "white"} + ), + ) + + def get_considerations_card_content(lff: pl.LazyFrame) -> html.Div: - """Deux barres de progression : part des marchés avec considération.""" + """Quatre barres groupées par type : considération positive + valeur renseignée.""" stats = compute_considerations_stats(lff) blocks = [] - for key, label, color in CONSIDERATIONS_DISPLAY: - count, pct = stats[key] + for key, label, color, color_ren in CONSIDERATIONS_DISPLAY: + count_pos, pct_pos = stats[key] + count_ren, pct_ren = stats[f"{key}_renseignees"] blocks.append( html.Div( className="mb-3", @@ -790,20 +806,22 @@ def get_considerations_card_content(lff: pl.LazyFrame) -> html.Div: children=[ html.Span(label), html.Span( - f"{format_number(count)} marchés", + f"{format_number(count_pos)} marchés", className="text-muted", ), ], ), - dbc.Progress( - dbc.Progress( - value=pct, - label=f"{pct} %", - bar=True, - color=color, - style={"color": "white"}, - ), + _progress_bar(pct_pos, color), + html.Div( + className="d-flex justify-content-end mt-1", + children=[ + html.Span( + f"{format_number(count_ren)} marchés (renseignée)", + className="text-muted", + ), + ], ), + _progress_bar(pct_ren, color_ren), ], ) ) diff --git a/tests/test_figures.py b/tests/test_figures.py index 53c6365..3ffb42b 100644 --- a/tests/test_figures.py +++ b/tests/test_figures.py @@ -42,6 +42,9 @@ def test_compute_considerations_stats_basic(): # 4 marchés au total. Social : u1, u3 -> 2/4 = 50%. Env : u2 -> 1/4 = 25%. assert stats["sociales"] == (2, 50) assert stats["environnementales"] == (1, 25) + # Renseignées : social u1/u2/u3/u4 non null -> 4/4=100%. Env : u1/u2/u4 non null -> 3/4=75%. + assert stats["sociales_renseignees"] == (4, 100) + assert stats["environnementales_renseignees"] == (3, 75) def test_compute_considerations_stats_dedup_per_uid(): @@ -73,6 +76,8 @@ def test_compute_considerations_stats_dedup_per_uid(): # 2 marchés distincts. Social : u1 -> 1/2 = 50%. assert stats["sociales"] == (1, 50) assert stats["environnementales"] == (0, 0) + assert stats["sociales_renseignees"] == (2, 100) + assert stats["environnementales_renseignees"] == (2, 100) def test_compute_considerations_stats_missing_column(): @@ -90,6 +95,8 @@ def test_compute_considerations_stats_missing_column(): # Colonne env absente -> (0, 0) sans exception. Social : 1/2 = 50%. assert stats["sociales"] == (1, 50) assert stats["environnementales"] == (0, 0) + assert stats["sociales_renseignees"] == (2, 100) + assert stats["environnementales_renseignees"] == (0, 0) def test_compute_considerations_stats_empty(): @@ -107,9 +114,11 @@ def test_compute_considerations_stats_empty(): assert stats["sociales"] == (0, 0) assert stats["environnementales"] == (0, 0) + assert stats["sociales_renseignees"] == (0, 0) + assert stats["environnementales_renseignees"] == (0, 0) -def test_get_considerations_card_content_returns_two_progress_bars(): +def test_get_considerations_card_content_returns_four_progress_bars(): import dash_bootstrap_components as dbc from dash import html @@ -146,17 +155,22 @@ def test_get_considerations_card_content_returns_two_progress_bars(): return found all_bars = find_progress(div, []) - # Structure imbriquée : outer (track) + inner (bar=True, couleur + texte blanc) inner_bars = [b for b in all_bars if getattr(b, "bar", False)] - assert len(inner_bars) == 2 + # 4 barres internes : 2 positives + 2 renseignées + assert len(inner_bars) == 4 - # Sociales (#CC6677) : u1 -> 50%. Environnementales (#117733) : u2 -> 50%. - social_bar, env_bar = inner_bars[0], inner_bars[1] - assert social_bar.value == 50 - assert social_bar.label == "50 %" - assert social_bar.color == "#CC6677" - assert social_bar.style["color"] == "white" - assert env_bar.value == 50 - assert env_bar.label == "50 %" - assert env_bar.color == "#117733" - assert env_bar.style["color"] == "white" + social_pos, social_ren, env_pos, env_ren = inner_bars + # Sociales positives : u1 -> 1/2 = 50% + assert social_pos.value == 50 + assert social_pos.color == "#CC6677" + assert social_pos.style["color"] == "white" + # Sociales renseignées : u1 + u2 non null -> 2/2 = 100% + assert social_ren.value == 100 + assert social_ren.color == "#E5B2BB" + # Environnementales positives : u2 -> 1/2 = 50% + assert env_pos.value == 50 + assert env_pos.color == "#117733" + assert env_pos.style["color"] == "white" + # Environnementales renseignées : u1 (Sans objet) + u2 -> 2/2 = 100% + assert env_ren.value == 100 + assert env_ren.color == "#88BB99"