### Main Shiny server function
resd_default <- resd_dsl
shinyServer(function(input, output, session) {
values <- reactiveValues(selected=TRUE, ## Current data subset selection
anglemax=0)
## Things to do when changing the base dataset
observe({
input$select
})
## Button handler: set random effects SE to zero
observe({
if (input$zeroresd > 0 || input$egger){
updateNumericInput(session, "resd", value=0)
}
})
## Button handler: reset random effects SE to data value
observe({
if (input$resetresd > 0) {
dat <- getdata_static()
updateNumericInput(session, "resd", value=resd_default(dat))
}
})
## Return the full dataset currently chosen
getdata_base <- reactive({
globals$newdata <<- NULL # note global assignment <<-
dat <- switch(input$select,
"1"=magnesium,
"2"=catheter,
"3"=aspirin,
"4"=symmetric,
"5"={ ## user-uploaded dataset
output$filechoiceui <- renderUI({ list(
checkboxInput('header', 'Header', TRUE),
radioButtons('sep', 'Separator',
c(Comma=',', Semicolon=';', Tab='\t'),
selected=',', inline=TRUE
),
radioButtons('quote', 'Quote',
c(None='', 'Double Quote'='"', 'Single Quote'="'"),
selected='"', inline=TRUE)
) })
inFile <- input$data
validate( need(inFile != "", "Please select a dataset") ) ## needed to avoid function returning before data chosen
if (is.null(inFile)) userdata <- NULL
else userdata <- read.csv(inFile$datapath, header = input$header,
sep = input$sep, quote = input$quote)
## Silence if error occurs - but maybe that's OK - can switch to another data set.
validate(need(ncol(userdata)>=3, "Need at least 3 columns in the data"))
names(userdata)[1:3] <- c("name","est","se")
validate(need(is.numeric(userdata$est), "Second column (estimate) should be numeric"))
validate(need(is.numeric(userdata$se), "Second column (SE) should be numeric"))
userdata
},
"6" = {
if (exists("userdata", envir = MetaAnalyser:::.dat_env))
get("userdata", envir = MetaAnalyser:::.dat_env) else NULL
}
)
dat
})
## Add user-added studies to base data, append meta-analysis summary statistics
getdata_static <- reactive({
dat <- getdata_base()
get_newstudy <- reactive({
if (input$submitnewdata == 0) return(NULL)
isolate({
res <- data.frame(name=input$newname, est=input$newest, se=input$newse)
if (is.null(input$newest) || is.null(input$newse) ||
is.na(res$est) || is.na(res$se)) res <- NULL
res
})
})
globals$newdata <<- rbind(globals$newdata, get_newstudy()) # note global assignment <<-
if (!is.null(globals$newdata)){
dat <- rbind(dat, globals$newdata)
updateNumericInput(session, "newname", value="")
updateNumericInput(session, "newest", value="")
updateNumericInput(session, "newse", value="")
}
n <- nrow(dat)
dat$key <- 1:n
dat$wtfe <- (1/dat$se^2)
dat$pwtfe <- dat$wtfe / sum(1/dat$se^2)
resd <- if (input$egger) 0 else resd_default(dat)
updateNumericInput(session, "resd", value=resd)
sm <- metasumm(dat, resd, input$egger)
dat$est <- sm$est
dat$pwtre <- sm$pwtre
attr(dat, "pool") <- sm$pool
globals$pool <<- sm$pool
attr(dat, "poolse") <- sm$poolse
attr(dat, "poolci") <- sm$poolci
values$selected <- rep(TRUE, n)
# proxy = dataTableProxy('datatables')
# selectRows(proxy, numeric()) # this doesn't work at resetting selection when changing data
attr(dat, "plotkeys") <- rep(c("points","strings","topbar","baseline_vertical","baseline_horiz","current_vertical","current_horiz","pivot","baseline_point","current_point"),
c(n, n, 2, 2, 2, 2, 2, 3, 1, 1))
dat
})
## Update the study-selection indicator in the data after clicking,
## and recompute meta-analysis summary statistics
## TODO nicer dplyr like syntax for defining data
getdata_dynamic <- reactive({
dat <- getdata_static()
n <- nrow(dat)
dat$selected <- factor(rep("No", n), levels=c("Yes","No")) # ggvis prefers grouping variable to be factor
dat$selected[values$selected] <- "Yes"
summ <- metasumm(dat[values$selected,,drop=FALSE], input$resd, input$egger)
dat$est[values$selected] <- summ$est
dat$wtfe <- 1/(dat$se^2)
dat$wtre <- 1/(dat$se^2 + input$resd^2)
dat$holesize <- 100*(dat$wtfe - dat$wtre)/sum(dat$wtfe)
dat$percwtfe <- 100*dat$wtfe/sum(dat$wtfe)
dat$percwtre <- 100*dat$wtre/sum(dat$wtre)
dat$yfe <- switch(input$ytype, perc=dat$percwtfe, var=dat$wtfe)
dat$yre <- switch(input$ytype, perc=dat$percwtre, var=dat$wtre)
aspect_ratio <- default_options()$width / default_options()$height
if (is.null(input$yrange)) {top <- 100; bottom <- 0}
else {
top <- input$yrange[2]
bottom <- input$yrange[1] - (top - input$yrange[1])/40
}
bottomdyn <- bottom # + 0.01*(top-bottom)
xex <- 0.1
dxr <- diff(range(dat$est))
xrange <- range(dat$est) + c(-1, 1)*xex*dxr
yrange <- c(0, top)
## TODO choose default point size more intelligently
sqsize <- dxr / 100 * input$pointsize
dat$dx <- sqsize*sqrt(dat$wtfe)
dat$dy <- dat$dx/diff(xrange)*diff(yrange)*aspect_ratio
dat$dxre <- sqsize*sqrt(dat$wtfe - dat$wtre)
dat$dyre <- dat$dxre/diff(xrange)*diff(yrange)*aspect_ratio
pool.static <- attr(dat, "pool")
pool.dyn <- summ$pool
is_biased <- if (!isTRUE(all.equal(input$resd, resd_default(dat))) ||
!all(values$selected)) "Yes" else "No"
### Tilt the top bar proportional to bias, up to certain maximum angle
bias <- (globals$pool - pool.dyn) / (dxr/2)
degrees_tilt <- sign(bias * values$anglemax) * min(values$anglemax, abs(bias * values$anglemax))
globals$pool <<- pool.dyn
angle <- degrees_tilt * pi/180
hl <- pool.dyn - min(dat$est)
hr <- max(dat$est) - pool.dyn
xltilt <- pool.dyn - hl*cos(angle)
xrtilt <- pool.dyn + hr*cos(angle)
yr <- top; xr <- dxr # adjust for differently-scaled x/y axes
ybtilt <- top - hl*sin(angle)*yr/xr
yttilt <- top + hr*sin(angle)*yr/xr
dat$stringtop <- top + (dat$est - pool.dyn)*sin(angle)*yr/xr
dat$estplot <- dat$est + (pool.dyn - dat$est)*(1 - cos(angle)) # plotted x position, different from actual estimate if bar is tilted
vtop <- top + (pool.static - pool.dyn)*sin(angle)*yr/xr
pivot.dx <- dxr / 25
pivot.dy <- 0.04*top
pivot.top <- 0.99*top
## Append plot coordinates to pass to ggvis, based on selected data
auxdata <- list(
## List of data frames to make into reactive data sources
topbar = data.frame(x = c(xltilt, xrtilt), y = c(ybtilt, yttilt), key=2*n+1:2),
baseline_vertical = data.frame(x = rep(pool.static, 2), y = c(bottom, vtop-pivot.dy), key=2*n+3:4, is_biased=is_biased),
baseline_horiz = data.frame(x = attr(dat, "poolci"),
y = rep(bottom, 2), key=2*n+5:6, is_biased=is_biased),
current_vertical = data.frame(x = rep(pool.dyn, 2), y = c(bottomdyn, top-pivot.dy), key=2*n+7:8),
current_horiz= data.frame(x = summ$poolci,
y = rep(bottomdyn, 2), key=2*n+9:10),
current_pivot = data.frame(x=c(pool.dyn, pool.dyn-pivot.dx/2, pool.dyn+pivot.dx/2),
y=c(pivot.top, pivot.top-pivot.dy, pivot.top-pivot.dy), key=2*n+11:13),
baseline_pivot = data.frame(x=c(pool.static, pool.static-pivot.dx/2, pool.static+pivot.dx/2),
y=c(pivot.top, pivot.top-pivot.dy, pivot.top-pivot.dy), key=2*n+11:13, is_biased=is_biased),
baseline_point = data.frame(x=pool.static, y=bottom, key=2*n+14),
current_point = data.frame(x=pool.dyn, y=bottom, key=2*n+15)
)
for (i in seq(along=auxdata))
auxdata[[i]]$show_scales <- if (input$show_scales) "Yes" else "No"
attr(dat, "summ") <- summ
attr(dat, "aux") <- c(list(xrange=xrange), auxdata)
dat
})
## Reactive data sources to enable animation in ggvis
getdata_string <- function(i){
reactive({
dat <- getdata_dynamic()
n <- nrow(dat)
dat <- dat[i,,drop=FALSE]
ret <- data.frame(x=rep(dat$estplot, 2), y=c(dat$stringtop, dat$yre+dat$dy),
selected=rep(dat$selected, 2), key=n + 1:2)
if (!input$show_scales) ret$selected <- "No"
ret
})
}
getdata_allstrings <- reactive({
dat <- getdata_dynamic()
res <- data.frame(x=rep(dat$estplot, each=2),
y=as.numeric(rbind(dat$stringtop, dat$yre+dat$dx)),
selected=rep(dat$selected, each=2),
key=nrow(dat) + 1:(2*nrow(dat)), # TODO rejig keys
group=rep(1:nrow(dat), each=2)) %>% group_by(group)
res
})
getdata_topbar <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$topbar
ret
})
getdata_baseline_vertical <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$baseline_vertical
ret$show_scales <- if (input$show_scales) "Yes" else "No"
if (!input$show_scales) ret$is_biased <- "Noscales"
ret
})
getdata_baseline_horiz <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$baseline_horiz
ret
})
getdata_current_vertical <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$current_vertical
ret
})
getdata_current_horiz <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$current_horiz
ret
})
getdata_baseline_pivot <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$baseline_pivot
ret$show_scales <- if (input$show_scales) "Yes" else "No"
if (!input$show_scales) ret$is_biased <- "Noscales"
ret
})
getdata_current_pivot <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$current_pivot
ret
})
getdata_baseline_point <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$baseline_point
ret
})
getdata_current_point <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$current_point
ret
})
## fix the axis ranges so they don't wiggle when dataset is perturbed
getdata_xdomain <- reactive({
dat <- getdata_dynamic()
ret <- attr(dat, "aux")$xrange
ret
})
getdata_ydomain <- reactive({
input$yrange
})
getdata_holescale <- reactive({
dat <- getdata_dynamic()
c(0, max(dat$yfe))
})
## Change the limits of the y-axis range selector according to
## whether percentage weights or absolute weights selected
observe({
if (input$ytype == "perc"){
ymin <- 0; ymax <- 100
lab <- "y-axis range (percentage weight)"
} else {
dat <- getdata_static()
yex <- 0.1
ymax <- signif(max(dat$wtfe)*(1 + yex), 3)
ymin <- 0
lab <- "y-axis range (inverse variance)"
}
output$yslider <- renderUI({
sliderInput("yrange", label = h4(lab),
min = ymin, max = ymax, value = c(ymin, ymax))
})
})
## Print the data table, with excluded studies greyed out, and other summary info
observe({
dat <- getdata_dynamic()
sel <- (dat$selected=="Yes")
summ <- attr(dat, "summ")
Isq <- summ$Isq
digits <- 3 # let user choose?
if (sum(sel) > 0) {
dat <- dat[,c("name","est","se","wtre","wtfe","percwtre","percwtfe")]
dat$name <- as.character(dat$name)
for (i in c("est","se","wtre","wtfe","percwtre","percwtfe"))
dat[,i] <- formatC(dat[,i], digits=digits, format="f")
names(dat) <- c("Name", "Estimate", "SE",
"Inverse variance", "Inverse variance (fixed effects)",
"Weight (%)", "Weight (%, fixed effects)")
## Table responds to clicks on plot - but this requires
## devel version of DT >=0.1.16 on github not CRAN.
## Plot doesn't respond to clicks on table
## below line breaks both things
## todo MRE with mtcars
## FIXME if exclude in one dataset, should reset selection when changing data
## difficult to reproduce this
## isolate(values$selected <- !(1:nrow(dat) %in% input$datatables_rows_selected))
output$datatables = DT::renderDataTable(dat, server = (nrow(dat)>100))
proxy = dataTableProxy('datatables')
selectRows(proxy, which(!sel))
output$Isq = renderUI({
div(paste("I-squared =",round(100*Isq,1),"%"))
})
output$pooled <- renderUI({
div(paste("Pooled estimate = ", round(summ$pool, digits=digits), " (",
round(summ$poolci[1], digits), ", ",
round(summ$poolci[2], digits), ")", sep=""))
})
}
output$invisible <- renderUI({
conditionalPanel(condition="false",
## used for animated tilt/untilt
checkboxInput("tilt", label=NULL, value = FALSE)
)
})
})
## Combine reactive value and hidden input to achieve animated tilt/untilt - unclear why need both.
observe({
if (isTRUE(input$tilt)) {
invalidateLater(1000, session)
}
updateCheckboxInput(session, "tilt", value=FALSE)
values$anglemax <- 0
})
hover_fn <- function(df){
alldata <- isolate(getdata_static())
keys <- attr(alldata, "plotkeys")
if (df$key %in% which(keys=="points")){
dat <- alldata[alldata$key == df$key,]
sprintf("%s, estimate %s<br>Click to include / exclude", dat$name, round(as.numeric(dat$est), 3))
}
#else if (df$key %in% which(keys=="strings"))
#else if (df$key %in% which(keys=="topbar"))
else if (df$key %in% which(keys=="baseline_vertical"))
sprintf("\"Baseline\" pooled estimate")
else if (df$key %in% which(keys=="baseline_horiz"))
sprintf("95%% confidence interval for \"baseline\" pooled estimate")
else if (df$key %in% which(keys=="current_vertical") && input$show_scales)
sprintf("\"Current\" pooled estimate")
else if (df$key %in% which(keys=="current_horiz"))
sprintf("95%% confidence interval for \"current\" pooled estimate")
else if (df$key %in% which(keys=="topbar") && input$show_scales)
sprintf("Top bar spans the range of study results")
}
click_fn <- function(data, location, session) {
alldata <- isolate(getdata_static())
keys <- attr(alldata, "plotkeys")
if (data$key %in% which(keys=="points")){
isolate(values$selected[data$key] <- !values$selected[data$key])
isolate(values$anglemax <- 20)
updateNumericInput(session, "resd", value = resd_default(alldata[values$selected,]))
updateCheckboxInput(session, "tilt", value=TRUE)
}
}
## Draw the actual plot.
p <- ggvis(x=~estplot, y=~yre, key:=~key, data=getdata_dynamic) %>%
scale_numeric("x", domain = getdata_xdomain) %>%
scale_numeric("y", domain = getdata_ydomain)
bias_fill <- prop("fill", ~ is_biased, scale="scale_biaspoolcolor")
bias_stroke <- prop("stroke", ~ is_biased, scale="scale_biaspoolcolor")
show_scales_stroke <- prop("stroke", ~ show_scales, scale="scale_showscalescolor")
show_scales_fill <- prop("fill", ~ show_scales, scale="scale_showscalescolor")
show_scales_strokewidth <- prop("strokeWidth", ~ show_scales, scale="scale_pooledstroke")
p <- p %>%
## add original estimate and CI in gray
layer_paths(x = ~x, y = ~y, data=getdata_baseline_vertical, strokeWidth:=3, bias_stroke) %>%
layer_paths(x = ~x, y = ~y, data=getdata_baseline_horiz, show_scales_strokewidth, bias_stroke) %>%
## current estimate in black
layer_paths(x = ~x, y = ~y, data=getdata_current_vertical, strokeWidth:=3, show_scales_stroke) %>%
layer_paths(x = ~x, y = ~y, data=getdata_current_horiz, show_scales_strokewidth) %>%
## top bar
layer_paths(x = ~x, y = ~y, data=getdata_topbar, strokeWidth:=6, show_scales_stroke) %>%
layer_paths(x = ~x, y = ~y, data=getdata_baseline_pivot, bias_fill, bias_stroke) %>%
layer_paths(x = ~x, y = ~y, data=getdata_current_pivot, show_scales_fill, show_scales_stroke) %>%
layer_points(x = ~x, y = ~y, data=getdata_baseline_point, fill:="grey") %>%
layer_points(x = ~x, y = ~y, data=getdata_current_point, fill:="black") %>%
scale_nominal("scale_pooledstroke", domain = c("Yes", "No"), range = c(10, 3)) %>%
scale_nominal("scale_showscalescolor", domain = c("Yes", "No"), range = c("black", "white")) %>%
scale_nominal("scale_stringcolor", domain = c("Yes", "No"), range = c("lightgray", "white")) %>%
scale_nominal("scale_biaspoolcolor", domain = c("Yes", "No", "Noscales"), range = c("lightgray", "black", "white"))
## Draw strings
## Hard-code the maximum number of strings that can be drawn.
## unclear how else to let the upper limit of the loop react to a
## change in the number of rows in the base dataset, while retaining animations.
## geom_segment would make this cleaner, but not implemented yet.
## feed dplyr grouped data to layer_paths? https://github.com/rstudio/ggvis/issues/375
max_rows <- 50
for (i in seq_len(max_rows)){
ai <- getdata_string(i)
p <- layer_paths(p, x = ~x, y = ~y, data=ai,
prop("stroke", ~ selected, scale="scale_stringcolor"), opacity := 0.5)
}
## if put key in layer_points but not string layer, then hover doesn't know key
## if put key in both, then draws only one
## need key for hover and click
# p <- layer_paths(p, x = ~x, y = ~y, key:=~key, data=getdata_allstrings, prop("stroke", ~ selected, scale="scale_stringcolor"), opacity := 0.5)
a_props <- axis_props(title=list(fontSize=15), labels=list(fontSize=14))
p <- p %>%
add_tooltip(hover_fn) %>%
handle_click(click_fn) %>%
add_axis("x", title="Estimate", grid=FALSE, properties=a_props) %>%
add_axis("y", title="Study weight", grid=FALSE, properties=a_props) %>%
## Implement the plot symbol as four adjacent rectangles
## top rectangle
layer_rects(x=~est-dx, y=~yre+dyre, x2=~est+dx, y2=~yre+dy, fill=~selected, strokeWidth:=0, opacity:=0.5) %>%
## left rectangle
layer_rects(x=~est-dx, y=~yre-dyre, x2=~est-dxre, y2=~yre+dyre, fill=~selected, strokeWidth:=0, opacity:=0.5) %>%
## bottom rectangle
layer_rects(x=~est-dx, y=~yre-dy, x2=~est+dx, y2=~yre-dyre, fill=~selected, strokeWidth:=0, opacity:=0.5) %>%
## right rectangle
layer_rects(x=~est+dxre, y=~yre-dyre, x2=~est+dx, y2=~yre+dyre, fill=~selected, strokeWidth:=0, opacity:=0.5) %>%
## and an invisible one in the middle that can be hovered / clicked
layer_rects(x=~est-dxre, y=~yre-dyre, x2=~est+dxre, y2=~yre+dyre, strokeWidth:=0, opacity:=0) %>%
hide_legend("size") %>%
scale_nominal("fill", range = c("blue", "lightgray")) %>%
hide_legend("fill") %>%
scale_nominal("stroke", range = c("black", "lightgray")) %>%
hide_legend("stroke") %>%
set_options(duration = 500) ## animation transition duration
observe({ # don't draw plot until RE SD has been computed from data
bind_shiny(p, "ggvis")
})
})
## TODO
## Selection doesn't get reset when changing the base dataset
## This bug was there before moving to datatables
## datatables: make selection color darker
## Egger reincludes all points
## get strings group_by thing working. some bug or misunderstanding with key
## after update of shiny, Ok for static plot but strings go away on reclick
## Posted to ggvis forum (but not github issue yet)
## could try to debug manually
## button horiz alignment is a fudge and breaks on some screens
## Y axis title can't be changed dynamically
## https://github.com/rstudio/ggvis/issues/203
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.