genmod / application_neo4j /static /js /script_dataset.js
Quentin Lhoest
add app
f0806e2
const edgeInfos_dataset = {
"Fait partie de cette organisation": {
color: "#a05195",name : "Fait partie de cette organisation",
tooltip: "Cette personne est membre de cette organisation"
},
"A publié": {
color: "#003f5c",name : "A publié",
tooltip: "Un auteur (personne ou organistaion) a publié un modèle"
},
"A été utilisé dans ce modèle": {
color: "#f50f0f",name : "A été utilisé dans ce modèle",
tooltip: "Ce dataset a servi pour l'entraînement de ce modèle"
},
"A généré": {
color: "#8f7340",name : "A permis de générer",
tooltip: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)"
},
"finetune": {
color: "#d9c00d",name : "Finetune",
tooltip: "Ajustement : le modèle source est ré-entraîné sur un jeu de données spécifique afin de pouvoir être performant pour une tâche précise."
},
"adapter": {
color: "#9af17c",name : "Adapter",
tooltip: "Adaptation : méthode d’ajustement qui peut être utilisée avec peu de ressources de calcul."
},
"quantized": {
color: "#1bc3c6",name : "Quantized",
tooltip:"Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire."
},
"merge": {
color: "#e407e4",name : "Merge",
tooltip:"Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance."
}
};
function buildEdgeLegend() {
const legendContainer = document.getElementById("legend-edges");
legendContainer.innerHTML = ""; // reset
Object.entries(edgeInfos_dataset).forEach(([relation, {color, name,tooltip}]) => {
const li = document.createElement("li");
li.className = "list-group-item d-flex align-items-center";
// ajout Bootstrap tooltip
li.setAttribute("data-bs-toggle", "tooltip");
li.setAttribute("data-bs-placement", "top");
li.setAttribute("data-bs-html", "true");
li.setAttribute("title", tooltip);
const span = document.createElement("span");
span.className = "legend-color edge me-2";
span.style.backgroundColor = color;
li.appendChild(span);
li.appendChild(document.createTextNode(name));
legendContainer.appendChild(li);
});
// nécessaire pour activer les tooltips Bootstrap dynamiques
const tooltipTriggerList = [].slice.call(legendContainer.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
}
/**
* 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_dataset[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 });
const renderer = new Sigma(graph, container);
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));
});
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) {
$('#train-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
$('#train-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 tableTrain = $('#train-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;
tableTrain.search(query).draw();
});
}
tableTrain.clear();
console.log("GraphData nodes:", graphData.nodes);
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",
String(node.ascendantsCount ?? "0"),
String(node.descendantsCount ?? "0"),
String(node.citationCount ?? "0")
];
tableTrain.row.add(rowData).node().setAttribute("data-node-id", node.id);
});
tableTrain.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();
}
});