Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -3,647 +3,116 @@ import random
|
|
3 |
import json
|
4 |
import fastapi
|
5 |
from fastapi import FastAPI
|
|
|
|
|
6 |
|
7 |
-
#
|
8 |
-
|
|
|
|
|
|
|
9 |
|
10 |
-
#
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
"CHL": {"name": "Chile", "percent": random.randint(10, 90)},
|
17 |
-
"PER": {"name": "Peru", "percent": random.randint(10, 90)},
|
18 |
-
"ESP": {"name": "Spain", "percent": random.randint(10, 90)},
|
19 |
-
"BRA": {"name": "Brazil", "percent": random.randint(10, 90)},
|
20 |
-
"VEN": {"name": "Venezuela", "percent": random.randint(10, 90)},
|
21 |
-
"ECU": {"name": "Ecuador", "percent": random.randint(10, 90)},
|
22 |
-
"BOL": {"name": "Bolivia", "percent": random.randint(10, 90)},
|
23 |
-
"PRY": {"name": "Paraguay", "percent": random.randint(10, 90)},
|
24 |
-
"URY": {"name": "Uruguay", "percent": random.randint(10, 90)},
|
25 |
-
"CRI": {"name": "Costa Rica", "percent": random.randint(10, 90)},
|
26 |
-
"PAN": {"name": "Panama", "percent": random.randint(10, 90)},
|
27 |
-
"DOM": {"name": "Dominican Republic", "percent": random.randint(10, 90)},
|
28 |
-
"GTM": {"name": "Guatemala", "percent": random.randint(10, 90)},
|
29 |
-
"HND": {"name": "Honduras", "percent": random.randint(10, 90)},
|
30 |
-
"SLV": {"name": "El Salvador", "percent": random.randint(10, 90)},
|
31 |
-
"NIC": {"name": "Nicaragua", "percent": random.randint(10, 90)},
|
32 |
-
"CUB": {"name": "Cuba", "percent": random.randint(10, 90)}
|
33 |
-
}
|
34 |
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
<html>
|
39 |
-
<head>
|
40 |
-
<meta charset="utf-8">
|
41 |
-
<title>Cartograf铆a de anotaci贸n</title>
|
42 |
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
43 |
-
<style>
|
44 |
-
body {
|
45 |
-
margin: 0;
|
46 |
-
padding: 20px;
|
47 |
-
background-color: #0f1218;
|
48 |
-
color: #fff;
|
49 |
-
font-family: system-ui, -apple-system, sans-serif;
|
50 |
-
}
|
51 |
-
h1 {
|
52 |
-
margin-bottom: 20px;
|
53 |
-
}
|
54 |
-
.container {
|
55 |
-
display: flex;
|
56 |
-
width: 100%;
|
57 |
-
}
|
58 |
-
.map-container {
|
59 |
-
flex: 3;
|
60 |
-
height: 600px;
|
61 |
-
position: relative;
|
62 |
-
background-color: #0f1218;
|
63 |
-
}
|
64 |
-
.stats-container {
|
65 |
-
flex: 1;
|
66 |
-
padding: 20px;
|
67 |
-
background-color: #161b22;
|
68 |
-
border-radius: 8px;
|
69 |
-
margin-right: 20px;
|
70 |
-
}
|
71 |
-
#tooltip {
|
72 |
-
position: absolute;
|
73 |
-
background-color: rgba(0, 0, 0, 0.8);
|
74 |
-
border-radius: 5px;
|
75 |
-
padding: 8px;
|
76 |
-
color: white;
|
77 |
-
font-size: 12px;
|
78 |
-
pointer-events: none;
|
79 |
-
opacity: 0;
|
80 |
-
transition: opacity 0.3s;
|
81 |
-
border: 1px solid rgba(255, 255, 255, 0.2);
|
82 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
83 |
-
z-index: 1000;
|
84 |
-
}
|
85 |
-
.country {
|
86 |
-
cursor: pointer;
|
87 |
-
transition: opacity 0.3s;
|
88 |
-
}
|
89 |
-
.country:hover {
|
90 |
-
opacity: 0.8;
|
91 |
-
}
|
92 |
-
.stat-title {
|
93 |
-
font-size: 1.2rem;
|
94 |
-
margin-bottom: 20px;
|
95 |
-
font-weight: bold;
|
96 |
-
}
|
97 |
-
.stat-item {
|
98 |
-
margin-bottom: 10px;
|
99 |
-
color: #abb4c2;
|
100 |
-
}
|
101 |
-
.stat-value {
|
102 |
-
font-weight: bold;
|
103 |
-
color: white;
|
104 |
-
}
|
105 |
-
.stat-bar-container {
|
106 |
-
width: 100%;
|
107 |
-
height: 8px;
|
108 |
-
background-color: #30363d;
|
109 |
-
border-radius: 4px;
|
110 |
-
margin-top: 5px;
|
111 |
-
overflow: hidden;
|
112 |
-
}
|
113 |
-
.stat-bar {
|
114 |
-
height: 100%;
|
115 |
-
background: linear-gradient(to right, #4a1942, #f32b7b);
|
116 |
-
border-radius: 4px;
|
117 |
-
}
|
118 |
-
.top-countries {
|
119 |
-
margin-top: 30px;
|
120 |
-
}
|
121 |
-
.country-stat {
|
122 |
-
display: flex;
|
123 |
-
justify-content: space-between;
|
124 |
-
margin-bottom: 8px;
|
125 |
-
align-items: center;
|
126 |
-
font-size: 14px;
|
127 |
-
}
|
128 |
-
.country-bar {
|
129 |
-
flex: 1;
|
130 |
-
height: 6px;
|
131 |
-
background-color: #30363d;
|
132 |
-
border-radius: 3px;
|
133 |
-
overflow: hidden;
|
134 |
-
margin: 0 10px;
|
135 |
-
}
|
136 |
-
.country-bar-fill {
|
137 |
-
height: 100%;
|
138 |
-
background: linear-gradient(to right, #4a1942, #f32b7b);
|
139 |
-
border-radius: 3px;
|
140 |
-
}
|
141 |
-
.country-value {
|
142 |
-
width: 80px;
|
143 |
-
text-align: right;
|
144 |
-
}
|
145 |
-
.legend {
|
146 |
-
margin-top: 20px;
|
147 |
-
}
|
148 |
-
.footer-note {
|
149 |
-
margin-top: 30px;
|
150 |
-
font-style: italic;
|
151 |
-
font-size: 0.9em;
|
152 |
-
color: #abb4c2;
|
153 |
-
}
|
154 |
-
</style>
|
155 |
-
</head>
|
156 |
-
<body>
|
157 |
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
<span class="stat-value" id="avg-percent">0%</span>
|
175 |
-
</div>
|
176 |
-
|
177 |
-
<div class="top-countries">
|
178 |
-
<div class="stat-item">Los 5 pa铆ses con mayor recolecci贸n:</div>
|
179 |
-
<div id="top-countries-list">
|
180 |
-
<!-- Will be populated by JavaScript -->
|
181 |
-
</div>
|
182 |
-
</div>
|
183 |
-
|
184 |
-
<div class="footer-note">
|
185 |
-
Selecciona un pa铆s en el mapa para ver informaci贸n detallada.
|
186 |
-
</div>
|
187 |
-
</div>
|
188 |
-
|
189 |
-
<div class="map-container" id="map-container"></div>
|
190 |
-
</div>
|
191 |
|
192 |
-
|
|
|
193 |
|
194 |
-
|
195 |
-
|
196 |
-
const countryData = COUNTRY_DATA_PLACEHOLDER;
|
197 |
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
// Add ocean background
|
250 |
-
svg.append('rect')
|
251 |
-
.attr('width', width)
|
252 |
-
.attr('height', height)
|
253 |
-
.attr('fill', '#0f1218');
|
254 |
-
|
255 |
-
// Filter the features - check for id match
|
256 |
-
const relevantFeatures = data.features.filter(d =>
|
257 |
-
relevantCountryCodes.includes(d.id)
|
258 |
-
);
|
259 |
-
|
260 |
-
console.log('Filtered features count:', relevantFeatures.length);
|
261 |
-
|
262 |
-
// If we still don't have matches, look more deeply at the data structure
|
263 |
-
if (relevantFeatures.length === 0) {
|
264 |
-
console.log('No matches found using id, checking all feature properties');
|
265 |
-
|
266 |
-
// Get the first 10 features to examine their structure
|
267 |
-
const firstTen = data.features.slice(0, 10);
|
268 |
-
console.log('First ten features:', firstTen);
|
269 |
-
|
270 |
-
// Look for our countries in all features
|
271 |
-
const latinAmericanFeatures = data.features.filter(f => {
|
272 |
-
// Check various properties that might contain the country code
|
273 |
-
return relevantCountryCodes.includes(f.id) ||
|
274 |
-
(f.properties && relevantCountryCodes.includes(f.properties.iso_a3)) ||
|
275 |
-
(f.properties && relevantCountryCodes.includes(f.properties.name));
|
276 |
-
});
|
277 |
-
|
278 |
-
console.log('Latin American features found:', latinAmericanFeatures.length);
|
279 |
-
|
280 |
-
// If there are still no matches, just use all features and filter visually
|
281 |
-
if (latinAmericanFeatures.length === 0) {
|
282 |
-
console.log('Still no matches, using all features and hiding non-Latin American countries');
|
283 |
-
|
284 |
-
// Draw all countries but only color our target ones
|
285 |
-
svg.selectAll('.country')
|
286 |
-
.data(data.features)
|
287 |
-
.enter()
|
288 |
-
.append('path')
|
289 |
-
.attr('class', 'country')
|
290 |
-
.attr('d', path)
|
291 |
-
.attr('fill', function(d) {
|
292 |
-
// Try to match with id or iso_a3
|
293 |
-
if (d.id && countryData[d.id]) {
|
294 |
-
return colorScale(countryData[d.id].percent);
|
295 |
-
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) {
|
296 |
-
return colorScale(countryData[d.properties.iso_a3].percent);
|
297 |
-
} else if (d.properties && d.properties.name && countryData[d.properties.name]) {
|
298 |
-
return colorScale(countryData[d.properties.name].percent);
|
299 |
-
} else {
|
300 |
-
// Try checking if it's a Latin American country by name
|
301 |
-
const latinAmericanCountries = [
|
302 |
-
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain',
|
303 |
-
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay',
|
304 |
-
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic',
|
305 |
-
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba'
|
306 |
-
];
|
307 |
-
|
308 |
-
if (d.properties && latinAmericanCountries.includes(d.properties.name)) {
|
309 |
-
// Find the matching country in our data
|
310 |
-
for (const code in countryData) {
|
311 |
-
if (countryData[code].name === d.properties.name) {
|
312 |
-
return colorScale(countryData[code].percent);
|
313 |
-
}
|
314 |
-
}
|
315 |
-
}
|
316 |
-
|
317 |
-
// If not a target country, make it transparent
|
318 |
-
return 'transparent';
|
319 |
-
}
|
320 |
-
})
|
321 |
-
.attr('stroke', function(d) {
|
322 |
-
// Only show outlines for Latin American countries
|
323 |
-
const latinAmericanCountries = [
|
324 |
-
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain',
|
325 |
-
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay',
|
326 |
-
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic',
|
327 |
-
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba'
|
328 |
-
];
|
329 |
-
|
330 |
-
if (d.properties && latinAmericanCountries.includes(d.properties.name)) {
|
331 |
-
return '#0f1218';
|
332 |
-
} else {
|
333 |
-
return 'transparent';
|
334 |
-
}
|
335 |
-
})
|
336 |
-
.attr('stroke-width', 1)
|
337 |
-
.on('mouseover', function(event, d) {
|
338 |
-
// Only enable hover for target countries
|
339 |
-
if (d.id && countryData[d.id]) {
|
340 |
-
const iso = d.id;
|
341 |
-
showTooltip(event, iso);
|
342 |
-
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) {
|
343 |
-
const iso = d.properties.iso_a3;
|
344 |
-
showTooltip(event, iso);
|
345 |
-
} else if (d.properties && d.properties.name) {
|
346 |
-
// Find the matching country in our data by name
|
347 |
-
for (const code in countryData) {
|
348 |
-
if (countryData[code].name === d.properties.name) {
|
349 |
-
showTooltip(event, code);
|
350 |
-
break;
|
351 |
-
}
|
352 |
-
}
|
353 |
-
}
|
354 |
-
})
|
355 |
-
.on('mousemove', function(event) {
|
356 |
-
tooltip.style('left', (event.pageX + 15) + 'px')
|
357 |
-
.style('top', (event.pageY + 15) + 'px');
|
358 |
-
})
|
359 |
-
.on('mouseout', function() {
|
360 |
-
d3.select(this)
|
361 |
-
.attr('stroke', function(d) {
|
362 |
-
const latinAmericanCountries = [
|
363 |
-
'Mexico', 'Argentina', 'Colombia', 'Chile', 'Peru', 'Spain',
|
364 |
-
'Brazil', 'Venezuela', 'Ecuador', 'Bolivia', 'Paraguay',
|
365 |
-
'Uruguay', 'Costa Rica', 'Panama', 'Dominican Republic',
|
366 |
-
'Guatemala', 'Honduras', 'El Salvador', 'Nicaragua', 'Cuba'
|
367 |
-
];
|
368 |
-
|
369 |
-
if (d.properties && latinAmericanCountries.includes(d.properties.name)) {
|
370 |
-
return '#0f1218';
|
371 |
-
} else {
|
372 |
-
return 'transparent';
|
373 |
-
}
|
374 |
-
})
|
375 |
-
.attr('stroke-width', 1);
|
376 |
-
|
377 |
-
tooltip.style('opacity', 0);
|
378 |
-
});
|
379 |
-
} else {
|
380 |
-
// Draw only the Latin American features
|
381 |
-
svg.selectAll('.country')
|
382 |
-
.data(latinAmericanFeatures)
|
383 |
-
.enter()
|
384 |
-
.append('path')
|
385 |
-
.attr('class', 'country')
|
386 |
-
.attr('d', path)
|
387 |
-
.attr('fill', function(d) {
|
388 |
-
if (d.id && countryData[d.id]) {
|
389 |
-
return colorScale(countryData[d.id].percent);
|
390 |
-
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) {
|
391 |
-
return colorScale(countryData[d.properties.iso_a3].percent);
|
392 |
-
} else {
|
393 |
-
return colorScale(50); // Default to mid-range if no match
|
394 |
-
}
|
395 |
-
})
|
396 |
-
.attr('stroke', '#0f1218')
|
397 |
-
.attr('stroke-width', 1)
|
398 |
-
.on('mouseover', function(event, d) {
|
399 |
-
if (d.id && countryData[d.id]) {
|
400 |
-
const iso = d.id;
|
401 |
-
showTooltip(event, iso);
|
402 |
-
} else if (d.properties && d.properties.iso_a3 && countryData[d.properties.iso_a3]) {
|
403 |
-
const iso = d.properties.iso_a3;
|
404 |
-
showTooltip(event, iso);
|
405 |
-
}
|
406 |
-
})
|
407 |
-
.on('mousemove', function(event) {
|
408 |
-
tooltip.style('left', (event.pageX + 15) + 'px')
|
409 |
-
.style('top', (event.pageY + 15) + 'px');
|
410 |
-
})
|
411 |
-
.on('mouseout', function() {
|
412 |
-
d3.select(this)
|
413 |
-
.attr('stroke', '#0f1218')
|
414 |
-
.attr('stroke-width', 1);
|
415 |
-
|
416 |
-
tooltip.style('opacity', 0);
|
417 |
-
});
|
418 |
-
}
|
419 |
-
} else {
|
420 |
-
// Draw only our target countries
|
421 |
-
svg.selectAll('.country')
|
422 |
-
.data(relevantFeatures)
|
423 |
-
.enter()
|
424 |
-
.append('path')
|
425 |
-
.attr('class', 'country')
|
426 |
-
.attr('d', path)
|
427 |
-
.attr('fill', function(d) {
|
428 |
-
const iso = d.id;
|
429 |
-
return colorScale(countryData[iso].percent);
|
430 |
-
})
|
431 |
-
.attr('stroke', '#0f1218')
|
432 |
-
.attr('stroke-width', 1)
|
433 |
-
.on('mouseover', function(event, d) {
|
434 |
-
const iso = d.id;
|
435 |
-
showTooltip(event, iso);
|
436 |
-
})
|
437 |
-
.on('mousemove', function(event) {
|
438 |
-
tooltip.style('left', (event.pageX + 15) + 'px')
|
439 |
-
.style('top', (event.pageY + 15) + 'px');
|
440 |
-
})
|
441 |
-
.on('mouseout', function() {
|
442 |
-
d3.select(this)
|
443 |
-
.attr('stroke', '#0f1218')
|
444 |
-
.attr('stroke-width', 1);
|
445 |
-
|
446 |
-
tooltip.style('opacity', 0);
|
447 |
-
});
|
448 |
-
}
|
449 |
-
|
450 |
-
// Function to show tooltip
|
451 |
-
function showTooltip(event, iso) {
|
452 |
-
d3.select(event.currentTarget)
|
453 |
-
.attr('stroke', '#fff')
|
454 |
-
.attr('stroke-width', 1.5);
|
455 |
-
|
456 |
-
tooltip.style('opacity', 1)
|
457 |
-
.style('left', (event.pageX + 15) + 'px')
|
458 |
-
.style('top', (event.pageY + 15) + 'px')
|
459 |
-
.html('<strong>' + countryData[iso].name + '</strong><br/>' +
|
460 |
-
'Progress: ' + countryData[iso].percent + '%');
|
461 |
-
}
|
462 |
-
|
463 |
-
// Add a legend on the right side of the map
|
464 |
-
const legendWidth = 200;
|
465 |
-
const legendHeight = 15;
|
466 |
-
const legendX = width - legendWidth - 20;
|
467 |
-
const legendY = 20;
|
468 |
-
|
469 |
-
// Create legend group
|
470 |
-
const legend = svg.append('g')
|
471 |
-
.attr('transform', 'translate(' + legendX + ',' + legendY + ')');
|
472 |
-
|
473 |
-
// Legend title
|
474 |
-
legend.append('text')
|
475 |
-
.attr('x', legendWidth / 2)
|
476 |
-
.attr('y', -5)
|
477 |
-
.attr('text-anchor', 'middle')
|
478 |
-
.style('fill', '#fff')
|
479 |
-
.style('font-size', '12px')
|
480 |
-
.text('Porcentaje de Datos Recolectado');
|
481 |
-
|
482 |
-
// Create gradient for legend
|
483 |
-
const defs = svg.append('defs');
|
484 |
-
const gradient = defs.append('linearGradient')
|
485 |
-
.attr('id', 'legendGradient')
|
486 |
-
.attr('x1', '0%')
|
487 |
-
.attr('x2', '100%')
|
488 |
-
.attr('y1', '0%')
|
489 |
-
.attr('y2', '0%');
|
490 |
-
|
491 |
-
gradient.append('stop')
|
492 |
-
.attr('offset', '0%')
|
493 |
-
.attr('stop-color', '#4a1942');
|
494 |
-
|
495 |
-
gradient.append('stop')
|
496 |
-
.attr('offset', '100%')
|
497 |
-
.attr('stop-color', '#f32b7b');
|
498 |
-
|
499 |
-
// Add legend rectangle
|
500 |
-
legend.append('rect')
|
501 |
-
.attr('width', legendWidth)
|
502 |
-
.attr('height', legendHeight)
|
503 |
-
.style('fill', 'url(#legendGradient)')
|
504 |
-
.style('stroke', 'none');
|
505 |
-
|
506 |
-
// Add min and max labels
|
507 |
-
legend.append('text')
|
508 |
-
.attr('x', 0)
|
509 |
-
.attr('y', legendHeight + 15)
|
510 |
-
.attr('text-anchor', 'start')
|
511 |
-
.style('fill', '#fff')
|
512 |
-
.style('font-size', '12px')
|
513 |
-
.text('0%');
|
514 |
-
|
515 |
-
legend.append('text')
|
516 |
-
.attr('x', legendWidth / 2)
|
517 |
-
.attr('y', legendHeight + 15)
|
518 |
-
.attr('text-anchor', 'middle')
|
519 |
-
.style('fill', '#fff')
|
520 |
-
.style('font-size', '12px')
|
521 |
-
.text('50%');
|
522 |
-
|
523 |
-
legend.append('text')
|
524 |
-
.attr('x', legendWidth)
|
525 |
-
.attr('y', legendHeight + 15)
|
526 |
-
.attr('text-anchor', 'end')
|
527 |
-
.style('fill', '#fff')
|
528 |
-
.style('font-size', '12px')
|
529 |
-
.text('100%');
|
530 |
-
|
531 |
-
// Update statistics
|
532 |
-
updateStatistics();
|
533 |
-
})
|
534 |
-
.catch(function(error) {
|
535 |
-
console.error('Error loading or rendering the map:', error);
|
536 |
-
container.innerHTML = '<div style="color: white; text-align: center; padding: 20px;">Error loading map: ' + error.message + '</div>';
|
537 |
-
});
|
538 |
-
|
539 |
-
// Function to update statistics
|
540 |
-
function updateStatistics() {
|
541 |
-
console.log('Updating statistics');
|
542 |
-
|
543 |
-
// Add random document counts to countries that don't have them
|
544 |
-
Object.keys(countryData).forEach(code => {
|
545 |
-
if (!countryData[code].documents) {
|
546 |
-
countryData[code].documents = Math.floor(Math.random() * 300000) + 300000;
|
547 |
-
}
|
548 |
-
});
|
549 |
-
|
550 |
-
// Calculate total documents
|
551 |
-
const totalDocs = Object.values(countryData).reduce((sum, country) => {
|
552 |
-
return sum + (country.documents || 0);
|
553 |
-
}, 0);
|
554 |
-
|
555 |
-
// Calculate average percentage
|
556 |
-
const avgPercent = Object.values(countryData).reduce((sum, country) => {
|
557 |
-
return sum + country.percent;
|
558 |
-
}, 0) / Object.values(countryData).length;
|
559 |
-
|
560 |
-
// Update the stats
|
561 |
-
document.getElementById('total-docs').textContent = totalDocs.toLocaleString();
|
562 |
-
document.getElementById('avg-percent').textContent = avgPercent.toFixed(1) + '%';
|
563 |
-
|
564 |
-
// Create an array of countries with document counts
|
565 |
-
const countriesWithDocs = Object.keys(countryData).map(code => {
|
566 |
-
return {
|
567 |
-
code: code,
|
568 |
-
name: countryData[code].name,
|
569 |
-
percent: countryData[code].percent,
|
570 |
-
documents: countryData[code].documents
|
571 |
-
};
|
572 |
-
});
|
573 |
-
|
574 |
-
// Sort by document count descending
|
575 |
-
countriesWithDocs.sort((a, b) => b.documents - a.documents);
|
576 |
-
|
577 |
-
// Take the top 5
|
578 |
-
const topCountries = countriesWithDocs.slice(0, 5);
|
579 |
-
|
580 |
-
// Update the top countries list
|
581 |
-
const topCountriesList = document.getElementById('top-countries-list');
|
582 |
-
topCountriesList.innerHTML = '';
|
583 |
-
|
584 |
-
topCountries.forEach(country => {
|
585 |
-
const countryDiv = document.createElement('div');
|
586 |
-
countryDiv.className = 'country-stat';
|
587 |
-
|
588 |
-
countryDiv.innerHTML = `
|
589 |
-
<span>${country.name}</span>
|
590 |
-
<div class="country-bar">
|
591 |
-
<div class="country-bar-fill" style="width: ${country.percent}%;"></div>
|
592 |
-
</div>
|
593 |
-
<span class="country-value">${country.documents.toLocaleString()}</span>
|
594 |
-
`;
|
595 |
-
|
596 |
-
topCountriesList.appendChild(countryDiv);
|
597 |
-
});
|
598 |
-
|
599 |
-
console.log('Statistics updated');
|
600 |
-
}
|
601 |
-
|
602 |
-
// Handle window resize
|
603 |
-
window.addEventListener('resize', function() {
|
604 |
-
console.log('Window resized');
|
605 |
-
|
606 |
-
const width = container.clientWidth;
|
607 |
-
const height = container.clientHeight;
|
608 |
-
|
609 |
-
// Update SVG dimensions
|
610 |
-
d3.select('svg')
|
611 |
-
.attr('width', width)
|
612 |
-
.attr('height', height);
|
613 |
-
|
614 |
-
// Update projection
|
615 |
-
projection.scale(width / 3)
|
616 |
-
.translate([width / 2, height / 2]);
|
617 |
-
|
618 |
-
// Update paths
|
619 |
-
d3.selectAll('path').attr('d', path);
|
620 |
-
|
621 |
-
// Update legend position
|
622 |
-
const legendX = width - 220;
|
623 |
-
d3.select('.legend')
|
624 |
-
.attr('transform', 'translate(' + legendX + ',20)');
|
625 |
-
});
|
626 |
-
});
|
627 |
-
</script>
|
628 |
-
</body>
|
629 |
-
</html>
|
630 |
-
"""
|
631 |
|
632 |
# Route to serve the map visualization
|
633 |
@app.get("/d3-map")
|
634 |
async def serve_map():
|
635 |
-
# Generate
|
636 |
-
country_data =
|
637 |
-
|
638 |
-
|
639 |
-
for code in country_data:
|
640 |
-
country_data[code]["documents"] = random.randint(300000, 700000)
|
641 |
|
642 |
# Convert to JSON for JavaScript
|
643 |
country_data_json = json.dumps(country_data)
|
644 |
|
645 |
# Replace the placeholder with actual data
|
646 |
-
|
|
|
|
|
|
|
647 |
|
648 |
return fastapi.responses.HTMLResponse(content=html_content)
|
649 |
|
@@ -663,7 +132,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="pink", secondary_hue="purple"))
|
|
663 |
def refresh():
|
664 |
return create_iframe()
|
665 |
|
666 |
-
gr.Button("
|
667 |
|
668 |
# Mount the Gradio app to the FastAPI app
|
669 |
gr.mount_gradio_app(app, demo, path="/")
|
|
|
3 |
import json
|
4 |
import fastapi
|
5 |
from fastapi import FastAPI
|
6 |
+
import os
|
7 |
+
import argilla as rg
|
8 |
|
9 |
+
# Initialize Argilla client
|
10 |
+
client = rg.Argilla(
|
11 |
+
api_url=os.getenv("ARGILLA_API_URL", ""),
|
12 |
+
api_key=os.getenv("ARGILLA_API_KEY", "")
|
13 |
+
)
|
14 |
|
15 |
+
# List of countries to check
|
16 |
+
COUNTRIES = [
|
17 |
+
"MEX", "ARG", "COL", "CHL", "PER", "ESP", "BRA",
|
18 |
+
"VEN", "ECU", "BOL", "PRY", "URY", "CRI", "PAN",
|
19 |
+
"DOM", "GTM", "HND", "SLV", "NIC", "CUB"
|
20 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
def count_answers_per_space(country: str):
|
23 |
+
"""
|
24 |
+
Count the number of answers for a specific country's answering space.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
+
Args:
|
27 |
+
country: Country code (e.g., "CHL")
|
28 |
+
|
29 |
+
Returns:
|
30 |
+
Dictionary with statistics about answers in the space
|
31 |
+
"""
|
32 |
+
# Convert country code to full country name
|
33 |
+
country_mapping = {
|
34 |
+
"MEX": "Mexico", "ARG": "Argentina", "COL": "Colombia",
|
35 |
+
"CHL": "Chile", "PER": "Peru", "ESP": "Spain",
|
36 |
+
"BRA": "Brazil", "VEN": "Venezuela", "ECU": "Ecuador",
|
37 |
+
"BOL": "Bolivia", "PRY": "Paraguay", "URY": "Uruguay",
|
38 |
+
"CRI": "Costa Rica", "PAN": "Panama", "DOM": "Dominican Republic",
|
39 |
+
"GTM": "Guatemala", "HND": "Honduras", "SLV": "El Salvador",
|
40 |
+
"NIC": "Nicaragua", "CUB": "Cuba"
|
41 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
|
43 |
+
full_country_name = country_mapping.get(country, country)
|
44 |
+
dataset_name = f"{full_country_name}_responder_preguntas"
|
45 |
|
46 |
+
try:
|
47 |
+
dataset = client.datasets(dataset_name)
|
|
|
48 |
|
49 |
+
# Get all records with their responses
|
50 |
+
records = dataset.records(with_responses=True)
|
51 |
+
|
52 |
+
# Count total questions
|
53 |
+
total_questions = len(list(records))
|
54 |
+
|
55 |
+
# Count answered questions
|
56 |
+
answered_questions = 0
|
57 |
+
total_answers = 0
|
58 |
+
answers_per_user = {}
|
59 |
+
|
60 |
+
for record in records:
|
61 |
+
responses = record.responses.get("text", [])
|
62 |
+
if responses:
|
63 |
+
answered_questions += 1
|
64 |
+
total_answers += len(responses)
|
65 |
+
|
66 |
+
# Count per user
|
67 |
+
for response in responses:
|
68 |
+
user_id = str(response.user_id)
|
69 |
+
if user_id in answers_per_user:
|
70 |
+
answers_per_user[user_id] += 1
|
71 |
+
else:
|
72 |
+
answers_per_user[user_id] = 1
|
73 |
+
|
74 |
+
percentage_complete = (answered_questions / total_questions * 100) if total_questions > 0 else 0
|
75 |
+
|
76 |
+
return {
|
77 |
+
"name": full_country_name,
|
78 |
+
"total_questions": total_questions,
|
79 |
+
"answered_questions": answered_questions,
|
80 |
+
"total_answers": total_answers,
|
81 |
+
"percent": round(percentage_complete, 2),
|
82 |
+
"documents": total_answers * 10 # Approximation of document count
|
83 |
+
}
|
84 |
+
|
85 |
+
except Exception as e:
|
86 |
+
# If space doesn't exist, return zero values
|
87 |
+
print(f"No dataset found for {dataset_name}: {e}")
|
88 |
+
return {
|
89 |
+
"name": full_country_name,
|
90 |
+
"total_questions": 0,
|
91 |
+
"answered_questions": 0,
|
92 |
+
"total_answers": 0,
|
93 |
+
"percent": 0,
|
94 |
+
"documents": 0
|
95 |
+
}
|
96 |
+
|
97 |
+
# Create a FastAPI app
|
98 |
+
app = FastAPI()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
# Route to serve the map visualization
|
101 |
@app.get("/d3-map")
|
102 |
async def serve_map():
|
103 |
+
# Generate data for each country by querying Argilla
|
104 |
+
country_data = {}
|
105 |
+
for country_code in COUNTRIES:
|
106 |
+
country_data[country_code] = count_answers_per_space(country_code)
|
|
|
|
|
107 |
|
108 |
# Convert to JSON for JavaScript
|
109 |
country_data_json = json.dumps(country_data)
|
110 |
|
111 |
# Replace the placeholder with actual data
|
112 |
+
with open('template.txt', 'r') as f:
|
113 |
+
html_template = f.read()
|
114 |
+
|
115 |
+
html_content = html_template.replace("COUNTRY_DATA_PLACEHOLDER", country_data_json)
|
116 |
|
117 |
return fastapi.responses.HTMLResponse(content=html_content)
|
118 |
|
|
|
132 |
def refresh():
|
133 |
return create_iframe()
|
134 |
|
135 |
+
gr.Button("Actualizar Datos").click(fn=refresh, outputs=iframe_output)
|
136 |
|
137 |
# Mount the Gradio app to the FastAPI app
|
138 |
gr.mount_gradio_app(app, demo, path="/")
|