Quentin Lhoest
add app
f0806e2
/**
* Fonction principale pour créer et afficher le graphe Sigma.
* Appelée une seule fois lorsque l'utilisateur clique sur "Voir l'arbre".
* @param {object} graphData - Les données des nœuds et arêtes.
* @returns {Sigma} L'instance du renderer Sigma.
*/
function initializeSigmaGraph(graphData) {
const container = document.getElementById("sigma-container");
if (!container) {
console.error("Conteneur Sigma non trouvé. Initialisation annulée.");
return null;
}
console.log("Initialisation du graphe Sigma...");
const Graph = window.graphology.Graph;
const graph = new Graph();
const TYPE_COLORS = {
personne: '#007bff',
organisation: '#007',
Modèle: '#b1b1b1',
Dataset: '#ffc107',
Author: '#007bff',
Model: '#b1b1b1',
Unknown: '#999',
faut: '#999'
};
const addedNodeIds = new Set();
graphData.nodes.forEach(node => {
if (addedNodeIds.has(node.id)) return;
const rawSize = Math.log10(node.downloads || node.followers || 1) * 2 + (depth === 0 ? 5 : 0);
graph.addNode(node.id, {
id: node.id, label: node.id, size: Math.max(5, rawSize), color: TYPE_COLORS[node.label] || '#999',
dataCat: node.label, x: Math.random(), y: Math.random(),
followers: node.followers, downloads: node.downloads, createdAt: node.createdAt,
createdAt_dataset: node.createdAt_dataset, task: node.task, bfsDepth: node.distance
});
addedNodeIds.add(node.id);
});
graphData.edges.forEach(edge => {
console.log(edge.relation);
const edgeColor = edgeInfos[edge.relation]["color"] || "#ccc";
if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
graph.addEdge(edge.source, edge.target, {
label: edge.relation || '',
color: edgeColor || '#ccc',
size: 3,
type: 'arrow'
});
} else {
console.warn(`Arête ignorée: ${edge.source} -> ${edge.target} (nœuds manquants)`);
}
});
graphologyLibrary.layoutForceAtlas2.assign(graph, {
iterations: 100,
settings: {
gravity: 0.01, // attraction faible vers le centre
scalingRatio: 10, // augmente fortement la répulsion
slowDown: 1, // vitesse normale
strongGravityMode: false,
barnesHutOptimize: true,
barnesHutTheta: 0.5 // précision de l’approximation
}
});
const renderer = new Sigma(graph, container);
// Juste après l'initialisation du renderer
const state = {
hoveredNode: null,
hoveredNeighbors: null
};
renderer.setSetting("nodeReducer", (node, data) => {
const res = { ...data };
if (state.hoveredNode && state.hoveredPredecessors) {
// Garder seulement le nœud survolé et ses prédécesseurs visibles en couleur
if (!state.hoveredPredecessors.has(node)) {
res.color = "#f1f1f1"; // gris
res.label = ""; // masquer le label
}
}
return res;
});
renderer.setSetting("edgeReducer", (edge, data) => {
const res = { ...data };
if (state.hoveredNode && state.hoveredPredecessors) {
const [source, target] = graph.extremities(edge);
// Montrer seulement les arêtes dans le chemin des prédécesseurs
if (!state.hoveredPredecessors.has(source) || !state.hoveredPredecessors.has(target)) {
res.hidden = true;
}
}
return res;
});
buildEdgeLegend();
// Mettre en place les interactions
const filtersContainer = document.getElementById('graph-filters-container');
if (filtersContainer) {
filtersContainer.addEventListener('change', (event) => {
if (event.target.type === 'checkbox') {
updateGraphVisibility(graph, renderer);
}
});
}
renderer.on('clickNode', ({ node }) => {
showNodeInfo(graph.getNodeAttributes(node));
});
// Quand on survole un nœud
renderer.on("enterNode", ({ node }) => {
state.hoveredNode = node;
state.hoveredPredecessors = getAllPredecessors(graph, node);
renderer.refresh({ skipIndexation: true });
});
// Quand on sort du nœud
renderer.on("leaveNode", () => {
state.hoveredNode = null;
state.hoveredPredecessors = null;
renderer.refresh({ skipIndexation: true });
});
setupTableRowClickListeners(renderer, graph);
return renderer;
}
/**
* Met en place l'écouteur de clic sur les lignes des tableaux pour interagir avec le graphe.
* @param {Sigma} renderer - L'instance du renderer Sigma.
* @param {Graph} graph - L'instance du graphe Graphology.
*/
function setupTableRowClickListeners(renderer, graph) {
$('#descendance-table tbody, #ascendance-table tbody').on('click', 'tr', function() {
const nodeId = $(this).attr('data-node-id');
if (!nodeId || !renderer || !graph.hasNode(nodeId)) return;
// Effet de "highlight" temporaire sur le nœud dans le graphe
const originalSize = graph.getNodeAttribute(nodeId, "size");
const highlightSize = originalSize * 1.6;
graph.setNodeAttribute(nodeId, "size", highlightSize);
renderer.refresh(); // Nécessaire pour forcer le rendu
setTimeout(() => {
graph.setNodeAttribute(nodeId, "size", originalSize);
renderer.refresh();
}, 1000);
// Supprime la classe active des autres lignes
$('#descendance-table tbody tr, #ascendance-table tbody tr').removeClass('active');
$(this).addClass('active');
showNodeInfo(graph.getNodeAttributes(nodeId));
const pos = renderer.getNodeDisplayData(nodeId);
if (pos) {
renderer.getCamera().animate({ x: pos.x, y: pos.y, ratio: 0.2 }, { duration: 600 });
}
});
}
// ===================================================================
// POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE
// ===================================================================
document.addEventListener("DOMContentLoaded", () => {
// --- ÉTAPE 1: LECTURE DES DONNÉES ET INITIALISATION DES TABLEAUX ---
const sigmaContainer = document.getElementById("sigma-container");
let graphData;
if (sigmaContainer && sigmaContainer.dataset.graph) {
try {
graphData = JSON.parse(sigmaContainer.dataset.graph);
} catch (e) {
console.error("Erreur d'analyse JSON:", e);
return;
}
const common_dt_options = {
"language": { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" },
"pageLength": 10,
"responsive": true,"scrollX": true , "scrollY":true,
"columnDefs": [
{
// Appliquer notre plugin 'numeric-string' aux colonnes cibles
"type": "numeric-string",
"targets": [2, 4, 6, 7, 8, 9] // Indices des colonnes: Downloads, Likes, Distance, etc.
}
]
// "searching" est activé par défaut, on peut le customiser
};
const tableDescendance = $('#descendance-table').DataTable(common_dt_options);
const tableAscendance = $('#ascendance-table').DataTable(common_dt_options);
// Gérer la barre de recherche personnalisée
const customSearch = document.getElementById('custom-table-search');
if (customSearch) {
customSearch.addEventListener('keyup', function() {
const query = this.value;
tableDescendance.search(query).draw();
tableAscendance.search(query).draw();
});
}
tableAscendance.clear();
tableDescendance.clear();
graphData.nodes.forEach(node => {
// Filtrer pour n'inclure que les modèles dans ces tableaux
if (node.label !== 'Modèle' && node.label !== 'Model') return;
const distance = node.distance;
if (distance === 0) return;
// Construction de la ligne avec les nouvelles données
const rowData = [
node.id || "N/A",
node.author || "Inconnu",
// La conversion en string via la condition est parfaite ici
String(node.downloads ?? "Inconnu"),
node.task || "Inconnue",
String(node.likes ?? "0"),
String(node.createdAt ?? "Inconnue"),
node.dataset || "Inconnu",
node.license || "Inconnue",
distance > 0 ? `+${distance}` : String(distance ?? 0),
String(node.ascendantsCount ?? "0"),
String(node.descendantsCount ?? "0"),
String(node.citationCount ?? "0")
];
const tableToUpdate = distance > 0 ? tableDescendance : tableAscendance;
tableToUpdate.row.add(rowData).node().setAttribute("data-node-id", node.id);
});
tableAscendance.draw();
tableDescendance.draw();
}
// --- ÉTAPE 2: GESTION DES INTERACTIONS DE L'UTILISATEUR ---
// Logique du bouton "Voir l'arbre généalogique"
const showGraphBtn = document.getElementById('show-graph-btn');
const graphSection = document.getElementById('genealogy-graph-section');
let sigmaInstance = null; // Garde en mémoire l'instance du graphe
if (showGraphBtn && graphSection && graphData) {
showGraphBtn.addEventListener('click', () => {
if (graphSection.style.display === 'none') {
graphSection.style.display = 'block';
if (!sigmaInstance) {
sigmaInstance = initializeSigmaGraph(graphData);
}
graphSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
graphSection.style.display = 'none';
document.getElementById("node-info-card").style.display = "none";
}
});
}
const input = document.getElementById('search-input');
const suggestionsList = document.getElementById('suggestions-list');
const modelCheckbox = document.getElementById('filter-model');
const datasetCheckbox = document.getElementById('filter-dataset');
// Cette fonction sera notre point central pour l'autocomplétion
function fetchAutocomplete() {
const query = input.value.trim();
// 1. Déterminer quel filtre est actif
let activeFilter = '';
if (modelCheckbox.checked) {
activeFilter = 'Model';
} else if (datasetCheckbox.checked) {
activeFilter = 'Dataset';
}
// Si le champ est vide, on cache les suggestions
if (query.length < 2) {
suggestionsList.style.display = 'none';
return;
}
// 2. Construire l'URL avec le filtre
// On commence avec la requête de base
let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`;
// On ajoute le paramètre 'filter' SEULEMENT s'il y en a un d'actif
if (activeFilter) {
fetchUrl += `&filter=${activeFilter}`;
}
// 3. Lancer la requête fetch avec la bonne URL
fetch(fetchUrl)
.then(response => response.json())
.then(data => {
suggestionsList.innerHTML = '';
if (data.length > 0) {
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
li.addEventListener('click', () => {
input.value = item.name;
suggestionsList.style.display = 'none';
});
suggestionsList.appendChild(li);
});
suggestionsList.style.display = 'block';
} else {
suggestionsList.style.display = 'none';
}
});
}
// On écoute les événements sur le champ de saisie
input.addEventListener('input', fetchAutocomplete);
// AMÉLIORATION : On écoute aussi les changements sur les checkboxes !
// Si l'utilisateur change de filtre, on relance immédiatement une recherche.
modelCheckbox.addEventListener('change', fetchAutocomplete);
datasetCheckbox.addEventListener('change', fetchAutocomplete);
// Logique pour cacher les suggestions si on clique ailleurs (inchangée)
document.addEventListener('click', (e) => {
if (!input.contains(e.target) && !suggestionsList.contains(e.target)) {
suggestionsList.style.display = 'none';
}
});
// Logique de la checkbox de profondeur (inchangée)
const checkbox = document.getElementById('depth-unlimited');
if (checkbox) {
const depthInput = document.getElementById('depth');
function toggleDepthInput() { if(depthInput) depthInput.disabled = checkbox.checked; }
checkbox.addEventListener('change', toggleDepthInput);
toggleDepthInput();
}
});