// 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();
}