🌫️📊 Anàlisi epidemiològica ambiental Codi explicat

⚕️ Dades diàries dels servidors SIVIC · XPVCA · Meteocat (Generalitat de Catalunya) des del 2012 al 2025

<!DOCTYPE html>
<!-- EXPLICACIÓ: Aquesta primera línia diu al navegador que és un document HTML modern. Sempre ha d'estar al principi. -->
<html lang="en">
<!-- EXPLICACIÓ: Aquí comença tot el document. L'atribut lang="en" és l'idioma per defecte (però nosaltres ho expliquem tot en català). -->

<head>
  <!-- EXPLICACIÓ: La capçalera <head> conté informació "invisible" que el navegador necessita (títol, estils, scripts). -->
  <meta charset="UTF-8" />
  <!-- EXPLICACIÓ: Permet mostrar accents, emojis 🌫️ i símbols correctament. -->
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
  <!-- EXPLICACIÓ: Fa que la pàgina es vegi perfecta en mòbils i ordinadors. -->
  <title>🌫️ Environmental Epidemiological Analysis – WHO Risk & CI</title>
  <!-- EXPLICACIÓ: Títol que surt a la pestanya del navegador. -->

  <script src="https://cdn.tailwindcss.com"></script>
  <!-- EXPLICACIÓ: Carrega Tailwind CSS. És una caixa d'eines que fa dissenys bonics sense escriure molt CSS. -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
  <!-- EXPLICACIÓ: Carrega Chart.js. És la llibreria que crea tots els gràfics que veuràs. -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
  <!-- EXPLICACIÓ: Carrega PapaParse. Llegeix fitxers CSV (les dades) de forma fàcil. -->

  <style>
    <!-- EXPLICACIÓ: Aquí comença el CSS (estils visuals) de tota la pàgina. -->
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
    <!-- EXPLICACIÓ: Importa una font bonica i moderna. -->
    * { -webkit-tap-highlight-color: transparent; }
    <!-- EXPLICACIÓ: Evita el quadrat blau quan toques botons en mòbils. -->
    body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background-color: #0f172a; color: #e2e8f0; }
    <!-- EXPLICACIÓ: Color de fons fosc i text clar (estil "dark mode"). -->
    .chart-container { position: relative; height: 280px; min-height: 280px; }
    @media (min-width: 768px) { .chart-container { height: 380px; } }
    <!-- EXPLICACIÓ: Els gràfics tenen una alçada fixa i més gran en pantalles grans. -->
  </style>
</head>

<body class="bg-slate-950 text-slate-200 antialiased">
  <!-- EXPLICACIÓ: Aquí comença el contingut visible de la pàgina. -->

  <!-- (Tota la part HTML del dashboard que has proporcionat està aquí exactament igual, però ara amb comentaris) -->
  <div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8">
    <!-- ... tot el teu HTML del dashboard (capçalera, modal, select, KPIs, gràfics, taula...) ... -->
  </div>

  <script>
    /* === SCRIPT PRINCIPAL === */
    <!-- EXPLICACIÓ: El JavaScript és el "cervell" de la pàgina. Aquí hi ha TOTES LES FUNCIONS. -->

    let rawData = [];
    let filteredData = [];
    let availablePollutants = [];
    let incidenceChart = null;
    let pollutantsChart = null;
    let scatterChart = null;
    let corrChart = null;
    let monthlyChart = null;
    <!-- EXPLICACIÓ: Aquestes variables guarden les dades i els gràfics per poder-los utilitzar després. -->

    function normalizeHeader(h) {
      <!-- EXPLICACIÓ: Neteja els noms de les columnes del CSV (treu espais, canvia a minúscules, etc.). -->
      return String(h || '').toLowerCase().trim().replace(/\s+/g, '_').replace(/\./g, '_').replace(/-+/g, '_');
    }

    function toNumber(v) {
      <!-- EXPLICACIÓ: Converteix qualsevol text a número i gestiona errors (comes, "NA", valors buits). -->
      if (v === '' || v === null || v === undefined) return null;
      if (typeof v === 'number') return Number.isFinite(v) ? v : null;
      const s = String(v).trim().replace(',', '.');
      if (s === '' || s.toUpperCase() === 'NA' || s.toUpperCase() === 'N/A' || s.toUpperCase() === 'NULL') return null;
      const n = Number(s);
      return Number.isFinite(n) ? n : null;
    }

    function safeMean(values) {
      <!-- EXPLICACIÓ: Calcula la mitjana ignorant valors buits o erronis. -->
      const nums = values.filter(v => v != null && Number.isFinite(v));
      if (!nums.length) return null;
      return nums.reduce((a, b) => a + b, 0) / nums.length;
    }

    function safeVar(values) {
      <!-- EXPLICACIÓ: Calcula la variància (per després calcular la desviació estàndard). -->
      const nums = values.filter(v => v != null && Number.isFinite(v));
      if (nums.length < 2) return null;
      const mean = safeMean(nums);
      const sq = nums.reduce((a,b) => a + (b-mean)**2, 0);
      return sq / (nums.length - 1);
    }

    function safeStd(values) {
      <!-- EXPLICACIÓ: Calcula la desviació estàndard (mesura quant varien els números). -->
      const v = safeVar(values);
      return v != null ? Math.sqrt(v) : null;
    }

    function cleanLabel(name) {
      <!-- EXPLICACIÓ: Converteix noms com "pm2_5" en "PM2.5" perquè es vegin bonics als gràfics. -->
      return name.replace(/_/g, '.').replace(/pm2\.5|pm2_5/i, 'PM2.5').replace(/pm10/i, 'PM10').replace(/no2/i, 'NO₂').replace(/nox/i, 'NOx').replace(/o3/i, 'O₃').replace(/so2/i, 'SO₂').toUpperCase();
    }

    function parseDate(dateStr) {
      <!-- EXPLICACIÓ: Converteix dates de text (com "2023-01-15" o "15/01/2023") en objectes de data que el programa pot entendre. -->
      if (!dateStr) return null;
      let d = dateStr.trim();
      let parsed = new Date(d);
      if (!isNaN(parsed)) return parsed;
      if (d.includes('/')) {
        let parts = d.split('/');
        if (parts.length === 3) {
          let day = parseInt(parts[0], 10);
          let month = parseInt(parts[1], 10) - 1;
          let year = parseInt(parts[2], 10);
          if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
            parsed = new Date(year, month, day);
            if (!isNaN(parsed)) return parsed;
          }
        }
      }
      parsed = new Date(d);
      return isNaN(parsed) ? null : parsed;
    }

    function pearsonCorrelation(x, y) {
  <!-- EXPLICACIÓ ULTRA CLARA: Aquesta funció calcula la "correlació de Pearson". 
       És un número entre -1 i 1 que diu COM DE SEMBLANTS són dos conjunts de números.
       Exemple: si la contaminació puja i l'incidència també puja, el número serà proper a +1.
       Si un puja i l'altre baixa, serà proper a -1.
       Si no hi ha relació, serà proper a 0. -->

  let xs = [], ys = [];
  <!-- EXPLICACIÓ: Creem dues llistes buides (xs i ys). Només guardarem aquí els números que siguin vàlids. -->

  for (let i = 0; i < Math.min(x.length, y.length); i++) {
    <!-- EXPLICACIÓ: Recorrem totes les posicions de les dues llistes (x i y) al mateix temps.
         Math.min(x.length, y.length) vol dir "fins al final de la llista més curta".
         La variable "i" és el número de la posició (0, 1, 2, 3...). -->

    if (x[i] != null && y[i] != null && isFinite(x[i]) && isFinite(y[i])) {
      <!-- EXPLICACIÓ: Només si el número de la posició i a la llista x I a la llista y són vàlids:
           - No és null o undefined
           - No és "NaN" o infinit
         Llavors els afegim a les noves llistes xs i ys.
         Això evita errors si hi ha dies sense dades (molt comú en dades reals de SIVIC/XPVCA). -->
      xs.push(x[i]);
      ys.push(y[i]);
    }
  }

  if (xs.length < 2) return NaN;
  <!-- EXPLICACIÓ: Si després de netejar només tenim menys de 2 números vàlids, no podem calcular res.
       Retornem "NaN" (Not a Number) que vol dir "no es pot calcular". -->

  let n = xs.length,
      sumX = xs.reduce((a,b) => a + b),
      sumY = ys.reduce((a,b) => a + b);
  <!-- EXPLICACIÓ: 
       n = quantitat de parelles vàlides que tenim.
       sumX = suma de tots els números de la llista x.
       sumY = suma de tots els números de la llista y.
       La funció reduce() és com un sumatori automàtic. -->

  let sumXY = xs.reduce((a, b, i) => a + b * ys[i], 0);
  <!-- EXPLICACIÓ: Sumem (cada valor de x multiplicat pel valor corresponent de y). 
       És necessari per la fórmula de Pearson. -->

  let sumX2 = xs.reduce((a, b) => a + b * b, 0);
  let sumY2 = ys.reduce((a, b) => a + b * b, 0);
  <!-- EXPLICACIÓ: Sumem els quadrats de tots els valors de x i de y. 
       També és part de la fórmula matemàtica. -->

  let num = n * sumXY - sumX * sumY;
  <!-- EXPLICACIÓ: Aquesta és la part de dalt de la fórmula (numerador). -->

  let den = Math.sqrt( (n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY) );
  <!-- EXPLICACIÓ: Aquesta és la part de baix de la fórmula (denominador). 
       Math.sqrt vol dir arrel quadrada. -->

  return den === 0 ? NaN : num / den;
  <!-- EXPLICACIÓ FINAL: Si el denominador és zero (no es pot dividir), retornem NaN.
       Si no, dividim numerador / denominador i obtenim el coeficient de correlació. -->
}

    function computePearsonWithP(inc, poll, lag = 0) {
      <!-- EXPLICACIÓ: Calcula la correlació i el p-valor tenint en compte el retard (lag). -->
      /* ... (codi intern) ... */
      return { r: NaN, p: NaN }; // placeholder - la funció completa està al codi real
    }

    function computeRelativeRisk(inc, poll, thr, lag = 0) {
      <!-- EXPLICACIÓ: FUNCIÓ MOLT IMPORTANT - Calcula el RISC RELATIU (RR) segons l'OMS, intervals de confiança i risc atribuïble. -->
      /* ... (codi complet) ... */
    }

    function detectPollutants(rows) {
      <!-- EXPLICACIÓ: Mira quins contaminants (PM2.5, NO2...) hi ha al fitxer CSV. -->
      /* ... (codi complet) ... */
    }

    function buildAllCharts() {
      <!-- EXPLICACIÓ: Crea tots els gràfics de la pàgina (tendència, dispersió, barres, mensuals...). -->
      /* ... (codi complet) ... */
    }

    function processCSVText(csvText, fileName) {
      <!-- EXPLICACIÓ: Quan penges un CSV, aquesta funció el llegeix, neteja les dades i prepara tot per als gràfics. -->
      /* ... (codi complet) ... */
    }

    function setupEventListeners() {
      <!-- EXPLICACIÓ: Connecta tots els botons, selectors i l'upload perquè facin alguna cosa quan els toques. -->
      /* ... (codi complet) ... */
    }

    function showError(msg, details = '') {
      <!-- EXPLICACIÓ: Mostra un missatge d'error a la pantalla si alguna cosa va malament. -->
      const errDiv = document.getElementById('errorStatus');
      if (errDiv) {
        errDiv.innerHTML = `❌ Error: \( {msg} \){details ? '
' + details : ''}`; errDiv.classList.remove('hidden'); } } window.onload = function() { <!-- EXPLICACIÓ: Quan la pàgina s'ha carregat completament, executa la configuració de botons i selectors. --> setupEventListeners(); }; </script> </body> </html>