République
française
dsfr-data
Documentation — Web Components DSFR pour la dataviz
Composant de transformation de données.
Filtre, groupe, agrège et trie les données recues d'une <dsfr-data-source>
avant de les transmettre aux composants de visualisation.
Ne fait aucun fetch HTTP : c'est un pur transformateur.
Invisible : Ce composant ne rend rien visuellement. Il transforme les données et les redistribue aux composants de visualisation via le systeme d'événements.
Voir les exemples live dans le Guide utilisateur
<dsfr-data-query> est un pur transformateur de données. Il recoit les données
d'une <dsfr-data-source> (ou <dsfr-data-normalize>) via l'attribut source
et applique des transformations client-side (filter, group-by, aggregate, sort, limit).
| Attribut | Type | Format / Valeurs | Description |
|---|---|---|---|
id | String | id="mon-query" | Identifiant unique (requis). Les composants de visualisation l'utilisent pour s'abonner aux données transformees. |
source | String | source="id-source" | ID de la source de données (dsfr-data-source ou dsfr-data-normalize) |
where | String | "champ:op:val, ..." | Clause WHERE / Filtres — syntaxe colon UNIQUEMENT : "champ:opérateur:valeur, champ2:opérateur:valeur2" (opérateurs : eq, neq, gt, gte, lt, lte, contains, notcontains, in, notin, isnull, isnotnull — multi-valeurs séparées par |). La syntaxe ODSQL n'est PAS supportee ici (elle l'est sur le where de dsfr-data-source) : une clause non parsable est signalee via reportConfigError (#277). **La clause part au serveur** dès lors que l'amont a un adaptateur qui sait la traduire et que cette requête est seule lectrice de sa chaîne (#856) — avec ou sans group-by. Elle est traduite au dialecte de l'adaptateur (#275) et posée en overlay clé par émetteur (ADR-031) : elle se fusionne avec les clauses des facettes, de la recherche et du contexte au lieu de les écraser, et elle lève l'attente d'un require-where posé sur la source (#854). Elle reste calculée dans le navigateur quand la chaîne est partagée (#765), quand un transformateur amont renomme des colonnes (#394), quand une clause est intraduisible, ou avec explode (#736). CHAMPS MULTIPLES (#1026) : nom|commune:contains:martin applique le MÊME opérateur et la MÊME valeur à plusieurs champs, reliés par un OU — la ligne passe dès qu'un des champs satisfait la clause ; les clauses entre elles restent en ET. Délégué en or=(…) sur Tabular (une seule clause multi-champs par requête, valeur sans , . ( ) " &, ni in / notin), en (… OR …) sur Opendatasoft et Grist (SQL) ; INSEE et les sources sans adaptateur le calculent dans le navigateur. CHAMP TABLEAU (#953, ex-#842) : eq / neq / in / notin regardent DANS le tableau. tags: ['urgent','social'] matche tags:eq:urgent, comme le compte déjà value="count:tags:urgent" de dsfr-data-kpi, et comme le retient le portail quand la clause lui est déléguée. C'est ce qui a été mesuré le 2026-09-19 sur deux portails et deux endpoints — keyword du catalogue de data.economie.gouv.fr, themes_attendus de retours-formulaire-votre-avis-copie sur data.education.gouv.fr : where=champ = "x" trouve la ligne sur n'importe quel ÉLÉMENT, et jamais sur le rendu texte complet du tableau. La règle exacte, côté client : eq(valeur, v) = (valeur est un tableau et un élément vaut v) OU String(valeur) === String(v) Le second terme est un repli que le portail n'a pas (['a','b'] matche 'a,b' en local, le portail rend 0) : il est gardé pour que eq / in ne puissent que GAGNER des correspondances, jamais en perdre. neq / notin en sont la négation, donc eux en perdent — et le portail fait pareil. VALEURS ABSENTES (#958) : une ligne dont le champ est nul ne satisfait **ni eq ni neq** — la logique SQL à trois valeurs qu'applique Opendatasoft. Mesuré le 2026-09-20 sur themes_attendus de retours-formulaire-votre-avis-copie (176 lignes dont 21 nulles) : = "Elèves" -> 124, != "Elèves" -> **31** (= 155 renseignées − 124), et non 52. eq les excluait déjà ; neq les gardait, d'où le même champ:neq:valeur rendant 31 lignes délégué et 52 au client. Pour retrouver les lignes absentes, les nommer : champ:isnull. ⚠️ notin et notcontains gardent, eux, les valeurs absentes — et c'est aligné aussi : ODSQL n'a pas d'infixe not in / not like, donc ils se délèguent en NOT champ in (…) / NOT champ like "%…%", une négation booléenne qui garde les nulles (mesuré : 52). Seul != est à trois valeurs. ⚠️ tags:contains:urgent n'est toujours PAS un équivalent d'eq : il cherche une sous-chaîne dans String(tableau), donc « non-urgent » y matche « urgent », et la recherche traverse la virgule entre deux éléments. Pour éclater un multivalué avant un group-by, c'est explode (#736). Le booléen dérivé en amont (dsfr-data-normalize compute="a_urgent = when contains(tags,'urgent') then 1 else 0" puis where="a_urgent:eq:1") reste valide — le filtre final porte sur un scalaire, donc regroupable et délégable — mais il n'est plus NÉCESSAIRE pour obtenir le même compte des deux côtés. Pendant une version mineure, un avertissement de transition nomme le champ et la valeur des lignes qui se mettent à compter (dédupliqué par couple champ/valeur, jamais par ligne). |
filter | String | (même format que where) | Alias pour where (compatibilite) |
group-by | String | "champ1, champ2" | Champs de regroupement (séparés par virgule). Ordre d'application : le filtre (where de la query, de la source ou d'un dsfr-data-context) passe AVANT le regroupement — c'est aussi l'ordre ODSQL quand le regroupement est délégué au serveur. Un filtre ne peut donc pas viser un alias d'agrégat (montant__sum) : la colonne n'existe pas encore, l'API répond 400. Pour filtrer un résultat agrégé, poser une seconde dsfr-data-query en aval avec son propre where. Délégation au serveur seulement si la query est la SEULE lectrice de sa source (#765) : la source n'a qu'un regroupement, servi à tous ses abonnés. Source partagée (un KPI, un autre graphique…) : calcul côté client sur les lignes chargées, avec un avertissement. Pour garder l'agrégation serveur, donner à la query sa propre dsfr-data-source. |
aggregate | String | "champ:fn" ou "champ:fn:alias" | Agrégations pour mode generic/tabular Format: "field:function, field2:function" Ex: "population:sum, count:count" running_sum (#738) n'est pas une réduction de groupe mais un CUMUL : il produit une ligne par ligne de sortie, chacune portant la somme des précédentes, calculée APRÈS order-by. Sans order-by, l'ordre des lignes reçues fait foi et le résultat n'a en général pas de sens : un avertissement console le signale. Le cumul reste toujours côté client. Ex. group-by="mois" aggregate="montant:sum, montant__sum:running_sum" avec order-by="mois:asc". diff (#775) en est l'inverse : l'écart de chaque ligne avec la précédente, pour retrouver le flux d'une série publiée déjà cumulée (aggregate="cumul:diff" → colonne cumul__diff). Mêmes règles : après order-by, jamais délégué, avertissement sans order-by. La première ligne vaut null, jamais 0 — un incrément inconnu n'est pas un incrément nul ; une valeur non numérique donne null pour elle et pour la suivante. ## share et share_percent — la part du total (#926) champ:share rend, pour chaque ligne de sortie, **la valeur de la ligne divisée par la somme de cette colonne sur toutes les lignes de sortie** — une répartition, sans seconde source ni jointure. Ex. group-by="typologie" aggregate="lics:sum, lics__sum:share" produit lics__sum__share (0,334 pour 33,4 %). share_percent rend la même part **en points de pourcentage** (33,4), la forme qu'attend un axe de graphique : une fraction dessinée sur un axe intitulé « % » y afficherait 0,33. Réserver share à ce qui sera formaté (dsfr-data-kpi format="pourcentage", qui met une fraction à l'échelle, comme le ratio de #673). **Le dénominateur, et ce qu'il signifie.** C'est la somme de la colonne sur les lignes de sortie **avant limit** — pas sur le jeu entier. Trois conséquences, qui sont le piège de cette fonction bien plus que sa syntaxe : - une part est toujours une part **de l'ensemble filtré** : where, facettes, recherche et dsfr-data-context déplacent le dénominateur. C'est presque toujours ce qu'on veut (« part des licences de cette région »), mais il faut le dire en page : le même graphique montre 33,4 % sans filtre et 16,3 % en Bretagne, et les deux sont justes ; - avec limit, les parts affichées **ne somment pas à 100 %** : un top 10 montre la part de chaque ligne dans le TOUT, pas dans le top 10. C'est volontaire — l'inverse ferait d'une troncature d'affichage une redéfinition silencieuse du total ; - si la source est tronquée (max-records, pagination), le dénominateur l'est aussi. Un total faux ne se voit pas : les parts somment quand même à 100 %. **Ce qu'une part suppose.** Que les lignes soient une partition — chaque unité comptée une fois et une seule. Après explode, une ligne multivaluée compte dans N groupes : les parts somment alors à plus de 100 %, et il faut écrire au lecteur « part des licences portant ce label », pas « répartition ». Une colonne qui mêle des signes opposés n'a pas de part : la somme peut s'annuler. **Règles de calcul.** Total nul, absent ou non numérique : la part vaut null, jamais l'infini ni un zéro de complaisance. Une valeur non numérique donne null pour sa ligne et ne compte pas au dénominateur (même règle que sum, #301). Comme les cumulées : calcul toujours côté client, jamais délégué — et, comme elles, **demander une part empêche la délégation serveur du regroupement** : la query regroupe alors sur les lignes chargées, donc relever max-records avant de poser l'attribut sur un jeu volumineux. L'ordre des lignes, lui, est indifférent : pas d'order-by requis, pas d'avertissement. |
order-by | String | "champ:asc" ou "champ:desc" | Tri des résultats Format: "field:direction" ou "field__function:direction" Ex: "total_pop:desc" ou "population__sum:desc" |
limit | Number | Entier positif. 0 = illimite | Limite de résultats |
Attributs retires (#277, #279) : transform, page-size et refresh se configurent sur <dsfr-data-source> ; server-side est sans objet (le relais de commandes vers la source est toujours actif).
where / filter (modes generic et tabular)Format : champ:operateur:valeur. Plusieurs filtres separes par , (logique ET).
where="population:gt:5000" where="population:gt:5000, region:in:IDF|OCC|BRE" where="email:isnull" <!-- pas de valeur pour isnull/isnotnull --> where="region:in:IDF|OCC|BRE" <!-- separateur | pour in/notin -->
Les valeurs "true"/"false" sont parsees en booleen, les chaines numériques en nombre.
aggregateFormat de base : champ:fonction — le champ de sortie est nomme champ__fonction.
Avec alias : champ:fonction:alias — le champ de sortie porte le nom de l'alias.
aggregate="population:sum" <!-- sortie : population__sum --> aggregate="population:sum:total" <!-- sortie : total --> aggregate="pop:sum:total, pop:count:nb" <!-- deux agrégations -->
Fonctions : count (nombre de lignes), sum, avg, min, max.
order-byFormat : champ:direction ou champ (défaut : asc).
Pour trier des resultats agrégés, utiliser le nom du champ de sortie.
order-by="nom:asc" order-by="population__sum:desc" <!-- champ agrège sans alias --> order-by="total:desc" <!-- champ agrège avec alias -->
explode
Une cellule qui contient une liste (["a", "b"], une ChoiceList Grist, un champ
découpé par split sur dsfr-data-normalize)
n'est pas une modalité. Sans explode, elle est ramenée en chaîne pour former la clé
de groupe : "a,b" devient une modalité à part entière, une COMBINAISON comptée pour
une valeur — là où dsfr-data-facets éclate le même champ.
Deux composants branchés sur le même champ donnaient donc des chiffres différents sans rien
signaler (#736). explode aligne le regroupement sur les facettes.
| Attribut | Type | Description |
|---|---|---|
explode | String | Champs multivalués à éclater avant le regroupement (séparés par virgule). Sans cet attribut, une cellule tableau est ramenée en chaîne pour la clé de groupe : ["a", "b"] devient la modalité "a,b", une COMBINAISON comptée comme une valeur — là où dsfr-data-facets éclate le même champ (#421). Les deux composants branchés sur le même champ donnaient donc des chiffres différents, sans rien signaler (#736). Avec explode="tags", chaque élément de la cellule produit sa propre ligne : les modalités du regroupement sont exactement celles de la facette du même champ, et une ligne portant N valeurs compte dans N groupes (les agrégats la comptent donc N fois). Règles, alignées sur les facettes : les éléments vides sont ignorés, et une cellule sans aucune valeur (tableau vide, null, chaîne vide) ne produit AUCUNE ligne — pas de groupe « non renseigné », comme la facette n'a pas de modalité vide. Une cellule scalaire est inchangée. Chaque champ listé doit figurer dans group-by (sinon erreur de configuration et champ ignoré : éclater un champ hors regroupement dupliquerait les lignes et gonflerait les sommes). L'éclatement force le regroupement CÔTÉ CLIENT : aucune API du pipeline ne sait éclater un champ multivalué, déléguer produirait à nouveau des combinaisons. Sur une source volumineuse, penser au plafond de lignes rapatriées. Par défaut vide : le comportement historique est conservé. |
<dsfr-data-query id="par-theme" source="src" group-by="themes" explode="themes" aggregate="montant:sum" order-by="montant__sum:desc"> </dsfr-data-query>
Une ligne portant N valeurs compte dans N groupes : les agrégats la comptent donc N fois — c'est
voulu, et c'est ce que fait déjà la facette. Les éléments vides sont ignorés, et une cellule sans
aucune valeur (tableau vide, null, chaîne vide) ne produit aucune ligne : pas de
groupe « non renseigné ». Une cellule scalaire est inchangée. Chaque champ listé doit figurer dans
group-by, sinon la configuration est refusée et le champ ignoré : éclater un champ
hors regroupement dupliquerait les lignes et gonflerait les sommes. L'éclatement force le
regroupement côté client — aucune API du pipeline ne sait éclater un champ
multivalué : sur une source volumineuse, penser au plafond de lignes rapatriées.
require-where
Sur une page d'exploration, afficher le jeu entier avant tout choix de l'utilisateur est au mieux
inutile, au pire coûteux. require-where met la requête en attente : elle n'émet
aucune ligne, publie dsfr-data-idle, et les afficheurs en aval rendent leur
idle-message au lieu du jeu complet.
| Attribut | Type | Description |
|---|---|---|
require-where | Boolean | N'émettre aucune ligne tant qu'aucun filtre n'est posé (#690). Pendant de require-where sur dsfr-data-source, pour les pages d'exploration : la requête reste en attente, émet dsfr-data-idle, et les afficheurs en aval rendent « choisissez un filtre » au lieu du jeu entier. Ce qui compte comme filtre : le where (ou filter) de CETTE requête — sur un query, c'est la surface de filtrage que la page pilote — et toute clause where non vide reçue par commande (facettes, recherche, dsfr-data-context). Tout retirer fait repasser la requête en attente. |
Ce qui compte comme filtre : le where (ou filter) de CETTE requête —
sur un query, c'est la surface de filtrage que la page pilote — et toute clause where
non vide reçue par commande (facettes, recherche,
dsfr-data-context). Tout retirer fait repasser la requête en
attente : jamais de requête « tout » implicite. C'est le pendant client de
require-where sur
<dsfr-data-source>, qui, lui, empêche le fetch.
Utilisables dans les attributs where / filter (format "champ:operateur:valeur") :
| Operateur | Description | Exemple |
|---|---|---|
eq | Egal a | status:eq:active |
neq | Different de | status:neq:archived |
gt | Strictement superieur | population:gt:5000 |
gte | Superieur ou egal | population:gte:5000 |
lt | Strictement inferieur | population:lt:1000 |
lte | Inferieur ou egal | population:lte:1000 |
contains | Contient (insensible a la casse) | nom:contains:paris |
notcontains | Ne contient pas | nom:notcontains:test |
in | Dans une liste (separateur |) | region:in:IDF|OCC|BRE |
notin | Pas dans une liste | region:notin:IDF|OCC |
isnull | Est null/undefined | email:isnull |
isnotnull | N'est pas null | email:isnotnull |
Recupere les données d'une <dsfr-data-source> et les filtre/trie cote client.
Ici on filtre les 10 premieres lignes triees par nom.
Voir l'exemple live
<dsfr-data-source id="raw-data" url="https://mon-api.gouv.fr/records" transform="results" ></dsfr-data-source> <dsfr-data-query id="filtered-data" source="raw-data" where="population:gt:5000" order-by="nom:asc" limit="10" ></dsfr-data-query> <dsfr-data-chart source="filtered-data" type="bar" label-field="nom" value-field="population" ></dsfr-data-chart>
Regroupe les données par region et calcule la somme de la population et le nombre d'enregistrements par groupe. Voir l'exemple live
<dsfr-data-source id="communes" url="https://mon-api.gouv.fr/communes" transform="results" ></dsfr-data-source> <dsfr-data-query id="stats-region" source="communes" group-by="region" aggregate="population:sum, population:count" order-by="population__sum:desc" limit="10" ></dsfr-data-query> <dsfr-data-chart source="stats-region" type="bar" label-field="region" value-field="population__sum" ></dsfr-data-chart>
Utilise <dsfr-data-source> pour interroger une API OpenDataSoft,
puis <dsfr-data-query> pour le tri et la limite cote client.
Voir l'exemple live
<dsfr-data-source id="ods-src" api-type="opendatasoft" dataset-id="demographyref-communes-2021" base-url="https://data.opendatasoft.com" select="sum(pop_municipale) as total_pop, nom_reg" where="pop_municipale > 5000" group-by="nom_reg" ></dsfr-data-source> <dsfr-data-query id="ods-stats" source="ods-src" order-by="total_pop:desc" limit="15" ></dsfr-data-query> <dsfr-data-chart source="ods-stats" type="bar" label-field="nom_reg" value-field="total_pop" ></dsfr-data-chart>
Utilise <dsfr-data-source> pour interroger une API OpenDataSoft,
puis <dsfr-data-query> pour le groupement et l'agrégation.
Voir l'exemple live
<dsfr-data-source id="ods-src" api-type="opendatasoft" dataset-id="rappelconso-v2-gtin-trie" base-url="https://data.economie.gouv.fr" server-side page-size="100" ></dsfr-data-source> <dsfr-data-query id="ods-stats" source="ods-src" group-by="categorie_produit" aggregate="libelle:count:nombre" order-by="nombre:desc" limit="8" ></dsfr-data-query> <dsfr-data-chart source="ods-stats" type="pie" label-field="categorie_produit" value-field="nombre" ></dsfr-data-chart>
Plusieurs filtres peuvent etre combines en les separant par des virgules. Tous les filtres doivent etre satisfaits (logique ET). Voir l'exemple live
<dsfr-data-query id="filtered" source="raw-data" where="population:gte:10000, region:in:IDF|OCC|BRE, status:eq:active" order-by="population:desc" limit="20" ></dsfr-data-query>
L'attribut headers sur <dsfr-data-source> permet de passer des headers HTTP
(API key, Bearer token) pour acceder a des datasets prives.
Voir l'exemple live
<dsfr-data-source id="private-src" api-type="opendatasoft" dataset-id="mon-dataset-prive" base-url="https://mon-instance.opendatasoft.com" headers='{"apikey":"ma-clé-api"}' ></dsfr-data-source> <dsfr-data-query id="private-data" source="private-src" group-by="region" aggregate="population:sum:total" order-by="total:desc" ></dsfr-data-query> <dsfr-data-chart source="private-data" type="bar" label-field="region" value-field="total" ></dsfr-data-chart>
Securite : les headers sont visibles dans le code source HTML. Ne les utilisez que pour des clés a acces restreint (lecture seule) ou dans des contextes proteges (intranet, applications internes).
L'attribut refresh sur <dsfr-data-source> permet de re-exécuter
la requête a intervalles reguliers.
<dsfr-data-source id="live-src" api-type="opendatasoft" dataset-id="mon-dataset" base-url="https://data.opendatasoft.com" select="count(*) as total" refresh="30" ></dsfr-data-source> <dsfr-data-query id="live-stats" source="live-src" ></dsfr-data-query>
Le composant emet les mêmes événements que <dsfr-data-source> :
dsfr-data-loaded : Données transformees disponiblesdsfr-data-loading : Traitement en coursdsfr-data-error : Erreur de traitement ou de requête| Méthode | Retour | Description |
|---|---|---|
reload() | void | Force le rechargement / re-traitement des données |
getData() | Array | Retourne les données transformees actuelles |
isLoading() | Boolean | Indique si un traitement est en cours |
getError() | Error | null | Retourne l'erreur eventuelle |
Pipeline recommande : Utilisez <dsfr-data-source> pour le fetch HTTP
et <dsfr-data-query> pour les transformations client-side (filter, group-by, aggregate, sort).
Pour les gros volumes, deleguer les agrégations a la source via les attributs de <dsfr-data-source>.
Nommage des champs agrégés : En mode generic, les champs agrégés sont nommes
automatiquement champ__fonction (ex: population__sum, population__count).
Vous pouvez definir un alias avec le format "champ:fonction:alias".
Chainabilite : Un <dsfr-data-query> peut servir de source a un autre
<dsfr-data-query> en utilisant son id comme attribut source,
permettant de construire des pipelines de transformation.