// Variables de zoom var minZoom = 0.5; var maxZoom = 3.0; var currentZoom = 1.0; var zoomStep = 0.1; var originalScaleFactor = 0.9; // Variables para estado de controles var controlsVisible = false; var isMobile = false; // ms to wait after dragging before auto-rotating var rotationDelay = 20000 // autorotation speed var degPerSec = 6 // start angles var angles = { x: -20, y: 40, z: 0} // colors var colorWater = '#CFE2F3' var colorLand = '#749AD2' var colorGraticule = '#CFE2F3' // Variables globales var currentCountry var selectedDataType = 'population' var canvas = d3.select('#globe') var context var water = {type: 'Sphere'} var projection = d3.geoOrthographic().precision(0.1) var graticule = d3.geoGraticule10() var path var v0 var r0 var q0 var lastTime = d3.now() var degPerMs = degPerSec / 1000 var width, height var land var borders var autorotate, now, diff, rotation // Variables para tooltip var tooltip = d3.select('#tooltip') var tooltipVisible = false // Variables de datos var countries var countryList // Configuración de colores para cada tipo de dato var colorConfig = { population: { baseColor: [123, 23, 0], lightColor: [245, 245, 245], gradient: true }, exports: { baseColor: [0, 100, 0], lightColor: [240, 255, 240], gradient: true }, government: { colors: { 'Emirato unitario': [255, 0, 0], 'República parlamentaria': [0, 0, 255], 'República semipresidencialista': [255, 165, 0], 'Monarquía parlamentaria': [0, 128, 0], 'República presidencialista': [128, 0, 128], 'Monarquía constitucional': [0, 191, 255], 'Federal': [255, 215, 0], 'Democracia parlamentaria': [139, 0, 0], 'Democracia constitucional': [0, 0, 139], 'Monarquía absoluta': [60, 179, 113], 'República federal': [220, 20, 60], 'República islámica': [105, 105, 105], 'Estado comunista': [169, 169, 169], 'Presidencialista': [255, 105, 180], 'Parlamentaria': [178, 34, 34], 'Constitucional': [34, 139, 34], 'Unitaria': [65, 105, 225], 'Autoritaria': [210, 105, 30], 'null': [200, 200, 200], 'Desconocido': [200, 200, 200] }, gradient: false } } // Detectar si es dispositivo móvil function checkMobile() { isMobile = window.innerWidth <= 768; updateMobileControls(); return isMobile; } // Actualizar controles para móvil function updateMobileControls() { var mobileToggle = d3.select('#mobileControlsToggle'); if (isMobile) { mobileToggle.style('display', 'block'); // Colapsar controles por defecto en móvil if (!controlsVisible) { collapseAllControls(); } } else { mobileToggle.style('display', 'none'); expandAllControls(); } } // Colapsar todos los controles function collapseAllControls() { d3.select('#controls').classed('collapsed', true); d3.select('#zoomControls').classed('collapsed', true); d3.select('#legend').classed('collapsed', true); d3.select('#instructions').classed('collapsed', true); d3.select('#controlsToggle').html('▶'); d3.select('#zoomControlsToggle').html('◀'); d3.select('#legendToggle').html('▲'); d3.select('#instructionsToggle').html('◀'); controlsVisible = false; d3.select('#mobileControlsToggle').text('Mostrar Controles'); } // Expandir todos los controles function expandAllControls() { d3.select('#controls').classed('collapsed', false); d3.select('#zoomControls').classed('collapsed', false); d3.select('#legend').classed('collapsed', false); d3.select('#instructions').classed('collapsed', false); d3.select('#controlsToggle').html('◀'); d3.select('#zoomControlsToggle').html('▶'); d3.select('#legendToggle').html('▼'); d3.select('#instructionsToggle').html('▶'); controlsVisible = true; d3.select('#mobileControlsToggle').text('Ocultar Controles'); } // Alternar controles en móvil function toggleMobileControls() { if (controlsVisible) { collapseAllControls(); } else { expandAllControls(); } controlsVisible = !controlsVisible; } // Setup de controles plegables - VERSIÓN CORREGIDA function setupCollapsibleControls() { console.log("Configurando controles plegables..."); // Header toggle d3.select('#headerToggle').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); console.log("Header toggle clicked"); var header = d3.select('#header'); var isCollapsed = header.classed('collapsed'); header.classed('collapsed', !isCollapsed); d3.select(this).html(isCollapsed ? '▼' : '▲'); }); // Controls toggle d3.select('#controlsToggle').on('click', function() { var event = d3.event; if (event) { event.stopPropagation(); event.preventDefault(); } console.log("Controls toggle clicked"); var controls = d3.select('#controls'); var isCollapsed = controls.classed('collapsed'); controls.classed('collapsed', !isCollapsed); d3.select(this).html(isCollapsed ? '◀' : '▶'); }); // Zoom controls toggle d3.select('#zoomControlsToggle').on('click', function() { var event = d3.event; if (event) { event.stopPropagation(); event.preventDefault(); } console.log("Zoom controls toggle clicked"); var zoomControls = d3.select('#zoomControls'); var isCollapsed = zoomControls.classed('collapsed'); zoomControls.classed('collapsed', !isCollapsed); d3.select(this).html(isCollapsed ? '▶' : '◀'); }); // Legend toggle d3.select('#legendToggle').on('click', function() { var event = d3.event; if (event) { event.stopPropagation(); event.preventDefault(); } console.log("Legend toggle clicked"); var legend = d3.select('#legend'); var isCollapsed = legend.classed('collapsed'); legend.classed('collapsed', !isCollapsed); d3.select(this).html(isCollapsed ? '▼' : '▲'); }); // Instructions toggle d3.select('#instructionsToggle').on('click', function() { var event = d3.event; if (event) { event.stopPropagation(); event.preventDefault(); } console.log("Instructions toggle clicked"); var instructions = d3.select('#instructions'); var isCollapsed = instructions.classed('collapsed'); instructions.classed('collapsed', !isCollapsed); d3.select(this).html(isCollapsed ? '▶' : '◀'); }); // Mobile controls toggle d3.select('#mobileControlsToggle').on('click', function() { var event = d3.event; if (event) { event.stopPropagation(); event.preventDefault(); } console.log("Mobile controls toggle clicked"); toggleMobileControls(); }); // Prevenir propagación de eventos en los paneles de controles d3.select('#controls').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); }); d3.select('#zoomControls').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); }); d3.select('#legend').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); }); d3.select('#instructions').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); }); } // Pantalla completa function setupFullscreen() { var fullscreenBtn = d3.select('#fullscreenBtn'); fullscreenBtn.on('click', function() { var event = d3.event; if (event) event.stopPropagation(); var elem = document.documentElement; if (!document.fullscreenElement) { if (elem.requestFullscreen) { elem.requestFullscreen(); } else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen(); } else if (elem.msRequestFullscreen) { elem.msRequestFullscreen(); } fullscreenBtn.select('span').text('⛶'); fullscreenBtn.select('span + span').text(' Salir de pantalla completa'); } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } fullscreenBtn.select('span').text('⛶'); fullscreenBtn.select('span + span').text(' Pantalla completa'); } }); // Actualizar botón cuando cambia el estado de pantalla completa document.addEventListener('fullscreenchange', updateFullscreenButton); document.addEventListener('webkitfullscreenchange', updateFullscreenButton); document.addEventListener('msfullscreenchange', updateFullscreenButton); function updateFullscreenButton() { if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) { fullscreenBtn.select('span').text('⛶'); fullscreenBtn.select('span + span').text(' Salir de pantalla completa'); } else { fullscreenBtn.select('span').text('⛶'); fullscreenBtn.select('span + span').text(' Pantalla completa'); } } } function initCanvas() { var pixelRatio = window.devicePixelRatio || 1; if (globalThis.screen.availWidth >= 1024) { width = 800; height = 800; } else { width = Math.min(document.documentElement.clientWidth, 800); height = Math.min(document.documentElement.clientWidth, 800); } canvas.attr('width', width * pixelRatio) .attr('height', height * pixelRatio); canvas.style('width', width + 'px') .style('height', height + 'px'); context = canvas.node().getContext('2d'); context.scale(pixelRatio, pixelRatio); updateProjectionScale(); path = d3.geoPath(projection).context(context); return { width: width, height: height }; } function updateProjectionScale() { var effectiveScaleFactor = originalScaleFactor * currentZoom; projection .scale((effectiveScaleFactor * Math.min(width, height)) / 2) .translate([width / 2, height / 2]); } function updateZoomDisplay() { var zoomPercent = Math.round(currentZoom * 100); d3.select('#zoomLevel').text(zoomPercent + '%'); updateProjectionScale(); render(); } function zoomIn() { if (currentZoom < maxZoom) { currentZoom = Math.min(currentZoom + zoomStep, maxZoom); updateZoomDisplay(); } } function zoomOut() { if (currentZoom > minZoom) { currentZoom = Math.max(currentZoom - zoomStep, minZoom); updateZoomDisplay(); } } function resetZoom() { currentZoom = 1.0; updateZoomDisplay(); } function setupZoomControls() { d3.select('#zoomIn').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); zoomIn(); }); d3.select('#zoomOut').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); zoomOut(); }); d3.select('#resetZoom').on('click', function() { var event = d3.event; if (event) event.stopPropagation(); resetZoom(); }); canvas.on('wheel', function() { var event = d3.event; if (event) { event.preventDefault(); var delta = event.deltaY; if (delta < 0) { zoomIn(); } else { zoomOut(); } } }); // Zoom táctil var lastTouchDistance = 0; canvas.on('touchstart', function() { var event = d3.event; if (event && event.touches.length === 2) { lastTouchDistance = getTouchDistance(event.touches); event.preventDefault(); } }); // Corregir evento touchmove canvas.on('touchmove', function() { var event = d3.event; if (event) { event.preventDefault(); mousemove.call(this); } }); function getTouchDistance(touches) { var dx = touches[0].clientX - touches[1].clientX; var dy = touches[0].clientY - touches[1].clientY; return Math.sqrt(dx * dx + dy * dy); } } function setupTooltip() { tooltip.style('position', 'absolute') .style('background', 'rgba(0, 0, 0, 0.85)') .style('color', 'white') .style('padding', '8px 12px') .style('border-radius', '4px') .style('font-family', 'Arial, sans-serif') .style('font-size', '14px') .style('max-width', '250px') .style('z-index', '1000') .style('pointer-events', 'none') .style('opacity', '0') .style('transition', 'opacity 0.2s') .style('box-shadow', '0 2px 10px rgba(0, 0, 0, 0.3)'); } function setupControls() { var selector = d3.select('#dataSelector'); selector.on('change', function() { var event = d3.event; if (event) event.stopPropagation(); selectedDataType = this.value; updateLegend(); render(); }); } function updateLegend() { var legend = d3.select('#legend'); legend.html(''); if (selectedDataType === 'government') { legend.append('h4').text('Tipos de Gobierno'); var config = colorConfig.government; var existingTypes = {}; countryList.forEach(function(country) { var govType = country.government_type; if (govType && govType !== 'null') { if (!existingTypes[govType]) { existingTypes[govType] = true; } } }); Object.keys(existingTypes).sort().forEach(function(type) { var colorKey = Object.keys(config.colors).find(function(key) { return type.includes(key) || key.includes(type); }) || type; var color = config.colors[colorKey] || [200, 200, 200]; var item = legend.append('div').attr('class', 'legend-item'); item.append('div').attr('class', 'legend-color') .style('background-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`); item.append('span').text(type); }); } else { var config = colorConfig[selectedDataType]; // Definir los títulos en español según el tipo de dato var legendTitles = { 'population': 'Población', 'exports': 'Exportaciones' }; var legendTitle = legendTitles[selectedDataType] || selectedDataType.charAt(0).toUpperCase() + selectedDataType.slice(1); legend.append('h4').text(legendTitle); var gradientItem = legend.append('div').attr('class', 'legend-item'); gradientItem.append('div').attr('class', 'legend-color') .style('background', `linear-gradient(to right, rgb(${config.lightColor.join(',')}), rgb(${config.baseColor.join(',')}))`); gradientItem.append('span').text('Bajo a alto'); } } function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function getCountryData(country) { if (!countryList) return null; var data = countryList.find(function(c) { var countryId = parseInt(country.id); var dataId = parseInt(c.id); return (!isNaN(countryId) && !isNaN(dataId) && countryId === dataId) || (country.properties && country.properties.name && c.name && c.name.toLowerCase() === country.properties.name.toLowerCase()); }); // console.log(data) if (!data) { return null; } switch(selectedDataType) { case 'population': var flag = data.flag var popValue = parseInt(data.population) || 0; return { flag: flag, label: 'Población', value: popValue > 0 ? numberWithCommas(popValue) : 'N/A', rawValue: popValue }; case 'exports': var expValue = parseInt(data.exports) || 0; return { label: 'Exportaciones', value: expValue > 0 ? numberWithCommas(expValue) : 'N/A', rawValue: expValue }; case 'government': var govType = data.government_type; if (!govType || govType === 'null' || govType.toLowerCase() === 'null') { govType = 'Desconocido'; } return { label: 'Tipo de Gobierno', value: govType, rawValue: govType }; default: return null; } } function enter(country) { var countryData = getCountryData(country); console.log(countryList) if (countryData) { var countryInfo = countryList.find(function(c) { return parseInt(c.id, 10) === parseInt(country.id, 10); }); if (countryInfo) { // Obtener posición del mouse var mousePos = d3.mouse(canvas.node()); var rect = canvas.node().getBoundingClientRect(); console.log(country) var tooltipContent = `${countryInfo.name_in_spanish} ${countryInfo.flag}
${countryData.label}: ${countryData.value}`; var x = mousePos[0] + 15; var y = mousePos[1] - 15; var tooltipWidth = 200; if (x + tooltipWidth > width) { x = width - tooltipWidth - 10; } if (y < 30) { y = 30; } console.log(countryInfo) tooltip.html(tooltipContent) .style('left', (rect.left + x) + 'px') .style('top', (y) + 'px') .style('opacity', 1); tooltipVisible = true; } } } function leave(country) { if (tooltipVisible) { tooltip.style('opacity', 0); tooltipVisible = false; } } function setAngles() { var rotation = projection.rotate() rotation[0] = angles.y rotation[1] = angles.x rotation[2] = angles.z projection.rotate(rotation) } function getCountryColor(countryData) { if (!countryData) { return 'rgb(200, 200, 200)'; } var config = colorConfig[selectedDataType]; if (selectedDataType === 'government') { var govType = countryData.rawValue; if (!govType || govType === 'Desconocido') { return 'rgb(200, 200, 200)'; } if (config.colors[govType]) { var color = config.colors[govType]; return `rgb(${color[0]}, ${color[1]}, ${color[2]})`; } var matchedKey = Object.keys(config.colors).find(function(key) { return govType.includes(key) || key.includes(govType); }); if (matchedKey) { var color = config.colors[matchedKey]; return `rgb(${color[0]}, ${color[1]}, ${color[2]})`; } return 'rgb(200, 200, 200)'; } else { var maxValue = getMaxValue(); if (maxValue === 0 || countryData.rawValue === 0) { return `rgb(${config.lightColor.join(',')})`; } var factor = countryData.rawValue / maxValue; var color = [ config.lightColor[0] + (factor * (config.baseColor[0] - config.lightColor[0])), config.lightColor[1] + (factor * (config.baseColor[1] - config.lightColor[1])), config.lightColor[2] + (factor * (config.baseColor[2] - config.lightColor[2])) ]; return `rgb(${Math.round(color[0])}, ${Math.round(color[1])}, ${Math.round(color[2])})`; } } function getMaxValue() { if (!countryList) return 0; var values = countryList.map(function(country) { switch(selectedDataType) { case 'population': return parseInt(country.population) || 0; case 'exports': return parseInt(country.exports) || 0; default: return 0; } }).filter(function(val) { return typeof val === 'number' && !isNaN(val); }); return values.length > 0 ? Math.max(...values) : 0; } function render() { if (!context || !path || !countryList || !countries) return; // Limpiar canvas var pixelRatio = window.devicePixelRatio || 1; context.clearRect(0, 0, width * pixelRatio, height * pixelRatio); // Restaurar transformaciones context.save(); context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); // Dibujar agua fill(water, colorWater); // Dibujar graticule stroke(graticule, colorGraticule); // Dibujar países con colores según el tipo de dato seleccionado countries.features.forEach(function(feature) { var countryData = getCountryData(feature); if (countryData && (selectedDataType === 'government' || countryData.rawValue > 0)) { var color = getCountryColor(countryData); fill(feature, color); } else { fill(feature, 'rgb(200, 200, 200)'); } }); // Resaltar país seleccionado if (currentCountry) { fill(currentCountry, 'rgba(255, 255, 255, 0.3)'); } // Dibujar bordes stroke(borders, 'white'); context.restore(); } function fill(obj, color) { context.beginPath(); path(obj); context.fillStyle = color; context.fill(); } function stroke(obj, color) { context.beginPath(); path(obj); context.strokeStyle = color; context.lineWidth = 0.5; context.stroke(); } function rotate(elapsed) { now = d3.now(); diff = now - lastTime; if (diff < elapsed) { rotation = projection.rotate(); rotation[0] += diff * degPerMs; projection.rotate(rotation); render(); } lastTime = now; } function loadData(cb) { d3.json('https://unpkg.com/world-atlas@1/world/110m.json', function(error, world) { if (error) throw error; d3.tsv('https://raw.githubusercontent.com/resquivel0810/resquivel/refs/heads/main/data/world-country-data.tsv', function(error, data) { if (error) throw error; data = data.map(function(d) { var id = parseInt(d.id); return { id: isNaN(id) ? d.id : id, name: d.name, flag: d.flag, name_in_spanish: d.name_in_spanish, population: d.population, exports: d.exports, government_type: d.government_type }; }); cb(world, data); }); }); } function polygonContains(polygon, point) { var n = polygon.length; var p = polygon[n - 1]; var x = point[0], y = point[1]; var x0 = p[0], y0 = p[1]; var x1, y1; var inside = false; for (var i = 0; i < n; ++i) { p = polygon[i], x1 = p[0], y1 = p[1]; if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside; x0 = x1, y0 = y1; } return inside; } function mousemove() { var event = d3.event; var c = getCountry(this); if (!c) { if (currentCountry) { leave(currentCountry); currentCountry = undefined; render(); } return; } if (c === currentCountry) { if (tooltipVisible) { var mousePos = d3.mouse(canvas.node()); var rect = canvas.node().getBoundingClientRect(); tooltip.style('left', (rect.left + mousePos[0] + 15) + 'px') .style('top', (mousePos[1]) + 'px'); } return; } currentCountry = c; render(); enter(c); } function getCountry(event) { var pos = projection.invert(d3.mouse(event)); return countries.features.find(function(f) { return f.geometry.coordinates.find(function(c1) { return polygonContains(c1, pos) || c1.find(function(c2) { return polygonContains(c2, pos); }); }); }); } function dragstarted() { var event = d3.event; v0 = versor.cartesian(projection.invert(d3.mouse(this))) r0 = projection.rotate() q0 = versor(r0) stopRotation() // Cambiar cursor canvas.classed('dragging', true); } function dragged() { var event = d3.event; var v1 = versor.cartesian(projection.rotate(r0).invert(d3.mouse(this))) var q1 = versor.multiply(q0, versor.delta(v0, v1)) var r1 = versor.rotation(q1) projection.rotate(r1) render() if (tooltipVisible && currentCountry) { var mousePos = d3.mouse(canvas.node()); var rect = canvas.node().getBoundingClientRect(); tooltip.style('left', (rect.left + mousePos[0] + 15) + 'px') .style('top', (mousePos[1]) + 'px'); } } function dragended() { // Restaurar cursor canvas.classed('dragging', false); startRotation(rotationDelay) } function startRotation(delay) { if (autorotate) { autorotate.restart(rotate, delay || 0); } } function stopRotation() { if (autorotate) { autorotate.stop(); } } // Inicializar function init() { // Detectar si es móvil primero checkMobile(); // Setup inicial setAngles(); setupTooltip(); setupControls(); setupZoomControls(); setupCollapsibleControls(); setupFullscreen(); // Configurar canvas y eventos canvas .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended) ) .on('mousemove', mousemove); canvas.on('touchmove', function(event) { event.preventDefault(); mousemove.call(this); }); // Cargar datos loadData(function(world, data) { borders = topojson.mesh(world, world.objects.countries, (a, b) => a !== b); countries = topojson.feature(world, world.objects.countries); countryList = data; land = topojson.feature(world, world.objects.land); initCanvas(); updateLegend(); render(); autorotate = d3.timer(rotate); // Redimensionar window.addEventListener('resize', function() { checkMobile(); initCanvas(); render(); }); // Ocultar controles automáticamente después de 5 segundos en móvil if (isMobile) { setTimeout(function() { if (!controlsVisible) { collapseAllControls(); } }, 5000); } console.log("Aplicación inicializada correctamente"); }); } // Verificar si hay errores en la consola window.addEventListener('error', function(event) { console.error('Error en la aplicación:', event.error); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }