# ============================================================ # QTL-Mapper v7 ENHANCED โ€” FIXED VERSION # Fixes: # 1. Added colourpicker to required_packages # 2. Customization controls now fully wired to plots & downloads # 3. make_chromosome_ideogram_reference accepts color/size params # 4. Manhattan plot height/width driven by sliders (uiOutput) # 5. Ideogram width now respected in renderPlot # 6. PNG downloads use custom dimensions & styles # 7. threshold_style mapped correctly to base R lty values # 8. compare_bar PNG uses custom dimensions # 9. All colourInput references safe-guarded with defaults # ============================================================ options(repos = c(CRAN = "https://cloud.r-project.org")) required_packages <- c( "shiny", "randomForest", "caret", "DT", "ggplot2", "plotly", "dplyr", "glmnet", "shinyjs", "scales", "e1071", "xgboost", "writexl", "colourpicker" ) for (pkg in required_packages) { if (!require(pkg, character.only = TRUE, quietly = TRUE)) { install.packages(pkg, dependencies = TRUE) library(pkg, character.only = TRUE) } } # ============================================================ # 6 MODELS # ============================================================ MAPPING_MODELS <- list( "SIM" = list(label = "Simple Interval Mapping (SIM)", desc = "Single-marker regression; LOD = -log10(p)"), "CIM" = list(label = "Composite Interval Mapping (CIM)", desc = "Cartographer-like CIM with cofactors"), "MQM" = list(label = "Multiple QTL Model (MQM)", desc = "Forward stepwise multi-QTL selection"), "RF" = list(label = "Random Forest QTL", desc = "Non-parametric; permutation importance"), "SVR" = list(label = "Support Vector Regression", desc = "Kernel-based; permutation importance"), "XGBOOST" = list(label = "XGBoost QTL", desc = "Gradient boosting; gain importance") ) MODEL_COLORS <- c( "SIM" = "#CC0000", "CIM" = "#FF3333", "MQM" = "#990000", "RF" = "#FF6600", "SVR" = "#CC3300", "XGBOOST" = "#FF0066" ) # ============================================================ # HELPERS # ============================================================ cn <- function(x) trimws(gsub('[\r\n\"]', '', as.character(x))) calc_genetic_distance <- function(marker1, marker2, marker_info) { m1 <- marker_info[marker_info$Marker == marker1, ] m2 <- marker_info[marker_info$Marker == marker2, ] if (nrow(m1) == 0 || nrow(m2) == 0) return(Inf) if (m1$Chromosome[1] == m2$Chromosome[1]) return(abs(m1$Position[1] - m2$Position[1])) return(Inf) } # Safe colour getter with fallback get_color <- function(input_val, fallback) { if (is.null(input_val) || nchar(trimws(input_val)) == 0) return(fallback) input_val } # ============================================================ # GENOTYPE LOADER # ============================================================ load_genotype_file <- function(filepath) { lines <- readLines(filepath, warn = FALSE) lines <- gsub("\r", "", lines) lines <- lines[nchar(trimws(lines)) > 0] if (length(lines) < 4) stop("Need >= 4 rows: header + chr + pos + >= 1 sample") header <- cn(strsplit(lines[1], ",")[[1]]) chr_row <- cn(strsplit(lines[2], ",")[[1]]) pos_row <- cn(strsplit(lines[3], ",")[[1]]) data_lines <- lines[-(1:3)] gd <- read.csv(text = paste(data_lines, collapse = "\n"), header = FALSE, stringsAsFactors = FALSE, quote = '"', fill = TRUE) if (ncol(gd) < length(header)) { for (k in (ncol(gd)+1):length(header)) gd[[paste0("V",k)]] <- NA } if (ncol(gd) > length(header)) gd <- gd[, 1:length(header)] colnames(gd) <- header rownames(gd) <- cn(as.character(gd[, 1])) gd <- gd[, -1, drop = FALSE] for (col in colnames(gd)) gd[[col]] <- suppressWarnings(as.numeric(as.character(gd[[col]]))) marker_info <- data.frame( Marker = colnames(gd), Chromosome = cn(chr_row[-1][seq_along(colnames(gd))]), Position = suppressWarnings(as.numeric(cn(pos_row[-1][seq_along(colnames(gd))]))), stringsAsFactors = FALSE ) marker_info <- marker_info[!is.na(marker_info$Position) & marker_info$Chromosome != "", ] valid_cols <- colnames(gd)[colnames(gd) %in% marker_info$Marker] gd <- gd[, valid_cols, drop = FALSE] list(geno = gd, marker_info = marker_info, n_samples = nrow(gd), n_markers = ncol(gd), chromosomes = sort(unique(marker_info$Chromosome))) } # ============================================================ # SIM # ============================================================ qtl_scan_sim <- function(X_valid, y_valid, marker_info) { results_list <- list() for (m in colnames(X_valid)) { geno <- X_valid[, m] valid_idx <- !is.na(geno) & !is.na(y_valid) geno_v <- geno[valid_idx]; pheno_v <- y_valid[valid_idx] if (length(unique(geno_v)) < 2 || length(pheno_v) < 8) next m_info <- marker_info[marker_info$Marker == m, ] if (nrow(m_info) == 0) next m_chr <- as.character(m_info$Chromosome[1]); m_pos <- m_info$Position[1] if (is.na(m_pos)) next tryCatch({ fit <- lm(pheno_v ~ geno_v) ct <- coef(summary(fit)) if (!"geno_v" %in% rownames(ct)) next p_val <- ct["geno_v", "Pr(>|t|)"]; lod <- -log10(max(p_val, 1e-300)) a_code <- geno_v - 1; d_code <- as.numeric(geno_v == 1) has_het <- length(unique(d_code)) > 1 lm_eff <- if (has_het) lm(pheno_v ~ a_code + d_code) else lm(pheno_v ~ a_code) pve_m <- max(0, summary(lm_eff)$r.squared * 100) ce <- coef(lm_eff); se_t <- coef(summary(lm_eff)) a_eff <- ce["a_code"] se_a <- if ("a_code" %in% rownames(se_t)) se_t["a_code", "Std. Error"] else NA d_eff <- if (has_het) ce["d_code"] else NA d_rat <- if (!is.na(d_eff) && !is.na(a_eff) && a_eff != 0) abs(d_eff / a_eff) else NA dom_cl <- if (is.na(d_rat)) "Additive" else if (d_rat > 1) "Overdominance" else if (d_rat > 0.5) "Dominance" else if (d_rat > 0.2) "Partial Dom." else "Additive" results_list[[m]] <- data.frame( Marker=m, Chromosome=m_chr, Position=m_pos, LOD=lod, P_Value=p_val, PVE_Marker_Percent=pve_m, PVE_CIM_Percent=pve_m, Additive_Effect=a_eff, SE_Additive=se_a, CI_Lower=ifelse(is.na(se_a),NA,a_eff-1.96*se_a), CI_Upper=ifelse(is.na(se_a),NA,a_eff+1.96*se_a), Dominance_Effect=d_eff, Dominance_Ratio=d_rat, Dominance_Class=dom_cl, N_Cofactors=0, N_Samples=length(pheno_v), Method="SIM", stringsAsFactors=FALSE) }, error=function(e) {}) } if (length(results_list) == 0) return(data.frame(Message="No results from SIM")) res <- do.call(rbind, results_list) res[order(res$Chromosome, res$Position), ] } # ============================================================ # CIM # ============================================================ qtl_scan_cim_improved <- function(X_valid, y_valid, marker_info, window_size = 10, n_cofactors = 5) { cat("\n=== CIM ===\n") initial_pvals <- sapply(colnames(X_valid), function(m) { g <- X_valid[, m]; valid_idx <- !is.na(g) & !is.na(y_valid) if (sum(valid_idx) < 8 || length(unique(g[valid_idx])) < 2) return(1) tryCatch(coef(summary(lm(y_valid[valid_idx] ~ g[valid_idx])))[2, 4], error=function(e) 1) }) sorted_markers <- names(sort(initial_pvals)) cofactors <- character(0) min_distance <- 20 for (candidate in sorted_markers) { if (length(cofactors) >= n_cofactors) break if (initial_pvals[candidate] > 0.01) break too_close <- FALSE if (length(cofactors) > 0) { for (existing_cof in cofactors) { if (calc_genetic_distance(candidate, existing_cof, marker_info) < min_distance) { too_close <- TRUE; break } } } if (!too_close) cofactors <- c(cofactors, candidate) } results_list <- list() for (m in colnames(X_valid)) { geno <- X_valid[, m]; valid_idx <- !is.na(geno) & !is.na(y_valid) geno_v <- geno[valid_idx]; pheno_v <- y_valid[valid_idx] if (length(unique(geno_v)) < 2 || length(pheno_v) < 8) next m_info <- marker_info[marker_info$Marker == m, ] if (nrow(m_info) == 0) next m_chr <- as.character(m_info$Chromosome[1]); m_pos <- m_info$Position[1] if (is.na(m_pos)) next chr_markers <- marker_info[marker_info$Chromosome == m_chr, ] chr_markers <- chr_markers[order(chr_markers$Position), ] focal_idx <- which(chr_markers$Marker == m) exclude_markers <- character(0) if (length(focal_idx) > 0) { ws <- max(1, focal_idx - window_size); we <- min(nrow(chr_markers), focal_idx + window_size) exclude_markers <- chr_markers$Marker[ws:we] } active_cof <- setdiff(cofactors, c(m, exclude_markers)) active_cof <- active_cof[active_cof %in% colnames(X_valid)] tryCatch({ df <- data.frame(y = pheno_v, focal = geno_v) if (length(active_cof) > 0) { cof_matrix <- X_valid[valid_idx, active_cof, drop=FALSE] cof_matrix[is.na(cof_matrix)] <- 0 df <- cbind(df, cof_matrix) } df <- df[complete.cases(df), ] if (nrow(df) < 8) return(NULL) if (length(active_cof) > 0) { fit_full <- lm(as.formula(paste("y ~ focal +", paste(active_cof, collapse=" + "))), data=df) fit_reduced <- lm(as.formula(paste("y ~", paste(active_cof, collapse=" + "))), data=df) } else { fit_full <- lm(y ~ focal, data=df); fit_reduced <- lm(y ~ 1, data=df) } anova_result <- anova(fit_reduced, fit_full) p_val <- anova_result[2, "Pr(>F)"]; lod <- -log10(max(p_val, 1e-300)) ct <- coef(summary(fit_full)) if (!"focal" %in% rownames(ct)) return(NULL) RSS_full <- sum(residuals(fit_full)^2); RSS_reduced <- sum(residuals(fit_reduced)^2) pve_cim <- if (RSS_reduced > 0) max(0, (RSS_reduced - RSS_full) / RSS_reduced * 100) else NA a_code <- df$focal - 1; d_code <- as.numeric(df$focal == 1) has_het <- length(unique(d_code)) > 1 lm_effects <- if (has_het) lm(df$y ~ a_code + d_code) else lm(df$y ~ a_code) pve_marker <- max(0, summary(lm_effects)$r.squared * 100) ce <- coef(lm_effects); se_table <- coef(summary(lm_effects)) a_eff <- ce["a_code"] se_a <- if ("a_code" %in% rownames(se_table)) se_table["a_code", "Std. Error"] else NA d_eff <- if (has_het) ce["d_code"] else NA d_ratio <- if (!is.na(d_eff) && !is.na(a_eff) && a_eff != 0) abs(d_eff / a_eff) else NA dom_class <- if (is.na(d_ratio)) "Additive" else if (d_ratio > 1) "Overdominance" else if (d_ratio > 0.5) "Dominance" else if (d_ratio > 0.2) "Partial Dom." else "Additive" results_list[[m]] <- data.frame( Marker=m, Chromosome=m_chr, Position=m_pos, LOD=lod, P_Value=p_val, PVE_Marker_Percent=pve_marker, PVE_CIM_Percent=pve_cim, Additive_Effect=a_eff, SE_Additive=se_a, CI_Lower=ifelse(is.na(se_a),NA,a_eff-1.96*se_a), CI_Upper=ifelse(is.na(se_a),NA,a_eff+1.96*se_a), Dominance_Effect=d_eff, Dominance_Ratio=d_ratio, Dominance_Class=dom_class, N_Cofactors=length(active_cof), N_Samples=nrow(df), Method="CIM", stringsAsFactors=FALSE) }, error=function(e) cat("CIM error at", m, ":", e$message, "\n")) } if (length(results_list) == 0) return(data.frame(Message="CIM produced no results")) res <- do.call(rbind, results_list) res[order(res$Chromosome, res$Position), ] } # ============================================================ # MQM # ============================================================ qtl_scan_mqm <- function(X, y, marker_info) { valid <- !is.na(y); X_v <- X[valid,,drop=FALSE]; y_v <- y[valid] p_vals <- sapply(colnames(X_v), function(m) { g <- X_v[,m]; v2 <- !is.na(g) if (length(unique(g[v2]))<2||sum(v2)<8) return(1) tryCatch(coef(summary(lm(y_v[v2]~g[v2])))[2,4],error=function(e) 1) }) selected <- character(0); threshold <- 0.01 for (cand in names(sort(p_vals))[1:min(20,length(p_vals))]) { test_model <- c(selected, cand) df_t <- data.frame(y=y_v, X_v[,test_model,drop=FALSE]) df_t <- df_t[complete.cases(df_t),] if (nrow(df_t)<10) next tryCatch({ fw <- lm(y~.,data=df_t) wo <- if(length(selected)>0) lm(as.formula(paste("y~",paste(selected,collapse="+"))),data=df_t) else lm(y~1,data=df_t) p <- anova(wo,fw)[2,"Pr(>F)"] if (!is.na(p)&&p0){cm<-X_v[v2,cof_m,drop=FALSE];cm[is.na(cm)]<-0;df2<-cbind(df2,cm)} df2 <- df2[complete.cases(df2),] if (nrow(df2)<8) return(NULL) ff <- if(length(cof_m)>0) lm(as.formula(paste("y~focal+",paste(cof_m,collapse="+"))),data=df2) else lm(y~focal,data=df2) fr <- if(length(cof_m)>0) lm(as.formula(paste("y~",paste(cof_m,collapse="+"))),data=df2) else lm(y~1,data=df2) ct2 <- coef(summary(ff)) if (!"focal"%in%rownames(ct2)) next p2 <- ct2["focal","Pr(>|t|)"]; lod2 <- -log10(max(p2,1e-300)) RF2 <- sum(residuals(ff)^2); RR2 <- sum(residuals(fr)^2) pvc <- if(RR2>0) max(0,(RR2-RF2)/RR2*100) else NA ac <- gv-1; dc2 <- as.numeric(gv==1); h2 <- length(unique(dc2))>1 le <- if(h2) lm(yv~ac+dc2) else lm(yv~ac) pvm <- max(0,summary(le)$r.squared*100) ce2 <- coef(le); se2 <- coef(summary(le)) ae <- ce2["ac"]; sea <- if("ac"%in%rownames(se2)) se2["ac","Std. Error"] else NA de <- if(h2) ce2["dc2"] else NA dr <- if(!is.na(de)&&!is.na(ae)&&ae!=0) abs(de/ae) else NA dc3 <- if(is.na(dr)) "Additive" else if(dr>1) "Overdominance" else if(dr>0.5) "Dominance" else if(dr>0.2) "Partial Dom." else "Additive" results_list[[m]] <- data.frame( Marker=m,Chromosome=as.character(mi$Chromosome[1]),Position=mi$Position[1], LOD=lod2,P_Value=p2,PVE_Marker_Percent=pvm,PVE_CIM_Percent=pvc, Additive_Effect=ae,SE_Additive=sea, CI_Lower=ifelse(is.na(sea),NA,ae-1.96*sea),CI_Upper=ifelse(is.na(sea),NA,ae+1.96*sea), Dominance_Effect=de,Dominance_Ratio=dr,Dominance_Class=dc3, N_Cofactors=length(cof_m),N_Samples=nrow(df2),Method="MQM",stringsAsFactors=FALSE) },error=function(e){}) } if (length(results_list)==0) return(data.frame(Message="MQM no results")) res <- do.call(rbind,results_list); res[order(res$Chromosome,res$Position),] } # ============================================================ # RF # ============================================================ qtl_scan_rf <- function(X, y, marker_info) { valid <- !is.na(y)&apply(X,1,function(r) !any(is.na(r))) X_v <- X[valid,,drop=FALSE]; y_v <- y[valid] rf_fit <- tryCatch(randomForest(x=X_v,y=y_v,ntree=500,importance=TRUE),error=function(e) NULL) if (is.null(rf_fit)) return(data.frame(Message="Random Forest failed")) imp <- importance(rf_fit,type=1)[,1] imp_df <- data.frame(Marker=names(imp),Importance=imp) imp_df <- imp_df[imp_df$Importance>0,] if (nrow(imp_df)==0) return(data.frame(Message="No important markers")) imp_df <- merge(imp_df,marker_info,by="Marker",all.x=TRUE) imp_df <- imp_df[!is.na(imp_df$Position),] max_imp <- max(imp_df$Importance); imp_df$LOD <- imp_df$Importance/max_imp*10 r2_vec <- sapply(imp_df$Marker,function(m){ g<-X_v[,m];v<-!is.na(g);if(sum(v)<5) return(NA) summary(lm(y_v[v]~g[v]))$r.squared*100}) imp_df$PVE_Marker_Percent<-r2_vec; imp_df$PVE_CIM_Percent<-r2_vec imp_df$Additive_Effect<-NA; imp_df$P_Value<-10^(-imp_df$LOD) imp_df$SE_Additive<-NA; imp_df$CI_Lower<-NA; imp_df$CI_Upper<-NA imp_df$Dominance_Effect<-NA; imp_df$Dominance_Ratio<-NA imp_df$Dominance_Class<-"Additive"; imp_df$N_Cofactors<-0 imp_df$N_Samples<-sum(valid); imp_df$Method<-"RF" imp_df <- imp_df[order(imp_df$Chromosome,imp_df$Position),] imp_df[,c("Marker","Chromosome","Position","LOD","P_Value","PVE_Marker_Percent","PVE_CIM_Percent", "Additive_Effect","SE_Additive","CI_Lower","CI_Upper","Dominance_Effect", "Dominance_Ratio","Dominance_Class","N_Cofactors","N_Samples","Method")] } # ============================================================ # SVR # ============================================================ qtl_scan_svr <- function(X, y, marker_info) { valid <- !is.na(y)&apply(X,1,function(r) !any(is.na(r))) X_v <- X[valid,,drop=FALSE]; y_v <- y[valid] svr_fit <- tryCatch(svm(x=X_v,y=y_v,kernel="radial",scale=TRUE),error=function(e) NULL) if (is.null(svr_fit)) return(data.frame(Message="SVR failed")) baseline_pred <- predict(svr_fit,X_v) baseline_error <- mean((y_v-baseline_pred)^2) importance <- sapply(colnames(X_v),function(m){ X_perm <- X_v; X_perm[,m] <- sample(X_perm[,m]) perm_pred <- predict(svr_fit,X_perm) perm_error <- mean((y_v-perm_pred)^2) (perm_error-baseline_error)/baseline_error}) imp_df <- data.frame(Marker=names(importance),Importance=importance) imp_df <- imp_df[imp_df$Importance>0,] if (nrow(imp_df)==0) return(data.frame(Message="No important markers")) imp_df <- merge(imp_df,marker_info,by="Marker",all.x=TRUE) imp_df <- imp_df[!is.na(imp_df$Position),] max_imp <- max(imp_df$Importance); imp_df$LOD <- imp_df$Importance/max_imp*10 r2_vec <- sapply(imp_df$Marker,function(m){ g<-X_v[,m];v<-!is.na(g);if(sum(v)<5) return(NA) summary(lm(y_v[v]~g[v]))$r.squared*100}) imp_df$PVE_Marker_Percent<-r2_vec; imp_df$PVE_CIM_Percent<-r2_vec imp_df$Additive_Effect<-NA; imp_df$P_Value<-10^(-imp_df$LOD) imp_df$SE_Additive<-NA; imp_df$CI_Lower<-NA; imp_df$CI_Upper<-NA imp_df$Dominance_Effect<-NA; imp_df$Dominance_Ratio<-NA imp_df$Dominance_Class<-"Additive"; imp_df$N_Cofactors<-0 imp_df$N_Samples<-sum(valid); imp_df$Method<-"SVR" imp_df <- imp_df[order(imp_df$Chromosome,imp_df$Position),] imp_df[,c("Marker","Chromosome","Position","LOD","P_Value","PVE_Marker_Percent","PVE_CIM_Percent", "Additive_Effect","SE_Additive","CI_Lower","CI_Upper","Dominance_Effect", "Dominance_Ratio","Dominance_Class","N_Cofactors","N_Samples","Method")] } # ============================================================ # XGBOOST # ============================================================ qtl_scan_xgboost <- function(X, y, marker_info) { valid <- !is.na(y)&apply(X,1,function(r) !any(is.na(r))) X_v <- X[valid,,drop=FALSE]; y_v <- y[valid] dtrain <- xgb.DMatrix(data=as.matrix(X_v),label=y_v) xgb_fit <- tryCatch(xgb.train( data=dtrain,nrounds=100,objective="reg:squarederror",verbose=0, params=list(max_depth=3,eta=0.1,subsample=0.8,colsample_bytree=0.8)), error=function(e) NULL) if (is.null(xgb_fit)) return(data.frame(Message="XGBoost failed")) importance_matrix <- xgb.importance(model=xgb_fit) imp_df <- data.frame(Marker=importance_matrix$Feature,Importance=importance_matrix$Gain) imp_df <- imp_df[imp_df$Importance>0,] if (nrow(imp_df)==0) return(data.frame(Message="No important markers")) imp_df <- merge(imp_df,marker_info,by="Marker",all.x=TRUE) imp_df <- imp_df[!is.na(imp_df$Position),] max_imp <- max(imp_df$Importance); imp_df$LOD <- imp_df$Importance/max_imp*12 r2_vec <- sapply(imp_df$Marker,function(m){ g<-X_v[,m];v<-!is.na(g);if(sum(v)<5) return(NA) summary(lm(y_v[v]~g[v]))$r.squared*100}) imp_df$PVE_Marker_Percent<-r2_vec; imp_df$PVE_CIM_Percent<-r2_vec imp_df$Additive_Effect<-NA; imp_df$P_Value<-10^(-imp_df$LOD) imp_df$SE_Additive<-NA; imp_df$CI_Lower<-NA; imp_df$CI_Upper<-NA imp_df$Dominance_Effect<-NA; imp_df$Dominance_Ratio<-NA imp_df$Dominance_Class<-"Additive"; imp_df$N_Cofactors<-0 imp_df$N_Samples<-sum(valid); imp_df$Method<-"XGBOOST" imp_df <- imp_df[order(imp_df$Chromosome,imp_df$Position),] imp_df[,c("Marker","Chromosome","Position","LOD","P_Value","PVE_Marker_Percent","PVE_CIM_Percent", "Additive_Effect","SE_Additive","CI_Lower","CI_Upper","Dominance_Effect", "Dominance_Ratio","Dominance_Class","N_Cofactors","N_Samples","Method")] } # ============================================================ # MAIN DISPATCHER # ============================================================ qtl_scan <- function(genotype_matrix, phenotype_vector, marker_info, method="CIM", n_cofactors=10, window_size=10) { cat("\n=== QTL SCAN === Method:", method, "\n") valid_rows <- !is.na(phenotype_vector) X_valid <- genotype_matrix[valid_rows,,drop=FALSE] y_valid <- phenotype_vector[valid_rows] nzv <- apply(X_valid, 2, function(x) length(unique(na.omit(x))) < 2) X_valid <- X_valid[, !nzv, drop=FALSE] cat("Markers after NZV removal:", ncol(X_valid), "\n") if (method == "SIM") return(qtl_scan_sim(X_valid, y_valid, marker_info)) if (method == "CIM") return(qtl_scan_cim_improved(X_valid, y_valid, marker_info, window_size, n_cofactors)) if (method == "MQM") return(qtl_scan_mqm(X_valid, y_valid, marker_info)) if (method == "RF") return(qtl_scan_rf(X_valid, y_valid, marker_info)) if (method == "SVR") return(qtl_scan_svr(X_valid, y_valid, marker_info)) if (method == "XGBOOST") return(qtl_scan_xgboost(X_valid, y_valid, marker_info)) data.frame(Message=paste("Unknown method:", method)) } # ============================================================ # LOD SUPPORT INTERVAL # ============================================================ add_lod_support_interval <- function(sig_df, all_df, lod_drop=1.5) { sig_df$Low_Flank <- sig_df$High_Flank <- NA_character_ sig_df$Interval_Start <- sig_df$Interval_End <- sig_df$Interval_Size_cM <- NA_real_ for (i in 1:nrow(sig_df)) { m <- sig_df$Marker[i]; ch <- sig_df$Chromosome[i] peak_lod <- sig_df$LOD[i]; peak_pos <- sig_df$Position[i] chr_data <- all_df[as.character(all_df$Chromosome)==as.character(ch)&!is.na(all_df$LOD)&!is.na(all_df$Position),] chr_data <- chr_data[order(chr_data$Position),] if (nrow(chr_data)==0) next threshold <- peak_lod - lod_drop interval_markers <- chr_data[chr_data$LOD >= threshold,] if (nrow(interval_markers) > 0) { interval_start <- min(interval_markers$Position, peak_pos) interval_end <- max(interval_markers$Position, peak_pos) sig_df$Interval_Start[i] <- interval_start sig_df$Interval_End[i] <- interval_end sig_df$Interval_Size_cM[i] <- interval_end - interval_start below_markers <- chr_data[chr_data$Position < interval_start,] above_markers <- chr_data[chr_data$Position > interval_end,] sig_df$Low_Flank[i] <- if(nrow(below_markers)>0) below_markers$Marker[nrow(below_markers)] else m sig_df$High_Flank[i] <- if(nrow(above_markers)>0) above_markers$Marker[1] else m } else { peak_idx <- which(chr_data$Marker==m) if (length(peak_idx)>0) { sig_df$Low_Flank[i] <- if(peak_idx>1) chr_data$Marker[peak_idx-1] else m sig_df$High_Flank[i] <- if(peak_idx= lod_threshold, ] if (nrow(sig_qtls)==0) next col_m <- model_colors[trait] if (is.na(col_m)) col_m <- "#CC0000" x_off <- x_center + bar_w/2 + 0.08 + (ti-1)*0.22 for (ri in 1:nrow(sig_qtls)) { pos <- sig_qtls$Position[ri] lod_val <- sig_qtls$LOD[ri] segments(x_center + bar_w/2, pos, x_off - 0.04, pos, col=col_m, lwd=1.5) points(x_off, pos, pch=16, col=col_m, cex=pmax(qtl_size_min, pmin(qtl_size_max, 0.6 + lod_val/8))) text(x_off + 0.15, pos, paste0(round(lod_val,1)), cex=0.55, col=col_m, font=2, adj=0) } } text(x_center, chr_min - max_pos*0.04, paste0("Chr", chr), cex=0.72, font=2, col="#1A1A1A", adj=0.5) } legend_x <- n_chr * x_gap + 0.5 legend_y <- max_pos * 0.95 text(legend_x, legend_y + max_pos*0.07, "Models", cex=0.75, font=2, col="#1A1A1A", adj=0) for (ti in seq_along(model_names)) { tr <- model_names[ti] col_m <- model_colors[tr]; if (is.na(col_m)) col_m <- "#CC0000" y_leg <- legend_y - (ti-1)*max_pos*0.07 qtl_d <- qtl_list[[tr]] n_sig <- if(!is.null(qtl_d)&&!"Message"%in%colnames(qtl_d)) sum(!is.na(qtl_d$LOD)&qtl_d$LOD>=lod_threshold,na.rm=TRUE) else 0 points(legend_x, y_leg, pch=16, col=col_m, cex=1.2) text(legend_x + 0.18, y_leg, paste0(tr, " (", n_sig, " QTL)"), cex=0.65, col=col_m, font=1, adj=0) } ttl <- if (!is.null(title_str)) title_str else paste0("QTL Chromosome Map | LOD โ‰ฅ ", lod_threshold) title(main=ttl, cex.main=0.95, font.main=2, col.main="#1A1A1A", line=2) } # ============================================================ # CHROMOSOME-WISE MANHATTAN PLOT (PLOTLY) โ€” FULLY CUSTOMIZABLE # ============================================================ make_plotly_manhattan_chromosomewise <- function(scan_all, lod_threshold=2.0, title="QTL Manhattan Plot", nonsig_color="#3A7D44", sig_color="#CC0000", point_size=6, sig_point_size=10, threshold_line_width=1.5, threshold_line_style="dash") { blank <- plot_ly() %>% layout(title=title, plot_bgcolor="white", paper_bgcolor="white") if (is.null(scan_all) || "Message" %in% colnames(scan_all)) return(blank) pd <- scan_all[!is.na(scan_all$LOD) & !is.na(scan_all$Position) & !is.na(scan_all$Chromosome), ] if (nrow(pd) == 0) return(blank) pd$Chromosome <- as.character(pd$Chromosome) pd$sig <- pd$LOD >= lod_threshold chroms <- sort(unique(pd$Chromosome)) subplot_list <- list() for (chr in chroms) { chr_data <- pd[pd$Chromosome == chr, ] chr_data <- chr_data[order(chr_data$Position), ] nonsig <- chr_data[!chr_data$sig, ] sig_data <- chr_data[chr_data$sig, ] p <- plot_ly() if (nrow(nonsig) > 0) { p <- p %>% add_trace( data = nonsig, x = ~Position, y = ~LOD, type = "scatter", mode = "markers", marker = list(size = point_size, color = nonsig_color, opacity = 0.7), text = ~paste0("", Marker, "
Chr: ", Chromosome, "
Pos: ", round(Position, 1), " cM
LOD: ", round(LOD, 2), "
PVE: ", round(PVE_Marker_Percent, 1), "%"), hoverinfo = "text", showlegend = FALSE, name = paste("Chr", chr) ) } if (nrow(sig_data) > 0) { p <- p %>% add_trace( data = sig_data, x = ~Position, y = ~LOD, type = "scatter", mode = "markers", marker = list(size = sig_point_size, color = sig_color, symbol = "diamond", line = list(width = 1, color = "#660000")), text = ~paste0("", Marker, "
Chr: ", Chromosome, "
Pos: ", round(Position, 1), " cM
LOD: ", round(LOD, 2), "
PVE: ", round(PVE_Marker_Percent, 1), "%"), hoverinfo = "text", showlegend = FALSE, name = paste("Chr", chr, "Significant") ) } p <- p %>% add_segments( x = min(chr_data$Position, na.rm=TRUE), xend = max(chr_data$Position, na.rm=TRUE), y = lod_threshold, yend = lod_threshold, line = list(color = sig_color, dash = threshold_line_style, width = threshold_line_width), showlegend = FALSE, hoverinfo = "none" ) p <- p %>% layout( xaxis = list(title = paste("Chr", chr, "Position (cM)"), gridcolor = "#EEEEEE", zeroline = FALSE), yaxis = list(title = if(chr == chroms[1]) "LOD Score" else "", gridcolor = "#EEEEEE", zeroline = FALSE), plot_bgcolor = "white", paper_bgcolor = "white" ) subplot_list[[chr]] <- p } n_chroms <- length(chroms) n_cols <- ceiling(sqrt(n_chroms)) n_rows <- ceiling(n_chroms / n_cols) combined_plot <- subplot( subplot_list, nrows = n_rows, shareY = TRUE, titleX = TRUE, titleY = TRUE, margin = 0.05 ) combined_plot %>% layout( title = list( text = paste0(title, " | ", sum(pd$sig, na.rm=TRUE), " significant QTLs"), font = list(size = 14, color = "#111111") ), showlegend = FALSE, font = list(color = "#111111"), hoverlabel = list(bgcolor = "#FFFFFF", font = list(color = "#111111", size = 11)) ) } # ============================================================ # COMMON QTL FINDER # ============================================================ find_common_qtl <- function(compare_res, lod_threshold, min_models=2, window_cM=5) { sig_list <- list() for (mth in names(compare_res)) { r <- compare_res[[mth]] if (is.null(r)||"Message"%in%colnames(r)) next sig <- r[!is.na(r$LOD)&r$LOD>=lod_threshold, c("Marker","Chromosome","Position","LOD","PVE_Marker_Percent","Additive_Effect"),drop=FALSE] if (nrow(sig)>0){sig$Model<-mth;sig_list[[mth]]<-sig} } if (length(sig_list)==0) return(NULL) all_sig <- do.call(rbind,sig_list) all_sig$Chromosome <- as.character(all_sig$Chromosome) marker_counts <- table(all_sig$Marker) all_sig$N_Models_Detected <- as.integer(marker_counts[all_sig$Marker]) common <- all_sig[all_sig$N_Models_Detected>=min_models,] if (nrow(common)==0) return(NULL) markers_uniq <- unique(common$Marker) summary_rows <- lapply(markers_uniq, function(mk) { rows <- common[common$Marker==mk,] base <- rows[which.max(rows$LOD), c("Marker","Chromosome","Position","N_Models_Detected")] base$Models_List <- paste(sort(unique(rows$Model)), collapse=", ") base$Mean_LOD <- round(mean(rows$LOD, na.rm=TRUE), 3) base$Max_LOD <- round(max(rows$LOD, na.rm=TRUE), 3) base$Mean_PVE <- round(mean(rows$PVE_Marker_Percent, na.rm=TRUE), 3) base$Mean_Add_Eff <- round(mean(rows$Additive_Effect, na.rm=TRUE), 3) for (mth in names(compare_res)) { r_mth <- rows[rows$Model==mth,] base[[paste0("LOD_",mth)]] <- if(nrow(r_mth)>0) round(r_mth$LOD[1],3) else NA } base }) result <- do.call(rbind, summary_rows) result[order(-result$N_Models_Detected, -result$Max_LOD),] } # ============================================================ # UI # ============================================================ ui <- fluidPage( shinyjs::useShinyjs(), tags$head( tags$link(rel="stylesheet", href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"), tags$style(HTML(" *{box-sizing:border-box;} body{font-family:'Inter',sans-serif;background:#F7F7F5;color:#1A1A1A;font-size:14px;margin:0;} .navbar{background:#1A4A1F!important;border-bottom:none!important;} .navbar .navbar-brand,.navbar-default .navbar-brand{color:#FFFFFF!important;font-weight:700;} .navbar-default .navbar-nav>li>a{color:#D0E8D0!important;} .navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover{ background:#2E6B35!important;color:#FFFFFF!important;} .navbar-default .navbar-nav>li>a:hover{background:#2E6B35!important;color:#FFFFFF!important;} .card{background:#FFFFFF;border:1px solid #DDDDDD;border-radius:10px; margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);} .card-body{padding:16px;} .info-box{background:#EAF4EA;border:1px solid #A8D4A8;border-left:4px solid #2E6B35; border-radius:6px;padding:10px 14px;margin:8px 0;font-size:13px;color:#1A4A1F;} .btn-primary{background:#2E6B35!important;border-color:#1A4A1F!important;color:#FFFFFF!important;} .btn-primary:hover{background:#1A4A1F!important;} .btn-success{background:#3A7D44!important;border-color:#2E6B35!important;color:#FFFFFF!important;} .btn-info{background:#4A8FA0!important;border-color:#2E6B35!important;color:#FFFFFF!important;} .btn-warning{background:#C8760A!important;border-color:#A05E05!important;color:#FFFFFF!important;} .btn-danger{background:#990000!important;border-color:#660000!important;color:#FFFFFF!important;} .download-row{padding:10px 0 6px 0;display:flex;flex-wrap:wrap;gap:8px;} label{font-weight:500;color:#1A1A1A;} .shiny-input-container{margin-bottom:12px;} h4{color:#1A4A1F;font-weight:600;} .section-title{font-size:13px;font-weight:600;color:#2E6B35;margin-bottom:6px; border-bottom:1px solid #CCDDCC;padding-bottom:4px;} .custom-section{background:#F9FBFA;padding:12px;border-radius:6px;margin:8px 0; border:1px solid #D5E8D5;} ")) ), navbarPage( title = "๐ŸŒฟ QTL-Mapper v7 Enhanced", id = "nav", # โ”€โ”€ DATA UPLOAD โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tabPanel("๐Ÿ“ Data Upload", fluidRow( column(4, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ“‚ Input Files"), fileInput("geno_file","Genotype CSV",accept=".csv", placeholder="CSV: row1=header, row2=chr, row3=pos"), fileInput("pheno_file","Phenotype CSV",accept=".csv"), selectInput("pheno_col","Trait / Phenotype",choices=NULL) )) ), column(4, div(class="card", div(class="card-body", div(class="section-title","โš™๏ธ Analysis Settings"), selectInput("mapping_method","Mapping Method", choices=setNames(names(MAPPING_MODELS),sapply(MAPPING_MODELS,`[[`,"label")), selected="CIM"), sliderInput("lod_threshold","LOD Threshold",min=0.5,max=10,value=2.5,step=0.1), sliderInput("val_prop","Validation Fraction",min=0.1,max=0.4,value=0.2,step=0.05), conditionalPanel("input.mapping_method == 'CIM'", numericInput("n_cofactors","Cofactors (K)",value=5,min=1,max=20), numericInput("window_size","Window (ยฑ markers)",value=10,min=3,max=30) ), numericInput("lod_drop","LOD Drop (support interval)",value=1.0,min=0.3,max=2.0,step=0.1), br(), actionButton("run_btn","โ–ถ Run Analysis",class="btn btn-primary",style="width:100%;") )) ), column(4, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ“Š Dataset Info"), uiOutput("dataset_info_ui"), br(), div(class="info-box", "6 models: SIM ยท CIM ยท MQM ยท RF ยท SVR ยท XGBoost",br(), "Chromosome-wise Manhattan plots (no marker labels)",br(), "Full plot customization available in Plot Settings tab" ) )) ) ) ), # โ”€โ”€ PLOT CUSTOMIZATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tabPanel("๐ŸŽจ Plot Settings", fluidRow( column(6, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ“Š Manhattan Plot Customization"), div(class="custom-section", h5("๐Ÿ“ Plot Dimensions"), sliderInput("manhattan_height","Plot Height (pixels)",min=400,max=1200,value=800,step=50), sliderInput("manhattan_width","Plot Width (pixels)",min=800,max=2000,value=1400,step=100) ), div(class="custom-section", h5("๐ŸŽฏ Point Styling"), sliderInput("nonsig_point_size","Non-Significant Point Size",min=2,max=12,value=6,step=1), sliderInput("sig_point_size","Significant Point Size",min=4,max=20,value=10,step=1), colourpicker::colourInput("nonsig_color","Non-Significant Color","#3A7D44"), colourpicker::colourInput("sig_color","Significant Color","#CC0000") ), div(class="custom-section", h5("๐Ÿ“ Threshold Line"), sliderInput("threshold_width","Threshold Line Width",min=0.5,max=4,value=1.5,step=0.5), selectInput("threshold_style","Threshold Line Style", choices=c("Solid"="solid","Dashed"="dash","Dotted"="dot","Dash-Dot"="dashdot"), selected="dash") ) )) ), column(6, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ—บ Ideogram Plot Customization"), div(class="custom-section", h5("๐Ÿ“ Plot Dimensions"), sliderInput("ideogram_height","Plot Height (pixels)",min=400,max=1200,value=900,step=50), sliderInput("ideogram_width","Plot Width (pixels)",min=800,max=2000,value=1400,step=100) ), div(class="custom-section", h5("๐ŸŽจ Chromosome Colors"), colourpicker::colourInput("chr_dark","Dark Band Color","#2E6B35"), colourpicker::colourInput("chr_light","Light Band Color","#6AAB6E"), colourpicker::colourInput("chr_border","Border Color","#1A4A1F") ), div(class="custom-section", h5("๐Ÿ”ด QTL Marker Sizing"), sliderInput("qtl_marker_size_min","Min Marker Size",min=0.3,max=1.5,value=0.7,step=0.1), sliderInput("qtl_marker_size_max","Max Marker Size",min=1.0,max=3.0,value=2.0,step=0.1) ) )) ) ) ), # โ”€โ”€ RESULTS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tabPanel("๐Ÿ“Š Results", div(class="download-row", downloadButton("dl_manhattan_png","๐Ÿ–ผ Manhattan (PNG)",class="btn btn-info"), downloadButton("dl_idiogram_png","๐Ÿ—บ Ideogram (PNG)",class="btn btn-info"), downloadButton("dl_qtl_csv","๐Ÿ“ฅ QTL Table (CSV)",class="btn btn-success"), downloadButton("dl_full_csv","๐Ÿ“ฅ Full Scan (CSV)",class="btn btn-warning") ), fluidRow( column(12, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ“Š Chromosome-wise Manhattan Plot"), # Dynamic height driven by slider via uiOutput uiOutput("manhattan_plot_ui") )) ) ), fluidRow( column(12, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ—บ Chromosome Ideogram"), uiOutput("idiogram_plot_ui") )) ) ), fluidRow(column(12, div(class="card", div(class="card-body", div(class="section-title","๐Ÿ“‹ Significant QTL Table"), DTOutput("qtl_table") )) )) ), # โ”€โ”€ COMPARE MODELS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tabPanel("โš–๏ธ Compare Models", div(class="card", div(class="card-body", div(class="section-title","Select Models to Compare"), checkboxGroupInput("compare_methods","", choices=setNames(names(MAPPING_MODELS),sapply(MAPPING_MODELS,`[[`,"label")), selected=c("SIM","CIM","MQM"),inline=TRUE), actionButton("run_compare","๐Ÿš€ Run Comparison",class="btn btn-primary"), uiOutput("compare_status_ui") )), div(class="download-row", downloadButton("dl_compare_idiogram_png","๐Ÿ–ผ Compare Ideogram (PNG)",class="btn btn-info"), downloadButton("dl_compare_bar_png","๐Ÿ“Š Compare Bar (PNG)",class="btn btn-info"), downloadButton("dl_compare_csv","๐Ÿ“ฅ Compare Table (CSV)",class="btn btn-success") ), fluidRow( column(6, div(class="card", div(class="card-body", div(class="section-title","Multi-Model Chromosome Ideogram"), plotOutput("compare_ideogram",height="520px") )) ), column(6, div(class="card", div(class="card-body", div(class="section-title","QTL Counts per Model"), plotlyOutput("compare_bar",height="520px") )) ) ), fluidRow(column(12, div(class="card", div(class="card-body", div(class="section-title","Full Comparison Table"), DTOutput("compare_table") )) )) ), # โ”€โ”€ COMMON QTL โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tabPanel("๐ŸŽฏ Common QTL", div(class="card", div(class="card-body", div(class="section-title","Common QTL Across Models"), sliderInput("min_models","Minimum Models Detecting QTL",min=2,max=6,value=2,step=1,width="400px"), uiOutput("common_qtl_summary_ui") )), div(class="download-row", downloadButton("dl_common_idiogram_png","๐Ÿ–ผ Common Ideogram (PNG)",class="btn btn-info"), downloadButton("dl_common_csv","๐Ÿ“ฅ Common QTL (CSV)",class="btn btn-success") ), fluidRow( column(6, div(class="card", div(class="card-body", div(class="section-title","Common QTL Ideogram"), plotOutput("common_ideogram",height="500px") )) ), column(6, div(class="card", div(class="card-body", div(class="section-title","Common QTL Counts"), plotlyOutput("common_bar",height="500px") )) ) ), fluidRow(column(12, div(class="card", div(class="card-body", div(class="section-title","Common QTL Table"), DTOutput("common_qtl_table") )) )) ) ) ) # ============================================================ # SERVER # ============================================================ server <- function(input, output, session) { rv <- reactiveValues( geno=NULL, pheno=NULL, marker_info=NULL, all_markers=NULL, scan_all=NULL, compare_res=list() ) lod_thr <- reactive(input$lod_threshold) # โ”€โ”€ Safe color helpers (with fallbacks) โ”€โ”€ col_nonsig <- reactive(get_color(input$nonsig_color, "#3A7D44")) col_sig <- reactive(get_color(input$sig_color, "#CC0000")) col_chr_dark <- reactive(get_color(input$chr_dark, "#2E6B35")) col_chr_light <- reactive(get_color(input$chr_light, "#6AAB6E")) col_chr_border <- reactive(get_color(input$chr_border, "#1A4A1F")) # โ”€โ”€ DATASET INFO โ”€โ”€ output$dataset_info_ui <- renderUI({ if (is.null(rv$geno)) return(div(class="info-box","๐Ÿ“‚ No genotype file loaded")) tagList( div(class="info-box", tags$b(nrow(rv$geno)), " Samples ยท ", tags$b(ncol(rv$geno)), " Markers ยท ", tags$b(length(unique(rv$marker_info$Chromosome))), " Chromosomes" ) ) }) # โ”€โ”€ LOAD GENOTYPE โ”€โ”€ observeEvent(input$geno_file, { req(input$geno_file) tryCatch({ res <- load_genotype_file(input$geno_file$datapath) rv$geno <- res$geno; rv$marker_info <- res$marker_info; rv$all_markers <- res$marker_info showNotification(paste("โœ… Loaded",res$n_markers,"markers,", length(res$chromosomes),"chromosomes"),type="message") }, error=function(e) showNotification(paste("โŒ",e$message),type="error")) }) # โ”€โ”€ LOAD PHENOTYPE โ”€โ”€ observeEvent(input$pheno_file, { req(input$pheno_file) tryCatch({ pd <- read.csv(input$pheno_file$datapath) rownames(pd) <- pd[,1]; pd <- pd[,-1,drop=FALSE] for (col in colnames(pd)) pd[[col]] <- as.numeric(as.character(pd[[col]])) rv$pheno <- pd updateSelectInput(session,"pheno_col",choices=colnames(pd),selected=colnames(pd)[1]) showNotification(paste("โœ… Loaded",ncol(pd),"traits"),type="message") }, error=function(e) showNotification(paste("โŒ",e$message),type="error")) }) # โ”€โ”€ RUN ANALYSIS โ”€โ”€ observeEvent(input$run_btn, { req(rv$geno, rv$pheno, input$pheno_col) showNotification(paste("๐Ÿš€ Running",input$mapping_method,"..."),type="message",duration=5) tryCatch({ cs <- intersect(rownames(rv$geno),rownames(rv$pheno)) X <- rv$geno[cs,,drop=FALSE] y <- as.numeric(rv$pheno[cs,input$pheno_col]) set.seed(42) tr_idx <- caret::createDataPartition(y,p=1-input$val_prop,list=FALSE) X_tr <- X[tr_idx,,drop=FALSE]; y_tr <- y[tr_idx] withProgress(message=paste("Scanning genome with",input$mapping_method),value=0.1,{ rv$scan_all <- qtl_scan(X_tr, y_tr, rv$marker_info, method=input$mapping_method, n_cofactors=input$n_cofactors, window_size=input$window_size) incProgress(0.9) }) n_sig <- if(!is.null(rv$scan_all)&&!"Message"%in%colnames(rv$scan_all)) sum(rv$scan_all$LOD>=lod_thr()&!is.na(rv$scan_all$LOD),na.rm=TRUE) else 0 showNotification(paste("โœ…",n_sig,"significant QTLs detected"),type="message") updateTabsetPanel(session,"nav",selected="๐Ÿ“Š Results") }, error=function(e) showNotification(paste("โŒ",e$message),type="error")) }) # โ”€โ”€ RUN COMPARISON โ”€โ”€ observeEvent(input$run_compare, { req(rv$geno, rv$pheno, input$compare_methods) cs <- intersect(rownames(rv$geno),rownames(rv$pheno)) X <- rv$geno[cs,,drop=FALSE] y <- as.numeric(rv$pheno[cs,input$pheno_col]) set.seed(42) tr_idx <- caret::createDataPartition(y,p=1-input$val_prop,list=FALSE) X_tr <- X[tr_idx,,drop=FALSE]; y_tr <- y[tr_idx] compare_list <- list() withProgress(message="Comparing models",value=0,{ for (i in seq_along(input$compare_methods)) { mth <- input$compare_methods[i] incProgress(1/length(input$compare_methods),detail=mth) res <- tryCatch(qtl_scan(X_tr, y_tr, rv$marker_info, method=mth, n_cofactors=input$n_cofactors, window_size=input$window_size), error=function(e) NULL) if (!is.null(res)) compare_list[[mth]] <- res } }) rv$compare_res <- compare_list showNotification(paste("โœ… Compared",length(compare_list),"models"),type="message") }) output$compare_status_ui <- renderUI({ if (length(rv$compare_res)==0) return(NULL) div(class="info-box", tags$b(length(rv$compare_res)), " models compared: ", paste(names(rv$compare_res),collapse=" ยท ")) }) # โ”€โ”€ DYNAMIC UI FOR PLOTS (height/width from sliders) โ”€โ”€ output$manhattan_plot_ui <- renderUI({ h <- if(!is.null(input$manhattan_height)) input$manhattan_height else 800 plotlyOutput("manhattan_plot", height=paste0(h,"px")) }) output$idiogram_plot_ui <- renderUI({ h <- if(!is.null(input$ideogram_height)) input$ideogram_height else 900 plotOutput("idiogram_plot", height=paste0(h,"px")) }) # โ”€โ”€ MANHATTAN PLOT (interactive, fully customised) โ”€โ”€ output$manhattan_plot <- renderPlotly({ req(rv$scan_all) make_plotly_manhattan_chromosomewise( rv$scan_all, lod_thr(), paste(input$mapping_method, "Manhattan โ€” Trait:", input$pheno_col), nonsig_color = col_nonsig(), sig_color = col_sig(), point_size = input$nonsig_point_size, sig_point_size = input$sig_point_size, threshold_line_width = input$threshold_width, threshold_line_style = input$threshold_style ) }) # โ”€โ”€ IDEOGRAM PLOT (base R, fully customised) โ”€โ”€ output$idiogram_plot <- renderPlot({ req(rv$scan_all, rv$all_markers) res_list <- list() if (!is.null(rv$scan_all) && !"Message" %in% colnames(rv$scan_all)) res_list[[input$mapping_method]] <- rv$scan_all par(bg="white") make_chromosome_ideogram_reference( res_list, rv$all_markers, lod_thr(), title_str = paste(input$mapping_method, "โ€” Trait:", input$pheno_col), band_dark_col = col_chr_dark(), band_light_col = col_chr_light(), border_col = col_chr_border(), qtl_size_min = input$qtl_marker_size_min, qtl_size_max = input$qtl_marker_size_max ) }, bg="white", width = function() if(!is.null(input$ideogram_width)) input$ideogram_width else 1400, height = function() if(!is.null(input$ideogram_height)) input$ideogram_height else 900) # โ”€โ”€ COMPARE IDEOGRAM โ”€โ”€ output$compare_ideogram <- renderPlot({ req(length(rv$compare_res)>0, rv$all_markers) par(bg="white") make_chromosome_ideogram_reference( rv$compare_res, rv$all_markers, lod_thr(), title_str = "Multi-Model QTL Comparison", band_dark_col = col_chr_dark(), band_light_col = col_chr_light(), border_col = col_chr_border(), qtl_size_min = input$qtl_marker_size_min, qtl_size_max = input$qtl_marker_size_max ) }, bg="white") # โ”€โ”€ COMPARE BAR CHART โ”€โ”€ output$compare_bar <- renderPlotly({ req(length(rv$compare_res)>0) counts <- sapply(rv$compare_res, function(r) { if(is.null(r)||"Message"%in%colnames(r)) return(0) sum(!is.na(r$LOD)&r$LOD>=lod_thr(),na.rm=TRUE) }) df_bar <- data.frame(Model=names(counts),N_QTL=as.numeric(counts)) df_bar$Color <- MODEL_COLORS[df_bar$Model] df_bar$Color[is.na(df_bar$Color)] <- "#CC0000" plot_ly(df_bar, x=~Model, y=~N_QTL, type="bar", marker=list(color=~Color,line=list(color="#330000",width=1)), text=~N_QTL, textposition="outside", hovertext=~paste0(Model,": ",N_QTL," QTLs"), hoverinfo="text") %>% layout(title=list(text=paste0("QTLs per Model (LOD โ‰ฅ ",lod_thr(),")"), font=list(size=14)), xaxis=list(title="",color="#111111"), yaxis=list(title="# Significant QTLs",color="#111111"), plot_bgcolor="white",paper_bgcolor="white", font=list(color="#111111")) }) # โ”€โ”€ COMMON QTL โ”€โ”€ common_qtl_data <- reactive({ req(length(rv$compare_res)>0) find_common_qtl(rv$compare_res, lod_thr(), min_models=input$min_models) }) output$common_qtl_summary_ui <- renderUI({ cq <- common_qtl_data() if (is.null(cq)||nrow(cq)==0) return(div(class="info-box","โš ๏ธ No common QTLs โ€” run Compare Models first")) div(class="info-box", tags$b(nrow(cq)), " common QTLs detected across โ‰ฅ ", tags$b(input$min_models), " models") }) output$common_ideogram <- renderPlot({ cq <- common_qtl_data() req(!is.null(cq), nrow(cq)>0, rv$all_markers) cq_list <- list() for (mth in names(rv$compare_res)) { r <- rv$compare_res[[mth]] if (is.null(r)||"Message"%in%colnames(r)) next common_markers <- r[r$Marker %in% cq$Marker,] if (nrow(common_markers)>0) cq_list[[mth]] <- common_markers } par(bg="white") make_chromosome_ideogram_reference( cq_list, rv$all_markers, lod_thr(), title_str = "Common QTL (across โ‰ฅ2 models)", band_dark_col = col_chr_dark(), band_light_col = col_chr_light(), border_col = col_chr_border(), qtl_size_min = input$qtl_marker_size_min, qtl_size_max = input$qtl_marker_size_max ) }, bg="white") output$common_bar <- renderPlotly({ cq <- common_qtl_data() req(!is.null(cq), nrow(cq)>0) df_bar <- cq[order(-cq$Max_LOD),][1:min(20,nrow(cq)),] plot_ly(df_bar, x=~Max_LOD, y=~reorder(Marker,Max_LOD), type="bar", orientation="h", marker=list(color="#CC0000",line=list(color="#660000",width=1)), text=~paste0(Marker," | ",N_Models_Detected," models"), hovertext=~paste0(Marker,"
Chr ",Chromosome," @ ",Position," cM
Max LOD: ",Max_LOD), hoverinfo="text") %>% layout(title=list(text="Top Common QTLs by Max LOD",font=list(size=14)), xaxis=list(title="Max LOD",color="#111111"), yaxis=list(title="",color="#111111"), plot_bgcolor="white",paper_bgcolor="white", font=list(color="#111111")) }) # โ”€โ”€ TABLES โ”€โ”€ output$qtl_table <- renderDT({ req(rv$scan_all) if ("Message"%in%colnames(rv$scan_all)) return(datatable(data.frame(Status=rv$scan_all$Message[1]))) sig <- rv$scan_all[!is.na(rv$scan_all$LOD)&rv$scan_all$LOD>=lod_thr(),,drop=FALSE] if (nrow(sig)==0) return(datatable(data.frame(Status="No significant QTLs at current threshold"))) sig <- add_lod_support_interval(sig,rv$scan_all,input$lod_drop) sig <- sig[order(-sig$LOD),] datatable(sig,options=list(scrollX=TRUE,pageLength=15),rownames=FALSE,class="compact stripe") }) output$compare_table <- renderDT({ req(length(rv$compare_res)>0) all_res <- lapply(names(rv$compare_res), function(mth){ r <- rv$compare_res[[mth]] if(is.null(r)||"Message"%in%colnames(r)) return(NULL) sig <- r[!is.na(r$LOD)&r$LOD>=lod_thr(),,drop=FALSE] if(nrow(sig)>0){sig$Model<-mth;sig} else NULL }) all_res <- do.call(rbind, Filter(Negate(is.null), all_res)) if (is.null(all_res)||nrow(all_res)==0) return(datatable(data.frame(Status="No significant QTLs"))) datatable(all_res[order(-all_res$LOD),],options=list(scrollX=TRUE,pageLength=15), rownames=FALSE,class="compact stripe") }) output$common_qtl_table <- renderDT({ cq <- common_qtl_data() if(is.null(cq)||nrow(cq)==0) return(datatable(data.frame(Status="Run Compare Models first"))) datatable(cq,options=list(scrollX=TRUE,pageLength=15),rownames=FALSE,class="compact stripe") }) # ============================================================ # DOWNLOADS โ€” PNG with full custom settings # ============================================================ # Helper: save ideogram PNG with all custom colours/sizes save_ideogram_png <- function(qtl_list, markers, threshold, title_str, file, width, height) { png(file, width=width, height=height, res=150, bg="white") make_chromosome_ideogram_reference( qtl_list, markers, threshold, title_str = title_str, band_dark_col = col_chr_dark(), band_light_col = col_chr_light(), border_col = col_chr_border(), qtl_size_min = input$qtl_marker_size_min, qtl_size_max = input$qtl_marker_size_max ) dev.off() } # Helper: lty integer from style name (for base R PNG) lty_from_style <- function(style) { switch(style, "solid"=1, "dash"=2, "dot"=3, "dashdot"=4, 2) } # Manhattan PNG โ€” base R, chromosome-per-panel, all custom settings output$dl_manhattan_png <- downloadHandler( filename = function() paste0("manhattan_chromwise_",input$mapping_method,"_",Sys.Date(),".png"), content = function(file) { req(rv$scan_all) pd <- rv$scan_all[!is.na(rv$scan_all$LOD) & !is.na(rv$scan_all$Position) & !is.na(rv$scan_all$Chromosome),] if (nrow(pd)==0) { png(file,100,100); dev.off(); return() } pd$Chromosome <- as.character(pd$Chromosome) pd$sig <- pd$LOD >= lod_thr() chroms <- sort(unique(pd$Chromosome)) n_chroms <- length(chroms) n_cols <- ceiling(sqrt(n_chroms)) n_rows <- ceiling(n_chroms / n_cols) png(file, width=input$manhattan_width, height=input$manhattan_height, res=150, bg="white") par(mfrow=c(n_rows, n_cols), mar=c(4,4,3,1), bg="white") for (chr in chroms) { chr_data <- pd[pd$Chromosome==chr,] chr_data <- chr_data[order(chr_data$Position),] y_max <- max(chr_data$LOD, na.rm=TRUE) * 1.1 plot(chr_data$Position, chr_data$LOD, type="n", xlab=paste("Chr",chr,"Position (cM)"), ylab="LOD Score", main=paste("Chr",chr), ylim=c(0,y_max), frame.plot=FALSE, col.main="#111111", cex.main=0.9, cex.lab=0.8) nonsig <- chr_data[!chr_data$sig,] sig_data <- chr_data[chr_data$sig,] if (nrow(nonsig)>0) points(nonsig$Position, nonsig$LOD, pch=16, cex=input$nonsig_point_size/5, col=col_nonsig()) # Significant โ€” diamond, no text labels if (nrow(sig_data)>0) points(sig_data$Position, sig_data$LOD, pch=18, cex=input$sig_point_size/5, col=col_sig()) abline(h=lod_thr(), lty=lty_from_style(input$threshold_style), col=col_sig(), lwd=input$threshold_width) } dev.off() } ) # Ideogram PNG output$dl_idiogram_png <- downloadHandler( filename = function() paste0("idiogram_",input$mapping_method,"_",Sys.Date(),".png"), content = function(file) { req(rv$scan_all, rv$all_markers) res_list <- list() if (!is.null(rv$scan_all)&&!"Message"%in%colnames(rv$scan_all)) res_list[[input$mapping_method]] <- rv$scan_all save_ideogram_png(res_list, rv$all_markers, lod_thr(), paste(input$mapping_method,"โ€” Trait:",input$pheno_col), file, input$ideogram_width, input$ideogram_height) } ) # Compare ideogram PNG output$dl_compare_idiogram_png <- downloadHandler( filename = function() paste0("compare_idiogram_",Sys.Date(),".png"), content = function(file) { req(length(rv$compare_res)>0, rv$all_markers) save_ideogram_png(rv$compare_res, rv$all_markers, lod_thr(), "Multi-Model QTL Comparison", file, input$ideogram_width, input$ideogram_height) } ) # Compare bar PNG โ€” uses custom dimensions output$dl_compare_bar_png <- downloadHandler( filename = function() paste0("compare_bar_",Sys.Date(),".png"), content = function(file) { req(length(rv$compare_res)>0) counts <- sapply(rv$compare_res, function(r){ if(is.null(r)||"Message"%in%colnames(r)) return(0) sum(!is.na(r$LOD)&r$LOD>=lod_thr(),na.rm=TRUE) }) df_bar <- data.frame(Model=names(counts),N_QTL=as.numeric(counts)) df_bar$Color <- MODEL_COLORS[df_bar$Model] df_bar$Color[is.na(df_bar$Color)] <- "#CC0000" # Use manhattan width/height for bar chart download as well png(file, width=input$manhattan_width, height=max(400,input$manhattan_height/2), res=150, bg="white") par(mar=c(6,5,4,2), bg="white") bp <- barplot(df_bar$N_QTL, names.arg=df_bar$Model, col=df_bar$Color, border="#330000", main=paste0("QTLs per Model (LOD โ‰ฅ ",lod_thr(),")"), ylab="# Significant QTLs", las=2, cex.names=0.85, col.main="#111111") text(bp, df_bar$N_QTL + 0.1, df_bar$N_QTL, cex=0.8, font=2, col="#111111") dev.off() } ) # Common ideogram PNG output$dl_common_idiogram_png <- downloadHandler( filename = function() paste0("common_qtl_idiogram_",Sys.Date(),".png"), content = function(file) { cq <- common_qtl_data() req(!is.null(cq), nrow(cq)>0, rv$all_markers) cq_list <- list() for (mth in names(rv$compare_res)){ r <- rv$compare_res[[mth]] if(is.null(r)||"Message"%in%colnames(r)) next cm <- r[r$Marker %in% cq$Marker,] if(nrow(cm)>0) cq_list[[mth]] <- cm } save_ideogram_png(cq_list, rv$all_markers, lod_thr(), paste0("Common QTL (โ‰ฅ",input$min_models," models)"), file, input$ideogram_width, input$ideogram_height) } ) # โ”€โ”€ CSV DOWNLOADS โ”€โ”€ output$dl_qtl_csv <- downloadHandler( filename = function() paste0("qtl_significant_",input$mapping_method,"_",Sys.Date(),".csv"), content = function(file) { req(rv$scan_all) if("Message"%in%colnames(rv$scan_all)){write.csv(rv$scan_all,file,row.names=FALSE);return()} sig <- rv$scan_all[!is.na(rv$scan_all$LOD)&rv$scan_all$LOD>=lod_thr(),,drop=FALSE] if(nrow(sig)>0) sig <- add_lod_support_interval(sig,rv$scan_all,input$lod_drop) write.csv(sig[order(-sig$LOD),],file,row.names=FALSE) } ) output$dl_full_csv <- downloadHandler( filename = function() paste0("qtl_full_scan_",input$mapping_method,"_",Sys.Date(),".csv"), content = function(file) { req(rv$scan_all) write.csv(rv$scan_all,file,row.names=FALSE) } ) output$dl_compare_csv <- downloadHandler( filename = function() paste0("qtl_compare_",Sys.Date(),".csv"), content = function(file) { req(length(rv$compare_res)>0) all_res <- lapply(names(rv$compare_res), function(mth){ r <- rv$compare_res[[mth]] if(is.null(r)||"Message"%in%colnames(r)) return(NULL) sig <- r[!is.na(r$LOD)&r$LOD>=lod_thr(),,drop=FALSE] if(nrow(sig)>0){sig$Model<-mth;sig} else NULL }) all_res <- do.call(rbind, Filter(Negate(is.null),all_res)) if(is.null(all_res)) all_res <- data.frame(Status="No significant QTLs") write.csv(all_res,file,row.names=FALSE) } ) output$dl_common_csv <- downloadHandler( filename = function() paste0("qtl_common_",Sys.Date(),".csv"), content = function(file) { cq <- common_qtl_data() if(is.null(cq)||nrow(cq)==0) cq <- data.frame(Status="No common QTLs") write.csv(cq,file,row.names=FALSE) } ) } # โ”€โ”€ Run โ”€โ”€ shinyApp(ui=ui, server=server)