Filtres sur techniques, sous-traitance, innovant #65

This commit is contained in:
Colin Maudry
2026-03-19 18:36:26 +01:00
parent 9852d55a3d
commit 50c3947c04
3 changed files with 289 additions and 211 deletions
+1 -3
View File
@@ -94,7 +94,6 @@ button:hover:not([disabled]) {
padding: 28px 24px 0 24px; padding: 28px 24px 0 24px;
} }
#header > * { #header > * {
margin: 0 0 20px 0px; margin: 0 0 20px 0px;
} }
@@ -174,7 +173,6 @@ p.version > a {
margin-right: 12px; margin-right: 12px;
} }
/* --- Dashboard inputs --- */ /* --- Dashboard inputs --- */
.Select--multi .Select-value { .Select--multi .Select-value {
@@ -182,7 +180,7 @@ p.version > a {
background-color: rgba(255, 240, 240, 0.4); background-color: rgba(255, 240, 240, 0.4);
} }
#filters .col > * { #filters .row > * {
margin-bottom: 6px; margin-bottom: 6px;
} }
+197 -208
View File
@@ -1,5 +1,5 @@
import urllib.parse import urllib.parse
from datetime import datetime, timedelta from datetime import datetime
import dash_bootstrap_components as dbc import dash_bootstrap_components as dbc
import polars as pl import polars as pl
@@ -33,6 +33,7 @@ from src.utils import (
format_number, format_number,
get_enum_values_as_dict, get_enum_values_as_dict,
meta_content, meta_content,
prepare_dashboard_data,
) )
name = "Observatoire" name = "Observatoire"
@@ -60,78 +61,6 @@ options_departements = {
} }
def _apply_filters(
lff: pl.LazyFrame,
year,
acheteur_id,
acheteur_categorie,
acheteur_departement_code,
titulaire_id,
titulaire_categorie,
titulaire_departement_code,
marche_type,
considerations_sociales,
considerations_environnementales,
montant_min=None,
montant_max=None,
) -> pl.LazyFrame:
if year:
lff = lff.filter(pl.col("dateNotification").dt.year() == int(year))
else:
lff = lff.filter(
pl.col("dateNotification") > (datetime.now() - timedelta(days=365))
)
if acheteur_id:
lff = lff.filter(pl.col("acheteur_id").str.contains(acheteur_id))
else:
if acheteur_categorie:
lff = lff.filter(pl.col("acheteur_categorie") == acheteur_categorie)
if acheteur_departement_code:
lff = lff.filter(
pl.col("acheteur_departement_code").is_in(acheteur_departement_code)
)
if titulaire_id:
lff = lff.filter(pl.col("titulaire_id").str.contains(titulaire_id))
else:
if titulaire_categorie:
lff = lff.filter(pl.col("titulaire_categorie") == titulaire_categorie)
if titulaire_departement_code:
lff = lff.filter(
pl.col("titulaire_departement_code").is_in(titulaire_departement_code)
)
if marche_type:
lff = lff.filter(pl.col("type") == marche_type)
if considerations_sociales:
lff = lff.filter(
pl.col("considerationsSociales")
.str.split(", ")
.list.set_intersection(considerations_sociales)
.list.len()
> 0
)
if considerations_environnementales:
lff = lff.filter(
pl.col("considerationsEnvironnementales")
.str.split(", ")
.list.set_intersection(considerations_environnementales)
.list.len()
> 0
)
if montant_min is not None:
lff = lff.filter(pl.col("montant") >= montant_min)
if montant_max is not None:
lff = lff.filter(pl.col("montant") <= montant_max)
return lff
layout = [ layout = [
dcc.Location(id="dashboard_url", refresh="callback-nav"), dcc.Location(id="dashboard_url", refresh="callback-nav"),
dcc.Store(id="observatoire-filters", storage_type="local"), dcc.Store(id="observatoire-filters", storage_type="local"),
@@ -181,6 +110,8 @@ Alors, on fait comment ?
id="dashboard_year", id="dashboard_year",
options=options_years, options=options_years,
placeholder="12 derniers mois", placeholder="12 derniers mois",
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -192,6 +123,8 @@ Alors, on fait comment ?
placeholder="SIRET", placeholder="SIRET",
debounce=True, debounce=True,
style={"width": "100%"}, style={"width": "100%"},
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -203,6 +136,8 @@ Alors, on fait comment ?
"acheteur_categorie" "acheteur_categorie"
), ),
placeholder="Catégorie", placeholder="Catégorie",
persistence=True,
persistence_type="local",
) )
), ),
), ),
@@ -214,6 +149,8 @@ Alors, on fait comment ?
multi=True, multi=True,
placeholder="Département", placeholder="Département",
options=options_departements, options=options_departements,
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -225,6 +162,8 @@ Alors, on fait comment ?
placeholder="SIRET", placeholder="SIRET",
debounce=True, debounce=True,
style={"width": "100%"}, style={"width": "100%"},
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -236,6 +175,8 @@ Alors, on fait comment ?
options=get_enum_values_as_dict( options=get_enum_values_as_dict(
"titulaire_categorie" "titulaire_categorie"
), ),
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -247,6 +188,8 @@ Alors, on fait comment ?
multi=True, multi=True,
placeholder="Département", placeholder="Département",
options=options_departements, options=options_departements,
persistence=True,
persistence_type="local",
), ),
), ),
), ),
@@ -257,30 +200,8 @@ Alors, on fait comment ?
id="dashboard_marche_type", id="dashboard_marche_type",
placeholder="Type", placeholder="Type",
options=get_enum_values_as_dict("type"), options=get_enum_values_as_dict("type"),
), persistence=True,
), persistence_type="local",
),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsSociales",
placeholder="Considérations sociales",
options=get_enum_values_as_dict(
"considerationsSociales"
),
multi=True,
),
),
),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsEnvironnementales",
placeholder="Considérations environnementales",
multi=True,
options=get_enum_values_as_dict(
"considerationsEnvironnementales"
),
), ),
), ),
), ),
@@ -294,6 +215,8 @@ Alors, on fait comment ?
min=0, min=0,
debounce=True, debounce=True,
style={"width": "100%"}, style={"width": "100%"},
persistence=True,
persistence_type="local",
), ),
width=6, width=6,
), ),
@@ -305,11 +228,113 @@ Alors, on fait comment ?
min=0, min=0,
debounce=True, debounce=True,
style={"width": "100%"}, style={"width": "100%"},
persistence=True,
persistence_type="local",
), ),
width=6, width=6,
), ),
] ]
), ),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_techniques",
placeholder="Techniques d'achat",
options=get_enum_values_as_dict(
"techniques"
),
multi=True,
persistence=True,
persistence_type="local",
),
),
),
dbc.Row(
[
dbc.Col("Sous-traitance :", lg=5),
dbc.Col(
dbc.RadioItems(
id="dashboard_marche_sousTraitanceDeclaree",
options=[
{
"label": "Tous",
"value": "all",
},
{
"label": "Oui",
"value": "oui",
},
{
"label": "Non",
"value": "non",
},
],
value="all",
inline=True,
persistence=True,
persistence_type="local",
),
lg=7,
),
]
),
dbc.Row(
[
dbc.Col("Marché innovant :", lg=5),
dbc.Col(
dbc.RadioItems(
id="dashboard_marche_innovant",
options=[
{
"label": "Tous",
"value": "all",
},
{
"label": "Oui",
"value": "oui",
},
{
"label": "Non",
"value": "non",
},
],
value="all",
inline=True,
persistence=True,
persistence_type="local",
),
lg=7,
),
]
),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsSociales",
placeholder="Considérations sociales",
options=get_enum_values_as_dict(
"considerationsSociales"
),
multi=True,
persistence=True,
persistence_type="local",
),
),
),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id="dashboard_marche_considerationsEnvironnementales",
placeholder="Considérations environnementales",
multi=True,
options=get_enum_values_as_dict(
"considerationsEnvironnementales"
),
persistence=True,
persistence_type="local",
),
),
),
dcc.Download(id="download-observatoire"), dcc.Download(id="download-observatoire"),
dbc.Button( dbc.Button(
"Télécharger au format Excel", "Télécharger au format Excel",
@@ -350,10 +375,13 @@ Alors, on fait comment ?
Output("dashboard_titulaire_categorie", "value"), Output("dashboard_titulaire_categorie", "value"),
Output("dashboard_titulaire_departement_code", "value"), Output("dashboard_titulaire_departement_code", "value"),
Output("dashboard_marche_type", "value"), Output("dashboard_marche_type", "value"),
Output("dashboard_marche_considerationsSociales", "value"),
Output("dashboard_marche_considerationsEnvironnementales", "value"),
Output("dashboard_montant_min", "value"), Output("dashboard_montant_min", "value"),
Output("dashboard_montant_max", "value"), Output("dashboard_montant_max", "value"),
Output("dashboard_marche_techniques", "value"),
Output("dashboard_marche_innovant", "value"),
Output("dashboard_marche_sousTraitanceDeclaree", "value"),
Output("dashboard_marche_considerationsSociales", "value"),
Output("dashboard_marche_considerationsEnvironnementales", "value"),
Input("dashboard_url", "search"), Input("dashboard_url", "search"),
Input("dashboard_url", "pathname"), Input("dashboard_url", "pathname"),
State("observatoire-filters", "data"), State("observatoire-filters", "data"),
@@ -377,71 +405,11 @@ def restore_filters(search, _pathname, stored_filters):
None, None,
None, None,
None, None,
None,
None,
None,
) )
return (no_update,) * 15
if stored_filters:
return (
stored_filters.get("year"),
stored_filters.get("acheteur_id"),
stored_filters.get("acheteur_categorie"),
stored_filters.get("acheteur_departement_code"),
stored_filters.get("titulaire_id"),
stored_filters.get("titulaire_categorie"),
stored_filters.get("titulaire_departement_code"),
stored_filters.get("marche_type"),
stored_filters.get("considerations_sociales"),
stored_filters.get("considerations_environnementales"),
stored_filters.get("montant_min"),
stored_filters.get("montant_max"),
)
return (no_update,) * 12
@callback(
Output("observatoire-filters", "data"),
Input("dashboard_year", "value"),
Input("dashboard_acheteur_id", "value"),
Input("dashboard_acheteur_categorie", "value"),
Input("dashboard_acheteur_departement_code", "value"),
Input("dashboard_titulaire_id", "value"),
Input("dashboard_titulaire_categorie", "value"),
Input("dashboard_titulaire_departement_code", "value"),
Input("dashboard_marche_type", "value"),
Input("dashboard_marche_considerationsSociales", "value"),
Input("dashboard_marche_considerationsEnvironnementales", "value"),
Input("dashboard_montant_min", "value"),
Input("dashboard_montant_max", "value"),
prevent_initial_call=True,
)
def save_filters_to_storage(
year,
acheteur_id,
acheteur_categorie,
acheteur_departement_code,
titulaire_id,
titulaire_categorie,
titulaire_departement_code,
marche_type,
considerations_sociales,
considerations_environnementales,
montant_min,
montant_max,
):
return {
"year": year,
"acheteur_id": acheteur_id,
"acheteur_categorie": acheteur_categorie,
"acheteur_departement_code": acheteur_departement_code,
"titulaire_id": titulaire_id,
"titulaire_categorie": titulaire_categorie,
"titulaire_departement_code": titulaire_departement_code,
"marche_type": marche_type,
"considerations_sociales": considerations_sociales,
"considerations_environnementales": considerations_environnementales,
"montant_min": montant_min,
"montant_max": montant_max,
}
@callback( @callback(
@@ -503,10 +471,13 @@ def sync_observatoire_share_url(acheteur_id, titulaire_id, href):
Input("dashboard_titulaire_categorie", "value"), Input("dashboard_titulaire_categorie", "value"),
Input("dashboard_titulaire_departement_code", "value"), Input("dashboard_titulaire_departement_code", "value"),
Input("dashboard_marche_type", "value"), Input("dashboard_marche_type", "value"),
Input("dashboard_marche_considerationsSociales", "value"),
Input("dashboard_marche_considerationsEnvironnementales", "value"),
Input("dashboard_montant_min", "value"), Input("dashboard_montant_min", "value"),
Input("dashboard_montant_max", "value"), Input("dashboard_montant_max", "value"),
Input("dashboard_marche_techniques", "value"),
Input("dashboard_marche_innovant", "value"),
Input("dashboard_marche_sousTraitanceDeclaree", "value"),
Input("dashboard_marche_considerationsSociales", "value"),
Input("dashboard_marche_considerationsEnvironnementales", "value"),
) )
def udpate_dashboard_cards( def udpate_dashboard_cards(
dashboard_year, dashboard_year,
@@ -517,10 +488,13 @@ def udpate_dashboard_cards(
dashboard_titulaire_categorie, dashboard_titulaire_categorie,
dashboard_titulaire_departement_code, dashboard_titulaire_departement_code,
dashboard_marche_type, dashboard_marche_type,
dashboard_marche_considerations_sociales,
dashboard_marche_considerations_environnementales,
dashboard_montant_min, dashboard_montant_min,
dashboard_montant_max, dashboard_montant_max,
dashboard_marche_techniques,
dashboard_marche_innovant,
dashboard_marche_sous_traitance_declaree,
dashboard_marche_considerations_sociales,
dashboard_marche_considerations_environnementales,
): ):
lff: pl.LazyFrame = df.lazy() lff: pl.LazyFrame = df.lazy()
lff = lff.select( lff = lff.select(
@@ -531,23 +505,29 @@ def udpate_dashboard_cards(
"montant", "montant",
"considerationsSociales", "considerationsSociales",
"considerationsEnvironnementales", "considerationsEnvironnementales",
"marcheInnovant",
"sousTraitanceDeclaree",
"techniques",
"sourceDataset", "sourceDataset",
"type", "type",
) )
lff = _apply_filters( lff = prepare_dashboard_data(
lff, lff=lff,
dashboard_year, year=dashboard_year,
dashboard_acheteur_id, acheteur_id=dashboard_acheteur_id,
dashboard_acheteur_categorie, acheteur_categorie=dashboard_acheteur_categorie,
dashboard_acheteur_departement_code, acheteur_departement_code=dashboard_acheteur_departement_code,
dashboard_titulaire_id, titulaire_id=dashboard_titulaire_id,
dashboard_titulaire_categorie, titulaire_categorie=dashboard_titulaire_categorie,
dashboard_titulaire_departement_code, titulaire_departement_code=dashboard_titulaire_departement_code,
dashboard_marche_type, type=dashboard_marche_type,
dashboard_marche_considerations_sociales, considerations_sociales=dashboard_marche_considerations_sociales,
dashboard_marche_considerations_environnementales, considerations_environnementales=dashboard_marche_considerations_environnementales,
montant_min=dashboard_montant_min, montant_min=dashboard_montant_min,
montant_max=dashboard_montant_max, montant_max=dashboard_montant_max,
techniques=dashboard_marche_techniques,
marche_innovant=dashboard_marche_innovant,
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
) )
# Génération des métriques # Génération des métriques
@@ -620,7 +600,7 @@ def udpate_dashboard_cards(
cards.append( cards.append(
make_card( make_card(
title="Catégorie d'entreprise", title="Catégorie d'entreprise",
subtitle="en nombre de marchés attribués", subtitle="en nombre de titulaires",
fig=donut_titulaire_categorie, fig=donut_titulaire_categorie,
) )
) )
@@ -683,41 +663,50 @@ def udpate_dashboard_cards(
State("dashboard_titulaire_categorie", "value"), State("dashboard_titulaire_categorie", "value"),
State("dashboard_titulaire_departement_code", "value"), State("dashboard_titulaire_departement_code", "value"),
State("dashboard_marche_type", "value"), State("dashboard_marche_type", "value"),
State("dashboard_marche_considerationsSociales", "value"),
State("dashboard_marche_considerationsEnvironnementales", "value"),
State("dashboard_montant_min", "value"), State("dashboard_montant_min", "value"),
State("dashboard_montant_max", "value"), State("dashboard_montant_max", "value"),
State("dashboard_marche_techniques", "value"),
State("dashboard_marche_innovant", "value"),
State("dashboard_marche_sousTraitanceDeclaree", "value"),
State("dashboard_marche_considerationsSociales", "value"),
State("dashboard_marche_considerationsEnvironnementales", "value"),
prevent_initial_call=True, prevent_initial_call=True,
) )
def download_observatoire( def download_observatoire(
_n_clicks, _n_clicks,
year, dashboard_year,
acheteur_id, dashboard_acheteur_id,
acheteur_categorie, dashboard_acheteur_categorie,
acheteur_departement_code, dashboard_acheteur_departement_code,
titulaire_id, dashboard_titulaire_id,
titulaire_categorie, dashboard_titulaire_categorie,
titulaire_departement_code, dashboard_titulaire_departement_code,
marche_type, dashboard_marche_type,
considerations_sociales, dashboard_montant_min,
considerations_environnementales, dashboard_montant_max,
montant_min, dashboard_marche_techniques,
montant_max, dashboard_marche_innovant,
dashboard_marche_sous_traitance_declaree,
dashboard_considerations_sociales,
dashboard_considerations_environnementales,
): ):
lff = _apply_filters( lff = prepare_dashboard_data(
df.lazy(), lff=df.lazy(),
year, year=dashboard_year,
acheteur_id, acheteur_id=dashboard_acheteur_id,
acheteur_categorie, acheteur_categorie=dashboard_acheteur_categorie,
acheteur_departement_code, acheteur_departement_code=dashboard_acheteur_departement_code,
titulaire_id, titulaire_id=dashboard_titulaire_id,
titulaire_categorie, titulaire_categorie=dashboard_titulaire_categorie,
titulaire_departement_code, titulaire_departement_code=dashboard_titulaire_departement_code,
marche_type, type=dashboard_marche_type,
considerations_sociales, considerations_sociales=dashboard_considerations_sociales,
considerations_environnementales, considerations_environnementales=dashboard_considerations_environnementales,
montant_min=montant_min, montant_min=dashboard_montant_min,
montant_max=montant_max, montant_max=dashboard_montant_max,
techniques=dashboard_marche_techniques,
marche_innovant=dashboard_marche_innovant,
sous_traitance_declaree=dashboard_marche_sous_traitance_declaree,
) )
def to_bytes(buffer): def to_bytes(buffer):
+91
View File
@@ -3,6 +3,7 @@ import logging
import os import os
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta
from time import localtime, sleep from time import localtime, sleep
import polars as pl import polars as pl
@@ -694,6 +695,96 @@ def prepare_table_data(
) )
def prepare_dashboard_data(
lff: pl.LazyFrame,
year,
acheteur_id,
acheteur_categorie,
acheteur_departement_code,
titulaire_id,
titulaire_categorie,
titulaire_departement_code,
type,
considerations_sociales,
considerations_environnementales,
techniques,
marche_innovant,
sous_traitance_declaree,
montant_min=None,
montant_max=None,
) -> pl.LazyFrame:
if year:
lff = lff.filter(pl.col("dateNotification").dt.year() == int(year))
else:
lff = lff.filter(
pl.col("dateNotification") > (datetime.now() - timedelta(days=365))
)
if acheteur_id:
lff = lff.filter(pl.col("acheteur_id").str.contains(acheteur_id))
else:
if acheteur_categorie:
lff = lff.filter(pl.col("acheteur_categorie") == acheteur_categorie)
if acheteur_departement_code:
lff = lff.filter(
pl.col("acheteur_departement_code").is_in(acheteur_departement_code)
)
if titulaire_id:
lff = lff.filter(pl.col("titulaire_id").str.contains(titulaire_id))
else:
if titulaire_categorie:
lff = lff.filter(pl.col("titulaire_categorie") == titulaire_categorie)
if titulaire_departement_code:
lff = lff.filter(
pl.col("titulaire_departement_code").is_in(titulaire_departement_code)
)
if type:
lff = lff.filter(pl.col("type") == type)
if marche_innovant != "all":
lff = lff.filter(pl.col("marcheInnovant") == marche_innovant)
if sous_traitance_declaree != "all":
lff = lff.filter(pl.col("sousTraitanceDeclaree") == sous_traitance_declaree)
if techniques:
lff = lff.filter(
pl.col("techniques")
.str.split(", ")
.list.set_intersection(techniques)
.list.len()
> 0
)
if considerations_sociales:
lff = lff.filter(
pl.col("considerationsSociales")
.str.split(", ")
.list.set_intersection(considerations_sociales)
.list.len()
> 0
)
if considerations_environnementales:
lff = lff.filter(
pl.col("considerationsEnvironnementales")
.str.split(", ")
.list.set_intersection(considerations_environnementales)
.list.len()
> 0
)
if montant_min is not None:
lff = lff.filter(pl.col("montant") >= montant_min)
if montant_max is not None:
lff = lff.filter(pl.col("montant") <= montant_max)
return lff
def get_button_properties(height): def get_button_properties(height):
if height > 65000: if height > 65000:
download_disabled = True download_disabled = True