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

Fonctionnement

<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).

Attributs

AttributTypeFormat / ValeursDescription
idStringid="mon-query"Identifiant unique (requis). Les composants de visualisation l'utilisent pour s'abonner aux données transformees.
sourceStringsource="id-source"ID de la source de données (dsfr-data-source ou dsfr-data-normalize)
whereString"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).
filterString(même format que where)Alias pour where (compatibilite)
group-byString"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.
aggregateString"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-byString"champ:asc" ou "champ:desc"Tri des résultats Format: "field:direction" ou "field__function:direction" Ex: "total_pop:desc" ou "population__sum:desc"
limitNumberEntier positif. 0 = illimiteLimite 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).

Syntaxe detaillee des attributs clés

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.

aggregate

Format 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-by

Format : 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 -->

Champs multivalués : 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.

AttributTypeDescription
explodeStringChamps 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.

En attente d'un filtre : 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.

AttributTypeDescription
require-whereBooleanN'é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.

Operateurs de filtre

Utilisables dans les attributs where / filter (format "champ:operateur:valeur") :

OperateurDescriptionExemple
eqEgal astatus:eq:active
neqDifferent destatus:neq:archived
gtStrictement superieurpopulation:gt:5000
gteSuperieur ou egalpopulation:gte:5000
ltStrictement inferieurpopulation:lt:1000
lteInferieur ou egalpopulation:lte:1000
containsContient (insensible a la casse)nom:contains:paris
notcontainsNe contient pasnom:notcontains:test
inDans une liste (separateur |)region:in:IDF|OCC|BRE
notinPas dans une listeregion:notin:IDF|OCC
isnullEst null/undefinedemail:isnull
isnotnullN'est pas nullemail:isnotnull

Exemples

Mode generic : filtrer et trier des données existantes

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>

Mode generic : grouper et agréger

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>

OpenDataSoft : requête serveur via dsfr-data-source

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>

OpenDataSoft : requête data.economie.gouv.fr via dsfr-data-source

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>

Combinaison de filtres multiples

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>

Dataset prive avec headers

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).

Rafraichissement automatique

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>

Événements

Le composant emet les mêmes événements que <dsfr-data-source> :

Méthodes publiques

MéthodeRetourDescription
reload()voidForce le rechargement / re-traitement des données
getData()ArrayRetourne les données transformees actuelles
isLoading()BooleanIndique si un traitement est en cours
getError()Error | nullRetourne l'erreur eventuelle

Conseils d'utilisation

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.