import Graph from "graphology"; import Sigma from "sigma"; import forceAtlas2 from "graphology-layout-forceatlas2"; let individus: any[] = []; let graph: Graph; let sigma: Sigma; let running = false; document.getElementById("fileInput")!.addEventListener("change", async (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (!file) return; const text = await file.text(); individus = JSON.parse(text); }); document.getElementById("generateGraph")!.addEventListener("click", () => { if (!individus.length) { alert("Veuillez charger un fichier JSON d’individus d’abord !"); return; } graph = new Graph(); running = true; document.getElementById("stopSim")!.removeAttribute("disabled"); const container = document.getElementById("sigma-container")!; const width = container.clientWidth || 800; const height = container.clientHeight || 600; const cx = 0; //width / 2; const cy = 0; //height / 2; // rayon maximum (on garde une marge) const maxRadius = Math.min(width, height) * 0.45; // la spirale progresse doucement de l'intérieur vers l'extérieur const turns = 5; // nombre de tours de spirale const angleStep = (2 * Math.PI * turns) / individus.length; // ajouter les nœuds avec positions initiales sur un cercle individus.forEach((ind, i) => { const t = i / individus.length; // 0 → 1 const angle = i * angleStep; const radius = t * maxRadius; // petite variation aléatoire pour éviter une grille parfaite (optionnel) const jitter = 0.02 * radius; // coordonnees const x = cx + Math.cos(angle) * (radius + (Math.random() - 0.5) * jitter); const y = cy + Math.sin(angle) * (radius + (Math.random() - 0.5) * jitter); ind.edges = 0; graph.addNode(String(i), { label: `${ind.prenom} (${ind.age} ans)`, x, // position initiale X y, // position initiale Y size: 1, color: ind.sexe === "F" ? "#ff99aa" : "#6699ff", }); }); // maintenant on peut créer Sigma en lui passant le conteneur sigma = new Sigma(graph, container, { renderLabels: false }); // rafraîchir pour que Sigma prenne en compte les positions initiales sigma.refresh(); // Lancer l’animation animateLinks(); }); document.getElementById("stopSim")!.addEventListener("click", () => { running = false; (document.getElementById("stopSim") as HTMLButtonElement).disabled = true; }); function updateSizes() { graph.forEachNode((node, attrs) => { const degree = graph.degree(node); //const size = 2 + Math.sqrt(degree) * 2; const size = 5 * (1 - Math.exp(-degree / 8)); graph.setNodeAttribute(node, "size", size); }); sigma.refresh(); } async function animateLinks() { const N = individus.length; for (let k = 0 ; k < N * 10 ; k++) { if (!running) break; const i = Math.floor(Math.random() * N); const j = Math.floor(Math.random() * N); console.log(`${i} ? ${j}`); if (i === j) { console.log(`${i} == ${j}`); continue; } if (graph.hasEdge(String(i), String(j))) { console.log(`${i} hasEdge ${j}`); continue; } const a = individus[i]; const b = individus[j]; // Homophilie const diffSexe = +(a.sexe == b.sexe); const diffAge = Math.abs(a.age - b.age) / 60; const diffLecture = Math.abs(a.lecture - b.lecture); const diffMusique = Math.abs(a.musique - b.musique); const diffSport = (1 - Math.abs(a.sport - b.sport)) * Math.pow((a.sport + b.sport) / 2, 2); const diffEtudes = Math.abs(a.etudes - b.etudes) / 3; const diffRichesse = Math.abs(a.richesse - b.richesse) / 3; const similitude = 1 - (diffSexe * 2 + diffAge * 2 + diffLecture + diffMusique + diffSport + diffEtudes + diffRichesse) / 9; // Attachement préférentiel const degreeA = graph.degree(String(i)) + 1; const degreeB = graph.degree(String(j)) + 1; const pref = (degreeA + degreeB) / (2 * individus.length); // Fermeture triadique const neighborsA = new Set(graph.neighbors(String(i))); const neighborsB = new Set(graph.neighbors(String(j))); const common = [...neighborsA].filter((n) => neighborsB.has(n)).length; const triadic = Math.min(common / 3, 0.5); // Probabilité globale const p = 0.15 * similitude + 0.45 * pref + 0.4 * triadic; const r = Math.random(); console.log(`${similitude} ${pref} ${triadic} ${p} (>${r})`); if (r < p) { individus[i].edges++; individus[j].edges++; graph.addEdge(String(i), String(j)); updateSizes(); } // ForceAtlas2 s’exécute par étapes if (graph.order % 10 === 0) { forceAtlas2.assign(graph, { iterations: 20, settings: { gravity: 0.1 } }); sigma.refresh(); await delay(40); // petit délai entre lots } } // Dernière stabilisation forceAtlas2.assign(graph, { iterations: 150, settings: { gravity: 0.1 } }); sigma.refresh(); } function delay(ms: number) { return new Promise((res) => setTimeout(res, ms)); }