|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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', |
|
|
Dé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, |
|
|
scalingRatio: 10, |
|
|
slowDown: 1, |
|
|
strongGravityMode: false, |
|
|
barnesHutOptimize: true, |
|
|
barnesHutTheta: 0.5 |
|
|
} |
|
|
}); |
|
|
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) { |
|
|
|
|
|
if (!state.hoveredPredecessors.has(node)) { |
|
|
res.color = "#f1f1f1"; |
|
|
res.label = ""; |
|
|
} |
|
|
} |
|
|
|
|
|
return res; |
|
|
}); |
|
|
|
|
|
renderer.setSetting("edgeReducer", (edge, data) => { |
|
|
const res = { ...data }; |
|
|
|
|
|
if (state.hoveredNode && state.hoveredPredecessors) { |
|
|
const [source, target] = graph.extremities(edge); |
|
|
|
|
|
if (!state.hoveredPredecessors.has(source) || !state.hoveredPredecessors.has(target)) { |
|
|
res.hidden = true; |
|
|
} |
|
|
} |
|
|
|
|
|
return res; |
|
|
}); |
|
|
|
|
|
buildEdgeLegend(); |
|
|
|
|
|
|
|
|
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)); |
|
|
}); |
|
|
|
|
|
renderer.on("enterNode", ({ node }) => { |
|
|
state.hoveredNode = node; |
|
|
state.hoveredPredecessors = getAllPredecessors(graph, node); |
|
|
renderer.refresh({ skipIndexation: true }); |
|
|
}); |
|
|
|
|
|
|
|
|
renderer.on("leaveNode", () => { |
|
|
state.hoveredNode = null; |
|
|
state.hoveredPredecessors = null; |
|
|
renderer.refresh({ skipIndexation: true }); |
|
|
}); |
|
|
|
|
|
setupTableRowClickListeners(renderer, graph); |
|
|
|
|
|
return renderer; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
const originalSize = graph.getNodeAttribute(nodeId, "size"); |
|
|
const highlightSize = originalSize * 1.6; |
|
|
|
|
|
graph.setNodeAttribute(nodeId, "size", highlightSize); |
|
|
renderer.refresh(); |
|
|
|
|
|
setTimeout(() => { |
|
|
graph.setNodeAttribute(nodeId, "size", originalSize); |
|
|
renderer.refresh(); |
|
|
}, 1000); |
|
|
|
|
|
$('#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 }); |
|
|
} |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => { |
|
|
|
|
|
|
|
|
|
|
|
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": [ |
|
|
{ |
|
|
|
|
|
"type": "numeric-string", |
|
|
"targets": [2, 4, 6, 7, 8, 9] |
|
|
} |
|
|
] |
|
|
|
|
|
}; |
|
|
const tableDescendance = $('#descendance-table').DataTable(common_dt_options); |
|
|
const tableAscendance = $('#ascendance-table').DataTable(common_dt_options); |
|
|
|
|
|
|
|
|
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 => { |
|
|
|
|
|
if (node.label !== 'Modèle' && node.label !== 'Model') return; |
|
|
const distance = node.distance; |
|
|
if (distance === 0) return; |
|
|
|
|
|
|
|
|
const rowData = [ |
|
|
node.id || "N/A", |
|
|
node.author || "Inconnu", |
|
|
|
|
|
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(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const showGraphBtn = document.getElementById('show-graph-btn'); |
|
|
const graphSection = document.getElementById('genealogy-graph-section'); |
|
|
let sigmaInstance = null; |
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
function fetchAutocomplete() { |
|
|
const query = input.value.trim(); |
|
|
|
|
|
|
|
|
let activeFilter = ''; |
|
|
if (modelCheckbox.checked) { |
|
|
activeFilter = 'Model'; |
|
|
} else if (datasetCheckbox.checked) { |
|
|
activeFilter = 'Dataset'; |
|
|
} |
|
|
|
|
|
|
|
|
if (query.length < 2) { |
|
|
suggestionsList.style.display = 'none'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`; |
|
|
|
|
|
if (activeFilter) { |
|
|
fetchUrl += `&filter=${activeFilter}`; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
modelCheckbox.addEventListener('change', fetchAutocomplete); |
|
|
datasetCheckbox.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(); |
|
|
} |
|
|
}); |