Graphe 2e version

This commit is contained in:
2025-10-19 09:08:49 +02:00
parent 27a2e99706
commit d3b100192c
6 changed files with 352 additions and 148 deletions

View File

@@ -1,13 +1,23 @@
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Graphe Mes Amis</title>
<title>Simulation de graphe d'amis</title>
<script type="module" src="src/network.ts"></script>
<style>
body { font-family: sans-serif; }
#sigma-container { width: 100%; height: 400px; border: 1px solid #ccc; margin-top: 1rem; }
#graph-container canvas {
display: block;
margin: 0 auto; /* si vous voulez le centrer sans perturber Sigma */
}
</style>
</head>
<body>
<div id="app" style="width: 100vw; height: 100vh; background: white"></div>
<script type="module" src="/src/main.ts"></script>
<h1>Simulation de réseau damitiés</h1>
<input type="file" id="fileInput" accept="application/json" />
<button id="generateGraph">Générer le graphe</button>
<button id="stopSim" disabled>⏸️ Stop</button>
<div id="sigma-container"></div>
</body>
</html>

View File

@@ -1,10 +1,13 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<title>Distribution des âges</title>
<title>Individus</title>
</head>
<body style="max-width: 700px; margin: 2rem auto; font-family: sans-serif;">
<body>
<div style="max-width: 700px; margin: 2rem auto; font-family: sans-serif;">
<h2>Distribution des âges</h2>
<section id="stats" style="margin-bottom: 2rem;">
<h3>Indicateurs clés</h3>
@@ -33,6 +36,16 @@
</div>
<div><canvas id="heatmapChart" width="700" height="200"></canvas>
<div><canvas id="radarChart" width="200" height="100"></canvas>
<script type="module" src="/src/individus.ts"></script>
<div style="text-align:center; margin-top:1rem;">
<button id="download-json" style="margin-right:1rem; padding:0.5rem 1rem;">
Télécharger JSON
</button>
<button id="download-csv" style="padding:0.5rem 1rem;">
Télécharger CSV
</button>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -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");

View File

@@ -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<HTMLDivElement>('#app')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
*/
download("individus.csv", csv, "text/csv");
});

154
src/network.ts Normal file
View File

@@ -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 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);
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 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 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 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));
}

134
src/network0.ts Normal file
View File

@@ -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<HTMLDivElement>('#app')!.innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://www.typescriptlang.org/" target="_blank">
<img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
</a>
<h1>Vite + TypeScript</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite and TypeScript logos to learn more
</p>
</div>
`
setupCounter(document.querySelector<HTMLButtonElement>('#counter')!)
*/