|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 => { |
|
|
|
|
|
if (node.label !== 'Modèle' && node.label !== 'Model') return; |
|
|
const distance = node.distance; |
|
|
|
|
|
|
|
|
|
|
|
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") |
|
|
]; |
|
|
table.row.add(rowData); |
|
|
}); |
|
|
table.draw(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
}); |
|
|
|
|
|
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); |
|
|
}); |
|
|
|
|
|
|
|
|
populateGraphModelsTable(graphData, table); |
|
|
|
|
|
renderer.refresh(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
|
|
|
|
$('#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 }); |
|
|
} |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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(); |
|
|
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 |
|
|
}); |
|
|
|
|
|
|
|
|
populateGraphModelsTable(graphData, graphModelsTable); |
|
|
|
|
|
const filtersContainer = document.getElementById('graph-filters-container'); |
|
|
if (filtersContainer) { |
|
|
|
|
|
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)); |
|
|
}); |
|
|
|
|
|
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 }); |
|
|
}); |
|
|
document.getElementById("export-btn").addEventListener("click", () => { |
|
|
const graph = renderer.getGraph(); |
|
|
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) |
|
|
|
|
|
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 => { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
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); |
|
|
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(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|