Brouillon à relire par Paul
Ce texte est un brouillon. Paul Perigault le relira et le réécrira avant publication.
ADR-0007 — Visualisation de données : contrat, rendu SVG, accessibilité, palettes
- Statut : proposé (2026-10-28). Documentation seulement : aucun composant n’est codé par cet ADR.
- Lié : ADR-0002, ADR-0006, D-030, D-038, issue #93. Cadre le lot analyse V2a à V5 (#94 à #100).
Contexte
Paul veut afficher de la BI sur ses sites : des visuels de données propres, une direction
artistique Encre, zéro JavaScript par défaut, une accessibilité par conception, une couleur
d’accent et une typographie modifiables par une variable pour un projet externe, et du Rust
seulement pour le calcul (pp-data), sans que le rendu en dépende. Réutilisables : Stat,
Table, Progress, Skeleton, EmptyState, Alert, Figure ; jetons --pp-color-*,
--pp-font-*, --pp-space-*, --pp-size-rule ; bibliothèques color et contrast de tokens.
Décision 1 : contrat de données
- Format : un
Datasetcolonnaire, versionné (schema: 1) : un axex(category,timeounumber) et desseries(id,label,unit,values). Colonnaire parce que c’est le format naturel d’unFloat64Array(WebAssembly) et le plus court en JSON. - Valeurs manquantes :
null, jamais0niNaN(aucunNaNni infini ne franchit la frontière). Une valeur manquante rompt la courbe, laisse un vide dans les barres et s’annonce dans le tableau (« donnée manquante »). - Unités : une unité par série (
€,%,ms…), jamais fondue dans la valeur ; deux séries d’unités différentes ne partagent pas un axe (erreurunit-mismatch, ou axes séparés demandés). - Dates : ISO 8601 (
2026-10-28,2026-10,2026-10-28T09:00:00Z), lues en UTC, sans fuseau implicite ; pas deDatelocale dans le domaine. - Locale fr (par
Intl, localefrpar défaut, propriétélocalepour changer) : séparateur décimal virgule, groupes de milliers par espace fine insécable (U+202F), unité précédée d’une espace insécable, dates en toutes lettres dans les libellés et ISO dans les attributsdatetime. Convention typographique, non une preuve. - Frontière :
infrastructure/parse-dataset.tsvalide toute entrée (JSON, fichier, sortie depp-data) et produit unDatasetdu domaine ou uneChartError. Le domaine ne reçoit jamais de donnée non validée (ADR-0006, décision 1). - Erreurs typées (
errors.ts, union fermée à champcode) :dataset.empty,dataset.length-mismatch,dataset.non-finite,dataset.duplicate-id,dataset.bad-date,dataset.unit-mismatch,dataset.too-many-points,chart.too-many-series. TypeResultmaison, TypeScript strict (ADR-0005), aucunany. - Compatibilité Rust/WebAssembly :
pp-dataconsomme et produit le mêmeDataset(f64plus masque des manquants). PortAggregator:(Dataset, Spec) → Result<Dataset, ChartError>, implémenté d’abord en TypeScript (référence), plus tard par l’adaptateur WebAssembly. Définitions figées pour que les deux s’accordent : quantiles de type 7 (Hyndman et Fan, 1996, défaut de R et NumPy), sous-échantillonnage LTTB (Steinarsson, 2013, thèse, université d’Islande). Parité attendue à 1e-9 près en relatif (seuil à confirmer en V4a). Le WebAssembly s’exécute au build (Node), jamais chez le lecteur : le rendu n’en dépend en aucun cas.
Décision 2 : rendu
- SVG statique produit au build par des composants Astro, zéro JS par défaut. Géométrie
calculée par le domaine (
dataviz), le composant ne fait que poser des éléments. - Aucune couleur brute, aucun attribut
style: couleurs par classes et attributsdata-seriesmappés en CSS vers--pp-*; géométrie en attributs SVG (x,y,d), polices et traits en CSS. Un test parcourt le SVG produit (pas de#…,rgb(,style=). - Amélioration progressive (rien n’est requis pour lire la donnée) : survol et focus par CSS
(
:hover,:focus-visible) ; filtres de séries par cases à cocher et:has(), sans script ; infobulle native<title>par point. Un îlot JS facultatif (behaviors) n’est admis que s’il est nécessaire (grands jeux de données) et laisse le repli CSS intact. - Types du premier jalon : courbe ; barres (simples et groupées, verticales ou horizontales) ; aire (simple, empilée de 4 séries au plus) ; jauge ; sparkline ; KPI (valeur, unité, variation) ; tableau. La jauge est une barre bornée avec repère de cible (bullet graph, Few, 2006), pas un arc.
- Hors périmètre du jalon : camembert et anneau (angles et aires sont lus moins bien que des
longueurs : Cleveland et McGill, 1984), 3D, radar, treemap, cartes, nuage de points, histogramme
et boîte à moustaches (après
pp-data), barres empilées, temps réel, zoom et défilement, export d’image. - Budgets (conventions, mesurées en CI à partir de V2a) : au plus 400 nœuds SVG et 10 ko bruts
par graphique (tableau lié exclu) ; sparkline ≤ 1,5 ko ; 6 séries au plus ; barres ≤ 60
catégories (au-delà : tri et regroupement « autres », ou tableau) ; tableau lié ≤ 200 lignes
(au-delà : en-tête et pagination par le composant
Table). - Sous-échantillonnage : courbes et aires plafonnées à 500 points par série (environ un point par pixel de la largeur de référence) par LTTB au build ; le tableau lié garde la donnée complète (ou l’agrégat annoncé comme tel). Barres : jamais échantillonnées, agrégées.
- Mise en page : un seul gabarit SVG (
viewBox,width: 100%) ; graduations choisies pour la largeur minimale (320 px) ; textes enfont-sizedes jetons, traits envector-effect: non-scaling-stroke. Étiquettes directes plutôt que légende.
Décision 3 : accessibilité (WCAG 2.2 AA)
- Alternative textuelle :
<figure>+<figcaption>(titre et résumé factuel produit depuis la donnée : étendue, minimum, maximum, tendance chiffrée) ;<svg role="img" aria-labelledby>renvoyant à<title>et<desc>; tableau de données lié (<table>avec<th scope>), référencé dans<desc>. Une valeur manquante est dite, pas omise. - Ordre de lecture = ordre du DOM : titre, résumé, graphique, tableau (voie clavier par défaut).
- Clavier (seulement si interactif) : au plus 50 points focalisables (
tabindex="0",aria-label« libellé, valeur, unité »), au-delà : aller au tableau. Focus visible par jetons (--pp-size-focus-*), cibles ≥--pp-size-target-min(WCAG 2.5.8) pour tout contrôle. - Contraste : tracés, barres et marques ≥ 3:1 contre le fond (1.4.11) ; texte ≥ 4,5:1 (1.4.3),
en
--pp-color-fgou--pp-color-muted, jamais en couleur de série ; deux barres voisines sont séparées par un espace. - Jamais la couleur seule (1.4.1) : étiquette directe en bout de courbe ; chaque série d’indice
ireçoit aussi un motif de trait (continu, tirets, points…) et un marqueur ; légende = marqueur plus libellé ; variation d’un KPI dite en mots (« hausse de 4,2 % ») avec signe, la couleur de--pp-color-status-*n’étant qu’un renfort. - Mouvement réduit : aucune animation par défaut, toute transition sous
prefers-reduced-motion: no-preference. Contraste forcé (forced-colors: active) : tracés enCanvasText, séries distinguées par motifs de trait et marqueurs. - Impression : thème clair forcé, tableau lié visible (mécanisme à valider en V2a :
detailsfermé masque son contenu), motifs conservés pour le noir et blanc.
Décision 4 : palettes
- Dérivation : par une fonction pure de
datavizà partir de la teinte d’accent (OKLCH) et des jetons neutres, jamais par une couleur codée en dur. Sortie : jetons--pp-chart-*générés danstokens.css(jeux clair et sombre,light-dark()), choisis plutôt que les couleurs relatives CSS parce qu’une valeur calculée à l’exécution ne se vérifie pas en CI. - Catégorielle (6 couleurs) : la teinte d’accent en tête, puis des écarts de teinte et une alternance de deux niveaux de clarté, pour que deux couleurs voisines diffèrent aussi en luminance (ce qui survit aux daltonismes). Les écarts sont fixés et calibrés en V2a sous la contrainte ci-dessous. Réalisé en V2-0 (D-039) : la catégorielle n’est pas une table d’écarts fixes mais un échantillonnage du point le plus éloigné, par teinte, qui atteint ΔE00 ≥ 15 sur 12 teintes et deux thèmes ; une table fixe plafonnait vers 8 à 9 en pire cas. Au-delà de 6 séries : regrouper, ne pas étirer la palette.
- Séquentielle : une teinte (l’accent), 7 pas de clarté monotone, contrôlés en luminance croissante (ColorBrewer, Brewer, 2003, pour le principe ; pas de palette empruntée).
- Divergente : deux extrémités (l’accent et sa teinte opposée) de clarté décroissante vers un
centre neutre (
--pp-color-line) ; l’axe bleu-orangé est le plus sûr pour la protanopie et la deutéranopie. - Vérification daltonisme : simulation de la protanopie, de la deutéranopie et de la tritanopie par les matrices de Machado, Oliveira et Fernandes (2009, IEEE TVCG 15(6), gravité 1,0), appliquées en RGB linéaire ; écart CIEDE2000 (CIE 142-2001 ; implémentation de Sharma, Wu et Dalal, 2005) entre chaque paire de couleurs simulées. Critère : ΔE00 ≥ 10 pour toute paire de la catégorielle, ≥ 3 entre pas voisins de la séquentielle (seuils de convention, à confirmer par la calibration de V2a ; les marqueurs et motifs restent exigés en plus, décision 3). Test Vitest sur l’accent par défaut et sur 12 teintes d’accent espacées de 30°, thèmes clair et sombre.
- Contrat d’une variable (réalisation : point ouvert 1) : un projet externe fournit une teinte
d’accent ;
create(ou le build detokens) appelle la dérivation, qui régénère accent, catégorielle, séquentielle, divergente,--pp-color-on-accent, et refuse (erreur typée) une teinte dont la dérivation échoue au contrôle. Typographie : une variable de pile de police dont dérivent--pp-font-family-sanset--pp-font-family-mono; axes et libellés en--pp-font-size--1, chiffres enfont-variant-numeric: tabular-nums.
Décision 5 : direction artistique (identité Encre)
- Rapport données/encre (Tufte, 1983) : tout élément qui ne porte pas de donnée disparaît : pas de cadre, pas de fond de tracé, pas de grille verticale, pas d’ombre, de dégradé ni de flou (interdits d’AGENTS.md).
- Annotations : texte direct, 3 au plus, trait de rappel fin. Traits : courbes à 2
--pp-size-rule(jetons de trait de graphique à créer en V2a), grille horizontale à 1--pp-size-ruleen--pp-color-line, ligne de base pleine ; barres pleines à coins droits ; aire à 12 % d’opacité de la couleur de série au plus, trait au-dessus. - Axes : barres à partir de zéro ; courbes avec ou sans zéro, dit dans la description ; graduations « rondes » (Heckbert, 1990), 5 au plus.
- États : vide =
EmptyState; erreur = erreur typée qui fait échouer le build par défaut, ouAlertplus tableau de repli si la donnée est valide et le rendu impossible ; chargement =Skeleton, seulement pour un îlot interactif (un graphique statique n’a pas d’état de chargement).
Décision 6 : architecture
- Nouveau paquet
@paulperigault/dataviz(packages/dataviz), TypeScript pur, sans dépendance de rendu, dépendant detokens(couleur, contraste). Sens :tokens ← dataviz ← astro;cssn’en dépend pas. Un projet non Astro réutilise le domaine ; ligne à ajouter au tableau d’AGENTS.md. - Structure (ADR-0006) :
domain/(contrat, échelles, graduations, géométrie, formatage fr, palettes, simulation du daltonisme, LTTB de référence) ;application/(buildChartModel:Dataset+ options → modèle géométrique ;ports.ts:Aggregator,Formatter) ;infrastructure/(parse-dataset, adaptateurIntl) ;errors.ts;index.ts. - Composants :
packages/astro/src/data/(LineChart,BarChart,AreaChart,Gauge,Sparkline,Kpi,DataTable), sans couches (ADR-0006, décision 3), ≤ 120 lignes ; ils reçoivent un modèle, ne calculent rien. Polymorphismeasfermé (figure | div | section). - Tests : calculs purs sans double (échelles, graduations, géométrie, formatage, simulation :
valeurs de référence publiées), propriétés à graine fixe (sans dépendance) ; rendu
par instantané limité à un par composant ; budgets (nœuds, octets) ; absence de couleur brute et de
style; axe en e2e (2 thèmes, 2 fenêtres) ; contrôle du daltonisme.guardsvérifie la règle de dépendance.
Décision 7 : découpage
| Issue | Ce que l’ADR impose |
|---|---|
| #94 V2a | dataviz (contrat, échelles, palettes) ; LineChart, BarChart ; budgets ; test daltonien |
| #95 V2b | AreaChart, Gauge (bullet) sur le même contrat ; aire empilée ≤ 4 séries |
| #96 V3a | Kpi (variation dite en mots), Sparkline ≤ 1,5 ko, tendance annoncée |
| #97 V3b | DataTable, légende marqueur plus libellé, filtres :has() sans JS, cibles ≥ minimum |
| #98 V4a | pp-data : même Dataset en f64, quantiles de type 7, LTTB, aucun panic |
| #99 V4b | adaptateur WebAssembly du port Aggregator ; parité 1e-9 ; le rendu s’en passe |
| #100 V5 | page assemblant tout, soumise aux budgets, impression, contraste forcé, mouvement réduit |
Critères d’acceptation à corriger (issues non modifiées ; ajouts proposés) :
- Nouvelle issue à créer avant #94 : « [V2-0] paquet dataviz : contrat, échelles, formatage,
palettes » (le socle dépasse la taille
pagede #94) ; #94 en serait bloquée. - #94 : «
Datasetvalidé à la frontière, erreurs typées » ; « aucune couleur brute nistyledans le SVG (test) » ; « test daltonien (3 types) sur l’accent par défaut et 12 teintes » ; « budget de 400 nœuds et 10 ko » ; « titre, description et tableau liés » ; « motifs de trait et marqueurs » ; « formats fr ». - #95 : « jauge en barre bornée avec cible » ; « aire empilée ≤ 4 séries » ; mêmes critères que #94.
- #96 : « variation dite en mots » ; « sparkline ≤ 1,5 ko ». #97 : «
<th scope>» ; « filtres sans JavaScript » ; « tableau complet affiché sans filtre ». - #98 : remplacer «
cargo clippysans avertissement » par les critères d’ADR-0006 ; ajouter « quantiles de type 7 et LTTB conformes à la référence TypeScript » ; la dépendance à #93 reste. - #99 : ajouter « parité 1e-9 en CI », « temps de chargement mesuré », « build sans Rust », « le rendu fonctionne sans le paquet ». #100 : ajouter « impression », « contraste forcé », « mouvement réduit », « budget de poids de la page ». #93 : la preuve chiffrée des palettes vient du test de #94.
Points ouverts et risques
Noms et lieu du contrat d’une variable (teinte d’accent, pile de police): fermé le 2026-10-28 (D-039). Entrées--pp-accent-hue(teinte OKLCH en degrés ; clarté et chroma fixés par le système : clairoklch(47% 0.19 h), sombreoklch(78% 0.1 h)) et--pp-font-stack(pile du texte, dont dérive--pp-font-family-sans;--pp-font-family-monoà part). Sorties inchangées :--pp-color-accent,--pp-chart-*,--pp-font-family-*. Les neutres d’Encre (teinte 270.7) ne suivent pas l’accent : choix explicite à trancher au branchement danstokensetcreate. La dérivation est livrée par@paulperigault/dataviz(V2-0, #120) ; le branchement est l’issue #121.- Seuils ΔE00 (10 et 3) : conventions ; la calibration de V2a peut les ajuster, sans les affaiblir sans écrit. Risque : 6 couleurs tritan-sûres sur fond sombre et clair sont peut-être inatteignables ; repli : 5 séries au plus, motifs de trait plus visibles.
- Un gabarit SVG unique contre deux (compact et large) : le premier est plus léger, peut rendre
des étiquettes petites sur mobile ; à trancher à la maquette de V2a, avec l’impression du tableau
dans un
detailsfermé. - Risque de poids : un paquet de plus et des SVG verbeux ; parade : budgets bloquants en CI et arrêt du lot si un graphique dépasse sans justification écrite.