◆ Portail Antalaha

Annuaire › Code ouvert

Pluviométrie mensuelle - tableau de bord (p5.js)

tutorial · Voir le site ↗
Connectez-vous pour copier ce site

Code publié par son auteur, en lecture seule. « Copier dans mon espace » crée un nouveau site à vous (non publié) que vous pouvez modifier librement.

js/sketch.js · 8.2 Ko

/* ============================================================
   sketch.js - programme principal du tableau de bord pluviométrie
   ============================================================
   Rôle : créer le canevas, tenir à jour l'état (année choisie,
   mode, animation) et appeler les fonctions de dessin dans
   l'ordre à chaque image.

   setup() : appelée une fois ; draw() : appelée en boucle.
   ============================================================ */

/* ---------- État partagé (variables globales) ---------- */
let zone = { x0: 0, y0: 0, x1: 0, y1: 0, larg: 0, haut: 0, colonne: 0 };  // zone de tracé (calculée)
let echelle = { maxi: 500, pas: 100 };   // échelle verticale
let choixCourant = 'moyenne';            // 'moyenne' ou une année
let valeursCibles = [];                  // valeurs à atteindre (12 nombres)
let valeursAffichees = [];               // valeurs actuellement dessinées (animées)
let moyenneSerie = [];                   // moyenne pluriannuelle (12 nombres)
let modeCourbe = false;                  // false = histogramme, true = courbe
let melange = 0;                         // 0 = histogramme pur ... 1 = courbe pure (fondu)
let indexSelection = -1;                 // mois sélectionné (-1 = aucun)

/* ------------------------------------------------------------
   calculerTailleCanevas()
   But : largeur = celle de la boîte HTML ; hauteur = ratio de la
         largeur, bornée (voir config.js).
   Retour : {w, h}
   ------------------------------------------------------------ */
function calculerTailleCanevas() {
  const boite = document.getElementById('conteneur-canvas');
  const w = Math.max(240, boite ? boite.clientWidth : windowWidth);
  const h = constrain(w * RATIO_HAUTEUR, HAUTEUR_MIN, HAUTEUR_MAX);
  return { w: Math.floor(w), h: Math.floor(h) };
}

/* ------------------------------------------------------------
   calculerZone()
   But : (re)calculer le rectangle de tracé à partir de la taille
         du canevas et des marges. À rappeler si le canevas change.
   ------------------------------------------------------------ */
function calculerZone() {
  zone.x0 = MARGE_GAUCHE;
  zone.y0 = MARGE_HAUT;
  zone.x1 = width - MARGE_DROITE;
  zone.y1 = height - MARGE_BAS;
  zone.larg = zone.x1 - zone.x0;
  zone.haut = zone.y1 - zone.y0;
  zone.colonne = zone.larg / 12;     // 12 mois -> 12 colonnes de même largeur
}

/* ------------------------------------------------------------
   setup()
   But : préparer le canevas, les données et les commandes HTML.
   ------------------------------------------------------------ */
function setup() {
  const t = calculerTailleCanevas();
  createCanvas(t.w, t.h).parent('conteneur-canvas');
  calculerZone();

  textFont(POLICE);

  // Données
  echelle = echelleVerticale();
  moyenneSerie = serieMoyenne();
  valeursCibles = serieChoisie(choixCourant);
  // Au départ, toutes les barres valent 0 : elles vont "pousser" jusqu'à
  // leur valeur grâce à l'animation (c'est l'animation d'apparition).
  valeursAffichees = new Array(12).fill(0);

  // Éléments HTML
  remplirChoixAnnee();
  remplirTableauHTML();
  brancherCommandes();
  mettreAJourTextes();
}

/* ------------------------------------------------------------
   draw()
   But : redessiner le graphique, couche après couche.
   Comme au théâtre : on commence par le fond, on termine par
   l'infobulle qui doit passer PAR-DESSUS tout.
   ------------------------------------------------------------ */
function draw() {
  background(COULEURS.fond);               // efface l'image précédente

  mettreAJourAnimation();

  dessinerSurvol();                        // 1. colonne survolée (arrière-plan)
  dessinerGrille();                        // 2. grille + graduations
  dessinerNomsMois();                      // 3. noms des mois

  // 4. Les données : on dessine les deux modes avec une opacité
  //    complémentaire (fondu enchaîné). On saute celui qui est invisible.
  if (melange < 0.995) dessinerHistogramme(valeursAffichees, 1 - melange);
  if (melange > 0.005) dessinerCourbe(valeursAffichees, melange);

  // 5. Ligne de moyenne (inutile si on affiche déjà la moyenne)
  if (choixCourant !== 'moyenne') dessinerLigneMoyenne(moyenneSerie);

  dessinerLegende(libelleChoix(choixCourant), choixCourant !== 'moyenne');
  dessinerMention();
  dessinerInfobulle(valeursAffichees);     // 6. infobulle tout en haut
}

/* ------------------------------------------------------------
   mettreAJourAnimation()
   But : rapprocher les valeurs affichées de leurs cibles, et le
         mélange de son état cible.

   lerp(a, b, t) : renvoie un nombre entre a et b.
     t = 0 -> a ; t = 1 -> b ; t = 0,1 -> 10 % du chemin de a vers b.
   En répétant lerp() à chaque image avec la valeur actuelle comme
   point de départ, on obtient un mouvement qui ralentit en
   approchant du but (effet "amorti" très naturel).

   deltaTime = temps (en ms) écoulé depuis l'image précédente.
   On s'en sert pour que la vitesse soit LA MÊME sur un téléphone
   lent (30 images/s) ou un ordinateur rapide (60 images/s) :
     facteur = 1 - VITESSE^(temps écoulé en secondes)
   ------------------------------------------------------------ */
function mettreAJourAnimation() {
  const dt = Math.min(deltaTime, 100);                          // on borne : évite un saut après un onglet en veille
  const k = 1 - Math.pow(VITESSE_ANIMATION, dt / 1000);
  for (let i = 0; i < 12; i++) {
    valeursAffichees[i] = lerp(valeursAffichees[i], valeursCibles[i], k);
  }

  const km = 1 - Math.pow(VITESSE_MELANGE, dt / 1000);
  melange = lerp(melange, modeCourbe ? 1 : 0, km);
  if (Math.abs(melange - (modeCourbe ? 1 : 0)) < 0.002) melange = modeCourbe ? 1 : 0;   // on "colle" à la cible
}

/* ------------------------------------------------------------
   windowResized()
   But : adapter le graphique à la nouvelle taille de la fenêtre.
   ------------------------------------------------------------ */
function windowResized() {
  const t = calculerTailleCanevas();
  resizeCanvas(t.w, t.h);
  calculerZone();
}

/* ------------------------------------------------------------
   remplirChoixAnnee()
   But : fabriquer les <option> de la liste déroulante à partir
         des données (rien à écrire à la main dans le HTML).
   ------------------------------------------------------------ */
function remplirChoixAnnee() {
  const liste = document.getElementById('choix-annee');
  let html = '<option value="moyenne">' + libelleChoix('moyenne') + '</option>';
  for (const a of listeAnnees()) {
    html += '<option value="' + a + '">' + a + '</option>';
  }
  liste.innerHTML = html;
  liste.value = choixCourant;
}

/* ------------------------------------------------------------
   brancherCommandes()
   But : relier la liste déroulante et le bouton à notre code
         avec addEventListener (écouteurs d'événements).
   ------------------------------------------------------------ */
function brancherCommandes() {
  document.getElementById('choix-annee').addEventListener('change', function (e) {
    choixCourant = e.target.value;                  // 'moyenne' ou '2023'...
    valeursCibles = serieChoisie(choixCourant);     // les barres vont s'animer vers ces valeurs
    mettreAJourTextes();
  });

  document.getElementById('bouton-mode').addEventListener('click', function () {
    modeCourbe = !modeCourbe;                       // ! inverse un booléen
    mettreAJourTextes();
  });
}

/* ------------------------------------------------------------
   mettreAJourTextes()
   But : synchroniser les textes HTML (bouton, résumé) et la
         description accessible du canevas avec l'état courant.
   ------------------------------------------------------------ */
function mettreAJourTextes() {
  const bouton = document.getElementById('bouton-mode');
  bouton.textContent = modeCourbe ? 'Afficher l\'histogramme' : 'Afficher la courbe';
  bouton.setAttribute('aria-pressed', modeCourbe ? 'true' : 'false');

  const resume = texteResume(choixCourant);
  document.getElementById('resume').textContent = resume;

  // describe() : texte alternatif du canevas, lu par les lecteurs d'écran.
  describe((modeCourbe ? 'Courbe' : 'Histogramme') + ' de la pluviométrie mensuelle. ' + resume);
}