Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -44,17 +44,29 @@ HTML_TEMPLATE = """
|
|
44 |
body {
|
45 |
margin: 0;
|
46 |
padding: 20px;
|
47 |
-
background-color: #
|
48 |
color: #fff;
|
49 |
font-family: sans-serif;
|
50 |
}
|
51 |
h1 {
|
52 |
margin-bottom: 20px;
|
53 |
}
|
54 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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
|
135 |
const relevantCountryCodes = Object.keys(countryData);
|
136 |
|
137 |
-
//
|
|
|
|
|
|
|
|
|
|
|
138 |
svg.selectAll('.country')
|
139 |
-
.data(
|
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', '#
|
161 |
-
.attr('stroke-width',
|
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', '#
|
176 |
.attr('stroke-width', 1);
|
177 |
|
178 |
tooltip.style('opacity', 0);
|
179 |
});
|
180 |
|
181 |
-
// Add a legend
|
182 |
-
const legendWidth =
|
183 |
const legendHeight = 15;
|
184 |
const legendX = width - legendWidth - 20;
|
|
|
185 |
|
|
|
186 |
const legend = svg.append('g')
|
187 |
-
.attr('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
// Create gradient for legend
|
190 |
const defs = svg.append('defs');
|
191 |
const gradient = defs.append('linearGradient')
|
192 |
-
.attr('id', '
|
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 |
-
.
|
220 |
-
.attr('ry', 2)
|
221 |
-
.style('fill', 'url(#dataGradient)');
|
222 |
|
223 |
-
// Add
|
224 |
legend.append('text')
|
225 |
.attr('x', 0)
|
226 |
.attr('y', legendHeight + 15)
|
227 |
.attr('text-anchor', 'start')
|
228 |
-
.
|
229 |
-
.
|
230 |
.text('0%');
|
231 |
|
232 |
legend.append('text')
|
233 |
.attr('x', legendWidth / 2)
|
234 |
.attr('y', legendHeight + 15)
|
235 |
.attr('text-anchor', 'middle')
|
236 |
-
.
|
237 |
-
.
|
238 |
.text('50%');
|
239 |
|
240 |
legend.append('text')
|
241 |
.attr('x', legendWidth)
|
242 |
.attr('y', legendHeight + 15)
|
243 |
.attr('text-anchor', 'end')
|
244 |
-
.
|
245 |
-
.
|
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
|
271 |
-
|
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 |
|