Files
graph-mesamis/src/network.ts
2025-10-22 17:48:22 +02:00

166 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Graph from "graphology";
import Sigma from "sigma";
import forceAtlas2 from "graphology-layout-forceatlas2";
const p_pref = 0.45; // 0.45
const p_triadic = 0.45; // 0.40
const p_similitude = 1 - p_pref - p_triadic;
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 dindividus dabord !");
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 lanimation
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 * 15 ; 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) {
continue;
}
if (graph.hasEdge(String(i), String(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 * 4 + diffEtudes + diffRichesse) / 12;
// 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 = p_similitude * similitude + p_pref * pref + p_triadic * 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 sexé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));
}