cjerzak commited on
Commit
5199cd8
·
verified ·
1 Parent(s): 23938b8

Update Warmup/app_v1.R

Browse files
Files changed (1) hide show
  1. Warmup/app_v1.R +419 -0
Warmup/app_v1.R CHANGED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(ggplot2)
13
+ library(ggiraph) # For interactive hover tooltips and selections
14
+
15
+ # =============================
16
+ # UI
17
+ # =============================
18
+ ui <- dashboardPage(
19
+ skin = "black",
20
+ dashboardHeader(
21
+ title = span(
22
+ style = "font-weight: 600; font-size: 13px;",
23
+ a(
24
+ href = "http://www.globalleadershipproject.net",
25
+ "GlobalLeadershipProject.net",
26
+ target = "_blank",
27
+ style = "color: white; text-decoration: underline;"
28
+ )
29
+ )
30
+ ),
31
+ dashboardSidebar(
32
+ sidebarMenu(
33
+ menuItem("Map Type", tabName = "cartogramTab", icon = icon("globe"))
34
+ ),
35
+ div(
36
+ style = "margin: 15px;",
37
+ selectInput(
38
+ inputId = "indexChoice",
39
+ label = "Select Representation Index:",
40
+ choices = c("Overall", "RepresentationGap", "Ethnicity",
41
+ "Gender", "Religion", "Language"),
42
+ selected = "Overall"
43
+ )
44
+ ),
45
+
46
+ # ---- Here is the minimal "Share" button HTML + JS inlined in Shiny ----
47
+ # We wrap it in tags$div(...) and tags$script(HTML(...)) so it is recognized
48
+ # by Shiny. You can adjust the styling or placement as needed.
49
+ tags$div(
50
+ style = "text-align: left; margin: 1em 0 1em 2em;",
51
+ HTML('
52
+ <button id="share-button"
53
+ style="
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ gap: 8px;
58
+ padding: 5px 10px;
59
+ font-size: 16px;
60
+ font-weight: normal;
61
+ color: #000;
62
+ background-color: #fff;
63
+ border: 1px solid #ddd;
64
+ border-radius: 6px;
65
+ cursor: pointer;
66
+ box-shadow: 0 1.5px 0 #000;
67
+ ">
68
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
69
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
70
+ <circle cx="18" cy="5" r="3"></circle>
71
+ <circle cx="6" cy="12" r="3"></circle>
72
+ <circle cx="18" cy="19" r="3"></circle>
73
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
74
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
75
+ </svg>
76
+ <strong>Share</strong>
77
+ </button>
78
+ '),
79
+ # Insert the JS as well
80
+ tags$script(
81
+ HTML("
82
+ (function() {
83
+ const shareBtn = document.getElementById('share-button');
84
+ // Reusable helper function to show a small “Copied!” message
85
+ function showCopyNotification() {
86
+ const notification = document.createElement('div');
87
+ notification.innerText = 'Copied to clipboard';
88
+ notification.style.position = 'fixed';
89
+ notification.style.bottom = '20px';
90
+ notification.style.right = '20px';
91
+ notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
92
+ notification.style.color = '#fff';
93
+ notification.style.padding = '8px 12px';
94
+ notification.style.borderRadius = '4px';
95
+ notification.style.zIndex = '9999';
96
+ document.body.appendChild(notification);
97
+ setTimeout(() => { notification.remove(); }, 2000);
98
+ }
99
+ shareBtn.addEventListener('click', function() {
100
+ const currentURL = window.location.href;
101
+ const pageTitle = document.title || 'Check this out!';
102
+ // If browser supports Web Share API
103
+ if (navigator.share) {
104
+ navigator.share({
105
+ title: pageTitle,
106
+ text: '',
107
+ url: currentURL
108
+ })
109
+ .catch((error) => {
110
+ console.log('Sharing failed', error);
111
+ });
112
+ } else {
113
+ // Fallback: Copy URL
114
+ if (navigator.clipboard && navigator.clipboard.writeText) {
115
+ navigator.clipboard.writeText(currentURL).then(() => {
116
+ showCopyNotification();
117
+ }, (err) => {
118
+ console.error('Could not copy text: ', err);
119
+ });
120
+ } else {
121
+ // Double fallback for older browsers
122
+ const textArea = document.createElement('textarea');
123
+ textArea.value = currentURL;
124
+ document.body.appendChild(textArea);
125
+ textArea.select();
126
+ try {
127
+ document.execCommand('copy');
128
+ showCopyNotification();
129
+ } catch (err) {
130
+ alert('Please copy this link:\\n' + currentURL);
131
+ }
132
+ document.body.removeChild(textArea);
133
+ }
134
+ }
135
+ });
136
+ })();
137
+ ")
138
+ )
139
+ )
140
+ # ---- End: Minimal Share button snippet ----
141
+ ),
142
+ dashboardBody(
143
+ tags$head(
144
+ tags$link(
145
+ href = "https://fonts.googleapis.com/css2?family=OCR+A+Extended&display=swap",
146
+ rel = "stylesheet"
147
+ ),
148
+ tags$style(HTML("
149
+ /* Force OCR A Extended font across the entire UI and all HTML elements */
150
+ html, body, h1, h2, h3, h4, h5, h6, p, div, span, label, input, button, select,
151
+ .box, .content-wrapper, .main-sidebar, .main-header .navbar, .main-header .logo,
152
+ .sidebar-menu, .sidebar-menu li a, .sidebar-menu .fa {
153
+ font-family: 'OCR A Extended', monospace !important;
154
+ }
155
+
156
+ /* Header gradient background */
157
+ .main-header .navbar {
158
+ background: linear-gradient(to right, #3b6978, #204051) !important;
159
+ }
160
+ /* Logo area (left corner of the header) */
161
+ .main-header .logo {
162
+ background: #1b2a2f !important;
163
+ color: #ffffff !important;
164
+ border-bottom: none;
165
+ font-size: 18px;
166
+ font-weight: 600;
167
+ }
168
+ /* Sidebar background */
169
+ .main-sidebar {
170
+ background-color: #1b2a2f !important;
171
+ }
172
+ /* Active or hovered tab in the sidebar */
173
+ .sidebar-menu > li.active > a,
174
+ .sidebar-menu > li:hover > a {
175
+ background-color: #344e5c !important;
176
+ border-left-color: #78cdd7 !important;
177
+ color: #ffffff !important;
178
+ }
179
+
180
+ /* Sidebar menu item icons */
181
+ .sidebar-menu .fa {
182
+ color: #78cdd7 !important;
183
+ }
184
+
185
+ /* Sidebar menu item text */
186
+ .sidebar-menu > li > a {
187
+ color: #b8c7ce !important;
188
+ font-size: 15px;
189
+ font-weight: 500;
190
+ }
191
+
192
+ /* Customize the boxes */
193
+ .box {
194
+ border-top: none !important;
195
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
196
+ border-radius: 6px;
197
+ }
198
+ .box.box-solid > .box-header {
199
+ background-color: #204051;
200
+ color: #fff;
201
+ border-radius: 6px 6px 0 0;
202
+ }
203
+ /* Plot box spacing */
204
+ .box .box-body {
205
+ padding: 0 !important;
206
+ }
207
+ /* Footer text styling (plot captions, etc.) */
208
+ .small, small {
209
+ font-size: 75%;
210
+ }
211
+ /* Responsive map container */
212
+ .map-container {
213
+ height: 70vh;
214
+ width: 100%;
215
+ }
216
+ @media (max-width: 768px) {
217
+ .map-container {
218
+ height: 50vh;
219
+ }
220
+ }
221
+ "))
222
+ ),
223
+ tabItem(
224
+ tabName = "cartogramTab",
225
+ fluidRow(
226
+ column(
227
+ width = 9,
228
+ box(
229
+ width = NULL,
230
+ title = strong("Global Leadership Project (GLP)"),
231
+ solidHeader = TRUE,
232
+ div(
233
+ class = "map-container",
234
+ girafeOutput("cartogramPlot", width = "100%", height = "100%")
235
+ )
236
+ )
237
+ ),
238
+
239
+ column(
240
+ width = 3,
241
+ box(
242
+ width = NULL,
243
+ title = strong("Selected Country Data"),
244
+ solidHeader = TRUE,
245
+ uiOutput("selectedCountryData"),
246
+ style = "overflow-y: auto;" # Scroll if content overflows
247
+ )
248
+ )
249
+ ),
250
+
251
+ # -- 2nd row: citation spanning the full width below --
252
+ fluidRow(
253
+ column(
254
+ width = 9,
255
+ box(
256
+ width = NULL,
257
+ title = strong("Citation"),
258
+ solidHeader = TRUE,
259
+
260
+ tags$p(
261
+ "John Gerring, Alan Hicken, Connor T. Jerzak, Erzen Öncel.
262
+ The Composition of Descriptive Representation. ",
263
+ em("American Political Science Review,"), " 118(2): 784–801, 2024.",
264
+ tags$a(
265
+ href = "https://www.cambridge.org/core/services/aop-cambridge-core/content/view/7EAEA1CA4C553AB9D76054D1FA9C0840/S0003055423000680a.pdf/the-composition-of-descriptive-representation.pdf",
266
+ "PDF",
267
+ target = "_blank"
268
+ ),
269
+ " | ",
270
+ tags$a(
271
+ href = "https://connorjerzak.com/wp-content/uploads/2024/07/CompositionBib.txt",
272
+ "BibTeX",
273
+ target = "_blank"
274
+ )
275
+ )
276
+ )
277
+ )
278
+ )
279
+ )
280
+ )
281
+ )
282
+
283
+ # =============================
284
+ # SERVER
285
+ # =============================
286
+ server <- function(input, output, session) {
287
+
288
+ # 1. Custom matches for countries not recognized by default in 'countrycode'
289
+ custom_iso_matches <- c("Kosovo" = "XKX",
290
+ "Somaliland" = "SOM") # or any valid code you prefer
291
+
292
+ # 2. Read CSV data and create ISO3 codes with custom matches
293
+ rankings_data <- reactive({
294
+ read_csv("CountryRepresentationRankings.csv", show_col_types = FALSE) %>%
295
+ mutate(iso_a3 = countrycode(
296
+ sourcevar = Country,
297
+ origin = "country.name",
298
+ destination = "iso3c",
299
+ custom_match = custom_iso_matches
300
+ ))
301
+ })
302
+
303
+ # 3. Read/prepare world map shapefile
304
+ world_sf <- reactive({
305
+ ne_countries(scale = "medium", returnclass = "sf") %>%
306
+ dplyr::select(name, iso_a3, pop_est, geometry) %>%
307
+ st_transform(crs = "ESRI:54009") # Mollweide projection
308
+ })
309
+
310
+ # 4. Create the joined sf object (currently just a regular map)
311
+ cartogram_sf <- reactive({
312
+ merged_sf <- world_sf() %>%
313
+ left_join(rankings_data(), by = "iso_a3")
314
+ merged_sf <- merged_sf[!is.na(merged_sf$Overall),]
315
+ merged_sf
316
+ })
317
+
318
+ # 5. Render the interactive map with ggiraph
319
+ output$cartogramPlot <- renderGirafe({
320
+ req(input$indexChoice)
321
+
322
+ plot_data <- cartogram_sf()
323
+ index_col <- input$indexChoice
324
+
325
+ # Build a tooltip string (same as data to display on click)
326
+ plot_data$tooltip_text <- paste0(
327
+ "<b>Country:</b> ", plot_data$Country, "<br/>",
328
+ "<b>Overall:</b> ", ifelse(!is.na(plot_data$Overall), plot_data$Overall, "N/A"), "<br/>",
329
+ "<b>Representation Gap:</b> ", ifelse(!is.na(plot_data$RepresentationGap), plot_data$RepresentationGap, "N/A"), "<br/>",
330
+ "<b>Ethnicity:</b> ", ifelse(!is.na(plot_data$Ethnicity), plot_data$Ethnicity, "N/A"), "<br/>",
331
+ "<b>Gender:</b> ", ifelse(!is.na(plot_data$Gender), plot_data$Gender, "N/A"), "<br/>",
332
+ "<b>Religion:</b> ", ifelse(!is.na(plot_data$Religion), plot_data$Religion, "N/A"), "<br/>",
333
+ "<b>Language:</b> ", ifelse(!is.na(plot_data$Language), plot_data$Language, "N/A")
334
+ )
335
+
336
+ p <- ggplot(plot_data) +
337
+ geom_sf_interactive(
338
+ aes(
339
+ fill = get(index_col),
340
+ tooltip = tooltip_text,
341
+ data_id = iso_a3 # Enable selection with unique identifier
342
+ ),
343
+ color = "grey20",
344
+ size = 0.1
345
+ ) +
346
+ scale_fill_viridis_c(option = "D", na.value = "white") +
347
+ coord_sf(expand = FALSE) +
348
+ theme_void(base_size = 14, base_family = "sans") +
349
+ labs(
350
+ fill = paste(index_col, "Index"),
351
+ title = "Country-level Representation",
352
+ subtitle = "Map Colored by Selected Representation Index",
353
+ caption = "Source: Global Leadership Project (GLP) & Natural Earth"
354
+ ) +
355
+ theme(
356
+ plot.title = element_text(face = "bold", hjust = 0.5, size = 20),
357
+ plot.subtitle = element_text(hjust = 0.5, size = 14),
358
+ plot.caption = element_text(hjust = 1, size = 10),
359
+ legend.position = "bottom",
360
+ legend.direction = "horizontal",
361
+ legend.key.width = unit(2, "cm")
362
+ )
363
+
364
+ girafe(
365
+ ggobj = p,
366
+ width_svg = 10,
367
+ height_svg = 6,
368
+ options = list(
369
+ opts_tooltip(
370
+ css = "background-color: white;
371
+ font-family: 'OCR A Extended', monospace;
372
+ color: black;"
373
+ ),
374
+ opts_selection(type = "single"), # Single selection: one country at a time
375
+ opts_zoom(min = 1, max = 10), # <-- Enables zooming
376
+ opts_toolbar(position = "topright") # <-- Adds a small toolbar
377
+ )
378
+ )
379
+ })
380
+
381
+ # Reactive to track the selected country's ISO code
382
+ selected_iso <- reactive({
383
+ input$cartogramPlot_selected # Returns the data_id of the selected country or NULL
384
+ })
385
+
386
+ # Reactive to fetch the selected country's data
387
+ selected_data <- reactive({
388
+ if (is.null(selected_iso())) {
389
+ return(NULL)
390
+ } else {
391
+ rankings_data() %>%
392
+ filter(iso_a3 == selected_iso())
393
+ }
394
+ })
395
+
396
+ # Render the selected country data below the map
397
+ output$selectedCountryData <- renderUI({
398
+ if (is.null(selected_data())) {
399
+ HTML("<p>Select a country by clicking on the map.</p>")
400
+ } else {
401
+ data <- selected_data()
402
+ html_content <- paste0(
403
+ "<b>Country:</b> ", data$Country, "<br/>",
404
+ "<b>Overall:</b> ", ifelse(is.na(data$Overall), "N/A", data$Overall), "<br/>",
405
+ "<b>Representation Gap:</b> ", ifelse(is.na(data$RepresentationGap), "N/A", data$RepresentationGap), "<br/>",
406
+ "<b>Ethnicity:</b> ", ifelse(is.na(data$Ethnicity), "N/A", data$Ethnicity), "<br/>",
407
+ "<b>Gender:</b> ", ifelse(is.na(data$Gender), "N/A", data$Gender), "<br/>",
408
+ "<b>Religion:</b> ", ifelse(is.na(data$Religion), "N/A", data$Religion), "<br/>",
409
+ "<b>Language:</b> ", ifelse(is.na(data$Language), "N/A", data$Language)
410
+ )
411
+ HTML(html_content)
412
+ }
413
+ })
414
+ }
415
+
416
+ # =============================
417
+ # Launch the Shiny App
418
+ # =============================
419
+ shinyApp(ui = ui, server = server)