qtl / app.R
softwareDevelopment's picture
Update app.R
c195b2d verified
Raw
History Blame Contribute Delete
65.9 kB
# ============================================================
# 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)&&p<threshold) selected <- test_model
},error=function(e){})
}
if (length(selected)==0) selected <- names(sort(p_vals))[1:min(5,length(p_vals))]
results_list <- list()
for (m in colnames(X_v)) {
g <- X_v[,m]; v2 <- !is.na(g)&!is.na(y_v)
gv <- g[v2]; yv <- y_v[v2]
if (length(unique(gv))<2||length(yv)<8) next
mi <- marker_info[marker_info$Marker==m,]
if (nrow(mi)==0) next
cof_m <- setdiff(selected,m)
tryCatch({
df2 <- data.frame(y=yv,focal=gv)
if (length(cof_m)>0){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<nrow(chr_data)) chr_data$Marker[peak_idx+1] else m
sig_df$Interval_Start[i] <- sig_df$Position[i]
sig_df$Interval_End[i] <- sig_df$Position[i]
sig_df$Interval_Size_cM[i] <- 0
}
}
}
sig_df
}
# ============================================================
# CHROMOSOME IDEOGRAM β€” NOW ACCEPTS COLOR & SIZE PARAMS
# ============================================================
make_chromosome_ideogram_reference <- function(qtl_list, all_markers_df,
lod_threshold = 2.0,
model_colors = MODEL_COLORS,
title_str = NULL,
band_dark_col = "#2E6B35",
band_light_col = "#6AAB6E",
border_col = "#1A4A1F",
qtl_size_min = 0.7,
qtl_size_max = 2.0) {
if (is.null(all_markers_df) || nrow(all_markers_df) == 0) return(invisible(NULL))
chroms <- sort(unique(as.character(all_markers_df$Chromosome)))
n_chr <- length(chroms)
x_gap <- 1.8
bar_w <- 0.55
max_pos <- max(all_markers_df$Position, na.rm=TRUE)
model_names <- names(qtl_list)
par(mar=c(5, 4, 4, 8), bg="white", xpd=TRUE)
plot(0, 0, type="n",
xlim=c(0.2, n_chr * x_gap + 0.3),
ylim=c(-8, max_pos * 1.12),
xaxt="n", yaxt="n", xlab="", ylab="", main="", frame.plot=FALSE, bty="n")
y_ticks <- pretty(c(0, max_pos), n=7)
axis(2, at=y_ticks, labels=y_ticks, cex.axis=0.72, las=1,
col="#555555", col.axis="#333333", tcl=-0.3, lwd=0.8)
mtext("Genetic Position (cM)", side=2, line=2.5, cex=0.82, col="#222222", font=2)
for (ci in seq_along(chroms)) {
chr <- chroms[ci]
x_center <- (ci - 1) * x_gap + 0.9
chr_m <- all_markers_df[as.character(all_markers_df$Chromosome)==chr, ]
if (nrow(chr_m)==0) next
chr_len <- max(chr_m$Position, na.rm=TRUE)
chr_min <- min(chr_m$Position, na.rm=TRUE)
n_bands <- max(8, round((chr_len - chr_min) / 8))
band_h <- (chr_len - chr_min) / n_bands
for (bi in 1:n_bands) {
band_bot <- chr_min + (bi-1)*band_h
band_top <- chr_min + bi*band_h
bcol <- if (bi %% 2 == 1) band_dark_col else band_light_col
rect(x_center - bar_w/2, band_bot, x_center + bar_w/2, band_top,
col=bcol, border=NA)
}
rect(x_center - bar_w/2, chr_min, x_center + bar_w/2, chr_len,
col=NA, border=border_col, lwd=1.8)
symbols(x_center, chr_min, circles=bar_w/2, inches=FALSE,
add=TRUE, fg=border_col, bg=band_light_col, lwd=1.2)
symbols(x_center, chr_len, circles=bar_w/2, inches=FALSE,
add=TRUE, fg=border_col, bg=band_dark_col, lwd=1.2)
cen_pos <- chr_min + (chr_len - chr_min)*0.42
polygon(c(x_center-bar_w/2, x_center, x_center+bar_w/2, x_center),
c(cen_pos - chr_len*0.025, cen_pos,
cen_pos - chr_len*0.025, cen_pos - chr_len*0.05),
col="white", border=border_col, lwd=1.2)
polygon(c(x_center-bar_w/2, x_center, x_center+bar_w/2, x_center),
c(cen_pos + chr_len*0.025, cen_pos,
cen_pos + chr_len*0.025, cen_pos + chr_len*0.05),
col="white", border=border_col, lwd=1.2)
n_models <- length(model_names)
for (ti in seq_along(model_names)) {
trait <- model_names[ti]
qtl_data <- qtl_list[[trait]]
if (is.null(qtl_data) || "Message" %in% colnames(qtl_data)) next
sig_qtls <- qtl_data[as.character(qtl_data$Chromosome)==chr &
!is.na(qtl_data$LOD) &
qtl_data$LOD >= 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("<b>", Marker, "</b><br>Chr: ", Chromosome,
"<br>Pos: ", round(Position, 1), " cM<br>LOD: ", round(LOD, 2),
"<br>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("<b>", Marker, "</b><br>Chr: ", Chromosome,
"<br>Pos: ", round(Position, 1), " cM<br>LOD: ", round(LOD, 2),
"<br>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,"<br>Chr ",Chromosome," @ ",Position," cM<br>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)