# setwd("~/Downloads") { # app.R options(error = NULL) # ------------------------------ # 1. Load Packages # ------------------------------ library(shiny) library(shinydashboard) library(leaflet) library(raster) library(DT) library(readr) library(dplyr) # For data manipulation library(ggplot2) # For histogram library(RColorBrewer) library(sp) # For handling map clicks/extracting raster values # ------------------------------ # 2. Data & Config # ------------------------------ # Define time periods corresponding to each band in the GeoTIFF time_periods <- c("1990–1992", "1993–1995", "1996–1998", "1999–2001", "2002–2004", "2005–2007", "2008–2010", "2011–2013", "2014–2016", "2017–2019") # Load GeoTIFF data (multi-band) wealth_stack <- stack("wealth_map.tif") # Clean up out-of-range values wealth_stack[wealth_stack <= 0 | wealth_stack > 1] <- NA # Scale by 100 # wealth_stack <- 100*wealth_stack all_vals <- values(wealth_stack) all_vals <- all_vals[!is.na(all_vals)] q_breaks_legend <- quantile(all_vals, probs = seq(0, 1, 0.2), na.rm = TRUE) q_breaks <- quantile(all_vals, probs = seq(0, 1, 0.1), na.rm = TRUE) # Load improvement data (change in IWI by state/province) improvement_data <- read_csv("poverty_improvement_by_state.csv") # Pre-calculate the mean IWI for each band (for the "Trends Over Time" chart). band_means <- sapply(seq_len(nlayers(wealth_stack)), function(i) { vals <- values(wealth_stack[[i]]) vals <- vals[!is.na(vals)] mean(vals) }) # ------------------------------ # 3. UI # ------------------------------ ui <- dashboardPage( title = "Wealth Map of Africa - AI Development Lab", # Add this line # -- Header dashboardHeader( title = span( style = "font-weight: 600; font-size: 18px;", a( href = "http://aidevlab.org", "aidevlab.org", target = "_blank", style = "font-family: 'OCR A Std', monospace; color: white; text-decoration: underline;" ) ), titleWidth = 250 ), # -- Sidebar dashboardSidebar( width = 250, tags$style(HTML(" @media (max-width: 768px) { .sidebar-toggle { padding: 15px !important; } .sidebar-toggle .icon-bar { width: 25px !important; height: 3px !important; } } ")), sidebarMenu( id = "tabs", menuItem("Wealth Map", tabName = "mapTab", icon = icon("map"), selected = TRUE), menuItem("Improvement Data", tabName = "improvementTab", icon = icon("table")), menuItem("Trends Over Time", tabName = "trendTab", icon = icon("chart-line")) ), # Show inputs only for the map tab conditionalPanel( condition = "input.tabs == 'mapTab'", br(), # Larger, more touch-friendly time period slider div( style = "padding: 15px 15px 15px 15px !important;", # top right bottom left sliderInput( inputId = "time_index", label = tags$span(style = "font-size: 16px;", "Select Time Period:"), min = 1, max = length(time_periods), value = 1, step = 1, animate = animationOptions(interval = 3300, loop = TRUE), width = "100%" ) ), # Show the currently selected year range clearly div( style = "padding: 0 15px; margin-bottom: 20px;", strong(style = "font-size: 16px;", "Selected Period:"), textOutput("current_year_range", inline = TRUE) ), div( style = "padding: 0 15px;", selectInput( "color_palette", tags$span(style = "font-size: 16px;", "Color Palette:"), choices = c("Viridis" = "viridis", "Plasma" = "plasma", "Magma" = "magma", "Inferno"= "inferno", "Spectral (Brewer)" = "Spectral"), selected = "plasma", width = "100%" ) ), div( style = "padding: 0 15px; margin-bottom: 20px;", sliderInput( "opacity", tags$span(style = "font-size: 16px;", "Map Opacity:"), min = 0.2, max = 1, value = 0.8, step = 0.1, width = "100%" ) ) ), # Share button with improved mobile styling tags$div( style = "text-align: center; margin: 20px 0;", HTML(' '), # Insert the JS as well tags$script( HTML(" (function() { const shareBtn = document.getElementById('share-button'); // Reusable helper function to show a small \"Copied!\" message function showCopyNotification() { const notification = document.createElement('div'); notification.innerText = 'Copied to clipboard'; notification.style.position = 'fixed'; notification.style.bottom = '20px'; notification.style.left = '50%'; notification.style.transform = 'translateX(-50%)'; notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; notification.style.color = '#fff'; notification.style.padding = '10px 16px'; notification.style.borderRadius = '6px'; notification.style.zIndex = '9999'; notification.style.fontSize = '16px'; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 2000); } shareBtn.addEventListener('click', function() { const currentURL = window.location.href; const pageTitle = document.title || 'Check this out!'; // If browser supports Web Share API (most mobile browsers) if (navigator.share) { navigator.share({ title: pageTitle, text: '', url: currentURL }) .catch((error) => { console.log('Sharing failed', error); }); } else { // Fallback: Copy URL if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(currentURL).then(() => { showCopyNotification(); }, (err) => { console.error('Could not copy text: ', err); }); } else { // Double fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = currentURL; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); showCopyNotification(); } catch (err) { alert('Please copy this link:\\n' + currentURL); } document.body.removeChild(textArea); } } }); })(); ") ) ) ), # -- Body dashboardBody( tags$head( # Viewport meta tag for proper mobile scaling tags$meta(name = "viewport", content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"), tags$link(rel = "stylesheet", href = "https://fonts.cdnfonts.com/css/ocr-a-std"), # Additional mobile-friendly styles tags$style(HTML(" body { font-family: 'OCR A Std', monospace !important; } .slider-animate-button { background-color: #ffffff !important; color: #000000 !important; border: 2px solid #000000 !important; border-radius: 6px !important; padding: 8px 15px !important; font-size: 18px !important; margin-top: 10px !important; margin-left: 5px !important; opacity: 1 !important; top: 5px !important; position: relative !important; /* enable top/left offsets */ } .slider-animate-container { margin-top: 10px !important; /* Adjust this value as needed */ margin-bottom: 20px !important; } /* Mobile-friendly boxes and layouts */ @media (max-width: 768px) { .box { margin-bottom: 20px !important; border-radius: 8px !important; } .box-header { padding: 15px !important; } .box-title { font-size: 18px !important; } .box-body { padding: 15px !important; } .nav-tabs-custom .nav-tabs li a { padding: 15px !important; font-size: 16px !important; } /* Increase button sizes for touch */ .btn { padding: 12px 18px !important; font-size: 16px !important; } /* Larger inputs and form controls */ .form-control { height: 45px !important; font-size: 16px !important; } /* Improve DataTable mobile view */ .dataTables_wrapper .dataTables_length, .dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info, .dataTables_wrapper .dataTables_paginate { text-align: center !important; float: none !important; margin-bottom: 10px !important; } /* Make sure text doesn't overflow on small screens */ p, h1, h2, h3, h4, h5, h6 { word-wrap: break-word !important; } } /* Ensure value boxes stack nicely */ .small-box { border-radius: 8px !important; margin-bottom: 20px !important; } .small-box .icon { font-size: 70px !important; } @media (max-width: 768px) { .small-box h3 { font-size: 24px !important; } .small-box p { font-size: 16px !important; } .small-box .icon { display: none !important; } } /* Make leaflet controls more touch friendly */ .leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { border: 2px solid rgba(0,0,0,0.2) !important; } .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { font-size: 18px !important; width: 34px !important; height: 34px !important; line-height: 34px !important; } /* Ensure plots are responsive */ .shiny-plot-output { width: 100% !important; max-width: 100% !important; } ")) ), tabItems( # ---------- MAP TAB ---------- tabItem( tabName = "mapTab", fluidRow( column( width = 12, # Value Boxes - will stack on mobile div( class = "row", div(class = "col-sm-4 col-xs-12", valueBoxOutput("highest_iwi_vb", width = NULL)), div(class = "col-sm-4 col-xs-12", valueBoxOutput("lowest_iwi_vb", width = NULL)), div(class = "col-sm-4 col-xs-12", valueBoxOutput("avg_iwi_vb", width = NULL)) ) ) ), fluidRow( # Map - full width on mobile column( width = 12, div( class = "row", div( class = "col-md-8 col-sm-12", box( title = span("Wealth Map of Africa", style = "font-family: 'OCR A Std', monospace; font-size: 18px;"), width = NULL, solidHeader = TRUE, status = "primary", leafletOutput("map", height = "450px"), p(style = "padding-top: 10px; font-size: 14px;", "Tap anywhere on the map to view the time-series of IWI for that location.") ) ), # Histogram - will position below map on mobile div( class = "col-md-4 col-sm-12", box( title = span("IWI Distribution", style = "font-family: 'OCR A Std', monospace; font-size: 16px;"), width = NULL, solidHeader = TRUE, status = "info", plotOutput("iwi_histogram", height = "200px"), p(style = "font-size: 14px;", "Distribution of International Wealth Index values for the selected time period."), strong(style = "font-size: 14px;", "Note:"), span(style = "font-size: 14px;", " Areas without human settlements are excluded."), div( style = "margin-top: 10px;", p(HTML("[Paper PDF]")) ) ) ) ) ) ), # Time series at clicked location fluidRow( column( width = 12, box( title = span("Time Series at Tapped Location", style = "font-family: 'OCR A Std', monospace; font-size: 16px;"), width = NULL, solidHeader = TRUE, status = "warning", plotOutput("clicked_ts_plot", height = "250px"), p(style = "font-size: 14px;", "Tap on the map to see the IWI time-series (1990–2019) for that location.") ) ) ), ## How It Works fluidRow( box( title = tagList(icon("cogs"), "How It Works"), status = "primary", solidHeader = TRUE, collapsible = TRUE, collapsed = TRUE, width = 12, HTML("
These wealth-index predictions are AI-generated by a sequence-aware neural network trained on 30 years of Demographic and Health Surveys (DHS) ground-truth data.