cjerzak commited on
Commit
377339a
·
verified ·
1 Parent(s): 5a0b121

Create app_v1pt1.R

Browse files
Files changed (1) hide show
  1. Warmup/app_v1pt1.R +456 -0
Warmup/app_v1pt1.R ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # setwd("~/Downloads")
2
+
3
+ options(error = NULL)
4
+ library(shiny)
5
+ library(shinydashboard)
6
+ library(dplyr)
7
+ library(readr)
8
+ library(sf)
9
+ library(rnaturalearth)
10
+ library(rnaturalearthdata)
11
+ library(countrycode)
12
+ library(leaflet) # <-- For OpenStreetMap-based map
13
+
14
+ # =============================
15
+ # UI
16
+ # =============================
17
+ ui <- dashboardPage(
18
+ skin = "black",
19
+ dashboardHeader(
20
+ title = span(
21
+ style = "font-weight: 600; font-size: 13px;",
22
+ a(
23
+ href = "http://www.globalleadershipproject.net",
24
+ "GlobalLeadershipProject.net",
25
+ target = "_blank",
26
+ style = "color: white; text-decoration: underline;"
27
+ )
28
+ )
29
+ ),
30
+ dashboardSidebar(
31
+ sidebarMenu(
32
+ menuItem("Map Type", tabName = "cartogramTab", icon = icon("globe"))
33
+ ),
34
+ div(
35
+ style = "margin: 15px;",
36
+ selectInput(
37
+ inputId = "indexChoice",
38
+ label = "Select Representation Index:",
39
+ choices = c("Overall", "RepresentationGap", "Ethnicity",
40
+ "Gender", "Religion", "Language"),
41
+ selected = "Overall"
42
+ )
43
+ ),
44
+
45
+ # ---- Minimal "Share" button HTML + JS inlined ----
46
+ tags$div(
47
+ style = "text-align: left; margin: 1em 0 1em 2em;",
48
+ HTML('
49
+ <button id="share-button"
50
+ style="
51
+ display: inline-flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ gap: 8px;
55
+ padding: 5px 10px;
56
+ font-size: 16px;
57
+ font-weight: normal;
58
+ color: #000;
59
+ background-color: #fff;
60
+ border: 1px solid #ddd;
61
+ border-radius: 6px;
62
+ cursor: pointer;
63
+ box-shadow: 0 1.5px 0 #000;
64
+ ">
65
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
66
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
67
+ <circle cx="18" cy="5" r="3"></circle>
68
+ <circle cx="6" cy="12" r="3"></circle>
69
+ <circle cx="18" cy="19" r="3"></circle>
70
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
71
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
72
+ </svg>
73
+ <strong>Share</strong>
74
+ </button>
75
+ '),
76
+ tags$script(
77
+ HTML("
78
+ (function() {
79
+ const shareBtn = document.getElementById('share-button');
80
+ // Reusable helper function to show a small “Copied!” message
81
+ function showCopyNotification() {
82
+ const notification = document.createElement('div');
83
+ notification.innerText = 'Copied to clipboard';
84
+ notification.style.position = 'fixed';
85
+ notification.style.bottom = '20px';
86
+ notification.style.right = '20px';
87
+ notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
88
+ notification.style.color = '#fff';
89
+ notification.style.padding = '8px 12px';
90
+ notification.style.borderRadius = '4px';
91
+ notification.style.zIndex = '9999';
92
+ document.body.appendChild(notification);
93
+ setTimeout(() => { notification.remove(); }, 2000);
94
+ }
95
+ shareBtn.addEventListener('click', function() {
96
+ const currentURL = window.location.href;
97
+ const pageTitle = document.title || 'Check this out!';
98
+ // If browser supports Web Share API
99
+ if (navigator.share) {
100
+ navigator.share({
101
+ title: pageTitle,
102
+ text: '',
103
+ url: currentURL
104
+ })
105
+ .catch((error) => {
106
+ console.log('Sharing failed', error);
107
+ });
108
+ } else {
109
+ // Fallback: Copy URL
110
+ if (navigator.clipboard && navigator.clipboard.writeText) {
111
+ navigator.clipboard.writeText(currentURL).then(() => {
112
+ showCopyNotification();
113
+ }, (err) => {
114
+ console.error('Could not copy text: ', err);
115
+ });
116
+ } else {
117
+ // Double fallback for older browsers
118
+ const textArea = document.createElement('textarea');
119
+ textArea.value = currentURL;
120
+ document.body.appendChild(textArea);
121
+ textArea.select();
122
+ try {
123
+ document.execCommand('copy');
124
+ showCopyNotification();
125
+ } catch (err) {
126
+ alert('Please copy this link:\\n' + currentURL);
127
+ }
128
+ document.body.removeChild(textArea);
129
+ }
130
+ }
131
+ });
132
+ })();
133
+ ")
134
+ )
135
+ )
136
+ # ---- End: Minimal Share button snippet ----
137
+ ),
138
+ dashboardBody(
139
+ tags$head(
140
+ tags$link(
141
+ href = "https://fonts.googleapis.com/css2?family=OCR+A+Extended&display=swap",
142
+ rel = "stylesheet"
143
+ ),
144
+ tags$style(HTML("
145
+ /* Force OCR A Extended font across the entire UI and all HTML elements */
146
+ html, body, h1, h2, h3, h4, h5, h6, p, div, span, label, input, button, select,
147
+ .box, .content-wrapper, .main-sidebar, .main-header .navbar, .main-header .logo,
148
+ .sidebar-menu, .sidebar-menu li a, .sidebar-menu .fa {
149
+ font-family: 'OCR A Extended', monospace !important;
150
+ }
151
+
152
+ /* Header gradient background */
153
+ .main-header .navbar {
154
+ background: linear-gradient(to right, #3b6978, #204051) !important;
155
+ }
156
+ /* Logo area (left corner of the header) */
157
+ .main-header .logo {
158
+ background: #1b2a2f !important;
159
+ color: #ffffff !important;
160
+ border-bottom: none;
161
+ font-size: 18px;
162
+ font-weight: 600;
163
+ }
164
+ /* Sidebar background */
165
+ .main-sidebar {
166
+ background-color: #1b2a2f !important;
167
+ }
168
+ /* Active or hovered tab in the sidebar */
169
+ .sidebar-menu > li.active > a,
170
+ .sidebar-menu > li:hover > a {
171
+ background-color: #344e5c !important;
172
+ border-left-color: #78cdd7 !important;
173
+ color: #ffffff !important;
174
+ }
175
+
176
+ /* Sidebar menu item icons */
177
+ .sidebar-menu .fa {
178
+ color: #78cdd7 !important;
179
+ }
180
+
181
+ /* Sidebar menu item text */
182
+ .sidebar-menu > li > a {
183
+ color: #b8c7ce !important;
184
+ font-size: 15px;
185
+ font-weight: 500;
186
+ }
187
+
188
+ /* Customize the boxes */
189
+ .box {
190
+ border-top: none !important;
191
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
192
+ border-radius: 6px;
193
+ }
194
+ .box.box-solid > .box-header {
195
+ background-color: #204051;
196
+ color: #fff;
197
+ border-radius: 6px 6px 0 0;
198
+ }
199
+ /* Plot box spacing */
200
+ .box .box-body {
201
+ padding: 0 !important;
202
+ }
203
+ /* Footer text styling (plot captions, etc.) */
204
+ .small, small {
205
+ font-size: 75%;
206
+ }
207
+ /* Responsive map container */
208
+ .map-container {
209
+ height: 70vh;
210
+ width: 100%;
211
+ }
212
+ @media (max-width: 768px) {
213
+ .map-container {
214
+ height: 50vh;
215
+ }
216
+ }
217
+ "))
218
+ ),
219
+ tabItem(
220
+ tabName = "cartogramTab",
221
+ fluidRow(
222
+ column(
223
+ width = 9,
224
+ box(
225
+ width = NULL,
226
+ title = strong("Country-level Representation"),
227
+ solidHeader = TRUE,
228
+ div(
229
+ class = "map-container",
230
+ leafletOutput("cartogramPlot", width = "100%", height = "100%")
231
+ )
232
+ )
233
+ ),
234
+
235
+ column(
236
+ width = 3,
237
+ box(
238
+ width = NULL,
239
+ title = strong("Selected Country Data"),
240
+ solidHeader = TRUE,
241
+ uiOutput("selectedCountryData"),
242
+ style = "overflow-y: auto;" # Scroll if content overflows
243
+ )
244
+ )
245
+ ),
246
+
247
+ fluidRow(
248
+ column(
249
+ width = 9,
250
+ box(
251
+ width = NULL,
252
+ title = strong("Citation"),
253
+ solidHeader = TRUE,
254
+
255
+ tags$p(
256
+ "John Gerring, Connor T. Jerzak, Erzen Öncel.
257
+ The Composition of Descriptive Representation. ",
258
+ em("American Political Science Review,"), " 118(2): 784–801, 2024.",
259
+ tags$a(
260
+ href = "https://www.cambridge.org/core/services/aop-cambridge-core/content/view/7EAEA1CA4C553AB9D76054D1FA9C0840/S0003055423000680a.pdf/the-composition-of-descriptive-representation.pdf",
261
+ "PDF",
262
+ target = "_blank"
263
+ ),
264
+ " | ",
265
+ tags$a(
266
+ href = "https://connorjerzak.com/wp-content/uploads/2024/07/CompositionBib.txt",
267
+ "BibTeX",
268
+ target = "_blank"
269
+ ),
270
+ " | ",
271
+ tags$a(
272
+ href = "https://www.youtube.com/watch?v=nnfDj1NdOMo",
273
+ "YouTube",
274
+ target = "_blank"
275
+ )
276
+ )
277
+ )
278
+ )
279
+ )
280
+ )
281
+ )
282
+ )
283
+
284
+ # =============================
285
+ # SERVER
286
+ # =============================
287
+ server <- function(input, output, session) {
288
+
289
+ # 1. Custom matches for countries not recognized by default in 'countrycode'
290
+ custom_iso_matches <- c("Kosovo" = "XKX",
291
+ "Somaliland" = "SOM") # or any valid code you prefer
292
+
293
+ # 2. Read CSV data and create ISO3 codes with custom matches
294
+ rankings_data <- reactive({
295
+ read_csv("CountryRepresentationRankings.csv", show_col_types = FALSE) %>%
296
+ mutate(iso_a3 = countrycode(
297
+ sourcevar = Country,
298
+ origin = "country.name",
299
+ destination = "iso3c",
300
+ custom_match = custom_iso_matches
301
+ ))
302
+ })
303
+
304
+ # 3. Read/prepare world map shapefile (still from Natural Earth),
305
+ # but transform to lat/lon (EPSG:4326) to align with Leaflet.
306
+ world_sf <- reactive({
307
+ ne_countries(scale = "medium", returnclass = "sf") %>%
308
+ dplyr::select(name, iso_a3, pop_est, geometry) %>%
309
+ st_transform(crs = 4326) # Leaflet requires lat/lon
310
+ })
311
+
312
+ # 4. Create the joined sf object
313
+ cartogram_sf <- reactive({
314
+ merged_sf <- world_sf() %>%
315
+ left_join(rankings_data(), by = "iso_a3")
316
+ # Filter out countries with no data in "Overall"
317
+ merged_sf[!is.na(merged_sf$Overall), ]
318
+ })
319
+
320
+ # 5. Create the Leaflet map with OSM tiles
321
+ # and dynamically add polygons based on index choice.
322
+
323
+ # Initialize the leaflet map (empty) so it renders once:
324
+ output$cartogramPlot <- renderLeaflet({
325
+ leaflet() %>%
326
+ addProviderTiles("OpenStreetMap.Mapnik") %>%
327
+ setView(lng = 0, lat = 20, zoom = 2) # A broad global view
328
+ })
329
+
330
+ # Observe changes in the chosen index and update polygons + legend
331
+ observeEvent(input$indexChoice, {
332
+
333
+ plot_data <- cartogram_sf()
334
+ index_col <- input$indexChoice
335
+
336
+ # Build a color palette based on the chosen index
337
+ pal <- colorNumeric(
338
+ palette = "viridis",
339
+ domain = plot_data[[index_col]],
340
+ na.color = "white"
341
+ )
342
+
343
+ # Construct a label/popup-like text
344
+ # Construct an indicator function for clarity
345
+ starIfSelected <- function(colName) {
346
+ if (index_col == colName) "*" else ""
347
+ }
348
+
349
+ # Build a label/popup to show all columns
350
+ labels <- sprintf(
351
+ paste0(
352
+ "<strong>Country:</strong> %s<br/>",
353
+ "<strong>%sOverall:</strong> %s<br/>",
354
+ "<strong>%sRepresentation Gap:</strong> %s<br/>",
355
+ "<strong>%sEthnicity:</strong> %s<br/>",
356
+ "<strong>%sGender:</strong> %s<br/>",
357
+ "<strong>%sReligion:</strong> %s<br/>",
358
+ "<strong>%sLanguage:</strong> %s"
359
+ ),
360
+ # 1. Country
361
+ ifelse(is.na(plot_data$Country), "N/A", plot_data$Country),
362
+
363
+ # 2. Overall
364
+ starIfSelected("Overall"),
365
+ ifelse(is.na(plot_data$Overall), "N/A", plot_data$Overall),
366
+
367
+ # 3. RepresentationGap
368
+ starIfSelected("RepresentationGap"),
369
+ ifelse(is.na(plot_data$RepresentationGap), "N/A", plot_data$RepresentationGap),
370
+
371
+ # 4. Ethnicity
372
+ starIfSelected("Ethnicity"),
373
+ ifelse(is.na(plot_data$Ethnicity), "N/A", plot_data$Ethnicity),
374
+
375
+ # 5. Gender
376
+ starIfSelected("Gender"),
377
+ ifelse(is.na(plot_data$Gender), "N/A", plot_data$Gender),
378
+
379
+ # 6. Religion
380
+ starIfSelected("Religion"),
381
+ ifelse(is.na(plot_data$Religion), "N/A", plot_data$Religion),
382
+
383
+ # 7. Language
384
+ starIfSelected("Language"),
385
+ ifelse(is.na(plot_data$Language), "N/A", plot_data$Language)
386
+ ) %>% lapply(htmltools::HTML)
387
+
388
+ leafletProxy("cartogramPlot", data = plot_data) %>%
389
+ clearShapes() %>% # clear existing polygons
390
+ clearControls() %>% # clear existing legends
391
+ addPolygons(
392
+ fillColor = ~pal(get(index_col)),
393
+ fillOpacity = 0.7,
394
+ color = "grey20",
395
+ weight = 0.4,
396
+ layerId = ~iso_a3,
397
+ label = labels,
398
+ highlightOptions = highlightOptions(
399
+ color = "white",
400
+ weight = 2,
401
+ bringToFront = TRUE
402
+ )
403
+ ) %>%
404
+ addLegend(
405
+ position = "bottomright",
406
+ pal = pal,
407
+ bins = 5,
408
+ values = plot_data[[index_col]],
409
+ title = paste(index_col, "Index"),
410
+ opacity = 1
411
+ )
412
+ }, ignoreNULL = FALSE) # Trigger once on startup too
413
+
414
+ # Track which country was clicked
415
+ selected_iso <- reactiveVal(NULL)
416
+
417
+ observeEvent(input$cartogramPlot_shape_click, {
418
+ click <- input$cartogramPlot_shape_click
419
+ if (!is.null(click$id)) {
420
+ selected_iso(click$id)
421
+ }
422
+ })
423
+
424
+ # Reactive to fetch the selected country's data
425
+ selected_data <- reactive({
426
+ iso <- selected_iso()
427
+ if (is.null(iso)) return(NULL)
428
+ rankings_data() %>%
429
+ filter(iso_a3 == iso)
430
+ })
431
+
432
+ # Render the selected country data in the box
433
+ output$selectedCountryData <- renderUI({
434
+ if (is.null(selected_data())) {
435
+ HTML("<p>Select a country by clicking on the map.</p>")
436
+ } else {
437
+ data <- selected_data()
438
+ html_content <- paste0(
439
+ "<b>Country:</b> ", data$Country, "<br/>",
440
+ "<b>Overall:</b> ", ifelse(is.na(data$Overall), "N/A", data$Overall), "<br/>",
441
+ "<b>Representation Gap:</b> ", ifelse(is.na(data$RepresentationGap), "N/A", data$RepresentationGap), "<br/>",
442
+ "<b>Ethnicity:</b> ", ifelse(is.na(data$Ethnicity), "N/A", data$Ethnicity), "<br/>",
443
+ "<b>Gender:</b> ", ifelse(is.na(data$Gender), "N/A", data$Gender), "<br/>",
444
+ "<b>Religion:</b> ", ifelse(is.na(data$Religion), "N/A", data$Religion), "<br/>",
445
+ "<b>Language:</b> ", ifelse(is.na(data$Language), "N/A", data$Language)
446
+ )
447
+ HTML(html_content)
448
+ }
449
+ })
450
+ }
451
+
452
+ # =============================
453
+ # Launch the Shiny App
454
+ # =============================
455
+ shinyApp(ui = ui, server = server)
456
+