|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
options(error = NULL) |
|
|
|
|
|
|
|
|
|
|
|
|
|
options(error = NULL) |
|
suppressPackageStartupMessages({ |
|
library(shiny) |
|
library(ggplot2) |
|
library(tidyr) |
|
library(scales) |
|
}) |
|
|
|
|
|
|
|
|
|
library( DescriptiveRepresentationCalculator ) |
|
|
|
|
|
|
|
|
|
ui <- fluidPage( |
|
|
|
tags$div( |
|
style = "margin-top: 20px;", |
|
tags$h2("Descriptive Representation Viewer"), |
|
tags$p( |
|
tags$a(href = "https://globalleadershipproject.net/", |
|
tags$span("🔗 GlobalLeadershipProject.net")) |
|
) |
|
), |
|
|
|
sidebarLayout( |
|
sidebarPanel( |
|
numericInput("bodyN", "Size of the political body (N):", |
|
value = 50, min = 1, step = 1), |
|
|
|
sliderInput("ngroups", "Number of population groups (K):", |
|
min = 2, max = 5, value = 3, step = 1), |
|
|
|
uiOutput("popShareInputs"), |
|
|
|
checkboxInput("hasBody", "I have the body’s member counts", FALSE), |
|
|
|
conditionalPanel( |
|
"input.hasBody == true", |
|
uiOutput("bodyCountInputs") |
|
), |
|
|
|
actionButton("go", "Compute", class = "btn-primary") |
|
), |
|
|
|
mainPanel( |
|
fluidRow( |
|
column(4, verbatimTextOutput("expBox")), |
|
column(4, verbatimTextOutput("obsBox")), |
|
column(4, verbatimTextOutput("relBox")) |
|
), |
|
hr(), |
|
plotOutput("sharePlot", height = "400px"), |
|
hr(), |
|
helpText( |
|
"Indices are based on the Rose Index (a = –0.5, b = 1). ", |
|
"Expected values follow Gerring, Jerzak & Öncel (2024) ", |
|
tags$a("[PDF]", |
|
href = "https://www.cambridge.org/core/services/aop-cambridge-core/" |
|
|> paste0("content/view/7EAEA1CA4C553AB9D76054D1FA9C0840/", |
|
"S0003055423000680a.pdf/the-composition-of-", |
|
"descriptive-representation.pdf"), |
|
target = "_blank") |
|
) |
|
) |
|
) |
|
) |
|
|
|
|
|
|
|
|
|
server <- function(input, output, session) { |
|
|
|
|
|
output$popShareInputs <- renderUI({ |
|
n <- input$ngroups |
|
if (n < 2) return() |
|
tagList( |
|
lapply(seq_len(n - 1), function(i) { |
|
numericInput( |
|
inputId = paste0("share", i), |
|
label = paste("Share of group", LETTERS[i]), |
|
value = round(1 / n, 3), |
|
min = 0, max = 1, step = 0.001 |
|
) |
|
}), |
|
tags$p( |
|
sprintf("The share of group %s is automatically set to 1 − (sum of the others).", |
|
LETTERS[n]), |
|
style = "font-style: italic;" |
|
) |
|
) |
|
}) |
|
|
|
|
|
output$bodyCountInputs <- renderUI({ |
|
n <- input$ngroups |
|
lapply(seq_len(n), function(i){ |
|
numericInput( |
|
inputId = paste0("count", i), |
|
label = paste("Members of group", LETTERS[i], "in body"), |
|
value = 0, min = 0, step = 1 |
|
) |
|
}) |
|
}) |
|
|
|
|
|
popShares <- eventReactive(input$go, { |
|
n <- input$ngroups |
|
shares_first <- sapply(seq_len(n - 1), |
|
function(i) input[[paste0("share", i)]]) |
|
if (anyNA(shares_first) || any(shares_first < 0)) { |
|
showNotification("All shares must be non‑negative numbers.", type = "error") |
|
return(NULL) |
|
} |
|
remainder <- 1 - sum(shares_first) |
|
if (remainder < -1e-6) { |
|
showNotification("The entered shares exceed 1. Reduce them so that K‑1 shares " |
|
|> paste("sum to at most 1."), type = "error") |
|
return(NULL) |
|
} |
|
shares <- c(shares_first, max(remainder, 0)) |
|
setNames(shares, LETTERS[seq_len(n)]) |
|
}) |
|
|
|
|
|
bodyCounts <- reactive({ |
|
req(input$hasBody) |
|
n <- input$ngroups |
|
counts <- sapply(seq_len(n), function(i) input[[paste0("count", i)]]) |
|
if (anyNA(counts) || any(counts < 0)) { |
|
showNotification("Body counts must be non‑negative integers.", type = "error") |
|
return(NULL) |
|
} |
|
setNames(counts, LETTERS[seq_len(n)]) |
|
}) |
|
|
|
|
|
observeEvent(input$go, { |
|
validate(need(!is.null(popShares()), "Fix population shares first.")) |
|
|
|
|
|
expVal <- ExpectedRepresentation(popShares(), input$bodyN) |
|
output$expBox <- renderText(sprintf("Expected representation:\n%.3f", expVal)) |
|
|
|
|
|
if (input$hasBody) { |
|
validate(need(!is.null(bodyCounts()), "Enter valid body counts.")) |
|
counts <- bodyCounts() |
|
bodyTotal <- sum(counts) |
|
if (bodyTotal == 0) { |
|
showNotification("Total body member count cannot be zero.", type = "error") |
|
output$obsBox <- renderText("Observed representation:\n—") |
|
output$relBox <- renderText("Relative representation:\n—") |
|
return() |
|
} |
|
if (bodyTotal != input$bodyN) { |
|
showNotification( |
|
sprintf("Sum of counts (%d) differs from N (%d). We use the counts.", |
|
bodyTotal, input$bodyN), |
|
type = "warning", duration = 7 |
|
) |
|
} |
|
bodyShares <- counts / bodyTotal |
|
obsVal <- ObservedRepresentation(NULL, popShares(), |
|
BodyShares = bodyShares) |
|
relVal <- RelativeRepresentation(obsVal, expVal) |
|
output$obsBox <- renderText(sprintf("Observed representation:\n%.3f", obsVal)) |
|
output$relBox <- renderText(sprintf("Relative representation:\n%.3f", relVal)) |
|
} else { |
|
output$obsBox <- renderText("Observed representation:\n—") |
|
output$relBox <- renderText("Relative representation:\n—") |
|
} |
|
}, ignoreNULL = TRUE) |
|
|
|
|
|
output$sharePlot <- renderPlot({ |
|
req(popShares()) |
|
|
|
|
|
if (input$hasBody && !is.null(bodyCounts())) { |
|
counts <- bodyCounts() |
|
bodyShares <- counts / sum(counts) |
|
} else { |
|
bodyShares <- rep(NA_real_, length(popShares())) |
|
} |
|
|
|
df <- data.frame( |
|
Group = factor(names(popShares()), levels = names(popShares())), |
|
Population = as.numeric(popShares()), |
|
Body = as.numeric(bodyShares) |
|
) |
|
|
|
df_long <- pivot_longer(df, -Group, names_to = "Type", values_to = "Share") |
|
|
|
ggplot(df_long, aes(Group, Share, fill = Type)) + |
|
geom_col(position = position_dodge(width = 0.6), |
|
width = 0.55, na.rm = TRUE) + |
|
scale_y_continuous(labels = percent_format(accuracy = 1)) + |
|
scale_fill_manual(values = c(Population = "grey60", Body = "steelblue")) + |
|
labs(x = NULL, y = "Share", fill = NULL) + |
|
theme_minimal(base_size = 14) + |
|
theme(legend.position = "bottom") |
|
}) |
|
} |
|
|
|
|
|
|
|
|
|
shinyApp(ui, server) |
|
} |