2025-10-19 09:08:49 +02:00
|
|
|
|
import Graph from "graphology";
|
|
|
|
|
|
import Sigma from "sigma";
|
|
|
|
|
|
import forceAtlas2 from "graphology-layout-forceatlas2";
|
|
|
|
|
|
|
2025-10-22 17:48:22 +02:00
|
|
|
|
const p_pref = 0.45; // 0.45
|
|
|
|
|
|
const p_triadic = 0.45; // 0.40
|
|
|
|
|
|
const p_similitude = 1 - p_pref - p_triadic;
|
|
|
|
|
|
|
2025-10-19 09:08:49 +02:00
|
|
|
|
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);
|
2025-10-19 21:32:16 +02:00
|
|
|
|
ind.edges = 0;
|
2025-10-19 09:08:49 +02:00
|
|
|
|
|
|
|
|
|
|
graph.addNode(String(i), {
|
|
|
|
|
|
label: `${ind.prenom} (${ind.age} ans)`,
|
|
|
|
|
|
x, // position initiale X
|
|
|
|
|
|
y, // position initiale Y
|
2025-10-19 21:32:16 +02:00
|
|
|
|
size: 1,
|
2025-10-19 09:08:49 +02:00
|
|
|
|
color: ind.sexe === "F" ? "#ff99aa" : "#6699ff",
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// maintenant on peut créer Sigma en lui passant le conteneur
|
2025-10-19 21:32:16 +02:00
|
|
|
|
sigma = new Sigma(graph, container, { renderLabels: false });
|
2025-10-19 09:08:49 +02:00
|
|
|
|
// 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() {
|
|
|
|
|
|
|
2025-10-19 21:32:16 +02:00
|
|
|
|
const N = individus.length;
|
2025-10-19 09:08:49 +02:00
|
|
|
|
|
2025-10-22 17:48:22 +02:00
|
|
|
|
for (let k = 0 ; k < N * 15 ; k++) {
|
2025-10-19 09:08:49 +02:00
|
|
|
|
if (!running) break;
|
|
|
|
|
|
|
2025-10-19 21:32:16 +02:00
|
|
|
|
const i = Math.floor(Math.random() * N);
|
|
|
|
|
|
const j = Math.floor(Math.random() * N);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`${i} ? ${j}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (i === j) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (graph.hasEdge(String(i), String(j))) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-19 09:08:49 +02:00
|
|
|
|
const a = individus[i];
|
|
|
|
|
|
const b = individus[j];
|
|
|
|
|
|
|
|
|
|
|
|
// Homophilie
|
2025-10-19 21:32:16 +02:00
|
|
|
|
const diffSexe = +(a.sexe == b.sexe);
|
2025-10-19 09:08:49 +02:00
|
|
|
|
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);
|
2025-10-19 21:32:16 +02:00
|
|
|
|
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;
|
2025-10-22 17:48:22 +02:00
|
|
|
|
const similitude = 1 - (diffSexe * 2 + diffAge * 2 + diffLecture + diffMusique + diffSport * 4 + diffEtudes + diffRichesse) / 12;
|
2025-10-19 09:08:49 +02:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-10-22 17:48:22 +02:00
|
|
|
|
const p = p_similitude * similitude + p_pref * pref + p_triadic * triadic;
|
2025-10-19 21:32:16 +02:00
|
|
|
|
const r = Math.random();
|
|
|
|
|
|
|
2025-10-22 17:48:22 +02:00
|
|
|
|
//console.log(`${similitude} ${pref} ${triadic} ${p} (>${r})`);
|
2025-10-19 09:08:49 +02:00
|
|
|
|
|
2025-10-19 21:32:16 +02:00
|
|
|
|
if (r < p) {
|
|
|
|
|
|
individus[i].edges++;
|
|
|
|
|
|
individus[j].edges++;
|
2025-10-19 09:08:49 +02:00
|
|
|
|
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));
|
|
|
|
|
|
}
|