genmod / application_neo4j /static /js /script_expert.js
Quentin Lhoest
add app
f0806e2
/**
* 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();
}
});