Spaces:
Running
Running
| /** | |
| * 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, { color }]) => { | |
| 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 = edgeDisplayName(key); | |
| 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 unknown = I18N['js.unknown'] || "Inconnu"; | |
| const unknownF = I18N['js.unknown_f'] || "Inconnue"; | |
| const na = I18N['js.na'] || "N/A"; | |
| const rowData = [ | |
| node.id || na, | |
| node.author || unknown, | |
| // La conversion en string via la condition est parfaite ici | |
| String(node.downloads ?? unknown), | |
| node.task || unknownF, | |
| String(node.likes ?? "0"), | |
| String(node.createdAt ?? unknownF), | |
| node.dataset || unknown, | |
| node.license || unknownF, | |
| 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 datatablesLangUrl = (window.__I18N_LANG === 'en') | |
| ? "https://cdn.datatables.net/plug-ins/1.13.4/i18n/en.json" | |
| : "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json"; | |
| const graphModelsTable = $('#graph-models-table').DataTable({ | |
| language: { "url": datatablesLangUrl }, | |
| 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(); | |
| } | |
| }); | |