ouhenio commited on
Commit
bcd0e3c
·
verified ·
1 Parent(s): bd01c61

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -597
app.py CHANGED
@@ -1,663 +1,265 @@
1
- import os
2
- from queue import Queue
3
- import json
4
  import gradio as gr
5
- import argilla as rg
6
- from argilla.webhooks import webhook_listener
7
- from dataclasses import dataclass, field, asdict
8
- from typing import Dict, List, Optional, Tuple, Any, Callable
9
- import logging
10
-
11
- # Set up logging
12
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
13
- logger = logging.getLogger(__name__)
14
 
15
- # ============================================================================
16
- # DATA MODELS - Clear definition of data structures
17
- # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- @dataclass
20
- class CountryData:
21
- """Data model for country information and annotation progress."""
22
- name: str
23
- target: int
24
- count: int = 0
25
- percent: int = 0
26
-
27
- def update_progress(self, new_count: Optional[int] = None):
28
- """Update the progress percentage based on count/target."""
29
- if new_count is not None:
30
- self.count = new_count
31
- self.percent = min(100, int((self.count / self.target) * 100))
32
- return self
33
-
34
- @dataclass
35
- class Event:
36
- """Data model for events in the system."""
37
- event_type: str
38
- timestamp: str = ""
39
- country: str = ""
40
- count: int = 0
41
- percent: int = 0
42
- error: str = ""
43
 
44
- @dataclass
45
- class ApplicationState:
46
- """Central state management for the application."""
47
- countries: Dict[str, CountryData] = field(default_factory=dict)
48
- events: Queue = field(default_factory=Queue)
49
-
50
- def to_dict(self) -> Dict[str, Any]:
51
- """Convert state to a serializable dictionary for the UI."""
52
- return {
53
- code: asdict(data) for code, data in self.countries.items()
54
- }
55
-
56
- def to_json(self) -> str:
57
- """Convert state to JSON for the UI."""
58
- return json.dumps(self.to_dict())
59
-
60
- def add_event(self, event: Event):
61
- """Add an event to the queue."""
62
- self.events.put(asdict(event))
63
-
64
- def get_next_event(self) -> Dict[str, Any]:
65
- """Get the next event from the queue."""
66
- if not self.events.empty():
67
- return self.events.get()
68
- return {}
69
-
70
- def update_country_progress(self, country_code: str, count: Optional[int] = None) -> bool:
71
- """Update a country's annotation progress."""
72
- if country_code in self.countries:
73
- if count is not None:
74
- self.countries[country_code].count = count
75
- self.countries[country_code].update_progress()
76
-
77
- # Create and add a progress update event
78
- self.add_event(Event(
79
- event_type="progress_update",
80
- country=self.countries[country_code].name,
81
- count=self.countries[country_code].count,
82
- percent=self.countries[country_code].percent
83
- ))
84
- return True
85
- return False
86
 
87
- def increment_country_progress(self, country_code: str) -> bool:
88
- """Increment a country's annotation count by 1."""
89
- if country_code in self.countries:
90
- self.countries[country_code].count += 1
91
- return self.update_country_progress(country_code)
92
- return False
93
-
94
- def get_stats(self) -> Tuple[int, float, int]:
95
- """Calculate overall statistics."""
96
- total = sum(data.count for data in self.countries.values())
97
- percentages = [data.percent for data in self.countries.values()]
98
- avg = sum(percentages) / len(percentages) if percentages else 0
99
- countries_50_plus = sum(1 for p in percentages if p >= 50)
100
 
101
- return total, avg, countries_50_plus
102
-
103
- # ============================================================================
104
- # CONFIGURATION - Separated from business logic
105
- # ============================================================================
106
-
107
- class Config:
108
- """Configuration for the application."""
109
- # Country mapping (ISO code to name and target)
110
- COUNTRY_MAPPING = {
111
- "MX": {"name": "Mexico", "target": 1000},
112
- "AR": {"name": "Argentina", "target": 800},
113
- "CO": {"name": "Colombia", "target": 700},
114
- "CL": {"name": "Chile", "target": 600},
115
- "PE": {"name": "Peru", "target": 600},
116
- "ES": {"name": "Spain", "target": 1200},
117
- "BR": {"name": "Brazil", "target": 1000},
118
- "VE": {"name": "Venezuela", "target": 500},
119
- "EC": {"name": "Ecuador", "target": 400},
120
- "BO": {"name": "Bolivia", "target": 300},
121
- "PY": {"name": "Paraguay", "target": 300},
122
- "UY": {"name": "Uruguay", "target": 300},
123
- "CR": {"name": "Costa Rica", "target": 250},
124
- "PA": {"name": "Panama", "target": 250},
125
- "DO": {"name": "Dominican Republic", "target": 300},
126
- "GT": {"name": "Guatemala", "target": 250},
127
- "HN": {"name": "Honduras", "target": 200},
128
- "SV": {"name": "El Salvador", "target": 200},
129
- "NI": {"name": "Nicaragua", "target": 200},
130
- "CU": {"name": "Cuba", "target": 300}
131
- }
132
-
133
- @classmethod
134
- def create_country_data(cls) -> Dict[str, CountryData]:
135
- """Create CountryData objects from the mapping."""
136
- return {
137
- code: CountryData(
138
- name=data["name"],
139
- target=data["target"]
140
- ) for code, data in cls.COUNTRY_MAPPING.items()
141
- }
142
-
143
- # ============================================================================
144
- # SERVICES - Business logic separated from presentation and data access
145
- # ============================================================================
146
-
147
- class ArgillaService:
148
- """Service for interacting with Argilla."""
149
- def __init__(self, api_url: Optional[str] = None, api_key: Optional[str] = None):
150
- """Initialize the Argilla service."""
151
- self.api_url = api_url or os.getenv("ARGILLA_API_URL")
152
- self.api_key = api_key or os.getenv("ARGILLA_API_KEY")
153
 
154
- self.client = rg.Argilla(
155
- api_url=self.api_url,
156
- api_key=self.api_key,
157
- )
158
- self.server = rg.get_webhook_server()
159
-
160
- def get_server(self):
161
- """Get the Argilla webhook server."""
162
- return self.server
163
-
164
- def get_client_base_url(self) -> str:
165
- """Get the base URL of the Argilla client."""
166
- return self.client.http_client.base_url if hasattr(self.client, 'http_client') else "Not connected"
167
-
168
- class CountryMappingService:
169
- """Service for mapping between dataset names and country codes."""
170
- @staticmethod
171
- def find_country_code_from_dataset(dataset_name: str) -> Optional[str]:
172
- """
173
- Try to extract a country code from a dataset name by matching
174
- country names in the dataset name.
175
- """
176
- dataset_name_lower = dataset_name.lower()
177
- for code, data in Config.COUNTRY_MAPPING.items():
178
- country_name = data["name"].lower()
179
- if country_name in dataset_name_lower:
180
- return code
181
- return None
182
-
183
- # ============================================================================
184
- # UI COMPONENTS - Presentation layer separated from business logic
185
- # ============================================================================
186
-
187
- class MapVisualization:
188
- """Component for D3.js map visualization."""
189
- @staticmethod
190
- def create_map_html() -> str:
191
- """Create the initial HTML container for the map."""
192
- return """
193
- <div id="map-container" style="width:100%; height:600px; position:relative; background-color:#111;">
194
- <div style="display:flex; justify-content:center; align-items:center; height:100%; color:white; font-family:sans-serif;">
195
- Loading map visualization...
196
- </div>
197
- </div>
198
- <div id="tooltip" style="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;"></div>
199
- """
200
-
201
- @staticmethod
202
- def create_d3_script(progress_data: str) -> str:
203
- """Create the D3.js script for rendering the map."""
204
- return f"""
205
- async () => {{
206
- // Load D3.js modules
207
- const script1 = document.createElement("script");
208
- script1.src = "https://cdn.jsdelivr.net/npm/d3@7";
209
- document.head.appendChild(script1);
210
-
211
- // Wait for D3 to load
212
- await new Promise(resolve => {{
213
- script1.onload = resolve;
214
- }});
215
-
216
- console.log("D3 loaded successfully");
217
-
218
- // Load topojson
219
- const script2 = document.createElement("script");
220
- script2.src = "https://cdn.jsdelivr.net/npm/topojson@3";
221
- document.head.appendChild(script2);
222
-
223
- await new Promise(resolve => {{
224
- script2.onload = resolve;
225
- }});
226
-
227
- console.log("TopoJSON loaded successfully");
228
-
229
- // The progress data passed from Python
230
- const progressData = {progress_data};
231
-
232
- // Set up the SVG container
233
- const mapContainer = document.getElementById('map-container');
234
- mapContainer.innerHTML = ''; // Clear loading message
235
-
236
- const width = mapContainer.clientWidth;
237
- const height = 600;
238
-
239
- const svg = d3.select("#map-container")
240
- .append("svg")
241
- .attr("width", width)
242
- .attr("height", height)
243
- .attr("viewBox", `0 0 ${{width}} ${{height}}`)
244
- .style("background-color", "#111");
245
-
246
- // Define color scale
247
- const colorScale = d3.scaleLinear()
248
- .domain([0, 100])
249
- .range(["#4a1942", "#f32b7b"]);
250
-
251
- // Set up projection focused on Latin America and Spain
252
- const projection = d3.geoMercator()
253
- .center([-60, 0])
254
- .scale(width / 5)
255
- .translate([width / 2, height / 2]);
256
-
257
- const path = d3.geoPath().projection(projection);
258
-
259
- // Tooltip setup
260
- const tooltip = d3.select("#tooltip");
261
-
262
- // Load the world GeoJSON data
263
- const response = await fetch("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson");
264
  const data = await response.json();
265
 
266
- // Draw the map
267
- svg.selectAll("path")
268
  .data(data.features)
269
  .enter()
270
- .append("path")
271
- .attr("d", path)
272
- .attr("stroke", "#f32b7b")
273
- .attr("stroke-width", 1)
274
- .attr("fill", d => {{
275
  // Get the ISO code from the properties
276
  const iso = d.properties.iso_a2;
277
 
278
- if (progressData[iso]) {{
279
- return colorScale(progressData[iso].percent);
280
  }}
281
- return "#2d3748"; // Default gray for non-tracked countries
282
  }})
283
- .on("mouseover", function(event, d) {{
284
  const iso = d.properties.iso_a2;
285
 
286
  d3.select(this)
287
- .attr("stroke", "#4a1942")
288
- .attr("stroke-width", 2);
289
 
290
- if (progressData[iso]) {{
291
- tooltip.style("opacity", 1)
292
- .style("left", (event.pageX + 15) + "px")
293
- .style("top", (event.pageY + 15) + "px")
294
  .html(`
295
- <strong>${{progressData[iso].name}}</strong><br/>
296
- Documents: ${{progressData[iso].count.toLocaleString()}}/${{progressData[iso].target.toLocaleString()}}<br/>
297
- Completion: ${{progressData[iso].percent}}%
298
  `);
299
  }}
300
  }})
301
- .on("mousemove", function(event) {{
302
- tooltip.style("left", (event.pageX + 15) + "px")
303
- .style("top", (event.pageY + 15) + "px");
304
  }})
305
- .on("mouseout", function() {{
306
  d3.select(this)
307
- .attr("stroke", "#f32b7b")
308
- .attr("stroke-width", 1);
309
 
310
- tooltip.style("opacity", 0);
311
  }});
312
 
313
- // Add legend
314
  const legendWidth = Math.min(width - 40, 200);
315
  const legendHeight = 15;
316
  const legendX = width - legendWidth - 20;
317
 
318
- const legend = svg.append("g")
319
- .attr("class", "legend")
320
- .attr("transform", `translate(${{legendX}}, 30)`);
321
 
322
  // Create gradient for legend
323
- const defs = svg.append("defs");
324
- const gradient = defs.append("linearGradient")
325
- .attr("id", "dataGradient")
326
- .attr("x1", "0%")
327
- .attr("y1", "0%")
328
- .attr("x2", "100%")
329
- .attr("y2", "0%");
330
-
331
- gradient.append("stop")
332
- .attr("offset", "0%")
333
- .attr("stop-color", "#4a1942");
334
-
335
- gradient.append("stop")
336
- .attr("offset", "100%")
337
- .attr("stop-color", "#f32b7b");
338
 
339
  // Add legend title
340
- legend.append("text")
341
- .attr("x", legendWidth / 2)
342
- .attr("y", -10)
343
- .attr("text-anchor", "middle")
344
- .attr("font-size", "12px")
345
- .attr("fill", "#f1f5f9")
346
- .text("Annotation Progress");
347
 
348
  // Add legend rectangle
349
- legend.append("rect")
350
- .attr("width", legendWidth)
351
- .attr("height", legendHeight)
352
- .attr("rx", 2)
353
- .attr("ry", 2)
354
- .style("fill", "url(#dataGradient)");
355
 
356
  // Add legend labels
357
- legend.append("text")
358
- .attr("x", 0)
359
- .attr("y", legendHeight + 15)
360
- .attr("text-anchor", "start")
361
- .attr("font-size", "10px")
362
- .attr("fill", "#94a3b8")
363
- .text("0%");
364
-
365
- legend.append("text")
366
- .attr("x", legendWidth / 2)
367
- .attr("y", legendHeight + 15)
368
- .attr("text-anchor", "middle")
369
- .attr("font-size", "10px")
370
- .attr("fill", "#94a3b8")
371
- .text("50%");
372
-
373
- legend.append("text")
374
- .attr("x", legendWidth)
375
- .attr("y", legendHeight + 15)
376
- .attr("text-anchor", "end")
377
- .attr("font-size", "10px")
378
- .attr("fill", "#94a3b8")
379
- .text("100%");
380
-
381
  // Handle window resize
382
- globalThis.resizeMap = () => {{
383
- const width = mapContainer.clientWidth;
384
 
385
  // Update SVG dimensions
386
- d3.select("svg")
387
- .attr("width", width)
388
- .attr("viewBox", `0 0 ${{width}} ${{height}}`);
389
 
390
  // Update projection
391
  projection.scale(width / 5)
392
  .translate([width / 2, height / 2]);
393
 
394
  // Update paths
395
- d3.selectAll("path").attr("d", path);
396
 
397
  // Update legend position
398
  const legendWidth = Math.min(width - 40, 200);
399
  const legendX = width - legendWidth - 20;
400
 
401
- d3.select(".legend")
402
- .attr("transform", `translate(${{legendX}}, 30)`);
403
- }};
404
-
405
- window.addEventListener('resize', globalThis.resizeMap);
406
  }}
407
- """
408
 
409
- # ============================================================================
410
- # APPLICATION FACTORY - Creates and configures the application
411
- # ============================================================================
 
412
 
413
- class ApplicationFactory:
414
- """Factory for creating the application components."""
415
- @classmethod
416
- def create_app_state(cls) -> ApplicationState:
417
- """Create and initialize the application state."""
418
- state = ApplicationState(countries=Config.create_country_data())
419
-
420
- # Initialize with some sample data
421
- for code in ["MX", "AR", "CO", "ES"]:
422
- sample_count = int(state.countries[code].target * 0.3)
423
- state.update_country_progress(code, sample_count)
424
-
425
- state.update_country_progress("BR", int(state.countries["BR"].target * 0.5))
426
- state.update_country_progress("CL", int(state.countries["CL"].target * 0.7))
427
-
428
- return state
429
 
430
- @classmethod
431
- def create_argilla_service(cls) -> ArgillaService:
432
- """Create the Argilla service."""
433
- return ArgillaService()
434
 
435
- @staticmethod
436
- def cleanup_existing_webhooks(argilla_client):
437
- """Clean up existing webhooks to avoid warnings."""
438
- try:
439
- # Get existing webhooks
440
- existing_webhooks = argilla_client.webhooks.list()
441
-
442
- # Look for our webhook
443
- for webhook in existing_webhooks:
444
- if "handle_response_created" in getattr(webhook, 'url', ''):
445
- logger.info(f"Removing existing webhook: {webhook.id}")
446
- argilla_client.webhooks.delete(webhook.id)
447
- break
448
- except Exception as e:
449
- logger.warning(f"Could not clean up webhooks: {e}")
450
 
451
- @classmethod
452
- def create_webhook_handler(cls, app_state: ApplicationState) -> Callable:
453
- """Create the webhook handler function."""
454
- country_service = CountryMappingService()
455
-
456
- # Define the webhook handler
457
- @webhook_listener(events=["response.created"])
458
- async def handle_response_created(response, type, timestamp):
459
- try:
460
- # Log the event
461
- logger.info(f"Received webhook event: {type} at {timestamp}")
462
-
463
- # Add basic event to the queue
464
- app_state.add_event(Event(
465
- event_type=type,
466
- timestamp=str(timestamp)
467
- ))
468
-
469
- # Extract dataset name
470
- record = response.record
471
- dataset_name = record.dataset.name
472
- logger.info(f"Processing response for dataset: {dataset_name}")
473
-
474
- # Find country code from dataset name
475
- country_code = country_service.find_country_code_from_dataset(dataset_name)
476
-
477
- # Update country progress if found
478
- if country_code:
479
- success = app_state.increment_country_progress(country_code)
480
- if success:
481
- country_data = app_state.countries[country_code]
482
- logger.info(
483
- f"Updated progress for {country_data.name}: "
484
- f"{country_data.count}/{country_data.target} ({country_data.percent}%)"
485
- )
486
-
487
- except Exception as e:
488
- logger.error(f"Error in webhook handler: {e}", exc_info=True)
489
- app_state.add_event(Event(
490
- event_type="error",
491
- error=str(e)
492
- ))
493
-
494
- return handle_response_created
495
-
496
- @classmethod
497
- def create_ui(cls, argilla_service: ArgillaService, app_state: ApplicationState):
498
- """Create the Gradio UI."""
499
- # Create and configure the Gradio interface
500
- demo = gr.Blocks(theme=gr.themes.Soft(primary_hue="pink", secondary_hue="purple"))
501
 
502
- with demo:
503
- argilla_server = argilla_service.get_client_base_url()
504
-
505
- with gr.Row():
506
- gr.Markdown(f"""
507
- # Latin America & Spain Annotation Progress Map
508
-
509
- ### Connected to Argilla server: {argilla_server}
510
-
511
- This dashboard visualizes annotation progress across Latin America and Spain.
512
- """)
513
-
514
- with gr.Row():
515
- with gr.Column(scale=2):
516
- # Map visualization - empty at first
517
- map_html = gr.HTML(MapVisualization.create_map_html(), label="Annotation Progress Map")
518
-
519
- # Hidden element to store map data
520
- map_data = gr.JSON(value=app_state.to_json(), visible=False)
521
-
522
- with gr.Column(scale=1):
523
- # Overall statistics
524
- with gr.Group():
525
- gr.Markdown("### Statistics")
526
- total_docs, avg_completion, countries_over_50 = app_state.get_stats()
527
- total_docs_ui = gr.Number(value=total_docs, label="Total Documents", interactive=False)
528
- avg_completion_ui = gr.Number(value=avg_completion, label="Average Completion (%)", interactive=False)
529
- countries_over_50_ui = gr.Number(value=countries_over_50, label="Countries Over 50%", interactive=False)
530
-
531
- # Country details
532
- with gr.Group():
533
- gr.Markdown("### Country Details")
534
- country_selector = gr.Dropdown(
535
- choices=[f"{data.name} ({code})" for code, data in app_state.countries.items()],
536
- label="Select Country"
537
- )
538
- country_progress = gr.JSON(label="Country Progress", value={})
539
-
540
- # Refresh button
541
- refresh_btn = gr.Button("Refresh Map")
542
-
543
- # UI interaction functions
544
- def update_map():
545
- return app_state.to_json()
546
-
547
- def update_country_details(country_selection):
548
- if not country_selection:
549
- return {}
550
-
551
- # Extract the country code from the selection (format: "Country Name (CODE)")
552
- code = country_selection.split("(")[-1].replace(")", "").strip()
553
-
554
- if code in app_state.countries:
555
- return asdict(app_state.countries[code])
556
- return {}
557
-
558
- def update_events():
559
- event = app_state.get_next_event()
560
- stats = app_state.get_stats()
561
-
562
- # If this is a progress update, update the map data
563
- if event.get("event_type") == "progress_update":
564
- # This will indirectly trigger a map refresh through the change event
565
- return event, app_state.to_json(), stats[0], stats[1], stats[2]
566
-
567
- return event, None, stats[0], stats[1], stats[2]
568
-
569
- # Set up event handlers
570
- refresh_btn.click(
571
- fn=update_map,
572
- inputs=None,
573
- outputs=map_data
574
- )
575
-
576
- country_selector.change(
577
- fn=update_country_details,
578
- inputs=[country_selector],
579
- outputs=[country_progress]
580
- )
581
-
582
- # Alternative approach to load JavaScript without using _js parameter
583
- # Create a hidden HTML component to hold our script
584
- js_holder = gr.HTML("", visible=False)
585
-
586
- # When map_data is updated, create a script tag with our D3 code
587
- def create_script_tag(data):
588
- script_content = MapVisualization.create_d3_script(data)
589
- html = f"""
590
- <div id="js-executor">
591
- <script>
592
- (async () => {{
593
- const scriptFn = {script_content};
594
- await scriptFn();
595
- }})();
596
- </script>
597
- </div>
598
- """
599
- return html
600
-
601
- map_data.change(
602
- fn=create_script_tag,
603
- inputs=map_data,
604
- outputs=js_holder
605
- )
606
-
607
- # Use timer to check for new events and update stats
608
- gr.Timer(1, active=True).tick(
609
- update_events,
610
- outputs=[events_json, map_data, total_docs_ui, avg_completion_ui, countries_over_50_ui]
611
- )
612
-
613
- # Initialize D3 on page load using an initial script tag
614
- initial_map_script = gr.HTML(
615
- f"""
616
- <div id="initial-js-executor">
617
- <script>
618
- document.addEventListener('DOMContentLoaded', async () => {{
619
- const scriptFn = {MapVisualization.create_d3_script(app_state.to_json())};
620
- await scriptFn();
621
- }});
622
- </script>
623
- </div>
624
- """,
625
- visible=False
626
- )
627
 
628
- return demo
629
-
630
- # ============================================================================
631
- # MAIN APPLICATION - Entry point and initialization
632
- # ============================================================================
633
-
634
- def create_application():
635
- """Create and configure the complete application."""
636
- # Create application components
637
- app_state = ApplicationFactory.create_app_state()
638
- argilla_service = ApplicationFactory.create_argilla_service()
639
 
640
- # Clean up existing webhooks
641
- ApplicationFactory.cleanup_existing_webhooks(argilla_service.client)
642
-
643
- # Create and register webhook handler
644
- webhook_handler = ApplicationFactory.create_webhook_handler(app_state)
645
-
646
- # Create the UI
647
- demo = ApplicationFactory.create_ui(argilla_service, app_state)
648
-
649
- # Mount the Gradio app to the FastAPI server
650
- server = argilla_service.get_server()
651
- gr.mount_gradio_app(server, demo, path="/")
652
-
653
- return server
654
 
655
- # Application entry point
656
  if __name__ == "__main__":
657
- import uvicorn
658
-
659
- # Create the application
660
- server = create_application()
661
-
662
- # Start the server
663
- uvicorn.run(server, host="0.0.0.0", port=7860)
 
 
 
 
1
  import gradio as gr
2
+ import json
3
+ import random
 
 
 
 
 
 
 
4
 
5
+ # Sample country data with random progress percentages
6
+ COUNTRY_DATA = {
7
+ "MX": {"name": "Mexico", "percent": random.randint(10, 90)},
8
+ "AR": {"name": "Argentina", "percent": random.randint(10, 90)},
9
+ "CO": {"name": "Colombia", "percent": random.randint(10, 90)},
10
+ "CL": {"name": "Chile", "percent": random.randint(10, 90)},
11
+ "PE": {"name": "Peru", "percent": random.randint(10, 90)},
12
+ "ES": {"name": "Spain", "percent": random.randint(10, 90)},
13
+ "BR": {"name": "Brazil", "percent": random.randint(10, 90)},
14
+ "VE": {"name": "Venezuela", "percent": random.randint(10, 90)},
15
+ "EC": {"name": "Ecuador", "percent": random.randint(10, 90)},
16
+ "BO": {"name": "Bolivia", "percent": random.randint(10, 90)},
17
+ "PY": {"name": "Paraguay", "percent": random.randint(10, 90)},
18
+ "UY": {"name": "Uruguay", "percent": random.randint(10, 90)},
19
+ "CR": {"name": "Costa Rica", "percent": random.randint(10, 90)},
20
+ "PA": {"name": "Panama", "percent": random.randint(10, 90)}
21
+ }
22
 
23
+ # Create the basic HTML container for the map
24
+ def create_map_container():
25
+ return """
26
+ <div id="map-container" style="width:100%; height:600px; position:relative; background-color:#111;">
27
+ <div style="display:flex; justify-content:center; align-items:center; height:100%; color:white; font-family:sans-serif;">
28
+ Loading map visualization...
29
+ </div>
30
+ </div>
31
+ <div id="tooltip" style="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;"></div>
32
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ # Create a script tag with the D3 code
35
+ def create_map_script():
36
+ # Convert country data to JSON for JavaScript
37
+ country_data_json = json.dumps(COUNTRY_DATA)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ return f"""
40
+ <script>
41
+ // Function to load D3.js and create the map
42
+ async function createMap() {{
43
+ // Load D3.js dynamically
44
+ await new Promise((resolve) => {{
45
+ const script = document.createElement('script');
46
+ script.src = 'https://d3js.org/d3.v7.min.js';
47
+ script.onload = resolve;
48
+ document.head.appendChild(script);
49
+ }});
 
 
50
 
51
+ console.log('D3 loaded successfully');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ // Country data from Python
54
+ const countryData = {country_data_json};
55
+
56
+ // Get container dimensions
57
+ const container = document.getElementById('map-container');
58
+ const width = container.clientWidth;
59
+ const height = container.clientHeight;
60
+
61
+ // Clear loading message
62
+ container.innerHTML = '';
63
+
64
+ // Create SVG
65
+ const svg = d3.select('#map-container')
66
+ .append('svg')
67
+ .attr('width', width)
68
+ .attr('height', height)
69
+ .attr('viewBox', `0 0 ${{width}} ${{height}}`);
70
+
71
+ // Create color scale
72
+ const colorScale = d3.scaleLinear()
73
+ .domain([0, 100])
74
+ .range(['#4a1942', '#f32b7b']);
75
+
76
+ // Set up projection focused on Latin America
77
+ const projection = d3.geoMercator()
78
+ .center([-60, 0])
79
+ .scale(width / 5)
80
+ .translate([width / 2, height / 2]);
81
+
82
+ const path = d3.geoPath().projection(projection);
83
+
84
+ // Tooltip setup
85
+ const tooltip = d3.select('#tooltip');
86
+
87
+ // Load GeoJSON data
88
+ try {{
89
+ const response = await fetch('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  const data = await response.json();
91
 
92
+ // Draw countries
93
+ svg.selectAll('path')
94
  .data(data.features)
95
  .enter()
96
+ .append('path')
97
+ .attr('d', path)
98
+ .attr('stroke', '#f32b7b')
99
+ .attr('stroke-width', 1)
100
+ .attr('fill', d => {{
101
  // Get the ISO code from the properties
102
  const iso = d.properties.iso_a2;
103
 
104
+ if (countryData[iso]) {{
105
+ return colorScale(countryData[iso].percent);
106
  }}
107
+ return '#2d3748'; // Default gray for other countries
108
  }})
109
+ .on('mouseover', function(event, d) {{
110
  const iso = d.properties.iso_a2;
111
 
112
  d3.select(this)
113
+ .attr('stroke', '#4a1942')
114
+ .attr('stroke-width', 2);
115
 
116
+ if (countryData[iso]) {{
117
+ tooltip.style('opacity', 1)
118
+ .style('left', (event.pageX + 15) + 'px')
119
+ .style('top', (event.pageY + 15) + 'px')
120
  .html(`
121
+ <strong>${{countryData[iso].name}}</strong><br/>
122
+ Progress: ${{countryData[iso].percent}}%
 
123
  `);
124
  }}
125
  }})
126
+ .on('mousemove', function(event) {{
127
+ tooltip.style('left', (event.pageX + 15) + 'px')
128
+ .style('top', (event.pageY + 15) + 'px');
129
  }})
130
+ .on('mouseout', function() {{
131
  d3.select(this)
132
+ .attr('stroke', '#f32b7b')
133
+ .attr('stroke-width', 1);
134
 
135
+ tooltip.style('opacity', 0);
136
  }});
137
 
138
+ // Add a legend
139
  const legendWidth = Math.min(width - 40, 200);
140
  const legendHeight = 15;
141
  const legendX = width - legendWidth - 20;
142
 
143
+ const legend = svg.append('g')
144
+ .attr('transform', `translate(${{legendX}}, 30)`);
 
145
 
146
  // Create gradient for legend
147
+ const defs = svg.append('defs');
148
+ const gradient = defs.append('linearGradient')
149
+ .attr('id', 'dataGradient')
150
+ .attr('x1', '0%')
151
+ .attr('y1', '0%')
152
+ .attr('x2', '100%')
153
+ .attr('y2', '0%');
154
+
155
+ gradient.append('stop')
156
+ .attr('offset', '0%')
157
+ .attr('stop-color', '#4a1942');
158
+
159
+ gradient.append('stop')
160
+ .attr('offset', '100%')
161
+ .attr('stop-color', '#f32b7b');
162
 
163
  // Add legend title
164
+ legend.append('text')
165
+ .attr('x', legendWidth / 2)
166
+ .attr('y', -10)
167
+ .attr('text-anchor', 'middle')
168
+ .attr('font-size', '12px')
169
+ .attr('fill', '#f1f5f9')
170
+ .text('Progress');
171
 
172
  // Add legend rectangle
173
+ legend.append('rect')
174
+ .attr('width', legendWidth)
175
+ .attr('height', legendHeight)
176
+ .attr('rx', 2)
177
+ .attr('ry', 2)
178
+ .style('fill', 'url(#dataGradient)');
179
 
180
  // Add legend labels
181
+ legend.append('text')
182
+ .attr('x', 0)
183
+ .attr('y', legendHeight + 15)
184
+ .attr('text-anchor', 'start')
185
+ .attr('font-size', '10px')
186
+ .attr('fill', '#94a3b8')
187
+ .text('0%');
188
+
189
+ legend.append('text')
190
+ .attr('x', legendWidth / 2)
191
+ .attr('y', legendHeight + 15)
192
+ .attr('text-anchor', 'middle')
193
+ .attr('font-size', '10px')
194
+ .attr('fill', '#94a3b8')
195
+ .text('50%');
196
+
197
+ legend.append('text')
198
+ .attr('x', legendWidth)
199
+ .attr('y', legendHeight + 15)
200
+ .attr('text-anchor', 'end')
201
+ .attr('font-size', '10px')
202
+ .attr('fill', '#94a3b8')
203
+ .text('100%');
204
+
205
  // Handle window resize
206
+ window.addEventListener('resize', () => {{
207
+ const width = container.clientWidth;
208
 
209
  // Update SVG dimensions
210
+ svg.attr('width', width)
211
+ .attr('viewBox', `0 0 ${{width}} ${{height}}`);
 
212
 
213
  // Update projection
214
  projection.scale(width / 5)
215
  .translate([width / 2, height / 2]);
216
 
217
  // Update paths
218
+ svg.selectAll('path').attr('d', path);
219
 
220
  // Update legend position
221
  const legendWidth = Math.min(width - 40, 200);
222
  const legendX = width - legendWidth - 20;
223
 
224
+ legend.attr('transform', `translate(${{legendX}}, 30)`);
225
+ }});
226
+ }} catch (error) {{
227
+ console.error('Error loading or rendering the map:', error);
228
+ container.innerHTML = `<div style="color: white; text-align: center;">Error loading map: ${{error.message}}</div>`;
229
  }}
230
+ }}
231
 
232
+ // Call the function when the DOM is ready
233
+ document.addEventListener('DOMContentLoaded', createMap);
234
+ </script>
235
+ """
236
 
237
+ # Create a simple Gradio interface
238
+ with gr.Blocks(theme=gr.themes.Soft(primary_hue="pink", secondary_hue="purple")) as demo:
239
+ gr.Markdown("# Latin America & Spain Map")
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ # Create a container for the map
242
+ map_container = gr.HTML(create_map_container())
 
 
243
 
244
+ # Create a container for the script
245
+ script_container = gr.HTML(create_map_script(), visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
+ # Button to generate new random data
248
+ def update_data():
249
+ # Create new random percentages
250
+ new_data = {
251
+ code: {"name": data["name"], "percent": random.randint(10, 90)}
252
+ for code, data in COUNTRY_DATA.items()
253
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
+ # Update the global variable
256
+ global COUNTRY_DATA
257
+ COUNTRY_DATA = new_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
+ # Return the updated script
260
+ return create_map_script()
 
 
 
 
 
 
 
 
 
261
 
262
+ gr.Button("Generate New Random Data").click(fn=update_data, outputs=script_container)
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
 
264
  if __name__ == "__main__":
265
+ demo.launch()