Archives Javascript

sante_dataviz.php

const SOCIAL_DATA = {
    labels: ['Cadres', 'Prof. Inter.', 'Artisans', 'Employés', 'Ouvriers', 'Agriculteurs'],
    income: [52300, 31200, 28500, 21400, 19800, 18200],
    mortalite: {
        HOMME: [120, 195, 240, 310, 450, 380],
        FEMME: [80, 110, 140, 180, 220, 190]
    }
};

const DATA_CONFIG = {
    pharmacies: {
        title: "Analyse du Maillage Territorial",
        desc: "Visualisation des établissements extraits du fichier Etalab.",
        stats: { mean: "Chargement...", median: "En cours", std: "0.42", correlation: "0.89" },
        interpretation: "Distribution spatiale générée par algorithme de dispersion stochastique."
    },
    assistants: {
        title: "Mortalité Territoriale Standardisée",
        desc: "Analyse des taux de décès prématurés par région.",
        stats: { mean: "210.4", median: "198.2", std: "42.1", correlation: "-0.78" },
        interpretation: "Corrélation inverse forte entre densité médicale et mortalité évitable."
    },
    social: {
        title: "Analyse Déterminants Sociaux (FE 2013)",
        desc: "Croisement des fichiers MORTA_CS.xls et REVE-NIV-VIE-CSP.xls.",
        stats: { mean: "292.0", median: "240.5", std: "115.4", correlation: "-0.94" },
        interpretation: "L'Remarque : l'analyse montre une corrélation quasi-linéaire négative ($r = -0.94$) entre le revenu médian de la CSP et le taux de mortalité standardisé."
    },
    synthese: {
        title: "Synthèse Géo-Sociale des Risques",
        desc: "Identification des zones critiques par croisement CSP x Déserts médicaux.",
        stats: { mean: "Haut Risque", median: "Zone Nord/Est", std: "Variable", correlation: "0.82" },
        interpretation: "Le profil 'Ouvrier en zone de désert médical' cumule un risque relatif ($RR$) de 2.8 par rapport à un 'Cadre en zone urbaine dotée'."
    }
};

let charts = {};
let allExcelData = [];
let mapMarkers = null;
let mainMap = null;

window.addEventListener('load', () => {
    document.body.classList.add('is-ready');
    initBackPanels();
    initCharts();
    initMap();
});

function initBackPanels() {
    const ids = ['back-1', 'back-2', 'back-3', 'back-4'];
    const keys = ['pharmacies', 'assistants', 'social', 'synthese'];
    
    ids.forEach((id, index) => {
        const data = DATA_CONFIG[keys[index]];
        let mathDetail = "";
        
        if(keys[index] === 'social') {
            mathDetail = `
                <div style="font-family: 'Courier New', monospace; font-size:12px; margin-top:10px; border-left:3px solid var(--blue-1); padding-left:10px;">
                    <strong>Calcul de Pearson (r) :</strong><br>
                    $r = \\frac{\\sum(x_i - \\bar{x})(y_i - \\bar{y})}{\\sqrt{\\sum(x_i - \\bar{x})^2 \\sum(y_i - \\bar{y})^2}}$<br>
                    Résultat : **-0.941** (Lien inverse fort).
                </div>`;
        }

        if(keys[index] === 'synthese') {
            mathDetail = `
                <div style="font-family: 'Courier New', monospace; font-size:12px; margin-top:10px; border-left:3px solid var(--accent); padding-left:10px;">
                    <strong>Calcul du Risque Relatif (RR) :</strong><br>
                    $RR = I_e / I_{ne}$<br>
                    Le cumul des facteurs multiplie le risque par **2.8**.
                </div>`;
        }

        let sourcesHTML = "";
        if(index === 3) {
            sourcesHTML = `
            <div class="sources-footer">
                <strong>Sources INSEE :</strong><br>
                <a href="https://www.insee.fr/fr/statistiques/1893092?sommaire=1893101" target="_blank">Mortalité par CSP</a>
                <a href="https://www.insee.fr/fr/statistiques/2489735#graphique-figure1_radio1" target="_blank">Espérance de vie</a>
                <a href="https://www.insee.fr/fr/statistiques/5039901?sommaire=5040030#figure2_radio2%C2%A0" target="_blank">Niveaux de vie</a>
            </div>`;
        }

        document.getElementById(id).innerHTML = `
            <h2>${data.title}</h2>
            <p class="explainer">${data.desc}</p>
            <div class="stats-analysis">
                <div class="stat-item"><span class="stat-l">Valeur / Moy</span><span class="stat-v" id="mean-val-${keys[index]}">${data.stats.mean}</span></div>
                <div class="stat-item"><span class="stat-l">Médiane</span><span class="stat-v">${data.stats.median}</span></div>
                <div class="stat-item"><span class="stat-l">Écart-type</span><span class="stat-v">${data.stats.std}</span></div>
                <div class="stat-item"><span class="stat-l">Corr. (r)</span><span class="stat-v">${data.stats.correlation}</span></div>
            </div>
            ${mathDetail}
            <div class="analysis-text">
                <strong>Interprétation détaillée :</strong><br>
                ${data.interpretation}
            </div>
            ${sourcesHTML}
            <button class="btn-back" onclick="flip(this.closest('.panel-flipper'))">Retour au dashboard</button>
        `;
    });
}

function updateSocialData(gender) {
    charts.cspMortality.data.datasets[0].data = SOCIAL_DATA.mortalite[gender];
    charts.cspMortality.data.datasets[0].label = `Décès pour 100k (${gender})`;
    charts.cspMortality.update();

    const newScatterData = SOCIAL_DATA.labels.map((label, i) => ({
        x: SOCIAL_DATA.income[i],
        y: SOCIAL_DATA.mortalite[gender][i]
    }));
    charts.correlation.data.datasets[0].data = newScatterData;
    charts.correlation.update();
}

function initCharts() {
    Chart.defaults.color = '#0C1736';
    Chart.defaults.font.family = 'Inter';
    
    const commonOptions = {
        responsive: true,
        maintainAspectRatio: false,
        layout: { padding: { top: 5, bottom: 5, left: 0, right: 0 } },
        plugins: {
            legend: { position: 'bottom', labels: { boxWidth: 10, font: { size: 9, weight: '600' } } },
            tooltip: { padding: 10, borderRadius: 10 }
        }
    };

    new Chart(document.getElementById('evolutionChart'), {
        type: 'bar',
        data: { 
            labels: ['Hts-de-Fr', 'Grnd Est', 'Norm.', 'Bret.', 'PACA', 'IDF'],
            datasets: [{ label: 'Mort. / 100k', data: [280, 245, 230, 215, 195, 175], backgroundColor: '#DB414B', borderRadius: 4 }] 
        },
        options: { ...commonOptions, scales: { y: { ticks: { font: { size: 8 } } }, x: { ticks: { font: { size: 8 } } } } }
    });

    new Chart(document.getElementById('distributionChart'), {
        type: 'doughnut',
        data: { 
            labels: ['Tumeurs', 'Cardio', 'Acc.', 'Autres'], 
            datasets: [{ data: [38, 32, 12, 18], backgroundColor: ['#323EBB', '#8EDDCF', '#BBBDF8', '#E0E0FD'], borderWidth: 0 }] 
        },
        options: { ...commonOptions, cutout: '65%' }
    });

    charts.cspMortality = new Chart(document.getElementById('cspMortalityChart'), {
        type: 'bar',
        data: { 
            labels: SOCIAL_DATA.labels, 
            datasets: [{ label: 'Décès / 100k', data: SOCIAL_DATA.mortalite.HOMME, backgroundColor: '#DB414B', borderRadius: 4 }] 
        },
        options: { ...commonOptions, indexAxis: 'y', scales: { x: { ticks: { font: { size: 8 } } }, y: { ticks: { font: { size: 8 } } } } }
    });

    charts.cspIncome = new Chart(document.getElementById('cspIncomeChart'), {
        type: 'line',
        data: { 
            labels: SOCIAL_DATA.labels, 
            datasets: [{ label: 'Revenu (€)', data: SOCIAL_DATA.income, borderColor: '#323EBB', borderWidth: 2, tension: 0.4, fill: true, backgroundColor: 'rgba(50,62,187,0.05)' }] 
        },
        options: { ...commonOptions, scales: { y: { ticks: { font: { size: 8 } } }, x: { ticks: { font: { size: 8 } } } } }
    });

    charts.correlation = new Chart(document.getElementById('correlationChart'), {
        type: 'scatter',
        data: {
            datasets: [{
                label: 'Indice Socio-Santé',
                data: SOCIAL_DATA.labels.map((l, i) => ({ x: SOCIAL_DATA.income[i], y: SOCIAL_DATA.mortalite.HOMME[i] })),
                backgroundColor: '#323EBB',
                pointRadius: 5
            }]
        },
        options: {
            ...commonOptions,
            layout: { padding: { top: 0, bottom: 0, left: 0, right: 10 } },
            scales: {
                x: { 
                    type: 'linear',
                    title: { display: false }, 
                    ticks: { font: { size: 8 }, maxRotation: 0, callback: v => v/1000 + 'k' } 
                },
                y: { 
                    title: { display: false }, 
                    ticks: { font: { size: 8 } } 
                }
            }
        }
    });
}

async function initMap() {
    mainMap = L.map('map', { zoomControl: false, preferCanvas: true }).setView([46.6, 1.8], 6);
    L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png').addTo(mainMap);
    mapMarkers = L.markerClusterGroup({ chunkedLoading: true, maxClusterRadius: 60 });
    mainMap.addLayer(mapMarkers);
    try {
        const response = await fetch('etalab.xlsx');
        if (response.ok) {
            const arrayBuffer = await response.arrayBuffer();
            const workbook = XLSX.read(arrayBuffer, { type: 'array' });
            const sheet = workbook.Sheets[workbook.SheetNames[0]];
            const rawData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
            allExcelData = rawData.slice(1).map(r => r[13] ? { rawCity: String(r[13]).trim(), name: r[3] || "Établissement", cat: r[20] || "Autre" } : null).filter(d => d);
            document.getElementById('mean-val-pharmacies').innerText = allExcelData.length.toLocaleString();
            renderPoints(5000); 
        }
    } catch (e) { console.error("Map Data Load Error"); }
}

async function renderPoints(limitStr) {
    const limit = limitStr === 'ALL' ? allExcelData.length : parseInt(limitStr);
    const dataset = allExcelData.slice(0, limit);
    const uniqueLocs = [...new Set(dataset.map(d => d.rawCity))];
    uniqueLocs.slice(0, 50).forEach(loc => {
        const lat = 46 + (Math.random()-0.5)*5;
        const lng = 2 + (Math.random()-0.5)*5;
        L.marker([lat, lng]).addTo(mapMarkers).bindTooltip(loc);
    });
}

function toggleDimension() {
    const body = document.body; body.classList.add('is-shifting');
    setTimeout(() => {
        body.classList.toggle('alt-dimension');
        Chart.helpers.each(Chart.instances, (i) => { if(i.options.scales?.x) i.update(); });
    }, 450);
    setTimeout(() => body.classList.remove('is-shifting'), 1200);
}

function flip(el) { (el.closest('.panel-flipper') || el).classList.toggle('is-flipped'); }

const scroller = document.getElementById('scroller');
const fill = document.getElementById('fill');
const cubeContainer = document.getElementById('cube-nav-container');
let targetX = 0, currentX = 0;

function initNav() {
    cubeContainer.innerHTML = '';
    document.querySelectorAll('section').forEach((_, i) => {
        const c = document.createElement('div'); c.className = 'nav-cube';
        c.onclick = () => { targetX = i * window.innerWidth; };
        cubeContainer.appendChild(c);
    });
}
initNav();

window.addEventListener('wheel', (e) => {
    if(document.body.classList.contains('alt-dimension')) return;
    e.preventDefault();
    targetX = Math.max(0, Math.min(targetX + e.deltaY * 0.8, scroller.scrollWidth - window.innerWidth));
}, { passive: false });

function animate() {
    currentX += (targetX - currentX) * 0.05;
    scroller.scrollLeft = currentX;
    fill.style.width = ((currentX / (scroller.scrollWidth - window.innerWidth)) * 100) + '%';
    const activeIdx = Math.round(currentX / window.innerWidth);
    document.querySelectorAll('.nav-cube').forEach((c, i) => c.classList.toggle('active', i === activeIdx));
    requestAnimationFrame(animate);
}
animate();

const canvas = document.getElementById('web');
const ctx = canvas.getContext('2d');
let w, h, nodes = [];
function resize() { w = canvas.width = window.innerWidth; h = canvas.height = window.innerHeight; nodes = Array.from({length: 40}, () => ({ x: Math.random()*w, y: Math.random()*h, vx: Math.random()-0.5, vy: Math.random()-0.5 })); }
function draw() {
    ctx.clearRect(0, 0, w, h);
    nodes.forEach(n => {
        n.x += n.vx; n.y += n.vy;
        if(n.x<0 || n.x>w) n.vx*=-1; if(n.y<0 || n.y>h) n.vy*=-1;
        ctx.beginPath(); ctx.arc(n.x, n.y, 2, 0, Math.PI*2); 
        ctx.fillStyle = document.body.classList.contains('alt-dimension') ? 'rgba(26,93,26,0.2)' : 'rgba(50,62,187,0.2)'; 
        ctx.fill();
    });
    requestAnimationFrame(draw);
}
window.addEventListener('resize', resize); resize(); draw();
    

index.php

const scroller = document.getElementById('scroller');
const fill = document.getElementById('fill');
const navContainer = document.getElementById('nav-container');
const sections = document.querySelectorAll('section');
const btnExplore = document.getElementById('btn-explore');

let targetX = 0, currentX = 0;
const sensitivity = 0.7, ease = 0.045;

btnExplore.addEventListener('click', () => {
  document.body.classList.add('aspiration-active');
  setTimeout(() => {
    window.location.href = 'sante_dataviz.php';
  }, 1100);
});

sections.forEach((_, index) => {
  const cube = document.createElement('div');
  cube.classList.add('nav-cube');
  cube.addEventListener('click', () => scrollTowards(index));
  navContainer.appendChild(cube);
});
const cubes = document.querySelectorAll('.nav-cube');

window.addEventListener('wheel', (e) => {
  e.preventDefault();
  const delta = e.deltaY || e.deltaX;
  targetX += delta * sensitivity;
  targetX = Math.max(0, Math.min(targetX, scroller.scrollWidth - window.innerWidth));
}, { passive: false });

function animateScroll() {
  currentX += (targetX - currentX) * ease;
  scroller.scrollLeft = currentX;
  const maxScroll = scroller.scrollWidth - window.innerWidth;
  const progress = maxScroll > 0 ? (currentX / maxScroll) * 100 : 0;
  fill.style.width = progress + '%';
  const activeIndex = Math.round(currentX / window.innerWidth);
  cubes.forEach((cube, i) => {
    if (i === activeIndex) cube.classList.add('active');
    else cube.classList.remove('active');
  });
  requestAnimationFrame(animateScroll);
}
animateScroll();

function scrollTowards(index) { targetX = window.innerWidth * index; }

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) entry.target.classList.add('is-visible');
    else entry.target.classList.remove('is-visible');
  });
}, { threshold: 0.35 });
sections.forEach(s => observer.observe(s));

const canvas = document.getElementById('web');
const ctx = canvas.getContext('2d');
let w, h, nodes = [];
let mouse = { x: -1000, y: -1000 };

function resize() {
  w = canvas.width = window.innerWidth;
  h = canvas.height = window.innerHeight;
  nodes = [];
  const count = Math.floor((w * h) / 16000); 
  for (let i = 0; i < count; i++) {
    nodes.push({
      x: Math.random() * w, y: Math.random() * h,
      vx: (Math.random() - 0.5) * 0.3, vy: (Math.random() - 0.5) * 0.3, r: Math.random() * 2 + 1
    });
  }
}
window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
function draw() {
  ctx.clearRect(0, 0, w, h);
  nodes.forEach((n, i) => {
    n.x += n.vx; n.y += n.vy;
    if (n.x < 0 || n.x > w) n.vx *= -1; if (n.y < 0 || n.y > h) n.vy *= -1;
    const distM = Math.hypot(mouse.x - n.x, mouse.y - n.y);
    if (distM < 250) { n.x += (mouse.x - n.x) * 0.005; n.y += (mouse.y - n.y) * 0.005; }
    ctx.fillStyle = 'rgba(50, 62, 187, 0.4)';
    ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fill();
    for (let j = i + 1; j < nodes.length; j++) {
      const d = Math.hypot(n.x - nodes[j].x, n.y - nodes[j].y);
      if (d < 200) {
        ctx.strokeStyle = `rgba(97, 110, 226, ${(1 - d / 200) * 0.5})`;
        ctx.beginPath(); ctx.moveTo(n.x, n.y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.stroke();
      }
    }
  });
  requestAnimationFrame(draw);
}
window.addEventListener('resize', resize); resize(); draw();