Composant invisible de connexion aux données. Récupère des données depuis une API REST (URL brute ou via un adapter specialise) et les distribue aux autres composants via le système d'événements. Supporte aussi les données JSON inline.

Invisible : Ce composant ne rend rien visuellement. Il agit comme un "pont" entre votre API et les composants de visualisation.

dsfr-data-source vs dsfr-data-query

dsfr-data-source se connecte a une API, récupère les données et les redistribue. En mode adapter, il gère la pagination automatique, les filtres server-side et la pagination pilotable (server-side).

dsfr-data-query est une couche de transformation client-side : il filtre, groupe, agrège et trie les données. Il ecoute un dsfr-data-source via l'attribut source et traite les données côté client.

Pattern recommande : <dsfr-data-source api-type="..."> + <dsfr-data-query source="..."> pour combiner fetch serveur et transformation client.

Modes de fonctionnement

Le composant supporte trois modes :

ModeDescription
URL brute Appel HTTP générique via l'attribut url. Supporte GET/POST, headers, params JSON, extraction via transform et pagination manuelle via paginate.
Adapter Active via api-type (sans url). L'adapter gère la construction d'URL, la pagination automatique, les filtres et le parsing spécifiques au provider (OpenDataSoft, Tabular, Grist, INSEE Melodi).
Inline data Données JSON passees directement via l'attribut data. Pas de fetch HTTP.

Types d'adapter

En mode adapter, l'attribut api-type sélectionne le provider :

api-typeAPIIdentifiant requisPaginationMax records
opendatasoft OpenDataSoft (v2.1) dataset-id offset, 100/page, max 10 pages 1 000 (relevable : max-records)
tabular Tabular API (data.gouv.fr) resource page, 200/page, max 125 pages 25 000 (relevable : max-records)
grist Grist (grist.numerique.gouv.fr, getgrist.com) base-url offset, 100/page, illimitee Illimite
insee INSEE Melodi (api.insee.fr/melodi) dataset-id page, 1 000/page, max 100 pages 100 000

Attributs

Attributs communs

AttributTypeFormat / ValeursDescription
idStringid="ma-source"Identifiant unique (requis). Les composants en aval l'utilisent pour s'abonner aux données.
refreshNumberEntier en secondes. 0 = offRafraîchissement automatique en secondes (0 = désactivé).
transformString"results", "data.items"Chemin JSONPath vers le tableau de données dans la réponse. Ex: "results", "data.items".
headersString'{"apikey":"abc123"}'En-têtes HTTP en JSON. Ex: '{"Authorization": "Bearer xxx"}'. Quand le fournisseur attend sa clé sous un en-tête précis (déclaré par sa configuration), un apikey nu est réécrit automatiquement au bon format (#655) ; détail par fournisseur : table des capacités d'ARCHITECTURE.
api-key-refString"tmdb"Référence vers une clé API déclarée dans window.DSFR_DATA_KEYS
cache-ttlNumberEntier en secondes. Défaut : 3600TTL du cache externe en secondes (0 = desactive). Actif uniquement si la page hote enregistre window.DSFR_DATA_CACHE_PROVIDER (#307) — no-op en embed anonyme. C'est un repli hors ligne, côté navigateur : il n'a AUCUN rapport avec le relais (relay-url), ne lui est pas transmis et ne règle pas la durée de son cache.

Attributs mode URL brute

AttributTypeFormat / ValeursDescription
urlStringURL relative ou absolueURL de l'API a interroger (mode URL brute). Vide en mode adapter ou en mode data inline.
methodGET · POSTGET | POSTMéthode HTTP : GET (défaut) ou POST.
paramsString'{"limit": 100}'Paramètres de requête en JSON. Mode URL : query string en GET, corps de la requête en POST. **Mode adaptateur** (#726) : les paires sont ajoutées à l'URL construite par l'adaptateur, ce qui sert les paramètres propres à l'API que la bibliothèque ne modélise pas — par exemple params='{"timezone":"Europe/Paris"}' pour lire des dates dans un fuseau donné, sans quitter le mode adaptateur. Les clés que l'adaptateur construit lui-même (il les déclare : clauses, pagination, projection, suffixes d'opérateur) sont réservées : elles sont refusées avec une erreur de configuration plutôt que d'écraser une clause (#1137). Seuls les adaptateurs qui acceptent des paramètres libres les transmettent (en chargement paginé comme en fetch-mode="export") ; les autres les ignorent — voir la table des capacités d'ARCHITECTURE.
paginateBooleanPresence = trueActive la pagination serveur en mode URL : injecte page/page_size dans l'URL et publie la meta. La source ne livre alors qu'UNE page. Une dsfr-data-query en aval qui regroupe ou agrège (group-by, aggregate, explode) calcule sur cette seule page : le chiffre est partiel. Le mode URL ne délègue rien et aucun attribut ne lui fait charger le jeu entier — sans paginate, c'est la page par défaut de l'API qui revient. La requête le dit donc sans se refuser (#1242) : avertissement en console, réserve au volet Diagnostic. Pour un chiffre sur tout le jeu, passer par un api-type qui sait le charger, ou par une URL qui rend déjà l'agrégat. L'ORDRE des lignes est celui de l'API, et la source ne peut rien en garantir (#1279) : elle ne connaît ni l'API ni ses paramètres de tri, et ne lit qu'une page à la fois. Une API qui pagine sans ordre total peut rendre une ligne sur deux pages et n'en jamais servir une autre. C'est à l'URL de porter un tri sur une colonne UNIQUE (ou complété par une colonne unique) — le paramètre de tri de l'API, écrit dans url ou dans params.
page-sizeNumberEntier positif. Défaut : 20Taille de page pour la pagination serveur (nombre de records par page).
use-proxyBooleanPresence = trueForce le passage par le proxy CORS générique (pour les APIs externes sans CORS). Ne vaut qu'en mode URL (url="…") : avec un api-type, l'attribut est sans effet sur un hôte que le proxy ne relaie pas par un endpoint dédié (un portail Opendatasoft, par exemple) — la requête part en direct, et la source le signale une fois en console. Sans effet sur une requête qui part au relais (relay-url) : le relais prend toutes les requêtes GET vers une autre origine, use-proxy ne garde que ce que le relais ne porte pas (requêtes POST, cible hors https).
proxy-urlString""Domaine du proxy CORS pour CETTE source (#340), prioritaire sur window.DSFR_DATA_PROXY et la config build-time. Sert à la fois la réécriture des hôtes connus et le use-proxy générique. Vide = résolution proxy globale habituelle. Ex: proxy-url="https://mon-proxy.fr". Seuls les hôtes connus sont relayés (Tabular, Grist gouv et SaaS, Albert, INSEE Melodi). Sur tout autre hôte — un portail Opendatasoft en mode adaptateur, ou une URL quelconque sans use-proxy — l'attribut est SANS EFFET : la requête part en direct vers l'API. La source l'écrit alors une fois en console (« proxy-url est sans effet »), et le volet Diagnostic le reprend. Pour faire passer un portail Opendatasoft (ou tout autre hôte) par le domaine du site, c'est relay-url qu'il faut poser : proxy-url désigne un autre contrat (endpoints dédiés et /cors-proxy, toutes méthodes, en-têtes transmis). Avec un relais, proxy-url ne sert plus que les requêtes que le relais ne porte pas (POST du mode SQL de Grist).

Attributs mode adapter

AttributTypeFormat / ValeursDescription
api-typeStringopendatasoft | tabular | grist | inseeType d'API : l'identifiant d'un adaptateur du registre (ceux de la bibliothèque, ou un adaptateur ajouté par registerAdapter). Toute autre valeur que generic active le mode adaptateur ; generic avec une url reste en mode URL.
base-urlString"https://data.iledefrance.fr"URL de base de l'API, pour les adaptateurs qui adressent un portail par son URL.
dataset-idString"mon-dataset-2024"Identifiant du jeu de données, pour les adaptateurs qui désignent un jeu par son identifiant.
resourceString"2876a346-d50c-..." (UUID)Identifiant de la ressource (fichier d'un jeu), pour les adaptateurs qui désignent une ressource.
whereStringColon : "champ:op:val"
ODS : ODSQL libre
Clause WHERE statique, déléguée à l'API de l'adaptateur. Une liste in dont une valeur porte une parenthèse ou une virgule est déléguée comme les autres (#1233) — notin, sur Tabular, ne l'est plus du tout : il est calculé dans le navigateur (#1312, voir plus bas). Sur l'API Tabular, qui écarte sans erreur une telle valeur écrite nue, l'adaptateur l'envoie entre guillemets — seule forme que l'API lise —, en chargement complet comme en pagination serveur ; les autres valeurs de la liste restent nues. Cette forme n'est écrite dans aucune documentation de l'API. Si elle est refusée, l'adaptateur se replie, et le volet Diagnostic le signale : - en chargement complet, il calcule la clause lui-même, sur les lignes chargées. Le résultat reste juste ; les autres clauses restent déléguées, mais toutes les lignes qu'elles gardent sont chargées (une requête par page, sous max-records), et un group-by posé à côté n'est plus délégué — les lignes filtrées sont rendues brutes, une dsfr-data-query en aval regroupe ; - en pagination serveur (server-side), où ce calcul n'est pas possible, la liste repart sans guillemets : la valeur est écartée par l'API et le résultat est incomplet. CLAUSE CALCULÉE PAR LA SOURCE (#1285, ADR-159). Un filtre n'est confié au serveur que si la STRUCTURE de la comparaison y est la même que dans le navigateur. contains et notcontains sont une sous-chaîne ; le like d'un portail Opendatasoft compare des mots (like "%aliment%" rend 0 ligne pour 14 366 « alimentation », mesuré). Sur Opendatasoft, une telle clause — venue d'un filtre de dsfr-data-context (dont dsfr-data-search en mode context) ou du where d'une dsfr-data-query seule lectrice de la source — n'est donc jamais confiée au portail : c'est la source qui la calcule, sur les lignes que le reste du filtre a laissées passer. Sur Tabular et Grist (#1285, lot 2), contains reste confié à leur LIKE SQL, jokers de la valeur échappés ; notcontains (leur NOT LIKE écarterait les valeurs absentes) et, sur Tabular, un contains dont la valeur porte * et notin (son not.in écarterait aussi les absents, #1312) sont calculés par la source. Cet attribut-ci suit la même règle quand il est écrit en grammaire colon (Tabular, Grist) ; sur Opendatasoft il est écrit en ODSQL et part tel quel (y écrire un like reste possible, avec sa sémantique de mots). Le résultat est juste, au prix d'un chargement plus large : toutes les lignes des autres clauses, sous max-records (au-delà, le filtre ne porte que sur les lignes lues, et la source le signale comme une troncature). Deux montages ne lui livrent pas le jeu et passent en ERREUR DE CONFIGURATION, qui nomme la clause et la correction : server-side (une page), et group-by, aggregate ou un select qui calcule posés sur la source (le serveur agrégerait des lignes non filtrées — porter le calcul par une dsfr-data-query en aval).
selectString"sum(pop) as total, region"Clause SELECT, liste séparée par des virgules. Sa grammaire dépend de l'adaptateur (table des capacités d'ARCHITECTURE, ligne « projection select ») : clause complète ou simple liste de noms de colonnes ; un adaptateur sans projection l'ignore. **Clause complète** : select="count(*) as total, region". Une expression (fonction, alias as, *, chemin pointé, opérateur) est transmise telle quelle ; un nom de champ qui n'est pas un identifiant nu (espace, accent, chiffre initial comme 1_uai) est échappé automatiquement (#767). Une virgule à l'intérieur d'une fonction ou d'une chaîne ne sépare pas. Un select fait UNIQUEMENT d'agrégats, sans group-by (select="sum(montant) as total") se charge en une requête d'une ligne, la valeur calculée par le serveur sur tout le jeu (#810) ; si le filtre ne garde aucune ligne, un count vaut 0 et les autres fonctions null. Quand une dsfr-data-query délègue son regroupement à cette source, le select émis est COMPOSÉ depuis l'aggregate de la query (colonnes d'agrégat + colonnes du group-by) : ce select ne l'écrase pas, sinon la colonne d'alias n'existerait pas dans la réponse et le chiffre affiché serait faux (#859). S'il définit une colonne par une expression aliasée (year(date) as annee) que le regroupement vise, la délégation est refusée — avertissement en console, regroupement calculé côté client. **Liste de noms de colonnes** (projection seule, #985) : select="nom, Code sexe", espaces et accents admis — l'API ne rend que ces colonnes, soit dix fois moins d'octets sur un jeu large. Aucune colonne n'est ajoutée d'office : une colonne lue en aval (graphique, liste, facette, filtre client) doit y figurer, et un nom inconnu du jeu fait répondre l'API en erreur. Sans effet quand un group-by ou un aggregate est posé (sur la source ou délégué par une query), si l'API refuse la projection à côté d'un agrégateur. Une expression (fonction, alias, *) est ignorée avec un avertissement : toutes les colonnes sont chargées.
group-byString"champ1, champ2"Group-by, délégué aux adaptateurs déclarant serverGroupBy. Avec un select en clause complète, un élément peut être une expression aliasée, avec ou sans fonction (year(date) as annee, periode as an), transmise telle quelle — l'alias as y est obligatoire (#641). Même découpe et même échappement que select (#767) : date_format(d, 'yyyy-MM') as m reste d'un seul tenant.
aggregateString"champ:fn" ou "champ:fn:alias"Agrégation, déléguée aux adaptateurs déclarant serverGroupBy.
order-byString"champ:asc" ou "champ:desc"Tri (champ:asc, champ2:desc), délégué aux adaptateurs déclarant serverOrderBy. Une API qui pagine par décalage et ne trie que sur une clé (Tabular) rend un ordre instable d'une page à l'autre dès que la clé n'est pas unique : des lignes reviennent deux fois, d'autres jamais, pour un compte juste. Un tri délégué qui s'étend sur plusieurs pages est donc rendu sûr par l'adaptateur (#1202, #1233), lignes brutes comme groupes d'un group-by : - tout le jeu est chargé : il est relu sans tri et trié sur place (une requête de plus), dans l'ordre du pipeline — vides, puis nombres, puis textes —, le même que celui d'une dsfr-data-query ; - limit ou max-records coupe le chargement : le tri reste au serveur, complété d'une clé de départage (l'identifiant de ligne, ou les autres colonnes du group-by) qui le rend total. Un chargement d'une seule page, et un regroupement trié sur sa seule colonne de regroupement, gardent le tri du serveur tel quel. Sur Opendatasoft (#1274), la pagination par décalage n'a AUCUN ordre garanti : le défaut touche aussi un chargement sans order-by. Dès qu'un chargement dépasse une page (100 lignes), l'adaptateur relit donc sur un ordre total — le tri demandé, inchangé et en tête, complété de toutes les colonnes triables du jeu (texte, nombre, date, booléen, dans l'ordre de ses champs ; pour des groupes triés, les autres clés du group-by). Coût : deux requêtes de plus, la première page relue et les métadonnées du jeu, lues une fois. Sans order-by, les lignes d'un chargement de plus d'une page arrivent alors triées sur les colonnes du jeu, et non plus dans l'ordre, arbitraire, de l'API. Un chargement d'une seule page ne change pas : ni tri ajouté, ni requête de plus. Si l'API refuse ce tri, ou si les colonnes du jeu sont illisibles, la source charge comme avant et le volet Diagnostic signale une pagination sans ordre garanti.
limitNumberEntier positif. 0 = illimiteLimite du nombre de résultats
max-recordsNumberEntier positif. 0 = plafond par défaut de l'adapterPlafond de lignes du chargement complet en mode adaptateur (#233, #1027), honoré par les adaptateurs qui paginent eux-mêmes leur chargement complet. 0 = plafond par défaut de l'adaptateur (valeurs par adaptateur : table des capacités d'ARCHITECTURE, ligne « plafond fetchAll »). À relever explicitement pour charger un jeu plus long par la pagination — par exemple une carte des ≈ 35 000 communes (max-records="40000") — ou pour les tableaux de bord « un fetch, N agrégations client » : attention au nombre de requêtes en boucle (une par page de l'API) et au poids mémoire. Un limit plus petit reste prioritaire. Quand le plafond coupe le jeu, la source signale la troncature (truncated) et un avertissement console cite max-records. Un chargement coupé par le plafond et trié (order-by) rend les premières lignes du tri, chacune une fois : sur Tabular, le tri délégué est complété d'une clé de départage (#1233). Si l'API la refuse, le tri du serveur est gardé tel quel et le volet Diagnostic signale un tri instable — des lignes à valeurs égales peuvent alors manquer ou être doublées aux limites de page. Sur Opendatasoft (#1274), un chargement coupé rend de même les premières lignes d'un ordre total, avec ou sans order-by : le tri demandé puis toutes les colonnes triables du jeu (voir order-by). L'API n'y sert pas plus de 10 000 lignes par la pagination, quel que soit ce plafond : au-delà, fetch-mode="export".
server-sideBooleanPresence = trueMode pagination serveur (datalist, tableaux). Ce qui est délégué ne change pas avec ce mode : une page porte les mêmes filtres, le même regroupement et les mêmes agrégats qu'un chargement complet — seule la façon dont les lignes arrivent change (#852). La source ne livre qu'UNE page : rien de ce qui se calcule côté client sur l'ensemble des lignes n'a de sens derrière elle. Une dsfr-data-query en aval dont le regroupement ou l'agrégat n'est pas délégué — part ou cumul, explode, agrégat sans group-by, fonction que l'adaptateur ne traduit pas, transformateur amont qui change les colonnes, source lue par d'autres composants, source déjà regroupée — passe en **erreur de configuration** au lieu d'émettre un chiffre partiel (#1242). Deux corrections : - retirer server-side : la source charge le jeu entier, dans la limite de max-records ; - si le jeu dépasse ce plafond, ou si une liste paginée lit la même source : donner à la requête sa propre source sans server-side, qui porte le regroupement délégable (group-by, aggregate) — le serveur regroupe alors le jeu entier, et la part ou le cumul se calcule en aval, sur les groupes. Une requête qui délègue réellement son regroupement, ou qui ne regroupe pas (filtre et tri d'un tableau paginé), n'est pas concernée. Les pages sont servies une à une et le jeu n'est jamais relu : seul le serveur peut tenir l'ordre d'une page à l'autre, faute de quoi un usager qui feuillette voit des lignes en double et n'en voit jamais d'autres. Chaque page est donc demandée sur un ordre total (#1274, #1233) : sur Opendatasoft, le tri de la liste (ou aucun) complété de toutes les colonnes triables du jeu, lues une fois dans ses métadonnées ; sur Tabular, le tri demandé complété de l'identifiant de ligne — sans tri, l'API suit déjà cet identifiant, rien n'est ajouté. Sur un très gros jeu sans filtre, ce tri allonge la réponse de la première page (mesuré sur 1,7 million de lignes : 5,5 s à froid, 0,7 s ensuite, contre 0,2 s sans tri) : un order-by sur une colonne unique ne coûte pas moins, un where qui réduit le jeu, si. Si l'API refuse la clé de départage, la page est servie comme avant et le volet Diagnostic le signale.

Relever le plafond de lignes : max-records

En mode adaptateur, le chargement complet s'arrête à un plafond de sécurité : 1 000 lignes sur Opendatasoft, 25 000 sur Tabular (125 pages de 200). max-records le relève explicitement, et vaut pour les deux adaptateurs : Opendatasoft (#233) et Tabular (#1027). Sur Tabular, une carte de toutes les communes (environ 35 000 lignes) demande max-records="40000", soit jusqu'à 200 requêtes de 200 lignes : c'est le prix à peser, en requêtes comme en mémoire. Un limit plus petit reste prioritaire.

<dsfr-data-source id="communes"
  api-type="tabular"
  resource="…"
  max-records="40000">
</dsfr-data-source>

Quand le plafond coupe le jeu, la troncature n'est pas muette : la source la signale (truncated, lisible dans le volet Diagnostic) et un avertissement console cite max-records comme moyen de la lever. C'est vrai aussi sur un group-by délégué, dont l'API ne donne pas le total : une page pleine au plafond avec une page suivante annoncée suffit à la signaler. Les autres adaptateurs (Grist, INSEE Melodi) ignorent l'attribut.

Au-delà d'une page : la source pagine sur un ordre total

Une API qui pagine par décalage ne rend chaque ligne une seule fois que si l'ordre est le même d'une page à l'autre. Sinon des lignes reviennent deux fois, d'autres jamais, et le nombre de lignes reste juste : rien ne le signale, et un sous-total, un cumul ou un historique sont faux. La source s'en charge, sans rien à écrire dans la page (#1202, #1233, #1274).

ChargementOpendatasoftTabular
Une seule page (100 lignes, 200 sur Tabular)Rien ne change : ni tri ajouté, ni requête de plus.Rien ne change.
Plusieurs pages, jeu completLa première page sert de sonde ; le jeu est relu trié sur order-by puis sur toutes ses colonnes triables. Deux requêtes de plus.Avec order-by : relu sans tri, trié sur place (une requête de plus). Sans : inchangé.
Coupé par limit ou max-recordsMême ordre total : les premières lignes de order-by puis des colonnes du jeu.Avec order-by : tri complété de l'identifiant de ligne.
server-sideChaque page part sur l'ordre total.Avec un tri : complété de l'identifiant de ligne. Sans : inchangé.
fetch-mode="export"Une requête : non concerné.Non concerné.

Sur Opendatasoft, l'API ne garantit aucun ordre sans tri : le défaut touche aussi une source sans order-by, et pas à chaque chargement (mesuré sur un jeu de 321 lignes : 297 distinctes dix fois sur dix un jour, 321 cinq fois sur cinq un autre). Conséquence visible : sans order-by, les lignes d'un jeu de plus de 100 lignes arrivent triées sur les colonnes du jeu, dans l'ordre de ses champs, et non plus dans l'ordre de l'API. Pour un autre ordre, poser order-by ; un cumul calculé en aval sans tri suit l'ordre livré.

Les colonnes retenues sont celles que l'API sait trier (texte, nombre, date, booléen), lues une fois dans les métadonnées du jeu ; pour des groupes triés, ce sont les autres clés du group-by. Sur un jeu de plus d'un million de lignes sans where, ce tri allonge la première réponse de quelques secondes. Si l'API refuse la clé de départage, la source charge comme avant et le volet Diagnostic le signale (« pagination sans ordre garanti » sur Opendatasoft, « tri serveur instable » sur Tabular) : poser alors un order-by sur une colonne unique, ou charger en une requête avec fetch-mode="export".

Grist. Un chargement complet tient en une requête : rien à paginer. En server-side, l'API /records ne sait pas sauter de lignes — son paramètre offset est ignoré, sans erreur. La source demande donc la page N par limit = N × page-size et n'en garde que la dernière tranche : chaque page est juste, et la page N coûte N pages de transfert. Un order-by paginé est complété par l'identifiant de ligne de Grist (id), pour que les ex-æquo ne changent pas de page. Avec un where à opérateur avancé, un group-by ou un aggregate, la requête passe par l'endpoint SQL du document, paginé sur le même ordre total.

INSEE Melodi. Au-delà de 1 000 observations, la source pagine par numéro de page, et l'API n'offre aucun paramètre de tri. L'ordre de ses observations a été mesuré stable d'une page à l'autre ; rien ne le garantit, et la source ne peut pas l'imposer. Elle vérifie donc, une fois toutes les pages reçues, qu'aucune observation n'est revenue deux fois — une observation s'identifie par ses dimensions. Si c'est le cas, le volet Diagnostic affiche « lignes revenues d'une page à l'autre » : autant d'observations manquent, alors que le compte reste juste. Resserrer le where pour que le chargement tienne en une page, ou recharger.

Tout charger en un seul appel : fetch-mode

En mode adaptateur, la source pagine par tranches de 100 lignes. Pour une page « un fetch, N agrégations client », c'est autant d'allers-retours ; et sur un group-by à beaucoup de groupes, le portail ne rend qu'une page de groupes. fetch-mode="export" passe par l'endpoint d'export du portail, qui rend tout d'un coup, avec les mêmes clauses (select, where, group-by, order-by).

AttributTypeDéfautDescription
fetch-moderecords · exportrecordsStratégie de chargement en mode adaptateur (#689) : records (défaut, comportement historique — pagination par pages) ou export, qui charge tout le jeu en **une seule requête**, ou en quelques plages, sur l'endpoint d'export de l'API. Implémenté par les adaptateurs qui ont un endpoint d'export (table des capacités d'ARCHITECTURE, ligne « chargement en une requête ») ; les autres ignorent l'attribut. Selon l'adaptateur, l'export porte les mêmes clauses (select, where, group-by, order-by) ou ne rend que des **lignes brutes** (#1055) : dans ce dernier cas, avec un where, group-by, aggregate ou order-by délégué (posé sur la source ou transmis par une dsfr-data-query), la source reste sur la pagination, qui les exécute côté serveur, et le dit en console. Un export binaire (colonnes projetées depuis select) charge son lecteur à la demande seulement. Pour une première page rapide sur un petit jeu, la pagination reste plus vive ; l'export l'emporte au-delà de 1 000 à 2 000 lignes. À activer pour une page « un fetch, N agrégations client », un jeu de plus de 1 000 lignes, ou un group-by à beaucoup de groupes : l'API les rend tous d'un coup au lieu d'une page. À ne pas activer avec server-side (pagination page par page), qui reste sur l'endpoint paginé et signale la contradiction dans la console. Une seule requête n'a pas d'ordre à tenir d'une page à l'autre : le mode export n'est pas concerné par la relecture sur un ordre total que la pagination impose au-delà d'une page (#1274, voir order-by), et les lignes y arrivent dans l'ordre de l'API, ou dans celui d'order-by. En mode export le total serveur est inconnu : la troncature est détectée en demandant une ligne de plus que le plafond max-records, qui borne aussi les lignes lues. Si l'API n'expose pas d'endpoint d'export, la source retombe une fois sur le chargement paginé, avec un avertissement en console.
<dsfr-data-source id="src"
  api-type="opendatasoft"
  base-url="https://data.economie.gouv.fr"
  dataset-id="mon-jeu"
  fetch-mode="export"
  max-records="20000">
</dsfr-data-source>

Implémenté par les adaptateurs OpenDataSoft et Tabular ; les autres l'ignorent, et une source dont le portail n'expose pas d'endpoint d'export retombe une fois sur le chargement paginé, avec un avertissement console. À ne pas combiner avec server-side, qui pagine page par page et reste sur l'endpoint paginé : la contradiction est signalée dans la console. Sur OpenDataSoft, le total serveur est inconnu en mode export, donc la troncature est détectée en demandant une ligne de plus que le plafond max-records — pensez à relever ce plafond en même temps.

Tabular : l'export Parquet de data.gouv (#1055)

Sur une source api-type="tabular", fetch-mode="export" lit l'export Parquet que data.gouv publie pour chaque ressource, par plages, en ne lisant que les colonnes du select. Mesuré sur le jeu IRVE (223 174 lignes) : le jeu entier en 0,66 s et 17 requêtes, là où la pagination met 10,8 s et 125 requêtes pour 25 000 lignes. Le lecteur (≈ 22 Ko gzip) n'est chargé qu'à ce moment-là : il n'alourdit pas les autres pages.

<dsfr-data-source id="bornes"
  api-type="tabular"
  resource="eb76d20a-8501-400e-b336-d85724de5435"
  select="nom_station, consolidated_latitude, consolidated_longitude"
  fetch-mode="export"
  max-records="250000">
</dsfr-data-source>

Attribut mode inline

AttributTypeFormat / ValeursDescription
dataStringJSON valide (tableau ou objet)Données JSON inline (pas de fetch)

Ne rien charger avant le premier filtre

Sans précaution, une source interroge son API dès le montage et rapatrie le jeu entier — une requête coûteuse dont personne ne regarde le résultat sur une page d'exploration, où l'utilisateur va de toute façon commencer par choisir une commune, une année, un thème. require-where retarde le premier appel jusqu'à ce filtre.

AttributTypeDéfautDescription
require-whereBooleanfalseNe rien charger tant qu'aucun filtre n'a été reçu (#690). Pensé pour les pages d'exploration : sans cet attribut, une source interroge l'API dès le montage et rapatrie le jeu entier — une requête coûteuse dont personne ne regarde le résultat. Avec lui, la source reste en attente, émet dsfr-data-idle et ne part chercher les données qu'au premier filtre. Ce qui compte comme filtre : les clauses reçues par commande — facettes, recherche, dsfr-data-context, délégation d'un dsfr-data-query (son where, avec ou sans group-by, quand elle est seule lectrice de la chaîne — #856). Le where STATIQUE de la source ne compte PAS : il fait partie de la définition du jeu, pas du geste de l'utilisateur ; le contraire rendrait l'attribut sans effet sur toute source qui restreint déjà son périmètre. Quand le dernier filtre est retiré, la source repasse en attente : jamais de requête « tout » implicite. Sans effet en mode données inline (data), qui ne fait aucune requête.
<dsfr-data-source id="src" api-type="opendatasoft" require-where
  base-url="https://data.economie.gouv.fr" dataset-id="equipements">
</dsfr-data-source>

<dsfr-data-facets source="src" fields="commune"></dsfr-data-facets>

<dsfr-data-list source="src"
  idle-message="Choisissez une commune pour afficher ses équipements">
</dsfr-data-list>

Ce qui compte comme filtre : les clauses reçues par commande — facettes, recherche, dsfr-data-context, délégation d'un dsfr-data-query. Le where STATIQUE de la source ne compte pas : il fait partie de la définition du jeu, pas du geste de l'utilisateur — le contraire rendrait l'attribut sans effet sur toute source qui restreint déjà son périmètre. Quand le dernier filtre est retiré, la source repasse en attente : jamais de requête « tout » implicite. Sans effet en mode data inline, qui ne fait aucune requête.

Pendant l'attente, la source publie dsfr-data-idle plutôt que des données vides. Les afficheurs en aval (list, display, kpi, chart, podium, search, a11y) rendent alors leur idle-message — « Choisissez un filtre pour afficher les données » par défaut — et non « aucune donnée », qui annoncerait une requête revenue vide. Un pendant purement client existe sur dsfr-data-query.

Pages à onglets : ne charger que ce qu'on regarde

Une page à onglets déclare ses sources pour tous les panneaux ; cinq sur six sont fermés à l'arrivée, et pourtant toutes les requêtes partent au chargement. Avec lazy, la première requête attend qu'un consommateur de la source entre dans une marge de 200 px autour du viewport. Un panneau fermé est en display:none : il n'a pas de boîte, il n'intersecte jamais, et l'observateur se déclenche à l'ouverture de l'onglet.

AttributTypeDéfautDescription
lazyBooleanfalseNe rien charger tant que personne ne regarde (#931, AM-083). Une page à onglets déclare ses sources pour TOUS les panneaux ; cinq sur six sont fermés à l'arrivée, et pourtant toutes les requêtes partent au chargement. Avec lazy, la première requête attend qu'un consommateur de cette source entre dans une marge de 200 px autour du viewport (IntersectionObserver, la même marge que dsfr-data-map et que le lazy de dsfr-data-repeat, #891). Un panneau d'onglet fermé est en display:none : il n'a pas de boîte, il n'intersecte donc jamais, et l'observateur se déclenche à l'ouverture de l'onglet. **Ce qui est observé** : les FEUILLES de la chaîne aval (chart, list, kpi, display, podium, a11y, repeat ; pour une couche de carte, la carte qui la porte), suivies à travers les transformateurs — un dsfr-data-query est un tuyau déclaré en haut de page, l'observer reviendrait à ne rien différer. lazy-target remplace cette détection par un sélecteur explicite. **Opt-in strict** : sans l'attribut, la source part au chargement, exactement comme avant. **Dégradations, toutes du côté « on charge » :** sans IntersectionObserver, la source part immédiatement ; si la page ne déclare AUCUN consommateur (ou si lazy-target ne désigne rien), la source part immédiatement et le dit en console — une source qui ne chargerait jamais serait pire que le trafic qu'on cherche à éviter. **Ce que lazy ne promet pas** : un IntersectionObserver n'est pas continu. Il échantillonne aux temps de rendu ; un défilement par crans rapides (barre de défilement jetée, scrollIntoView enchaînés) peut traverser un consommateur sans jamais le rapporter comme visible — la source reste alors en attente jusqu'au prochain passage. C'est le comportement du navigateur, pas un bug de la bibliothèque. Se cumule avec require-where : les deux portes doivent s'ouvrir, et require-where est évalué en premier (c'est son message d'attente que l'utilisateur doit lire). Sans effet en mode données inline (data), qui ne fait aucune requête.
lazy-targetString""Sélecteur CSS de l'élément dont la visibilité déclenche le chargement, à la place des consommateurs détectés (#931). Sans effet sans lazy. lazy lazy-target="#panneau-2" : la source part quand le panneau entre dans la marge de 200 px. À utiliser quand la détection automatique ne peut pas voir le bon élément — un consommateur créé en JavaScript, une carte dont on préfère observer la section entière, ou plusieurs blocs qu'on veut traiter comme un seul (le sélecteur peut désigner plusieurs éléments : le PREMIER vu ouvre la porte). Sélecteur invalide, ou qui ne désigne aucun élément : la source part immédiatement, avec un message en console. Rien de silencieux.
<!-- Les sources de l'onglet 2 ne partent qu'à son ouverture -->
<dsfr-data-source id="s-clubs" api-type="opendatasoft" lazy
  base-url="https://data.sports.gouv.fr" dataset-id="clubs">
</dsfr-data-source>

<div class="fr-tabs__panel" id="panneau-2">
  <dsfr-data-chart source="s-clubs" type="bar" label-field="dep" value-field="n"></dsfr-data-chart>
</div>

<!-- Cible explicite, quand la détection automatique ne voit pas le bon élément -->
<dsfr-data-source id="s-carte" api-type="opendatasoft" lazy lazy-target="#panneau-3"
  base-url="https://data.sports.gouv.fr" dataset-id="equipements">
</dsfr-data-source>

Ce qui est observé : les feuilles de la chaîne aval (chart, list, kpi, display, podium, a11y, repeat ; pour une couche de carte, la carte qui la porte), suivies à travers les transformateurs. Un dsfr-data-query est un tuyau déclaré en haut de page : l'observer reviendrait à ne rien différer.

Toutes les dégradations vont du côté « on charge » : sans IntersectionObserver, ou si la page ne déclare aucun consommateur (ou si lazy-target ne désigne rien), la source part immédiatement et le dit en console. Une source qui ne chargerait jamais serait pire que le trafic qu'on cherche à éviter.

Ce que lazy ne promet pas : un IntersectionObserver n'est pas continu. Il échantillonne aux temps de rendu ; un défilement par crans rapides peut traverser un consommateur sans jamais le rapporter comme visible — la source reste alors en attente jusqu'au prochain passage. C'est le comportement du navigateur, pas un bug.

Se cumule avec require-where : les deux portes doivent s'ouvrir, et require-where est évalué en premier — c'est son message d'attente que l'utilisateur doit lire. Pendant l'attente de lazy, la source publie dsfr-data-idle avec la raison lazy, que le volet Diagnostic distingue de l'attente d'un filtre.

Filtres et WHERE dynamique

WHERE statique

L'attribut where definit un filtre permanent sur la source.

En mode Tabular / Grist, format colon champ:operateur:valeur :

where="population:gt:5000"
where="population:gt:5000, region:in:IDF|OCC|BRE"
where="email:isnull"

En mode OpenDataSoft, syntaxe ODSQL libre :

where="population > 5000 AND status = 'active'"

WHERE dynamique (overlays)

Les composants en aval (dsfr-data-facets, dsfr-data-search) peuvent ajouter des filtres dynamiques via le système de commandes. Ces filtres sont fusionnes avec le where statique à chaque requête.

Chaque composant envoie un overlay identifie par une clé unique (whereKey). Les overlays sont combinés en ET logique. La méthode getEffectiveWhere() retourne la clause WHERE fusionnee.

<!-- WHERE statique + overlays dynamiques des facettes et recherche -->
<dsfr-data-source id="src" api-type="opendatasoft"
  dataset-id="rappelconso-v2-gtin-trie"
  base-url="https://data.economie.gouv.fr"
  where="categorie_produit = 'Alimentation'"
  server-side page-size="20">
</dsfr-data-source>

<!-- La recherche ajoute un overlay WHERE via server-search -->
<dsfr-data-search id="search" source="src"
  server-search>
</dsfr-data-search>

<!-- Les facettes ajoutent des overlays WHERE -->
<dsfr-data-facets id="facets" source="src"
  fields="categorie_produit, marque_produit" server-facets>
</dsfr-data-facets>

Mode server-side

Avec server-side, dsfr-data-source charge une seule page a la fois et ecoute les commandes des composants en aval pour naviguer entre les pages. Cela permet d'afficher des datasets de milliers d'enregistrements sans tout charger en memoire.

Le composant reagit a trois types de commandes :

CommandeSourceEffet
{ page }dsfr-data-listChange la page courante et re-fetche
{ where, whereKey }dsfr-data-facets, dsfr-data-searchAjoute/supprime un overlay WHERE et reset a la page 1
{ orderBy }dsfr-data-listChange le tri et re-fetche

Pagination serveur et regroupement : jamais sur une seule page

Une source en server-side ne livre qu'une page. Tout ce qui se calcule côté client sur l'ensemble des lignes n'a donc pas de sens derrière elle. Une dsfr-data-query en aval dont le regroupement ou l'agrégat n'est pas délégué au serveur passe en erreur de configuration : elle porte l'attribut data-dsfr-config-error, les blocs en aval affichent leur état d'erreur, et aucun chiffre partiel n'est émis. Sont concernés une part ou un cumul (share, share_percent, running_sum, diff), un explode, un agrégat sans group-by, une fonction que l'adaptateur ne traduit pas, un regroupement posé derrière un transformateur qui change les colonnes (compute, renommage, dépliage), une source lue par d'autres composants, une source déjà regroupée.

Le message d'erreur nomme la source et les deux corrections :

<!-- le tableau garde sa source paginée -->
<dsfr-data-source id="lignes" api-type="tabular" resource="…" server-side page-size="20"></dsfr-data-source>
<dsfr-data-list source="lignes" columns="region:Région, population:Population"></dsfr-data-list>

<!-- la répartition a la sienne : le serveur regroupe, la part se calcule sur les groupes -->
<dsfr-data-source id="par-region" api-type="tabular" resource="…"
  group-by="region" aggregate="population:sum"></dsfr-data-source>
<dsfr-data-query id="parts" source="par-region"
  aggregate="population__sum:share_percent:part"></dsfr-data-query>

Une requête qui délègue réellement son regroupement, ou qui ne regroupe pas (filtre et tri d'un tableau paginé), n'est pas concernée. En mode URL, paginate ne livre lui aussi qu'une page, mais aucun attribut ne lui fait charger le jeu entier : la requête calcule alors sur la page reçue et le dit, en console et au volet Diagnostic.

Relais cachable : servir les données depuis le cache du site

Un site qui a du cache (Varnish, CDN, cache de son CMS) veut que les données d'une dataviz passent par son domaine, sous une URL qui identifie la donnée. Avec relay-url, la source n'appelle plus le portail : elle appelle le site, sous la forme <relais>/<hôte>/<chemin>?<requête>, et c'est le relais du site qui va chercher la réponse et la met en cache. La source reste en mode adaptateur : filtres, regroupements, tri et pagination délégués vivent dans l'URL, que le relais transmet telle quelle.

AttributTypeDéfautDescription
relay-urlString""Préfixe du relais cachable du site hôte (ADR-155, #1232) : le chemin que le site a choisi pour sa route de relais (relay-url="/relais"), ou une URL absolue (relay-url="https://site.example/relais"). Vide : la valeur de window.DSFR_DATA_RELAY, sinon aucun relais — et alors rien ne change. Avec un relais, toute requête GET vers une autre origine part sous la forme relais/hôte/chemin?requête (le préfixe du relais, l'hôte de la cible, puis son chemin et sa requête), en mode adaptateur comme en mode URL : filtres, regroupements, tri et pagination délégués restent dans l'URL, que le relais transmet telle quelle. Deux cibles donnent deux URL, une même requête donne la même URL au caractère près : le site peut les mettre en cache. Le relais est une route que LE SITE HÔTE fournit, selon le contrat docs/RELAY.md (relais de référence : proxy/relay/node/). Aucune instance publique n'en expose. Sur une requête relayée, aucun en-tête n'est envoyé : ni headers, ni api-key-ref (la clé appartient au relais, qui l'ajoute par hôte), ni cookie. Si l'un de ces attributs est posé, la source l'écrit une fois en console. Ne passent PAS par le relais, et gardent le chemin habituel (direct ou proxy-url) : une URL relative ou de même origine ; une requête POST (mode SQL de Grist, method="POST") ; une cible hors https, sur un port explicite ou avec identifiants ; un chemin que le relais refuserait (%2f, %2e, //…) — ces deux derniers cas avec un avertissement. fetch-mode="export" sur l'API Tabular (export Parquet) retombe sur la pagination, relayée. Une URL de relais de plus de 8 000 caractères part quand même au relais, qui répond 414. Si le relais répond 503 (place momentanément prise : il n'a pas de file d'attente), la requête est réessayée trois fois au plus, après le délai qu'il annonce ; jamais sur un 429. Sans rapport avec cache-ttl : la durée de cache se règle sur le relais.
<!-- La requête part vers
     /donnees-relais/data.economie.gouv.fr/api/explore/v2.1/catalog/datasets/rappelconso/records?… -->
<dsfr-data-source id="rappels" api-type="opendatasoft" relay-url="/donnees-relais"
  base-url="https://data.economie.gouv.fr" dataset-id="rappelconso">
</dsfr-data-source>

<!-- Ou une fois pour toute la page, avant le chargement des composants -->
<script>window.DSFR_DATA_RELAY = '/donnees-relais';</script>

Le relais est une route que le site hôte fournit : la bibliothèque n'en embarque aucun, et aucune instance publique n'en expose. Son contrat (liste blanche d'hôtes, lecture seule, en-têtes de cache, statuts d'erreur) est décrit dans docs/RELAY.md du dépôt, avec un relais Node de référence (proxy/relay/node/) et une suite de conformance à lancer contre son propre relais.

Deux cibles, deux URL ; une même requête, la même URL au caractère près : le chemin et la requête sont repris tels que l'adaptateur les a écrits, sans réencodage ni tri des paramètres. Deux visiteurs qui posent les mêmes filtres dans un ordre différent produisent deux URL, donc deux entrées de cache.

Aucun en-tête ne part au relais : ni headers, ni api-key-ref, ni cookie. Si le portail exige une clé, c'est le relais qui l'ajoute, depuis sa configuration — la clé quitte le navigateur. La source l'écrit une fois en console quand l'un de ces attributs est posé. Hors relais, ils restent actifs.

Ce qui ne passe pas par le relais, et garde le chemin habituel (direct, ou proxy-url) : une URL relative ou de même origine ; une requête POST (method="POST", mode SQL de Grist) ; une cible hors https, sur un port explicite ou avec identifiants, et un chemin que le relais refuserait (%2f, %2e, //…) — ces deux derniers cas avec un avertissement en console. fetch-mode="export" sur l'API Tabular (export Parquet) retombe sur la pagination, qui est relayée. Une URL de relais de plus de 8 000 caractères part quand même au relais, qui répond 414 : la bibliothèque ne le contourne jamais en silence.

Sans rapport avec proxy-url ni cache-ttl : proxy-url désigne le proxy CORS (endpoints dédiés et /cors-proxy, cible dans un en-tête, donc non cachable) ; cache-ttl est un repli hors ligne côté navigateur. La durée de cache d'une donnée relayée se règle sur le relais.

Erreurs : les statuts du relais sont lus par le barème d'erreurs (hôte hors liste : 403 ; URL trop longue : 414 ; amont en panne : 502 ou 504 ; débit dépassé : 429), et « Détails techniques » montre l'URL du relais réellement appelée. Un relais sans file d'attente répond 503 quand un visiteur charge d'un coup plus de dataviz qu'il n'a de places : la requête est alors réessayée trois fois au plus, après le délai annoncé par Retry-After ; jamais sur un 429.

Exemples

Mode URL brute : API REST générique

<dsfr-data-source
  id="prix-ct"
  url="https://data.economie.gouv.fr/api/explore/v2.1/catalog/datasets/prix-controle-technique/records"
  transform="results"
></dsfr-data-source>

Mode URL brute : POST avec headers et params

<dsfr-data-source
  id="api-privee"
  url="https://mon-api.gouv.fr/data"
  method="POST"
  headers='{"Authorization": "Bearer TOKEN"}'
  params='{"limit": 100, "filters": {"annee": 2024}}'
  transform="data.items"
  refresh="60"
></dsfr-data-source>

Mode URL brute : pagination manuelle

Avec paginate, chaque changement de page dans un dsfr-data-list en aval declenche un nouvel appel API avec les paramètres de pagination.

L'ordre des lignes est celui de l'API : la source n'en connaît ni les paramètres de tri ni un identifiant de ligne, et ne lit qu'une page à la fois. Une API qui pagine sans ordre total peut rendre une ligne sur deux pages et n'en jamais servir une autre, sans que rien le signale. C'est à l'URL de porter le tri de l'API sur une colonne unique — dans url, ou dans params.

<dsfr-data-source
  id="browse"
  api-type="opendatasoft"
  dataset-id="rappelconso-v2-gtin-trie"
  base-url="https://data.economie.gouv.fr"
  server-side
  paginate
  page-size="20"
></dsfr-data-source>

<dsfr-data-list source="browse"
  columns="modeles_ou_references:Produit, categorie_produit:Catégorie, marque_produit:Marque"
  pagination="20">
</dsfr-data-list>

Auto-extraction : en mode paginate, pas besoin de transform="data". Les données sont automatiquement extraites depuis json.data et les metadonnees de pagination (json.meta) sont stockees pour calculer le nombre total de pages.

Mode URL brute : API externe avec proxy CORS

L'attribut use-proxy force le passage par le proxy CORS generique. Utile quand l'API cible ne renvoie pas les headers CORS necessaires.

<dsfr-data-source
  id="ghibli-films"
  url="https://ghibliapi.vercel.app/films"
  use-proxy
></dsfr-data-source>

Fonctionnement : en dev, la requête est routee via le middleware Vite /cors-proxy. En production, elle passe par l'endpoint equivalent sur le serveur proxy (<votre-proxy>/cors-proxy). L'URL cible est transmise dans le header X-Target-URL.

Mode adapter : OpenDataSoft

L'adapter gère automatiquement la construction d'URL, la pagination et le parsing. Pagination automatique (toutes les pages en une seule requête) jusqu'a 1 000 records.

<dsfr-data-source
  id="elus"
  api-type="opendatasoft"
  base-url="https://data.iledefrance.fr"
  dataset-id="elus-regionaux"
  where="sexe = 'F'"
></dsfr-data-source>

<dsfr-data-query id="stats" source="elus"
  group-by="groupe_politique"
  aggregate="nom:count:nb"
  order-by="nb:desc">
</dsfr-data-query>

<dsfr-data-chart source="stats" type="bar"
  label-field="groupe_politique" value-field="nb">
</dsfr-data-chart>

Mode adapter : OpenDataSoft (data.economie.gouv.fr)

L'adapter ODS gère la pagination, le tri serveur, la recherche full-text et les facettes. Ici un simple datalist pagine.

<dsfr-data-source
  id="src-ods"
  api-type="opendatasoft"
  dataset-id="rappelconso-v2-gtin-trie"
  base-url="https://data.economie.gouv.fr"
  server-side
  page-size="20"
></dsfr-data-source>

<dsfr-data-list source="src-ods"
  columns="modeles_ou_references:Produit, categorie_produit:Catégorie, marque_produit:Marque, date_publication:Date"
  server-sort
  pagination="20">
</dsfr-data-list>

Mode adapter : Grist

L'adapter Grist se connecte aux instances Grist (grist.numerique.gouv.fr ou getgrist.com). Il détecté automatiquement si la requête necessite le mode SQL (groupement, agrégation, opérateurs avancés) ou le mode Records standard.

<dsfr-data-source
  id="grist-data"
  api-type="grist"
  base-url="https://grist.numerique.gouv.fr/api/docs/DOC_ID/tables/TABLE_ID/records"
  headers='{"Authorization": "Bearer TOKEN"}'
></dsfr-data-source>

<dsfr-data-chart source="grist-data" type="bar"
  label-field="nom" value-field="valeur">
</dsfr-data-chart>

Aplatissement automatique : l'adapter Grist aplatit lui-même la structure records[].fields de la réponse — aucun dsfr-data-normalize n'est nécessaire en mode adapter. Le flatten de normalize ne sert que si vous appelez l'API Grist en mode URL brute (attribut url sans api-type), où la réponse arrive telle quelle.

Mode adapter : server-side avec datalist

Avec server-side, la source charge une page a la fois. Le dsfr-data-list en aval pilote la navigation entre les pages.

<dsfr-data-source
  id="srv"
  api-type="opendatasoft"
  dataset-id="rappelconso-v2-gtin-trie"
  base-url="https://data.economie.gouv.fr"
  server-side
  page-size="20"
></dsfr-data-source>

<dsfr-data-list source="srv"
  columns="modeles_ou_references:Produit, categorie_produit:Catégorie, marque_produit:Marque"
  server-sort
  pagination="20">
</dsfr-data-list>

Mode inline data

L'attribut data permet de passer des données JSON directement, sans aucun appel HTTP. Utile pour les exemples, prototypes ou données statiques.

<dsfr-data-source
  id="inline"
  data='[{"nom":"Paris","pop":2161000},{"nom":"Lyon","pop":516092},{"nom":"Marseille","pop":870731}]'
></dsfr-data-source>

<dsfr-data-chart source="inline" type="bar"
  label-field="nom" value-field="pop">
</dsfr-data-chart>

Registre de clés API (api-key-ref)

Au lieu d'ecrire les tokens directement dans les attributs HTML, declarez-les une seule fois dans un registre global window.DSFR_DATA_KEYS, puis referencez-les par nom avec api-key-ref. La clé est injectée comme header Authorization.

<script>
  window.DSFR_DATA_KEYS = {
    tmdb: 'Bearer eyJhbGciOiJIUzI1NiJ9...',
    monapi: 'Token abc123'
  };
</script>

<dsfr-data-source
  id="films"
  url="https://api.themoviedb.org/3/movie/popular"
  api-key-ref="tmdb"
  transform="results"
></dsfr-data-source>

Comportement : si api-key-ref est défini et que la clé existe dans window.DSFR_DATA_KEYS, elle est injectée en header Authorization à chaque requête. Si la clé n'est pas trouvee, un avertissement est emis dans la console et la requête est envoyee sans authentification. Fonctionne avec les deux modes (URL brute et adapter).

Securite : les clés restent lisibles par tout script sur la page (même modele de securite que l'attribut headers). Cette fonctionnalite est une commodite pour separer la declaration des clés du markup, pas un mecanisme de protection. Pour les clés sensibles, utilisez un proxy serveur.

Rafraîchissement automatique

L'attribut refresh re-execute la requête a intervalles reguliers (en secondes). Fonctionne dans les deux modes (URL brute et adapter).

<dsfr-data-source
  id="live"
  api-type="opendatasoft"
  base-url="https://data.opendatasoft.com"
  dataset-id="mon-dataset"
  refresh="30"
></dsfr-data-source>

Quand la source est en panne : un message lisible, dit une fois

Quand le chargement échoue, les blocs branchés sur la source ne montrent plus Erreur de chargement: HTTP 503: Service Unavailable en rouge. Chacun garde sa place et affiche, dans un encart neutre, une phrase que l'usager peut comprendre ; le code HTTP, l'adresse appelée et l'heure sont repliés dans « Détails techniques ». La phrase dépend de la cause :

CauseCe que lit l'usagerAction proposée
Service indisponible (HTTP 5xx, délai dépassé) ou réponse bloquée (pas d'en-tête CORS)Données momentanément indisponibles. Le service qui publie ces chiffres ne répond pas pour l'instant.Réessayer
Hors connexionVous semblez hors connexion. Les chiffres s'afficheront quand la connexion reviendra.Réessayer, et nouvel essai automatique au retour du réseau
Service très sollicité (HTTP 429)Le service est très sollicité. Réessayez dans quelques instants.Réessayer — jamais de nouvel essai automatique
Données introuvables (HTTP 404)Ces données ne sont plus publiées à cette adresse. Le producteur les a peut-être déplacées ou retirées.Lien « Consulter la page de ces données », si source-page est posé ; sinon aucune
Accès restreint (HTTP 401, 403)Ces données ne sont pas accessibles publiquement.Aucune
Page mal réglée (HTTP 400, source mal configurée)Cet affichage n'a pas pu être construit. Le problème vient de la page, pas de votre connexion.Aucune

« Réessayer » recharge la source à l'identique (mêmes filtres, même page) : c'est la méthode reload(), déclenchée par la commande { reload: true }. Après un échec hors connexion, la source se relance d'elle-même, une fois, au retour du réseau. L'erreur de configuration d'un composant d'affichage (attribut invalide) n'est pas concernée : elle reste une alerte, écrite pour l'intégrateur.

Pour dire la panne avec vos mots, posez error-message sur la source :

AttributTypeDéfautDescription
error-messageString""Phrase affichée à l'usager quand cette source est en panne (#1203), à la place du message du barème — dans les blocs branchés sur la source comme dans le bandeau dsfr-data-source-status. error-message="Les chiffres de la DGFiP sont en cours de mise à jour." : à poser quand l'intégrateur sait mieux que la bibliothèque dire qui publie les données et quoi faire. Ne remplace QUE la phrase usager d'un échec de chargement : le code HTTP, l'adresse appelée et l'heure restent dans « Détails techniques », l'Error de dsfr-data-error et la console ne changent pas. Sans effet sur une erreur de configuration de la source.
source-pageString""Adresse de la page PUBLIQUE de ces données (#1222) : la page du jeu sur le portail du producteur, celle qu'un usager peut lire — pas l'adresse d'API. source-page="https://data.economie.gouv.fr/explore/dataset/prix-carburants/" : quand la source répond « introuvable » (404, 410), le bloc en erreur et le bandeau dsfr-data-source-status proposent le lien « Consulter la page de ces données ». Sans cet attribut, aucun lien : la bibliothèque ne déduit l'adresse ni de base-url, ni de dataset-id, ni de resource, et ne montre jamais l'adresse d'API ni celle du relais (relay-url). Sans effet sur les autres pannes (service indisponible, hors connexion, accès restreint…) et sur le chargement : l'attribut ne déclenche aucune requête. N'est admise qu'une adresse http(s) ou relative ; toute autre (javascript:, data:…) est ignorée, avec un avertissement en console.

Données introuvables : le lien vers la page des données

Quand la source répond « introuvable » (HTTP 404 ou 410), l'usager n'a rien à réessayer : le producteur a déplacé ou retiré le jeu. Posez source-page sur la source avec l'adresse de la page publique de ces données — la page du jeu sur le portail du producteur, celle qu'un usager peut lire, pas l'adresse d'API. Les blocs en erreur et le bandeau proposent alors le lien « Consulter la page de ces données ».

<dsfr-data-source id="prix" api-type="opendatasoft"
  base-url="https://data.economie.gouv.fr" dataset-id="prix-carburants"
  source-page="https://data.economie.gouv.fr/explore/dataset/prix-carburants/">
</dsfr-data-source>

Dans une tuile de KPI : la forme compacte

L'encart complet est fait pour un bloc de graphique ; dans une tuile de KPI il doublerait sa hauteur. Un <dsfr-data-kpi> dont la source est en panne garde donc sa place et sa hauteur : « — » à la place du chiffre, son libellé (l'usager doit savoir quel chiffre manque) et une phrase courte. Il n'affiche jamais un nombre : ni 0, ni le chiffre du chargement précédent. Les autres afficheurs (graphique, jauge comprise, liste, podium, display) gardent l'encart complet.

CausePhrase de la tuileSans bandeau, la tuile porte aussi
Service indisponible, réponse bloquéeChiffre momentanément indisponibleRéessayer · Détails
Hors connexionVous semblez hors connexionRéessayer · Détails
Service très sollicité (HTTP 429)Le service est très sollicitéRéessayer · Détails
Données introuvables (HTTP 404, 410)Ce chiffre n'est plus publié à cette adresseLien de source-page · Détails
Accès restreint (HTTP 401, 403)Ce chiffre n'est pas accessible publiquementDétails
Page mal réglée (HTTP 400, source mal configurée)Ce chiffre n'a pas pu être affichéDétails

error-message remplace la phrase, comme dans l'encart complet. « Détails » est replié, à la suite de la phrase : il donne le code HTTP, l'adresse appelée et l'heure. Avec un bandeau dsfr-data-source-status, la tuile ne garde que le tiret, le libellé et la phrase : le bouton, le lien et le détail sont dans le bandeau. Dans une tuile étroite, le lien ou « Détails » passent à la ligne, et la tuile gagne une ligne. Une valeur absente des données (null, champ manquant) n'est pas une panne : la tuile affiche « — » et son libellé, sans phrase.

Accessibilité : le tiret est masqué aux lecteurs d'écran (aria-hidden), qui lisent le libellé et la phrase ; la figure se nomme « libellé : phrase », sans chiffre. Le bouton « Réessayer » garde 44 px de haut.

Tableau équivalent, carte, filtres, pagination : aucun ancien chiffre

Pendant une panne, aucun bloc ne garde à l'écran une ligne du dernier chargement : sans date, elle serait prise pour le chiffre du jour. Le graphique, le KPI, la liste, le podium et les fiches rendent le message décrit plus haut. Les blocs ci-dessous, qui n'avaient pas de rendu de panne, suivent la même règle.

BlocPendant la pannePhrase (service indisponible)
dsfr-data-a11yLe tableau est retiré. « Télécharger en CSV » est désactivé (aria-disabled, il garde le focus) et ne produit aucun fichier. La description lue avec le graphique ne compte plus de lignes.Données momentanément indisponibles. Le tableau et le téléchargement reviendront avec les données.
dsfr-data-map, dsfr-data-map-layerLa couche en panne est vidée : formes, grappes, carte de chaleur, entrée de légende, bandeau de troncature, et ses clones dans les encarts. Le fond reste manipulable. Si plus rien n'est tracé, le message des autres blocs est centré sur la carte ; si une autre couche trace encore, une ligne en bas de carte nomme la couche (son label, sinon le libellé de sa légende). La description lue ne compte plus ses marqueurs.Données momentanément indisponibles (message centré)
Couche « Bornes de recharge » : données momentanément indisponibles. (ligne d'état)
dsfr-data-list, dsfr-data-display en pagination serveurUn changement de page refusé garde la page précédente et le dit ; la pagination et l'adresse (url-sync) reviennent sur la page affichée, et redemander la page refusée relance la source. Tout autre échec (rafraîchissement, tri, filtre) rend le message de panne, sans aucune ligne.La page 4 n'a pas pu être chargée. Vous consultez toujours la page 3.
dsfr-data-facets, dsfr-data-searchLes filtres sont désactivés (aria-disabled, toujours atteignables au clavier) ; leurs compteurs et le compte de résultats ne sont plus affichés. La sélection et le terme déjà posés sont gardés et s'appliquent de nouveau au retour des données. « Réinitialiser les filtres » et la touche Échap restent actifs.Filtres momentanément indisponibles.
Export de dsfr-data-list (export)Aucun fichier tant que la liste est en panne.—
dsfr-data-repeatLes lignes estampées depuis le dernier chargement sont retirées — texte du gabarit et blocs qu'il contient —, avec les sources scopées (scopes) qu'elles servaient. Elles sont recréées au chargement réussi.Données momentanément indisponibles (message à la place des lignes)

Les phrases suivent la cause, comme l'encart : hors connexion, « Vous semblez hors connexion » ; HTTP 429, « Le service est très sollicité » ; données introuvables, « Ces données ne sont plus publiées à cette adresse » ; accès restreint, « Ces données ne sont pas accessibles publiquement ». Pour les filtres et la description du tableau équivalent, la cause suit la mention « indisponibles » : « Filtres indisponibles. Vous semblez hors connexion. » error-message s'applique partout. Quand le refus vient de la page elle-même (HTTP 400), les filtres restent actifs : le filtre posé peut être la cause du refus, et le retirer est alors le seul remède.

Avec un bandeau dsfr-data-source-status, aucun de ces blocs ne porte « Réessayer » ni n'annonce la panne une seconde fois : la carte et le tableau équivalent gardent leur phrase, sans bouton. Sans bandeau, la carte porte « Réessayer » (dans le message centré comme dans la ligne d'état) ; le tableau équivalent et les filtres n'annoncent rien de plus, la panne étant déjà dite par le graphique, la carte ou la liste qu'ils accompagnent.

Pendant un nouvel essai (« Réessayer », retour du réseau, rafraîchissement périodique) : le graphique, le KPI, la liste et les fiches affichent leur indicateur de chargement ; le tableau équivalent, la carte et les filtres gardent leur rendu de panne, phrase comprise, jusqu'au résultat — rien ne clignote. Au chargement réussi, tout revient sans rechargement de la page.

Le dernier relevé, sur demande : stale-if-error

Par défaut, une source en panne ne laisse aucun de ses anciens chiffres à l'écran. Pour un jeu qui change peu — un recensement, un budget voté, un référentiel —, un trou fait croire la page cassée alors que le chiffre d'il y a dix minutes est encore le chiffre du jour. Posez stale-if-error sur la source, avec une durée en secondes : pendant une panne passagère, les blocs branchés dessus gardent leurs derniers chiffres, atténués, sous l'étiquette « Non actualisé » et l'heure de leur relevé.

AttributTypeDéfautDescription
stale-if-errorNumber0Garder à l'écran le DERNIER RELEVÉ pendant une panne passagère (#1288) : durée, en secondes, pendant laquelle les derniers chiffres reçus restent affichables. Absent ou 0 (défaut) : une source en panne ne laisse aucun ancien chiffre à l'écran, chaque bloc dit la panne. stale-if-error="3600" : si la source échoue après avoir livré, les blocs branchés dessus (KPI, graphique, liste, fiches, podium, carte, tableau équivalent) gardent leurs chiffres, atténués, sous l'étiquette « Non actualisé » et la phrase « Chiffres relevés aujourd’hui à 14 h 32. » — l'heure est celle où la page a reçu ces chiffres. L'export CSV reste possible, daté dans son libellé et dans le nom du fichier. Passé la durée (comptée depuis le relevé), les blocs reviennent d'eux-mêmes au message de panne. C'est à l'intégrateur de le décider, jeu par jeu : un chiffre d'il y a une heure a un sens pour un recensement, pas pour un stock ou des places libres. La valeur EST la borne : il n'existe pas de relevé sans durée. Le relevé n'est jamais affiché, quelle que soit la valeur, quand : la cause n'est pas passagère (données introuvables 404 / 410, accès restreint 401 / 403, page mal réglée) ; l'appel en échec n'est pas celui qui a produit les chiffres (filtre, page, tri ou emprise changés) ; rien n'a encore été reçu. Les filtres (dsfr-data-facets, dsfr-data-search) restent désactivés pendant la panne. Mémoire : celle de la page. Une page OUVERTE pendant la panne n'a rien à montrer. getData() et getDataCache rendent toujours vide pendant la panne : seuls les afficheurs de la bibliothèque lisent le relevé, par getLastKnownData().
<!-- Pendant une panne passagère, les derniers chiffres restent une heure, datés -->
<dsfr-data-source id="aides" api-type="opendatasoft"
  base-url="https://data.economie.gouv.fr" dataset-id="aides-industrie"
  stale-if-error="3600">
</dsfr-data-source>

<dsfr-data-kpi source="aides" value="beneficiaires:sum" label="Bénéficiaires"></dsfr-data-kpi>
<dsfr-data-chart source="aides" type="bar" label-field="region" value-field="beneficiaires"></dsfr-data-chart>

C'est à vous de le décider, jeu par jeu : la bibliothèque ne sait pas si un chiffre est un recensement ou un nombre de places libres. Ne le posez pas sur une donnée que l'usager utilise pour agir tout de suite (prix, stock, file d'attente, alerte) : un tiret y est moins dangereux qu'une valeur d'il y a une heure. La valeur est la borne — il n'existe pas de relevé sans durée —, comptée depuis le relevé et non depuis la panne.

BlocPendant la panne, avec stale-if-errorTexte
dsfr-data-kpiLe chiffre reste, en gris de texte ; le filet devient pointillé ; ni couleur de seuil, ni tendance, ni lignes complémentaires. La tuile garde sa hauteur. Son nom accessible porte la date avec le chiffre.Relevé à 14 h 32 · non actualisé
dsfr-data-chart (cadre officiel compris)Le tracé reste, désaturé, sous une mention. Le nom accessible du graphique porte la date. databox-date n'est pas touché : c'est la date de la donnée, pas celle du relevé.« Non actualisé » — Chiffres relevés aujourd'hui à 14 h 32.
dsfr-data-list, dsfr-data-display, dsfr-data-podium, dsfr-data-repeatLes lignes restent, sous la même mention. Recherche, filtres de colonne, tri et pagination locaux répondent. En pagination ou en tri serveur, la page affichée reste et les autres sont neutralisées (aria-disabled) : une autre page serait une autre requête.Chiffres relevés aujourd'hui à 14 h 32. Les autres pages reviendront avec les données.
dsfr-data-a11yLe tableau reste ; sa légende devient visible et porte l'étiquette et la date. Le téléchargement reste possible : le libellé du bouton et le nom du fichier portent la date du relevé, le contenu reste brut.Données du graphique — « Non actualisé » chiffres relevés aujourd'hui à 14 h 32
Télécharger en CSV (chiffres relevés à 14 h 32)
données_relevé-2026-10-13-14h32.csv
Export de dsfr-data-listPermis lui aussi, daté de la même façon.Exporter CSV (chiffres relevés à 14 h 32)
export_relevé-2026-10-13-14h32.csv
dsfr-data-map, dsfr-data-map-layerLes formes restent tracées. Toute la carte est atténuée dès qu'une couche est périmée — formes grises, contours en pointillé, légende comprise — et une ligne en bas de carte la date ; avec plusieurs couches, elle nomme celle qui n'est pas à jour. Une bulle ouverte sur un point porte la même note.Points relevés aujourd'hui à 14 h 32.
Couche « Bornes de recharge » : points relevés aujourd'hui à 14 h 32.
dsfr-data-facets, dsfr-data-searchInchangé : désactivés pendant la panne, comme sans l'attribut.Filtres momentanément indisponibles.
dsfr-data-source-statusLe bandeau ne promet plus que les blocs « s'afficheront » : il dit qu'ils n'ont pas été actualisés. Il ne dit pas l'heure — chaque bloc dit la sienne, deux sources pouvant ne pas dater du même instant.Une partie des chiffres de cette page n'a pas pu être actualisée

Un autre jour que celui du relevé, la date est écrite en entier — « Chiffres relevés le 12 octobre à 18 h 05. » —, jamais « hier » ni « il y a 3 min ». Hors connexion, la mention ajoute « Vous semblez hors connexion. » ; error-message s'y ajoute aussi.

Le relevé n'est jamais affiché, quelle que soit la durée, et le bloc rend le message de panne décrit plus haut, quand :

Pendant un nouvel essai, les chiffres relevés restent en place — pas de retour à l'indicateur de chargement, une source en refresh ne fait pas clignoter la page — et le bouton dit « Nouvel essai en cours… ». Au chargement réussi, l'étiquette disparaît et les nouveaux chiffres s'affichent. Sans bandeau, chaque mention porte « Réessayer » et « Détails techniques » ; avec un bandeau, il les porte seul.

Pour un script de la page, rien ne change : getData() et getDataCache(id) rendent toujours vide pendant la panne. Le relevé n'est lu que par les composants d'affichage.

dsfr-data-source-status : le bandeau qui dit la panne une fois

Sans bandeau, huit blocs branchés sur une source en panne affichent huit messages et huit boutons « Réessayer ». Posez <dsfr-data-source-status> en haut du contenu : la panne est dite une fois par source, avec le seul bouton « Réessayer ». Les blocs concernés gardent leur place et leur message court, sans bouton ; les blocs d'une autre source ne sont pas touchés. Le bandeau n'affiche rien tant qu'aucune source n'est en panne.

AttributTypeDéfautDescription
sourceString""Id de la source à suivre. Vide : toutes les sources de la page, un message par source en panne. Une étape intermédiaire (dsfr-data-query…) branchée sur la source est suivie avec elle : c'est la source qui charge qui est relancée.
<!-- La panne de "prix" est dite ici, une fois -->
<dsfr-data-source-status source="prix"></dsfr-data-source-status>

<dsfr-data-source id="prix" api-type="opendatasoft"
  base-url="https://data.economie.gouv.fr" dataset-id="prix-carburants"
  error-message="Les prix sont en cours de mise à jour. Revenez dans quelques minutes.">
</dsfr-data-source>

<dsfr-data-kpi source="prix" valeur="avg:prix" label="Prix moyen"></dsfr-data-kpi>
<dsfr-data-chart source="prix" type="bar" label-field="ville" value-field="prix"></dsfr-data-chart>

Accessibilité : le bandeau est une région role="status" (annonce polie, qui n'interrompt pas la lecture). Les blocs en erreur ne sont plus des alertes assertives : sans bandeau, chacun est une région role="status" ; avec bandeau, seul le bandeau annonce. Le bouton « Réessayer » est un bouton DSFR d'au moins 44 px de haut.

Pendant la panne : les dernières lignes, périmées et datées

Une source qui échoue après avoir déjà livré ne jette pas ses lignes, mais elles ne passent plus pour à jour. Les composants n'en affichent aucune : ils gardent le message de panne décrit plus haut — tableau équivalent, carte et export compris —, sauf quand la source le demande par stale-if-error, et alors datées. Pour un script de la page, la règle est la même, avec ou sans cet attribut :

LectureSource à jourSource en panne
element.getData()les lignesnull ([] sur une requête, une jointure, un empilement, un pivot)
DsfrData.getDataCache(id)les lignesundefined
element.isStale()falsetrue, jusqu'au prochain chargement réussi — nouvel essai en cours compris
element.getLastKnownData()
DsfrData.getLastKnownData(id)
{ data, loadedAt }{ data, loadedAt, stale: { since, cause, originId, staleIfError?, sameRequest? } }

loadedAt est la date (Date.now()) du dernier chargement réussi, since celle du premier échec depuis, cause la cause du barème (service-indisponible, hors-connexion, service-sollicite, donnees-introuvables, acces-restreint, page-mal-reglee, reponse-bloquee) et originId la source qui a réellement échoué. Quand la source porte stale-if-error, staleIfError en rend la durée (en secondes) et sameRequest dit si l'appel en échec est celui qui avait produit ces lignes. Les lignes et leur date sont rendues ensemble : on ne peut pas obtenir les unes sans l'autre. Le marquage se lève au premier chargement réussi ; une source remise en attente d'un filtre (require-where) ou retirée de la page n'en garde aucune.

<script type="module">
  document.addEventListener('dsfr-data-error', (e) => {
    if (e.detail.sourceId !== 'prix' || e.detail.lastLoadedAt === undefined) return;
    const { data, loadedAt, stale } = document.getElementById('prix').getLastKnownData();
    // À n'afficher que DATÉ : « Données du … · mise à jour impossible »
    console.log(data.length, 'lignes du', new Date(loadedAt).toLocaleString('fr-FR'), stale.cause);
  });
</script>

Les transformateurs suivent la même règle : une requête, un dsfr-data-normalize, une jointure, un empilement ou un pivot dont la source est en panne relaient l'erreur et ne recalculent rien sur les anciennes lignes — ni quand un de leurs attributs change, ni quand une autre de leurs entrées se recharge.

Événements

Le composant émet des événements personnalises via le système data-bridge :

Méthodes publiques

MéthodeRetourDescription
reload()voidForce le rechargement des données, à l'identique. C'est ce que déclenche « Réessayer ».
getData()unknownRetourne les données actuelles. null avant le premier chargement, en attente d'un filtre, et pendant une panne : les lignes d'avant la panne ne sont pas rendues comme à jour (voir les dernières lignes, périmées et datées).
getLastKnownData(){ data, loadedAt, stale? } | undefinedLes lignes du dernier chargement réussi et leur date. stale ({ since, cause, originId }) est présent pendant une panne, absent sinon. undefined si la source n'a jamais rien livré.
isStale()Booleantrue quand la source garde des lignes périmées : de sa panne jusqu'au prochain chargement réussi.
isLoading()BooleanIndique si un chargement est en cours
getError()Error | nullRetourne l'erreur eventuelle
getAdapter()ApiAdapter | nullRetourne l'adapter actif (mode adapter uniquement, null en mode URL brute)
getEffectiveWhere(excludeKey?)StringRetourne la clause WHERE fusionnee (statique + tous les overlays dynamiques). Le paramètre excludeKey permet d'exclure un overlay spécifique (utilise par les facettes).

Conseils d'utilisation

Choisir le bon mode : Utilisez le mode URL brute pour vous connecter a n'importe quelle API REST. Utilisez le mode adapter pour les APIs connues (ODS, Tabular, Grist) afin de beneficier de la pagination automatique, des filtres server-side et de la construction d'URL geree.

server-side vs pagination automatique : Sans server-side, l'adapter charge toutes les données (multi-pages) en une seule operation. Avec server-side, il charge une seule page et attend les commandes des composants en aval. Utilisez server-side pour les grands datasets avec un dsfr-data-list pagine.

Pipeline de données : dsfr-data-source s'integre dans un pipeline : dsfr-data-source → dsfr-data-normalize → dsfr-data-query / dsfr-data-search / dsfr-data-facets → composants de visualisation.

Securite : les headers, tokens et clés dans window.DSFR_DATA_KEYS 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). Pour une separation propre entre markup et clés, utilisez api-key-ref. Sur une instance en mode serveur, une table Grist privée peut être publiée depuis l'app Sources : la source devient url="https://<instance>/api/public/sources/<jeton>/records" transform="records", la clé reste sur le serveur, et le lien se révoque à tout moment (quiconque l'a lit la table).