inst/shiny_admixtools/app.R

library(admixtools)
library(igraph)
library(htmlwidgets)
library(shiny)
library(shinythemes)
library(plotly)
library(tidyverse)
library(magrittr)
library(RColorBrewer)
library(shinyjs)
library(shinyBS)
library(shinyFiles)
library(shinyalert)
library(shinyWidgets)
library(shinydashboard)


#options(shiny.sanitize.errors = FALSE)

edgemul = 1000
dtstyle = 'font-size:70%'
pl = 15
do = list(pageLength = pl, lengthMenu = list(c(pl, -1), c(pl, 'All')),
          buttons = c('copy', 'csv', 'excel'), scrollX = TRUE, dom = 'Blfrtip')
actionButton = function(...) actionBttn(..., style = 'unite', size = 'sm')
#cols = gg_color_hue(5, 97, 15)
cols = c("#FFF1F0", "#F7F8E4", "#E2FDF2", "#E8F9FF", "#FFF1FF")
mutfuns = c(`SPR leaves`='spr_leaves', `SPR all`='spr_all', `Swap leaves`='swap_leaves', `Move admixture edge`='move_admixedge_once', `Flip admix`='flipadmix_random', `Change root`='place_root_random', `Mutate n`='mutate_n')

box = function (..., title = NULL, footer = NULL, status = NULL, solidHeader = FALSE,
                background = NULL, width = 6, height = NULL, collapsible = FALSE,
                collapsed = FALSE)
{
  boxClass <- "box"
  if (solidHeader || !is.null(background)) {
    boxClass <- paste(boxClass, "box-solid")
  }
  if (!is.null(status)) {
    validateStatus(status)
    boxClass <- paste0(boxClass, " box-", status)
  }
  if (collapsible && collapsed) {
    boxClass <- paste(boxClass, "collapsed-box")
  }
  # if (!is.null(background)) {
  #   validateColor(background)
  #   boxClass <- paste0(boxClass, " bg-", background)
  # }
  style <- NULL
  if (!is.null(height)) {
    style <- paste0("background:", background, "; height: ", validateCssUnit(height))
  }
  titleTag <- NULL
  if (!is.null(title)) {
    titleTag <- h3(class = "box-title", title)
  }
  collapseTag <- NULL
  if (collapsible) {
    buttonStatus <- status %OR% "default"
    collapseIcon <- if (collapsed)
      "plus"
    else "minus"
    collapseTag <- div(class = "box-tools pull-right", tags$button(class = paste0("btn btn-box-tool"),
                                                                   `data-widget` = "collapse", shiny::icon(collapseIcon)))
  }
  headerTag <- NULL
  if (!is.null(titleTag) || !is.null(collapseTag)) {
    headerTag <- div(class = "box-header", titleTag, collapseTag)
  }
  div(class = if (!is.null(width))
    paste0("col-sm-", width), div(class = boxClass, style = if (!is.null(style))
      style, headerTag, div(class = "box-body", style = paste('background:', background), ...), if (!is.null(footer))
        div(class = "box-footer", footer)))
}


tt = c('data' = 'Load data here',
       'dir' = 'Directory where precursors for f-statistics will be stored or have been stored.',
       'popfilediv' = 'Select a PLINK or EIGENSTRAT sample metadata textfile.',
       'graphfilediv' = 'A graph in ADMIXTOOLS format, or as a two column edge list.',
       'genofile1' = 'Select a Packedancestrymap .geno file to extract data from. This step should only be necessary once. After that, the extracted data can be used for any further analyses.',
       'adjpopdiv' = 'Assign individuals to populations. Population names can be changed, but should match population names in the graph file, if one is provided. Data will be read from disk and f2-statistics will be computed when switching to another sidebar item.',
       'bookmarkdiv' = 'Click this to save the current session. This will write files to disk and generate a URL which can be used to restore the state of the progam.',
       'qpgraph_fit' = 'Click this to see qpgraph fit statistics',
       'qpgraph_similar_minus1' = 'Fit all graphs that result from removing one admixture edge from the current graph',
       'qpgraph_similar_minusplus' = 'Fit all graphs that result from removing one admixture edge from the current graph and adding back one admixture edge',
       'qpgraph_similar_plus1' = 'Fit all graphs that result from adding one admixture edge to the current graph',
       'qpgraph_similar_decomposed' = 'Fit all trees that result from removing one edge from each admixture node',
       'qpgraph_similar_treeneighbors' = 'Fit all trees that result from removing one edge from each admixture node and then reattaching each edge to each other edge',
       'qpgraph_similar_flipadmix' = 'Fit all graphs that result from flipping the direction of each admixture edge (unless this would introduce a cycle)',
       'qpgraph_update' = 'Set the current graph to the displayed graph',
       'qpgraph_revert' = 'Display the original graph',
       'addleaf' = 'Choose a population which should be added to the current graph',
       'qpgraph_add' = 'Fit all graphs that result from adding this population to each edge',
       'optim_run' = 'Run optimization. The current graph is modified in a number of different ways, and each modified graph is evaluated. Can be run iteratively across several generations.',
       'optim_ngen' = 'Number of generations. In each generation the best fitting graphs are selected for the next generation.',
       'optim_sec' = 'Minimum number of seconds to run',
       'mutfuns' = 'SPR leaves: Subree prune and regraft leaf edges\n
                    SPR all: Subree prune and regraft any edges\\n
                    Swap leaves: Swap two random leaves\\\n
                    Move admixture edge: Shift one admixture edge\\\\n
                    Flip admix: Flip the direction of one admixture edge\\\\\n
                    Change root: Place the root of the tree on a random edge\\\\\\n
                    Mutate n: Combine two or more of the other mutation functions',
       'econstraints_update_div' = 'Set limits to minimum and maximum edge lengths',
       'aconstraints_update_div' = 'Set limits to admixture weights',
       'qpgraphbin' = 'Choose the qpGraph executable on your computer',
       'qpadmbin' = 'Choose the qpAdm executable on your computer',
       'qpgraph_genofile' = 'Choose the .geno or .bed file for qpGraph to use',
       'qpadm_genofile' = 'Choose the .geno or .bed file for qpAdm to use',
       'usef3precomp' = 'Differences between the original and R version of qpGraph can be due to the estimation of f-statistics, or due to the fitting step. Click this to use the f-statistics generated in the original qpGraph to use in qpGraph in R.',
       'init_resample' = 'Re-evaluate graph with different random intializations of starting weights. This is always done by default, but only the parameters for the starting weights that minimize the score are shown. Here, the table will show all estimated graph parameters (score, edge lengths, admixture weights) for all sets of random starting parameters.',
       'ind_resample' = 'Re-evaluate the graph leaving out each sample at a time. Samples are only left out if they are not the only sample in their respective population. This can take long, as data has to be read and f2 computed separately for each sample that is being left out.',
       'snp_resample' = 'Re-evaluate the graph leaving out SNP blocks. With jackknife, each SNP block is left out at a time. With bootstrap, SNP blocks are resampled with replacement to match the original number of SNP blocks (usually around 700). The number of random resamplings can be specified.',
       'clear_edge' = 'Clear the current selection',
       'delete_edge' = 'Delete the selected edge or node. Has to be an admixture edge or leaf node.',
       'add_edge' = 'Add an admixture edge by connecting two selected non-admixture edges. Fails if it introduces cycles or attempts to connect admixture edges.',
       'randgraph' = 'Generate a random admixture graph. Uses the current populations, a specified number of admixture nodes, and a specified outgroup.',
       'lsqmode' = 'Least squares mode. By default, the computation of the graph likelihood score uses the inverse of the f3-statistic covariance matrix. When fitting many populations, this inverse can be unstable. Least squares mode sets the offdiagonal elements of the f3-statistic covariance matrix to zero, which makes the matrix inversion stable, but can introduce bias.',
       'lambdascale' = 'Scale the estimated f2-statistics by a constant factor.',
       'seed' = 'Set a random seed to always pick the same initial weights and to make the results reproducible.',
       'multiprocess' = 'Enable parallel evaluation of graphs in multiple R processes. Faster, but may not work on all operating systems. See "A Future for R: A Comprehensive Overview" for more details.',
       'collapse_edges' = 'Collapse all nodes which are separated by less then a certain threshold of estimated drift.',
       'f2corr' = 'Untick this to compute f-statistics without bias correction factor.',
       'addpop' = 'Add a population',
       'removepop' = 'Remove a population',
       'showseledge' = 'Selected edges or nodes are shown here',
       'downloadgraph' = 'Download the fit of the current graph as a .tsv file.',
       'numstart' = 'How often optimization should be initialized with random weights',
       'qpgraph_diag' = 'Factor to add to diagonal elements of f3-statistic covariance matrix before inversion. Increase this value if optimization doesn\'t converge',
       'qpadm_fit' = 'Return to the qpadm fit statistics',
       'qpadm_diag' = 'Factor to add to diagonal elements of f4-statistic covariance matrix before inversion. Increase this value if optimization doesn\'t converge',
       'qpadm_constrained' = 'Click this to constrain the admixture weights to be between 0 and 1',
       'data_load' = 'Only a placeholder at the moment. Data is loaded when switching from the "Data" tab to another tab',
       'b' = '',
       'c' = '',
       'f2' = 'These are the estimated f2 statistics',
       'f3' = 'These are the estimated and fitted f3 statistics',
       'opt' = 'This table contains information about the admixture weight optimization step for each iteration, such as the randomly drawn initial weights, as well as the final score ("value", the quantity that is being minimized) and convergence parameters from the R "optim" function. Run "Resample / Initial weights" to see how other estimated graph parameters depend on the initial weights.')


format_table = function(dat) {

  if(is.null(dat)) return(tibble())
  print('ft1')
  print(names(dat))
  popcols = which(str_sub(names(dat), 1, 3) == 'pop')
  names = unique(c(as.matrix(dat[,popcols])))
  print('ft2')
  nam = shortest_unique_prefixes(names, 3)
  pcols = which(names(dat) %in% c('p', 'p.value'))
  print('ft3')
  numcols = setdiff(which(sapply(dat, class) == 'numeric'), pcols)
  print('ft4')

  if(length(popcols) > 0) dat %<>% mutate_at(popcols, ~recode(., !!!purrr::set_names(nam, names)))
  if('graph' %in% names(dat)) dat %<>% select(-graph)
  if('edges' %in% names(dat)) dat %<>% select(-edges)
  print('ft5')
  out = dat %>%
    mutate_at(numcols, ~round(., max(2, 2 - ceiling(log10(max(abs(na.omit(.)))))))) %>%
    mutate_at(pcols, format.pval) %>%
    rename_at(vars(pcols), ~'p.value')
  out
}

aa = '.shiny-notification { height: 100px; width: 400px; position: fixed; top: 20%; left: calc(50% - 200px);;}'

ui = function(request) {
  tagList(useShinyjs(),
          useShinyalert(),
          chooseSliderSkin('Modern', color = 'black'),
          bsTooltip('tooltips', 'Click this to turn tooltips on or off. Needs to be reactivated after switching tabs.'),
          dashboardPage(
            dashboardHeader(title = 'ADMIXTOOLS 2', titleWidth = 300,
                            tags$li(class = 'dropdown', actionLink('tooltips', label = '', icon = icon('question'))),
                            tags$li(class = 'dropdown', id = 'bookmarkdiv', actionLink('._bookmark_', label = '', icon = icon('save')))),
            dashboardSidebar(tags$head(tags$style(HTML(aa))), width = 300,
                             sidebarMenu(id = 'navbar',
                                         menuItem('Data', tabName = 'data', expandedName = 'data', id = 'datax', icon = icon('database'), startExpanded = TRUE,
                                                  menuItem('Extract settings', tabName = 'extract_settings',
                                                           checkboxInput('fix_populations', 'Fix populations', TRUE),
                                                           numericInput('max_miss', 'max missing', value = 0, step = 0.01, min = 0, max = 1),
                                                           splitLayout(
                                                             numericInput('min_maf', 'min MAF', value = 0, step = 0.01, min = 0, max = 0.5),
                                                             numericInput('max_maf', 'max MAF', value = 0.5, step = 0.01, min = 0, max = 0.5)),
                                                           radioButtons('trans_extract', 'Mutations', choices = c('both', 'only transitions', 'only transversions')),
                                                           fileInput('keepsnps', NULL, buttonLabel = 'SNP list'),
                                                           numericInput('maxmem', 'max RAM in GB', value = 15, step = 1, min = 0)
                                                  )),
                                         menuItem('f2', tabName = 'f2', expandedName = 'f2', icon = icon('dice-two'),
                                                  menuItem('Options', tabName = 'f2_options')),
                                         menuItem('f3', tabName = 'f3', expandedName = 'f3', icon = icon('dice-three'),
                                                  menuItem('Options', tabName = 'f3_options')),
                                         menuItem('f4', tabName = 'f4', expandedName = 'f4', icon = icon('dice-four'),
                                                  menuItem('Options', tabName = 'f4_options')),
                                         menuItem('qpAdm', tabName = 'qpadm', expandedName = 'qpadm', icon = icon('balance-scale'),
                                                  actionLink('qpadm_fit', 'Fit'),
                                                  menuItem('Compare', href = 'qpadm_comparison', tabName = 'qpadm_comparison',
                                                           splitLayout(
                                                             shinyFilesButton('qpadmbin', 'qpAdm bin', 'Select qpAdm bin', FALSE),
                                                             shinyFilesButton('qpadm_genofile', 'Geno file', 'Select Packedancestrymap geno file', FALSE)),
                                                           p(HTML('<center>or</center>')),
                                                           fileInput('qpadm_out', NULL, buttonLabel = 'qpAdm output'),
                                                           hr(),
                                                           actionButton('run_qpadm', 'Run')),
                                                  menuItem('Resample', tabName = 'qpadm_resample'),
                                                  menuItem('Options', tabName = 'qpadm_options',
                                                           numericInput('qpadm_diag', 'diag', value = 0.0001, step = 0.0001),
                                                           checkboxInput('qpadm_constrained', 'Constrain weights'),
                                                           checkboxInput('qpadm_results', 'Show results', value = TRUE),
                                                           checkboxInput('qpadm_rotate', 'Rotate left', value = FALSE))),
                                         menuItem('qpGraph', tabName = 'qpgraph', expandedName = 'qpgraph', id = 'qpgraph', icon = icon('project-diagram'),
                                                  checkboxInput('evaluate_graph', 'Evaluate graph', FALSE),
                                                  menuItem('Fit',
                                                             splitLayout(
                                                               shiny::actionButton('qpgraph_update', 'Update graph'),
                                                               shiny::actionButton('qpgraph_revert', 'Revert graph')),
                                                           #actionButton('options_update', 'Update'),
                                                           splitLayout(numericInput('qpgraph_diag', 'diag', value = 0.0001, step = 0.0001),
                                                           numericInput('numstart', '# init', value = 10, min = 1)),
                                                           splitLayout(numericInput('lambdascale', 'lambdascale', value = 1, step = 0.001),
                                                           numericInput('seed', 'Random seed', value = NULL)),
                                                           splitLayout(checkboxInput('lsqmode', 'lsqmode'),
                                                           checkboxInput('return_fstats', 'return_fstats'))),
                                                  menuItem('Modify', expandedName = 'qpgraph_modify', id = 'qpgraph_modify', tabName = 'qpgraph_modify',
                                                           hr(),
                                                           p('Selected'),
                                                           div(verbatimTextOutput('showseledge', placeholder = TRUE), tags$head(tags$style('#showseledge{max-height: 300px; background: white}'))),
                                                           splitLayout(
                                                             shiny::actionButton('clear_edge', 'Clear'),
                                                             shiny::actionButton('delete_edge', 'Delete'),
                                                             shiny::actionButton('add_edge', 'Add')),
                                                           uiOutput('qpgraph_add'),
                                                           hr(),
                                                           p('Randomize graph'),
                                                           uiOutput('nadmix'),
                                                           #splitLayout(checkboxInput('fixoutgroup', 'Fix outgroup', value = TRUE),
                                                           actionButton('randgraph', 'Randomize'),
                                                           hr()),
                                                  menuItem('Optimize', tabName = 'qpgraph_optim',
                                                           actionButton('optim_run', 'Run'),
                                                           numericInput('optim_ngen', '# generations', 10, 1, 1000),
                                                           numericInput('optim_sec', 'min seconds', 10, 1, 1000),
                                                           checkboxInput('optim_initrand', 'Start randomly'),
                                                           checkboxGroupInput('mutfuns', 'Graph modifications', choices = mutfuns, selected = mutfuns)),
                                                  menuItem('Constrain', tabName = 'qpgraph_constraints',
                                                           menuItem('Drift', tabName = 'qpgraph_econstraints',
                                                                    uiOutput('econstraints')),
                                                           menuItem('Admixture', tabName = 'qpgraph_aconstraints',
                                                                    uiOutput('aconstraints'))),
                                                  menuItem('Compare', tabName = 'qpgraph_comparison',
                                                           splitLayout(
                                                             shinyFilesButton('qpgraphbin', 'qpGraph bin', 'Select qpGraph bin', FALSE),
                                                             shinyFilesButton('qpgraph_genofile', 'Geno file', 'Select Packedancestrymap geno file', FALSE)),
                                                           checkboxInput('usef3precomp', 'Use qpGraph fstats in R'),
                                                           p(HTML('<center>or</center>')),
                                                           fileInput('qpgraph_out', NULL, buttonLabel = 'qpGraph output'),
                                                           hr(),
                                                           actionButton('run_qpgraph', 'Run')),
                                                  menuItem('Resample', tabName = 'qpgraph_resample',
                                                           menuItem('Initial weights', tabName = 'qpgraph_resample_init',
                                                                    sliderInput('numstart2', '# init', 1, 100, 10),
                                                                    actionButton('init_resample', 'Run')),
                                                           menuItem('Individuals', tabName = 'qpgraph_resample_individuals',
                                                                    actionButton('ind_resample', 'Run')),
                                                           menuItem('SNPs', tabName = 'qpgraph_resample_snps',
                                                                    checkboxInput('boot', 'Bootstrap'),
                                                                    conditionalPanel('input.boot',
                                                                                     sliderInput('bootnum', '# boot', 0, 1000, 100)),
                                                                    actionButton('snp_resample', 'Run'))),
                                                  menuItem('Load', fileInput('graphfile', NULL, placeholder = '', buttonLabel = 'Graph file')),
                                                  menuItem('Save', tabName = 'qpgraph_download', expandedName = 'qpgraph_download',
                                                           div(radioButtons('downloadgraph_format', 'Graph format',
                                                                            choices = c(`ADMIXTOOLS`='admixtools', `Edge list`='edgelist', `DOT`='dot'),
                                                                            selected = 'admixtools'),
                                                               downloadButton('downloadgraph', 'Save graph'))),
                                                  menuItem('Plot options',
                                                           checkboxGroupInput('plotopt', '',
                                                                              choices = c(`Highlight unidentifiable` = 'highlight_unidentifiable',
                                                                                          `Reorder edges`='reorder_edges',
                                                                                          `Shift edges down`='shift_down',
                                                                                          `Collapse edges`='collapse_edges'),
                                                                              selected = c('shift_down')),
                                                           sliderInput('collapse_threshold', 'Log10 collapse threshold', -6, 2, -3, step = 0.1))),
                                         menuItem('Options', tabName = 'options', expandedName = 'options', id = 'options', icon = icon('cogs'),
                                                  checkboxInput('multiprocess', 'multiprocess', value = FALSE),
                                                  checkboxInput('f2corr', 'f2 bias correction', value = TRUE),
                                                  checkboxInput('afprod', 'allele frequency products', value = FALSE)))),
            dashboardBody(uiOutput('dashboardbody'), tags$head(tags$style(HTML('.skin-black .sidebar .shiny-download-link a { color: #444; } .content {background-color: white } .main-header .logo {font-family: "GillSans", "Skia", "Avenir-Medium", "Herculanum", "Hiragino Maru Gothic Pro", "Maru Gothic Pro", "Hiragino", "Comic Sans MS"; font-weight: normal;};')))),
            skin = 'black'
          ))
}


server = function(input, output, session) {

  showLog()
  dp = '~/mnt/o2/projects/admixprograms/indpairs_v42.1/'
  dp = '~/Downloads/testinds/'
  dp = '/Users/robert/Downloads/countdata_eas5/'
  dp = '~/Downloads/countdata_iosifgraph/'
  dp = ''
  #dp = '~/Documents/countdata_afafam/'
  #dp = '~/Documents/countdata_pavel2/'
  global = reactiveValues(countdir = dp,
                          allinds = list.dirs(paste0(dp, '/pairs'),F,F),
                          poplist = list())
  global$graph = NULL
  home = normalizePath('~')
  volumes = getVolumes()
  #volumes = expr(c(home = normalizePath('~'), getVolumes()() %>% purrr::set_names()))

  shinyDirChoose(input, 'dir', roots = volumes)
  shinyFileChoose(input, 'genofile1', roots = volumes)
  shinyFileChoose(input, 'qpgraph_genofile', roots = volumes)
  shinyFileChoose(input, 'qpadm_genofile', roots = volumes)
  shinyFileChoose(input, 'qpgraphbin', roots = volumes)
  shinyFileChoose(input, 'qpadmbin', roots = volumes)

  #global$show_qpadm_rotate = FALSE
  #global$show_qpadm_results = TRUE

  shinyjs::disable('qpgraph_update')
  shinyjs::disable('qpgraph_revert')

  output$dirout = renderText({global$countdir})
  output$dirout = renderText({str_replace(global$countdir, '/Users/robert', '')})

  output$show_popadjust = renderText({as.numeric(show_popadjust())})
  output$show_extract = renderText({as.numeric(show_extract())})
  output$show_indselect = renderText({as.numeric(show_indselect())})
  output$genofile1out = renderText({parseFilePaths(eval(volumes), input$genofile1)$datapath %>% str_replace('\\.geno$', '') %>% str_replace('/Users/robert', '')})



  observeEvent(input$dir, {
    #if (!"path" %in% names(dir())) return()
    #volumes = eval(volumes)
    print('volumes')
    print(volumes)
    print('input$dir')
    print(c(input$dir, class(input$dir)))
    print('xxx')
    print(parseDirPath(volumes, input$dir))
    print(str(parseDirPath(volumes, input$dir)))
    print(as.character(parseDirPath(volumes, input$dir)))
    global$countdir = parseDirPath(volumes, input$dir)
    global$iscountdata = 'indivs' %in% list.dirs(global$countdir,F,F)
    global$isf2data = !'indivs' %in% list.dirs(global$countdir,F,F) &
      'block_lengths_f2.rds' %in% list.files(global$countdir)
    print('global$iscountdata')
    print(global$iscountdata)
    if(global$iscountdata) {
      global$allinds = list.dirs(paste0(global$countdir, '/pairs'),F,F)
    } else if(global$isf2data) {
      global$allinds = list.dirs(global$countdir,F,F)
      global$poplist = global$allinds %>% purrr::set_names(global$allinds)
      print('global$allinds')
      print(global$allinds)
    } else {

    }
  })

  observeEvent(input$textdirinput, {
    nam = ifelse(!is.null(input$textdirinput) && dir.exists(input$textdirinput), 'Load', 'Create')
    updateActionButton(session, "textdir", label = nam)
  })

  observeEvent(input$textdir, {

    justcreated = FALSE
    if(!dir.exists(input$textdirinput)) {
      #shinyalert('Could not find directory!', "Please write the path of a directory into the box. This should be an empty directory if you haven't extracted f2-statistics/counts yet, or otherwise the directory with extracted data.")
      dir.create(input$textdirinput)
      justcreated = TRUE
      #return()
    }
    global$countdir = normalizePath(input$textdirinput)
    global$iscountdata = 'indivs' %in% list.dirs(global$countdir,F,F)
    global$isf2data = !'indivs' %in% list.dirs(global$countdir,F,F) &
      'block_lengths_f2.rds' %in% list.files(global$countdir)
    print('global$iscountdata')
    print(global$iscountdata)
    if(global$iscountdata) {
      global$allinds = list.dirs(paste0(global$countdir, '/pairs'),F,F)
    } else if(global$isf2data) {
      global$allinds = list.dirs(global$countdir,F,F)
      global$poplist = global$allinds %>% purrr::set_names(global$allinds)
      print('global$allinds')
      print(global$allinds)
    } else {
      if(!justcreated) shinyalert("Directory does not seem to have f2 data!")
    }
  })

  observeEvent(input$navbar, {

    print('navbar detected')
    print(input$navbar)

  })

  tolisten = reactive({
    list(input$sidebarItemExpanded, input$qpadm_rotate, input$qpadm_results)
    #!is.null(input$sidebarItemExpanded) && (is.null(global$exp) || input$sidebarItemExpanded != global$exp)
  })

  get_bod = eventReactive(tolisten(), {
    print('expanded!')
    exp = input$sidebarItemExpanded
    print(exp)
    #if(is.null(exp) || !is.null(global$exp) && exp == global$exp) return(global$bod)
    if(is.null(exp)) exp = global$exp
    global$exp = exp

    if(exp == 'options') {

    } else if(exp == 'data') {

      print('navbar == data!')
      global$databod = get_loaddata()
      global$bod = global$databod

    } else {

      nam = map(paste0('pop', seq_len(length(global$poplist))), ~input[[.]])
      if(is.null(nam[[1]])) {
        shinyalert('Global poplist is NULL or has not been processed correctly!', global$poplist)
        return()
      }
      global$poplist = map(nam, ~input[[.]]) %>% purrr::set_names(nam)
      print('switch away from data')
      if(length(unlist(global$poplist)) == 0) {
        shinyalert('No samples selected!', '')
        return()
      }

      if(exp == 'f2') {

        print('switch to f2')

        global$bod = fluidRow(
          column(1, div(radioGroupButtons('fst', choices = c('f2', 'fst')))),
          column(11, tabsetPanel(tabPanel('Pop pairs', plotlyOutput(outputId = 'f2heatmap', height = '800', width='auto')),
                      tabPanel('Ind pairs', plotlyOutput(outputId = 'f2heatmap_indiv', height = '800', width='auto')),
                      tabPanel('Pop table', dto('f2stats')))))

      } else if(exp == 'f3') {

        print('switch to f3')

        global$bod = fluidRow(
          column(12, tabsetPanel(tabPanel('Pop table', dto('f3stats')))))

      } else if(exp == 'f4') {

        print('switch to f4')
        choices = names(global$poplist)
        global$f4_left = div(map(1:4, ~selectizeInput(paste0('f4pops', .),
                                                      paste0('Population ', .),
                                                      choices = choices,
                                                      multiple = TRUE,
                                                      selected = choices[seq(.*(.-1)/2+1, .*(.-1)/2+.)])),
                             checkboxInput('f4_allowdup', 'Allow duplicates'),
                             sliderInput('f4_se', 'Standard errors', min = 0, max = 5, value = 0, step = 0.01))
                             #checkboxInput('f4_se', 'Show standard errors'))
        popsinuse = unique(c(input$f4pops1, input$f4pops2, input$f4pops3, input$f4pops4))

        # update
        map(1:4, ~{
          fp = paste0('f4pops', .)
          sel = input[[fp]]
          updateSelectizeInput(session, fp, selected = sel,
                               choices = union(sel, setdiff(names(global$poplist), popsinuse)))})

        global$bod = fluidRow(
          column(3, global$f4_left),
          column(9, tabsetPanel(tabPanel('Plot', plotlyOutput(outputId = 'f4plot', height = '800', width='auto')),
                                tabPanel('Plot2', plotlyOutput(outputId = 'f4plot2', height = '800', width='auto')),
                                tabPanel('Table', dto('f4stats')))))

      } else if(exp == 'qpadm') {

        print('switch to qpAdm')
        choices = names(global$poplist)
        if(is.null(input$qpadmpops1) || is.null(input$qpadmpops2) || is.null(input$qpadmpops3)) {
          global$qpadmpops1 = choices[2:3]
          global$qpadmpops2 = choices[4:6]
          global$qpadmpops3 = choices[1]
        } else {
          global$qpadmpops1 = input$qpadmpops1
          global$qpadmpops2 = input$qpadmpops2
          global$qpadmpops3 = input$qpadmpops3
        }

        #global$qpadm_leftpanel = qpadm_leftpanel()
        global$bod = fluidRow(
          column(3, qpadm_leftpanel()),
          column(9, qpadm_rightpanel()))

      } else if(exp == 'qpgraph') {

        print('switch to qpGraph')

        if(is.null(global$graph) || !all(get_leafnames(global$graph) %in% names(global$poplist))) {
          numadm = 2
          if(!is.null(global$graph)) {
            shinyalert('Generating random graph because populations don\'t match',
                       setdiff(get_leafnames(global$graph), names(global$poplist)))
            numadm = numadmix(isolate(global$graph))
          }
          op = if(is.null(input$outpop) || input$outpop == '< undefined >') NULL else input$outpop
          global$graph = random_admixturegraph(na.omit(names(global$poplist)), numadm, TRUE, outpop = op)
        }

        #global$qpg_right = qpg_right_fit()
        #global$qpg_right_show = 'fit'
        #global$qpg_right = qpg_right()
        global$bod = fluidRow(
          column(8, plotlyOutput(outputId = 'graphplot', height = '800', width='auto')),
          column(4,     tabsetPanel(tabPanel('Fit', tabsetPanel(tabPanel('f2', dto('f2')),
                                                                tabPanel('f3', dto('f3')),
                                                                tabPanel('f4', dto('f4')),
                                                                tabPanel('opt', dto('opt')))),
                                    tabPanel('Similar', tabsetPanel(tabPanel('-1', dto('minus1')),
                                                                    tabPanel('+1', dto('plus1')),
                                                                    tabPanel('flip', dto('flipadmix')))),
                                    tabPanel('History', dto('history')),
                                    tabPanel('Compare', plotlyOutput('graphcomparison')),
                                    tabPanel('Resample', tabsetPanel(tabPanel('Starting values', dto('initresample')),
                                                                     tabPanel('Individuals', dto('indresample')),
                                                                     tabPanel('SNPs', dto('snpresample')))),
                                    id = 'qpgraph_tabset')))
      } else {
        shinyalert('Error', 'implement tab')
      }
    }
    global$bod
  })


  observeEvent(global$graph, {
    print('observe global$graph')
    updateSelectInput(session, 'addleaf',
                      choices = setdiff(names(global$poplist), get_leafnames(global$graph)))
  })

  get_graph = reactive({
    print('get graph')
    global$graph
  })

  # observeEvent(global$qpg_right, {
  #   print('global$qpg_right change detected!')
  #   global$qpgraphbod = fluidRow(
  #     column(8, plotlyOutput(outputId = 'graphplot', height = '800', width='auto')),
  #     column(4, global$qpg_right))
  # })
  # observeEvent(global$databod, {global$bod = global$databod})
  # observeEvent(global$qpadmbod, {global$bod = global$qpadmbod})
  observeEvent(global$qpgraphbod, {print('global$qpgraphbod change detected!'); global$bod = global$qpgraphbod})
  observeEvent(global$bod, {print('global$bod change detected!'); b = global$bod; global$bod = b})

  observeEvent(input$navbar, {
    print('navbar selected')
    print(input$navbar)
  })

  observeEvent(input$addpop, {
    pops = names(global$poplist)
    i = 0
    repeat({i=i+1; newpop = paste0('pop', i); if(!newpop %in% pops) break})
    global$poplist[newpop] = NA
  })
  observeEvent(input$removepop, {
    global$poplist = head(global$poplist, -1)
  })

  observeEvent(input$popfile, {

    inddat = read_table2(input$popfile$datapath, col_names = FALSE, col_types = cols())
    if(ncol(inddat) == 3) colnames(inddat) = c('ind', 'sex', 'pop')
    else if(ncol(inddat) == 6) colnames(inddat) = c('pop', 'ind', 'p1', 'p2', 'sex', 'pheno')
    else shinyalert('Error', paste0('sample file has ',ncol(inddat),' columns!'))
    global$poplist = inddat %>%
      select(ind, pop) %$% split(ind, factor(pop, levels = unique(pop)))

    if(length(global$allinds) == 0) { # f2data dir
      global$allinds = unlist(global$poplist)
    }

    print('observe')
    imap(names(global$poplist), ~{
      bttn = paste0('group', .y)
      print(bttn)
      observeEvent(input[[bttn]], {
        isolate({
          if(input[[bttn]] == 0) return()
          poplist = global$poplist
          #poplistbak = global$poplistbak
          countdir = global$countdir
          inds = poplist[[.x]]
          #if(is.null(global$poplistbak[[.x]])) {
          isgrouped = length(inds) == 1 && admixtools:::is_group(countdir, inds)
          if(!isgrouped) {
            if(.x %in% global$allinds) {
              shinyalert('Error', 'Group name cannot be identical to an existing sample or group name!')
              return()
            }
            print('add group')
            withProgress(message = paste0('grouping ', length(inds), ' samples...'), {
              group_samples(global$countdir, inds, .x, overwrite = TRUE)
            })
            global$allinds = union(global$allinds, .x)
            #global$poplistbak[[.x]] = inds
            global$poplist[[.x]] = .x
            updateSelectizeInput(session, .x, selected = .x, choices = global$allinds)
            #updateButton(session, bttn, 'ungroup')
          } else {
            print('delete group')
            withProgress(message = paste0('ungrouping ', length(inds), ' samples...'), {
              global$poplist[[.x]] = delete_groups(global$countdir, .x)
            })
            #global$poplist[[.x]] = global$poplistbak[[.x]]
            #global$poplistbak[[.x]] = NULL
            global$allinds = setdiff(global$allinds, .x)
            updateSelectizeInput(session, .x, selected = global$poplist[[.x]], choices = global$allinds)
            #updateButton(session, bttn, 'group')
          }
        })
      })
    })
  })


  observeEvent(input$graphfile, {
    gf = input$graphfile$datapath
    print('read graphfile')
    print(gf)
    colnums = read_lines(gf) %>% str_split('\\s+') %>% map_dbl(length)
    if(length(unique(colnums)) > 1) {
      print('admixtools format')
      global$graph = parse_qpgraph_graphfile(gf) %>% graph_from_edgelist()
    } else {
      print('two column format')
      global$graph = read_table2(gf, col_types = cols()) %>% select(1:2) %>%
        as.matrix %>% graph_from_edgelist()
    }
    print('read graphfile done')
  })

  observeEvent(input$qpgraph_out, {
    global$qpgraphout = parse_qpgraph_output(input$qpgraph_out$datapath)
    #global$qpg_right = plotlyOutput('graphcomparison')
  })

  observeEvent(input$randgraph, {
    print('randg')
    op = if(is.null(input$outpop) || input$outpop == '< undefined >') NULL else input$outpop
    global$graph = random_admixturegraph(get_leafnames(global$graph), input$nadmix, TRUE, outpop = op)
  })

  observeEvent(input$qpgraph_update, {
    print('qpgraph_update')
    global$graph = global$selgraph
    fit = get_fit()
    global$edges = fit$edges
    global$score = fit$score
    global$worst_residual = fit$worst_residual
    add_graph_to_history(global)
    global$tempgraph = FALSE
    shinyjs::disable('qpgraph_update')
    shinyjs::disable('qpgraph_revert')
  })
  observeEvent(input$qpgraph_revert, {
    print('qpgraph_revert')
    global$selgraph = global$graph
    fit = get_fit()
    global$edges = fit$edges
    global$score = fit$score
    global$worst_residual = fit$worst_residual
    add_graph_to_history(global)
    global$tempgraph = FALSE
    shinyjs::disable('qpgraph_update')
    shinyjs::disable('qpgraph_revert')
    print(global$score)
  })

  observeEvent(input$navbar, {
    print('navbar:')
    print(input$navbar)
  })

  observeEvent(input$optim_run, {

    numgen = input$optim_ngen
    stop_sec = input$optim_sec
    diag = input$qpgraph_diag
    lambdascale = as.numeric(input$lambdascale)
    f2blocks = get_f2blocks()
    pops = dimnames(f2blocks)[[1]]
    f2blocks = f2blocks[pops, pops,]
    outpop = get_outpop(global$graph)
    nadmix = numadmix(global$graph)
    g = NULL
    if(!input$optim_initrand) g = global$graph

    print('running opt...')
    print(pops)
    print(dim(f2blocks))
    print(paste(numgen, nadmix))
    mutfuns = sapply(input$mutfuns, get)

    withProgress(message = paste('Running find_graphs...\nCheck R console to see progress'), {
      # opt_results = find_graphs(f2blocks, outpop = outpop, numrep = numrep,
      #                           numgraphs = numgraphs, numgen = numgen, numsel = numsel,
      #                           numadmix = nadmix, initgraph = g, mutfuns = mutfuns, diag = diag)
      opt_results = find_graphs(f2blocks, outpop = outpop, stop_gen = numgen, stop_sec = stop_sec,
                                numadmix = nadmix, initgraph = g, mutfuns = mutfuns, diag = diag)
    })
    print(opt_results)
    opt_results %<>% filter(!is.na(score))
    print('opt done')
    if(nrow(opt_results) == 0) {
      shinyalert('No valid graphs in output!', 'Try increasing the "diag" parameter.')
    } else {
      winner = opt_results %>% slice_min(score) %$% graph[[1]]
      oldgraph = global$graph
      oldscore = global$score
      oldedges = global$edges
      oldwr = global$worst_residual
      withProgress(message = 'Fitting best graph...', {
        global$graph = winner
        fit = get_fit()
      })
      print('fit')
      print(str(fit))

      if(fit$score < oldscore) {
        shinyalert('Better graph found!', paste('Old score: ', round(oldscore, 2),
                                                '\nNew score: ', round(fit$score, 2)))
      } else {
        global$graph = oldgraph
        global$score = oldscore
        global$edges = oldedges
        global$worst_residual = oldwr
        add_graph_to_history(global)
        fit = get_fit()
        shinyalert('No better graph found!')
      }
      print('graph')
      print(global$graph)
    }
    global$seed = NULL
  })


  qpgraphfun = reactive({

    req(input$evaluate_graph)
    print('qpgraphfun')

    seed = isolate(as.numeric(input$seed))
    if(is.na(seed)) seed = global$seed
    if(input$usef3precomp && !is.null(global$precomp)) f3precomp = global$precomp
    else f3precomp = NULL

    #isolate({
      numstart = as.numeric(input$numstart)
      lambdascale = as.numeric(input$lambdascale)
      if(is.null(lambdascale) || length(lambdascale) == 0) lambdascale = 1
      diag = input$qpgraph_diag
      lsqmode = input$lsqmode
      return_fstats = input$return_fstats
      print(paste('qpgraphfun:', numstart, seed, diag, lsqmode, lambdascale))
      function(x, y, ...) {
        args = list(...)
        if(!'numstart' %in% names(args)) args[['numstart']] = numstart
        args = c(list(x, y), args, diag = diag, lambdascale = lambdascale, lsqmode = lsqmode,
                 seed = seed, f3precomp = list(f3precomp), return_fstats = return_fstats)
        do.call(quietly(qpgraph), args)$result
      }
    #})
  })

  get_fit = reactive({
    print('get_fit')
    if(!input$evaluate_graph) return(list(edges=global$edges, score=global$score, worst_residual=global$worst_residual))

    f2blocks = get_f2blocks()
    g = global$graph
    input$qpgraph_revert
    input$qpgraph_update
    input$aconstraints_update
    input$econstraints_update
    global$qpgraph_ranges = NULL
    global$qpgraph_scorerange = NULL
    req(g, f2blocks)

    replace_null = function(x, y) ifelse(is.null(x), y, x)

    edges = g %>% igraph::as_edgelist() %>% as_tibble(.name_repair = ~c('from', 'to')) %>%
      group_by(to) %>% mutate(type = ifelse(n() > 1, 'admix', 'normal')) %>% ungroup %>%
      mutate(eid = paste(from, to, sep = ' -> '), eid2 = str_remove_all(eid, '\\.|>| '))
    isolate({
      if(isTRUE(global$useconstraints)) {
        edges %<>% mutate(lower = map_dbl(eid2, ~replace_null(input[[.]][1], 0)),
                          upper = map_dbl(eid2, ~replace_null(input[[.]][2], 1e9))) %>%
          mutate(upper = ifelse(type == 'normal', upper/edgemul, upper),
                 lower = ifelse(type == 'normal', lower/edgemul, lower)) %>% select(-type, -eid2)
        #print(edges)
      }
    })

    withProgress(message = 'Fitting graph...', {
      fit = qpgraphfun()(f2blocks, edges)
    })
    global$edges = fit$edges
    global$score = fit$score
    global$worst_residual = fit$worst_residual
    add_graph_to_history(global)
    fit
  })

  get_history = reactive({
    print('get_history')
    isolate({
    hist = global$history %>% purrr::transpose() %>% as_tibble %>%
      transmute(graph, edges,
                nadmix = as.numeric(nadmix),
                score = as.numeric(score),
                worst_residual = map_dbl(worst_residual, ~ifelse(is.null(.), NA, .))) %>%
      mutate(n = 1:n(), .before = 1) %>% arrange(-n)
    })
    hist %>% mutate(x = list(get_fit())) %>% select(-x)
  })

  get_pdat = reactive({
    print('get_pdat')
    if(is.null(global$edges)) return()
    print('get_pdat2')
    init_plotdata()
  })

  get_f2blocks = reactive({
    print('get f2')
    poplist = global$poplist
    req(poplist, global$countdir)
    if(length(poplist) == 0) return()
    pops = rep(names(poplist), sapply(poplist, length))
    inds = NULL
    if(global$iscountdata) inds = unlist(poplist)
    withProgress(message = 'Reading data and computing f2...', {
      f2blocks = f2_from_precomp(global$countdir, inds = inds, pops = pops,
                                 apply_corr = input$f2corr, afprod = input$afprod)
    })
    print('get f2 done')
    f2blocks
  })

  get_fstblocks = reactive({
    print('get fst')
    poplist = global$poplist
    req(poplist, global$countdir)
    if(length(poplist) == 0) return()
    pops = rep(names(poplist), sapply(poplist, length))
    inds = NULL
    if(global$iscountdata) inds = unlist(poplist)
    withProgress(message = 'Reading data and computing fst...', {
      f2blocks = f2_from_precomp(global$countdir, inds = inds, pops = pops,
                                 apply_corr = input$f2corr, afprod = input$afprod, fst = TRUE)
    })
    print('get fst done')
    f2blocks
  })

  get_f2dat_indiv = reactive({
    print('get f2 indiv')
    poplist = global$poplist
    req(poplist, global$countdir)
    if(length(poplist) == 0) return()
    withProgress(message = 'Reading data and computing f2...', {
      f2dat = f2_from_precomp(global$countdir, inds = unlist(poplist), pops = unlist(poplist),
                              return_array = FALSE, afprod = input$afprod)
    })
    print('get f2 indiv done')
    f2dat
  })

  init_plotdata = reactive({

    print('init')
    input$qpgraph_update
    input$qpgraph_revert
    input$evaluate_graph
    graph = global$edges
    if('igraph' %in% class(graph)) {
      edges = graph %>% as_edgelist %>% as_tibble(.name_repair = ~c('from', 'to'))
    } else {
      edges = graph %>% as_tibble(.name_repair = make.unique)
      if(nrow(edges) == 0) browser()
      graph %<>% select(1:2) %>% as.matrix %>% graph_from_edgelist()
    }
    names(edges)[1:2] = c('from', 'to')
    if('collapse_edges' %in% input$plotopt) {
      withProgress(message = 'collapsing edges...', {
        edges %<>% admixtools:::collapse_edges(10^input$collapse_threshold)
      })
    }

    admixnodes = unique(edges[[2]][edges[[2]] %in% names(which(table(edges[[2]]) > 1))])
    graph = edges %>% select(1:2) %>% as.matrix %>% graph_from_edgelist()

    pos = data.frame(names(V(graph)), igraph::layout_as_tree(graph), stringsAsFactors = F) %>%
      set_colnames(c('node', 'x', 'y'))

    eg = edges %>% left_join(pos, by=c('from'='node')) %>%
      left_join(pos %>% transmute(to=node, xend=x, yend=y), by='to') %>%
      mutate(type = ifelse(to %in% admixnodes, 'admix', 'normal'))

    if('reorder_edges' %in% input$plotopt) {
      withProgress(message = 'reordering edges...', {
        eg = admixtools:::fix_layout(eg %>% rename(name = from), graph) %>% rename(from = name)
      })
    }
    if('shift_down' %in% input$plotopt) {
      withProgress(message = 'reordering edges...', {
        eg = admixtools:::fix_shiftdown(eg %>% rename(name = from), graph) %>% rename(from = name)
      })
    }
    # if('simplify_graph' %in% input$plotopt) {
    #   withProgress(message = 'simplifying...', {
    #     isolate({
    #       global$graph = admixtools:::edges_to_igraph(eg) %>% admixtools:::simplify_graph()
    #       global$edges = edges = global$graph %>%
    #         igraph::as_edgelist() %>% as_tibble(.name_repair = ~c('from', 'to'))
    #     })
    #     eg = edges %>%
    #       left_join(pos, by=c('from'='node')) %>%
    #       left_join(pos %>% transmute(to=node, xend=x, yend=y), by='to') %>%
    #       mutate(type = ifelse(to %in% admixnodes, 'admix', 'normal'))
    #   })
    # }

    if('weight' %in% names(edges)) {
      fa = function(x) paste0(round(x*100), '%')
      fe = function(x) round(x*edgemul)
      edges$label = ifelse(edges$type == 'admix', fa(edges$weight), fe(edges$weight))
      if(!is.null(global$qpgraph_ranges)) {
        edges %<>% left_join(global$qpgraph_ranges, by = c('from', 'to')) %>%
          mutate(label = ifelse(type == 'admix',
                                paste(fa(mid), paste0('[', fa(lo), '-', fa(hi), ']'), sep = '\n'),
                                paste(fe(mid), paste0('[', fe(lo), '-', fe(hi), ']'), sep = '\n')))
      }
    }
    if(!'label' %in% names(edges)) edges %<>% mutate(label='')
    e2 = edges %>% transmute(from, to, label) %>% left_join(count(., to), by = c('from'='to'))
    eg %<>% left_join(e2, by=c('from', 'to')) %>% mutate(indegree = replace_na(n, 0))
    nodes = eg %>% filter(to %in% get_leafnames(graph)) %>% rename(x=xend, y=yend, xend=x, yend=y) %>%
      transmute(name = to, x, y, xend, yend, to=NA, rownum = 1:n())

    namedList(nodes, eg)
  })

  show_popadjust = reactive(length(global$poplist) > 0)
  show_extract = reactive(isFALSE(global$iscountdata) && isFALSE(global$isf2data))
  show_indselect = reactive(show_extract() || isTRUE(global$iscountdata))

  observeEvent(input$minus1_cell_clicked, {
    row = input$minus1_rows_selected
    req(row)
    sel = get_minus1() %>% slice(row)
    global$selgraph = sel$graph[[1]]
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
    print('newgraph set...')
    print(numadmix(global$graph))
  })
  observeEvent(input$plus1_cell_clicked, {
    row = input$plus1_rows_selected
    req(row)
    sel = get_plus1() %>% slice(row)
    global$selgraph = sel$graph[[1]]
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$flipadmix_cell_clicked, {
    row = input$flipadmix_rows_selected
    req(row)
    sel = get_flipadmix() %>% slice(row)
    global$selgraph = sel$graph[[1]]
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$history_cell_clicked, {
    row = input$history_rows_selected
    req(row)
    sel = get_history() %>% slice(row)
    global$selgraph = sel$graph[[1]]
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    #add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$addleaf_cell_clicked, {
    row = input$addleaf_rows_selected
    req(row)
    sel = get_addleaf() %>% slice(row)
    global$selgraph = sel$graph[[1]]
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })

  observeEvent(input$initresample_cell_clicked, {
    row = input$initresample_rows_selected
    req(row)
    sel = get_initresample0() %>% slice(row)
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$indresample_cell_clicked, {
    row = input$indresample_rows_selected
    req(row)
    sel = get_indresample0() %>% slice(row)
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$snpresample_cell_clicked, {
    row = input$snpresample_rows_selected
    req(row)
    sel = get_snpresample0() %>% slice(row)
    global$edges = sel$edges[[1]]
    global$score = sel$score[[1]]
    global$worst_residual = sel$worst_residual[[1]]
    add_graph_to_history(global)
    global$tempgraph = TRUE
    shinyjs::enable('qpgraph_update')
    shinyjs::enable('qpgraph_revert')
  })
  observeEvent(input$qpadm_rot_cell_clicked, {
    row = input$qpadm_rot_rows_selected
    req(row)
    sel = get_qpadm_rotate() %>% slice(row)
    global$qpadmpops_left_remain = sel$left[[1]]
    global$qpadmpops_right_add = sel$right[[1]]
  })


  observeEvent(input$multiprocess, {
    print('multiprocess')
    if(input$multiprocess) future::plan('multiprocess')
    else future::plan('sequential')
  })


  make_reactive_graphtabs = function(graphfun, inp) {
    reactive({
      print('graph neighbors')
      f2blocks = get_f2blocks()
      print('graph neighbors2')
      #g = global$graph
      g = get_graph()
      withProgress(message = 'Finding graphs...', {
        out = g %>% graphfun
      })
      print(paste('found', nrow(out), 'graphs'))
      withProgress(message = paste('Evaluating', nrow(out), 'graphs...'), {
        qpgfun = qpgraphfun()
        out %<>% mutate(res = furrr::future_map(graph, ~qpgfun(f2blocks, .))) %>%
          unnest_wider(res) %>%
          select(starts_with('from'), starts_with('to'), starts_with('score'), starts_with('graph'), starts_with('edges'))
      })
      if(nrow(out) == 0) {
        shinyalert('No graphs could be evaluated!')
        return()
      }
      out %>% arrange(score)
    })
  }

  get_minus1 = make_reactive_graphtabs(graph_minusone, input$qpgraph_similar_minus1)
  get_plus1 = make_reactive_graphtabs(graph_plusone, input$qpgraph_similar_plus1)
  get_flipadmix = make_reactive_graphtabs(admixtools::graph_flipadmix, input$qpgraph_similar_flipadmix)

  get_addleaf = eventReactive(input$qpgraph_add_run, {
    f2blocks = get_f2blocks()
    g = global$graph
    withProgress(message = 'Finding graphs...', {
      out = g %>% graph_addleaf(input$addleaf)
    })
    withProgress(message = paste('Evaluating', nrow(out), 'graphs...'), {
      qpgfun = qpgraphfun()
      out %<>% mutate(res = furrr::future_map(graph, ~qpgfun(f2blocks, .))) %>%
        unnest_wider(res) %>%
        select(starts_with('from'), starts_with('to'), starts_with('score'), starts_with('graph'), starts_with('edges'))
    })
    if(nrow(out) > 0) out %<>% arrange(score)
    out
  })

  qpgraph_scorerange = function(out, low = 0, high = 1) {
    out %>%
      select(score) %>%
      filter(!is.na(score)) %>%
      arrange(score) %>%
      slice(ceiling((low+1e-10)*n()), round(n()/2), ceiling(n()*high)) %>%
      add_column(type = c('lo', 'mid', 'hi'), .before = 1) %>%
      deframe
  }
  qpgraph_ranges = function(out, low = 0, high = 1) {
    out %>%
      select(from, to, weight) %>%
      filter(!is.na(weight)) %>%
      group_by(from, to) %>%
      arrange(weight) %>%
      slice(ceiling((low+1e-10)*n()), round(n()/2), ceiling(n()*high)) %>%
      mutate(type = c('lo', 'mid', 'hi')) %>%
      ungroup %>%
      select(from, to, type, weight) %>%
      pivot_wider(names_from = type, values_from = weight)
  }

  add_graph_to_history = function(global) {
    print('add_graph_to_history')
    isolate({
      new = list(graph = global$graph,
                 edges = global$edges,
                 nadmix = numadmix(global$graph),
                 score = global$score,
                 worst_residual = if(is.null(global$worst_residual)) NA else global$worst_residual)
      global$history %<>% c(list(new))
    })
  }

  get_initresample0 = reactive({
    print('get initresample')
    input$init_resample
    f2blocks = get_f2blocks()
    g = global$graph
    numstart = as.numeric(isolate(input$numstart2))

    withProgress(message = paste('Evaluating graph ', numstart, ' times...'), {
      qpgfun = qpgraphfun()
      out = tibble(id = seq_len(numstart)) %>%
        mutate(res = furrr::future_map(id, ~qpgfun(f2blocks, g, numstart = 1))) %>%
        unnest_wider(res) %>%
        select(id, starts_with('score'), starts_with('opt'), starts_with('graph'), starts_with('edges'))
    })
  })
  get_initresample = reactive({
    out = get_initresample0()
    if(nrow(out) > 0 && 'edges' %in% names(out)) {
      global$qpgraph_scorerange = qpgraph_scorerange(out, low = 0.025, high = 0.975)
      out %<>% unnest(edges)
      global$qpgraph_ranges = qpgraph_ranges(out, low = 0.025, high = 0.975)
      out %<>% mutate(edge = paste(from, to, sep = ' -> ')) %>%
        select(id, opt, score, edge, weight) %>%
        pivot_wider(names_from = edge, values_from = weight) %>%
        unnest(opt)
    }
    print('initresample done')
    out
  })

  get_indresample0 = reactive({
    print('get indresample')
    input$ind_resample
    poplist = global$poplist
    countdir = global$countdir
    inds = unlist(poplist)
    numsingle = sum(sapply(poplist, length) == 1)
    pops = rep(names(poplist), sapply(poplist, length))
    f2blocks = get_f2blocks()
    g = global$graph
    qpgfun = qpgraphfun()
    fun = admixtools:::make_resample_inds_fun(~qpgfun(..., y = g))
    withProgress(message = paste('Evaluating graph ', length(inds)-numsingle, ' times...'), {
      out = fun(countdir, inds = inds, pops = pops, numstart = 1)
    })
    out
  })
  get_indresample = reactive({
    out = get_indresample0()
    if(nrow(out) > 0) {
      print('head(out)')
      print(head(out))
      print(head(out$error))
      global$qpgraph_scorerange = qpgraph_scorerange(out, low = 0.025, high = 0.975)
      out %<>% unnest(edges)
      global$qpgraph_ranges = qpgraph_ranges(out, low = 0.025, high = 0.975)
      out %<>%
        mutate(edge = paste(from, to, sep = ' -> ')) %>%
        select(ind, pop, edge, score, weight) %>%
        pivot_wider(names_from = edge, values_from = weight)
    }
    print('indresample done')
    out
  })

  get_snpresample0 = reactive({
    f2blocks = get_f2blocks()
    input$snp_resample
    g = global$graph
    bootnum = ifelse(input$boot, input$bootnum, FALSE)
    withProgress(message = paste('Evaluating graph ', ifelse(bootnum, bootnum, dim(f2blocks)[[3]]),' times...'), {
      out = admixtools:::make_resample_snps_fun(~qpgraphfun()(..., y = g))(f2blocks, boot = bootnum, numstart = 1)
    })
  })
  get_snpresample = reactive({
    out = get_snpresample0()
    if(nrow(out) > 0 && 'edges' %in% names(out)) {
      print('head(out)')
      print(head(out))
      global$qpgraph_scorerange = qpgraph_scorerange(out, low = 0.025, high = 0.975)
      out %<>% unnest(edges)
      global$qpgraph_ranges = qpgraph_ranges(out, low = 0.025, high = 0.975)
      out %<>%
        mutate(edge = paste(from, to, sep = ' -> ')) %>%
        select(id, edge, score, weight) %>%
        pivot_wider(names_from = edge, values_from = weight)
    }
    print('snpres done')
    out
  })

  get_qpadm_rightpanel = reactive({
    print('get qpadm_rightpanel')
    global$qpadm_rightpanel
  })

  output$dashboardbody = renderUI({

    print('dashboardbody')

    get_bod()

  })

  output$popselectors = renderUI({

    poplist = global$poplist
    indnames = unique(global$allinds)
    print('popselectors')
    print(indnames)

    sellist = imap(names(poplist), ~{
      #buttlab = ifelse(is.null(global$poplistbak[[.x]]), 'group', 'ungroup')
      isgrouped = length(poplist[[.x]]) == 1 && admixtools:::is_group(global$countdir, poplist[[.x]])
      buttlab = ifelse(isgrouped, 'ungroup', 'group')
      fillCol(tagList(textInput(paste0('pop', .y), NULL, .x, placeholder = 'pop name'),
                      shiny::actionButton(paste0('group', .y), buttlab),
                      selectizeInput(.x, NULL, indnames,
                                     poplist[[.x]],
                                     multiple = TRUE, width = 'auto'),
                      tags$style(type="text/css", paste0('.selectize-control{white-space:pre-wrap}'))), height = '1000px')})
    print('popselectors2')
    do.call(splitLayout, sellist)
  })

  output$econstraints = renderUI({
    global$useconstraints = FALSE
    print('econstraints')
    edgecols = isolate(get_edgecols())
    edges = isolate(get_fit()$edges) %>%
      mutate(nam = paste(from, to, sep = ' -> '), nam2 = str_remove_all(nam, '\\.|>| ')) %>%
      left_join(edgecols, by = c('nam2'='edge')) %>%
      filter(type == 'edge') %>% select(nam, nam2, weight, col)

    maxlen = ceiling(max(edges$weight))*edgemul

    print('econstraints 2')
    div(div(id = 'econstraints_update_div', actionButton('econstraints_update', 'Update')),
        sliderInput('any_econstraints', 'All edges', 0, maxlen, c(0, maxlen), dragRange = FALSE),
        edges %>%
          pmap(~{div(tags$style(HTML(paste0('[for=',..2,']+span>.irs-bar, [for=',..2,']+span>.irs>.irs-from, [for=',..2,']+span>.irs>.irs-to {background: ', ..4,'}'))),
                     sliderInput(..2, ..1, min = 0, max = maxlen, value = c(0, maxlen),
                                 dragRange = FALSE, width = '100%'))}))
  })

  output$aconstraints = renderUI({
    global$useconstraints = FALSE
    print('aconstraints')
    input$add_edge
    input$delete_edge
    edgecols = isolate(get_edgecols())
    edges = isolate(get_fit()$edges) %>%
      mutate(nam = paste(from, to, sep = ' -> '), nam2 = str_remove_all(nam, '\\.|>| ')) %>%
      left_join(edgecols, by = c('nam2'='edge')) %>%
      filter(type == 'admix') %>% select(nam, nam2, weight, col)

    div(div(id = 'aconstraints_update_div', actionButton('aconstraints_update', 'Update')),
        edges %>%
          pmap(~{div(tags$style(HTML(paste0('[for=',..2,']+span>.irs-bar, [for=',..2,']+span>.irs>.irs-from, [for=',..2,']+span>.irs>.irs-to {background: ', ..4,'}'))),
                     sliderInput(..2, ..1, min = 0, max = 1, value = c(0, 1),
                                 dragRange = FALSE, width = '100%'))})
    )
  })

  output$nadmix = renderUI({
    nad = 3
    if(!is.null(global$graph)) nad = numadmix(global$graph)
    div(sliderInput('nadmix', '# admix', 0, 10, nad),
        selectizeInput('outpop', 'Outgroup', c('< undefined >', names(global$poplist)), selected = names(global$poplist)[1]))
  })

  output$qpgraph_add = renderUI({
    print('selectInput addleaf')
    choices = setdiff(names(global$poplist), get_leafnames(global$graph))
    selectInput('addleaf', '', choices = choices)
  })

  output$showseledge = renderText({
    input$clear_edge
    print('showseledge1')
    se = global$seledge
    print('se')
    print(global$seledge)
    print(se)
    if(!is.null(se) && (is.na(se) || se == 'NA')) {
      print('is na')
      se = NULL
    }
    print('se2')
    print(se)
    se
  })

  observeEvent(input$any_econstraints, {

    print('update triggered by any_econstraints')
    req(global$graph)
    print('any_econstraints 2')
    edges = global$graph %>% igraph::as_edgelist() %>% as_tibble(.name_repair = ~c('from', 'to')) %>%
      group_by(to) %>% mutate(type = ifelse(n() > 1, 'admix', 'normal')) %>% ungroup %>%
      mutate(eid = paste(from, to, sep = '->') %>% str_remove_all('\\.|>| ')) %$% eid
    print('any_econstraints 3')
    if(is.null(isolate(input[[edges[1]]]))) return()
    edges %>% walk(~{updateSliderInput(session, ., value = c(input$any_econstraints[1],
                                                             input$any_econstraints[2]))})
    print('any_econstraints 4')
  })

  observeEvent(input$delete_edge, {

    print('del 1')
    eg = get_seledge()
    if(is.null(eg)) return()
    gnew = global$graph
    leaves = get_leafnames(gnew)
    print('del 2')
    print(eg)
    eg %<>% str_split('\n') %>% `[[`(1)
    if(str_detect(eg[1], ' -> ')) {
      eg %<>% str_split(' -> ')
      print(eg)
      for(i in 1:length(eg)) {
        #browser()
        gnew = delete_admix(gnew, eg[[i]][1], eg[[i]][2])
        # g <<- g
        # gnew <<- gnew
        if(!admixtools:::is_valid(gnew)) {
          shinyalert('Error', 'Could not delete edge!')
          return()
        }
      }
    } else {
      for(i in 1:length(eg)) {
        gnew = delete_leaf(gnew, eg[i])
        if(!admixtools:::is_valid(gnew)) {
          browser()
          shinyalert('Error', 'Could not delete node!')
          return()
        }
      }
    }
    newleaves = setdiff(get_leafnames(gnew), leaves)
    if(length(newleaves) > 0) {
      # newleaves <<- newleaves
      # oldgraph <<- global$graph
      # newgraph <<- g
      shinyalert('New leaves!', newleaves)
    }
    global$graph = gnew
    global$seledge = NULL
    global$edges = gnew %>% as_edgelist %>% as_tibble(.name_repair = ~c('from', 'to'))
    global$score = NA
    global$worst_residual = NA
    shinyjs::disable('clear_edge')
    shinyjs::disable('add_edge')
    shinyjs::disable('delete_edge')
    print('delete_edge done')
  })

  observeEvent(input$clear_edge, {
    print('clear')
    global$seledge = NULL
    shinyjs::disable('clear_edge')
    shinyjs::disable('add_edge')
    shinyjs::disable('delete_edge')
  })

  observeEvent(input$add_edge, {
    print('add')
    eg = get_seledge() %>% str_split('\n') %>% `[[`(1) %>% str_split(' -> ')
    g = global$graph
    leaves = get_leafnames(g)

    if(length(eg) == 1) {
      print('input$addleaf')
      print(input$addleaf)
      if(is.null(input$addleaf)) {
        shinyalert('Error', 'Can only add edge which connects exactly two other edges!')
        return()
      }
      from = eg[[1]][1]
      to = eg[[1]][2]
      gnew = admixtools:::insert_leaf(g, input$addleaf, from, to)
      if(!admixtools:::is_valid(gnew)) {
        shinyalert('Error', 'Could not add population!')
        return()
      }
      global$graph = gnew
      global$seledge = NULL
      global$edges = gnew %>% as_edgelist %>% as_tibble(.name_repair = ~c('from', 'to'))
      global$score = NA
      global$worst_residual = NA
      shinyjs::disable('clear_edge')
      shinyjs::disable('add_edge')
      shinyjs::disable('delete_edge')

    } else if(length(eg) == 2) {
      #from = eg[[1]][2]
      #to = eg[[2]][2]
      #print(paste(from, to))

      alert = function(x) shinyalert('Could not insert edge!', as.character(x))
      tryCatch({
        #gnew = admixtools:::insert_admix_old(g, from, to, allow_below_admix = TRUE)
        gnew = admixtools:::insert_admix(g, eg[[1]][1], eg[[1]][2], eg[[2]][1], eg[[2]][2])
      }, warning = alert, error = alert)
      if(!exists('gnew') || is.null(gnew)) return()
      if(!admixtools:::is_valid(gnew)) {
        shinyalert('Error', 'Could not insert edge!')
        return()
      }
      global$graph = gnew
      global$seledge = NULL
      global$edges = gnew %>% as_edgelist %>% as_tibble(.name_repair = ~c('from', 'to'))
      global$score = NA
      global$worst_residual = NA
      shinyjs::disable('clear_edge')
      shinyjs::disable('add_edge')
      shinyjs::disable('delete_edge')

      newleaves = setdiff(get_leafnames(gnew), leaves)
      if(length(newleaves) > 0) shinyalert('New leaves!', newleaves)
    } else {
      shinyalert('Error', 'Too many items selected!')
      return()
    }
  })

  observeEvent(input$run_qpadm, {
    print('observe run_qpadm')
    global$qpadmout = get_qpadmout()
    global$qpadm_rightpanel = plotOutput('qpadmcomparison')
  })
  observeEvent(input$run_qpgraph, {
    print('observe run_qpgraph')
    global$qpgraphout = get_qpgraphout()
    #global$qpg_right = plotlyOutput('graphcomparison')
  })

  map(1:4,
      ~observeEvent(input[[paste0('f4pops', .)]], {
        if(input$f4_allowdup) return()
        popsinuse = unique(c(input$f4pops1, input$f4pops2, input$f4pops3, input$f4pops4))
        map(setdiff(1:4, .), ~{
          fp = paste0('f4pops', .)
          sel = input[[fp]]
          updateSelectizeInput(session, fp, selected = sel,
                               choices = union(sel, setdiff(names(global$poplist), popsinuse)))})
      }))

  observeEvent(input$f4_allowdup, {
    map(1:4, ~{
      fp = paste0('f4pops', .)
      sel = input[[fp]]
      updateSelectizeInput(session, fp, selected = sel,
                           choices = names(global$poplist))})
  })

  map(1:3, ~observeEvent(input[[paste0('qpadmpops', .)]], {
        popsinuse = unique(c(input$qpadmpops1, input$qpadmpops2, input$qpadmpops3))
        map(setdiff(1:3, .), ~{
          fp = paste0('qpadmpops', .)
          sel = input[[fp]]
          global[[fp]] = sel
          choices = union(sel, setdiff(names(global$poplist), popsinuse))
          updateSelectizeInput(session, fp, selected = sel, choices = choices)})
      }))

  observeEvent(input$qpadm_randomize, {
    nam = sample(names(global$poplist))
    nl = floor(length(nam)/2)
    choices = list(nam[1], nam[2:nl], nam[(nl+1):length(nam)])
    map(1:3, ~{
      global[[paste0('qpadmpops', .)]] = choices[[.]]
      updateSelectizeInput(session, paste0('qpadmpops', .),
                           selected = choices[[.]], choices = nam)})
  })
  observeEvent(input$qpadm_rotate, {
    #global$show_qpadm_rotate = input$qpadm_rotate
    global$qpadmpops1 = input$qpadmpops1
    global$qpadmpops2 = input$qpadmpops2
    global$qpadmpops3 = input$qpadmpops3
  })

  observeEvent(input$qpadm_results, {
    global$qpadmpops1 = input$qpadmpops1
    global$qpadmpops2 = input$qpadmpops2
    global$qpadmpops3 = input$qpadmpops3
    #global$show_qpadm_results = input$qpadm_results
  })

  get_qpadm_rotate = reactive({
    f2blocks = get_f2blocks()
    p1 = input$qpadmpops1
    p2 = input$qpadmpops2
    p3 = input$qpadmpops3
    req(f2blocks, p1, p2, p3)

    withProgress(message = paste0('Generating qpadm models...'), {
      lr = admixtools:::all_lr2(p1, length(p2))
    })
    withProgress(message = paste0('Evaluating ', length(lr[[1]]), ' qpadm models...'), {
      out = admixtools:::qpadm_eval_rotate(f2blocks, p3, lr, p2, verbose = F) %>%
        select(chisq, p, feasible, left, right)
    })
    out
  })

  get_qpadmout = reactive({

    print('get_qpadmout')
    get_qpadm()
    volumes %<>% eval
    pref = parseFilePaths(volumes, input$qpadm_genofile)$datapath
    pref %<>% str_replace('\\.geno$', '') %>% normalizePath(mustWork = FALSE)
    bin = parseFilePaths(volumes, input$qpadmbin)$datapath %>% normalizePath(mustWork = FALSE)
    #if(length(bin) == 0) bin = '~/Downloads/admixtoools_hub_reldec19/bin/qpAdm'
    #if(length(pref) == 0) pref = '~/Documents/v42.1_afafam'
    target = input$qpadmpops1
    left = input$qpadmpops2
    right = input$qpadmpops3

    withProgress(message = 'running original qpAdm...', {
      dir = tempdir()
      out = tryCatch(qpadm_wrapper(left, right, target, bin = bin, pref = pref, outdir = dir),
                     error = function(e) shinyalert('Error!', as.character(e)))
    })
    out
  })
  get_qpgraphout = reactive({

    print('get_qpgraphout')
    get_fit()
    volumes %<>% eval
    pref = parseFilePaths(eval(volumes), input$qpgraph_genofile)$datapath
    pref %<>% str_replace('\\.geno$', '') %>% normalizePath(mustWork = FALSE)
    bin = parseFilePaths(volumes, input$qpgraphbin)$datapath %>% normalizePath(mustWork = FALSE)
    print('bin')
    print(bin)
    # if(length(bin) == 0) bin = '~/Downloads/admixtoools_hub_reldec19/bin/qpGraph'
    # if(length(pref) == 0) pref = '~/Documents/v42.1_afafam'
    g = global$graph

    withProgress(message = 'running original qpGraph...', {
      dir = tempdir() %>% normalizePath()
      print('pref')
      print(pref)
      print('dir')
      print(dir)
      out = qpgraph_wrapper(g, bin, pref, outdir = dir, zthresh = 0)
      out$f2 = NULL
      print('parse fstats')
      fstatsfile = paste0(dir, '/fstats.out')
      if(file.exists(fstatsfile)) global$precomp = admixtools:::parse_fstats(fstatsfile)
    })
    out
  })

  observeEvent(input$econstraints_update,  {global$useconstraints = TRUE})
  observeEvent(input$aconstraints_update,  {global$useconstraints = TRUE})

  dto = function(x) div(DT::DTOutput(x), style = dtstyle)
  #dtof4 = reactive({ if(input$return_fstats) dto('f4') else renderText("Activate 'return_fstats' under 'Fit'!") })

  observeEvent(input$qpadm_fit, {global$qpadm_rightpanel = qpadm_rightpanel_fit()})

  # observeEvent(input$qpgraph_fit, {
  #   print('fit!')
  #   global$qpgraph_ranges = NULL
  #   global$qpgraph_scorerange = NULL
  #   global$qpg_right = qpg_right_fit()
  # })

  qpadm_leftpanel = reactive({
    print('reactive qpadm_leftpanel')
    all = names(global$poplist)
    choices1 = setdiff(all, c(global$qpadmpops2, global$qpadmpops3))
    choices2 = setdiff(all, c(global$qpadmpops1, global$qpadmpops3))
    choices3 = setdiff(all, c(global$qpadmpops1, global$qpadmpops2))
    div(
      selectizeInput('qpadmpops1', 'Left', choices = choices1, multiple = TRUE, selected = global$qpadmpops1),
      selectizeInput('qpadmpops2', 'Right', choices = choices2, multiple = TRUE, selected = global$qpadmpops2),
      selectizeInput('qpadmpops3', 'Target', choices = choices3, multiple = FALSE, selected = global$qpadmpops3),
      splitLayout(actionButton('qpadm_randomize', 'Randomize')))
  })

  qpadm_rightpanel_fit = reactive({
    tabsetPanel(id = 'qpadm_tabset',
                tabPanel('Weights', dto('qpadm_weights')),
                tabPanel('f4', dto('qpadm_f4')),
                tabPanel('Rank drop', dto('qpadm_rankdrop')),
                tabPanel('Pop drop', dto('qpadm_popdrop')))
  })
  qpadm_rightpanel_rotate = reactive(div(dto('qpadm_rot'), style='padding-right:50px;'))

  qpadm_rightpanel = reactive({
    #res = global$show_qpadm_results
    #rot = global$show_qpadm_rotate
    res = if(is.null(input$qpadm_results)) TRUE else input$qpadm_results
    rot = if(is.null(input$qpadm_rotate)) FALSE else input$qpadm_rotate
    if(rot && res) splitLayout(cellWidths = c('50%', '50%'), qpadm_rightpanel_rotate(), qpadm_rightpanel_fit())
    else if(rot) qpadm_rightpanel_rotate()
    else if(res) qpadm_rightpanel_fit()
    else div()
    })

  get_loaddata = reactive({
    print('load_data')
    input$textdirinput
    bh = '140px'
    div(
      fluidRow(
        box(width=4, height=bh, background = cols[1], h4('Select data directory'),
            div(splitLayout(textInput('textdirinput', NULL, placeholder = 'Data directory'),
                            actionButton('textdir', 'Create')))),
        conditionalPanel('output.show_extract == "1"',
                         fluidRow(box(width=4, height=bh, background = cols[2], h4('Extract data'),
                                      splitLayout(
                                        div(
                                          textInput('textgenofile1', NULL, placeholder = 'genotype file')
                                          ),
                                        actionButton('extract_data', 'Extract data'))))),
        conditionalPanel('output.show_indselect == "1"', (
          box(width=4, height=bh, background = cols[3], h4('Select .ind or .fam file'),
              div(fileInput('popfile', NULL, placeholder = '', buttonLabel = 'Ind file'), id = 'popfilediv')))),
        fluidRow(column(12, conditionalPanel('input.extract_data > 0', verbatimTextOutput('console')))),
        #box(width=6, textOutput('console')),
        div(style = 'visibility: hidden', verbatimTextOutput('show_popadjust')),
        div(style = 'visibility: hidden', verbatimTextOutput('show_extract')),
        div(style = 'visibility: hidden', verbatimTextOutput('show_indselect')),
        conditionalPanel('output.show_popadjust == "1"', fluidRow(column(12,
                                                                         box(width=12, background = cols[5],
                                                                             fluidRow(column(2, h4('Adjust populations', id = 'adjpopdiv')),
                                                                                      column(1, div(actionButton('removepop', '–'),
                                                                                                    actionButton('addpop', '+')))),
                                                                             fluidRow(column(11, uiOutput('popselectors')))))))))
  })

  observeEvent(input$extract_data, {
    if(is.null(input$textgenofile1) || !file.exists(input$textgenofile1)) {
      shinyalert('Error!', 'Please provide path and name for a .geno or .bed file!')
      return()
    }
    pref = input$textgenofile1 %>% str_replace('\\.geno$|\\.bed$', '')

    oldnam = names(global$poplist)
    nam = map(paste0('pop', seq_len(length(global$poplist))), ~input[[.]])
    if(is.null(nam[[1]])) return()
    global$poplist = map(oldnam, ~input[[.]]) %>% purrr::set_names(nam)

    poplist = global$poplist
    inds = unlist(poplist)
    pops = rep(names(poplist), sapply(poplist, length))
    transitions = input$trans_extract %in% c('both', 'only transitions')
    transversions = input$trans_extract %in% c('both', 'only transversions')

    if(input$fix_populations) extractfun = function(...) extract_f2(pops = pops, ...)
    else extractfun = extract_counts

    print('inds')
    print(inds)

    print('global$countdir')
    print(global$countdir)

    withCallingHandlers({
      shinyjs::html('console', '')
      print(list(pref, global$countdir, inds = inds,
                 maxmiss = input$max_miss, minmaf = input$min_maf, maxmaf = input$max_maf,
                 transitions = transitions, transversions = transversions,
                 keepsnps = input$keepsnps, maxmem = input$maxmem*1e3))
      extractfun(pref, global$countdir, inds = inds,
                   maxmiss = input$max_miss, minmaf = input$min_maf, maxmaf = input$max_maf,
                   transitions = transitions, transversions = transversions,
                   keepsnps = input$keepsnps, maxmem = input$maxmem*1e3)
    },
    message = function(m) {
      shinyjs::html(id = 'console', html = m$message, add = TRUE)
    })

    global$iscountdata = 'indivs' %in% list.dirs(global$countdir,F,F)
    global$isf2data = !'indivs' %in% list.dirs(global$countdir,F,F) &
      'block_lengths_f2.rds' %in% list.files(global$countdir)
    if(global$iscountdata) {
      global$allinds = list.dirs(paste0(global$countdir, '/pairs'),F,F)
    } else if(global$isf2data) {
      global$allinds = list.dirs(global$countdir,F,F)
      global$poplist = global$allinds %>% purrr::set_names(global$allinds)
    }

  })


  observeEvent(event_data('plotly_click', source = 'src'), {

    print('observe seledge')
    ed = event_data('plotly_click', source = 'src')
    isolate({
      plt = plotly_graph()
      newe = ed_to_name(ed, plt)
      oldedge = global$seledge
      if(!is.null(oldedge) && is.na(oldedge)) {
        print('seledge is na. why?')
        oldedge = NULL
      }
      global$seledge = newe
      shinyjs::enable('clear_edge')
      shinyjs::enable('add_edge')
      shinyjs::enable('delete_edge')
    })
    if(!is.null(newe) && !is.null(oldedge) && newe != oldedge) global$seledge = paste(c(oldedge, newe), collapse = '\n')
  })

  get_seledge = reactive({
    print('get_seledge')
    print(global$seledge)
    global$seledge
  })

  ed_to_name = function(ed, plt) {
    dat = plt$x$data[[ed$curveNumber + 1]]
    type = ifelse(length(dat) == 14, 'text', ifelse('hovertext' %in% names(dat), 'edge', 'node'))
    pt = ed$pointNumber + 1

    if (type == 'edge') {
      name = dat$hovertext[pt]
    } else if (type == 'node') {
      col = ifelse(duplicated(dat$text)[pt], 5, 3)
      name = dat$text[pt]
    } else {
      name = dat$text[pt]
    }
    print('ed_to_name22')
    name
  }

  get_edgecols = reactive({
    plt = plotly_graph()
    edgecols = plt$x$data %>% map(~pluck(., 'line', 'color')) %>% discard(is.null)
    txt = plt$x$data[seq_len(length(edgecols))] %>% map(~pluck(., 'text'))
    tibble(edge = str_remove_all(unlist(txt), '\\.|>| '),
           col = rep(as.vector(edgecols), map_dbl(txt, length))) %>%
      unnest(col) %>% filter(!duplicated(.), !is.na(edge))
  })


  plotly_graph = reactive({

    print('plotly_graph')
    poplist = global$poplist
    global$graph
    pdat = get_pdat()
    req(pdat, poplist)
    nadmix = numadmix(graph_from_edgelist(as.matrix(global$edges)[,1:2]))
    textsize = 2.5
    print('plotly_graph 2')

    eg = pdat$eg
    nodelabs = imap(poplist, ~paste(.y, paste(.x, collapse = '\n'), sep = '\n')) %>%
      unlist %>% enframe(name = 'name', value = 'text')
    #nodelabs = poplist %>% group_by(pop) %>%
    #  summarize(text = paste(pop[1], paste(ind, collapse='\n'), sep = '\n'))
    nodes = pdat$nodes %>% left_join(nodelabs, by = c('name'))
    allnodes = eg %>% transmute(from, x, y, xend=0, yend=0, to=0) %>% filter(!duplicated(.))
    segtext = "ifelse(indegree == 1, to, from)"
    segtext = "paste(from, to, sep = ' -> ')"
    print('plotly_graph 3')
    sr = global$qpgraph_scorerange
    scoretext = paste0('score: ', ifelse(is.null(sr), round(global$score, 2),
                       paste0(round(sr['mid'], 2), '\n[', round(sr['lo'], 0), ' - ', round(sr['hi'], 0), ']')),
                       ifelse(is.na(global$worst_residual), '', paste0('\nWR: ', round(global$worst_residual, 2))),
                       '\nadmix: ', nadmix,
                       ifelse(global$tempgraph, '\n\nPREVIEW!', ''))

    gg = eg %>% mutate(rownum = 1:n()) %>%
      ggplot(aes(x=x, xend=xend, y=y, yend=yend, from=from, to=to)) +
      geom_segment(aes_string(linetype = 'type', col = 'as.factor(y)', text = segtext),
                   arrow=arrow(type = 'closed', angle = 10, length=unit(0.15, 'inches'))) +
      geom_point(data = allnodes, aes(x, y, text = from), col = 'black', alpha = 0) +
      annotate('text', x = min(nodes$x), y = max(nodes$yend), hjust = 0, vjust = 1,
               label = scoretext) +
      # annotate('text', x = min(nodes$x), y = max(nodes$yend), hjust = 0,
      #          label = scoretext) +
      theme(panel.background = element_blank(),
            axis.line = element_blank(),
            axis.text = element_blank(),
            axis.ticks = element_blank(),
            legend.position = 'none') +
      xlab('') + ylab('') +
      scale_linetype_manual(values = c(admix=3, normal=1)) +
      ggtitle('') +
      scale_x_continuous(expand = c(0.1, 0.1))

    if('highlight_unidentifiable' %in% input$plotopt) {
      unid = unidentifiable_edges(global$graph)
      unid2 = eg %>% right_join(unid %>% select(-type), by = c('from', 'to'))
      gg = gg + geom_segment(aes_string(linetype = 'type'), col = 'red', size = 1, data = unid2)
    }

    gg = gg + geom_text(aes(x = (x+xend)/2, y = (y+yend)/2, label = label, text = paste(from, to, sep = ' -> ')),
                size = textsize) +
              geom_text(data = nodes, aes_string(label = 'name', col = 'as.factor(yend)',
                                         from = NA, text = 'text'), size = textsize)

    print('plotly_graph 4')
    plt = plotly::ggplotly(gg, source = 'src', tooltip=c('text'))
    print('plotly_graph 5')
    plt
  })

  output$graphplot <- renderPlotly({

    print('graphplot')
    suppressWarnings({withProgress(message = 'Processing...', {plt = plotly_graph()})})
    #event_register(plt, 'plotly_hover')
    print('graphplot2')
    plt
  })

  get_f2 = reactive({

    print('get f2')
    f2blocks = if(input$fst == 'fst') get_fstblocks() else get_f2blocks()
    #req(f2blocks, p1, p2, p3, p4)
    f2(f2blocks)
  })

  get_f3 = reactive({

    print('get f3')
    f2blocks = get_f2blocks()
    f3(f2blocks)
  })

  get_f4 = reactive({

    print('get f4')
    input$f4_run
    f2blocks = get_f2blocks()
    p1 = input$f4pops1
    p2 = input$f4pops2
    p3 = input$f4pops3
    p4 = input$f4pops4
    req(f2blocks, p1, p2, p3, p4)
    f4(f2blocks, p1, p2, p3, p4)
  })

  get_qpadm = reactive({

    print('get qpadm')
    input$qpadm_run
    f2blocks = get_f2blocks()
    p1 = input$qpadmpops1
    p2 = input$qpadmpops2
    p3 = input$qpadmpops3
    req(f2blocks, p1, p2, p3)
    #if(global$show_qpadm_rotate) {
    if(input$qpadm_rotate) {
      p1 = if(is.null(global$qpadmpops_left_remain)) p1 else global$qpadmpops_left_remain
      p2 = union(p2, global$qpadmpops_right_add)
    }
    qpadm(f2blocks, p1, p2, p3)
  })

  plotly_f2 = reactive({

    print('get f2')
    f2blocks = if(input$fst == 'fst') get_fstblocks() else get_f2blocks()

    pdat = f2blocks %>%
      apply(1:2, mean, na.rm = T) %>%
      as_tibble(rownames = 'pop1') %>%
      pivot_longer(-pop1, 'pop2', values_to = 'est')

    gg = pdat %>%
      ggplot(aes(pop1, pop2, text = paste(pop1, pop2, round(est, 3), sep='\n'))) +
      geom_tile(aes(fill = est)) +
      xlab('') +
      ylab('') +
      theme(axis.text.x = element_text(angle = 90, hjust = 1), panel.background = element_blank()) +
      scale_fill_distiller(palette = 'Purples')

    plt = plotly::ggplotly(gg, source = 'src_f2', tooltip=c('text'))
    plt
  })

  plotly_f2_indiv = reactive({

    f2dat = get_f2dat_indiv()
    poplist = global$poplist
    ind = enframe(poplist, 'pop', 'ind') %>% unnest(ind)

    dat = f2dat %>%
      transmute(ind1 = pop1, ind2 = pop2, f2corr = f2, f2uncorr) %>%
      left_join(ind %>% transmute(ind1 = ind, pop1 = pop), by = 'ind1') %>%
      left_join(ind %>% transmute(ind2 = ind, pop2 = pop), by = 'ind2') %>%
      arrange(pop1, pop2) %>%
      mutate(i1 = as.numeric(factor(ind1, levels = unique(ind1[order(pop1)]))),
             i2 = as.numeric(factor(ind2, levels = unique(ind2[order(pop2)]))),
             f2 = ifelse(i1 > i2, f2corr, f2uncorr))
    d2 = enframe(sapply(poplist, length)) %>%
      arrange(name) %>%
      mutate(end = cumsum(value), start = lag(end, default = 0), mn = start + value/2 + 0.5)

    exp = 0
    gg = dat %>%
      ggplot(aes(i1, i2)) +
      geom_tile(aes(fill = f2, text = paste(ind1, ind2, round(f2, 3), sep = '\n'))) +
      geom_vline(aes(xintercept = end+0.5), data = d2 %>% head(-1) %>% rename(ind2 = name)) +
      geom_hline(aes(yintercept = end+0.5), data = d2 %>% head(-1) %>% rename(ind2 = name)) +
      scale_x_continuous(labels = d2$name, breaks = d2$mn, expand = c(exp, exp)) +
      scale_y_continuous(labels = d2$name, breaks = d2$mn, expand = c(exp, exp)) +
      xlab('') + ylab('') +
      theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5),
            panel.background = element_blank()) +
      scale_fill_distiller(palette = 'Purples')
    plt = plotly::ggplotly(gg, source = 'src_f2', tooltip = 'text')
    plt
  })

  plotly_f4 = reactive({

    print('get f4')
    input$f4_run
    #isolate({
    f2blocks = get_f2blocks()
    p1 = input$f4pops1[1]
    p2 = input$f4pops2[1:2]
    # f4pops2 needs at least two populations. how to check for this? otherwise, "Error: Only strings can be converted to symbols"
    f4dat = get_f4() %>% filter(pop1 == p1, pop2 %in% p2)
    req(f4dat)
    #})
    f4dat %<>%
      mutate(pop2 = recode(pop2, !!sym(p2[1]):='x', !!sym(p2[2]):='y')) %>%
      pivot_wider(names_from = pop2, values_from = c(est, se, z, p)) %>%
      mutate(mul = input$f4_se)
    xmin = min(f4dat$est_x - f4dat$se_x)
    xmax = max(f4dat$est_x + f4dat$se_x)
    ymin = min(f4dat$est_y - f4dat$se_y)
    ymax = max(f4dat$est_y + f4dat$se_y)
    gg = f4dat %>%
      ggplot(aes(est_x, est_y, col = pop3, text = paste0('f4(', p1, ', X; ', pop3, ', ', pop4, ')'))) +
      geom_smooth(method = 'lm', se = FALSE, formula=y~x-1) +
      geom_hline(yintercept = 0) +
      geom_vline(xintercept = 0) +
      geom_point() +
      theme(panel.background = element_blank()) +
      xlab(paste0('f4(', p1, ', ', p2[1], '; Pop 3, Pop 4)' )) +
      ylab(paste0('f4(', p1, ', ', p2[2], '; Pop 3, Pop 4)' )) +
      scale_x_continuous(limits = c(xmin, xmax)) +
      scale_y_continuous(limits = c(ymin, ymax))
    if(input$f4_se > 0) {
      gg = gg +
        geom_errorbar(aes(ymin = est_y - se_y*mul, ymax = est_y + se_y*mul), width = 0) +
        geom_errorbarh(aes(xmin = est_x - se_x*mul, xmax = est_x + se_x*mul), height = 0)
    }
    plt = plotly::ggplotly(gg, source = 'src_f4', tooltip=c('text'))
  })

  plotly_f42 = reactive({

    print('get f4')
    input$f4_run
    #isolate({
    f2blocks = get_f2blocks()
    f4dat = get_f4() %>% filter(pop1 == input$f4pops1[1], pop3 == input$f4pops3[1], pop4 == input$f4pops4[1])
    req(f4dat)
    #})
    # f4dat %<>%
    #   mutate(pop2 = recode(pop2, !!sym(p2[1]):='x', !!sym(p2[2]):='y')) %>%
    #   pivot_wider(names_from = pop2, values_from = c(est, se, z, p))
    # xmin = min(f4dat$est_x - f4dat$se_x)
    # xmax = max(f4dat$est_x + f4dat$se_x)
    # ymin = min(f4dat$est_y - f4dat$se_y)
    # ymax = max(f4dat$est_y + f4dat$se_y)
    gg = f4dat %>%
      ggplot(aes(pop2, est, col = pop2)) +
      geom_hline(yintercept = 0) +
      geom_point() +
      theme(panel.background = element_blank()) +
      xlab(paste0('' )) +
      ylab(paste0('f4(', input$f4pops1[1], ', X; ', input$f4pops3[1], ', ', input$f4pops4[1],')' ))
    if(input$f4_se > 0) {
      mul = input$f4_se
      gg = gg + geom_errorbar(aes(ymin = est - se*mul, ymax = est + se*mul), width = 0)
    }
    plt = plotly::ggplotly(gg, source = 'src_f42')
  })

  output$f2heatmap = renderPlotly({

    print('f2plot')
    plt = plotly_f2()
    plt
  })

  output$f2heatmap_indiv = renderPlotly({

    print('f2plot indiv')
    plt = plotly_f2_indiv()
    plt
  })

  output$f4plot = renderPlotly({

    print('f4plot')
    plt = plotly_f4()
    plt
  })
  output$f4plot2 = renderPlotly({

    print('f4plot')
    plt = plotly_f42()
    plt
  })

  output$qpadmcomparison = renderPlot({

    print('qpadmcomparison')

    withProgress(message = 'running qpadm in R...', {
      qpadm_R = get_qpadm()
    })

    original = global$qpadmout
    gg = plot_comparison(original, qpadm_R)
    #plotly::ggplotly(gg, tooltip = 'label')
    gg
  })
  output$graphcomparison = renderPlotly({

    print('graphcomparison')
    req(global$qpgraphout)

    withProgress(message = 'running qpgraph in R...', {
      qpgraph_R = get_fit()
    })

    #qpgraph_R$f3 = qpgraph_R$f3 %>% select(pop2, pop3, fit, est) # temporary; line should be removed
    gg = plot_comparison(global$qpgraphout, qpgraph_R)
    plotly::ggplotly(gg, tooltip = 'label')
  })



  output$downloadgraph = downloadHandler('graph.tsv', function(file) {
    edges = get_fit()$edges
    if(input$downloadgraph_format == 'admixtools') edges %>% admixtools:::fit_to_qpgraph_format() %>% write(file)
    else if(input$downloadgraph_format == 'edgelist') edges %>% write_tsv(file)
    else if(input$downloadgraph_format == 'dot') edges %>% admixtools:::write_dot(file)
    else stop()
  })

  ex = c('Buttons', 'Scroller')
  dtfun = function(...) DT::renderDataTable(..., extensions = ex, options = do, selection = 'single')

  output$f2 = dtfun({format_table(get_fit()$f2)})
  output$f3 = dtfun({format_table(get_fit()$f3)})
  output$f4 = dtfun({if(is.null(get_fit()$f4)) as.data.frame("Activate 'return_fstats' under 'Fit'!") %>% slice(-1) else format_table(get_fit()$f4)})
  output$opt = dtfun({format_table(get_fit()$opt)})
  output$history = dtfun({format_table(get_history() %>% select(-graph, -edges))})
  output$minus1 = dtfun({format_table(get_minus1())})
  output$plus1 = dtfun(format_table(get_plus1()))
  output$flipadmix = dtfun({format_table(get_flipadmix())})
  output$addleaf = dtfun({format_table(get_addleaf())})

  output$initresample = dtfun({format_table(get_initresample())})
  output$indresample = dtfun({format_table(get_indresample())})
  output$snpresample = dtfun({format_table(get_snpresample())})

  output$f2stats = dtfun({format_table(get_f2())})
  output$f3stats = dtfun({format_table(get_f3())})
  output$f4stats = dtfun({format_table(get_f4())})

  output$qpadm_weights = dtfun({format_table(get_qpadm()$weights)})
  output$qpadm_f4 = dtfun({format_table(get_qpadm()$f4)})
  output$qpadm_rankdrop = dtfun({format_table(get_qpadm()$rankdrop)})
  output$qpadm_popdrop = dtfun({format_table(get_qpadm()$popdrop)})

  output$qpadm_rot = dtfun({format_table(get_qpadm_rotate())})

  onBookmark(function(state) {
    names(global) %>% map(~{state$values[[.]] = global[[.]]})
  })
  onRestore(function(state) {
    names(state$values) %>% map(~{global[[.]] = state$values[[.]]})
  })

  #setBookmarkExclude('sidebarItemExpanded')
  #setBookmarkExclude('navbar')

  observeEvent(input$tooltips, {
    print('refresh tooltips')
    if(input$tooltips %% 2 == 1) {
      updateActionButton(session, 'tooltips', icon = icon('exclamation'))
      imap(tt, ~removeTooltip(session, .y))
      imap(tt, ~addTooltip(session, .y, .x))
    } else {
      updateActionButton(session, 'tooltips', icon = icon('question'))
      imap(tt, ~removeTooltip(session, .y))
    }
  })
}

shinyApp(ui = ui, server = server, enableBookmarking = 'server')

#runApp(list(ui = ui, server = server), display.mode = 'showcase')
uqrmaie1/admixtools documentation built on March 20, 2024, 8:24 a.m.