Spaces:
Runtime error
Runtime error
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Cartografía de anotación</title> | |
| <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; | |
| } | |
| .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; | |
| } | |
| </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 documentos:<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> | |
| </div> | |
| <div id="tooltip"></div> | |
| <script> | |
| // Country data from Python - will be replaced | |
| const countryData = COUNTRY_DATA_PLACEHOLDER; | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('Document loaded, initializing map...'); | |
| // Set up dimensions | |
| const container = document.getElementById('map-container'); | |
| const width = container.clientWidth; | |
| const height = container.clientHeight; | |
| console.log('Container dimensions:', width, height); | |
| // Create SVG | |
| const svg = d3.select('#map-container') | |
| .append('svg') | |
| .attr('width', width) | |
| .attr('height', height); | |
| console.log('SVG created'); | |
| // Create color scale | |
| const colorScale = d3.scaleLinear() | |
| .domain([0, 100]) | |
| .range(['#4a1942', '#f32b7b']); | |
| // Set up projection with specific focus | |
| const projection = d3.geoMercator() | |
| .center([-60, -15]) // Centered on South America | |
| .scale(width / 3) | |
| .translate([width / 2, height / 2]); | |
| const path = d3.geoPath().projection(projection); | |
| // Tooltip setup | |
| const tooltip = d3.select('#tooltip'); | |
| console.log('Loading GeoJSON data...'); | |
| // Load GeoJSON data | |
| d3.json('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson') | |
| .then(function(data) { | |
| console.log('GeoJSON data loaded'); | |
| // The relevant country codes | |
| const relevantCountryCodes = Object.keys(countryData); | |
| // Log countries in data | |
| console.log('Countries in countryData:', relevantCountryCodes); | |
| // Add ocean background | |
| svg.append('rect') | |
| .attr('width', width) | |
| .attr('height', height) | |
| .attr('fill', '#0f1218'); | |
| // Filter the features - check for id match | |
| const relevantFeatures = data.features.filter(d => | |
| relevantCountryCodes.includes(d.id) | |
| ); | |
| console.log('Filtered features count:', relevantFeatures.length); | |
| // Draw only our target countries | |
| svg.selectAll('.country') | |
| .data(relevantFeatures) | |
| .enter() | |
| .append('path') | |
| .attr('class', 'country') | |
| .attr('d', path) | |
| .attr('fill', function(d) { | |
| const iso = d.id; | |
| return colorScale(countryData[iso].percent); | |
| }) | |
| .attr('stroke', '#0f1218') | |
| .attr('stroke-width', 1) | |
| .on('mouseover', function(event, d) { | |
| const iso = d.id; | |
| showTooltip(event, iso); | |
| }) | |
| .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); | |
| }); | |
| // Function to show tooltip | |
| function showTooltip(event, iso) { | |
| d3.select(event.currentTarget) | |
| .attr('stroke', '#fff') | |
| .attr('stroke-width', 1.5); | |
| tooltip.style('opacity', 1) | |
| .style('left', (event.pageX + 15) + 'px') | |
| .style('top', (event.pageY + 15) + 'px') | |
| .html(`<strong>${countryData[iso].name}</strong><br/>` + | |
| `Preguntas totales: ${countryData[iso].total_questions}<br/>` + | |
| `Preguntas respondidas: ${countryData[iso].answered_questions}<br/>` + | |
| `Progreso: ${countryData[iso].percent}%`); | |
| } | |
| // Add a legend on the right side of the map | |
| const legendWidth = 200; | |
| const legendHeight = 15; | |
| const legendX = width - legendWidth - 20; | |
| const legendY = 20; | |
| // Create legend group | |
| const legend = svg.append('g') | |
| .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%'); | |
| // Update statistics | |
| updateStatistics(); | |
| }) | |
| .catch(function(error) { | |
| console.error('Error loading or rendering the map:', error); | |
| container.innerHTML = '<div style="color: white; text-align: center; padding: 20px;">Error loading map: ' + error.message + '</div>'; | |
| }); | |
| // Function to update statistics | |
| function updateStatistics() { | |
| console.log('Updating statistics'); | |
| // Calculate total documents | |
| const totalDocs = Object.values(countryData).reduce((sum, country) => { | |
| return sum + (country.documents || 0); | |
| }, 0); | |
| // Calculate average percentage | |
| const avgPercent = Object.values(countryData).reduce((sum, country) => { | |
| return sum + country.percent; | |
| }, 0) / Object.values(countryData).length; | |
| // Update the stats | |
| document.getElementById('total-docs').textContent = totalDocs.toLocaleString(); | |
| document.getElementById('avg-percent').textContent = avgPercent.toFixed(1) + '%'; | |
| // 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 | |
| }; | |
| }); | |
| // Sort by document count descending | |
| countriesWithDocs.sort((a, b) => b.documents - a.documents); | |
| // Take the top 5 | |
| const topCountries = countriesWithDocs.slice(0, 5); | |
| // Update the top countries list | |
| const topCountriesList = document.getElementById('top-countries-list'); | |
| topCountriesList.innerHTML = ''; | |
| 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.documents.toLocaleString()}</span> | |
| `; | |
| topCountriesList.appendChild(countryDiv); | |
| }); | |
| console.log('Statistics updated'); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', function() { | |
| console.log('Window resized'); | |
| const width = container.clientWidth; | |
| const height = container.clientHeight; | |
| // Update SVG dimensions | |
| d3.select('svg') | |
| .attr('width', width) | |
| .attr('height', height); | |
| // Update projection | |
| projection.scale(width / 3) | |
| .translate([width / 2, height / 2]); | |
| // Update paths | |
| d3.selectAll('path').attr('d', path); | |
| // Update legend position | |
| const legendX = width - 220; | |
| d3.select('.legend') | |
| .attr('transform', 'translate(' + legendX + ',20)'); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |