// 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('