cjerzak commited on
Commit
66f6b8b
·
verified ·
1 Parent(s): ecf87c4

Create app_v1pt1.R

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