From d3b100192c00fa56ca605299fef5b689c0372194 Mon Sep 17 00:00:00 2001 From: medina5 Date: Sun, 19 Oct 2025 09:08:49 +0200 Subject: [PATCH] Graphe 2e version --- index.html | 34 +++++++---- individus.html | 27 ++++++--- src/individus.ts | 2 +- src/main.ts | 149 +++++++-------------------------------------- src/network.ts | 154 +++++++++++++++++++++++++++++++++++++++++++++++ src/network0.ts | 134 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 148 deletions(-) create mode 100644 src/network.ts create mode 100644 src/network0.ts diff --git a/index.html b/index.html index aea8fbe..d5fc023 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,23 @@ - - - - - - - Graphe Mes Amis - - -
- - + + + + + Simulation de graphe d'amis + + + + +

Simulation de réseau d’amitiés

+ + + +
+ diff --git a/individus.html b/individus.html index df36a3b..360e50f 100644 --- a/individus.html +++ b/individus.html @@ -1,10 +1,13 @@ - - - Distribution des âges - - + + + + Individus + + + +

Distribution des âges

Indicateurs clés

@@ -33,6 +36,16 @@
- - +
+ + +
+
+ + + diff --git a/src/individus.ts b/src/individus.ts index ff4e432..e26bef8 100644 --- a/src/individus.ts +++ b/src/individus.ts @@ -5,7 +5,7 @@ import ChartDataLabels from "chartjs-plugin-datalabels"; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; import { jStat } from "jstat"; -const individus = generate(1000); +export const individus = generate(150); const individualMale = individus.filter(i => i.sexe === "M"); const individualFemale = individus.filter(i => i.sexe === "F"); diff --git a/src/main.ts b/src/main.ts index 869ef9b..82348f5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,134 +1,27 @@ -import './style.css'; -import Graph from "graphology"; -import Sigma from "sigma"; -import forceAtlas2 from "graphology-layout-forceatlas2"; +import { individus } from "./individus.ts"; -// --- Génération de données de base --- -const graph = new Graph(); - -const N = 30; -const colors = ["#ec635e", "#61afef", "#2c3029ff", "#e5c07b"]; - -for (let i = 0; i < N; i++) { - const sex = Math.random() < 0.5 ? "F" : "M"; - const education = Math.floor(Math.random() * 4); - const color = colors[education]; - - graph.addNode(`n${i}`, { - x: Math.random(), y: Math.random(), - label: `${sex} ${i}`, - sex, - education, - size: 6 + education, - color, - }); +function download(filename: string, content:any, type:string) { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); } -let total = N * 1.5 - - -// --- Calcul du layout ForceAtlas2 --- -/* -const positions = forceAtlas2(graph, { iterations: 50 }); - -// --- Application des positions calculées --- -for (const [node, pos] of Object.entries(positions)) { - graph.setNodeAttribute(node, "x", pos.x); - graph.setNodeAttribute(node, "y", pos.y); -} -*/ -// --- Rendu Sigma --- -const container = document.getElementById("app"); -const renderer = new Sigma(graph, container, { renderLabels: true }); - -// --- Fonctions dynamiques --- -function addLink() { - const a = `n${Math.floor(Math.random() * N)}`; - const b = `n${Math.floor(Math.random() * N)}`; - if (a !== b && !graph.hasEdge(a, b)) { - graph.addEdge(a, b, { color: "#da1515ff", size: 1 }); - } - if (total-- > 0) setTimeout(() => addLink(), 1000); else running = false; -} - -function removeLink(source, target) { - if (graph.hasEdge(source, target)) { - graph.dropEdge(source, target); - renderer.refresh(); - } -} - -// --- Exemple d’évolution dynamique --- -setTimeout(() => addLink(), 1000); -setTimeout(() => removeLink("n1", "n2"), 8000); - -// --- Animation du layout --- -// On crée une "simulation" ForceAtlas2 en incrémentant les positions à chaque frame. -let running = true; - -function stepLayout() { - if (!running) return; - - // Effectue une itération de ForceAtlas2 (ne recrée pas tout) - forceAtlas2.assign(graph, { iterations: 1, settings: { gravity: 0.1, scalingRatio: 10 } }); - - // Sigma détecte les changements automatiquement → inutile de refresh manuellement - requestAnimationFrame(stepLayout); -} - -// Lancement -stepLayout(); - - -/* -const layout = new ForceAtlas2Layout(graph, { - settings: { - gravity: 0.1, - slowDown: 10, - linLogMode: false, - outboundAttractionDistribution: false, - adjustSizes: true, - }, +document.getElementById("download-json")!.addEventListener("click", () => { + console.log(individus); + const json = JSON.stringify(individus, null, 2); + download("individus.json", json, "application/json"); }); -// --- Animation : on démarre le layout --- -layout.start(); +document.getElementById("download-csv")!.addEventListener("click", () => { + const headers = Object.keys(individus[0]); + const csv = [ + headers.join(";"), + ...individus.map(i => headers.map(h => i[h]).join(";")) + ].join("\n"); -// --- Optionnel : arrêt automatique après quelques secondes --- -setTimeout(() => { - layout.stop(); - console.log("Layout stabilisé"); -}, 5000); - -// --- Animation continue du rendu --- -function animate() { - // On redessine continuellement le graphe tant que le layout tourne - renderer.refresh(); - requestAnimationFrame(animate); -} -animate(); -*/ -//import typescriptLogo from './typescript.svg' -//import viteLogo from '/vite.svg' -//import { setupCounter } from './counter.ts' -/* -document.querySelector('#app')!.innerHTML = ` -
- - - - - - -

Vite + TypeScript

-
- -
-

- Click on the Vite and TypeScript logos to learn more -

-
-` - -setupCounter(document.querySelector('#counter')!) -*/ + download("individus.csv", csv, "text/csv"); +}); diff --git a/src/network.ts b/src/network.ts new file mode 100644 index 0000000..dc495c0 --- /dev/null +++ b/src/network.ts @@ -0,0 +1,154 @@ +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); + + graph.addNode(String(i), { + label: `${ind.prenom} (${ind.age} ans)`, + x, // position initiale X + y, // position initiale Y + size: 2, + color: ind.sexe === "F" ? "#ff99aa" : "#6699ff", + age: ind.age, + sexe: ind.sexe, + richesse: ind.richesse, + etudes: ind.etudes, + lecture: ind.lecture, + musique: ind.musique, + sport: ind.sport, + }); + }); + + // maintenant on peut créer Sigma en lui passant le conteneur + sigma = new Sigma(graph, container, { renderLabels: true }); + // 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 pairs: [number, number][] = []; + + // Liste de toutes les paires (i,j) + for (let i = 0; i < individus.length; i++) { + for (let j = i + 1; j < individus.length; j++) { + pairs.push([i, j]); + } + } + + // Mélanger un peu les paires pour éviter les patterns trop linéaires + pairs.sort(() => Math.random() - 0.5); + + for (const [i, j] of pairs) { + if (!running) break; + + const a = individus[i]; + const b = individus[j]; + + // Homophilie + 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 = Math.abs(a.sport - b.sport); + const similitude = 1 - (diffAge * 2 + diffLecture + diffMusique + diffSport) / 5; + + // 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; + + if (Math.random() < p) { + 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)); +} diff --git a/src/network0.ts b/src/network0.ts new file mode 100644 index 0000000..5906f37 --- /dev/null +++ b/src/network0.ts @@ -0,0 +1,134 @@ +import './style.css'; +import Graph from "graphology"; +import Sigma from "sigma"; +import forceAtlas2 from "graphology-layout-forceatlas2"; + +// --- Génération de données de base --- +const graph = new Graph(); + +const N = 30; +const colors = ["#ec635e", "#61afef", "#2c3029ff", "#e5c07b"]; + +for (let i = 0; i < N; i++) { + const sex = Math.random() < 0.5 ? "F" : "M"; + const education = Math.floor(Math.random() * 4); + const color = colors[education]; + + graph.addNode(`n${i}`, { + x: Math.random(), y: Math.random(), + label: `${sex} ${i}`, + sex, + education, + size: 6 + education, + color, + }); +} + +let total = N * 1.5 + + +// --- Calcul du layout ForceAtlas2 --- +/* +const positions = forceAtlas2(graph, { iterations: 50 }); + +// --- Application des positions calculées --- +for (const [node, pos] of Object.entries(positions)) { + graph.setNodeAttribute(node, "x", pos.x); + graph.setNodeAttribute(node, "y", pos.y); +} +*/ +// --- Rendu Sigma --- +const container = document.getElementById("app"); +const renderer = new Sigma(graph, container, { renderLabels: true }); + +// --- Fonctions dynamiques --- +function addLink() { + const a = `n${Math.floor(Math.random() * N)}`; + const b = `n${Math.floor(Math.random() * N)}`; + if (a !== b && !graph.hasEdge(a, b)) { + graph.addEdge(a, b, { color: "#da1515ff", size: 1 }); + } + if (total-- > 0) setTimeout(() => addLink(), 1000); else running = false; +} + +function removeLink(source, target) { + if (graph.hasEdge(source, target)) { + graph.dropEdge(source, target); + renderer.refresh(); + } +} + +// --- Exemple d’évolution dynamique --- +setTimeout(() => addLink(), 250); +setTimeout(() => removeLink("n1", "n2"), 8000); + +// --- Animation du layout --- +// On crée une "simulation" ForceAtlas2 en incrémentant les positions à chaque frame. +let running = true; + +function stepLayout() { + if (!running) return; + + // Effectue une itération de ForceAtlas2 (ne recrée pas tout) + forceAtlas2.assign(graph, { iterations: 1, settings: { gravity: 0.1, scalingRatio: 10 } }); + + // Sigma détecte les changements automatiquement → inutile de refresh manuellement + requestAnimationFrame(stepLayout); +} + +// Lancement +stepLayout(); + + +/* +const layout = new ForceAtlas2Layout(graph, { + settings: { + gravity: 0.1, + slowDown: 10, + linLogMode: false, + outboundAttractionDistribution: false, + adjustSizes: true, + }, +}); + +// --- Animation : on démarre le layout --- +layout.start(); + +// --- Optionnel : arrêt automatique après quelques secondes --- +setTimeout(() => { + layout.stop(); + console.log("Layout stabilisé"); +}, 5000); + +// --- Animation continue du rendu --- +function animate() { + // On redessine continuellement le graphe tant que le layout tourne + renderer.refresh(); + requestAnimationFrame(animate); +} +animate(); +*/ +//import typescriptLogo from './typescript.svg' +//import viteLogo from '/vite.svg' +//import { setupCounter } from './counter.ts' +/* +document.querySelector('#app')!.innerHTML = ` +
+ + + + + + +

Vite + TypeScript

+
+ +
+

+ Click on the Vite and TypeScript logos to learn more +

+
+` + +setupCounter(document.querySelector('#counter')!) +*/