Spaces:
Running
Running
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Cartograf铆a de anotaci贸n</title> | |
<!-- Use the full D3 library to avoid dependency issues --> | |
<script src="https://d3js.org/d3.v7.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
padding: 20px; | |
background-color: #0f1218; | |
color: #fff; | |
font-family: system-ui, -apple-system, sans-serif; | |
} | |
h1 { | |
margin-bottom: 20px; | |
} | |
.container { | |
display: flex; | |
width: 100%; | |
} | |
.map-container { | |
flex: 3; | |
height: 600px; | |
position: relative; | |
background-color: #0f1218; | |
} | |
.stats-container { | |
flex: 1; | |
padding: 20px; | |
background-color: #161b22; | |
border-radius: 8px; | |
margin-right: 20px; | |
} | |
#tooltip { | |
position: absolute; | |
background-color: rgba(0, 0, 0, 0.8); | |
border-radius: 5px; | |
padding: 8px; | |
color: white; | |
font-size: 12px; | |
pointer-events: none; | |
opacity: 0; | |
transition: opacity 0.3s; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
z-index: 1000; | |
will-change: transform; /* Optimize for transformations */ | |
} | |
.country { | |
cursor: pointer; | |
transition: opacity 0.3s; | |
} | |
.country:hover { | |
opacity: 0.8; | |
} | |
.stat-title { | |
font-size: 1.2rem; | |
margin-bottom: 20px; | |
font-weight: bold; | |
} | |
.stat-item { | |
margin-bottom: 10px; | |
color: #abb4c2; | |
} | |
.stat-value { | |
font-weight: bold; | |
color: white; | |
} | |
.stat-bar-container { | |
width: 100%; | |
height: 8px; | |
background-color: #30363d; | |
border-radius: 4px; | |
margin-top: 5px; | |
overflow: hidden; | |
} | |
.stat-bar { | |
height: 100%; | |
background: linear-gradient(to right, #4a1942, #f32b7b); | |
border-radius: 4px; | |
} | |
.top-countries { | |
margin-top: 30px; | |
} | |
.country-stat { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 8px; | |
align-items: center; | |
font-size: 14px; | |
} | |
.country-bar { | |
flex: 1; | |
height: 6px; | |
background-color: #30363d; | |
border-radius: 3px; | |
overflow: hidden; | |
margin: 0 10px; | |
} | |
.country-bar-fill { | |
height: 100%; | |
background: linear-gradient(to right, #4a1942, #f32b7b); | |
border-radius: 3px; | |
} | |
.country-value { | |
width: 80px; | |
text-align: right; | |
} | |
.legend { | |
margin-top: 20px; | |
} | |
.footer-note { | |
margin-top: 30px; | |
font-style: italic; | |
font-size: 0.9em; | |
color: #abb4c2; | |
} | |
/* Add loading indicator */ | |
.loading { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
color: white; | |
font-size: 18px; | |
background: rgba(0,0,0,0.5); | |
padding: 10px 20px; | |
border-radius: 5px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="stats-container"> | |
<div class="stat-title">Resumen General</div> | |
<div class="stat-item"> | |
Pa铆ses en la base de datos:<br> | |
<span class="stat-value">20</span> | |
</div> | |
<div class="stat-item"> | |
Total de respuestas:<br> | |
<span class="stat-value" id="total-docs">0</span> | |
</div> | |
<div class="stat-item"> | |
Promedio de completitud:<br> | |
<span class="stat-value" id="avg-percent">0%</span> | |
</div> | |
<div class="top-countries"> | |
<div class="stat-item">Los 5 pa铆ses con mayor recolecci贸n:</div> | |
<div id="top-countries-list"> | |
<!-- Will be populated by JavaScript --> | |
</div> | |
</div> | |
<div class="footer-note"> | |
Selecciona un pa铆s en el mapa para ver informaci贸n detallada. | |
</div> | |
</div> | |
<div class="map-container" id="map-container"> | |
<div class="loading" id="loading-indicator">Cargando mapa...</div> | |
</div> | |
</div> | |
<div id="tooltip"></div> | |
<script> | |
// Country data from Python - will be replaced | |
const countryData = COUNTRY_DATA_PLACEHOLDER; | |
// Pre-calculate statistics once at page load | |
const calculateStats = () => { | |
// Calculate total documents | |
const totalDocs = Object.values(countryData).reduce((sum, country) => { | |
console.log(country); | |
return sum + (country.total_answers || 0); | |
}, 0); | |
// Calculate average percentage | |
const avgPercent = Object.values(countryData).reduce((sum, country) => { | |
return sum + country.percent; | |
}, 0) / Object.values(countryData).length; | |
// Create an array of countries with document counts | |
const countriesWithDocs = Object.keys(countryData).map(code => { | |
return { | |
code: code, | |
name: countryData[code].name, | |
percent: countryData[code].percent, | |
documents: countryData[code].documents || 0, | |
total_answers: countryData[code].total_answers || 0, | |
answered_questions: countryData[code].answered_questions || 0 | |
}; | |
}); | |
// Sort by document count descending | |
countriesWithDocs.sort((a, b) => b.answered_questions - a.answered_questions); | |
// Take the top 5 | |
const topCountries = countriesWithDocs.slice(0, 5); | |
return { | |
totalDocs, | |
avgPercent, | |
topCountries | |
}; | |
}; | |
// Store the calculated stats | |
const stats = calculateStats(); | |
// Function to update the DOM with the statistics | |
function updateStatistics() { | |
// Update the stats | |
document.getElementById('total-docs').textContent = stats.totalDocs.toLocaleString(); | |
document.getElementById('avg-percent').textContent = stats.avgPercent.toFixed(1) + '%'; | |
// Update the top countries list - use document fragment for performance | |
const topCountriesList = document.getElementById('top-countries-list'); | |
const fragment = document.createDocumentFragment(); | |
stats.topCountries.forEach(country => { | |
const countryDiv = document.createElement('div'); | |
countryDiv.className = 'country-stat'; | |
countryDiv.innerHTML = ` | |
<span>${country.name}</span> | |
<div class="country-bar"> | |
<div class="country-bar-fill" style="width: ${country.percent}%;"></div> | |
</div> | |
<span class="country-value">${country.answered_questions.toLocaleString()}</span> | |
`; | |
fragment.appendChild(countryDiv); | |
}); | |
topCountriesList.innerHTML = ''; | |
topCountriesList.appendChild(fragment); | |
} | |
// Optimized function to show tooltip | |
function createTooltipHandler() { | |
const tooltip = d3.select('#tooltip'); | |
return function(event, iso) { | |
// Update the current country style | |
d3.select(event.currentTarget) | |
.attr('stroke', '#fff') | |
.attr('stroke-width', 1.5); | |
// Get country data | |
const country = countryData[iso]; | |
// Update tooltip content and position | |
tooltip.style('opacity', 1) | |
.style('left', (event.pageX + 15) + 'px') | |
.style('top', (event.pageY + 15) + 'px') | |
.html(`<strong>${country.name}</strong><br/>` + | |
`Preguntas totales: ${country.total_questions}<br/>` + | |
`Preguntas respondidas: ${country.answered_questions}<br/>` + | |
`Progreso: ${country.percent}%`); | |
}; | |
} | |
// Add the legend to the map | |
function addLegend(svg, width) { | |
const legendWidth = 200; | |
const legendHeight = 15; | |
const legendX = width - legendWidth - 20; | |
const legendY = 20; | |
// Create legend group | |
const legend = svg.append('g') | |
.attr('class', 'legend') | |
.attr('transform', 'translate(' + legendX + ',' + legendY + ')'); | |
// Legend title | |
legend.append('text') | |
.attr('x', legendWidth / 2) | |
.attr('y', -5) | |
.attr('text-anchor', 'middle') | |
.style('fill', '#fff') | |
.style('font-size', '12px') | |
.text('Porcentaje de Datos Recolectado'); | |
// Create gradient for legend | |
const defs = svg.append('defs'); | |
const gradient = defs.append('linearGradient') | |
.attr('id', 'legendGradient') | |
.attr('x1', '0%') | |
.attr('x2', '100%') | |
.attr('y1', '0%') | |
.attr('y2', '0%'); | |
gradient.append('stop') | |
.attr('offset', '0%') | |
.attr('stop-color', '#4a1942'); | |
gradient.append('stop') | |
.attr('offset', '100%') | |
.attr('stop-color', '#f32b7b'); | |
// Add legend rectangle | |
legend.append('rect') | |
.attr('width', legendWidth) | |
.attr('height', legendHeight) | |
.style('fill', 'url(#legendGradient)') | |
.style('stroke', 'none'); | |
// Add min and max labels | |
legend.append('text') | |
.attr('x', 0) | |
.attr('y', legendHeight + 15) | |
.attr('text-anchor', 'start') | |
.style('fill', '#fff') | |
.style('font-size', '12px') | |
.text('0%'); | |
legend.append('text') | |
.attr('x', legendWidth / 2) | |
.attr('y', legendHeight + 15) | |
.attr('text-anchor', 'middle') | |
.style('fill', '#fff') | |
.style('font-size', '12px') | |
.text('50%'); | |
legend.append('text') | |
.attr('x', legendWidth) | |
.attr('y', legendHeight + 15) | |
.attr('text-anchor', 'end') | |
.style('fill', '#fff') | |
.style('font-size', '12px') | |
.text('100%'); | |
} | |
// Main function to render the map | |
function initMap() { | |
// Cache the relevant country codes for filtering | |
const relevantCountryCodes = Object.keys(countryData); | |
// Update dimensions with container size | |
const container = document.getElementById('map-container'); | |
const width = container.clientWidth; | |
const height = container.clientHeight; | |
// Set up the SVG | |
const svg = d3.select('#map-container') | |
.append('svg') | |
.attr('width', width) | |
.attr('height', height); | |
// Add ocean background first (layering matters) | |
svg.append('rect') | |
.attr('width', width) | |
.attr('height', height) | |
.attr('fill', '#0f1218'); | |
// Define the color scale | |
const colorScale = d3.scaleLinear() | |
.domain([0, 100]) | |
.range(['#4a1942', '#f32b7b']); | |
// Set up the projection | |
const projection = d3.geoMercator() | |
.center([-60, -15]) // Centered on South America | |
.scale(width / 4) | |
.translate([width / 2, height / 2]); | |
const path = d3.geoPath().projection(projection); | |
// Add the legend before loading data | |
addLegend(svg, width); | |
// Create the tooltip handler | |
const showTooltip = createTooltipHandler(); | |
const tooltip = d3.select('#tooltip'); | |
// Store reference for resize handler | |
const mapState = { svg, projection, path }; | |
// Load GeoJSON data with a smaller, faster source if possible | |
d3.json('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson') | |
.then(function(data) { | |
// Hide loading indicator | |
document.getElementById('loading-indicator').style.display = 'none'; | |
// Filter only relevant countries for faster rendering | |
const relevantFeatures = data.features.filter(d => | |
relevantCountryCodes.includes(d.id) | |
); | |
// Add country paths | |
svg.selectAll('.country') | |
.data(relevantFeatures) | |
.enter() | |
.append('path') | |
.attr('class', 'country') | |
.attr('d', path) | |
.attr('fill', d => colorScale(countryData[d.id].percent)) | |
.attr('stroke', '#0f1218') | |
.attr('stroke-width', 1) | |
.on('mouseover', function(event, d) { | |
showTooltip(event, d.id); | |
}) | |
.on('mousemove', function(event) { | |
tooltip | |
.style('left', (event.pageX + 15) + 'px') | |
.style('top', (event.pageY + 15) + 'px'); | |
}) | |
.on('mouseout', function() { | |
d3.select(this) | |
.attr('stroke', '#0f1218') | |
.attr('stroke-width', 1); | |
tooltip.style('opacity', 0); | |
}); | |
}) | |
.catch(function(error) { | |
console.error('Error loading map data:', error); | |
document.getElementById('loading-indicator').style.display = 'none'; | |
container.innerHTML = '<div style="color: white; text-align: center; padding: 20px;">Error loading map: ' + error.message + '</div>'; | |
}); | |
return mapState; | |
} | |
// Debounced resize handler | |
function setupResizeHandler(mapState) { | |
let resizeTimer; | |
window.addEventListener('resize', function() { | |
clearTimeout(resizeTimer); | |
resizeTimer = setTimeout(function() { | |
const container = document.getElementById('map-container'); | |
const width = container.clientWidth; | |
const height = container.clientHeight; | |
// Update SVG dimensions | |
mapState.svg | |
.attr('width', width) | |
.attr('height', height); | |
// Update projection | |
mapState.projection | |
.scale(width / 4) | |
.translate([width / 2, height / 2]); | |
// Update paths | |
d3.selectAll('.country').attr('d', mapState.path); | |
// Update legend position | |
const legendX = width - 220; | |
d3.select('.legend') | |
.attr('transform', 'translate(' + legendX + ',20)'); | |
}, 250); // Debounce 250ms | |
}); | |
} | |
// Initialize on page load | |
document.addEventListener('DOMContentLoaded', function() { | |
// Update the statistics first | |
updateStatistics(); | |
// Initialize the map | |
const mapState = initMap(); | |
// Setup resize handler | |
setupResizeHandler(mapState); | |
}); | |
</script> | |
</body> | |
</html> |