doc2gl / visual_aggregate.html
Doc2GL Deploy
Deploy Doc2GL to HuggingFace Space
eaa2438
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue Agrégée - Doc2GL</title>
<script src="https://unpkg.com/vis-network@9.1.0/dist/vis-network.min.js"></script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #f5f5f5;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#container {
width: 100vw;
height: 100vh;
background: white;
position: relative;
}
#controls {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
z-index: 1000;
width: 320px;
max-height: 90vh;
overflow-y: auto;
transition: transform 0.3s ease;
}
#controls.closed {
transform: translateX(calc(100% + 40px));
}
#controls h3 {
margin: 0 0 20px 0;
color: #667eea;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
/* Bouton de fermeture */
#toggleControls {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 5px 8px;
width: auto;
margin: 0;
color: #667eea;
transition: transform 0.2s ease;
border-radius: 4px;
}
#toggleControls:hover {
transform: scale(1.2);
background: rgba(102, 126, 234, 0.1);
box-shadow: none;
}
/* Bouton flottant pour rouvrir */
#reopenButton {
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
z-index: 999;
display: none;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
#reopenButton:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
}
#reopenButton.visible {
display: flex;
}
.control-group {
margin-bottom: 20px;
}
.control-group label {
display: block;
font-size: 13px;
color: #555;
margin-bottom: 8px;
font-weight: 600;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
input[type="range"] {
flex: 1;
height: 6px;
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
#thresholdSlider {
background: linear-gradient(to right, #e74c3c 0%, #f39c12 25%, #3498db 50%, #27ae60 75%, #27ae60 100%);
}
#fuzzySlider, #semanticSlider {
background: linear-gradient(to right, #e74c3c 0%, #f39c12 50%, #27ae60 100%);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
background: #5568d3;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
transition: all 0.2s ease;
}
input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.2);
background: #5568d3;
}
.value-display {
min-width: 60px;
text-align: right;
font-weight: bold;
color: #667eea;
font-size: 14px;
}
#legend {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
#legend h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: #555;
font-weight: 600;
}
.legend-item {
display: flex;
align-items: center;
margin: 8px 0;
font-size: 12px;
}
.legend-color {
width: 18px;
height: 18px;
border-radius: 50%;
margin-right: 10px;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
#info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 15px 20px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
z-index: 1000;
font-size: 13px;
}
#info p {
margin: 5px 0;
color: #555;
}
#info strong {
color: #333;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
color: #667eea;
z-index: 999;
}
#error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 12px;
padding: 30px;
text-align: center;
color: #721c24;
display: none;
max-width: 450px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}
.stats {
display: flex;
gap: 15px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.stat-item {
flex: 1;
text-align: center;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
}
.stat-value {
font-size: 22px;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 11px;
color: #888;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.help-text {
font-size: 11px;
color: #888;
margin-top: 4px;
font-style: italic;
}
button {
width: 100%;
padding: 12px;
margin-top: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
</style>
</head>
<body>
<div id="loading">⏳ Chargement...</div>
<div id="error"></div>
<div id="container"></div>
<!-- Bouton flottant pour rouvrir le panneau -->
<button id="reopenButton" onclick="toggleControlPanel()" title="Ouvrir les contrôles">
⚙️
</button>
<!-- Panneau de contrôle -->
<div id="controls">
<button id="toggleControls" onclick="toggleControlPanel()" title="Fermer le panneau"></button>
<h3>
<span>🎛️</span>
<span>Contrôles</span>
</h3>
<!-- Seuil de fréquence -->
<div class="control-group">
<label for="thresholdSlider">🎯 Seuil de fréquence</label>
<div class="slider-container">
<input type="range" id="thresholdSlider" min="0" max="100" value="50" step="5">
<span class="value-display" id="thresholdValue">50%</span>
</div>
<div class="help-text">Masque les nœuds/arêtes en dessous du seuil</div>
</div>
<!-- Seuil Fuzzy -->
<div class="control-group">
<label for="fuzzySlider">🔤 Seuil Fuzzy (clustering)</label>
<div class="slider-container">
<input type="range" id="fuzzySlider" min="50" max="100" value="70" step="5">
<span class="value-display" id="fuzzyValue">70%</span>
</div>
<div class="help-text">Tolérance orthographique (70 = flexible)</div>
</div>
<!-- Seuil Sémantique -->
<div class="control-group">
<label for="semanticSlider">🧠 Seuil Sémantique (clustering)</label>
<div class="slider-container">
<input type="range" id="semanticSlider" min="50" max="100" value="70" step="5">
<span class="value-display" id="semanticValue">70%</span>
</div>
<div class="help-text">Tolérance de sens (70 = flexible)</div>
</div>
<!-- Bouton d'application -->
<button onclick="recomputeAggregation()">🔄 Recalculer l'agrégation</button>
<!-- Statistiques -->
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="visibleNodes">-</div>
<div class="stat-label">Nœuds</div>
</div>
<div class="stat-item">
<div class="stat-value" id="visibleEdges">-</div>
<div class="stat-label">Arêtes</div>
</div>
</div>
<!-- Légende des couleurs -->
<div id="legend">
<h4>🎨 Code couleur (fréquence)</h4>
<div class="legend-item">
<div class="legend-color" style="background: #27ae60;"></div>
<span><strong>≥75%</strong> - Universels</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #3498db;"></div>
<span><strong>50-74%</strong> - Fréquents</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f39c12;"></div>
<span><strong>25-49%</strong> - Moyens</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #e74c3c;"></div>
<span><strong>&lt;25%</strong> - Rares</span>
</div>
</div>
</div>
<!-- Informations en bas -->
<div id="info">
<p><strong>Documents:</strong> <span id="doc-count">-</span></p>
<p><strong>Clustering:</strong> <span id="clustering-info">-</span></p>
</div>
<script>
console.log("🚀 Script démarré");
// Variables globales
let allNodes = [];
let allEdges = [];
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let metadata = {};
// ═════════════════════════════════════════════════════════════
// FONCTION: Toggle du panneau de contrôle
// ═════════════════════════════════════════════════════════════
function toggleControlPanel() {
const controls = document.getElementById('controls');
const reopenButton = document.getElementById('reopenButton');
const isClosing = !controls.classList.contains('closed');
if (isClosing) {
// Fermer le panneau
controls.classList.add('closed');
reopenButton.classList.add('visible');
console.log("📦 Panneau de contrôle fermé");
} else {
// Ouvrir le panneau
controls.classList.remove('closed');
reopenButton.classList.remove('visible');
console.log("📦 Panneau de contrôle ouvert");
}
}
// ═════════════════════════════════════════════════════════════
// RACCOURCI CLAVIER: Touche 'C' pour toggle
// ═════════════════════════════════════════════════════════════
document.addEventListener('keydown', (e) => {
if (e.key === 'c' || e.key === 'C') {
toggleControlPanel();
}
});
// ═════════════════════════════════════════════════════════════
// FONCTION: Obtenir la couleur selon la fréquence
// ═════════════════════════════════════════════════════════════
function getNodeColor(freq) {
if (freq >= 75) return { background: '#27ae60', border: '#229954' };
if (freq >= 50) return { background: '#3498db', border: '#2980b9' };
if (freq >= 25) return { background: '#f39c12', border: '#e67e22' };
return { background: '#e74c3c', border: '#c0392b' };
}
// ═════════════════════════════════════════════════════════════
// FONCTION: Filtrer et mettre à jour le graphe
// ═════════════════════════════════════════════════════════════
function updateGraph(threshold) {
console.log(`🔄 Filtrage dynamique avec seuil: ${threshold}%`);
const filteredNodes = allNodes.filter(n => n.freq >= threshold);
const filteredNodeIds = new Set(filteredNodes.map(n => n.id));
const filteredEdges = allEdges.filter(e =>
e.freq >= threshold &&
filteredNodeIds.has(e.source) &&
filteredNodeIds.has(e.target)
);
console.log(`📊 Nœuds affichés: ${filteredNodes.length} (≥${threshold}%)`);
console.log(`📊 Arêtes affichées: ${filteredEdges.length} (≥${threshold}%)`);
const edgeMap = new Map();
filteredEdges.forEach(e => {
const key = `${e.source}${e.target}`;
if (edgeMap.has(key)) {
const existing = edgeMap.get(key);
if (e.freq > existing.freq) {
edgeMap.set(key, e);
}
} else {
edgeMap.set(key, e);
}
});
const uniqueEdges = Array.from(edgeMap.values());
const visNodes = filteredNodes.map(n => {
const colors = getNodeColor(n.freq);
return {
id: n.id,
label: n.label,
title: `${n.label}\nFréquence: ${n.freq}%`,
value: n.freq,
color: colors,
font: {
size: Math.max(16, 18 + n.freq / 8),
face: "Segoe UI, Arial",
color: "#2c3e50",
bold: { face: "Segoe UI, Arial", mod: "bold" },
strokeWidth: 0,
strokeColor: "transparent"
},
margin: 12
};
});
const visEdges = uniqueEdges.map(e => {
const width = Math.max(2, e.freq / 12);
return {
from: e.source,
to: e.target,
label: `${e.freq}%`,
font: {
size: 13,
align: "top",
color: "#34495e",
background: "rgba(255, 255, 255, 0.92)",
strokeWidth: 0,
bold: true
},
width: width,
color: {
color: "#4a5568",
highlight: "#667eea",
hover: "#667eea",
opacity: 0.85
},
arrows: {
to: {
enabled: true,
scaleFactor: 0.9,
type: "arrow"
}
},
smooth: {
enabled: true,
type: "cubicBezier",
forceDirection: "vertical",
roundness: 0.5
},
shadow: {
enabled: true,
color: "rgba(0,0,0,0.12)",
size: 5,
x: 2,
y: 2
}
};
});
nodesDataset.clear();
edgesDataset.clear();
nodesDataset.add(visNodes);
edgesDataset.add(visEdges);
document.getElementById('visibleNodes').textContent = filteredNodes.length;
document.getElementById('visibleEdges').textContent = uniqueEdges.length;
if (filteredNodes.length === 0) {
document.getElementById('error').innerHTML =
`<h3>⚠️ Graphe vide</h3>
<p>Aucun nœud au-dessus de ${threshold}%</p>
<p style="font-size:13px; color:#95a5a6;">→ Diminuez le seuil avec le curseur</p>`;
document.getElementById('error').style.display = 'block';
} else {
document.getElementById('error').style.display = 'none';
}
}
// ═════════════════════════════════════════════════════════════
// FONCTION: Recalculer l'agrégation avec nouveaux seuils
// ═════════════════════════════════════════════════════════════
function recomputeAggregation() {
const fuzzy = document.getElementById('fuzzySlider').value;
const semantic = document.getElementById('semanticSlider').value;
const threshold = document.getElementById('thresholdSlider').value;
console.log(`🔄 Recalcul avec fuzzy=${fuzzy}%, semantic=${semantic}%, threshold=${threshold}%`);
alert(`🔄 Recalcul demandé !\n\nPour recalculer l'agrégation avec:\n• Fuzzy: ${fuzzy}%\n• Sémantique: ${semantic}%\n• Seuil: ${threshold}%\n\n→ Cliquez à nouveau sur "Agréger les mindmaps" dans l'interface Gradio`);
}
// ═════════════════════════════════════════════════════════════
// MISE À JOUR DES VALEURS AFFICHÉES
// ═════════════════════════════════════════════════════════════
document.getElementById('thresholdSlider').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('thresholdValue').textContent = `${value}%`;
updateGraph(value);
});
document.getElementById('fuzzySlider').addEventListener('input', (e) => {
document.getElementById('fuzzyValue').textContent = `${e.target.value}%`;
});
document.getElementById('semanticSlider').addEventListener('input', (e) => {
document.getElementById('semanticValue').textContent = `${e.target.value}%`;
});
// ═════════════════════════════════════════════════════════════
// CHARGEMENT DES DONNÉES
// ═════════════════════════════════════════════════════════════
const jsonPath = "./aggregate.json";
fetch(jsonPath)
.then(response => {
console.log("📡 Status:", response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(graph => {
console.log("✅ JSON chargé:", graph);
document.getElementById("loading").style.display = "none";
metadata = graph.metadata || {};
allNodes = graph.nodes || [];
allEdges = graph.edges || [];
console.log(`📊 ${allNodes.length} nœuds, ${allEdges.length} arêtes`);
document.getElementById("doc-count").textContent = metadata.doc_count || "?";
const clusteringInfo = metadata.clustering_enabled
? `Hybride (F:${metadata.fuzzy_threshold}% + S:${metadata.semantic_threshold}%)`
: "Désactivé";
document.getElementById("clustering-info").textContent = clusteringInfo;
if (metadata.fuzzy_threshold) {
document.getElementById('fuzzySlider').value = metadata.fuzzy_threshold;
document.getElementById('fuzzyValue').textContent = `${metadata.fuzzy_threshold}%`;
}
if (metadata.semantic_threshold) {
document.getElementById('semanticSlider').value = metadata.semantic_threshold;
document.getElementById('semanticValue').textContent = `${metadata.semantic_threshold}%`;
}
if (allNodes.length === 0) {
document.getElementById("error").innerHTML =
"<h3>⚠️ Aucune donnée</h3><p>Le fichier aggregate.json est vide</p>";
document.getElementById("error").style.display = "block";
return;
}
nodesDataset = new vis.DataSet();
edgesDataset = new vis.DataSet();
const container = document.getElementById("container");
const data = { nodes: nodesDataset, edges: edgesDataset };
const options = {
nodes: {
shape: 'dot',
scaling: {
min: 20,
max: 70,
label: { enabled: true, min: 12, max: 20 }
},
shadow: {
enabled: true,
color: 'rgba(0,0,0,0.2)',
size: 10,
x: 2,
y: 2
},
borderWidth: 3,
borderWidthSelected: 5
},
edges: {
smooth: {
type: "continuous",
roundness: 0.2
},
shadow: false
},
physics: {
enabled: true,
stabilization: {
enabled: true,
iterations: 1000,
updateInterval: 25
},
barnesHut: {
gravitationalConstant: -8000,
springLength: 200,
springConstant: 0.001,
damping: 0.5,
avoidOverlap: 1
}
},
layout: {
improvedLayout: true,
hierarchical: false
},
interaction: {
hover: true,
tooltipDelay: 100,
zoomView: true,
dragView: true,
dragNodes: true,
navigationButtons: false
}
};
network = new vis.Network(container, data, options);
network.once('stabilizationIterationsDone', function() {
console.log("✅ Graphe stabilisé");
network.setOptions({ physics: false });
});
const initialThreshold = metadata.threshold || 50;
document.getElementById('thresholdSlider').value = initialThreshold;
document.getElementById('thresholdValue').textContent = `${initialThreshold}%`;
updateGraph(initialThreshold);
console.log("✅ Visualisation créée avec succès");
})
.catch(err => {
console.error("❌ ERREUR:", err);
document.getElementById("loading").style.display = "none";
document.getElementById("error").innerHTML =
`<h3>❌ Erreur de chargement</h3>
<p>${err.message}</p>
<p style="font-size:11px;">Fichier: ${jsonPath}</p>
<p style="font-size:11px;">Vérifiez que le serveur HTTP est lancé</p>`;
document.getElementById("error").style.display = "block";
});
</script>
</body>
</html>