10ed9c4c4b
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.2 KiB
4.2 KiB
Design : amélioration du style des exports Excel (#83)
Date : 2026-06-23 Issue : #83
Contexte
Les 6 fonctions d'export Excel du projet produisent des fichiers basiques : toutes les colonnes ont la même largeur par défaut et les en-têtes ne sont pas mis en valeur. L'objectif est d'améliorer la lisibilité en imitant les largeurs de colonnes du DataTable commun et en stylisant les en-têtes.
Périmètre
Toutes les fonctions d'export Excel :
| Fichier | Fonction | Page |
|---|---|---|
src/pages/tableau.py |
download_data |
/tableau |
src/pages/acheteur.py |
download_acheteur_data |
/acheteur |
src/pages/acheteur.py |
download_filtered_acheteur_data |
/acheteur |
src/pages/titulaire.py |
download_titulaire_data |
/titulaire |
src/pages/titulaire.py |
download_filtered_titulaire_data |
/titulaire |
src/pages/observatoire.py |
download_observatoire |
/observatoire |
Solution retenue : wrapper write_styled_excel (approche C)
Le besoin de text_wrap impose de créer le workbook manuellement (xlsxwriter.Workbook avec default_format_properties). Répéter ce boilerplate 6 fois est peu maintenable, donc on centralise dans une fonction utilitaire dans src/utils/table.py.
Détail du design
Constantes et wrapper — src/utils/table.py
import xlsxwriter
_EXCEL_MIN_COLUMN_WIDTH = 132 # ≈ 3.5 cm à 96 DPI
_EXCEL_HEADER_FORMAT = {
"bold": True,
"bg_color": "#b33821", # couleur primaire de l'app
"font_color": "white",
}
_EXCEL_COLUMN_WIDTHS = { # tirés des minWidth du DataTable commun (src/figures.py:269)
"objet": 350,
"acheteur_nom": 250,
"titulaire_nom": 250,
"acheteur_id": 160,
}
def write_styled_excel(df: pl.DataFrame, buffer, worksheet: str = "DECP") -> None:
col_widths = {
col: max(_EXCEL_MIN_COLUMN_WIDTH, _EXCEL_COLUMN_WIDTHS.get(col, 0))
for col in df.columns
}
wb = xlsxwriter.Workbook(buffer, {"default_format_properties": {"text_wrap": True}})
ws = wb.add_worksheet(worksheet)
df.write_excel(
workbook=wb,
worksheet=ws,
header_format=_EXCEL_HEADER_FORMAT,
column_widths=col_widths,
)
wb.close()
Comportement :
text_wrap=Trueviadefault_format_propertiess'applique à toutes les cellules de données.- Chaque colonne reçoit au minimum 132 px (≈ 3.5 cm) ; les colonnes avec largeur explicite utilisent leur valeur si elle est supérieure.
- En-têtes : fond rouge (
#b33821), texte blanc, gras. - Le wrapper accepte un
pl.DataFrame; les callbacks qui travaillent avec uneLazyFrameappellent.collect()(éventuellementengine="streaming") avant d'appeler le wrapper.
Mise à jour des 6 callbacks
Chaque bloc def to_bytes(buffer): est remplacé par un appel au wrapper.
Exemples :
# tableau.py — download_data
def to_bytes(buffer):
write_styled_excel(lff.collect(engine="streaming"), buffer)
# acheteur.py — download_acheteur_data (worksheet dynamique selon l'année)
def to_bytes(buffer):
write_styled_excel(
df_to_download, buffer,
worksheet="DECP" if annee in ["Toutes les années", None] else annee,
)
# acheteur.py — download_filtered_acheteur_data
def to_bytes(buffer):
write_styled_excel(lff.collect(engine="streaming"), buffer)
Titulaire et observatoire : même pattern.
Décisions clés
autofit=False(pas d'autofit Polars) : les largeurs sont entièrement contrôlées parcol_widths.text_wrapvia workbook : seule façon d'appliquer le wrapping à toutes les cellules viawrite_excel(les paramètrescolumn_formats/dtype_formatsde Polars n'exposent pas les propriétés xlsxwriter de format cellule).- Constantes privées (
_EXCEL_*) : non exportées, consommées uniquement parwrite_styled_excel. - Worksheet dynamique :
download_acheteur_dataetdownload_titulaire_datautilisent l'année comme nom de feuille quand elle est définie — conservé via le paramètreworksheet.