1+ {
2+ # ====================================================================
3+ # app.R – interactive demo for the DescriptiveRepresentation calculator
4+ # Upload this single file to a Shiny‑typed Hugging Face Space.
5+ # ====================================================================
6+
7+ # install.packages( "~/Documents/DescriptiveRepresentationCalculator-software/DescriptiveRepresentationCalculator", repos = NULL, type = "source",force = F)
8+ options(error = NULL )
9+
10+ # ====================================================================
11+ # app.R – interactive demo for the Descriptive Representation Viewer
12+ # Upload this single file to a Shiny‑typed Hugging Face Space.
13+ # ====================================================================
14+
15+ options(error = NULL )
16+ suppressPackageStartupMessages({
17+ library(shiny )
18+ library(ggplot2 )
19+ library(tidyr )
20+ library(scales )
21+ })
22+
23+ # --------------------------------------------------------------------
24+ # 1. Calculator functions (verbatim from Gerring, Jerzak & Öncel 2024)
25+ # --------------------------------------------------------------------
26+ library( DescriptiveRepresentationCalculator )
27+
28+ # --------------------------------------------------------------------
29+ # 2. Shiny user interface
30+ # --------------------------------------------------------------------
31+ ui <- fluidPage(
32+ # ---- Custom title block ---------------------------------------------------
33+ tags $ div(
34+ style = " margin-top: 20px;" ,
35+ tags $ h2(" Descriptive Representation Viewer" ),
36+ tags $ p(
37+ tags $ a(href = " https://globalleadershipproject.net/" ,
38+ tags $ span(" 🔗 GlobalLeadershipProject.net" ))
39+ )
40+ ),
41+
42+ sidebarLayout(
43+ sidebarPanel(
44+ numericInput(" bodyN" , " Size of the political body (N):" ,
45+ value = 50 , min = 1 , step = 1 ),
46+
47+ sliderInput(" ngroups" , " Number of population groups (K):" ,
48+ min = 2 , max = 5 , value = 3 , step = 1 ),
49+
50+ uiOutput(" popShareInputs" ), # K – 1 numeric inputs + note
51+
52+ checkboxInput(" hasBody" , " I have the body’s member counts" , FALSE ),
53+
54+ conditionalPanel(
55+ " input.hasBody == true" ,
56+ uiOutput(" bodyCountInputs" )
57+ ),
58+
59+ actionButton(" go" , " Compute" , class = " btn-primary" )
60+ ),
61+
62+ mainPanel(
63+ fluidRow(
64+ column(4 , verbatimTextOutput(" expBox" )),
65+ column(4 , verbatimTextOutput(" obsBox" )),
66+ column(4 , verbatimTextOutput(" relBox" ))
67+ ),
68+ hr(),
69+ plotOutput(" sharePlot" , height = " 400px" ),
70+ hr(),
71+ helpText(
72+ " Indices are based on the Rose Index (a = –0.5, b = 1). " ,
73+ " Expected values follow Gerring, Jerzak & Öncel (2024) " ,
74+ tags $ a(" [PDF]" ,
75+ href = " https://www.cambridge.org/core/services/aop-cambridge-core/"
76+ | > paste0(" content/view/7EAEA1CA4C553AB9D76054D1FA9C0840/" ,
77+ " S0003055423000680a.pdf/the-composition-of-" ,
78+ " descriptive-representation.pdf" ),
79+ target = " _blank" )
80+ )
81+ )
82+ )
83+ )
84+
85+ # --------------------------------------------------------------------
86+ # 3. Server logic
87+ # --------------------------------------------------------------------
88+ server <- function (input , output , session ) {
89+
90+ # ----- Dynamic numericInputs for the first K‑1 shares ----------------------
91+ output $ popShareInputs <- renderUI({
92+ n <- input $ ngroups
93+ if (n < 2 ) return ()
94+ tagList(
95+ lapply(seq_len(n - 1 ), function (i ) {
96+ numericInput(
97+ inputId = paste0(" share" , i ),
98+ label = paste(" Share of group" , LETTERS [i ]),
99+ value = round(1 / n , 3 ),
100+ min = 0 , max = 1 , step = 0.001
101+ )
102+ }),
103+ tags $ p(
104+ sprintf(" The share of group %s is automatically set to 1 − (sum of the others)." ,
105+ LETTERS [n ]),
106+ style = " font-style: italic;"
107+ )
108+ )
109+ })
110+
111+ # ----- Dynamic numericInputs for group‑wise body counts --------------------
112+ output $ bodyCountInputs <- renderUI({
113+ n <- input $ ngroups
114+ lapply(seq_len(n ), function (i ){
115+ numericInput(
116+ inputId = paste0(" count" , i ),
117+ label = paste(" Members of group" , LETTERS [i ], " in body" ),
118+ value = 0 , min = 0 , step = 1
119+ )
120+ })
121+ })
122+
123+ # ----- Helper producing the full K‑vector of population shares -------------
124+ popShares <- eventReactive(input $ go , {
125+ n <- input $ ngroups
126+ shares_first <- sapply(seq_len(n - 1 ),
127+ function (i ) input [[paste0(" share" , i )]])
128+ if (anyNA(shares_first ) || any(shares_first < 0 )) {
129+ showNotification(" All shares must be non‑negative numbers." , type = " error" )
130+ return (NULL )
131+ }
132+ remainder <- 1 - sum(shares_first )
133+ if (remainder < - 1e-6 ) {
134+ showNotification(" The entered shares exceed 1. Reduce them so that K‑1 shares "
135+ | > paste(" sum to at most 1." ), type = " error" )
136+ return (NULL )
137+ }
138+ shares <- c(shares_first , max(remainder , 0 ))
139+ setNames(shares , LETTERS [seq_len(n )])
140+ })
141+
142+ # ----- Helper reading body counts into a named vector ----------------------
143+ bodyCounts <- reactive({
144+ req(input $ hasBody )
145+ n <- input $ ngroups
146+ counts <- sapply(seq_len(n ), function (i ) input [[paste0(" count" , i )]])
147+ if (anyNA(counts ) || any(counts < 0 )) {
148+ showNotification(" Body counts must be non‑negative integers." , type = " error" )
149+ return (NULL )
150+ }
151+ setNames(counts , LETTERS [seq_len(n )])
152+ })
153+
154+ # ----- Main computation on “Compute” --------------------------------------
155+ observeEvent(input $ go , {
156+ validate(need(! is.null(popShares()), " Fix population shares first." ))
157+
158+ # Expected representation
159+ expVal <- ExpectedRepresentation(popShares(), input $ bodyN )
160+ output $ expBox <- renderText(sprintf(" Expected representation:\n %.3f" , expVal ))
161+
162+ # Observed / relative representation (if counts supplied)
163+ if (input $ hasBody ) {
164+ validate(need(! is.null(bodyCounts()), " Enter valid body counts." ))
165+ counts <- bodyCounts()
166+ bodyTotal <- sum(counts )
167+ if (bodyTotal == 0 ) {
168+ showNotification(" Total body member count cannot be zero." , type = " error" )
169+ output $ obsBox <- renderText(" Observed representation:\n —" )
170+ output $ relBox <- renderText(" Relative representation:\n —" )
171+ return ()
172+ }
173+ if (bodyTotal != input $ bodyN ) {
174+ showNotification(
175+ sprintf(" Sum of counts (%d) differs from N (%d). We use the counts." ,
176+ bodyTotal , input $ bodyN ),
177+ type = " warning" , duration = 7
178+ )
179+ }
180+ bodyShares <- counts / bodyTotal
181+ obsVal <- ObservedRepresentation(NULL , popShares(),
182+ BodyShares = bodyShares )
183+ relVal <- RelativeRepresentation(obsVal , expVal )
184+ output $ obsBox <- renderText(sprintf(" Observed representation:\n %.3f" , obsVal ))
185+ output $ relBox <- renderText(sprintf(" Relative representation:\n %.3f" , relVal ))
186+ } else {
187+ output $ obsBox <- renderText(" Observed representation:\n —" )
188+ output $ relBox <- renderText(" Relative representation:\n —" )
189+ }
190+ }, ignoreNULL = TRUE )
191+
192+ # ----- Bar plot ------------------------------------------------------------
193+ output $ sharePlot <- renderPlot({
194+ req(popShares())
195+
196+ # Body shares (only if counts provided)
197+ if (input $ hasBody && ! is.null(bodyCounts())) {
198+ counts <- bodyCounts()
199+ bodyShares <- counts / sum(counts )
200+ } else {
201+ bodyShares <- rep(NA_real_ , length(popShares()))
202+ }
203+
204+ df <- data.frame (
205+ Group = factor (names(popShares()), levels = names(popShares())),
206+ Population = as.numeric(popShares()),
207+ Body = as.numeric(bodyShares )
208+ )
209+
210+ df_long <- pivot_longer(df , - Group , names_to = " Type" , values_to = " Share" )
211+
212+ ggplot(df_long , aes(Group , Share , fill = Type )) +
213+ geom_col(position = position_dodge(width = 0.6 ),
214+ width = 0.55 , na.rm = TRUE ) +
215+ scale_y_continuous(labels = percent_format(accuracy = 1 )) +
216+ scale_fill_manual(values = c(Population = " grey60" , Body = " steelblue" )) +
217+ labs(x = NULL , y = " Share" , fill = NULL ) +
218+ theme_minimal(base_size = 14 ) +
219+ theme(legend.position = " bottom" )
220+ })
221+ }
222+
223+ # --------------------------------------------------------------------
224+ # 4. Launch the app
225+ # --------------------------------------------------------------------
226+ shinyApp(ui , server )
227+ }
0 commit comments