/** * Construit les cases à cocher pour les filtres d'arêtes dynamiquement. */ function buildEdgeFilters() { const container = document.getElementById('edge-filters-container'); if (!container) return; container.innerHTML = ''; Object.entries(edgeInfos).forEach(([key, { name }]) => { const div = document.createElement('div'); div.className = 'form-check form-switch'; const input = document.createElement('input'); input.className = 'form-check-input'; input.type = 'checkbox'; input.id = `filter-edge-${key.replace(/\s+/g, '-')}`; input.value = key; input.checked = true; const label = document.createElement('label'); label.className = 'form-check-label'; label.htmlFor = input.id; label.textContent = name; div.appendChild(input); div.appendChild(label); container.appendChild(div); }); } function populateGraphModelsTable(graphData, table) { table.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; // 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") ]; table.row.add(rowData); }); table.draw(); } /** * Met à jour la visibilité des nœuds et des arêtes dans le graphe. * Cache les nœuds dont les liens sont tous cachés et met à jour la table des modèles. * @param {Graph} graph - L'instance du graphe Graphology. * @param {Sigma} renderer - L'instance du renderer Sigma. * @param {DataTable} table - L'instance de la DataTable des modèles à mettre à jour. */ function updateGraphVisibility(graphData,graph, renderer, table) { const filtersContainer = document.getElementById('graph-filters-container'); if (!filtersContainer) return; const visibleNodeTypes = new Set(); filtersContainer.querySelectorAll('input[id^="filter-node-"]:checked').forEach(c => { visibleNodeTypes.add(c.value); if (c.value === 'Modèle') visibleNodeTypes.add('Model'); // if (c.value === 'personne') visibleNodeTypes.add('Author'); }); const visibleEdgeTypes = new Set(); filtersContainer.querySelectorAll('input[id^="filter-edge-"]:checked').forEach(c => visibleEdgeTypes.add(c.value)); graph.forEachNode((node, attr) => graph.setNodeAttribute(node, 'hidden', !visibleNodeTypes.has(attr.dataCat))); graph.forEachEdge((edge, attr, source, target) => { const sourceHidden = graph.getNodeAttribute(source, 'hidden'); const targetHidden = graph.getNodeAttribute(target, 'hidden'); const typeHidden = !visibleEdgeTypes.has(attr.label); graph.setEdgeAttribute(edge, 'hidden', sourceHidden || targetHidden || typeHidden); }); // On repeuple la table en se basant sur la nouvelle visibilité des nœuds populateGraphModelsTable(graphData, table); renderer.refresh(); } /** * 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) { $('#graph-models-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 $('#graph-models-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 }); } }); } /** * 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"); const nodeName = document.getElementById("node_name")?.textContent?.trim() || ""; if (!container) 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' }; 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 => { const info = edgeInfos[edge.relation]; const edgeColor = info ? info.color : '#ccc'; if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) { graph.addEdge(edge.source, edge.target, { label: edge.relation || '', color: edgeColor, size: 3, type: 'arrow' }); } }); graphologyLibrary.layoutForceAtlas2.assign(graph, { iterations: 100 }); const renderer = new Sigma(graph, container); 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(); buildEdgeFilters(); const graphModelsTable = $('#graph-models-table').DataTable({ language: { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" }, pageLength: 5, responsive: true, scrollX: true }); // Remplir la table une première fois avec tous les modèles populateGraphModelsTable(graphData, graphModelsTable); const filtersContainer = document.getElementById('graph-filters-container'); if (filtersContainer) { // L'écouteur appelle maintenant la fonction updateGraphVisibility qui est bien définie filtersContainer.addEventListener('change', () => updateGraphVisibility(graphData,graph, renderer, graphModelsTable)); } renderer.on('clickNode', ({ node }) => showNodeInfo(graph.getNodeAttributes(node))); setupTableRowClickListeners(renderer, graph); 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 }); }); document.getElementById("export-btn").addEventListener("click", () => { const graph = renderer.getGraph(); // Graph actuel de Sigma const graphData = { nodes: graph.nodes().map(n => graph.getNodeAttributes(n)), edges: graph.edges().map(e => ({ ...graph.getEdgeAttributes(e), source: graph.source(e), target: graph.target(e), })), }; console.log(graphData) // Récupération du HTML des légendes const legendHtmlnodes = document.getElementById("legend-nodes")?.innerHTML || ""; const legendHtmledges = document.getElementById("legend-edges")?.innerHTML || ""; fetch("/static/graph_export.html") .then(res => res.text()) .then(template => { // Remplacement des placeholders const finalHtml = template .replace("{{GRAPH_DATA}}", JSON.stringify(graphData)) .replaceAll("{{NODE_NAME}}", JSON.stringify(nodeName)) .replace("{{LEGEND_HTML_NODES}}", legendHtmlnodes) .replace("{{LEGEND_HTML_EDGES}}", legendHtmledges); const blob = new Blob([finalHtml], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "graph_export.html"; a.click(); URL.revokeObjectURL(url); }); }); return renderer; } // =================================================================== // POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE // =================================================================== document.addEventListener("DOMContentLoaded", () => { const sigmaContainer = document.getElementById("sigma-container"); if (sigmaContainer && sigmaContainer.dataset.graph) { let graphData; try { graphData = JSON.parse(sigmaContainer.dataset.graph); if (graphData && graphData.nodes && graphData.nodes.length > 0) { initializeSigmaGraph(graphData); } } catch (e) { console.error("Erreur d'analyse JSON des données du graphe:", e); } } // --- Logique du formulaire (autocomplétion, etc.) --- const input = document.getElementById('search-input'); const suggestionsList = document.getElementById('suggestions-list'); if(input && suggestionsList) { const modelCheckbox = document.getElementById('filter-model'); const datasetCheckbox = document.getElementById('filter-dataset'); const authorCheckbox = document.getElementById('filter-author'); // Assurez-vous d'avoir cet ID 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'; } }); } input.addEventListener('input', fetchAutocomplete); if (modelCheckbox) modelCheckbox.addEventListener('change', fetchAutocomplete); if (datasetCheckbox) datasetCheckbox.addEventListener('change', fetchAutocomplete); if (authorCheckbox) authorCheckbox.addEventListener('change', fetchAutocomplete); document.addEventListener('click', (e) => { if (!input.contains(e.target) && !suggestionsList.contains(e.target)) { suggestionsList.style.display = 'none'; } }); } 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(); } });