Update app.R
Browse files
app.R
CHANGED
@@ -1,58 +1,227 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
|
7 |
-
#
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
choices = unique(df$Species), selected = unique(df$Species)
|
18 |
-
),
|
19 |
-
hr(), # Add a horizontal rule
|
20 |
-
checkboxInput("by_species", "Show species", TRUE),
|
21 |
-
checkboxInput("show_margins", "Show marginal plots", TRUE),
|
22 |
-
checkboxInput("smooth", "Add smoother"),
|
23 |
-
),
|
24 |
-
plotOutput("scatter")
|
25 |
-
)
|
26 |
-
|
27 |
-
server <- function(input, output, session) {
|
28 |
-
subsetted <- reactive({
|
29 |
-
req(input$species)
|
30 |
-
df |> filter(Species %in% input$species)
|
31 |
})
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
)
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
}
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
}
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
}
|