Spaces:
Sleeping
Sleeping
import gradio as gr | |
import random | |
import json | |
import fastapi | |
from fastapi import FastAPI | |
# Create a FastAPI app | |
app = FastAPI() | |
# Sample country data with random progress percentages - using the proper ISO codes | |
def generate_data(): | |
return { | |
"MEX": {"name": "Mexico", "percent": random.randint(10, 90)}, | |
"ARG": {"name": "Argentina", "percent": random.randint(10, 90)}, | |
"COL": {"name": "Colombia", "percent": random.randint(10, 90)}, | |
"CHL": {"name": "Chile", "percent": random.randint(10, 90)}, | |
"PER": {"name": "Peru", "percent": random.randint(10, 90)}, | |
"ESP": {"name": "Spain", "percent": random.randint(10, 90)}, | |
"BRA": {"name": "Brazil", "percent": random.randint(10, 90)}, | |
"VEN": {"name": "Venezuela", "percent": random.randint(10, 90)}, | |
"ECU": {"name": "Ecuador", "percent": random.randint(10, 90)}, | |
"BOL": {"name": "Bolivia", "percent": random.randint(10, 90)}, | |
"PRY": {"name": "Paraguay", "percent": random.randint(10, 90)}, | |
"URY": {"name": "Uruguay", "percent": random.randint(10, 90)}, | |
"CRI": {"name": "Costa Rica", "percent": random.randint(10, 90)}, | |
"PAN": {"name": "Panama", "percent": random.randint(10, 90)}, | |
"DOM": {"name": "Dominican Republic", "percent": random.randint(10, 90)}, | |
"GTM": {"name": "Guatemala", "percent": random.randint(10, 90)}, | |
"HND": {"name": "Honduras", "percent": random.randint(10, 90)}, | |
"SLV": {"name": "El Salvador", "percent": random.randint(10, 90)}, | |
"NIC": {"name": "Nicaragua", "percent": random.randint(10, 90)}, | |
"CUB": {"name": "Cuba", "percent": random.randint(10, 90)} | |
} | |
# HTML template - avoiding f-strings with JavaScript template literals | |
HTML_TEMPLATE = """ | |
<!DOCTYPE html> | |
<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); | |
// Log some sample features to check the properties structure | |
const sampleFeatures = data.features.slice(0, 3); | |
console.log('Sample feature properties:', sampleFeatures.map(f => f.properties)); | |
// 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); | |
// If we still don't have matches, look more deeply at the data structure | |
if (relevantFeatures.length === 0) { | |
console.log('No matches found using id, checking all feature properties'); | |
// Get the first 10 features to examine their structure | |
const firstTen = data.features.slice(0, 10); | |
console.log('First ten features:', firstTen); | |
// Look for our countries in all features | |
const latinAmericanFeatures = data.features.filter(f => { | |
// Check various properties that might contain the country code | |
return relevantCountryCodes.includes(f.id) || | |
(f.properties && relevantCountryCodes.includes(f.properties.iso_a3)) || | |
(f.properties && relevantCountryCodes.includes(f.properties.name)); | |
}); | |
console.log('Latin American features found:', latinAmericanFeatures.length); | |
// If there are still no matches, just use all features and filter visually | |
if (latinAmericanFeatures.length === 0) { | |
console.log('Still no matches, using all features and hiding non-Latin American countries'); | |
// Draw all countries but only color our target ones | |
svg.selectAll('.country') | |
.data(data.features) | |
.enter() | |
.append('path') | |
.attr('class', 'country') | |
.attr('d', path) | |
.attr('fill', function(d) { | |
// Try to match with id or iso_a3 | |
if (d.id && countryData[d.id]) { | |
return colorScale(countryData[d.id].percent); | |
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) { | |
return colorScale(countryData[d.properties.iso_a3].percent); | |
} else if (d.properties && d.properties.name && countryData[d.properties.name]) { | |
return colorScale(countryData[d.properties.name].percent); | |
} else { | |
// Try checking if it's a Latin American country by name | |
const latinAmericanCountries = [ | |
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain', | |
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay', | |
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic', | |
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba' | |
]; | |
if (d.properties && latinAmericanCountries.includes(d.properties.name)) { | |
// Find the matching country in our data | |
for (const code in countryData) { | |
if (countryData[code].name === d.properties.name) { | |
return colorScale(countryData[code].percent); | |
} | |
} | |
} | |
// If not a target country, make it transparent | |
return 'transparent'; | |
} | |
}) | |
.attr('stroke', function(d) { | |
// Only show outlines for Latin American countries | |
const latinAmericanCountries = [ | |
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain', | |
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay', | |
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic', | |
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba' | |
]; | |
if (d.properties && latinAmericanCountries.includes(d.properties.name)) { | |
return '#0f1218'; | |
} else { | |
return 'transparent'; | |
} | |
}) | |
.attr('stroke-width', 1) | |
.on('mouseover', function(event, d) { | |
// Only enable hover for target countries | |
if (d.id && countryData[d.id]) { | |
const iso = d.id; | |
showTooltip(event, iso); | |
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) { | |
const iso = d.properties.iso_a3; | |
showTooltip(event, iso); | |
} else if (d.properties && d.properties.name) { | |
// Find the matching country in our data by name | |
for (const code in countryData) { | |
if (countryData[code].name === d.properties.name) { | |
showTooltip(event, code); | |
break; | |
} | |
} | |
} | |
}) | |
.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', function(d) { | |
const latinAmericanCountries = [ | |
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain', | |
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay', | |
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic', | |
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba' | |
]; | |
if (d.properties && latinAmericanCountries.includes(d.properties.name)) { | |
return '#0f1218'; | |
} else { | |
return 'transparent'; | |
} | |
}) | |
.attr('stroke-width', 1); | |
tooltip.style('opacity', 0); | |
}); | |
} else { | |
// Draw only the Latin American features | |
svg.selectAll('.country') | |
.data(latinAmericanFeatures) | |
.enter() | |
.append('path') | |
.attr('class', 'country') | |
.attr('d', path) | |
.attr('fill', function(d) { | |
if (d.id && countryData[d.id]) { | |
return colorScale(countryData[d.id].percent); | |
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) { | |
return colorScale(countryData[d.properties.iso_a3].percent); | |
} else { | |
return colorScale(50); // Default to mid-range if no match | |
} | |
}) | |
.attr('stroke', '#0f1218') | |
.attr('stroke-width', 1) | |
.on('mouseover', function(event, d) { | |
if (d.id && countryData[d.id]) { | |
const iso = d.id; | |
showTooltip(event, iso); | |
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) { | |
const iso = d.properties.iso_a3; | |
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); | |
}); | |
} | |
} else { | |
// 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/>' + | |
'Progress: ' + 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'); | |
// Add random document counts to countries that don't have them | |
Object.keys(countryData).forEach(code => { | |
if (!countryData[code].documents) { | |
countryData[code].documents = Math.floor(Math.random() * 300000) + 300000; | |
} | |
}); | |
// 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> | |
""" | |
# Route to serve the map visualization | |
async def serve_map(): | |
# Generate random data for each country with document counts | |
country_data = generate_data() | |
# Add random document counts | |
for code in country_data: | |
country_data[code]["documents"] = random.randint(300000, 700000) | |
# Convert to JSON for JavaScript | |
country_data_json = json.dumps(country_data) | |
# Replace the placeholder with actual data | |
html_content = HTML_TEMPLATE.replace("COUNTRY_DATA_PLACEHOLDER", country_data_json) | |
return fastapi.responses.HTMLResponse(content=html_content) | |
# Create a simple Gradio interface with an iframe | |
def create_iframe(): | |
# Add a random parameter to force reload | |
random_param = random.randint(1, 10000) | |
return '<iframe src="/d3-map?t={}" style="width:100%; height:650px; border:none;"></iframe>'.format(random_param) | |
# Create the Gradio blocks | |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="pink", secondary_hue="purple")) as demo: | |
gr.Markdown("# Latin America & Spain Progress Map") | |
iframe_output = gr.HTML(create_iframe()) | |
# Refresh button to generate new random data | |
def refresh(): | |
return create_iframe() | |
gr.Button("Generate New Data").click(fn=refresh, outputs=iframe_output) | |
# Mount the Gradio app to the FastAPI app | |
gr.mount_gradio_app(app, demo, path="/") | |
# Start the server | |
if __name__ == "__main__": | |
import uvicorn | |
uvicorn.run(app, host="0.0.0.0", port=7860) |