| { |
| |
| |
| |
| |
| |
| |
| 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) |
| } |