From f6a38a29bdf998df3f0dce5feea0012bc343298c Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:01:17 +0100 Subject: [PATCH 1/3] Top 10 titulaires sur les pages acheteur #55 --- README.md | 2 +- src/assets/style.css | 5 +++++ src/callbacks.py | 35 +++++++++++++++++++++++++++++++++++ src/pages/acheteur.py | 31 +++++++++++++++++++++++++------ src/utils.py | 12 +++++++----- 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 src/callbacks.py diff --git a/README.md b/README.md index 8fcee21..31c2153 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Ne pas oublier de mettre à jour les fichier .env. ## Notes de version -### 2.2.0 () +#### 2.2.0 () - Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) diff --git a/src/assets/style.css b/src/assets/style.css index bf70592..9fe5350 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -211,6 +211,11 @@ summary > h3 { grid-row: 2; } +.org_top { + grid-column: 1/3; + grid-row: 3; +} + /* Vue marché */ .marche_infos p { diff --git a/src/callbacks.py b/src/callbacks.py new file mode 100644 index 0000000..c49d77d --- /dev/null +++ b/src/callbacks.py @@ -0,0 +1,35 @@ +import polars as pl +from dash import dash_table, html + +from utils import add_links_in_dict, format_montant, setup_table_columns + + +def get_top_org_table(data, org_type: str): + dff = pl.DataFrame(data) + if dff.height == 0: + return html.Div() + + dff = dff.select(["uid", f"{org_type}_id", f"{org_type}_nom", "montant"]) + dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom").agg( + pl.len().alias("Attributions"), pl.sum("montant").alias("montant") + ) + dff_nb = dff_nb.sort(by="Attributions", descending=True) + dff_nb = dff_nb.cast(pl.String) + dff_nb = dff_nb.fill_null("") + dff_nb = format_montant(dff_nb, column="montant") + columns, tooltip = setup_table_columns( + dff_nb, hideable=False, exclude=[f"{org_type}_id"] + ) + data = dff_nb.to_dicts() + data = add_links_in_dict(data, f"{org_type}") + + print(dff_nb) + + return dash_table.DataTable( + data=data, + markdown_options={"html": True}, + page_action="native", + page_size=10, + columns=columns, + tooltip_header=tooltip, + ) diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 80a258f..cb8d224 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -3,6 +3,7 @@ import datetime import polars as pl from dash import Input, Output, State, callback, dash_table, dcc, html, register_page +from src.callbacks import get_top_org_table from src.figures import point_on_map from src.utils import ( add_links_in_dict, @@ -89,6 +90,13 @@ layout = [ ], ), html.Div(className="org_map", id="acheteur_map"), + html.Div( + className="org_top", + children=[ + html.H3("Top titulaires"), + html.Div(className="marches_table", id="top10_titulaires"), + ], + ), ], ), # récupérer les données de l'acheteur sur l'api annuaire @@ -146,22 +154,22 @@ def update_acheteur_infos(url): Input(component_id="acheteur_data", component_property="data"), ) def update_acheteur_stats(data): - df = pl.DataFrame(data) - if df.height == 0: - df = pl.DataFrame(schema=df.collect_schema()) - df_marches = df.unique("id") + dff = pl.DataFrame(data) + if dff.height == 0: + dff = pl.DataFrame(schema=df.collect_schema()) + df_marches = dff.unique("id") nb_marches = format_number(df_marches.height) # somme_marches = format_number(int(df_marches.select(pl.sum("montant")).item())) marches_attribues = [html.Strong(nb_marches), " marchés et accord-cadres attribués"] # + ", pour un total de ", html.Strong(somme_marches + " €")] del df_marches - nb_titulaires = df.unique("titulaire_id").height + nb_titulaires = dff.unique("titulaire_id").height nb_titulaires = [ html.Strong(format_number(nb_titulaires)), " titulaires (SIRET) différents", ] - del df + del dff return marches_attribues, nb_titulaires @@ -203,8 +211,11 @@ def get_acheteur_marches_data(url, acheteur_year: str) -> list[dict]: ) def get_last_marches_table(data) -> html.Div: dff = pl.DataFrame(data) + if dff.height == 0: + return html.Div(html.P("Aucun marché trouvé.")) dff = dff.cast(pl.String) dff = dff.fill_null("") + print("1", dff.columns) dff = format_montant(dff) columns, tooltip = setup_table_columns( dff, @@ -252,6 +263,14 @@ def get_last_marches_table(data) -> html.Div: return table +@callback( + Output(component_id="top10_titulaires", component_property="children"), + Input(component_id="acheteur_data", component_property="data"), +) +def get_top_titulaires(data): + return get_top_org_table(data, "titulaire") + + @callback( Output("download-acheteur-data", "data"), Input("btn-download-acheteur-data", "n_clicks"), diff --git a/src/utils.py b/src/utils.py index eace5ff..240a090 100644 --- a/src/utils.py +++ b/src/utils.py @@ -77,15 +77,16 @@ def add_links(dff: pl.DataFrame): return dff -def add_links_in_dict(data: list, org_type: str) -> list: +def add_links_in_dict(data: list[dict], org_type: str) -> list: new_data = [] for marche in data: org_id = marche[org_type + "_id"] marche[org_type + "_nom"] = ( f'{marche[org_type + "_nom"]}' ) - marche["id"] = f'{marche["id"]}' - marche["uid"] = f'{marche["uid"]}' + if marche.get("uid"): + marche["id"] = f'{marche["id"]}' + marche["uid"] = f'{marche["uid"]}' new_data.append(marche) return new_data @@ -124,7 +125,7 @@ def format_number(number) -> str: return number -def format_montant(dff: pl.DataFrame) -> pl.DataFrame: +def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: def format_function(expr, scale=None): # https://stackoverflow.com/a/78636786 expr = expr.cast(pl.String) @@ -155,7 +156,8 @@ def format_montant(dff: pl.DataFrame) -> pl.DataFrame: return montant - dff = dff.with_columns(pl.col("montant").pipe(format_function).alias("montant")) + print("3", dff.columns) + dff = dff.with_columns(pl.col(column).pipe(format_function).alias(column)) return dff From e9faf806096ad90c6ac0608f736dfdb2ebfd3235 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:30:50 +0100 Subject: [PATCH 2/3] Top 10 acheteurs sur les pages titulaire #55 --- src/assets/style.css | 5 +++-- src/callbacks.py | 31 ++++++++++++++++++++++++++++++- src/pages/titulaire.py | 18 +++++++++++++++++- src/utils.py | 2 +- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/assets/style.css b/src/assets/style.css index 9fe5350..b007d9a 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -53,9 +53,10 @@ div.logo > a { /* Réduire la taille du texte de la colonne Objet */ -td[data-dash-column="objet"] { +/* +td[data-dash-column="objet"],td[data-dash-column="titulaire_nom"],td[data-dash-column="acheteur_nom"], { font-size: 85%; -} +}*/ /* Couleur des en-têtes */ .dash-table-container diff --git a/src/callbacks.py b/src/callbacks.py index c49d77d..35534e8 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -13,7 +13,7 @@ def get_top_org_table(data, org_type: str): dff_nb = dff.group_by(f"{org_type}_id", f"{org_type}_nom").agg( pl.len().alias("Attributions"), pl.sum("montant").alias("montant") ) - dff_nb = dff_nb.sort(by="Attributions", descending=True) + dff_nb = dff_nb.sort(by="montant", descending=True) dff_nb = dff_nb.cast(pl.String) dff_nb = dff_nb.fill_null("") dff_nb = format_montant(dff_nb, column="montant") @@ -32,4 +32,33 @@ def get_top_org_table(data, org_type: str): page_size=10, columns=columns, tooltip_header=tooltip, + style_cell_conditional=[ + { + "if": {"column_id": "objet"}, + "minWidth": "350px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "14px", + "whiteSpace": "normal", + "fontSize": "85%", + }, + { + "if": {"column_id": "acheteur_nom"}, + "minWidth": "200px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "16px", + # "fontSize": "85%", + "whiteSpace": "normal", + }, + { + "if": {"column_id": "titulaire_nom"}, + "minWidth": "200px", + "textAlign": "left", + "overflow": "hidden", + "lineHeight": "16px", + "whiteSpace": "normal", + # "fontSize": "85%", + }, + ], ) diff --git a/src/pages/titulaire.py b/src/pages/titulaire.py index 07f07eb..0adeed9 100644 --- a/src/pages/titulaire.py +++ b/src/pages/titulaire.py @@ -3,6 +3,7 @@ import datetime import polars as pl from dash import Input, Output, State, callback, dash_table, dcc, html, register_page +from src.callbacks import get_top_org_table from src.figures import point_on_map from src.utils import ( add_links_in_dict, @@ -91,6 +92,13 @@ layout = [ ], ), html.Div(className="org_map", id="titulaire_map"), + html.Div( + className="org_top", + children=[ + html.H3("Top acheteurs"), + html.Div(className="marches_table", id="top10_acheteurs"), + ], + ), ], ), # récupérer les données de l'acheteur sur l'api annuaire @@ -161,7 +169,7 @@ def update_titulaire_stats(data): nb_acheteurs = dff.unique("acheteur_id").height nb_acheteurs = [ html.Strong(format_number(nb_acheteurs)), - " titulaires (SIRET) différents", + " acheteurs (SIRET) différents", ] del dff @@ -266,6 +274,14 @@ def get_last_marches_table(data) -> html.Div: return table +@callback( + Output(component_id="top10_acheteurs", component_property="children"), + Input(component_id="titulaire_data", component_property="data"), +) +def get_top_acheteurs(data): + return get_top_org_table(data, "acheteur") + + @callback( Output("download-titulaire-data", "data"), Input("btn-download-titulaire-data", "n_clicks"), diff --git a/src/utils.py b/src/utils.py index 240a090..783a236 100644 --- a/src/utils.py +++ b/src/utils.py @@ -144,7 +144,7 @@ def format_montant(dff: pl.DataFrame, column: str = "montant") -> pl.DataFrame: frac: pl.Expr = ( pl.when(frac.is_not_null() & ~frac.is_in(["0"])) - .then("," + frac) + .then("," + frac.str.head(2)) .otherwise(pl.lit("")) ) From bbb04a4487abf8d9138723b2be916a784efe06c7 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Mon, 10 Nov 2025 13:33:37 +0100 Subject: [PATCH 3/3] Changelog #55 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 31c2153..14b32ba 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Ne pas oublier de mettre à jour les fichier .env. #### 2.2.0 () +- Top acheteurs / titulaires par montant attribué/remporté (([#55](https://github.com/ColinMaudry/decp.info/issues/55))) - Moins de colonnes affichées par défaut ([#54](https://github.com/ColinMaudry/decp.info/issues/54)) ##### 2.1.6 (15 octobre 2025)