Graphe 2e version
This commit is contained in:
32
index.html
32
index.html
@@ -1,13 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" style="width: 100vw; height: 100vh; background: white"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<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>
|
||||
<h1>Simulation de réseau d’amitié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>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Distribution des âges</title>
|
||||
</head>
|
||||
<body style="max-width: 700px; margin: 2rem auto; font-family: sans-serif;">
|
||||
<title>Individus</title>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
<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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
149
src/main.ts
149
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<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
154
src/network.ts
Normal 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 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));
|
||||
}
|
||||
134
src/network0.ts
Normal file
134
src/network0.ts
Normal 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')!)
|
||||
*/
|
||||
Reference in New Issue
Block a user