### Setup ## Knit environment & parameters library(knitr) # options for this document GLOBALS <- params knitr::opts_chunk$set("message" = GLOBALS$debug) knitr::opts_chunk$set("warning" = GLOBALS$debug) knitr::opts_chunk$set("tidy" = FALSE) # already tidyed using stylr knitr::opts_chunk$set(autodep=TRUE) ## Libraries library("tidyverse") library("readxl") library("haven") library("magrittr") library("gt") library("googledrive") library("httr") library("fs") library("shiny") library("shinyjs") library("DT") #library("ggplotify") library("patchwork") #library(manipulateWidget) #library("sf") library("ggspatial") library("leaflet") library("plotly") library("bslib") library("thematic") ## Directories setup GLOBALS$plan_dir <- fs::path(GLOBALS$working_dir,"plans") GLOBALS$checkpoint <- fs::path(GLOBALS$working_dir,"checkpoint",ext="RData") if (params$clean_data) { unlink(GLOBALS$working_dir, recursive=TRUE) dir.create(GLOBALS$working_dir) dir.create(GLOBALS$plan_dir) } thematic_shiny()
```{css css-setup, echo=FALSE} @import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville&display=swap'); div.main-container { max-width: 1600px !important; } body { font-family: 'Libre Baskerville', serif; } img { display:block; float:none; margin-left:auto; margin-right:auto; width:80%; text-align:center; } .caption { display:block; float:none; margin-left:auto; margin-right:auto; width:80%; text-align:center; } aside { float: right; color: blue; }
/ aside mod from tufte.css but remapped to aside element /
aside { float: right; clear: right; margin-right: -60%; width: 50%; margin-top: 0.3rem; margin-bottom: 0; font-size: 1.1rem; line-height: 1.3; vertical-align: baseline; position: relative; }
/fix for DT scrolling and header alignment/ .dataTables_wrapper{ overflow-x: auto; clear:both; }
```r ## Dblient routines #TODO: Replace with library load when dbclient library is pushed to CRAN try(silent=TRUE, {source("../R/dbclient.R")}) try(silent=TRUE, {source("./R/dbclient.R")}) try(silent=TRUE, {source("dbclient.R")}) .onLoad()
### Refresh plan set refresh_checkpoint <- function(checkpoint=GLOBALS$checkpoint) { # Update id list tmpsubfile <- fs::path(params$working_dir,"submissions",ext="xlsx") googledrive::drive_deauth() googledrive::drive_download( googledrive::as_id(params$submission_gdrive_uri), path = tmpsubfile, overwrite = TRUE ) tmpxl <- readxl::read_excel(tmpsubfile) contestPlans.tb <- tmpxl %>% select(GLOBALS$submission_url_field, GLOBALS$submission_desc_field) names(contestPlans.tb) <- c("url","description") contestPlans.tb %<>% mutate( id = str_replace(url, ".*projects/(.*)$","\\1")) contestPlans.tb %<>% group_by(id) %>% slice_tail(n=1) %>% ungroup() # deduplicate repeat submission # Retrieve new plan files tmpmeta <- db2meta(contestPlans.tb$id,targetdir=GLOBALS$plan_dir) contestPlans.tb %<>% inner_join(tmpmeta,by=c("id"="plan_id")) # this drops plans that weren't retreiveable contestPlans.sflist <- db2sf(contestPlans.tb$id,targetdir=GLOBALS$plan_dir) # Update plan scores contestScores.df <- map_dfr(contestPlans.sflist, function(x) as.data.frame(x) %>% ungroup() %>% mutate(properties.contiguity = properties.contiguity=="contiguous") %>% filter(id>0) %>% summarize(across(starts_with("properties"), ~ mean(.x, na.rm = TRUE), .names = "{.col}_mn")) ) names(contestScores.df) <- str_replace(names(contestScores.df),"properties.","") contestPlans.tb %<>% bind_cols(contestScores.df) contestPlans.tb %<>% bind_cols(tibble(map=contestPlans.sflist)) if (params$apikey!="" & options()$dbclient.pscore.apikey =="") { options("dbclient.pscore.apikey"=apikey) } # add unassigned score -- detect incomplete districts unassigned.tb <- contestPlans.tb %>% rowwise() %>% transmute(head= map %>% as.data.frame() %>% slice_head(n=1)) %>% unpack(head) unassigned_scores.tb <- unassigned.tb %>% rowwise() %>% transmute( unassigned_geography=(length(geometry[[1]])>0), unassigned_population= sum(c_across(starts_with("properties.demographics")) > 0 ,na.rm=TRUE)>0 ) contestPlans.tb %<>% bind_cols(unassigned_scores.tb) # compute maj-min district scores -- these are missing in score results majmin.tb <- contestPlans.tb %>% select("id","map") %>% rename(planid=id) %>% unnest(map) %>% select(planid,id,starts_with("properties.demographics")) %>% filter(id>0) %>% rowwise() %>% mutate(percent_white = properties.demographics.white / properties.demographics.population, is_maj_min_district = percent_white <.5) %>% group_by(planid) %>% summarize(majMin_n=sum(is_maj_min_district),numdists=n()) %>% ungroup() %>% rename(id=planid) contestPlans.tb %<>% left_join(majmin.tb) tmpsplits.tib<-map_dfr(contestPlans.tb$id, function(x) { tmpblocks.tb <- db2df(x,targetdir = GLOBALS$plan_dir)[[1]] tmpblocks.tb %>% mutate(county=str_sub(BLOCKID,3,5)) %>% group_by(county) %>% summarize(splits=length(unique(DISTRICT))-1) %>% ungroup() %>% summarise(county_split_count = sum(splits>0), county_split_sum=sum(splits)) }) contestPlans.tb %<>% bind_cols(tmpsplits.tib) contestPlans.tb %<>% relocate(plan_name,url,unassigned_geography,unassigned_population,numdists,contiguity_mn,majMin_n,county_split_count,county_split_count) # Add Planscores if (options()$dbclient.pscore.apikey !="") { tmpplanscore.df <- generate_planscore_df(contestPlans.tb$id, targetdir=GLOBALS$plan_dir) contestPlans.tb %<>% left_join(tmpplanscore.df,by=c("id"="id")) contestPlans.tb %<>% relocate(plan_name,url,unassigned_geography,unassigned_population,numdists,contiguity_mn,majMin_n,county_split_count,county_split_count,`Efficiency Gap`,`Partisan Bias`) } # Cleanup & checkpoint save(contestPlans.tb, file=checkpoint) } if (GLOBALS$refresh_data) refresh_checkpoint()
load(GLOBALS$checkpoint) # convenience columns, sort order (affects menus) contestPlans.tb %<>% mutate(psViewURI = str_replace( planscoreURI, ".*/(.*)/index.json","https://planscore.campaignlegal.org/plan.html?\\1"), psView=paste("<a href='",psViewURI,"' target='_ps'>",psViewURI,"</a>",sep=""), dbView=paste("<a href='",url,"' target='_db'>",url,"</a>",sep="")) %>% arrange(plan_name) activeids <- contestPlans.tb %>% select(`id`) %>% pull
scores.tb <- contestPlans.tb %>% select(`id`, `plan_name`, `unassigned_geography`, `unassigned_population`, `numdists`, `plan_region`, `compactness_mn`, `contiguity_mn`, `county_split_sum`, `Partisan Bias`, `Efficiency Gap`,`majMin_n`) scoreVars <-setdiff( names(which(sapply(scores.tb,is.numeric))), c("numdists")) scoreRegions <- unique(scores.tb$plan_region) shinyApp( enableBookmarking = "url", options=list( display.mode="showcase", # NOTE: ignored in markdown environment height=1200 ), ui = function(request) { planlist_start<- contestPlans.tb$id ; names(planlist_start)<-contestPlans.tb$plan_name fluidPage( useShinyjs(), titlePanel("Compare Scores"), sidebarLayout( sidebarPanel( selectInput("crit_1", "First Criteria", scoreVars), selectInput("crit_2", "Second Criteria", tail(scoreVars,-1)), # TODO: dynamically remove first criteria selectInput("region", "regions", scoreRegions), checkboxInput("filterLegal", "Legal Plans Only", value = TRUE), # br(), # bookmarkButton() # TODO: Debug bookmarking in notebooks see: https://github.com/rstudio/shiny/pull/1209#issuecomment-227207713 ), mainPanel( tabsetPanel(type = "tabs", tabPanel("Score Tradeoffs ", plotOutput("Tradeoffs",brush="plan_brush")# , #verbatimTextOutput("info") ), tabPanel("Score Distributions", plotlyOutput("Distributions")) ), ) ), titlePanel("Plan Details"), sidebarLayout( sidebarPanel( selectInput("selected_plan", "Primary plan", planlist_start), actionButton('btn_d','Open in DistrictBuilder'), actionButton('btn_p','Open in Plan Score'), withTags({ br() hr() }), conditionalPanel( condition = "input.tabs == 'comparison'", selectInput("comparison_plan", "Comparison plan", tail(planlist_start,-1)), ), ), mainPanel( tabsetPanel(type = "tabs", id="tabs", tabPanel(value="single", "Plan Map", leafletOutput("map")#, textOutput("SelectedIDs") ), tabPanel(value="single", "Plan Overview", tableOutput("overview")), tabPanel(value="single", "Plan Districts", DTOutput("details")), tabPanel(value="comparison", "Map Comparison", plotOutput("compareMap"), tableOutput("compareTable")) ) ) ), ) }, server = function(input, output, session) { # update criteria pulldown 2 observe({ x <- input$crit_1 cur_c2 <- input$crit_2 y <- setdiff(scoreVars,x) # if current selection is invalidated, change it if (! cur_c2 %in% y) { cur_c2 <- head(y,1) } # Can also set the label and select items updateSelectInput(session, "crit_2", choices = y, selected = cur_c2 ) }) # Bookmarking observe({ # Trigger this observer every time an input changes reactiveValuesToList(input) session$doBookmark() }) onBookmarked(function(url) { updateQueryString(url) }) # update current table based on selected regions filtered.tb <- reactive({ if (input$filterLegal) { out <- scores.tb %>% filter(unassigned_geography==FALSE, unassigned_geography == FALSE, plan_region==input$region) } else { out <- scores.tb %>% filter(plan_region==input$region) } activeidsl <- out %>% select(`id`) %>% pull assign("activeids", activeidsl, envir = .GlobalEnv) out }) # Experimental: monitor active selections activeidsR <- reactive(activeids) output$Tradeoffs <- renderPlot({ #TODO: Note the approach to workaround lazy evaluation and brush_point failure # research a better way plot.tb <- filtered.tb() plot.tb$crit_1 <- plot.tb[[input$crit_1]] plot.tb$crit_2 <- plot.tb[[input$crit_2]] p1 <- plot.tb %>% #ggplot(aes(x=.data[[input$crit_1]], y= .data[[input$crit_2]], label=plan_name)) + ggplot(aes(x= crit_1, y= crit_2, label=plan_name)) + geom_point() + geom_smooth(aes(alpha=.01)) + geom_text(aes(alpha=.01)) + facet_wrap(nrow=1,facets=vars(numdists), labeller="label_both")+ coord_flip() + guides(alpha="none") + labs(size="",alpha="",x=input$crit_1,y=input$crit_2) curplot<<-p1 p1 }) # TODO: Plotly selection event handling # output$Selection <- renderText({ # pldata <- event_data("plotly_selected", source = "trade") # if (is.null(pldata)) return(NULL) # paste(out,names(pldata)) # }) output$SelectedIDs <- renderText({ paste("filtered.tb", dim(filtered.tb())[1], "activeidsR", length(activeidsR()) ) }) output$Distributions <- renderPlotly({ p1 <- filtered.tb() %>% ggplot(aes(y=.data[[input$crit_1]])) + # NOTE: .data workaround for tidy evaluation in shiny context geom_histogram() + facet_wrap(nrow=1,facets=vars(numdists), labeller="label_both")+ coord_flip() + guides(alpha="none") + labs(x="",alpha="") p2 <- filtered.tb() %>% ggplot(aes(y=.data[[input$crit_2]])) + geom_histogram() + facet_wrap(nrow=1,facets=vars(numdists), labeller="label_both")+ coord_flip() + guides(alpha="none") + labs(x="",alpha="") # TODO: abstract patchwork/subplot # TODO: density plot -- geom_density chokes ggplotify # p1/p2 subplot(ggplotly(p1,source="dist2"),ggplotly(p2,source="dist2"),nrows=2) }) ### Second Panel # update planlist pulldown 1 planlist <- reactive ({ sid = selected_ids() x <- filtered.tb() %>% select(id,plan_name) y <- x$id; names(y)<-x$plan_name if(length(sid)>0) { y<- y[which(y %in% sid)] } y }) observe({ y <- planlist() updateSelectInput(session, "selected_plan", choices = y, selected = head(y, 1) ) }) # update planlist pulldown 2 observe({ x <- input$selected_plan y <- planlist() y <- y[which(y!=x)] #NOTE: setdiff removes names from list... avoid updateSelectInput(session, "comparison_plan", choices = y, selected = head(y, 1) ) }) selected_ids <- reactive ({ pb <- input$plan_brush if (is.null(pb)) return(list()) plot.tb <- filtered.tb() plot.tb["crit_1"] <- plot.tb[[input$crit_1]] plot.tb["crit_2"] <- plot.tb[[input$crit_2]] bpi <- brushedPoints(df=plot.tb,pb,xvar = input$crit1_1,yvar=input$crit_2) plot.tb %<>% select(id,crit_1,crit_2) #TODO: Debug brushed points -- this is a workaround -- wouldn't work for multiple panels sid<- plot.tb %>% rowwise() %>% filter( crit_1 <= pb$ymax, crit_1 >= pb$ymin, crit_2 <= pb$xmax, crit_2 >= pb$xmin ) %>% pull(id) sid }) # precompute for efficiency across tabs curPlan <- reactive(contestPlans.tb %>% filter(id==input$selected_plan)) curMap <- reactive(curPlan()[[1,"map"]][[1]]) compPlan <- reactive({contestPlans.tb %>% filter(id==input$comparison_plan)}) compMap <- reactive(compPlan()[[1,"map"]][[1]]) #TODO: error presentation is a hack -- should figure out how to change output type... curPS <- reactive({ out <- curPlan()[[1,"districtScoreTable"]][[1]] if (is.null(out)) {out = tibble(warning="SCORES MISSING (Plan may be incomplete or corrupt)")} out }) # button handler observe({ curPlan() onclick("btn_p", runjs(paste(sep="", "window.open('", curPlan()["psViewURI"], "')"))) onclick("btn_d", runjs(paste(sep="", "window.open('", curPlan()["url"], "')"))) }) # # ggplotly based - not supported # output$map <- renderPlotly({ # p<- curMap() %>% ggplot() + geom_sf() # p %>% ggplotly() # NOTE - geom_sf not supported in ggplotly # }) # output$info <- renderText({ pb <- input$plan_brush xy_range_str <- function(e) { if(is.null(e)) return("NULL\n") paste0("xmin=", round(e$xmin, 1), " xmax=", round(e$xmax, 1), " ymin=", round(e$ymin, 1), " ymax=", round(e$ymax, 1)) } paste0( "brush: ", xy_range_str(pb), "selected: ", selected_ids(), collapse="," ) }) output$map <- renderLeaflet({ p<- curMap() %>% leaflet() %>% addTiles() %>% addPolygons(color = "green") p }) output$overview <- renderTable({ curPlan() %>% select(plan_name,plan_creator,description) }) output$details <- renderDT({ curPS() }) output$compareMap <- ({ renderPlot({ p1<- curMap() %>% ggplot(aes(color= curPlan()$plan_name )) + geom_sf()+ geom_sf(data=compMap(),aes(color=compPlan()$plan_name,alpha=0)) + guides(alpha="none") + labs(color="Plan") p1 }) }) output$compareTable <- ({ presTB <- reactive({ compTb <- bind_rows(curPlan(),compPlan()) %>% select(where(is.numeric)) pdiff<- apply(compTb,2,function(x)(x[1]-x[2])) tmpo <- bind_rows(compTb,pdiff) pname <- c(curPlan()$plan_name,compPlan()$plan_name,"Difference") tmpo <- bind_cols(planname=pname,tmpo) tmpo }) renderTable({ presTB() %>% rownames_to_column() %>% mutate(across(everything(),as.character)) %>% pivot_longer(-rowname) %>% pivot_wider(names_from=rowname, values_from=value) }) }) } )
### Explore set of plans pscore.tb <- reactive(contestPlans.tb %>% select(-map, -districtScoreTable)) # Score table # TODO: Auto size height to avoid vertical scrolling renderDT({ pscore.tb() %>% select(-file_src, -planscoreURI, -description) %>% relocate(plan_name, unassigned_geography, unassigned_population, numdists, contiguity_mn, majMin_n, county_split_count, county_split_sum, dbView, psView) %>% datatable(class = "cell-border stripe; hover", caption = "", editable=FALSE, escape=FALSE, selection=list(mode="single",selected=1), filter="top", rownames=FALSE, extensions = list( "ColReorder" = list(TRUE), "Buttons" = NULL ), options = list( dom = 'RrlBftpi', autoWidth=TRUE, #bAutoWidth=FALSE, pageLength=5, scrollX=FALSE, lengthMenu = list(c(5, 50, -1), c('5', '20', 'All')), buttons = list( 'copy', 'print', list( extend = 'collection', buttons = c('csv', 'excel', 'pdf'), text = 'Download' ), I('colvis') ) ) ) %>% formatStyle( 'unassigned_geography', backgroundColor = styleEqual(c(TRUE), c('lightpink') ) ) %>% formatStyle( 'unassigned_population', backgroundColor = styleEqual(c(TRUE), c('lightpink') ) ) %>% formatStyle( 'contiguity_mn', backgroundColor = styleEqual(c(1), c('lightgreen'),default='orange' ) ) }) #TODO: Set specified plan with row tab-selection -- complication is that table ID is autogenerated ## # rmarkdown::render_delayed({ # renderPrint({ # tn <- names(input) # ts<- tn[which(str_detect(tn,"rows_selected"))] # paste(ts,input) # }) # })
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.