fdaudens HF Staff commited on
Commit
7eca7c8
·
verified ·
1 Parent(s): 4b4df65

Add 1 files

Browse files
Files changed (1) hide show
  1. index.html +284 -223
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>World Data Visualizer</title>
7
  <script src="https://d3js.org/d3.v7.min.js"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
@@ -18,7 +18,7 @@
18
  .container {
19
  max-width: 1200px;
20
  margin: 0 auto;
21
- background-color: #fff;
22
  border-radius: 10px;
23
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
24
  padding: 20px;
@@ -42,7 +42,7 @@
42
  gap: 15px;
43
  margin-bottom: 25px;
44
  align-items: center;
45
- padding: 10px;
46
  background-color: #f8f9fa;
47
  border-radius: 8px;
48
  }
@@ -92,33 +92,41 @@
92
  border-radius: 4px;
93
  cursor: pointer;
94
  transition: all 0.3s;
 
 
 
95
  }
96
 
97
  .play-btn:hover {
98
  background-color: #27ae60;
99
  }
100
 
101
- .play-btn i {
102
- margin-right: 5px;
103
  }
104
 
105
  .year-display {
106
  font-weight: bold;
107
  min-width: 60px;
108
  text-align: center;
 
109
  }
110
 
111
  .chart-container {
112
  width: 100%;
113
  height: 600px;
114
  position: relative;
 
 
 
 
115
  }
116
 
117
  .tooltip {
118
  position: absolute;
119
- padding: 10px;
120
- background: rgba(0, 0, 0, 0.8);
121
- color: #fff;
122
  border-radius: 5px;
123
  pointer-events: none;
124
  text-align: center;
@@ -126,6 +134,9 @@
126
  transition: opacity 0.2s;
127
  font-size: 14px;
128
  z-index: 10;
 
 
 
129
  }
130
 
131
  .tooltip:after {
@@ -136,7 +147,7 @@
136
  margin-left: -8px;
137
  border-width: 8px;
138
  border-style: solid;
139
- border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
140
  }
141
 
142
  .loading {
@@ -166,6 +177,10 @@
166
  fill: #555;
167
  }
168
 
 
 
 
 
169
  @media (max-width: 768px) {
170
  .controls {
171
  flex-direction: column;
@@ -191,7 +206,7 @@
191
  <body>
192
  <div class="container">
193
  <h1>World Development Indicators</h1>
194
- <p class="subtitle">Life Expectancy vs. GDP per capita (1990-2023)</p>
195
 
196
  <div class="controls">
197
  <div class="filter-buttons" id="region-buttons">
@@ -203,14 +218,14 @@
203
  <i class="fas fa-play"></i> Play
204
  </button>
205
  <span class="year-display" id="year-value">1990</span>
206
- <input type="range" class="year-slider" id="year-slider" min="1990" max="2023" value="1990">
207
  </div>
208
  </div>
209
 
210
  <div class="chart-container" id="chart-area">
211
  <div class="loading" id="loading">
212
  <div class="spinner"></div>
213
- <p>Loading data...</p>
214
  </div>
215
  </div>
216
 
@@ -220,11 +235,11 @@
220
  <script>
221
  // Configuration
222
  const config = {
223
- margin: { top: 40, right: 40, bottom: 60, left: 70 },
224
- minBubbleSize: 5,
225
  maxBubbleSize: 40,
226
- animationDuration: 300,
227
- playInterval: 1000,
228
  regions: {
229
  "Africa": { color: "#e74c3c" },
230
  "Asia": { color: "#3498db" },
@@ -240,6 +255,8 @@
240
  let state = {
241
  data: null,
242
  filteredData: null,
 
 
243
  currentYear: 1990,
244
  activeRegions: Object.keys(config.regions),
245
  isPlaying: false,
@@ -263,7 +280,7 @@
263
 
264
  // Initialize the visualization
265
  async function init() {
266
- // Load data
267
  await loadData();
268
 
269
  // Setup UI controls
@@ -279,7 +296,7 @@
279
  dom.loading.style("display", "none");
280
  }
281
 
282
- // Load and process data
283
  async function loadData() {
284
  try {
285
  // Load the CSV data
@@ -291,282 +308,326 @@
291
  entity: d.Entity,
292
  code: d.Code,
293
  year: +d.Year,
294
- lifeExpectancy: d["Life expectancy - Sex: all - Age: 0 - Variant: estimates"] !== ""
295
- ? +d["Life expectancy - Sex: all - Age: 0 - Variant: estimates"]
296
- : null,
297
- gdpPerCapita: d["GDP per capita, PPP (constant 2021 international $)"] !== ""
298
- ? +d["GDP per capita, PPP (constant 2021 international $)"]
299
- : null,
300
- population: d["Population (historical)"] !== ""
301
- ? +d["Population (historical)"]
302
- : null,
303
- region: d["World regions according to OWID"] || "Unknown"
304
  };
305
- }).filter(d => d.gdpPerCapita !== null && d.lifeExpectancy !== null && d.population !== null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  } catch (error) {
307
  console.error("Error loading data:", error);
308
- dom.loading.html("<p>Error loading data. Please try again.</p>");
309
  }
310
  }
311
 
312
  // Setup UI controls
313
  function setupControls() {
314
- // Create region filter buttons
315
- Object.keys(config.regions).forEach(region => {
316
- dom.regionButtons.append("button")
317
- .classed("filter-btn", true)
318
- .style("background-color", config.regions[region].color)
 
 
319
  .text(region)
320
- .attr("data-region", region)
321
  .on("click", function() {
322
- toggleRegion(region);
323
- d3.select(this).classed("active", state.activeRegions.includes(region));
 
 
 
 
 
 
 
 
324
  });
325
  });
326
-
327
- // Year slider
328
  dom.yearSlider.on("input", function() {
329
  state.currentYear = +this.value;
330
  dom.yearValue.text(state.currentYear);
331
- if (!state.isPlaying) {
332
- updateChart();
333
- }
334
  });
335
-
336
- // Play button
337
- dom.playBtn.on("click", togglePlay);
338
- }
339
-
340
- // Toggle play/pause animation
341
- function togglePlay() {
342
- state.isPlaying = !state.isPlaying;
343
-
344
- if (state.isPlaying) {
345
- dom.playBtn.html('<i class="fas fa-pause"></i> Pause');
346
- dom.playBtn.style("background-color", "#e74c3c");
347
- playAnimation();
348
- } else {
349
- dom.playBtn.html('<i class="fas fa-play"></i> Play');
350
- dom.playBtn.style("background-color", "#2ecc71");
351
- pauseAnimation();
352
- }
353
- }
354
-
355
- // Play the animation
356
- function playAnimation() {
357
- clearInterval(state.playIntervalId);
358
 
359
- state.playIntervalId = setInterval(() => {
360
- if (state.currentYear < 2023) {
361
- state.currentYear++;
362
- dom.yearSlider.property("value", state.currentYear);
363
- dom.yearValue.text(state.currentYear);
364
- updateChart();
 
 
 
 
 
 
 
 
 
 
365
  } else {
366
- pauseAnimation();
 
 
 
 
 
367
  }
368
- }, config.playInterval);
369
- }
370
-
371
- // Pause the animation
372
- function pauseAnimation() {
373
- clearInterval(state.playIntervalId);
374
- state.isPlaying = false;
375
- dom.playBtn.html('<i class="fas fa-play"></i> Play');
376
- dom.playBtn.style("background-color", "#2ecc71");
377
- }
378
-
379
- // Toggle region filter
380
- function toggleRegion(region) {
381
- if (state.activeRegions.includes(region)) {
382
- state.activeRegions = state.activeRegions.filter(r => r !== region);
383
- } else {
384
- state.activeRegions = [...state.activeRegions, region];
385
- }
386
-
387
- if (state.activeRegions.length === 0) {
388
- // If all regions are deselected, select all
389
- state.activeRegions = Object.keys(config.regions);
390
- }
391
-
392
- updateChart();
393
  }
394
 
395
- // Initialize chart
396
  function initChart() {
 
 
 
397
  // Create SVG
398
- const width = dom.chartArea.node().clientWidth - config.margin.left - config.margin.right;
399
- const height = dom.chartArea.node().clientHeight - config.margin.top - config.margin.bottom;
400
 
401
  state.svg = dom.chartArea.append("svg")
402
- .attr("width", width + config.margin.left + config.margin.right)
403
- .attr("height", height + config.margin.top + config.margin.bottom)
404
- .append("g")
405
- .attr("transform", `translate(${config.margin.left},${config.margin.top})`);
 
 
406
 
407
  // Create scales
408
- const gdpExtent = d3.extent(state.data, d => d.gdpPerCapita);
409
- const lifeExpectancyExtent = d3.extent(state.data, d => d.lifeExpectancy);
410
- const populationExtent = d3.extent(state.data, d => d.population);
411
 
412
  state.x = d3.scaleLog()
413
- .domain([100, gdpExtent[1]])
414
- .range([0, width]);
415
 
416
  state.y = d3.scaleLinear()
417
- .domain([20, lifeExpectancyExtent[1] + 5])
418
- .range([height, 0]);
419
 
420
  state.size = d3.scaleSqrt()
421
- .domain(populationExtent)
422
  .range([config.minBubbleSize, config.maxBubbleSize]);
423
 
424
- // Add axes
425
- state.svg.append("g")
426
  .attr("class", "x-axis")
427
- .attr("transform", `translate(0,${height})`);
428
 
429
- state.svg.append("g")
430
  .attr("class", "y-axis");
431
 
432
  // Add axis labels
433
- state.svg.append("text")
434
  .attr("class", "axis-label")
435
- .attr("x", width / 2)
436
- .attr("y", height + config.margin.bottom - 10)
437
- .attr("text-anchor", "middle")
438
  .text("GDP per capita (PPP, constant 2021 international $)");
439
 
440
- state.svg.append("text")
441
  .attr("class", "axis-label")
442
  .attr("transform", "rotate(-90)")
443
- .attr("x", -height / 2)
444
- .attr("y", -config.margin.left + 15)
445
- .attr("text-anchor", "middle")
446
  .text("Life Expectancy (years)");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
 
449
- // Update chart with current filters and year
450
  function updateChart() {
451
- // Filter data for current year
452
- const yearData = state.data.filter(d =>
 
 
453
  d.year === state.currentYear &&
454
  state.activeRegions.includes(d.region)
455
  );
456
 
457
- // Update the chart
458
- updateBubbles(yearData);
459
- }
460
-
461
- // Update bubbles on the chart
462
- function updateBubbles(data) {
463
- // Update scales domain for current data
464
- const gdpExtent = d3.extent(data, d => d.gdpPerCapita);
465
- const lifeExpectancyExtent = d3.extent(data, d => d.lifeExpectancy);
466
 
467
- state.x.domain([Math.max(10, gdpExtent[0]), gdpExtent[1]]);
468
- state.y.domain([Math.max(20, lifeExpectancyExtent[0] - 5), lifeExpectancyExtent[1] + 5]);
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
  // Update axes
471
- const width = dom.chartArea.node().clientWidth - config.margin.left - config.margin.right;
472
- const height = dom.chartArea.node().clientHeight - config.margin.top - config.margin.bottom;
473
 
474
  state.svg.select(".x-axis")
475
  .transition()
476
  .duration(config.animationDuration)
477
- .call(d3.axisBottom(state.x).tickFormat(d3.format("$,.0f")));
478
 
479
  state.svg.select(".y-axis")
480
  .transition()
481
  .duration(config.animationDuration)
482
- .call(d3.axisLeft(state.y));
483
 
484
- // Bind data to bubbles
485
- const bubbles = state.svg.selectAll(".bubble")
486
- .data(data, d => d.entity);
487
 
488
- // Remove old bubbles
489
- bubbles.exit()
490
- .transition()
491
- .duration(config.animationDuration)
 
 
 
 
 
 
 
492
  .attr("r", 0)
493
- .style("opacity", 0)
494
  .remove();
495
 
496
- // Update existing bubbles
497
- bubbles.transition()
498
- .duration(config.animationDuration)
499
- .attr("cx", d => state.x(d.gdpPerCapita))
500
- .attr("cy", d => state.y(d.lifeExpectancy))
501
- .attr("r", d => state.size(d.population))
502
- .style("fill", d => config.regions[d.region]?.color || "#ccc")
503
- .style("opacity", 0.8);
504
-
505
- // Add new bubbles
506
- bubbles.enter()
507
- .append("circle")
508
- .attr("class", "bubble")
509
- .attr("cx", d => state.x(d.gdpPerCapita))
510
- .attr("cy", d => state.y(d.lifeExpectancy))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  .attr("r", d => state.size(d.population))
512
- .style("fill", d => config.regions[d.region]?.color || "#ccc")
513
- .style("opacity", 0)
514
- .on("mouseover", showTooltip)
515
- .on("mouseout", hideTooltip)
516
- .transition()
517
- .duration(config.animationDuration)
518
- .style("opacity", 0.8);
519
- }
520
-
521
- // Show tooltip
522
- function showTooltip(event, d) {
523
- const populationFormatted = d3.format(",")(Math.round(d.population / 1000000) * 1000000);
524
-
525
- dom.tooltip.html(`
526
- <strong>${d.entity}</strong><br>
527
- Region: ${d.region}<br>
528
- GDP: ${d3.format("$,.0f")(d.gdpPerCapita)}<br>
529
- Life Expectancy: ${d.lifeExpectancy.toFixed(1)} years<br>
530
- Population: ${populationFormatted}
531
- `)
532
- .style("left", (event.pageX) + "px")
533
- .style("top", (event.pageY - 28) + "px")
534
- .style("opacity", 1);
535
- }
536
-
537
- // Hide tooltip
538
- function hideTooltip() {
539
- dom.tooltip.style("opacity", 0);
540
- }
541
-
542
- // Handle window resize
543
- function handleResize() {
544
- const width = dom.chartArea.node().clientWidth - config.margin.left - config.margin.right;
545
- const height = dom.chartArea.node().clientHeight - config.margin.top - config.margin.bottom;
546
-
547
- state.svg.attr("width", width + config.margin.left + config.margin.right)
548
- .attr("height", height + config.margin.top + config.margin.bottom);
549
-
550
- state.x.range([0, width]);
551
- state.y.range([height, 0]);
552
-
553
- state.svg.select(".x-axis")
554
- .attr("transform", `translate(0,${height})`)
555
- .call(d3.axisBottom(state.x).tickFormat(d3.format("$,.0f")));
556
-
557
- state.svg.select(".y-axis")
558
- .call(d3.axisLeft(state.y));
559
-
560
- state.svg.select(".axis-label")
561
- .attr("x", width / 2)
562
- .attr("y", height + config.margin.bottom - 10);
563
 
564
- updateChart();
565
- }
566
-
567
- // Initialize the visualization when the window loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  window.addEventListener("load", init);
569
- window.addEventListener("resize", handleResize);
 
 
 
 
 
 
 
570
  </script>
571
- <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body>
572
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>World Development Indicators</title>
7
  <script src="https://d3js.org/d3.v7.min.js"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
 
18
  .container {
19
  max-width: 1200px;
20
  margin: 0 auto;
21
+ background-color: white;
22
  border-radius: 10px;
23
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
24
  padding: 20px;
 
42
  gap: 15px;
43
  margin-bottom: 25px;
44
  align-items: center;
45
+ padding: 15px;
46
  background-color: #f8f9fa;
47
  border-radius: 8px;
48
  }
 
92
  border-radius: 4px;
93
  cursor: pointer;
94
  transition: all 0.3s;
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 5px;
98
  }
99
 
100
  .play-btn:hover {
101
  background-color: #27ae60;
102
  }
103
 
104
+ .play-btn.playing {
105
+ background-color: #e74c3c;
106
  }
107
 
108
  .year-display {
109
  font-weight: bold;
110
  min-width: 60px;
111
  text-align: center;
112
+ font-size: 16px;
113
  }
114
 
115
  .chart-container {
116
  width: 100%;
117
  height: 600px;
118
  position: relative;
119
+ border-radius: 8px;
120
+ overflow: hidden;
121
+ background-color: white;
122
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
123
  }
124
 
125
  .tooltip {
126
  position: absolute;
127
+ padding: 12px;
128
+ background: rgba(0, 0, 0, 0.85);
129
+ color: white;
130
  border-radius: 5px;
131
  pointer-events: none;
132
  text-align: center;
 
134
  transition: opacity 0.2s;
135
  font-size: 14px;
136
  z-index: 10;
137
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
138
+ max-width: 220px;
139
+ line-height: 1.4;
140
  }
141
 
142
  .tooltip:after {
 
147
  margin-left: -8px;
148
  border-width: 8px;
149
  border-style: solid;
150
+ border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent;
151
  }
152
 
153
  .loading {
 
177
  fill: #555;
178
  }
179
 
180
+ .legend {
181
+ font-size: 12px;
182
+ }
183
+
184
  @media (max-width: 768px) {
185
  .controls {
186
  flex-direction: column;
 
206
  <body>
207
  <div class="container">
208
  <h1>World Development Indicators</h1>
209
+ <p class="subtitle">Life Expectancy vs. GDP per capita</p>
210
 
211
  <div class="controls">
212
  <div class="filter-buttons" id="region-buttons">
 
218
  <i class="fas fa-play"></i> Play
219
  </button>
220
  <span class="year-display" id="year-value">1990</span>
221
+ <input type="range" class="year-slider" id="year-slider" min="1950" max="2023" value="1990" step="1">
222
  </div>
223
  </div>
224
 
225
  <div class="chart-container" id="chart-area">
226
  <div class="loading" id="loading">
227
  <div class="spinner"></div>
228
+ <p>Loading data visualization...</p>
229
  </div>
230
  </div>
231
 
 
235
  <script>
236
  // Configuration
237
  const config = {
238
+ margin: { top: 50, right: 40, bottom: 70, left: 80 },
239
+ minBubbleSize: 3,
240
  maxBubbleSize: 40,
241
+ animationDuration: 500,
242
+ playInterval: 800,
243
  regions: {
244
  "Africa": { color: "#e74c3c" },
245
  "Asia": { color: "#3498db" },
 
255
  let state = {
256
  data: null,
257
  filteredData: null,
258
+ minYear: 1950,
259
+ maxYear: 2023,
260
  currentYear: 1990,
261
  activeRegions: Object.keys(config.regions),
262
  isPlaying: false,
 
280
 
281
  // Initialize the visualization
282
  async function init() {
283
+ // Load data from CSV file
284
  await loadData();
285
 
286
  // Setup UI controls
 
296
  dom.loading.style("display", "none");
297
  }
298
 
299
+ // Load and process data from CSV
300
  async function loadData() {
301
  try {
302
  // Load the CSV data
 
308
  entity: d.Entity,
309
  code: d.Code,
310
  year: +d.Year,
311
+ lifeExpectancy: d['Life expectancy - Sex: all - Age: 0 - Variant: estimates'] ? +d['Life expectancy - Sex: all - Age: 0 - Variant: estimates'] : null,
312
+ gdpPerCapita: d['GDP per capita, PPP (constant 2021 international $)'] ? +d['GDP per capita, PPP (constant 2021 international $)'] : null,
313
+ population: d['Population (historical)'] ? +d['Population (historical)'] : null,
314
+ region: d['World regions according to OWID'] || 'Unknown'
 
 
 
 
 
 
315
  };
316
+ }).filter(d =>
317
+ d.lifeExpectancy !== null &&
318
+ d.gdpPerCapita !== null &&
319
+ d.population !== null
320
+ );
321
+
322
+ // Filter out any invalid data points
323
+ state.data = state.data.filter(d =>
324
+ !isNaN(d.year) &&
325
+ !isNaN(d.lifeExpectancy) &&
326
+ !isNaN(d.gdpPerCapita) &&
327
+ !isNaN(d.population)
328
+ );
329
+
330
+ // Determine min and max years from data
331
+ state.minYear = d3.min(state.data, d => d.year);
332
+ state.maxYear = d3.max(state.data, d => d.year);
333
+ state.currentYear = state.minYear;
334
+
335
+ // Update slider range
336
+ dom.yearSlider.attr("min", state.minYear)
337
+ .attr("max", state.maxYear)
338
+ .attr("value", state.currentYear);
339
+ dom.yearValue.text(state.currentYear);
340
+
341
+ console.log("Data loaded successfully:", state.data);
342
  } catch (error) {
343
  console.error("Error loading data:", error);
344
+ dom.loading.html(`<p style="color: #e74c3c;">Error loading data. Please check if the 'world-data.csv' file exists.</p>`);
345
  }
346
  }
347
 
348
  // Setup UI controls
349
  function setupControls() {
350
+ // Create region buttons
351
+ dom.regionButtons.selectAll("*").remove();
352
+
353
+ Object.entries(config.regions).forEach(([region, { color }]) => {
354
+ const btn = dom.regionButtons.append("button")
355
+ .attr("class", "filter-btn active")
356
+ .style("background-color", color)
357
  .text(region)
 
358
  .on("click", function() {
359
+ const isActive = d3.select(this).classed("active");
360
+ d3.select(this).classed("active", !isActive);
361
+
362
+ if (isActive) {
363
+ state.activeRegions = state.activeRegions.filter(r => r !== region);
364
+ } else {
365
+ state.activeRegions.push(region);
366
+ }
367
+
368
+ updateChart();
369
  });
370
  });
371
+
372
+ // Year slider event
373
  dom.yearSlider.on("input", function() {
374
  state.currentYear = +this.value;
375
  dom.yearValue.text(state.currentYear);
376
+ updateChart();
 
 
377
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
+ // Play button event
380
+ dom.playBtn.on("click", function() {
381
+ state.isPlaying = !state.isPlaying;
382
+
383
+ if (state.isPlaying) {
384
+ d3.select(this).classed("playing", true)
385
+ .html('<i class="fas fa-pause"></i> Pause');
386
+
387
+ state.playIntervalId = setInterval(() => {
388
+ state.currentYear = state.currentYear < state.maxYear ?
389
+ state.currentYear + 1 : state.minYear;
390
+
391
+ dom.yearSlider.property("value", state.currentYear);
392
+ dom.yearValue.text(state.currentYear);
393
+ updateChart();
394
+ }, config.playInterval);
395
  } else {
396
+ d3.select(this).classed("playing", false)
397
+ .html('<i class="fas fa-play"></i> Play');
398
+
399
+ if (state.playIntervalId) {
400
+ clearInterval(state.playIntervalId);
401
+ }
402
  }
403
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
+ // Initialize the chart structure
407
  function initChart() {
408
+ // Clear any existing SVG
409
+ dom.chartArea.selectAll("svg").remove();
410
+
411
  // Create SVG
412
+ const width = dom.chartArea.node().clientWidth;
413
+ const height = dom.chartArea.node().clientHeight;
414
 
415
  state.svg = dom.chartArea.append("svg")
416
+ .attr("width", width)
417
+ .attr("height", height);
418
+
419
+ // Create main chart group
420
+ const chartGroup = state.svg.append("g")
421
+ .attr("transform", `translate(${config.margin.left}, ${config.margin.top})`);
422
 
423
  // Create scales
424
+ const innerWidth = width - config.margin.left - config.margin.right;
425
+ const innerHeight = height - config.margin.top - config.margin.bottom;
 
426
 
427
  state.x = d3.scaleLog()
428
+ .range([0, innerWidth]);
 
429
 
430
  state.y = d3.scaleLinear()
431
+ .range([innerHeight, 0]);
 
432
 
433
  state.size = d3.scaleSqrt()
 
434
  .range([config.minBubbleSize, config.maxBubbleSize]);
435
 
436
+ // Add axes groups
437
+ chartGroup.append("g")
438
  .attr("class", "x-axis")
439
+ .attr("transform", `translate(0, ${innerHeight})`);
440
 
441
+ chartGroup.append("g")
442
  .attr("class", "y-axis");
443
 
444
  // Add axis labels
445
+ chartGroup.append("text")
446
  .attr("class", "axis-label")
447
+ .attr("x", innerWidth / 2)
448
+ .attr("y", innerHeight + 40)
 
449
  .text("GDP per capita (PPP, constant 2021 international $)");
450
 
451
+ chartGroup.append("text")
452
  .attr("class", "axis-label")
453
  .attr("transform", "rotate(-90)")
454
+ .attr("x", -innerHeight / 2)
455
+ .attr("y", -40)
 
456
  .text("Life Expectancy (years)");
457
+
458
+ // Add title
459
+ chartGroup.append("text")
460
+ .attr("x", innerWidth / 2)
461
+ .attr("y", -20)
462
+ .attr("text-anchor", "middle")
463
+ .style("font-size", "16px")
464
+ .style("font-weight", "bold")
465
+ .text("Life Expectancy vs. GDP per capita");
466
+
467
+ // Add year display on chart
468
+ chartGroup.append("text")
469
+ .attr("id", "chart-year")
470
+ .attr("x", innerWidth - 10)
471
+ .attr("y", -10)
472
+ .attr("text-anchor", "end")
473
+ .style("font-size", "24px")
474
+ .style("font-weight", "bold")
475
+ .style("fill", "#444")
476
+ .text(state.currentYear);
477
+
478
+ // Add legend
479
+ const legend = state.svg.append("g")
480
+ .attr("class", "legend")
481
+ .attr("transform", `translate(${width - config.margin.right - 150}, ${config.margin.top})`);
482
+
483
+ Object.entries(config.regions).forEach(([region, {color}], i) => {
484
+ const legendItem = legend.append("g")
485
+ .attr("transform", `translate(0, ${i * 20})`);
486
+
487
+ legendItem.append("circle")
488
+ .attr("r", 5)
489
+ .attr("fill", color);
490
+
491
+ legendItem.append("text")
492
+ .attr("x", 10)
493
+ .attr("y", 5)
494
+ .style("font-size", "10px")
495
+ .text(region);
496
+ });
497
  }
498
 
499
+ // Update chart with current data
500
  function updateChart() {
501
+ if (!state.data) return;
502
+
503
+ // Filter data for current year and active regions
504
+ state.filteredData = state.data.filter(d =>
505
  d.year === state.currentYear &&
506
  state.activeRegions.includes(d.region)
507
  );
508
 
509
+ // Get SVG dimensions
510
+ const width = dom.chartArea.node().clientWidth;
511
+ const height = dom.chartArea.node().clientHeight;
512
+ const innerWidth = width - config.margin.left - config.margin.right;
513
+ const innerHeight = height - config.margin.top - config.margin.bottom;
 
 
 
 
514
 
515
+ // Update scales
516
+ state.x.domain([
517
+ d3.min(state.data, d => d.gdpPerCapita) * 0.8,
518
+ d3.max(state.data, d => d.gdpPerCapita) * 1.2
519
+ ]);
520
+
521
+ state.y.domain([
522
+ d3.min(state.data, d => d.lifeExpectancy) * 0.9,
523
+ d3.max(state.data, d => d.lifeExpectancy) * 1.05
524
+ ]);
525
+
526
+ state.size.domain([
527
+ d3.min(state.data, d => d.population),
528
+ d3.max(state.data, d => d.population)
529
+ ]);
530
 
531
  // Update axes
532
+ const xAxis = d3.axisBottom(state.x).ticks(5, ".0f");
533
+ const yAxis = d3.axisLeft(state.y);
534
 
535
  state.svg.select(".x-axis")
536
  .transition()
537
  .duration(config.animationDuration)
538
+ .call(xAxis);
539
 
540
  state.svg.select(".y-axis")
541
  .transition()
542
  .duration(config.animationDuration)
543
+ .call(yAxis);
544
 
545
+ // Update year display
546
+ state.svg.select("#chart-year")
547
+ .text(state.currentYear);
548
 
549
+ // Create transition for bubbles
550
+ const t = state.svg.transition()
551
+ .duration(config.animationDuration);
552
+
553
+ // Bind data to circles
554
+ const circles = state.svg.selectAll("g.country")
555
+ .data(state.filteredData, d => d.code + d.year);
556
+
557
+ // Exit old bubbles
558
+ circles.exit()
559
+ .transition(t)
560
  .attr("r", 0)
 
561
  .remove();
562
 
563
+ // Enter new bubbles
564
+ const newCircles = circles.enter()
565
+ .append("g")
566
+ .attr("class", "country")
567
+ .attr("transform", d => {
568
+ const xPos = state.x(d.gdpPerCapita) + config.margin.left;
569
+ const yPos = state.y(d.lifeExpectancy) + config.margin.top;
570
+ return `translate(${xPos}, ${yPos})`;
571
+ })
572
+ .on("mouseover", function(event, d) {
573
+ dom.tooltip.style("opacity", 1)
574
+ .html(`
575
+ <strong>${d.entity}</strong><br>
576
+ Year: ${d.year}<br>
577
+ Life Expectancy: ${d.lifeExpectancy.toFixed(1)} years<br>
578
+ GDP per capita: $${d.gdpPerCapita.toFixed(2)}<br>
579
+ Population: ${d3.format(",.0f")(d.population)}
580
+ `)
581
+ .style("left", (event.pageX + 10) + "px")
582
+ .style("top", (event.pageY - 10) + "px");
583
+ })
584
+ .on("mouseout", function() {
585
+ dom.tooltip.style("opacity", 0);
586
+ });
587
+
588
+ newCircles.append("circle")
589
+ .attr("r", 0)
590
+ .attr("fill", d => config.regions[d.region].color)
591
+ .attr("opacity", 0.7)
592
+ .transition(t)
593
  .attr("r", d => state.size(d.population))
594
+ .attr("stroke", "#fff")
595
+ .attr("stroke-width", 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
 
597
+ // Update existing bubbles
598
+ circles.transition(t)
599
+ .attr("transform", d => {
600
+ const xPos = state.x(d.gdpPerCapita) + config.margin.left;
601
+ const yPos = state.y(d.lifeExpectancy) + config.margin.top;
602
+ return `translate(${xPos}, ${yPos})`;
603
+ })
604
+ .select("circle")
605
+ .attr("r", d => state.size(d.population))
606
+ .attr("fill", d => config.regions[d.region].color);
607
+
608
+ // Add country labels for larger bubbles
609
+ circles.selectAll("text").remove();
610
+ newCircles.filter(d => state.size(d.population) > 15)
611
+ .append("text")
612
+ .attr("dy", ".3em")
613
+ .style("text-anchor", "middle")
614
+ .style("font-size", "10px")
615
+ .style("font-weight", "bold")
616
+ .style("fill", "#fff")
617
+ .style("pointer-events", "none")
618
+ .text(d => d.code);
619
+ }
620
+
621
+ // Initialize the application
622
  window.addEventListener("load", init);
623
+
624
+ // Handle window resize
625
+ window.addEventListener("resize", function() {
626
+ if (state.svg) {
627
+ initChart();
628
+ updateChart();
629
+ }
630
+ });
631
  </script>
632
+ </body>
633
  </html>