chrisli124 commited on
Commit
ebae247
Β·
1 Parent(s): 0c2c1d0

add heatmap layer

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
35
  wealth_map.tif filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
35
  wealth_map.tif filter=lfs diff=lfs merge=lfs -text
36
+ *.geojson filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1,4 +1,3 @@
1
  .venv/
2
  __pycache__
3
- .DS_Store
4
- keys.py
 
1
  .venv/
2
  __pycache__
3
+ .DS_Store
 
app.py CHANGED
@@ -6,11 +6,11 @@ import matplotlib.pyplot as plt
6
  import rasterio
7
  from rasterio.plot import show
8
  import geopandas as gpd
9
- from ipyleaflet import Map, TileLayer, basemaps, ColorMap, RasterLayer, LegendControl, GeoJSON
10
  from shinywidgets import output_widget, register_widget
11
  import plotnine as p9
12
- # from palettable.colorbrewer.diverging import Spectral_10
13
- # from palettable.colorbrewer.sequential import Blues_9, OrRd_9, PuBuGn_9, Reds_9
14
  import os
15
  import base64
16
  import tempfile
@@ -18,6 +18,11 @@ import json
18
  from datetime import datetime
19
  from helpers.fetch_data import fetch_data
20
  from helpers.residuals import get_residual_plot
 
 
 
 
 
21
 
22
  # ------------------------------
23
  # 1. Data & Config
@@ -25,21 +30,38 @@ from helpers.residuals import get_residual_plot
25
 
26
  # Define time periods corresponding to each band in the GeoTIFF
27
  time_periods = ["1990–1992", "1993–1995", "1996–1998", "1999–2001", "2002–2004",
28
- "2005–2007", "2008–2010", "2011–2013", "2014–2016", "2017–2019"]
29
 
30
  # Load GeoTIFF data (multi-band)
31
- # Note: In a real application, you'd need to adjust this path
32
  wealth_stack = rasterio.open("wealth_map.tif")
33
 
34
- with open('data/no_somaliland.geojson') as a:
 
35
  country_json = json.load(a)
36
-
 
37
  IWI_df = pd.read_csv('data/mean_IWI_by_country.csv')
38
 
 
39
  residual_data = pd.read_csv('data/residual_by_country.csv')
40
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
 
 
 
 
 
43
 
44
  # Function to clean up out-of-range values and get values
45
  def get_clean_values(src, band_idx=1):
@@ -48,6 +70,7 @@ def get_clean_values(src, band_idx=1):
48
  band_data[(band_data <= 0) | (band_data > 1)] = np.nan
49
  return band_data
50
 
 
51
  # Get all values across all bands for quantiles
52
  all_vals = []
53
  for i in range(1, wealth_stack.count + 1):
@@ -59,7 +82,7 @@ q_breaks_legend = np.quantile(all_vals, np.linspace(0, 1, 6))
59
  q_breaks = np.quantile(all_vals, np.linspace(0, 1, 11))
60
 
61
  # Get raster bounds for proper positioning on the map
62
- bounds = [[wealth_stack.bounds.bottom, wealth_stack.bounds.left],
63
  [wealth_stack.bounds.top, wealth_stack.bounds.right]]
64
 
65
  # Load improvement data (change in IWI by state/province)
@@ -225,106 +248,129 @@ app_ui = ui.page_fluid(
225
  ),
226
  ui.page_navbar(
227
  ui.nav_panel("Wealth Map",
228
- ui.layout_sidebar(
229
- ui.sidebar(
230
- ui.h4("Map Controls"),
231
- ui.input_slider(
232
- "time_index",
233
- "Select Time Period (Years):",
234
- min=1,
235
- max=len(time_periods),
236
- value=1,
237
- step=1,
238
- animate=True
239
- ),
240
- ui.strong("Currently Selected: "),
241
- ui.output_text("current_year_range", inline=True),
242
- ui.input_select(
243
- "color_palette",
244
- "Select Color Palette:",
245
- {
246
- "blue": "blue",
247
- "red": "red",
248
- "orange": "orange",
249
- "purple": "purple",
250
- "Spectral": "Spectral (Brewer)"
251
- },
252
- selected="red"
253
- ),
254
- ui.input_slider(
255
- "opacity",
256
- "Map Opacity:",
257
- min=0.2,
258
- max=1,
259
- value=0.8,
260
- step=0.1
261
- ),
262
- ui.HTML(share_button_html)
263
- ),
264
- ui.layout_column_wrap(
265
- ui.value_box(
266
- "Highest IWI",
267
- ui.output_text("highest_iwi"),
268
- showcase=ui.tags.i(class_="fa fa-arrow-up"),
269
- theme="success"
270
- ),
271
- ui.value_box(
272
- "Lowest IWI",
273
- ui.output_text("lowest_iwi"),
274
- showcase=ui.tags.i(class_="fa fa-arrow-down"),
275
- theme="danger"
276
- ),
277
- ui.value_box(
278
- "Average IWI",
279
- ui.output_text("avg_iwi"),
280
- showcase=ui.tags.i(class_="fa fa-balance-scale"),
281
- theme="primary"
282
- ),
283
- width=1/3
284
- ),
285
- ui.layout_column_wrap(
286
- ui.card(
287
- ui.card_header(ui.h3("Wealth Map of Africa", class_="title-text")),
288
- output_widget("map"),
289
- ui.p("Click anywhere on the map to view the time-series of IWI for that specific location (shown below).")
290
- ),
291
- ui.card(
292
- ui.card_header(ui.h3("Time Series at Clicked Location", class_="subtitle-text")),
293
- ui.output_plot("clicked_ts_plot"),
294
- ui.p("Click on the map to see the full IWI time-series (1990–2019) for that location.")
295
- )
296
- ),
297
-
298
- ui.card(
299
- ui.card_header(ui.h3("Ground Truth vs. Prediction Residual Distribution (Selected Country)", class_="subtitle-text")),
300
- ui.output_plot("iwi_residuals"),
301
- ui.p("This chart shows the distribution of residuals between ground truth and predicted IWI values based on the selected country."),
302
- ui.strong("Note: wealth estimates for areas without human settlements have been excluded from the analysis."),
303
- ui.HTML("<a href='https://doi.org/10.24963/ijcai.2023/684' target='_blank'>[Paper PDF]</a>")
304
- ),
305
- )
306
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  ui.nav_panel("Improvement Data",
308
- ui.layout_columns(
309
- ui.card(
310
- ui.card_header(ui.h3("Poverty Improvement by State", class_="title-text")),
311
- ui.p("This table shows the estimated improvement in mean IWI between 1990–1992 and 2017–2019 for each province in Africa. "
312
- "The 'Improvement' column indicates the change in IWI over this period. You can sort or filter the table, "
313
- "and use the download button to export the data."),
314
- ui.download_button("download_data", "Download CSV", icon="download"),
315
- ui.card(ui.output_data_frame("improvement_table")),
316
-
317
- )
318
- )
319
- ),
 
 
 
320
  ui.nav_panel("Trends Over Time",
321
- ui.card(
322
- ui.card_header(ui.h3("Average Wealth Index Across Africa Over Time", class_="title-text")),
323
- ui.p("This chart aggregates the mean IWI across all of Africa in each of the ten time periods. "
324
- "It provides a high-level view of how wealth (as measured by IWI) has changed over time."),
325
- ui.output_plot("trend_plot")
326
- )
327
- ),
 
328
  title=ui.HTML(
329
  "<span style='font-weight: 600; font-size: 16px;'>"
330
  "<a href='http://aidevlab.org' target='_blank' "
@@ -339,9 +385,27 @@ app_ui = ui.page_fluid(
339
  # ------------------------------
340
  # 3. Server logic
341
  # ------------------------------
 
 
342
  def server(input, output, session):
343
  # Initialize the map widget
344
  m = Map(center=(0, 20), zoom=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  geo_json = GeoJSON(
346
  data=country_json,
347
  style={
@@ -351,189 +415,136 @@ def server(input, output, session):
351
  'color': 'white', 'dashArray': '0', 'fillOpacity': 0.5
352
  }
353
  )
354
- m.add_layer(geo_json)
355
-
356
-
357
-
358
  # Register the map widget with Shiny
359
- map_widget = register_widget("map", m)
360
-
361
  # Store clicked point values
362
  clicked_point_vals = reactive.Value(None)
363
  selected_country = reactive.Value(None)
364
 
365
  admin_layer = reactive.Value(None)
366
  selected_admin = reactive.Value(None)
367
-
368
  # Get the currently selected raster layer
369
  @reactive.Calc
370
  def selected_raster():
371
  band_idx = input.time_index()
372
  return get_clean_values(wealth_stack, band_idx)
373
-
374
  # Display selected time period
375
  @output
376
  @render.text
377
  def current_year_range():
378
- return time_periods[input.time_index() - 1] # Adjust for 0-based indexing
379
-
380
- # Function to get color palette based on user selection
381
- # @reactive.Calc
382
- # def get_palette():
383
- # palette_name = input.color_palette()
384
- # if palette_name == "blue":
385
- # return Blues_9.hex_colors
386
- # elif palette_name == "orange":
387
- # return OrRd_9.hex_colors
388
- # elif palette_name == "red":
389
- # return Reds_9.hex_colors
390
- # elif palette_name == "purple":
391
- # return PuBuGn_9.hex_colors
392
- # else: # Spectral
393
- # return Spectral_10.hex_colors
394
-
395
- # Create a RasterLayer for the map
396
- # @reactive.effect
397
  # @reactive.event(input.time_index, input.color_palette, input.opacity)
398
- # def _():
399
- # # Remove existing raster layers
400
- # for layer in m.layers:
401
- # if isinstance(layer, RasterLayer):
402
- # m.remove_layer(layer)
403
-
404
- # # Get current raster data
405
- # raster_data = selected_raster()
406
-
407
- # # Create a temporary GeoTIFF file
408
- # with tempfile.NamedTemporaryFile(suffix='.tif', delete=False) as tmp:
409
- # temp_path = tmp.name
410
-
411
- # # Create a new GeoTIFF with the selected band
412
- # with rasterio.open(
413
- # temp_path,
414
- # 'w',
415
- # driver='GTiff',
416
- # height=raster_data.shape[0],
417
- # width=raster_data.shape[1],
418
- # count=1,
419
- # dtype=raster_data.dtype,
420
- # crs=wealth_stack.crs,
421
- # transform=wealth_stack.transform,
422
- # ) as dst:
423
- # dst.write(raster_data, 1)
424
-
425
- # # Create a ColorMap for the raster
426
- # colormap = ColorMap(
427
- # vmin=q_breaks[0],
428
- # vmax=q_breaks[-1]
429
- # # palette=get_palette()
430
- # )
431
-
432
- # # Add the raster layer to the map
433
- # raster_layer = RasterLayer(
434
- # url=temp_path,
435
- # bounds=bounds,
436
- # colormap=colormap,
437
- # opacity=input.opacity()
438
- # )
439
-
440
- # m.add_layer(raster_layer)
441
-
442
- # # Add legend
443
- # for ctrl in m.controls:
444
- # if isinstance(ctrl, LegendControl):
445
- # m.remove_control(ctrl)
446
-
447
- # legend = LegendControl({"IWI": colormap}, position="bottomright")
448
- # m.add_control(legend)
449
-
450
  # Handle map clicks
451
  @reactive.effect
452
  def _():
453
  # Set up click event handler
454
- def handle_map_click(event = None, feature = None, **kwargs):
455
- coords = feature['geometry']['coordinates'][0] #extract feature coordinates
456
- latitudes = [coords[x][1] for x in range(len(coords))]
 
457
  longitudes = [coords[y][0] for y in range(len(coords))]
458
- country_name= feature['properties']['sovereignt'] #find country name
459
- country_abbrev= feature['properties']['sov_a3'] #find country abbreviation
 
 
460
 
461
- selected_country.set(country_name) #set the country name
462
 
463
- centroid = (np.mean(latitudes),np.mean(longitudes)) #lock view position to the country's centroid
 
464
  m.center = centroid
465
  m.zoom = 5
466
-
467
  # Register click handler
468
  geo_json.on_click(handle_map_click)
469
-
470
  # Display value boxes
471
  @output
472
  @render.text
473
  def highest_iwi():
474
  raster_data = selected_raster()
475
  return f"{np.nanmax(raster_data):.3f}"
476
-
477
  @output
478
  @render.text
479
  def lowest_iwi():
480
  raster_data = selected_raster()
481
  return f"{np.nanmin(raster_data):.3f}"
482
-
483
  @output
484
  @render.text
485
  def avg_iwi():
486
  raster_data = selected_raster()
487
  return f"{np.nanmean(raster_data):.3f}"
488
-
489
  # Generate trend plot for mean IWI across Africa
490
  @output
491
  @render.plot
492
  def trend_plot():
493
- fig, ax = plt.subplots(figsize=(12, 8))
494
- ax.plot(range(len(time_periods)), band_means, marker='o', color="darkorange", linewidth=2, markersize=6)
 
495
  ax.set_xticks(range(len(time_periods)))
496
  ax.set_xticklabels(time_periods, rotation=45, ha="right")
497
  ax.set_ylabel("Mean IWI")
498
  ax.set_ylim(0.1, 0.3)
499
  ax.set_title("Average IWI Over Time (Africa)")
500
  ax.grid(True, linestyle='--', alpha=0.7)
501
-
502
  plt.tight_layout()
503
  return fig
504
-
505
  # Generate histogram plot
506
  @output
507
  @render.plot
508
  def iwi_residuals():
509
  country_name = selected_country.get()
510
  fig = get_residual_plot(country_name, residual_data)
511
- return fig
512
-
513
  # Plot time series at clicked location
514
  @output
515
  @render.plot
516
  def clicked_ts_plot():
517
  country_name = selected_country.get()
518
-
519
  fig, ax = plt.subplots(figsize=(10, 4))
520
-
521
  if country_name is None:
522
  ax.text(0.5, 0.5, "Click on the map to see the IWI time-series here.",
523
- horizontalalignment='center', verticalalignment='center',
524
- transform=ax.transAxes, fontsize=14)
525
  else:
526
- ax.plot(IWI_df['Band_Number'], IWI_df[country_name], marker='o', color="darkorange", linewidth=2, markersize=6)
527
- ax.set_xticks(range(1,len(IWI_df['Band_Number'])+1))
 
528
  ax.set_xticklabels(time_periods, rotation=45)
529
  ax.set_ylabel("IWI (0 to 1)")
530
  ax.set_ylim(0, 1)
531
  ax.set_title(f"Time Series of IWI in {country_name}")
532
  ax.grid(True, linestyle='--', alpha=0.7)
533
-
534
  plt.tight_layout()
535
  return fig
536
-
537
  # Display improvement data table
538
  @output
539
  @render.data_frame
@@ -543,15 +554,24 @@ def server(input, output, session):
543
  filters=True,
544
  height="800px"
545
  )
546
-
547
  # Download CSV handler
548
- @session.download(filename=lambda: f"poverty_improvement_{datetime.now().strftime('%Y-%m-%d')}.csv")
 
549
  def download_data():
550
  return improvement_data.to_csv(index=False)
551
-
552
-
 
 
 
 
 
 
 
 
553
 
554
  # ------------------------------
555
  # 4. Create and run the app
556
  # ------------------------------
557
- app = App(app_ui, server)
 
6
  import rasterio
7
  from rasterio.plot import show
8
  import geopandas as gpd
9
+ from ipyleaflet import Map, TileLayer, basemaps, ColorMap, RasterLayer, LegendControl, GeoJSON, MarkerCluster, Marker, DivIcon, Polygon
10
  from shinywidgets import output_widget, register_widget
11
  import plotnine as p9
12
+ import matplotlib.cm as cm
13
+ import matplotlib.colors as colors
14
  import os
15
  import base64
16
  import tempfile
 
18
  from datetime import datetime
19
  from helpers.fetch_data import fetch_data
20
  from helpers.residuals import get_residual_plot
21
+ from helpers.reduce_precision import reduce_coordinate_precision
22
+ from shapely.geometry import shape
23
+ from shapely.geometry import mapping
24
+ from shapely.geometry import Polygon as ShapelyPolygon
25
+ import io
26
 
27
  # ------------------------------
28
  # 1. Data & Config
 
30
 
31
  # Define time periods corresponding to each band in the GeoTIFF
32
  time_periods = ["1990–1992", "1993–1995", "1996–1998", "1999–2001", "2002–2004",
33
+ "2005–2007", "2008–2010", "2011–2013", "2014–2016", "2017–2019"]
34
 
35
  # Load GeoTIFF data (multi-band)
 
36
  wealth_stack = rasterio.open("wealth_map.tif")
37
 
38
+ #load country data
39
+ with open('data/no_somaliland.geojson') as a:
40
  country_json = json.load(a)
41
+
42
+ #load IWI by country
43
  IWI_df = pd.read_csv('data/mean_IWI_by_country.csv')
44
 
45
+ #load residual data
46
  residual_data = pd.read_csv('data/residual_by_country.csv')
47
 
48
+ selected_map = reactive.Value(None)
49
+
50
+ #load band 1 data by default
51
+ with open('data/band_1.geojson') as w:
52
+ band_1_data = json.load(w)
53
+ band_1_data = reduce_coordinate_precision(band_1_data, precision=5) #reduce precision to aid in rendering
54
+
55
+ IWI_values = [feature['properties']['IWI']
56
+ for feature in band_1_data['features']]
57
+ norm = colors.Normalize(vmin=min(IWI_values), vmax=max(IWI_values))
58
+ colormap = cm.get_cmap("plasma")
59
 
60
 
61
+ def get_color(iwi):
62
+ rgba = colormap(norm(iwi)) # Convert to RGBA
63
+ return colors.to_hex(rgba) # Convert to HEX
64
+
65
 
66
  # Function to clean up out-of-range values and get values
67
  def get_clean_values(src, band_idx=1):
 
70
  band_data[(band_data <= 0) | (band_data > 1)] = np.nan
71
  return band_data
72
 
73
+
74
  # Get all values across all bands for quantiles
75
  all_vals = []
76
  for i in range(1, wealth_stack.count + 1):
 
82
  q_breaks = np.quantile(all_vals, np.linspace(0, 1, 11))
83
 
84
  # Get raster bounds for proper positioning on the map
85
+ bounds = [[wealth_stack.bounds.bottom, wealth_stack.bounds.left],
86
  [wealth_stack.bounds.top, wealth_stack.bounds.right]]
87
 
88
  # Load improvement data (change in IWI by state/province)
 
248
  ),
249
  ui.page_navbar(
250
  ui.nav_panel("Wealth Map",
251
+ ui.layout_sidebar(
252
+ ui.sidebar(
253
+ ui.h4("Map Controls"),
254
+ ui.input_switch(
255
+ "SelectedMap", "Enable Country View", False),
256
+ ui.input_slider(
257
+ "time_index",
258
+ "Select Time Period (Years):",
259
+ min=1,
260
+ max=len(time_periods),
261
+ value=1,
262
+ step=1,
263
+ animate=True
264
+ ),
265
+ ui.strong("Currently Selected: "),
266
+ ui.output_text("current_year_range", inline=True),
267
+ ui.input_select(
268
+ "color_palette",
269
+ "Select Color Palette:",
270
+ {
271
+ "blue": "blue",
272
+ "red": "red",
273
+ "orange": "orange",
274
+ "purple": "purple",
275
+ "Spectral": "Spectral (Brewer)"
276
+ },
277
+ selected="red"
278
+ ),
279
+ ui.input_slider(
280
+ "opacity",
281
+ "Map Opacity:",
282
+ min=0.2,
283
+ max=1,
284
+ value=0.8,
285
+ step=0.1
286
+ ),
287
+ ui.accordion(ui.accordion_panel(
288
+ 'How it works', ui.HTML("<p>These wealth-index predictions are AI-generated by a"
289
+ "sequence-aware neural network trained on 30 years of <em>Demographic and Health Surveys (DHS)</em> ground-truth data.</p"
290
+ "<ul><li>πŸ” 57,100+ geo-referenced survey points from DHS</li> <li>βš™οΈ Multi-spectral satellite bands & raster-to-vector feature extraction</li><li>🎯 Calibrated & validated with held-out DHS clusters (1990–2019)</li></ul>")
291
+ ), id="map_instructions", open=False, multiple=False),
292
+ ui.HTML(share_button_html)
293
+ ),
294
+ ui.layout_column_wrap(
295
+ ui.value_box(
296
+ "Highest IWI",
297
+ ui.output_text("highest_iwi"),
298
+ showcase=ui.tags.i(class_="fa fa-arrow-up"),
299
+ theme="success"
300
+ ),
301
+ ui.value_box(
302
+ "Lowest IWI",
303
+ ui.output_text("lowest_iwi"),
304
+ showcase=ui.tags.i(class_="fa fa-arrow-down"),
305
+ theme="danger"
306
+ ),
307
+ ui.value_box(
308
+ "Average IWI",
309
+ ui.output_text("avg_iwi"),
310
+ showcase=ui.tags.i(
311
+ class_="fa fa-balance-scale"),
312
+ theme="primary"
313
+ ),
314
+ width=1/3
315
+ ),
316
+ ui.layout_column_wrap(
317
+ ui.card(
318
+ ui.card_header(
319
+ ui.h3("Wealth Map of Africa", class_="title-text")),
320
+ output_widget("country_map"),
321
+ ui.p(
322
+ "Click anywhere on the map to view the time-series of IWI for that specific location (shown below).")
323
+ ),
324
+ ui.card(
325
+ ui.card_header(
326
+ ui.h3("Time Series at Clicked Location", class_="subtitle-text")),
327
+ ui.output_plot("clicked_ts_plot"),
328
+ ui.p(
329
+ "Click on the map to see the full IWI time-series (1990–2019) for that location."),
330
+ ui.download_button(
331
+ "download_country_data", "Download CSV", icon="download"),
332
+
333
+ )
334
+ ),
335
+
336
+ ui.card(
337
+ ui.card_header(ui.h3(
338
+ "Ground Truth vs. Prediction Residual Distribution (Selected Country)", class_="subtitle-text")),
339
+ ui.output_plot("iwi_residuals"),
340
+ ui.p(
341
+ "This chart shows the distribution of residuals between ground truth and predicted IWI values based on the selected country."),
342
+ ui.strong(
343
+ "Note: wealth estimates for areas without human settlements have been excluded from the analysis."),
344
+ ui.HTML(
345
+ "<a href='https://doi.org/10.24963/ijcai.2023/684' target='_blank'>[Paper PDF]</a>")
346
+ )
347
+ )
348
+ ),
349
  ui.nav_panel("Improvement Data",
350
+ ui.layout_columns(
351
+ ui.card(
352
+ ui.card_header(
353
+ ui.h3("Poverty Improvement by State", class_="title-text")),
354
+ ui.p("This table shows the estimated improvement in mean IWI between 1990–1992 and 2017–2019 for each province in Africa. "
355
+ "The 'Improvement' column indicates the change in IWI over this period. You can sort or filter the table, "
356
+ "and use the download button to export the data."),
357
+ ui.download_button(
358
+ "download_data", "Download CSV", icon="download"),
359
+ ui.card(ui.output_data_frame(
360
+ "improvement_table")),
361
+
362
+ )
363
+ )
364
+ ),
365
  ui.nav_panel("Trends Over Time",
366
+ ui.card(
367
+ ui.card_header(
368
+ ui.h3("Average Wealth Index Across Africa Over Time", class_="title-text")),
369
+ ui.p("This chart aggregates the mean IWI across all of Africa in each of the ten time periods. "
370
+ "It provides a high-level view of how wealth (as measured by IWI) has changed over time."),
371
+ ui.output_plot("trend_plot")
372
+ )
373
+ ),
374
  title=ui.HTML(
375
  "<span style='font-weight: 600; font-size: 16px;'>"
376
  "<a href='http://aidevlab.org' target='_blank' "
 
385
  # ------------------------------
386
  # 3. Server logic
387
  # ------------------------------
388
+
389
+
390
  def server(input, output, session):
391
  # Initialize the map widget
392
  m = Map(center=(0, 20), zoom=3)
393
+ for feature in band_1_data["features"]:
394
+ iwi = feature["properties"]["IWI"]
395
+ feature["properties"]["style"] = {
396
+ "color": get_color(iwi),
397
+ "fillColor": get_color(iwi), # Fill color based on IWI
398
+ "fillOpacity": 0.7,
399
+ "weight": 1
400
+ }
401
+
402
+ band_1_json = GeoJSON(data=band_1_data,
403
+ style={'radius': 2, 'opacity': 0.8, 'weight': 1.9},
404
+ point_style={'radius': 3},
405
+ name='Release'
406
+ )
407
+ m.add_layer(band_1_json)
408
+
409
  geo_json = GeoJSON(
410
  data=country_json,
411
  style={
 
415
  'color': 'white', 'dashArray': '0', 'fillOpacity': 0.5
416
  }
417
  )
 
 
 
 
418
  # Register the map widget with Shiny
419
+ map_widget = register_widget("country_map", m)
420
+
421
  # Store clicked point values
422
  clicked_point_vals = reactive.Value(None)
423
  selected_country = reactive.Value(None)
424
 
425
  admin_layer = reactive.Value(None)
426
  selected_admin = reactive.Value(None)
427
+
428
  # Get the currently selected raster layer
429
  @reactive.Calc
430
  def selected_raster():
431
  band_idx = input.time_index()
432
  return get_clean_values(wealth_stack, band_idx)
433
+
434
  # Display selected time period
435
  @output
436
  @render.text
437
  def current_year_range():
438
+ # Adjust for 0-based indexing
439
+ return time_periods[input.time_index() - 1]
440
+
441
+ # Create a Country layer for the map
442
+ @reactive.effect
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  # @reactive.event(input.time_index, input.color_palette, input.opacity)
444
+ def _():
445
+ if input.SelectedMap() == True:
446
+ m.remove_layer(band_1_json)
447
+ m.add_layer(geo_json)
448
+ return m
449
+ elif input.SelectedMap() == False:
450
+ for layer in m.layers:
451
+ if layer == geo_json:
452
+ m.remove_layer(layer)
453
+ m.add_layer(band_1_json)
454
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  # Handle map clicks
456
  @reactive.effect
457
  def _():
458
  # Set up click event handler
459
+ def handle_map_click(event=None, feature=None, **kwargs):
460
+ # extract feature coordinates
461
+ coords = feature['geometry']['coordinates'][0]
462
+ latitudes = [coords[x][1] for x in range(len(coords))]
463
  longitudes = [coords[y][0] for y in range(len(coords))]
464
+ # find country name
465
+ country_name = feature['properties']['sovereignt']
466
+ # find country abbreviation
467
+ country_abbrev = feature['properties']['sov_a3']
468
 
469
+ selected_country.set(country_name) # set the country name
470
 
471
+ # lock view position to the country's centroid
472
+ centroid = (np.mean(latitudes), np.mean(longitudes))
473
  m.center = centroid
474
  m.zoom = 5
475
+
476
  # Register click handler
477
  geo_json.on_click(handle_map_click)
478
+
479
  # Display value boxes
480
  @output
481
  @render.text
482
  def highest_iwi():
483
  raster_data = selected_raster()
484
  return f"{np.nanmax(raster_data):.3f}"
485
+
486
  @output
487
  @render.text
488
  def lowest_iwi():
489
  raster_data = selected_raster()
490
  return f"{np.nanmin(raster_data):.3f}"
491
+
492
  @output
493
  @render.text
494
  def avg_iwi():
495
  raster_data = selected_raster()
496
  return f"{np.nanmean(raster_data):.3f}"
497
+
498
  # Generate trend plot for mean IWI across Africa
499
  @output
500
  @render.plot
501
  def trend_plot():
502
+ fig, ax = plt.subplots(figsize=(10, 4))
503
+ ax.plot(range(len(time_periods)), band_means, marker='o',
504
+ color="darkorange", linewidth=2, markersize=6)
505
  ax.set_xticks(range(len(time_periods)))
506
  ax.set_xticklabels(time_periods, rotation=45, ha="right")
507
  ax.set_ylabel("Mean IWI")
508
  ax.set_ylim(0.1, 0.3)
509
  ax.set_title("Average IWI Over Time (Africa)")
510
  ax.grid(True, linestyle='--', alpha=0.7)
511
+
512
  plt.tight_layout()
513
  return fig
514
+
515
  # Generate histogram plot
516
  @output
517
  @render.plot
518
  def iwi_residuals():
519
  country_name = selected_country.get()
520
  fig = get_residual_plot(country_name, residual_data)
521
+ return fig
522
+
523
  # Plot time series at clicked location
524
  @output
525
  @render.plot
526
  def clicked_ts_plot():
527
  country_name = selected_country.get()
528
+
529
  fig, ax = plt.subplots(figsize=(10, 4))
530
+
531
  if country_name is None:
532
  ax.text(0.5, 0.5, "Click on the map to see the IWI time-series here.",
533
+ horizontalalignment='center', verticalalignment='center',
534
+ transform=ax.transAxes, fontsize=14)
535
  else:
536
+ ax.plot(IWI_df['Band_Number'], IWI_df[country_name],
537
+ marker='o', color="darkorange", linewidth=2, markersize=6)
538
+ ax.set_xticks(range(1, len(IWI_df['Band_Number'])+1))
539
  ax.set_xticklabels(time_periods, rotation=45)
540
  ax.set_ylabel("IWI (0 to 1)")
541
  ax.set_ylim(0, 1)
542
  ax.set_title(f"Time Series of IWI in {country_name}")
543
  ax.grid(True, linestyle='--', alpha=0.7)
544
+
545
  plt.tight_layout()
546
  return fig
547
+
548
  # Display improvement data table
549
  @output
550
  @render.data_frame
 
554
  filters=True,
555
  height="800px"
556
  )
557
+
558
  # Download CSV handler
559
+ @output
560
+ @render.download(filename=lambda: f"poverty_improvement_{datetime.now().strftime('%Y-%m-%d')}.csv")
561
  def download_data():
562
  return improvement_data.to_csv(index=False)
563
+
564
+ @output
565
+ @render.download(filename=lambda: f"{selected_country.get()}_IWI.csv")
566
+ async def download_country_data():
567
+ country_name = selected_country.get()
568
+ buf = io.StringIO()
569
+ country_data = pd.DataFrame(IWI_df[country_name])
570
+ country_data.to_csv(buf, index=False)
571
+ yield buf.getvalue()
572
+
573
 
574
  # ------------------------------
575
  # 4. Create and run the app
576
  # ------------------------------
577
+ app = App(app_ui, server)
data/IWI_by_admin2.geojson CHANGED
The diff for this file is too large to render. See raw diff
 
data/band_1.geojson ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3622bf0268d2a7141d48495366c4e5c259882c6219cace3d246d336299b567ac
3
+ size 136321755
data/ground_truth.geojson CHANGED
The diff for this file is too large to render. See raw diff
 
data/mean_IWI_by_admin.geojson CHANGED
The diff for this file is too large to render. See raw diff
 
data/no_somaliland.geojson CHANGED
The diff for this file is too large to render. See raw diff
 
helpers/reduce_precision.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ def reduce_coordinate_precision(geojson_data, precision=5):
4
+ """Reduces the decimal precision of coordinates in GeoJSON data."""
5
+ features = geojson_data.get('features', [])
6
+ for feature in features:
7
+ if feature['geometry']['type'] == 'Polygon':
8
+ coords = feature['geometry']['coordinates']
9
+ rounded_coords = [[[round(coord, precision) for coord in point] for point in ring] for ring in coords]
10
+ feature['geometry']['coordinates'] = rounded_coords
11
+
12
+ return geojson_data