mapa-blend-es / template.txt
ouhenio's picture
Update template.txt
15ffb5b verified
<!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>