cjerzak commited on
Commit
03a0eb2
·
verified ·
1 Parent(s): beb15a9

Update warmup/app_v1.R

Browse files
Files changed (1) hide show
  1. warmup/app_v1.R +524 -0
warmup/app_v1.R CHANGED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setwd("~/Downloads")
2
+ {
3
+ # app.R
4
+ options(error = NULL)
5
+
6
+ # ------------------------------
7
+ # 1. Load Packages
8
+ # ------------------------------
9
+ library(shiny)
10
+ library(shinydashboard)
11
+ library(leaflet)
12
+ library(raster)
13
+ library(DT)
14
+ library(readr)
15
+ library(dplyr) # For data manipulation
16
+ library(ggplot2) # For histogram
17
+ library(RColorBrewer)
18
+ library(sp) # For handling map clicks/extracting raster values
19
+
20
+ # ------------------------------
21
+ # 2. Data & Config
22
+ # ------------------------------
23
+
24
+ # Define time periods corresponding to each band in the GeoTIFF
25
+ time_periods <- c("1990–1992", "1993–1995", "1996–1998", "1999–2001", "2002–2004",
26
+ "2005–2007", "2008–2010", "2011–2013", "2014–2016", "2017–2019")
27
+
28
+ # Load GeoTIFF data (multi-band)
29
+ wealth_stack <- stack("wealth_map.tif")
30
+
31
+ # Clean up out-of-range values
32
+ wealth_stack[wealth_stack <= 0 | wealth_stack > 1] <- NA
33
+
34
+ # Load improvement data (change in IWI by state/province)
35
+ improvement_data <- read_csv("poverty_improvement_by_state.csv")
36
+
37
+ # Pre-calculate the mean IWI for each band (for the "Trends Over Time" chart).
38
+ band_means <- sapply(seq_len(nlayers(wealth_stack)), function(i) {
39
+ vals <- values(wealth_stack[[i]])
40
+ vals <- vals[!is.na(vals)]
41
+ mean(vals)
42
+ })
43
+
44
+ # ------------------------------
45
+ # 3. UI
46
+ # ------------------------------
47
+ ui <- dashboardPage(
48
+ # -- Header
49
+ dashboardHeader(
50
+ title = span(
51
+ style = "font-weight: 600; font-size: 16px;",
52
+ a(
53
+ href = "http://aidevlab.org",
54
+ "aidevlab.org",
55
+ target = "_blank",
56
+ style = "font-family: 'OCR A Std', monospace; color: white; text-decoration: underline;"
57
+ )
58
+ )
59
+ ),
60
+
61
+ # -- Sidebar
62
+ dashboardSidebar(
63
+ sidebarMenu(
64
+ id = "tabs",
65
+ menuItem("Wealth Map", tabName = "mapTab", icon = icon("map")),
66
+ menuItem("Improvement Data", tabName = "improvementTab", icon = icon("table")),
67
+ menuItem("Trends Over Time", tabName = "trendTab", icon = icon("chart-line"))
68
+ ),
69
+ # Show inputs only for the map tab
70
+ conditionalPanel(
71
+ condition = "input.tabs == 'mapTab'",
72
+ br(),
73
+ # Replaces the old selectInput for time periods with a slider that can animate
74
+ sliderInput(
75
+ inputId = "time_index",
76
+ label = "Select Time Period (Years):",
77
+ min = 1,
78
+ max = length(time_periods),
79
+ value = 1,
80
+ step = 1,
81
+ animate = animationOptions(interval = 1500, loop = TRUE)
82
+ ),
83
+ # Show the currently selected year range clearly
84
+ strong("Currently Selected: "),
85
+ textOutput("current_year_range", inline = TRUE),
86
+ br(), br(),
87
+
88
+ selectInput("color_palette", "Select Color Palette:",
89
+ choices = c("Viridis" = "viridis",
90
+ "Plasma" = "plasma",
91
+ "Magma" = "magma",
92
+ "Inferno"= "inferno",
93
+ "Spectral (Brewer)" = "Spectral"),
94
+ selected = "plasma"),
95
+ sliderInput("opacity", "Map Opacity:", min = 0.2, max = 1, value = 0.8, step = 0.1)
96
+ ),
97
+ # ---- Here is the minimal "Share" button HTML + JS inlined in Shiny ----
98
+ # We wrap it in tags$div(...) and tags$script(HTML(...)) so it is recognized
99
+ # by Shiny. You can adjust the styling or placement as needed.
100
+ tags$div(
101
+ style = "text-align: left; margin: 1em 0 1em 2em;",
102
+ HTML('
103
+ <button id="share-button"
104
+ style="
105
+ display: inline-flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ gap: 8px;
109
+ padding: 5px 10px;
110
+ font-size: 16px;
111
+ font-weight: normal;
112
+ color: #000;
113
+ background-color: #fff;
114
+ border: 1px solid #ddd;
115
+ border-radius: 6px;
116
+ cursor: pointer;
117
+ box-shadow: 0 1.5px 0 #000;
118
+ ">
119
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
120
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
121
+ <circle cx="18" cy="5" r="3"></circle>
122
+ <circle cx="6" cy="12" r="3"></circle>
123
+ <circle cx="18" cy="19" r="3"></circle>
124
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
125
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
126
+ </svg>
127
+ <strong>Share</strong>
128
+ </button>
129
+ '),
130
+ # Insert the JS as well
131
+ tags$script(
132
+ HTML("
133
+ (function() {
134
+ const shareBtn = document.getElementById('share-button');
135
+ // Reusable helper function to show a small “Copied!” message
136
+ function showCopyNotification() {
137
+ const notification = document.createElement('div');
138
+ notification.innerText = 'Copied to clipboard';
139
+ notification.style.position = 'fixed';
140
+ notification.style.bottom = '20px';
141
+ notification.style.right = '20px';
142
+ notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
143
+ notification.style.color = '#fff';
144
+ notification.style.padding = '8px 12px';
145
+ notification.style.borderRadius = '4px';
146
+ notification.style.zIndex = '9999';
147
+ document.body.appendChild(notification);
148
+ setTimeout(() => { notification.remove(); }, 2000);
149
+ }
150
+ shareBtn.addEventListener('click', function() {
151
+ const currentURL = window.location.href;
152
+ const pageTitle = document.title || 'Check this out!';
153
+ // If browser supports Web Share API
154
+ if (navigator.share) {
155
+ navigator.share({
156
+ title: pageTitle,
157
+ text: '',
158
+ url: currentURL
159
+ })
160
+ .catch((error) => {
161
+ console.log('Sharing failed', error);
162
+ });
163
+ } else {
164
+ // Fallback: Copy URL
165
+ if (navigator.clipboard && navigator.clipboard.writeText) {
166
+ navigator.clipboard.writeText(currentURL).then(() => {
167
+ showCopyNotification();
168
+ }, (err) => {
169
+ console.error('Could not copy text: ', err);
170
+ });
171
+ } else {
172
+ // Double fallback for older browsers
173
+ const textArea = document.createElement('textarea');
174
+ textArea.value = currentURL;
175
+ document.body.appendChild(textArea);
176
+ textArea.select();
177
+ try {
178
+ document.execCommand('copy');
179
+ showCopyNotification();
180
+ } catch (err) {
181
+ alert('Please copy this link:\\n' + currentURL);
182
+ }
183
+ document.body.removeChild(textArea);
184
+ }
185
+ }
186
+ });
187
+ })();
188
+ ")
189
+ )
190
+ )
191
+ # ---- End: Minimal Share button snippet ----
192
+ ),
193
+
194
+ # -- Body
195
+ dashboardBody(
196
+ tags$head(
197
+ tags$link(rel = "stylesheet", href = "https://fonts.cdnfonts.com/css/ocr-a-std"),
198
+ # Make the "play" button whiter/brighter
199
+ tags$style(HTML("
200
+ body {
201
+ font-family: 'OCR A Std', monospace !important;
202
+ }
203
+ .slider-animate-button {
204
+ background-color: #ffffff !important;
205
+ color: #000000 !important;
206
+ border: 2px solid #000000 !important;
207
+ border-radius: 5px !important;
208
+ padding: 5px 10px !important;
209
+ top: 10px !important;
210
+ }
211
+ "))
212
+ ),
213
+ tabItems(
214
+ # ---------- MAP TAB ----------
215
+ tabItem(
216
+ tabName = "mapTab",
217
+ fluidRow(
218
+ # Value Boxes across the top for key stats
219
+ valueBoxOutput("highest_iwi_vb", width = 4),
220
+ valueBoxOutput("lowest_iwi_vb", width = 4),
221
+ valueBoxOutput("avg_iwi_vb", width = 4)
222
+ ),
223
+ fluidRow(
224
+ # Map
225
+ box(
226
+ title = "Wealth Map of Africa", width = 8, solidHeader = TRUE, status = "primary",
227
+ leafletOutput("map", height = "550px"),
228
+ p("Click anywhere on the map to view the time-series of IWI for that specific location (shown below).")
229
+ ),
230
+ # Histogram
231
+ box(
232
+ title = "IWI Distribution (Selected Period)", width = 4, solidHeader = TRUE, status = "info",
233
+ plotOutput("iwi_histogram", height = "250px"),
234
+ p("This histogram shows the distribution of the International Wealth Index (IWI) values for the selected time period across Africa."),
235
+ br(),
236
+ strong("Note:"),
237
+ " Wealth estimates for areas without human settlements have been excluded from the analysis."
238
+ )
239
+ ),
240
+ # Time series at clicked location
241
+ fluidRow(
242
+ box(
243
+ title = "Time Series at Clicked Location", width = 12, solidHeader = TRUE, status = "warning",
244
+ plotOutput("clicked_ts_plot", height = "300px"),
245
+ p("Click on the map to see the full IWI time-series (1990–2019) for that location.")
246
+ )
247
+ )
248
+ ),
249
+
250
+ # ---------- IMPROVEMENT DATA TAB ----------
251
+ tabItem(
252
+ tabName = "improvementTab",
253
+ fluidRow(
254
+ box(
255
+ width = 12, title = "Poverty Improvement by State", status = "primary", solidHeader = TRUE,
256
+ p("This table shows the estimated improvement in mean IWI between 1990–1992 and 2017–2019 for each province in Africa.
257
+ The 'Improvement' column indicates the change in IWI over this period. You can sort or filter the table,
258
+ and use the download button to export the data."),
259
+ downloadButton("download_data", "Download CSV", icon = icon("download")),
260
+ br(), br(),
261
+ DTOutput("improvement_table")
262
+ )
263
+ )
264
+ ),
265
+
266
+ # ---------- TRENDS OVER TIME TAB ----------
267
+ tabItem(
268
+ tabName = "trendTab",
269
+ fluidRow(
270
+ box(
271
+ width = 12, title = "Average Wealth Index Across Africa Over Time", status = "success", solidHeader = TRUE,
272
+ p("This chart aggregates the mean IWI across all of Africa in each of the ten time periods.
273
+ It provides a high-level view of how wealth (as measured by IWI) has changed over time."),
274
+ plotOutput("trend_plot", height = "400px")
275
+ )
276
+ )
277
+ )
278
+ )
279
+ )
280
+ )
281
+
282
+ # ------------------------------
283
+ # 4. Server
284
+ # ------------------------------
285
+ server <- function(input, output, session) {
286
+
287
+ # ReactiveVal to store the time-series of the last clicked point (across all periods).
288
+ clicked_point_vals <- reactiveVal(NULL)
289
+
290
+ # ----------------------------------
291
+ # Reactive expression for selected raster layer
292
+ # ----------------------------------
293
+ selected_raster <- reactive({
294
+ req(input$time_index)
295
+ wealth_stack[[input$time_index]]
296
+ })
297
+
298
+ # ----------------------------------
299
+ # Custom color palette function
300
+ # (reactive to user-selected palette)
301
+ # ----------------------------------
302
+ color_pal <- reactive({
303
+ palette_choice <- switch(
304
+ input$color_palette,
305
+ "viridis" = "viridis",
306
+ "plasma" = "plasma",
307
+ "magma" = "magma",
308
+ "inferno" = "inferno",
309
+ # Fallback to a Brewer palette for "Spectral"
310
+ "Spectral" = "Spectral"
311
+ )
312
+ colorNumeric(
313
+ palette = palette_choice,
314
+ domain = c(0, 1), # Domain for map: 0 to 1
315
+ na.color = "transparent"
316
+ )
317
+ })
318
+
319
+ # ----------------------------------
320
+ # Display the currently selected time period (year range)
321
+ # ----------------------------------
322
+ output$current_year_range <- renderText({
323
+ time_periods[input$time_index]
324
+ })
325
+
326
+ # ----------------------------------
327
+ # 1. MAP OUTPUT
328
+ # ----------------------------------
329
+ output$map <- renderLeaflet({
330
+ # We'll create 5 legend steps: 1, 0.75, 0.5, 0.25, 0
331
+ legend_values <- seq(1, 0, length.out = 5)
332
+
333
+ leaflet() %>%
334
+ addProviderTiles(providers$OpenStreetMap) %>%
335
+ setView(lng = 20, lat = 0, zoom = 3) %>% # Center on Africa
336
+ addLegend(
337
+ position = "bottomright",
338
+ colors = color_pal()(legend_values),
339
+ labels = sprintf("%.2f", legend_values),
340
+ title = "IWI",
341
+ opacity = 1
342
+ )
343
+ })
344
+
345
+ # Redraw the raster when inputs change
346
+ observeEvent(list(input$time_index, input$color_palette, input$opacity), {
347
+ leafletProxy("map") %>%
348
+ clearImages() %>%
349
+ addRasterImage(
350
+ selected_raster(),
351
+ colors = color_pal(),
352
+ opacity = input$opacity,
353
+ project = TRUE
354
+ )
355
+ })
356
+
357
+ # ----------------------------------
358
+ # Handle clicks on the map to show full time-series at that location
359
+ # ----------------------------------
360
+ observeEvent(input$map_click, {
361
+ click <- input$map_click
362
+ if (!is.null(click)) {
363
+ lat <- click$lat
364
+ lng <- click$lng
365
+
366
+ # Convert clicked point to SpatialPoints
367
+ coords <- data.frame(lng = lng, lat = lat)
368
+ sp_pt <- SpatialPoints(coords, proj4string = CRS("+proj=longlat +datum=WGS84 +no_defs"))
369
+
370
+ # Extract values across ALL bands at the clicked location
371
+ extracted_vals <- raster::extract(wealth_stack, sp_pt)
372
+ # extracted_vals is a 1x10 matrix if the point is valid
373
+ if (!is.null(extracted_vals)) {
374
+ # Convert to numeric vector
375
+ clicked_point_vals(as.numeric(extracted_vals))
376
+ } else {
377
+ # If the point is outside the raster or invalid
378
+ clicked_point_vals(NULL)
379
+ }
380
+ }
381
+ })
382
+
383
+ # Plot the time-series for the clicked location
384
+ output$clicked_ts_plot <- renderPlot({
385
+ vals <- clicked_point_vals()
386
+ if (is.null(vals)) {
387
+ # No location clicked yet or invalid click
388
+ plot.new()
389
+ title("Click on the map to see the IWI time-series here.")
390
+ return()
391
+ }
392
+
393
+ # If user clicked in a region with all NAs, do not plot
394
+ if (all(is.na(vals))) {
395
+ plot.new()
396
+ title("No data at this location. Try another spot.")
397
+ return()
398
+ }
399
+
400
+ df <- data.frame(Period = factor(time_periods, levels = time_periods),
401
+ IWI = vals)
402
+
403
+ ggplot(df, aes(x = Period, y = IWI, group = 1)) +
404
+ geom_line(color = "darkorange", size = 1) +
405
+ geom_point(color = "darkorange", size = 2) +
406
+ labs(title = "Time Series of IWI at Clicked Location",
407
+ x = "Time Period",
408
+ y = "IWI (0 to 1)") +
409
+ ylim(0, 1) +
410
+ theme_minimal(base_size = 14) +
411
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
412
+ })
413
+
414
+ # ----------------------------------
415
+ # 2. HISTOGRAM OUTPUT (for selected time period)
416
+ # ----------------------------------
417
+ output$iwi_histogram <- renderPlot({
418
+ # Extract raster values for histogram
419
+ r_vals <- values(selected_raster())
420
+ r_vals <- r_vals[!is.na(r_vals)]
421
+
422
+ ggplot(data.frame(iwi = r_vals), aes(x = iwi)) +
423
+ geom_histogram(binwidth = 0.02, fill = "#2c7bb6", color = "white", alpha = 0.7) +
424
+ labs(x = "IWI (0 to 1)", y = "Frequency") +
425
+ theme_minimal(base_size = 14)
426
+ })
427
+
428
+ # ----------------------------------
429
+ # 3. VALUE BOXES FOR KEY STATS
430
+ # ----------------------------------
431
+ # Compute stats for current raster
432
+ raster_stats <- reactive({
433
+ r_vals <- values(selected_raster())
434
+ r_vals <- r_vals[!is.na(r_vals)]
435
+ list(
436
+ highest = max(r_vals, na.rm = TRUE),
437
+ lowest = min(r_vals, na.rm = TRUE),
438
+ average = mean(r_vals, na.rm = TRUE)
439
+ )
440
+ })
441
+
442
+ # Highest IWI
443
+ output$highest_iwi_vb <- renderValueBox({
444
+ valueBox(
445
+ value = round(raster_stats()$highest, 3),
446
+ subtitle = "Highest IWI",
447
+ icon = icon("arrow-up"),
448
+ color = "green"
449
+ )
450
+ })
451
+
452
+ # Lowest IWI
453
+ output$lowest_iwi_vb <- renderValueBox({
454
+ valueBox(
455
+ value = round(raster_stats()$lowest, 3),
456
+ subtitle = "Lowest IWI",
457
+ icon = icon("arrow-down"),
458
+ color = "red"
459
+ )
460
+ })
461
+
462
+ # Average IWI
463
+ output$avg_iwi_vb <- renderValueBox({
464
+ valueBox(
465
+ value = round(raster_stats()$average, 3),
466
+ subtitle = "Average IWI",
467
+ icon = icon("balance-scale"),
468
+ color = "blue"
469
+ )
470
+ })
471
+
472
+ # ----------------------------------
473
+ # 4. IMPROVEMENT DATA TABLE
474
+ # ----------------------------------
475
+ output$improvement_table <- renderDT({
476
+ datatable(
477
+ improvement_data,
478
+ filter = "top",
479
+ options = list(
480
+ scrollX = TRUE,
481
+ pageLength = 20,
482
+ autoWidth = TRUE
483
+ )
484
+ )
485
+ })
486
+
487
+ # Download CSV
488
+ output$download_data <- downloadHandler(
489
+ filename = function() {
490
+ paste0("poverty_improvement_", Sys.Date(), ".csv")
491
+ },
492
+ content = function(file) {
493
+ write.csv(improvement_data, file, row.names = FALSE)
494
+ }
495
+ )
496
+
497
+ # ----------------------------------
498
+ # 5. TRENDS OVER TIME (line chart of mean IWI across all Africa)
499
+ # ----------------------------------
500
+ output$trend_plot <- renderPlot({
501
+ df <- data.frame(
502
+ Period = factor(time_periods, levels = time_periods),
503
+ MeanIWI = band_means
504
+ )
505
+
506
+ ggplot(df, aes(x = Period, y = MeanIWI, group = 1)) +
507
+ geom_line(color = "#2c7bb6", size = 1.1) +
508
+ geom_point(color = "#2c7bb6", size = 2) +
509
+ labs(
510
+ title = "Average IWI Over Time (Africa)",
511
+ x = "Time Period",
512
+ y = "Mean IWI"
513
+ ) +
514
+ ylim(0, 1) +
515
+ theme_minimal(base_size = 14) +
516
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
517
+ })
518
+ }
519
+
520
+ # ------------------------------
521
+ # 6. Run the App
522
+ # ------------------------------
523
+ shinyApp(ui = ui, server = server)
524
+ }