Améliorations sur le style de l'ag grid, ajout nb marchés uniques #47
This commit is contained in:
@@ -636,6 +636,10 @@ button.show-hide {
|
||||
--ag-internal-content-line-height: 20px;
|
||||
}
|
||||
|
||||
.ag-cell {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.ag-header-cell-text {
|
||||
font-family: "Archivo Narrow", sans-serif;
|
||||
font-size: 18px;
|
||||
@@ -645,6 +649,43 @@ button.show-hide {
|
||||
.ag-cell[col-id="marche"] a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Numéro de ligne sous la loupe : repère de position pendant le défilement,
|
||||
discret pour ne pas concurrencer le lien. */
|
||||
.ag-cell[col-id="marche"] {
|
||||
text-align: center;
|
||||
}
|
||||
.marche-row-number {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Infobulle : largeur bornée avec retour à la ligne plutôt qu'une largeur
|
||||
qui suit tout le texte. .ag-tooltip = infobulle native (ex. colonne
|
||||
"objet" tronquée) ; .ag-tooltip-custom = infobulle rendue par un
|
||||
tooltipComponent personnalisé (ex. en-têtes de colonnes, cf.
|
||||
grid_column_defs) — AG Grid n'ajoute PAS la classe .ag-tooltip dans ce
|
||||
cas, il faut donc cibler les deux. */
|
||||
.ag-tooltip,
|
||||
.ag-tooltip-custom {
|
||||
max-width: 400px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
background-color: #fdd;
|
||||
}
|
||||
|
||||
/* .ag-tooltip-custom n'a, contrairement à .ag-tooltip, aucun style visuel
|
||||
par défaut côté AG Grid (voir dash_ag_grid.min.js : seuls position/z-index
|
||||
sont posés) — on reprend ici les mêmes variables de thème que .ag-tooltip
|
||||
pour un rendu identique (bordure grise, padding, coins arrondis). */
|
||||
.ag-tooltip-custom {
|
||||
border: var(--ag-tooltip-border);
|
||||
border-radius: var(--ag-border-radius);
|
||||
color: var(--ag-tooltip-text-color);
|
||||
padding: var(--ag-widget-container-vertical-padding)
|
||||
var(--ag-widget-container-horizontal-padding);
|
||||
}
|
||||
/* fin overrides AG Grid*/
|
||||
|
||||
#btn-copy-url:before {
|
||||
|
||||
@@ -1131,6 +1131,9 @@ def ag_grid(grid_id: str, column_defs: list[dict]) -> "dag.AgGrid":
|
||||
# Permet de sélectionner/copier le texte des infobulles (ex.
|
||||
# colonne "objet" tronquée, cf. tooltipField dans grid_column_defs).
|
||||
"tooltipInteraction": True,
|
||||
# Délai avant apparition des infobulles natives (200ms au lieu
|
||||
# des 2000ms par défaut d'AG Grid).
|
||||
"tooltipShowDelay": 200,
|
||||
"localeText": AG_GRID_LOCALE_FR,
|
||||
"theme": {
|
||||
"function": (
|
||||
|
||||
+16
-10
@@ -24,7 +24,7 @@ from src.pages._compte_shell import current_user_has_subscription
|
||||
from src.saved_views import db as saved_views_db
|
||||
from src.saved_views import ui as saved_views_ui
|
||||
from src.utils import get_data_update_timestamp, logger
|
||||
from src.utils.grid import fetch_grid_page, grid_column_defs
|
||||
from src.utils.grid import apply_persisted_layout, fetch_grid_page, grid_column_defs
|
||||
from src.utils.query_ast import (
|
||||
ast_from_dict,
|
||||
ast_to_dict,
|
||||
@@ -522,11 +522,13 @@ def update_hidden_columns_from_checkboxes(selected_columns):
|
||||
@callback(
|
||||
Output("tableau_grid", "columnDefs"),
|
||||
Input("tableau-hidden-columns", "data"),
|
||||
State("tableau_grid", "columnState"),
|
||||
)
|
||||
def apply_hidden_columns(hidden_columns):
|
||||
def apply_hidden_columns(hidden_columns, column_state):
|
||||
if hidden_columns is None:
|
||||
hidden_columns = get_default_hidden_columns("tableau")
|
||||
return grid_column_defs(hidden_columns)
|
||||
defs = grid_column_defs(hidden_columns)
|
||||
return apply_persisted_layout(defs, column_state)
|
||||
|
||||
|
||||
@callback(
|
||||
@@ -560,16 +562,20 @@ def toggle_tableau_columns(click_open, click_close, is_open):
|
||||
|
||||
@callback(
|
||||
Output("tableau_grid", "filterModel", allow_duplicate=True),
|
||||
Output("tableau_grid", "resetColumnState", allow_duplicate=True),
|
||||
Output("tableau_grid", "columnState", allow_duplicate=True),
|
||||
Input("btn-tableau-reset", "n_clicks"),
|
||||
State("tableau_grid", "columnState"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def reset_view(n_clicks):
|
||||
# resetColumnState remet les colonnes (tri inclus) à l'état déclaré dans
|
||||
# columnDefs — qui reflète déjà la visibilité choisie via le sélecteur de
|
||||
# colonnes (cf. apply_hidden_columns), donc ça ne touche pas au choix de
|
||||
# colonnes affichées, seulement au tri.
|
||||
return {}, True
|
||||
def reset_view(n_clicks, column_state):
|
||||
# On ne touche qu'au tri (sort/sortIndex) : on réécrit le columnState tel
|
||||
# quel pour préserver la largeur, l'épinglage et l'ordre des colonnes
|
||||
# choisis par l'utilisateur (auparavant resetColumnState les effaçait
|
||||
# aussi, cf. #47).
|
||||
cleared_sort = [
|
||||
{**col, "sort": None, "sortIndex": None} for col in (column_state or [])
|
||||
]
|
||||
return {}, cleared_sort
|
||||
|
||||
|
||||
@callback(
|
||||
|
||||
+77
-19
@@ -55,7 +55,17 @@ def fetch_grid_page(
|
||||
offset=start_row,
|
||||
)
|
||||
page = postprocess_page(page)
|
||||
return page.to_dicts(), total, total_unique
|
||||
rows = page.to_dicts()
|
||||
# Numéro de ligne absolu (position dans les résultats filtrés/triés),
|
||||
# ajouté sous le lien loupe : repère visuel pendant le défilement infini
|
||||
# de la grille. Calculé ici (pas dans postprocess_page, partagé avec les
|
||||
# dash_table paginées d'acheteur.py/titulaire.py) car il dépend de
|
||||
# `start_row`, propre au chargement par bloc de l'AG Grid.
|
||||
for i, row in enumerate(rows):
|
||||
row["marche"] = (
|
||||
f'{row["marche"]}<div class="marche-row-number">{start_row + i + 1}</div>'
|
||||
)
|
||||
return rows, total, total_unique
|
||||
|
||||
|
||||
def export_dataframe(filter_model, sort_model, hidden_columns) -> pl.DataFrame:
|
||||
@@ -98,17 +108,28 @@ _BOOLEAN_LIKE_COLUMNS = {
|
||||
# Codes/identifiants qui ne suivent pas le pattern de suffixe `_id`/`_code`
|
||||
# (ex. codeCPV, idAccordCadre) mais restent des valeurs courtes.
|
||||
_SHORT_CODE_COLUMNS = {
|
||||
"uid",
|
||||
"id",
|
||||
"codeCPV",
|
||||
"idAccordCadre",
|
||||
"lieuExecution_typeCode",
|
||||
"sourceFile",
|
||||
}
|
||||
|
||||
# Colonnes "nom" qui ne suivent pas le suffixe `_nom` (ex. sourceDataset).
|
||||
_WIDE_LABEL_COLUMNS = {"sourceDataset"}
|
||||
|
||||
# Colonnes avec infobulle systématique sur les cellules (valeur complète au
|
||||
# survol, pas seulement quand le texte est tronqué).
|
||||
_CELL_TOOLTIP_COLUMNS = {
|
||||
"procedure",
|
||||
"considerationsSociales",
|
||||
"considerationsEnvironnementales",
|
||||
"objet",
|
||||
"ccag",
|
||||
"modalitesExecution",
|
||||
"acheteur_nom",
|
||||
"titulaire_nom",
|
||||
"acheteur_id",
|
||||
"titulaire_id",
|
||||
}
|
||||
|
||||
|
||||
def _filter_for(col_type) -> str:
|
||||
if col_type.is_numeric():
|
||||
@@ -121,23 +142,24 @@ def _filter_for(col_type) -> str:
|
||||
def _column_width(col: str, col_type) -> dict:
|
||||
"""Largeur par défaut selon la nature de la colonne.
|
||||
|
||||
`columnSize="responsiveSizeToFit"` (cf. ag_grid()) étire les colonnes
|
||||
proportionnellement à cette largeur pour remplir l'espace disponible :
|
||||
sans ça, les codes/booléens s'étirent autant que les noms/libellés.
|
||||
Sert de base à l'affichage initial (pas de columnSize="responsiveSizeToFit",
|
||||
cf. ag_grid() : les colonnes ne s'étirent/compressent pas automatiquement).
|
||||
Pas de maxWidth ici : resizable=True (defaultColDef) doit pouvoir élargir
|
||||
librement une colonne à la souris, sans plafond artificiel.
|
||||
"""
|
||||
if col == "montant":
|
||||
return {"width": 150, "maxWidth": 200}
|
||||
return {"width": 150}
|
||||
if col_type == pl.Date:
|
||||
return {"width": 140, "maxWidth": 160}
|
||||
return {"width": 220}
|
||||
if col_type.is_numeric():
|
||||
return {"width": 120, "maxWidth": 150}
|
||||
return {"width": 150}
|
||||
if col in _BOOLEAN_LIKE_COLUMNS:
|
||||
return {"width": 110, "maxWidth": 140}
|
||||
if col in _SHORT_CODE_COLUMNS or col.endswith(("_id", "_code")):
|
||||
return {"width": 130, "maxWidth": 170}
|
||||
return {"width": 110}
|
||||
if col in _SHORT_CODE_COLUMNS or col.endswith(("_code")):
|
||||
return {"width": 180}
|
||||
if col in _WIDE_LABEL_COLUMNS or col.endswith("_nom"):
|
||||
return {"width": 200}
|
||||
return {"width": 170}
|
||||
return {"width": 300}
|
||||
return {"width": 190}
|
||||
|
||||
|
||||
def grid_column_defs(hidden_columns=None):
|
||||
@@ -169,11 +191,22 @@ def grid_column_defs(hidden_columns=None):
|
||||
"hide": col in hidden,
|
||||
}
|
||||
if meta.get("description"):
|
||||
# Texte brut (pas de markdown) : le renderer "markdown" côté
|
||||
# dash-ag-grid n'est enregistré que comme cellRenderer, pas
|
||||
# comme tooltipComponent — posé sur un en-tête, il retombe sur
|
||||
# le rendu natif d'AG Grid, qui affiche la chaîne telle quelle
|
||||
# (donc "**gras**" apparaissait littéralement avec les astérisques).
|
||||
col_def["headerTooltip"] = (
|
||||
f"{meta.get('title', col)} ({col}) — {meta['description']}"
|
||||
)
|
||||
if col in _LINK_COLUMNS:
|
||||
col_def["cellRenderer"] = "markdown"
|
||||
if col in _CELL_TOOLTIP_COLUMNS:
|
||||
# Valeur complète en infobulle. Pour les colonnes-liens
|
||||
# (_LINK_COLUMNS), la cellule contient du HTML (<a href=...>) :
|
||||
# on pointe vers la copie texte brut posée par add_links() dans
|
||||
# src.utils.table plutôt que d'afficher ce balisage tel quel.
|
||||
col_def["tooltipField"] = f"{col}_tooltip" if col in _LINK_COLUMNS else col
|
||||
if col != "objet":
|
||||
col_def.update(_column_width(col, col_type))
|
||||
if col == "objet":
|
||||
@@ -183,8 +216,33 @@ def grid_column_defs(hidden_columns=None):
|
||||
# rowHeight fixe côté dashGridOptions plutôt qu'autoHeight ici.
|
||||
col_def["wrapText"] = True
|
||||
col_def["minWidth"] = 360
|
||||
# Le texte tronqué par la hauteur de ligne fixe reste consultable
|
||||
# en entier via l'infobulle native AG Grid au survol.
|
||||
col_def["tooltipField"] = "objet"
|
||||
defs.append(col_def)
|
||||
return defs
|
||||
|
||||
|
||||
def apply_persisted_layout(
|
||||
defs: list[dict], column_state: list[dict] | None
|
||||
) -> list[dict]:
|
||||
"""Réapplique la largeur et l'ordre des colonnes personnalisés par
|
||||
l'utilisateur (columnState restauré via la persistance AG Grid) par-dessus
|
||||
des columnDefs fraîchement régénérés par grid_column_defs(), qui eux
|
||||
ignorent tout état précédent et ne portent que la largeur par défaut et
|
||||
l'ordre du schéma.
|
||||
|
||||
Sans ça, tout changement de columnDefs (à chaque chargement de page ou
|
||||
changement de colonnes affichées) écrase silencieusement le
|
||||
redimensionnement/réordonnancement fait par l'utilisateur, cf. #47.
|
||||
"""
|
||||
if not column_state:
|
||||
return defs
|
||||
order = {col["colId"]: i for i, col in enumerate(column_state)}
|
||||
widths = {col["colId"]: col["width"] for col in column_state if col.get("width")}
|
||||
for col_def in defs:
|
||||
field = col_def["field"]
|
||||
if field in widths:
|
||||
col_def["width"] = widths[field]
|
||||
# Colonnes déjà connues dans leur ordre persisté, colonnes nouvelles
|
||||
# (ex. ajout au schéma, jamais vues dans columnState) à la fin dans
|
||||
# leur ordre par défaut.
|
||||
defs.sort(key=lambda d: order.get(d["field"], len(order)))
|
||||
return defs
|
||||
|
||||
@@ -50,7 +50,26 @@ def add_resource_link(dff: pl.DataFrame) -> pl.DataFrame:
|
||||
return dff
|
||||
|
||||
|
||||
_TOOLTIP_SHADOW_COLUMNS = [
|
||||
"acheteur_nom",
|
||||
"titulaire_nom",
|
||||
"acheteur_id",
|
||||
"titulaire_id",
|
||||
]
|
||||
|
||||
|
||||
def add_links(dff: pl.DataFrame):
|
||||
# Copie du texte brut avant réécriture en HTML ci-dessous, pour que la
|
||||
# grille AG Grid (cf. grid_column_defs dans src.utils.grid) puisse
|
||||
# afficher une infobulle lisible sur ces colonnes plutôt que le balisage
|
||||
# <a href=...> brut.
|
||||
dff = dff.with_columns(
|
||||
[
|
||||
pl.col(c).alias(f"{c}_tooltip")
|
||||
for c in _TOOLTIP_SHADOW_COLUMNS
|
||||
if c in dff.columns
|
||||
]
|
||||
)
|
||||
for col in ["uid", "acheteur_nom", "titulaire_nom", "acheteur_id", "titulaire_id"]:
|
||||
if col in dff.columns:
|
||||
if col.startswith("titulaire_"):
|
||||
|
||||
Reference in New Issue
Block a user