|
import gradio as gr |
|
import random |
|
import json |
|
import fastapi |
|
from fastapi import FastAPI |
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
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 = """ |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Latin America & Spain Map</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> |
|
<h1>Latin America & Spain Progress Map</h1> |
|
|
|
<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> |
|
""" |
|
|
|
|
|
@app.get("/d3-map") |
|
async def serve_map(): |
|
|
|
country_data = generate_data() |
|
|
|
|
|
for code in country_data: |
|
country_data[code]["documents"] = random.randint(300000, 700000) |
|
|
|
|
|
country_data_json = json.dumps(country_data) |
|
|
|
|
|
html_content = HTML_TEMPLATE.replace("COUNTRY_DATA_PLACEHOLDER", country_data_json) |
|
|
|
return fastapi.responses.HTMLResponse(content=html_content) |
|
|
|
|
|
def create_iframe(): |
|
|
|
random_param = random.randint(1, 10000) |
|
return '<iframe src="/d3-map?t={}" style="width:100%; height:650px; border:none;"></iframe>'.format(random_param) |
|
|
|
|
|
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()) |
|
|
|
|
|
def refresh(): |
|
return create_iframe() |
|
|
|
gr.Button("Generate New Data").click(fn=refresh, outputs=iframe_output) |
|
|
|
|
|
gr.mount_gradio_app(app, demo, path="/") |
|
|
|
|
|
if __name__ == "__main__": |
|
import uvicorn |
|
uvicorn.run(app, host="0.0.0.0", port=7860) |