ouhenio commited on
Commit
9ed5b46
·
verified ·
1 Parent(s): 5de44fc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -66
app.py CHANGED
@@ -44,17 +44,29 @@ HTML_TEMPLATE = """
44
  body {
45
  margin: 0;
46
  padding: 20px;
47
- background-color: #111;
48
  color: #fff;
49
  font-family: sans-serif;
50
  }
51
  h1 {
52
  margin-bottom: 20px;
53
  }
54
- #map-container {
 
55
  width: 100%;
 
 
 
56
  height: 600px;
57
  position: relative;
 
 
 
 
 
 
 
 
58
  }
59
  #tooltip {
60
  position: absolute;
@@ -69,24 +81,99 @@ HTML_TEMPLATE = """
69
  border: 1px solid rgba(255, 255, 255, 0.2);
70
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
71
  }
72
- .ocean {
73
- fill: #111;
74
- }
75
  .country {
76
- stroke: #444;
77
- stroke-width: 0.5;
78
- fill: #222;
79
- }
80
- .selected-country {
81
- stroke: #f32b7b;
82
- stroke-width: 1;
83
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
  </style>
86
  </head>
87
  <body>
88
  <h1>Latin America & Spain Progress Map</h1>
89
- <div id="map-container"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  <div id="tooltip"></div>
91
 
92
  <script>
@@ -97,7 +184,7 @@ HTML_TEMPLATE = """
97
  // Set up dimensions
98
  const container = document.getElementById('map-container');
99
  const width = container.clientWidth;
100
- const height = container.clientHeight || 600;
101
 
102
  // Create SVG
103
  const svg = d3.select('#map-container')
@@ -106,21 +193,15 @@ HTML_TEMPLATE = """
106
  .attr('height', height)
107
  .attr('viewBox', '0 0 ' + width + ' ' + height);
108
 
109
- // Add ocean background
110
- svg.append('rect')
111
- .attr('class', 'ocean')
112
- .attr('width', width)
113
- .attr('height', height);
114
-
115
  // Create color scale
116
  const colorScale = d3.scaleLinear()
117
  .domain([0, 100])
118
  .range(['#4a1942', '#f32b7b']);
119
 
120
- // Set up projection focused on Latin America and Spain
121
  const projection = d3.geoMercator()
122
  .center([-60, 0])
123
- .scale(width / 5)
124
  .translate([width / 2, height / 2]);
125
 
126
  const path = d3.geoPath().projection(projection);
@@ -131,34 +212,33 @@ HTML_TEMPLATE = """
131
  // Load GeoJSON data
132
  d3.json('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson')
133
  .then(function(data) {
134
- // Filter out non-relevant countries
135
  const relevantCountryCodes = Object.keys(countryData);
136
 
137
- // First draw all countries with a neutral background
 
 
 
 
 
138
  svg.selectAll('.country')
139
- .data(data.features)
140
  .enter()
141
  .append('path')
142
  .attr('class', 'country')
143
- .attr('d', path);
144
-
145
- // Then draw only our target countries on top
146
- svg.selectAll('.selected-country')
147
- .data(data.features.filter(d => relevantCountryCodes.includes(d.properties.iso_a2)))
148
- .enter()
149
- .append('path')
150
- .attr('class', 'selected-country')
151
  .attr('d', path)
152
  .attr('fill', function(d) {
153
  const iso = d.properties.iso_a2;
154
  return colorScale(countryData[iso].percent);
155
  })
 
 
156
  .on('mouseover', function(event, d) {
157
  const iso = d.properties.iso_a2;
158
 
159
  d3.select(this)
160
- .attr('stroke', '#4a1942')
161
- .attr('stroke-width', 2);
162
 
163
  tooltip.style('opacity', 1)
164
  .style('left', (event.pageX + 15) + 'px')
@@ -172,27 +252,38 @@ HTML_TEMPLATE = """
172
  })
173
  .on('mouseout', function() {
174
  d3.select(this)
175
- .attr('stroke', '#f32b7b')
176
  .attr('stroke-width', 1);
177
 
178
  tooltip.style('opacity', 0);
179
  });
180
 
181
- // Add a legend
182
- const legendWidth = Math.min(width - 40, 200);
183
  const legendHeight = 15;
184
  const legendX = width - legendWidth - 20;
 
185
 
 
186
  const legend = svg.append('g')
187
- .attr('transform', 'translate(' + legendX + ', 30)');
 
 
 
 
 
 
 
 
 
188
 
189
  // Create gradient for legend
190
  const defs = svg.append('defs');
191
  const gradient = defs.append('linearGradient')
192
- .attr('id', 'dataGradient')
193
  .attr('x1', '0%')
194
- .attr('y1', '0%')
195
  .attr('x2', '100%')
 
196
  .attr('y2', '0%');
197
 
198
  gradient.append('stop')
@@ -203,74 +294,119 @@ HTML_TEMPLATE = """
203
  .attr('offset', '100%')
204
  .attr('stop-color', '#f32b7b');
205
 
206
- // Add legend title
207
- legend.append('text')
208
- .attr('x', legendWidth / 2)
209
- .attr('y', -10)
210
- .attr('text-anchor', 'middle')
211
- .attr('font-size', '12px')
212
- .attr('fill', '#f1f5f9')
213
- .text('Progress');
214
-
215
  // Add legend rectangle
216
  legend.append('rect')
217
  .attr('width', legendWidth)
218
  .attr('height', legendHeight)
219
- .attr('rx', 2)
220
- .attr('ry', 2)
221
- .style('fill', 'url(#dataGradient)');
222
 
223
- // Add legend labels
224
  legend.append('text')
225
  .attr('x', 0)
226
  .attr('y', legendHeight + 15)
227
  .attr('text-anchor', 'start')
228
- .attr('font-size', '10px')
229
- .attr('fill', '#94a3b8')
230
  .text('0%');
231
 
232
  legend.append('text')
233
  .attr('x', legendWidth / 2)
234
  .attr('y', legendHeight + 15)
235
  .attr('text-anchor', 'middle')
236
- .attr('font-size', '10px')
237
- .attr('fill', '#94a3b8')
238
  .text('50%');
239
 
240
  legend.append('text')
241
  .attr('x', legendWidth)
242
  .attr('y', legendHeight + 15)
243
  .attr('text-anchor', 'end')
244
- .attr('font-size', '10px')
245
- .attr('fill', '#94a3b8')
246
  .text('100%');
 
 
 
247
  })
248
  .catch(function(error) {
249
  console.error('Error loading or rendering the map:', error);
250
  container.innerHTML = '<div style="color: white; text-align: center;">Error loading map: ' + error.message + '</div>';
251
  });
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  // Handle window resize
254
  window.addEventListener('resize', function() {
255
  const width = container.clientWidth;
 
256
 
257
  // Update SVG dimensions
258
  d3.select('svg')
259
  .attr('width', width)
 
260
  .attr('viewBox', '0 0 ' + width + ' ' + height);
261
 
262
  // Update projection
263
- projection.scale(width / 5)
264
  .translate([width / 2, height / 2]);
265
 
266
  // Update paths
267
  d3.selectAll('path').attr('d', path);
268
 
269
  // Update legend position
270
- const legendWidth = Math.min(width - 40, 200);
271
- const legendX = width - legendWidth - 20;
272
-
273
- d3.select('g').attr('transform', 'translate(' + legendX + ', 30)');
274
  });
275
  });
276
  </script>
@@ -281,9 +417,13 @@ HTML_TEMPLATE = """
281
  # Route to serve the map visualization
282
  @app.get("/d3-map")
283
  async def serve_map():
284
- # Generate random data
285
  country_data = generate_data()
286
 
 
 
 
 
287
  # Convert to JSON for JavaScript
288
  country_data_json = json.dumps(country_data)
289
 
 
44
  body {
45
  margin: 0;
46
  padding: 20px;
47
+ background-color: #0f1218;
48
  color: #fff;
49
  font-family: 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;
 
81
  border: 1px solid rgba(255, 255, 255, 0.2);
82
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
83
  }
 
 
 
84
  .country {
 
 
 
 
 
 
 
85
  cursor: pointer;
86
+ transition: opacity 0.3s;
87
+ }
88
+ .country:hover {
89
+ opacity: 0.8;
90
+ }
91
+ .stat-title {
92
+ font-size: 1.2rem;
93
+ margin-bottom: 20px;
94
+ font-weight: bold;
95
+ }
96
+ .stat-item {
97
+ margin-bottom: 10px;
98
+ color: #abb4c2;
99
+ }
100
+ .stat-value {
101
+ font-weight: bold;
102
+ color: white;
103
+ }
104
+ .stat-bar-container {
105
+ width: 100%;
106
+ height: 8px;
107
+ background-color: #30363d;
108
+ border-radius: 4px;
109
+ margin-top: 5px;
110
+ overflow: hidden;
111
+ }
112
+ .stat-bar {
113
+ height: 100%;
114
+ background: linear-gradient(to right, #4a1942, #f32b7b);
115
+ border-radius: 4px;
116
+ }
117
+ .top-countries {
118
+ margin-top: 30px;
119
+ }
120
+ .country-stat {
121
+ display: flex;
122
+ justify-content: space-between;
123
+ margin-bottom: 8px;
124
+ align-items: center;
125
+ }
126
+ .country-bar {
127
+ width: 60%;
128
+ height: 6px;
129
+ background-color: #30363d;
130
+ border-radius: 3px;
131
+ overflow: hidden;
132
+ }
133
+ .country-bar-fill {
134
+ height: 100%;
135
+ background: linear-gradient(to right, #4a1942, #f32b7b);
136
+ border-radius: 3px;
137
+ }
138
+ .legend {
139
+ margin-top: 20px;
140
  }
141
  </style>
142
  </head>
143
  <body>
144
  <h1>Latin America & Spain Progress Map</h1>
145
+
146
+ <div class="container">
147
+ <div class="stats-container">
148
+ <div class="stat-title">Resumen General</div>
149
+
150
+ <div class="stat-item">
151
+ Países en la base de datos: <span class="stat-value">20</span>
152
+ </div>
153
+
154
+ <div class="stat-item">
155
+ Total de documentos: <span class="stat-value" id="total-docs">0</span>
156
+ </div>
157
+
158
+ <div class="stat-item">
159
+ Promedio de completitud: <span class="stat-value" id="avg-percent">0%</span>
160
+ </div>
161
+
162
+ <div class="top-countries">
163
+ <div class="stat-item">Los 5 países con mayor recolección:</div>
164
+ <div id="top-countries-list">
165
+ <!-- Will be populated by JavaScript -->
166
+ </div>
167
+ </div>
168
+
169
+ <div class="stat-item" style="margin-top: 30px; font-style: italic; font-size: 0.9em;">
170
+ Selecciona un país en el mapa para ver información detallada.
171
+ </div>
172
+ </div>
173
+
174
+ <div class="map-container" id="map-container"></div>
175
+ </div>
176
+
177
  <div id="tooltip"></div>
178
 
179
  <script>
 
184
  // Set up dimensions
185
  const container = document.getElementById('map-container');
186
  const width = container.clientWidth;
187
+ const height = container.clientHeight;
188
 
189
  // Create SVG
190
  const svg = d3.select('#map-container')
 
193
  .attr('height', height)
194
  .attr('viewBox', '0 0 ' + width + ' ' + height);
195
 
 
 
 
 
 
 
196
  // Create color scale
197
  const colorScale = d3.scaleLinear()
198
  .domain([0, 100])
199
  .range(['#4a1942', '#f32b7b']);
200
 
201
+ // Set up projection focused on Latin America with Spain
202
  const projection = d3.geoMercator()
203
  .center([-60, 0])
204
+ .scale(width / 3.5)
205
  .translate([width / 2, height / 2]);
206
 
207
  const path = d3.geoPath().projection(projection);
 
212
  // Load GeoJSON data
213
  d3.json('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson')
214
  .then(function(data) {
215
+ // Filter to only include our target countries
216
  const relevantCountryCodes = Object.keys(countryData);
217
 
218
+ // Filter the features
219
+ const relevantFeatures = data.features.filter(d =>
220
+ relevantCountryCodes.includes(d.properties.iso_a2)
221
+ );
222
+
223
+ // Draw only our target countries
224
  svg.selectAll('.country')
225
+ .data(relevantFeatures)
226
  .enter()
227
  .append('path')
228
  .attr('class', 'country')
 
 
 
 
 
 
 
 
229
  .attr('d', path)
230
  .attr('fill', function(d) {
231
  const iso = d.properties.iso_a2;
232
  return colorScale(countryData[iso].percent);
233
  })
234
+ .attr('stroke', '#0f1218')
235
+ .attr('stroke-width', 1)
236
  .on('mouseover', function(event, d) {
237
  const iso = d.properties.iso_a2;
238
 
239
  d3.select(this)
240
+ .attr('stroke', '#fff')
241
+ .attr('stroke-width', 1.5);
242
 
243
  tooltip.style('opacity', 1)
244
  .style('left', (event.pageX + 15) + 'px')
 
252
  })
253
  .on('mouseout', function() {
254
  d3.select(this)
255
+ .attr('stroke', '#0f1218')
256
  .attr('stroke-width', 1);
257
 
258
  tooltip.style('opacity', 0);
259
  });
260
 
261
+ // Add a legend at the top right
262
+ const legendWidth = 200;
263
  const legendHeight = 15;
264
  const legendX = width - legendWidth - 20;
265
+ const legendY = 20;
266
 
267
+ // Create a legend group
268
  const legend = svg.append('g')
269
+ .attr('class', 'legend')
270
+ .attr('transform', 'translate(' + legendX + ',' + legendY + ')');
271
+
272
+ // Legend title
273
+ legend.append('text')
274
+ .attr('x', legendWidth / 2)
275
+ .attr('y', -5)
276
+ .attr('text-anchor', 'middle')
277
+ .style('fill', '#fff')
278
+ .text('Porcentaje de Datos Recolectados');
279
 
280
  // Create gradient for legend
281
  const defs = svg.append('defs');
282
  const gradient = defs.append('linearGradient')
283
+ .attr('id', 'legendGradient')
284
  .attr('x1', '0%')
 
285
  .attr('x2', '100%')
286
+ .attr('y1', '0%')
287
  .attr('y2', '0%');
288
 
289
  gradient.append('stop')
 
294
  .attr('offset', '100%')
295
  .attr('stop-color', '#f32b7b');
296
 
 
 
 
 
 
 
 
 
 
297
  // Add legend rectangle
298
  legend.append('rect')
299
  .attr('width', legendWidth)
300
  .attr('height', legendHeight)
301
+ .style('fill', 'url(#legendGradient)');
 
 
302
 
303
+ // Add min and max labels
304
  legend.append('text')
305
  .attr('x', 0)
306
  .attr('y', legendHeight + 15)
307
  .attr('text-anchor', 'start')
308
+ .style('fill', '#fff')
309
+ .style('font-size', '12px')
310
  .text('0%');
311
 
312
  legend.append('text')
313
  .attr('x', legendWidth / 2)
314
  .attr('y', legendHeight + 15)
315
  .attr('text-anchor', 'middle')
316
+ .style('fill', '#fff')
317
+ .style('font-size', '12px')
318
  .text('50%');
319
 
320
  legend.append('text')
321
  .attr('x', legendWidth)
322
  .attr('y', legendHeight + 15)
323
  .attr('text-anchor', 'end')
324
+ .style('fill', '#fff')
325
+ .style('font-size', '12px')
326
  .text('100%');
327
+
328
+ // Update statistics
329
+ updateStatistics();
330
  })
331
  .catch(function(error) {
332
  console.error('Error loading or rendering the map:', error);
333
  container.innerHTML = '<div style="color: white; text-align: center;">Error loading map: ' + error.message + '</div>';
334
  });
335
 
336
+ // Function to update statistics
337
+ function updateStatistics() {
338
+ // Calculate total documents (mock data)
339
+ const totalDocs = Object.values(countryData).reduce((sum, country) => {
340
+ return sum + (country.documents || Math.floor(Math.random() * 100000) + 50000);
341
+ }, 0);
342
+
343
+ // Calculate average percentage
344
+ const avgPercent = Object.values(countryData).reduce((sum, country) => {
345
+ return sum + country.percent;
346
+ }, 0) / Object.values(countryData).length;
347
+
348
+ // Update the stats
349
+ document.getElementById('total-docs').textContent = totalDocs.toLocaleString();
350
+ document.getElementById('avg-percent').textContent = avgPercent.toFixed(1) + '%';
351
+
352
+ // Create an array of countries with document counts
353
+ const countriesWithDocs = Object.keys(countryData).map(code => {
354
+ return {
355
+ code: code,
356
+ name: countryData[code].name,
357
+ percent: countryData[code].percent,
358
+ docs: countryData[code].documents || Math.floor(Math.random() * 100000) + 50000
359
+ };
360
+ });
361
+
362
+ // Sort by document count descending
363
+ countriesWithDocs.sort((a, b) => b.docs - a.docs);
364
+
365
+ // Take the top 5
366
+ const topCountries = countriesWithDocs.slice(0, 5);
367
+
368
+ // Update the top countries list
369
+ const topCountriesList = document.getElementById('top-countries-list');
370
+ topCountriesList.innerHTML = '';
371
+
372
+ topCountries.forEach(country => {
373
+ const countryDiv = document.createElement('div');
374
+ countryDiv.className = 'country-stat';
375
+
376
+ countryDiv.innerHTML = `
377
+ <span>${country.name}</span>
378
+ <div class="country-bar">
379
+ <div class="country-bar-fill" style="width: ${country.percent}%;"></div>
380
+ </div>
381
+ <span>${country.docs.toLocaleString()}</span>
382
+ `;
383
+
384
+ topCountriesList.appendChild(countryDiv);
385
+ });
386
+ }
387
+
388
  // Handle window resize
389
  window.addEventListener('resize', function() {
390
  const width = container.clientWidth;
391
+ const height = container.clientHeight;
392
 
393
  // Update SVG dimensions
394
  d3.select('svg')
395
  .attr('width', width)
396
+ .attr('height', height)
397
  .attr('viewBox', '0 0 ' + width + ' ' + height);
398
 
399
  // Update projection
400
+ projection.scale(width / 3.5)
401
  .translate([width / 2, height / 2]);
402
 
403
  // Update paths
404
  d3.selectAll('path').attr('d', path);
405
 
406
  // Update legend position
407
+ const legendX = width - 220;
408
+ d3.select('.legend')
409
+ .attr('transform', 'translate(' + legendX + ',20)');
 
410
  });
411
  });
412
  </script>
 
417
  # Route to serve the map visualization
418
  @app.get("/d3-map")
419
  async def serve_map():
420
+ # Generate random data for each country with document counts
421
  country_data = generate_data()
422
 
423
+ # Add random document counts
424
+ for code in country_data:
425
+ country_data[code]["documents"] = random.randint(50000, 700000)
426
+
427
  # Convert to JSON for JavaScript
428
  country_data_json = json.dumps(country_data)
429