// Variables globales var svg, sankey, width, height, margin; var color = d3.scaleOrdinal(d3.schemeCategory20); var currentGraph = null; var originalGraph = null; var isTooltipVisible = false; var lastMouseX = 0; var lastMouseY = 0; var currentYear = '2026'; // Inicializar margen por defecto margin = {top: 20, right: 20, bottom: 20, left: 20}; // URLs de datos por año y sector - Solo los archivos que EXISTEN realmente var dataUrls = { '2024': { 'all': 'https://resquivel0810.github.io/resquivel/data/sankey-egresos-2024.json', '07 Defensa Nacional': 'https://resquivel0810.github.io/resquivel/data/sankey-egresos-2024-07.json' }, '2025': { 'all': 'https://resquivel0810.github.io/resquivel/data/sankey-egresos-2025.json' }, '2026': { 'all': 'https://resquivel0810.github.io/resquivel/data/sankey-egresos-2026.json' } }; // Variable para llevar el control del filtro actual por año var currentFilterByYear = { '2024': 'all', '2025': 'all', '2026': 'all' }; // Inicializar gráfico function initChart() { console.log("Inicializando gráfico para", currentYear + "..."); // Limpiar SVG existente d3.select("#dataviz").select("svg").remove(); d3.select("#dataviz .loading").style("display", "block"); d3.select("#noDataMessage").style("display", "none"); // Actualizar texto de loading d3.select("#dataviz .loading").text("Cargando diagrama Sankey para " + currentYear + "..."); // Actualizar opciones del select updateSelectOptionsForYear(currentYear); // Cargar datos para el año actual loadDataForYear(currentYear); } // Mostrar/ocultar y actualizar leyenda function updateLegend(year, sector) { var legend = d3.select("#legend"); var legendValue = d3.select("#legendValue"); var legendNote = d3.select("#legendNote"); if (sector === 'all') { // Para "Todos los sectores", mostrar en millones legendValue.text("Valores en millones (M)"); legendNote.text("Cada $1 M = $1,000,000"); legend.style("display", "block"); } else { // Para sectores específicos, mostrar normalmente legendValue.text("Valores en unidades monetarias"); legendNote.text("Valores exactos"); legend.style("display", "block"); } } // Cargar datos para un año específico - USANDO CALLBACK (D3 v4) function loadDataForYear(year, sector) { // Si no se especifica sector, usar el actual if (!sector) { sector = currentFilterByYear[year] || 'all'; } // Actualizar leyenda updateLegend(year, sector); console.log("Intentando cargar datos para:", year, "sector:", sector); // Verificar si ya tenemos los datos en caché var cacheKey = year + '_' + sector; if (window.dataCache && window.dataCache[cacheKey]) { console.log("Usando datos en caché para", year, "sector:", sector); processLoadedData(window.dataCache[cacheKey], year); return; } // Verificar si el sector tiene datos disponibles if (!hasDataForSector(year, sector)) { console.warn("No hay datos disponibles para", year, "sector:", sector); showNoDataMessage(year, sector); return; } var url = dataUrls[year][sector]; console.log("Cargando datos desde:", url); // Ocultar loading y mostrar mensaje de carga d3.select("#dataviz .loading").style("display", "block"); d3.select("#dataviz .loading").text("Cargando datos para " + year + " - " + (sector === 'all' ? 'Todos los sectores' : sector) + "..."); d3.select("#noDataMessage").style("display", "none"); // USAR CALLBACK - D3 v4 d3.json(url, function(error, graph) { if (error) { console.error("Error cargando datos para", year, "sector:", sector, "Error:", error); showNoDataMessage(year, sector); // Intentar con 'all' si hay error y no estamos ya en 'all' if (sector !== 'all' && dataUrls[year] && dataUrls[year]['all']) { console.log("Intentando cargar datos 'all' como alternativa..."); setTimeout(function() { var selectId = 'sectorFilter' + year; var select = document.getElementById(selectId); if (select) { select.value = 'all'; } loadDataForYear(year, 'all'); }, 1500); } return; } if (!graph) { console.error("No se recibieron datos para", year, "sector:", sector); showNoDataMessage(year, sector); return; } console.log("Datos recibidos para", year, "sector:", sector, "Datos:", graph); // Normalizar datos var normalizedData = normalizeData(graph); if (!normalizedData || normalizedData.nodes.length === 0) { console.error("Datos inválidos o vacíos para", year, "sector:", sector); showNoDataMessage(year, sector); return; } // Guardar datos en caché if (!window.dataCache) window.dataCache = {}; window.dataCache[cacheKey] = normalizedData; // Actualizar filtro actual currentFilterByYear[year] = sector; processLoadedData(normalizedData, year); }); } // Verificar si un sector tiene datos disponibles function hasDataForSector(year, sector) { return dataUrls[year] && dataUrls[year][sector] !== undefined; } // Mostrar mensaje de datos no encontrados function showNoDataMessage(year, sector) { console.log("Mostrando mensaje de error para", year, "sector:", sector); // Ocultar loading d3.select("#dataviz .loading").style("display", "none"); // Crear mensaje informativo var sectorDisplayName = sector === 'all' ? 'Todos los sectores' : sector; var message = '
' + '

⚠️ Datos no disponibles

' + '

No se encontraron datos para:

' + '
' + '

Año: ' + year + '

' + '

Sector: ' + sectorDisplayName + '

' + '
' + '

' + 'El archivo de datos específico no existe en el servidor.' + '

'; // Solo mostrar botón de "Ver todos" si no estamos ya en 'all' y existe datos 'all' para este año if (sector !== 'all' && dataUrls[year] && dataUrls[year]['all']) { message += ''; } message += '
'; // Mostrar mensaje var noDataMessage = d3.select("#noDataMessage"); noDataMessage .html(message) .style("display", "block"); // Limpiar SVG existente d3.select("#dataviz").select("svg").remove(); // Configurar evento para el botón setTimeout(function() { var loadAllBtn = document.getElementById('loadAllSectorsBtn'); if (loadAllBtn) { loadAllBtn.addEventListener('click', function() { // Actualizar el select a 'all' var selectId = 'sectorFilter' + year; var select = document.getElementById(selectId); if (select) { select.value = 'all'; } // Cargar datos 'all' loadDataForYear(year, 'all'); }); } }, 100); } // Procesar datos cargados function processLoadedData(data, year) { console.log("Procesando datos para", year); // Guardar todos los datos originales originalGraph = data; currentGraph = JSON.parse(JSON.stringify(originalGraph)); // Copia profunda // Actualizar el gráfico createSankey(currentGraph); } // Función para actualizar las opciones del select function updateSelectOptionsForYear(year) { var selectId = 'sectorFilter' + year; var select = document.getElementById(selectId); if (!select) return; // Opciones base var options = ''; // Lista de posibles sectores (en el orden que quieras mostrar) var allPossibleSectors = [ 'Total Gasto Programable bruto', '01 Poder Legislativo', '02 Oficina de la Presidencia de la República', '03 Poder Judicial', '07 Defensa Nacional' ]; // Verificar qué archivos existen para este año if (dataUrls[year]) { // Solo agregar opciones si el archivo existe allPossibleSectors.forEach(function(sector) { if (dataUrls[year][sector]) { options += ''; } }); } // Aplicar opciones select.innerHTML = options; // Restaurar selección anterior si existe y está disponible var previousSelection = currentFilterByYear[year]; if (previousSelection && select.querySelector('option[value="' + previousSelection + '"]')) { select.value = previousSelection; } else { select.value = 'all'; currentFilterByYear[year] = 'all'; } } // Cambiar a un año específico function switchYear(year) { console.log("Cambiando a año:", year); // Actualizar año actual currentYear = year; // Actualizar tabs activas document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); if (button.dataset.year === year) { button.classList.add('active'); } }); document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); if (content.id === 'content-' + year) { content.classList.add('active'); } }); // Actualizar opciones del select updateSelectOptionsForYear(year); // Cargar datos con el filtro actual para este año var currentSector = currentFilterByYear[year] || 'all'; loadDataForYear(year, currentSector); } // Normalizar datos para Sankey function normalizeData(graph) { try { if (graph.nodes && graph.links) { return processSankeyData(graph); } if (Array.isArray(graph)) { return getSampleDataForYear(currentYear); } var nodes = graph.Nodes || graph.nodelist || graph.data || []; var links = graph.Links || graph.linklist || graph.edges || []; if (nodes.length > 0 || links.length > 0) { return processSankeyData({nodes: nodes, links: links}); } return null; } catch (error) { console.error("Error normalizando datos:", error); return null; } } // Procesar datos en formato Sankey function processSankeyData(data) { var nodes = []; var links = []; // Procesar nodos if (Array.isArray(data.nodes)) { nodes = data.nodes.map(function(node, index) { var name = "Node " + (index + 1); if (typeof node === 'string') { name = node; } else if (typeof node === 'object') { if (node.name !== undefined) name = String(node.name); else if (node.Name !== undefined) name = String(node.Name); else if (node.id !== undefined) name = String(node.id); else if (node.ID !== undefined) name = String(node.ID); else if (node.label !== undefined) name = String(node.label); else if (node.LABEL !== undefined) name = String(node.LABEL); else if (node.title !== undefined) name = String(node.title); } var value = 0; if (typeof node === 'object') { if (node.value !== undefined) value = parseFloat(node.value) || 0; else if (node.Value !== undefined) value = parseFloat(node.Value) || 0; else if (node.size !== undefined) value = parseFloat(node.size) || 0; else if (node.val !== undefined) value = parseFloat(node.val) || 0; } return { id: index, name: name, value: value, original: node }; }); } // Procesar enlaces if (Array.isArray(data.links)) { links = data.links.map(function(link, index) { var source = link.source; var target = link.target; if (typeof source === 'object' && source !== null) { source = source.index || source.id || 0; } if (typeof target === 'object' && target !== null) { target = target.index || target.id || 0; } source = parseInt(source) || 0; target = parseInt(target) || 0; var value = parseFloat(link.value || link.Value || link.size || 0) || 0; return { source: source, target: target, value: value, original: link }; }).filter(function(link) { return link.source >= 0 && link.target >= 0 && link.source < nodes.length && link.target < nodes.length && link.value > 0; }); } // Calcular valores de nodos si no están definidos nodes.forEach(function(node) { if (node.value === 0) { var incoming = links.filter(function(l) { return l.target === node.id; }); var outgoing = links.filter(function(l) { return l.source === node.id; }); var totalIn = d3.sum(incoming, function(d) { return d.value; }); var totalOut = d3.sum(outgoing, function(d) { return d.value; }); node.value = Math.max(totalIn, totalOut) || 1; } }); return { nodes: nodes, links: links }; } // Crear gráfico Sankey function createSankey(graph) { try { // Limpiar SVG existente d3.select("#dataviz").select("svg").remove(); // Obtener dimensiones del contenedor var container = document.getElementById('dataviz'); width = container.clientWidth; height = container.clientHeight; // Asegurarse de que margin esté definido if (!margin) { margin = {top: 20, right: 20, bottom: 20, left: 20}; } // Calcular dimensiones internas var innerWidth = width - margin.left - margin.right; var innerHeight = height - margin.top - margin.bottom; // Crear SVG svg = d3.select("#dataviz") .append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Configurar Sankey con nuevas dimensiones sankey = d3.sankey() .nodeWidth(20) .nodePadding(10) .size([innerWidth, innerHeight]); // Ocultar loading y mensaje de error d3.select("#dataviz .loading").style("display", "none"); d3.select("#noDataMessage").style("display", "none"); if (!graph || graph.nodes.length === 0) { d3.select("#noDataMessage").style("display", "block"); d3.select("#noDataMessage").html('

No hay datos para mostrar

El conjunto de datos está vacío.

'); return; } // Aplicar layout Sankey sankey .nodes(graph.nodes) .links(graph.links) .layout(32); // Crear enlaces var link = svg.append("g") .selectAll(".link") .data(graph.links) .enter().append("path") .attr("class", "link") .attr("d", sankey.link()) .style("stroke-width", function(d) { return Math.max(1, d.dy || 3); }) .style("stroke", function(d) { var sourceNode = graph.nodes[d.source]; return color(sourceNode ? sourceNode.name : "Unknown"); }) .style("stroke-opacity", 0.4) .on("mouseover", function(event, d) { var sourceNode = graph.nodes[d.source]; var targetNode = graph.nodes[d.target]; showTooltip(event, '
Flujo: ' + event.source.name + ' → ' + event.target.name + '
' + '
' + formatCurrency(event.target.value) + '
' + '
Año: ' + currentYear + '
'); d3.select(this).style("stroke-opacity", 0.8); }) .on("mousemove", function(event) { lastMouseX = event.pageX; lastMouseY = event.pageY; updateTooltipPosition(); }) .on("mouseout", function() { hideTooltip(); d3.select(this).style("stroke-opacity", 0.4); }); // Crear nodos var node = svg.append("g") .selectAll(".node") .data(graph.nodes) .enter().append("g") .attr("class", "node") .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) .call(d3.drag() .subject(function(d) { return {x: d.x, y: d.y}; }) .on("start", function() { this.parentNode.appendChild(this); }) .on("drag", dragmove)); // Rectángulos de nodos node.append("rect") .attr("height", function(d) { return d.dy; }) .attr("width", sankey.nodeWidth()) .style("fill", function(d) { return color(d.name); }) .style("stroke", function(d) { return d3.rgb(color(d.name)).darker(1); }) .on("mouseover", function(event, d) { // console.log(currentSector) showTooltip(event, '
' + event.name + '
' + '
' + formatCurrency(event.value) + '
' + '
Año: ' + currentYear + '
'); d3.select(this).classed("highlighted", true); link.style("stroke-opacity", function(linkData) { return (linkData.source === d.id || linkData.target === d.id) ? 0.8 : 0.2; }); }) .on("mousemove", function(event) { lastMouseX = event.pageX; lastMouseY = event.pageY; updateTooltipPosition(); }) .on("mouseout", function() { hideTooltip(); d3.select(this).classed("highlighted", false); link.style("stroke-opacity", 0.4); }); // Etiquetas de texto para nodos node.append("text") .attr("x", -6) .attr("y", function(d) { return d.dy / 2; }) .attr("dy", ".35em") .attr("text-anchor", "end") .attr("class", "label") .text(function(d) { return d.name.length > 20 ? d.name.substring(0, 17) + "..." : d.name; }) .filter(function(d) { return d.x < innerWidth / 2; }) .attr("x", 6 + sankey.nodeWidth()) .attr("text-anchor", "start"); // Función para mover nodos function dragmove(d) { var x = d3.event.x; var y = Math.max(0, Math.min(innerHeight - d.dy, d3.event.y)); d.x = x; d.y = y; d3.select(this) .attr("transform", "translate(" + x + "," + y + ")"); sankey.relayout(); link.attr("d", sankey.link()); } console.log("Gráfico creado exitosamente para", currentYear); } catch (error) { console.error("Error creando gráfico:", error); d3.select("#dataviz .loading").style("display", "none"); d3.select("#noDataMessage").style("display", "block"); d3.select("#noDataMessage").html('

Error creando el diagrama

' + error.message + '

'); } } // Funciones tooltip function showTooltip(event, content) { var tooltip = d3.select("#tooltip"); lastMouseX = event.pageX; lastMouseY = event.pageY; tooltip .html(content) .style("opacity", 1); isTooltipVisible = true; updateTooltipPosition(); } function updateTooltipPosition() { if (!isTooltipVisible) return; var tooltip = d3.select("#tooltip"); var x = lastMouseX + 15; var y = lastMouseY - 400; var tooltipWidth = 320; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; if (x + tooltipWidth > windowWidth) { x = lastMouseX - tooltipWidth + 150; y = lastMouseY - 600; } if (y < 20) y = 20; if (y > windowHeight - 100) y = windowHeight + 200; tooltip .style("left", x + "px") .style("top", y + "px"); } function hideTooltip() { var tooltip = d3.select("#tooltip"); tooltip.style("opacity", 0); isTooltipVisible = false; } // Formatear moneda function formatCurrency(value) { if (isNaN(value) || !isFinite(value)) { value = 0; } return value.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }); } // Datos de ejemplo por año function getSampleDataForYear(year) { var yearMultiplier = { '2024': 0.9, '2025': 1.0, '2026': 1.1 }; var multiplier = yearMultiplier[year] || 1.0; return { nodes: [ {name: "Total Gasto Programable bruto", value: Math.round(1000000 * multiplier)}, {name: "01 Poder Legislativo", value: Math.round(150000 * multiplier)}, {name: "02 Oficina de la Presidencia de la República", value: Math.round(200000 * multiplier)}, {name: "03 Poder Judicial", value: Math.round(180000 * multiplier)}, {name: "Gastos Operativos", value: Math.round(300000 * multiplier)}, {name: "Inversiones", value: Math.round(170000 * multiplier)}, {name: "Servicios Públicos", value: Math.round(120000 * multiplier)}, {name: "Infraestructura", value: Math.round(90000 * multiplier)} ], links: [ {source: 0, target: 1, value: Math.round(150000 * multiplier)}, {source: 0, target: 2, value: Math.round(200000 * multiplier)}, {source: 0, target: 3, value: Math.round(180000 * multiplier)}, {source: 1, target: 4, value: Math.round(80000 * multiplier)}, {source: 2, target: 4, value: Math.round(120000 * multiplier)}, {source: 3, target: 4, value: Math.round(100000 * multiplier)}, {source: 1, target: 5, value: Math.round(70000 * multiplier)}, {source: 2, target: 5, value: Math.round(80000 * multiplier)}, {source: 3, target: 5, value: Math.round(80000 * multiplier)}, {source: 4, target: 6, value: Math.round(180000 * multiplier)}, {source: 5, target: 7, value: Math.round(120000 * multiplier)} ] }; } // Inicializar cuando el DOM esté listo document.addEventListener("DOMContentLoaded", function() { // Inicializar caché window.dataCache = {}; // Configurar event listeners para los tabs document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', function() { var year = this.dataset.year; switchYear(year); }); }); // Configurar event listeners para los selects de filtro document.querySelectorAll('.filter-select').forEach(select => { select.addEventListener('change', function() { var year = this.id.replace('sectorFilter', ''); var sector = this.value; // Actualizar el valor del filtro currentFilterByYear[year] = sector; // Si estamos en un año diferente al actual, cambiar primero el año if (year !== currentYear) { currentYear = year; document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); if (button.dataset.year === year) { button.classList.add('active'); } }); document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); if (content.id === 'content-' + year) { content.classList.add('active'); } }); } // Cargar datos loadDataForYear(year, sector); }); }); // Inicializar gráfico initChart(); // Redimensionar var resizeTimer; window.addEventListener("resize", function() { clearTimeout(resizeTimer); resizeTimer = setTimeout(function() { if (currentGraph) { createSankey(currentGraph); } }, 250); }); // Evento global de mousemove para seguir el tooltip document.addEventListener("mousemove", function(event) { if (isTooltipVisible) { lastMouseX = event.pageX; lastMouseY = event.pageY; updateTooltipPosition(); } }); }); // Función para usar datos de ejemplo (para pruebas) window.useSampleData = function() { var sampleData = getSampleDataForYear(currentYear); originalGraph = sampleData; currentGraph = JSON.parse(JSON.stringify(originalGraph)); createSankey(currentGraph); };