cjerzak commited on
Commit
16a2711
·
verified ·
1 Parent(s): 50a762a

Update app.R

Browse files
Files changed (1) hide show
  1. app.R +248 -351
app.R CHANGED
@@ -1,105 +1,119 @@
1
- # setwd("~/Dropbox/OptimizingSI/Analysis/ono")
2
- # install.packages("~/Documents/strategize-software/strategize", repos = NULL, type = "source", force = FALSE)
3
-
4
- # =============================================================================
5
- # app_ono.R
6
- # Async, navigation‑friendly Shiny demo for strategize‑Ono
7
- # ---------------------------------------------------------------------------
8
- # * Heavy strategize jobs run in a background R session via future/promises.
9
- # * UI stays responsive; you can browse old results while a new run crunches.
10
- # * STARTUP‑SAFE and INPUT‑SAFE:
11
- # • req(input$case_type) prevents length‑zero error.
12
- # • Reactive inputs are captured (isolated) *before* the future() call,
13
- # fixing “Can't access reactive value outside reactive consumer.”
14
- # =============================================================================
15
 
16
  options(error = NULL)
17
-
18
  library(shiny)
19
  library(ggplot2)
20
  library(strategize)
21
  library(dplyr)
22
 
23
- # ---- Async helpers ----------------------------------------------------------
24
- library(promises)
25
- library(future) ; plan(multisession) # 1 worker per core
26
- library(shinyjs)
27
-
28
- # =============================================================================
29
- # Custom plotting function (unchanged)
30
- # =============================================================================
31
- plot_factor <- function(pi_star_list,
32
- pi_star_se_list,
33
- factor_name,
34
  zStar = 1.96,
35
  n_strategies = 1L) {
36
-
37
- probs <- lapply(pi_star_list, function(x) x[[factor_name]])
38
- ses <- lapply(pi_star_se_list, function(x) x[[factor_name]])
39
  levels <- names(probs[[1]])
40
 
41
- df <- do.call(rbind, lapply(seq_len(n_strategies), function(i) {
 
42
  data.frame(
43
- Strategy = if (n_strategies == 1) "Optimal"
44
- else c("Democrat", "Republican")[i],
45
- Level = levels,
46
  Probability = probs[[i]]
 
47
  )
48
  }))
49
 
50
- df$Level_num <- as.numeric(as.factor(df$Level))
51
- df$x_dodged <- if (n_strategies == 1)
52
- df$Level_num
53
- else
54
- df$Level_num + ifelse(df$Strategy == "Democrat", -0.05, 0.05)
 
 
 
 
 
55
 
56
- ggplot(df, aes(x = x_dodged, y = Probability, color = Strategy)) +
57
- geom_segment(aes(x = x_dodged, xend = x_dodged,
58
- y = 0, yend = Probability), size = 0.3) +
59
- geom_point(size = 2.5) +
60
- geom_text(aes(label = sprintf("%.2f", Probability)),
61
- vjust = -0.7, size = 3) +
62
- scale_x_continuous(breaks = unique(df$Level_num),
63
- labels = unique(df$Level),
64
- limits = c(min(df$x_dodged) - 0.20,
65
- max(df$x_dodged) + 0.20)) +
66
- labs(title = "Optimal Distribution for:",
67
- subtitle = sprintf("*%s*",
68
- gsub(factor_name, pattern = "\\.", replace = " ")),
69
- x = "Level",
70
- y = "Probability") +
71
- theme_minimal(base_size = 18) +
72
- theme(legend.position = "none",
73
- legend.title = element_blank(),
74
- panel.grid.major = element_blank(),
75
- panel.grid.minor = element_blank(),
76
- axis.line = element_line(color = "black", size = 0.5),
77
- axis.text.x = element_text(angle = 45, hjust = 1,
78
- margin = margin(r = 10))) +
79
- scale_color_manual(values = c(Democrat = "#89cff0",
80
- Republican = "red",
81
- Optimal = "black"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
- # =============================================================================
85
- # UI (identical to previous async version—only shinyjs::useShinyjs() added)
86
- # =============================================================================
87
  ui <- fluidPage(
88
- useShinyjs(),
89
-
90
  titlePanel("Exploring strategize with the candidate choice conjoint data"),
91
 
92
  tags$p(
93
  style = "text-align: left; margin-top: -10px;",
94
- tags$a(href = "https://strategizelab.org/",
95
- target = "_blank",
96
- title = "strategizelab.org",
97
- style = "color: #337ab7; text-decoration: none;",
98
- "strategizelab.org ",
99
- icon("external-link", style = "font-size: 12px;"))
 
 
100
  ),
101
 
102
- # ---- Share button (unchanged) --------------------------------------------
103
  tags$div(
104
  style = "text-align: left; margin: 0.5em 0 0.5em 0em;",
105
  HTML('
@@ -119,9 +133,8 @@ ui <- fluidPage(
119
  cursor: pointer;
120
  box-shadow: 0 1.5px 0 #000;
121
  ">
122
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
123
- stroke="currentColor" stroke-width="2" stroke-linecap="round"
124
- stroke-linejoin="round">
125
  <circle cx="18" cy="5" r="3"></circle>
126
  <circle cx="6" cy="12" r="3"></circle>
127
  <circle cx="18" cy="19" r="3"></circle>
@@ -133,36 +146,68 @@ ui <- fluidPage(
133
  '),
134
  tags$script(
135
  HTML("
136
- (function() {
137
- const shareBtn = document.getElementById('share-button');
138
- function toast() {
139
- const n = document.createElement('div');
140
- n.innerText = 'Copied to clipboard';
141
- Object.assign(n.style, {
142
- position:'fixed',bottom:'20px',right:'20px',
143
- background:'rgba(0,0,0,0.8)',color:'#fff',
144
- padding:'8px 12px',borderRadius:'4px',zIndex:9999});
145
- document.body.appendChild(n); setTimeout(()=>n.remove(),2000);
146
- }
147
- shareBtn.addEventListener('click', ()=>{
148
- const url = window.location.href;
149
- if (navigator.share) {
150
- navigator.share({title:document.title||'Link',url})
151
- .catch(()=>{});
152
- } else if (navigator.clipboard) {
153
- navigator.clipboard.writeText(url).then(toast);
154
- } else {
155
- const ta = document.createElement('textarea');
156
- ta.value=url; document.body.appendChild(ta); ta.select();
157
- try{document.execCommand('copy'); toast();}
158
- catch(e){alert('Copy this link:\\n'+url);} ta.remove();
159
  }
160
- });
161
- })();")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  )
163
- ),
 
164
 
165
  sidebarLayout(
 
166
  sidebarPanel(
167
  h4("Analysis Options"),
168
  radioButtons("case_type", "Case Type:",
@@ -172,23 +217,32 @@ ui <- fluidPage(
172
  condition = "input.case_type == 'Average'",
173
  selectInput("respondent_group", "Respondent Group:",
174
  choices = c("All", "Democrat", "Independent", "Republican"),
175
- selected = "Democrat")
176
  ),
177
- numericInput("lambda_input", "Lambda (regularization):",
178
- value = 0.01, min = 1e-6, max = 10, step = 0.01),
 
 
 
 
 
 
 
 
 
179
  actionButton("compute", "Compute Results", class = "btn-primary"),
180
- div(id = "status_text",
181
- style = "margin-top:6px; font-style:italic; color:#555;"),
182
  hr(),
183
  h4("Visualization"),
184
- selectInput("factor", "Select Factor to Display:", choices = NULL),
 
185
  br(),
186
- selectInput("previousResults", "View Previous Results:", choices = NULL),
 
187
  hr(),
188
  h5("Instructions:"),
189
  p("1. Select a case type and, for Average case, a respondent group."),
190
- p("2. Specify the single lambda to be used by strategize."),
191
- p("3. Click 'Compute Results' to generate optimal strategies."),
192
  p("4. Choose a factor to view its distribution."),
193
  p("5. Use 'View Previous Results' to toggle among past computations.")
194
  ),
@@ -199,25 +253,21 @@ ui <- fluidPage(
199
  plotOutput("strategy_plot", height = "600px")),
200
  tabPanel("Q Value",
201
  verbatimTextOutput("q_value"),
202
- p("Q represents the estimated outcome under the optimal strategy,",
203
- "with 95% confidence interval.")),
204
  tabPanel("About",
205
  h3("About this page"),
206
  p("This page app explores the ",
207
- a("strategize R package",
208
- href = "https://github.com/cjerzak/strategize-software/",
209
- target = "_blank"),
210
- " using Ono forced conjoint experimental data.",
211
- "It computes optimal strategies for Average (optimizing for a respondent",
212
- "group) and Adversarial (optimizing for both parties in competition) cases",
213
- "on the fly."),
214
- p(strong("Average Case:"), "Optimizes candidate characteristics for a",
215
- "selected respondent group."),
216
- p(strong("Adversarial Case:"), "Finds equilibrium strategies for Democrats",
217
- "and Republicans."),
218
  p(strong("More information:"),
219
- a("strategizelab.org", href = "https://strategizelab.org",
220
- target = "_blank"))
221
  )
222
  ),
223
  br(),
@@ -229,254 +279,101 @@ ui <- fluidPage(
229
  )
230
  )
231
 
232
- # =============================================================================
233
- # SERVER
234
- # =============================================================================
235
  server <- function(input, output, session) {
 
 
 
236
 
237
- # ---- Data load (unchanged) -----------------------------------------------
238
- load("Processed_OnoData.RData")
239
- Primary2016 <- read.csv("PrimaryCandidates2016 - Sheet1.csv")
240
-
241
- # ---- Reactive stores ------------------------------------------------------
242
  cachedResults <- reactiveValues(data = list())
243
- runningFlags <- reactiveValues(active = list())
244
 
245
- # ---- Factor dropdown updater ---------------------------------------------
246
  observe({
247
- req(input$case_type)
248
  if (input$case_type == "Average") {
249
- factors <- setdiff(colnames(FACTOR_MAT_FULL), "Office")
250
  } else {
251
- factors <- setdiff(colnames(FACTOR_MAT_FULL),
252
- c("Office", "Party.affiliation", "Party.competition"))
253
  }
254
- updateSelectInput(session, "factor",
255
- choices = factors,
256
- selected = factors[1])
257
  })
258
 
259
- # ===========================================================================
260
- # Compute Results button
261
- # ===========================================================================
262
  observeEvent(input$compute, {
263
-
264
- ## ---- CAPTURE reactive inputs ------------------------------------------
265
- case_type <- isolate(input$case_type)
266
- respondent_group <- isolate(input$respondent_group)
267
- my_lambda <- isolate(input$lambda_input)
268
-
269
- label <- if (case_type == "Average") {
270
- paste0("Case=Average, Group=", respondent_group,
271
- ", Lambda=", my_lambda)
272
- } else {
273
- paste0("Case=Adversarial, Lambda=", my_lambda)
274
- }
275
-
276
- runningFlags$active[[label]] <- TRUE
277
- cachedResults$data[[label]] <- NULL
278
- updateSelectInput(session, "previousResults",
279
- choices = names(cachedResults$data),
280
- selected = label)
281
- shinyjs::html("status_text", "")
282
- shinyjs::html("status_text", "submitting…") # Immediately show “submitting…”
283
- shinyjs::delay(2000, shinyjs::html("status_text", "submitted")) # Two‑second later switch to “submitted”
284
- shinyjs::disable("compute")
285
- showNotification(sprintf("Job '%s' submitted …", label),
286
- type = "message", duration = 3)
287
-
288
- ## ---- FUTURE -----------------------------------------------------------
289
- future({
290
 
291
- strategize_start <- Sys.time()
292
-
293
- # --------------- shared hyper‑params ----------------------------------
294
- params <- list(
295
- nSGD = 1000L,
296
- batch_size = 50L,
297
- penalty_type = "KL",
298
- nFolds = 3L,
299
- use_optax = TRUE,
300
- compute_se = FALSE,
301
- conf_level = 0.95,
302
- conda_env = "strategize",
303
- conda_env_required = TRUE
304
- )
305
-
306
- if (case_type == "Average") {
307
- # ---------- Average case --------------------------------------------
308
- indices <- if (respondent_group == "All") {
309
- which(my_data$Office == "President")
310
- } else {
311
- which(my_data_FULL$R_Partisanship == respondent_group &
312
- my_data$Office == "President")
313
- }
314
-
315
- FACTOR_MAT <- FACTOR_MAT_FULL[indices,
316
- !colnames(FACTOR_MAT_FULL) %in%
317
- c("Office", "Party.affiliation", "Party.competition")]
318
- Yobs <- Yobs_FULL[indices]
319
- X <- X_FULL[indices, ]
320
- pair_id <- pair_id_FULL[indices]
321
- assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
322
-
323
- Qoptimized <- strategize(
324
- Y = Yobs,
325
- W = FACTOR_MAT,
326
- X = X,
327
- pair_id = pair_id,
328
- p_list = assignmentProbList[colnames(FACTOR_MAT)],
329
- lambda = my_lambda,
330
- diff = TRUE,
331
- adversarial = FALSE,
332
- use_regularization = TRUE,
333
- K = 1L,
334
- nSGD = params$nSGD,
335
- penalty_type = params$penalty_type,
336
- folds = params$nFolds,
337
- use_optax = params$use_optax,
338
- compute_se = params$compute_se,
339
- conf_level = params$conf_level,
340
- conda_env = params$conda_env,
341
- conda_env_required = params$conda_env_required
342
- )
343
- Qoptimized$n_strategies <- 1L
344
-
345
  } else {
346
- # ---------- Adversarial case ----------------------------------------
347
- DROP <- c("Office", "Party.affiliation", "Party.competition")
348
- FACTOR_MAT <- FACTOR_MAT_FULL[, !colnames(FACTOR_MAT_FULL) %in% DROP]
349
- assignmentProbList <- assignmentProbList_FULL[!names(assignmentProbList_FULL) %in% DROP]
350
-
351
- # Build Primary slates
352
- FactorOptions <- apply(FACTOR_MAT, 2, table)
353
- prior_alpha <- 10
354
- Primary_D <- Primary2016[Primary2016$Party == "Democratic",
355
- colnames(FACTOR_MAT)]
356
- Primary_R <- Primary2016[Primary2016$Party == "Republican",
357
- colnames(FACTOR_MAT)]
358
- slate_fun <- function(df) {
359
- lapply(colnames(df), function(col) {
360
- post <- FactorOptions[[col]]; post[] <- prior_alpha
361
- emp <- table(df[[col]]); emp <- emp[names(emp) != "Unclear"]
362
- post[names(emp)] <- post[names(emp)] + emp
363
- prop.table(post)
364
- }) |> setNames(colnames(df))
365
- }
366
- slate_list <- list(Democratic = slate_fun(Primary_D),
367
- Republican = slate_fun(Primary_R))
368
-
369
- indices <- which(my_data$R_Partisanship %in% c("Republican", "Democrat") &
370
- my_data$Office == "President")
371
- FACTOR_MAT <- FACTOR_MAT_FULL[indices,
372
- !colnames(FACTOR_MAT_FULL) %in%
373
- c("Office", "Party.competition", "Party.affiliation")]
374
- Yobs <- Yobs_FULL[indices]
375
- my_data_red <- my_data_FULL[indices, ]
376
- pair_id <- pair_id_FULL[indices]
377
- cluster_var <- cluster_var_FULL[indices]
378
- my_data_red$Party.affiliation_clean <-
379
- ifelse(my_data_red$Party.affiliation == "Republican Party", "Republican",
380
- ifelse(my_data_red$Party.affiliation == "Democratic Party","Democrat","Independent"))
381
-
382
- assignmentProbList <- assignmentProbList_FULL[colnames(FACTOR_MAT)]
383
- slate_list$Democratic <- slate_list$Democratic[names(assignmentProbList)]
384
- slate_list$Republican <- slate_list$Republican[names(assignmentProbList)]
385
-
386
- Qoptimized <- strategize(
387
- Y = Yobs,
388
- W = FACTOR_MAT,
389
- X = NULL,
390
- p_list = assignmentProbList,
391
- slate_list = slate_list,
392
- varcov_cluster_variable = cluster_var,
393
- competing_group_variable_respondent = my_data_red$R_Partisanship,
394
- competing_group_variable_candidate = my_data_red$Party.affiliation_clean,
395
- competing_group_competition_variable_candidate =
396
- my_data_red$Party.competition,
397
- pair_id = pair_id,
398
- respondent_id = my_data_red$respondentIndex,
399
- respondent_task_id = my_data_red$task,
400
- profile_order = my_data_red$profile,
401
- lambda = my_lambda,
402
- diff = TRUE,
403
- use_regularization = TRUE,
404
- force_gaussian = FALSE,
405
- adversarial = TRUE,
406
- K = 1L,
407
- nMonte_adversarial = 20L,
408
- nSGD = params$nSGD,
409
- penalty_type = params$penalty_type,
410
- learning_rate_max = 0.001,
411
- use_optax = params$use_optax,
412
- compute_se = params$compute_se,
413
- conf_level = params$conf_level,
414
- conda_env = params$conda_env,
415
- conda_env_required = params$conda_env_required
416
- )
417
- Qoptimized$n_strategies <- 2L
418
  }
419
 
420
- Qoptimized$runtime_seconds <-
421
- as.numeric(difftime(Sys.time(), strategize_start, units = "secs"))
422
- Qoptimized[c("pi_star_point", "pi_star_se", "Q_point",
423
- "Q_se", "n_strategies", "runtime_seconds")]
424
- }) %...>% # success handler
425
- (function(res) {
426
- cachedResults$data[[label]] <- res
427
- runningFlags$active[[label]] <- FALSE
428
- updateSelectInput(session, "previousResults",
429
- choices = names(cachedResults$data),
430
- selected = label)
431
- shinyjs::html("status_text", "complete!")
432
- shinyjs::enable("compute")
433
- showNotification(sprintf("Job '%s' finished (%.1f s).",
434
- label, res$runtime_seconds),
435
- type = "message", duration = 6)
436
- }) %...!% # error handler
437
- (function(err) {
438
- runningFlags$active[[label]] <- FALSE
439
- cachedResults$data[[label]] <- NULL
440
- shinyjs::html("status_text", "error – see log")
441
- shinyjs::enable("compute")
442
- showNotification(paste("Error in", label, ":", err$message),
443
- type = "error", duration = 8)
444
- })
445
-
446
- NULL # return value of observeEvent
447
  })
448
 
449
- # ---- Helper: fetch selected result or show waiting msg -------------------
450
  selectedResult <- reactive({
451
- lbl <- input$previousResults ; req(lbl)
452
- if (isTRUE(runningFlags$active[[lbl]]))
453
- validate("Computation is still running – please wait…")
454
- res <- cachedResults$data[[lbl]]
455
- validate(need(!is.null(res), "No finished result selected."))
456
- res
457
  })
458
 
459
- # ---- Outputs -------------------------------------------------------------
460
  output$strategy_plot <- renderPlot({
461
- res <- selectedResult()
462
- plot_factor(res$pi_star_point, res$pi_star_se,
463
- factor_name = input$factor,
464
- n_strategies = res$n_strategies)
 
 
 
 
 
465
  })
466
 
 
467
  output$q_value <- renderText({
468
- res <- selectedResult()
469
- q_pt <- res$Q_point; q_se <- res$Q_se
470
- txt <- if (length(q_se) && q_se > 0)
471
- sprintf("Estimated Q Value: %.3f ± %.3f", q_pt, 1.96*q_se)
472
- else sprintf("Estimated Q Value: %.3f", q_pt)
473
- sprintf("%s (Runtime: %.2f s)", txt, res$runtime_seconds)
 
 
 
 
474
  })
475
 
476
- output$selection_summary <- renderText({ input$previousResults })
 
 
 
477
  }
478
 
479
- # =============================================================================
480
- # Run the app
481
- # =============================================================================
482
  shinyApp(ui, server)
 
 
1
+ # setwd("~/Dropbox/OptimizingSI/App")
2
+ # install.packages( "~/Documents/strategize-software/strategize", repos = NULL, type = "source",force = F)
3
+ # Script: app_ono.R
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  options(error = NULL)
 
6
  library(shiny)
7
  library(ggplot2)
8
  library(strategize)
9
  library(dplyr)
10
 
11
+ # Custom plotting function for optimal strategy distributions
12
+ plot_factor <- function(pi_star_list,
13
+ pi_star_se_list,
14
+ factor_name,
 
 
 
 
 
 
 
15
  zStar = 1.96,
16
  n_strategies = 1L) {
17
+ probs <- lapply(pi_star_list, function(x) x[[factor_name]])
18
+ ses <- lapply(pi_star_se_list, function(x) x[[factor_name]])
 
19
  levels <- names(probs[[1]])
20
 
21
+ # Create data frame for plotting
22
+ df <- do.call(rbind, lapply(1:n_strategies, function(i) {
23
  data.frame(
24
+ Strategy = if (n_strategies == 1) "Optimal" else c("Democrat", "Republican")[i],
25
+ Level = levels,
 
26
  Probability = probs[[i]]
27
+ #SE = ses[[i]]
28
  )
29
  }))
30
 
31
+ # Manual dodging: Create numeric x-positions with offsets
32
+ df$Level_num <- as.numeric(as.factor(df$Level)) # Convert Level to numeric (1, 2, ...)
33
+ if (n_strategies == 1) {
34
+ df$x_dodged <- df$Level_num # No dodging for single strategy
35
+ } else {
36
+ # Apply ±offset for Democrat/Republican
37
+ df$x_dodged <- df$Level_num +
38
+ ifelse(df$Strategy == "Democrat",
39
+ -0.05, 0.05)
40
+ }
41
 
42
+ # Plot with ggplot2
43
+ p <- ggplot(df, aes(x = x_dodged,
44
+ y = Probability,
45
+ color = Strategy)) +
46
+ # Segment from y=0 to y=Probability
47
+ geom_segment(
48
+ aes(x = x_dodged, xend = x_dodged,
49
+ y = 0, yend = Probability),
50
+ size = 0.3
51
+ ) +
52
+ # Point at the probability
53
+ geom_point(
54
+ size = 2.5
55
+ ) +
56
+ # Text label above the point
57
+ geom_text(
58
+ aes(x = x_dodged,
59
+ label = sprintf("%.2f", Probability)),
60
+ vjust = -0.7,
61
+ size = 3
62
+ ) +
63
+ # Set x-axis with original Level labels
64
+ scale_x_continuous(
65
+ breaks = unique(df$Level_num),
66
+ labels = unique(df$Level),
67
+ limits = c(min(df$x_dodged)-0.20,
68
+ max(df$x_dodged)+0.20)
69
+ ) +
70
+ # Labels
71
+ labs(
72
+ title = "Optimal Distribution for:",
73
+ subtitle = sprintf("*%s*", gsub(factor_name,
74
+ pattern = "\\.",
75
+ replace = " ")),
76
+ x = "Level",
77
+ y = "Probability"
78
+ ) +
79
+ # Apply Tufte's minimalistic theme
80
+ theme_minimal(base_size = 18,
81
+ base_line_size = 0) +
82
+ theme(
83
+ legend.position = "none",
84
+ legend.title = element_blank(),
85
+ panel.grid.major = element_blank(),
86
+ panel.grid.minor = element_blank(),
87
+ axis.line = element_line(color = "black", size = 0.5),
88
+ axis.text.x = element_text(angle = 45,
89
+ hjust = 1,
90
+ margin = margin(r = 10)) # Add right margin
91
+ ) +
92
+ # Manual color scale for different strategies
93
+ scale_color_manual(values = c("Democrat" = "#89cff0",
94
+ "Republican" = "red",
95
+ "Optimal" = "black"))
96
+
97
+ return(p)
98
  }
99
 
100
+ # UI Definition
 
 
101
  ui <- fluidPage(
 
 
102
  titlePanel("Exploring strategize with the candidate choice conjoint data"),
103
 
104
  tags$p(
105
  style = "text-align: left; margin-top: -10px;",
106
+ tags$a(
107
+ href = "https://strategizelab.org/",
108
+ target = "_blank",
109
+ title = "strategizelab.org",
110
+ style = "color: #337ab7; text-decoration: none;",
111
+ "strategizelab.org ",
112
+ icon("external-link", style = "font-size: 12px;")
113
+ )
114
  ),
115
 
116
+ # ---- Minimal "Share" button HTML + JS inlined ----
117
  tags$div(
118
  style = "text-align: left; margin: 0.5em 0 0.5em 0em;",
119
  HTML('
 
133
  cursor: pointer;
134
  box-shadow: 0 1.5px 0 #000;
135
  ">
136
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
137
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
138
  <circle cx="18" cy="5" r="3"></circle>
139
  <circle cx="6" cy="12" r="3"></circle>
140
  <circle cx="18" cy="19" r="3"></circle>
 
146
  '),
147
  tags$script(
148
  HTML("
149
+ (function() {
150
+ const shareBtn = document.getElementById('share-button');
151
+ // Reusable helper function to show a small “Copied!” message
152
+ function showCopyNotification() {
153
+ const notification = document.createElement('div');
154
+ notification.innerText = 'Copied to clipboard';
155
+ notification.style.position = 'fixed';
156
+ notification.style.bottom = '20px';
157
+ notification.style.right = '20px';
158
+ notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
159
+ notification.style.color = '#fff';
160
+ notification.style.padding = '8px 12px';
161
+ notification.style.borderRadius = '4px';
162
+ notification.style.zIndex = '9999';
163
+ document.body.appendChild(notification);
164
+ setTimeout(() => { notification.remove(); }, 2000);
 
 
 
 
 
 
 
165
  }
166
+ shareBtn.addEventListener('click', function() {
167
+ const currentURL = window.location.href;
168
+ const pageTitle = document.title || 'Check this out!';
169
+ // If browser supports Web Share API
170
+ if (navigator.share) {
171
+ navigator.share({
172
+ title: pageTitle,
173
+ text: '',
174
+ url: currentURL
175
+ })
176
+ .catch((error) => {
177
+ console.log('Sharing failed', error);
178
+ });
179
+ } else {
180
+ // Fallback: Copy URL
181
+ if (navigator.clipboard && navigator.clipboard.writeText) {
182
+ navigator.clipboard.writeText(currentURL).then(() => {
183
+ showCopyNotification();
184
+ }, (err) => {
185
+ console.error('Could not copy text: ', err);
186
+ });
187
+ } else {
188
+ // Double fallback for older browsers
189
+ const textArea = document.createElement('textarea');
190
+ textArea.value = currentURL;
191
+ document.body.appendChild(textArea);
192
+ textArea.select();
193
+ try {
194
+ document.execCommand('copy');
195
+ showCopyNotification();
196
+ } catch (err) {
197
+ alert('Please copy this link:\\n' + currentURL);
198
+ }
199
+ document.body.removeChild(textArea);
200
+ }
201
+ }
202
+ });
203
+ })();
204
+ ")
205
  )
206
+ ),
207
+ # ---- End: Minimal Share button snippet ----
208
 
209
  sidebarLayout(
210
+ # -- In app_ono.R, inside `ui` definition --
211
  sidebarPanel(
212
  h4("Analysis Options"),
213
  radioButtons("case_type", "Case Type:",
 
217
  condition = "input.case_type == 'Average'",
218
  selectInput("respondent_group", "Respondent Group:",
219
  choices = c("All", "Democrat", "Independent", "Republican"),
220
+ selected = "All")
221
  ),
222
+ # Selected lambda
223
+ selectInput(
224
+ inputId = "lambda_input",
225
+ label = "Lambda (regularization):",
226
+ choices = c("0.001" = 0.001,
227
+ "0.01" = 0.01,
228
+ "0.1" = 0.1),
229
+ selected = 0.01
230
+ )
231
+ ,
232
+
233
  actionButton("compute", "Compute Results", class = "btn-primary"),
 
 
234
  hr(),
235
  h4("Visualization"),
236
+ selectInput("factor", "Select Factor to Display:",
237
+ choices = NULL),
238
  br(),
239
+ selectInput("previousResults", "View Previous Results:",
240
+ choices = NULL),
241
  hr(),
242
  h5("Instructions:"),
243
  p("1. Select a case type and, for Average case, a respondent group."),
244
+ p("2. Specify the single lambda to be used."),
245
+ p("3. Click 'Compute Results' to retrieve the pre-computed optimal strategies."),
246
  p("4. Choose a factor to view its distribution."),
247
  p("5. Use 'View Previous Results' to toggle among past computations.")
248
  ),
 
253
  plotOutput("strategy_plot", height = "600px")),
254
  tabPanel("Q Value",
255
  verbatimTextOutput("q_value"),
256
+ p("Q represents the estimated outcome
257
+ under the optimal strategy, with 95% confidence interval.")),
258
  tabPanel("About",
259
  h3("About this page"),
260
  p("This page app explores the ",
261
+ a("strategize R package", href = "https://github.com/cjerzak/strategize-software/", target = "_blank"),
262
+ " using Ono forced conjoint experimental data.
263
+ It computes optimal strategies for Average (optimizing for a respondent group)
264
+ and Adversarial (optimizing for both parties in competition) cases on the fly."),
265
+ p(strong("Average Case:"),
266
+ "Optimizes candidate characteristics for a selected respondent group."),
267
+ p(strong("Adversarial Case:"),
268
+ "Finds equilibrium strategies for Democrats and Republicans."),
 
 
 
269
  p(strong("More information:"),
270
+ a("strategizelab.org", href = "https://strategizelab.org", target = "_blank"))
 
271
  )
272
  ),
273
  br(),
 
279
  )
280
  )
281
 
282
+ # Server Definition
 
 
283
  server <- function(input, output, session) {
284
+ # Load data
285
+ load("./AppData/Processed_OnoData.RData")
286
+ Primary2016 <- read.csv("./AppData/PrimaryCandidates2016 - Sheet1.csv")
287
 
288
+ # Prepare a storage structure for caching multiple results
 
 
 
 
289
  cachedResults <- reactiveValues(data = list())
 
290
 
291
+ # Dynamic update of factor choices
292
  observe({
 
293
  if (input$case_type == "Average") {
294
+ factors <- colnames(FACTOR_MAT_FULL)[!colnames(FACTOR_MAT_FULL) %in% c("Office")]
295
  } else {
296
+ factors <- colnames(FACTOR_MAT_FULL)[!colnames(FACTOR_MAT_FULL) %in% c("Office", "Party.affiliation", "Party.competition")]
 
297
  }
298
+ updateSelectInput(session, "factor", choices = factors, selected = factors[1])
 
 
299
  })
300
 
301
+ # Generate a new result and cache it
302
+ # -- In app_ono.R, inside `server` definition --
 
303
  observeEvent(input$compute, {
304
+ withProgress(message = "Retrieving results...", value = 0, {
305
+ incProgress(0.2, detail = "Looking up precomputed results...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ # Construct a human-readable label (as before)
308
+ if (input$case_type == "Average") {
309
+ label <- paste("Case=Average, Group=", input$respondent_group,
310
+ ", Lambda=", input$lambda_input, sep="")
311
+ lam_char <- gsub("\\.", "PT", as.character(input$lambda_input))
312
+ filename <- paste0("Average_", input$respondent_group,
313
+ "_lambda", lam_char, ".rds")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  } else {
315
+ label <- paste("Case=Adversarial, Lambda=", input$lambda_input, sep="")
316
+ lam_char <- gsub("\\.", "PT", as.character(input$lambda_input))
317
+ filename <- paste0("Adversarial_lambda", lam_char, ".rds")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  }
319
 
320
+ # Read the matching pre-computed .rds file from disk
321
+ file_path <- file.path("AppResults", filename)
322
+ Qoptimized <- readRDS(file_path)
323
+
324
+ # Store the loaded results in our reactive cache
325
+ cachedResults$data[[label]] <- Qoptimized
326
+
327
+ incProgress(0.8, detail = "Finishing up...")
328
+
329
+ # Update the choice list for previous results
330
+ updateSelectInput(session, "previousResults",
331
+ choices = names(cachedResults$data),
332
+ selected = label)
333
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  })
335
 
336
+ # Reactive to pick the result the user wants to display
337
  selectedResult <- reactive({
338
+ validate(
339
+ need(input$previousResults != "", "No result computed or selected yet.")
340
+ )
341
+ cachedResults$data[[input$previousResults]]
 
 
342
  })
343
 
344
+ # Render strategy plot
345
  output$strategy_plot <- renderPlot({
346
+ req(selectedResult())
347
+ factor_name <- input$factor
348
+ pi_star_list <- selectedResult()$pi_star_point
349
+ pi_star_se_list <- selectedResult()$pi_star_se
350
+ n_strategies <- selectedResult()$n_strategies
351
+ plot_factor(pi_star_list = pi_star_list,
352
+ pi_star_se_list = pi_star_se_list,
353
+ factor_name = factor_name,
354
+ n_strategies = n_strategies)
355
  })
356
 
357
+ # Render Q value
358
  output$q_value <- renderText({
359
+ req(selectedResult())
360
+ q_point <- selectedResult()$Q_point
361
+ q_se <- selectedResult()$Q_se
362
+ show_se <- length(q_se) > 0
363
+ if(show_se){ show_se <- q_se > 0 }
364
+ if(!show_se){ render_text <- paste("Estimated Q Value:", sprintf("%.3f", q_point)) }
365
+ if(show_se){ render_text <- paste("Estimated Q Value:", sprintf("%.3f ± %.3f", q_point, 1.96 * q_se)) }
366
+ sprintf("%s (Runtime: %.3f s)",
367
+ render_text,
368
+ selectedResult()$runtime_seconds)
369
  })
370
 
371
+ # Show which set of parameters (label) is currently selected
372
+ output$selection_summary <- renderText({
373
+ input$previousResults
374
+ })
375
  }
376
 
377
+ # Run the app
 
 
378
  shinyApp(ui, server)
379
+