feat(observatoire): ajouter barres 'valeur renseignée' dans tuile considérations
Affiche 4 barres groupées par type (sociales/env) : une pour les marchés avec au moins une considération, une pour ceux qui ont une valeur renseignée (non nulle, y compris 'Sans objet'). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+36
-18
@@ -738,13 +738,18 @@ CONSIDERATIONS_COLUMNS = {
|
|||||||
def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int]]:
|
def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int]]:
|
||||||
"""Part des marchés (uid distincts) ayant au moins une considération.
|
"""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).
|
Dénominateur = tous les uid distincts. Colonne absente -> (0, 0).
|
||||||
"""
|
"""
|
||||||
names = lff.collect_schema().names()
|
names = lff.collect_schema().names()
|
||||||
present = {key: col for key, col in CONSIDERATIONS_COLUMNS.items() if col in 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:
|
if not present:
|
||||||
return stats
|
return stats
|
||||||
@@ -761,26 +766,37 @@ def compute_considerations_stats(lff: pl.LazyFrame) -> dict[str, tuple[int, int]
|
|||||||
return stats
|
return stats
|
||||||
|
|
||||||
for key, col in present.items():
|
for key, col in present.items():
|
||||||
count = agg.filter(pl.col(col).str.contains(CONSIDERATIONS_REGEX)).height
|
count_pos = agg.filter(pl.col(col).str.contains(CONSIDERATIONS_REGEX)).height
|
||||||
pct = round(100 * count / total)
|
count_ren = agg.filter(pl.col(col).is_not_null()).height
|
||||||
stats[key] = (count, pct)
|
stats[key] = (count_pos, round(100 * count_pos / total))
|
||||||
|
stats[f"{key}_renseignees"] = (count_ren, round(100 * count_ren / total))
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# (clé, libellé, couleur principale, couleur renseignée)
|
||||||
CONSIDERATIONS_DISPLAY = [
|
CONSIDERATIONS_DISPLAY = [
|
||||||
("sociales", "Sociales", "#CC6677"),
|
("sociales", "Sociales", "#CC6677", "#E5B2BB"),
|
||||||
("environnementales", "Environnementales", "#117733"),
|
("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:
|
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)
|
stats = compute_considerations_stats(lff)
|
||||||
|
|
||||||
blocks = []
|
blocks = []
|
||||||
for key, label, color in CONSIDERATIONS_DISPLAY:
|
for key, label, color, color_ren in CONSIDERATIONS_DISPLAY:
|
||||||
count, pct = stats[key]
|
count_pos, pct_pos = stats[key]
|
||||||
|
count_ren, pct_ren = stats[f"{key}_renseignees"]
|
||||||
blocks.append(
|
blocks.append(
|
||||||
html.Div(
|
html.Div(
|
||||||
className="mb-3",
|
className="mb-3",
|
||||||
@@ -790,20 +806,22 @@ def get_considerations_card_content(lff: pl.LazyFrame) -> html.Div:
|
|||||||
children=[
|
children=[
|
||||||
html.Span(label),
|
html.Span(label),
|
||||||
html.Span(
|
html.Span(
|
||||||
f"{format_number(count)} marchés",
|
f"{format_number(count_pos)} marchés",
|
||||||
className="text-muted",
|
className="text-muted",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
dbc.Progress(
|
_progress_bar(pct_pos, color),
|
||||||
dbc.Progress(
|
html.Div(
|
||||||
value=pct,
|
className="d-flex justify-content-end mt-1",
|
||||||
label=f"{pct} %",
|
children=[
|
||||||
bar=True,
|
html.Span(
|
||||||
color=color,
|
f"{format_number(count_ren)} marchés (renseignée)",
|
||||||
style={"color": "white"},
|
className="text-muted",
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
_progress_bar(pct_ren, color_ren),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+27
-13
@@ -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%.
|
# 4 marchés au total. Social : u1, u3 -> 2/4 = 50%. Env : u2 -> 1/4 = 25%.
|
||||||
assert stats["sociales"] == (2, 50)
|
assert stats["sociales"] == (2, 50)
|
||||||
assert stats["environnementales"] == (1, 25)
|
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():
|
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%.
|
# 2 marchés distincts. Social : u1 -> 1/2 = 50%.
|
||||||
assert stats["sociales"] == (1, 50)
|
assert stats["sociales"] == (1, 50)
|
||||||
assert stats["environnementales"] == (0, 0)
|
assert stats["environnementales"] == (0, 0)
|
||||||
|
assert stats["sociales_renseignees"] == (2, 100)
|
||||||
|
assert stats["environnementales_renseignees"] == (2, 100)
|
||||||
|
|
||||||
|
|
||||||
def test_compute_considerations_stats_missing_column():
|
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%.
|
# Colonne env absente -> (0, 0) sans exception. Social : 1/2 = 50%.
|
||||||
assert stats["sociales"] == (1, 50)
|
assert stats["sociales"] == (1, 50)
|
||||||
assert stats["environnementales"] == (0, 0)
|
assert stats["environnementales"] == (0, 0)
|
||||||
|
assert stats["sociales_renseignees"] == (2, 100)
|
||||||
|
assert stats["environnementales_renseignees"] == (0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_compute_considerations_stats_empty():
|
def test_compute_considerations_stats_empty():
|
||||||
@@ -107,9 +114,11 @@ def test_compute_considerations_stats_empty():
|
|||||||
|
|
||||||
assert stats["sociales"] == (0, 0)
|
assert stats["sociales"] == (0, 0)
|
||||||
assert stats["environnementales"] == (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
|
import dash_bootstrap_components as dbc
|
||||||
from dash import html
|
from dash import html
|
||||||
|
|
||||||
@@ -146,17 +155,22 @@ def test_get_considerations_card_content_returns_two_progress_bars():
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
all_bars = find_progress(div, [])
|
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)]
|
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_pos, social_ren, env_pos, env_ren = inner_bars
|
||||||
social_bar, env_bar = inner_bars[0], inner_bars[1]
|
# Sociales positives : u1 -> 1/2 = 50%
|
||||||
assert social_bar.value == 50
|
assert social_pos.value == 50
|
||||||
assert social_bar.label == "50 %"
|
assert social_pos.color == "#CC6677"
|
||||||
assert social_bar.color == "#CC6677"
|
assert social_pos.style["color"] == "white"
|
||||||
assert social_bar.style["color"] == "white"
|
# Sociales renseignées : u1 + u2 non null -> 2/2 = 100%
|
||||||
assert env_bar.value == 50
|
assert social_ren.value == 100
|
||||||
assert env_bar.label == "50 %"
|
assert social_ren.color == "#E5B2BB"
|
||||||
assert env_bar.color == "#117733"
|
# Environnementales positives : u2 -> 1/2 = 50%
|
||||||
assert env_bar.style["color"] == "white"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user