# `web/` — front statique du catalogue

Page HTML unique, sans build, sans serveur applicatif : `index.html` charge
`../catalog/sites.json` par `fetch()` et dessine trois vues avec Apache ECharts
(vendoré, aucun appel réseau au chargement). C'est délibéré : aucun modèle ne
tourne derrière cette page, donc l'AGPL-3.0 d'`ultralytics` (voir
`CLAUDE.md`) ne s'applique pas à ce qui est servi ici.

## Lancer en local

```
.\venv\Scripts\python.exe -m http.server 8000
```

puis ouvrir `http://localhost:8000/web/`. Ouvrir `index.html` directement
(`file://`) ne fonctionne PAS : les navigateurs bloquent `fetch()` sur des
fichiers locaux par CORS. Un message d'erreur explicite s'affiche dans ce cas
et rappelle la commande ci-dessus.

## Fichiers

| Fichier | Rôle |
|---|---|
| `index.html` | Toute la page : CSS et JS inline, pas de build. |
| `vendor/echarts.min.js` | Apache ECharts 5, téléchargé depuis jsdelivr, vendoré tel quel. Licence Apache-2.0. |
| `france.geo.json` | Contour de la France métropolitaine (voir licence ci-dessous). |
| `README.md` | Ce fichier. |

## Source et licence du contour de France

`france.geo.json` est extrait de **Natural Earth**, résolution 1:50m
(`ne_50m_admin_0_countries`), depuis le dépôt de référence
`nvkelso/natural-earth-vector` (les données brutes de naturalearthdata.com).

- **Licence : domaine public.** Termes exacts sur
  <https://www.naturalearthdata.com/about/terms-of-use/> : *« All versions of
  Natural Earth raster + vector map data found on this website are in the
  public domain »* et *« No permission is needed to use Natural Earth.
  Crediting the authors is unnecessary. »* Aucune attribution n'est requise ;
  ce fichier la donne quand même par courtoisie ("Made with Natural Earth").
- **Extraction :** la feature `ADMIN == "France"` du jeu de données contient
  10 polygones (la France métropolitaine, la Corse, une île au large de La
  Rochelle, et les territoires ultramarins — Guyane, Martinique, Guadeloupe,
  Mayotte, Réunion). Seuls les 3 premiers (métropole + Corse + l'île côtière)
  sont conservés : ce projet cible un hypermarché à Saint-Herblain et un
  catalogue de Carrefour en France métropolitaine, pas les DOM-TOM.
- **Taille :** 12 890 octets (bien sous la cible de ~50 Ko du plan de tâches) —
  la résolution 1:50m donne un contour reconnaissable (Bretagne, Cotentin,
  golfe du Lion) sans repasser par un outil de simplification séparé.
- Régénération : voir la commande Python utilisée, conservée ici pour mémoire :
  télécharger `ne_50m_admin_0_countries.geojson` depuis le dépôt ci-dessus,
  filtrer la feature `ADMIN == "France"`, ne garder que les polygones d'indices
  0, 1, 2 (Corse, métropole, île de Ré), et réécrire une `FeatureCollection` à
  un seul `Feature` de type `MultiPolygon`.

## Compétence dataviz appliquée

Configuré via la compétence `dataviz` (chargée avant tout code de graphe,
comme l'exige le plan de tâches) :

- **Palette documentée, inchangée.** Toutes les teintes viennent de
  `references/palette.md` de la compétence : bleu slot-1 (`#2a78d6` clair /
  `#3987e5` sombre) comme unique teinte catégorielle/séquentielle, rampe
  séquentielle bleue pour la magnitude, couleurs de statut fixes (`warning`
  `#fab219`, `critical` `#d03b3b`) réservées aux anomalies — jamais réutilisées
  comme "série 4". Aucune teinte inventée : pas de passage par le validateur
  nécessaire (règle 6, "documented palette only").
- **Un seul hue par graphe.** La carte encode la magnitude (comptage) par une
  rampe séquentielle bleue (taille du point *et* couleur, un renforcement
  volontaire, pas une redondance nuisible) ; le classement et l'histogramme
  utilisent la teinte catégorielle slot-1 en aplat — jamais une rampe sur des
  catégories nominales (anti-pattern documenté : "value-ramp on nominal
  categories").
- **La rampe séquentielle est bornée pour ne pas faire disparaître le zéro.**
  Par défaut une rampe séquentielle laisse le "quasi-zéro" se fondre dans la
  surface (comportement voulu pour un vrai zéro sans intérêt). Ici, un
  comptage à 0 est une anomalie connue (le lot OSM est probablement un
  bâtiment) que la consigne demande de **rendre visible, pas lisser**. La
  rampe est donc bornée à l'intérieur des paliers déjà documentés pour la
  rampe *ordinale* (`#86b6ef` / step 250 en clair, `#184f95` / step 600 en
  sombre — cf. `palette.md`, "the step nearest the surface must still clear
  2:1") au lieu des paliers extrêmes 100/700, et le site à 0 reçoit en plus un
  anneau de 2 px en couleur de statut critique (`#d03b3b`) + une légende et
  une note de bas de graphe. Trois signaux redondants (couleur bornée,
  anneau, texte) pour un seul fait : ne pas l'escamoter.
- **Légende toujours présente pour ≥ 2 séries.** La carte a deux séries
  (sites comptés / échecs de collecte) : une légende HTML custom les
  distingue (rond bleu vs losange orange), en plus du `visualMap` continu qui
  sert de légende de couleur pour la magnitude.
- **Vue tableau = jumeau accessible des trois graphes.** Une seule table
  triable (clic sur un en-tête) sert de secours accessible pour la carte, le
  classement et l'histogramme : mêmes données (nom, commune, statut,
  comptage, aire), pas de dépendance à la seule couleur.
- **Étiquetage sélectif, jamais un chiffre par point.** Pas de label sur
  chaque point de la carte ni chaque barre — les valeurs vivent dans l'infobulle
  et la vue tableau (règle "never a number on every point").
- **Tooltip : valeur en avant, texte non fiable échappé.** Les noms
  d'enseigne/commune viennent de tags OSM (donnée non fiable) ; ils sont
  échappés avant insertion dans les tooltips ECharts (qui exigent du HTML), et
  le panneau de détail (sous notre contrôle direct) utilise `textContent`
  partout — jamais de concaténation HTML sur ces valeurs.
- **Thème clair/sombre sélectionné, pas un flip automatique.** Script
  bloquant dans `<head>` (évite le flash), `data-theme` sur `<html>`,
  persistance `localStorage`, et un jeu de couleurs recalculé (pas une simple
  inversion CSS) pour chaque graphe ECharts au changement de thème — ECharts
  ne lit pas les variables CSS, donc `renderAllCharts()` reconstruit les
  options avec les hex du thème actif.
- **Classement plafonné à 25 barres.** `sites.json` de test n'a que 2 sites
  "ok", mais la tâche 7 en amènera ~347. Un graphe à barres à 347 catégories
  serait illisible ; le classement affiche le top 25 par comptage avec une
  légende explicite ("Top 25 sur N"), la liste complète restant dans la vue
  tableau. Décision prise pour que le front tienne à la fois pour 3 et pour
  ~347 sites, comme demandé.

## Note de qualité des données, volontairement visible

Dans les données réelles à 3 sites, un site "ok" a un comptage de 0 : son
polygone OSM `amenity=parking` est en réalité un bâtiment, pas un parking
(limite connue de l'heuristique de sélection du lot dans `stores.py`). Ce
site n'est filtré nulle part : il apparaît sur la carte (anneau rouge), dans
l'histogramme (le bin à 0 est marqué) et dans la vue tableau (puce rouge).
Le classement, lui, est plafonné au top 25 par comptage (voir plus haut) :
à 3 sites un 0 y tient encore, mais à l'échelle réelle (~347 sites comptés)
un comptage nul ne fera jamais partie du top 25 et n'y apparaîtra donc pas —
la carte, l'histogramme et la vue tableau restent les vues qui le montrent
à toute échelle. C'est le sens du design honnête : la lacune doit se voir,
pas se lisser.

## Ce qui n'a pas pu être vérifié visuellement par l'agent qui a écrit ce code

Voir `.superpowers/sdd/task-6-report.md` pour le détail de ce qui a été
vérifié dans un vrai navigateur au moment de l'écriture, et ce qui reste à la
charge d'une vérification visuelle humaine ou du contrôleur.
