// Variables globales var svg, sankey, width, height, margin; var color = d3.scaleOrdinal(d3.schemeCategory20); var currentGraph = null; var isTooltipVisible = false; // Añadido para controlar visibilidad del tooltip var lastMouseX = 0; // Añadido para posición del mouse var lastMouseY = 0; // Añadido para posición del mouse // Inicializar gráfico function initChart() { console.log("Inicializando gráfico..."); // Limpiar SVG existente d3.select("#my_dataviz").select("svg").remove(); d3.select("#my_dataviz .loading").style("display", "block"); // Obtener dimensiones del contenedor var container = document.getElementById('my_dataviz'); width = container.clientWidth; height = container.clientHeight; margin = {top: 20, right: 20, bottom: 20, left: 20}; // Crear SVG svg = d3.select("#my_dataviz") .append("svg") .attr("width", width) .attr("height", height) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Ajustar dimensiones para cálculos internos width = width - margin.left - margin.right; height = height - margin.top - margin.bottom; // Configurar Sankey sankey = d3.sankey() .nodeWidth(20) .nodePadding(10) .size([width, height]); // Cargar datos loadData(); } // Cargar datos function loadData() { d3.json("https://resquivel0810.github.io/resquivel/data/data_sankey.json", function(error, graph) { if (error) { console.error("Error cargando datos:", error); showError("Error loading data: " + error); return; } if (!graph) { showError("No data received"); return; } // Normalizar datos var normalizedData = normalizeData(graph); if (!normalizedData) { showError("Invalid data structure"); return; } currentGraph = normalizedData; createSankey(normalizedData); }); } // Normalizar datos para Sankey function normalizeData(graph) { try { if (graph.nodes && graph.links) { return processSankeyData(graph); } if (Array.isArray(graph)) { return getSampleData(); } 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 { // Ocultar loading d3.select("#my_dataviz .loading").style("display", "none"); if (graph.nodes.length === 0) { showError("No hay nodos para mostrar"); 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]; var sourceName = sourceNode ? sourceNode.name : "Source"; var targetName = targetNode ? targetNode.name : "Target"; showTooltip(event, '
Flow: ' + graph.links[d].source.name + ' → ' + graph.links[d].target.name + '
' + '
' + formatCurrency(graph.links[d].value) + '
'); d3.select(this).style("stroke-opacity", 0.8); }) .on("mousemove", function(event) { // Actualizar posición del mouse 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 || 0) + "," + (d.y || 0) + ")"; }) .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 || 40; }) .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) { showTooltip(event, '
' + graph.nodes[d].name + '
' + '
' + formatCurrency(graph.nodes[d].value) + '
'); d3.select(this).classed("highlighted", true); link.style("stroke-opacity", function(linkData) { var sourceNode = graph.nodes[linkData.source]; var targetNode = graph.nodes[linkData.target]; return (sourceNode === d || targetNode === d) ? 0.8 : 0.2; }); }) .on("mousemove", function(event) { // Actualizar posición del mouse 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 || 40) / 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 < width / 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(height - (d.dy || 40), 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"); } catch (error) { console.error("Error creando gráfico:", error); showError("Error creating diagram: " + error.message); } } // Funciones tooltip function showTooltip(event, content) { var tooltip = d3.select("#tooltip"); // Guardar posición del mouse 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; var tooltipWidth = 320; var windowWidth = window.innerWidth; var windowHeight = window.innerHeight; if (x + tooltipWidth > windowWidth) { x = lastMouseX - tooltipWidth - 20; } if (y < 20) y = 20; if (y > windowHeight - 100) y = windowHeight - 100; tooltip .style("left", x + "px") .style("top", y - 200 + "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 }); } // Mostrar error function showError(message) { d3.select("#my_dataviz .loading").style("display", "none"); d3.select("#my_dataviz") .html('
' + '

⚠️ Error

' + '

' + message + '

' + '' + '
'); } // Datos de ejemplo function getSampleData() { return { nodes: [ {name: "Total Revenue", value: 1000000}, {name: "Cost of Goods", value: 400000}, {name: "Gross Profit", value: 600000}, {name: "Operating Expenses", value: 300000}, {name: "Operating Profit", value: 300000}, {name: "Taxes", value: 90000}, {name: "Net Profit", value: 210000} ], links: [ {source: 0, target: 1, value: 400000}, {source: 0, target: 2, value: 600000}, {source: 2, target: 3, value: 300000}, {source: 2, target: 4, value: 300000}, {source: 4, target: 5, value: 90000}, {source: 4, target: 6, value: 210000} ] }; } // Inicializar cuando el DOM esté listo document.addEventListener("DOMContentLoaded", function() { initChart(); // Redimensionar var resizeTimer; window.addEventListener("resize", function() { clearTimeout(resizeTimer); resizeTimer = setTimeout(initChart, 250); }); // Añadido: 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 window.useSampleData = function() { currentGraph = getSampleData(); createSankey(currentGraph); };