Skip to content

epic(core): dsfr-data-pivot — repli long → wide (cast/spread), symétrique de dsfr-data-unpivot #255

Description

@bmatge

Contexte

Le pipeline source → normalize → query → (chart|list|kpi) suppose un format long/tidy : une observation par ligne. Parfait pour alimenter un graphe, mais insuffisant dès qu'on veut un tableau croisé (cross-tab) : entités en lignes, valeurs d'un champ catégoriel en colonnes, mesures en cellules.

Beaucoup de jeux ODS / Grist sont servis en long :

entite | critere    | valeur
A      | Critère 1  | 24
A      | Critère 2  | 39
B      | Critère 1  | 0
B      | Critère 2  | 0

…alors que l'utilisateur attend la forme wide (lisible, comparable ligne à ligne) :

entite | Critère 1 | Critère 2
A      | 24        | 39
B      | 0         | 0

Aujourd'hui cette bascule long → wide n'a aucun composant. On la fait en JavaScript écrit à la main : group-by sur l'identité de ligne, éclatement du champ pivot en colonnes, remplissage des trous. Cet epic la rend déclarative.

L'asymétrie à corriger

La lib possède déjà dsfr-data-unpivot (#225) qui fait wide → long. Sa propre doc le dit : « c'est l'inverse exact d'un pivot ». Le sens retour long → wide manque. Cet epic ajoute le pendant symétrique : dsfr-data-pivot.

Cas d'usage cible : un agent publie un jeu en format long (entité × critère → valeur) et veut un tableau croisé déclaratif — sans savoir coder.

Vérifications faites sur l'état de la lib

  • dsfr-data-unpivot = wide → long uniquement, unidirectionnel (packages/core/src/components/dsfr-data-unpivot.ts). Pas de mode retour.
  • Aucun composant pivot / spread / cast / crosstab n'existe (recherche repo : 0 occurrence de dsfr-data-pivot).
  • dsfr-data-normalize ne fabrique pas de colonnes par éclatement (le compute de feat(core): attribut compute sur dsfr-data-normalize (colonnes calculées, version simple) #226 est ligne-à-ligne, pas un group-by/spread).
  • dsfr-data-join assemble N sources sur une clé, mais ne déplie pas un champ catégoriel en colonnes.
  • dsfr-data-list : si colonnes est omis, toutes les clés du 1er objet deviennent colonnes → un pivot produisant des clés dynamiques s'affiche tout seul ; avec colonnes, on choisit/ordonne.
  • Réactivité : comme unpivot/join, le composant doit recalculer quand la source amont ré-émet.

Décision de placement

Nouveau composant invisible, pur transformateur, frère de dsfr-data-query / dsfr-data-join / dsfr-data-unpivot. Pas dans normalize (préparation ligne-à-ligne) ni query (agrégation pure) : le pivot remodèle la forme du tableau, il mérite son propre poste. La valeur est laissée brute ; le typage reste délégué à dsfr-data-normalize (numeric-auto) — exactement comme unpivot.

API proposée

Attribut Type Défaut Requis Description
id String oui Identifiant de la sortie.
source String "" oui Source amont (format long).
id-cols String "" oui Colonnes formant l'identité de ligne (l'index), virgule-séparées. Les lignes sont regroupées par leur combinaison. Ex : "entite, type".
pivot-col String "" oui Colonne dont les valeurs deviennent des noms de colonnes. Ex : "critere". (alias accepté : names-from)
value-col String "" oui Colonne dont les valeurs remplissent les cellules. Ex : "valeur". (alias : values-from)
agg String "first" non Réduction quand plusieurs lignes tombent dans la même cellule : first, last, sum, mean, min, max, count, concat.
fill String "" non Valeur des cellules absentes (entité sans relevé pour ce critère). Ex : "NC".
pivot-values String "" non Liste blanche ordonnée des valeurs de pivot-col à matérialiser en colonnes (et leur ordre). Vide = dérivées des données. Permet de figer / choisir / ordonner les colonnes (sélecteur de colonnes déclaratif).
sort String "first-seen" non Ordre des colonnes dérivées quand pivot-values est vide : first-seen, asc, desc.
prefix String "" non Préfixe des colonnes générées (évite la collision avec id-cols). Ex : "c_".

Décisions / points ouverts à trancher dans le cœur

  • Schéma dynamique : le jeu de colonnes dépend des données. Documenter le contrat pour les consommateurs aval (dsfr-data-list sans colonnes = tout ; avec colonnes ou pivot-values = stable).
  • Collision pivot valueid-col : préfixer (prefix) ou erreur explicite (data-dsfr-config-error, pattern existant).
  • Cellule multi-valeurs sans agg compatible : défaut first + warning, ou erreur ?
  • Valeur nulle côté pivot-col : ligne ignorée vs colonne "(vide)".

✅ Critère d'acceptation n°1 — cas-exemple en HTML pur (ZÉRO JS)

Tableau croisé depuis une source long, sans aucun <script> :

<dsfr-data-source id="long" data='[
  {"entite":"A","critere":"Critère 1","valeur":24},
  {"entite":"A","critere":"Critère 2","valeur":39},
  {"entite":"B","critere":"Critère 1","valeur":0},
  {"entite":"B","critere":"Critère 2","valeur":0}
]'></dsfr-data-source>

<!-- repli long → wide : les critères deviennent des colonnes -->
<dsfr-data-pivot id="wide" source="long"
  id-cols="entite"
  pivot-col="critere"
  value-col="valeur"
  agg="first" fill="NC">
</dsfr-data-pivot>

<!-- la grille affiche : entite | Critère 1 | Critère 2 -->
<dsfr-data-list source="wide"
  colonnes="entite:Entité, Critère 1:Critère 1, Critère 2:Critère 2"
  tri="entite:asc" export="csv,html">
</dsfr-data-list>

Sous-issues

  • cœur dsfr-data-pivot (group-by id-cols, pivot-col → colonnes, value-col → cellules, agg, fill)
  • pivot-values (liste blanche ordonnée) + sort + prefix
  • agrégations first / last / sum / mean / min / max / count / concat + gestion collisions / erreurs
  • réactivité (recalcul sur ré-émission amont) + nouvelles valeurs de pivot-col → nouvelles colonnes
  • doc + spec + skill (+ exemple guide) ; node pipeline-helper ; éventuelle action builder-IA
  • (optionnel) column-picker sur dsfr-data-list ou pivot-values pilotable, pour choisir dynamiquement les colonnes affichées

Critères d'acceptation de l'epic

  1. Le bloc ci-dessus rend le tableau croisé attendu depuis une source long, sans aucun <script>.
  2. Cellules correctes ; cellule absente → fill ; agg respecté quand plusieurs relevés tombent dans la même cellule.
  3. Une nouvelle valeur de pivot-col (ex. "Critère 3") ajoutée à la source apparaît comme nouvelle colonne sans modifier le HTML (symétrie avec le critère « nouveau mois » de feat(core): dsfr-data-unpivot — bascule tableur wide → tidy (melt déclaratif) #225).
  4. pivot-values fige / ordonne / sélectionne le jeu de colonnes ; consommé proprement par dsfr-data-list (colonnes) et dsfr-data-chart.
  5. Tests unitaires pivot verts ; tests/apps/builder-ia/skills.test.ts vert ; CI verte ; changeset minor présent.

Liens

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestepicfeature-requestNouvelle fonctionnalitépkg:coreTouche la lib packages/core (composants dsfr-data-*)status:parkedParked / on hold — re-évaluer plus tard (souvent : user testing requis avant action)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions