softwareDevelopment's picture
Update app.R
6687151 verified
Raw
History Blame Contribute Delete
98.3 kB
library(shiny)
library(openxlsx)
library(igraph)
library(DT)
library(ggplot2)
library(dplyr)
library(RColorBrewer)
library(ggrepel)
library(scales)
# ═══════════════════════════════════════════════════════════════════════════════
# DATA LOADING & AUTO-CONVERSION
# ═══════════════════════════════════════════════════════════════════════════════
load_and_convert_data <- function(filepath) {
wb_raw <- tryCatch(loadWorkbook(filepath),
error = function(e) stop("Cannot open workbook: ", e$message))
df_raw <- read.xlsx(filepath, sheet = 1, colNames = TRUE)
if (!("Chromosome" %in% colnames(df_raw))) stop("Column 'Chromosome' not found.")
if (!("Marker" %in% colnames(df_raw))) stop("Column 'Marker' not found.")
chr_col <- as.character(df_raw$Chromosome)
has_formula <- any(grepl("^=", chr_col, ignore.case = FALSE), na.rm = TRUE)
if (has_formula) {
inferred <- infer_chromosome_from_marker(df_raw$Marker)
n_inferred <- sum(!is.na(inferred))
if (n_inferred / nrow(df_raw) >= 0.5) {
df_raw$Chromosome <- inferred
} else {
df_raw$Chromosome <- seq_len(nrow(df_raw))
warning("Could not infer chromosomes from marker names.")
}
}
if (ncol(df_raw) < 3) {
df_raw$.input_order <- seq_len(nrow(df_raw))
return(df_raw)
}
geno_cols <- df_raw[, 3:ncol(df_raw), drop = FALSE]
sample_vals <- unique(unlist(geno_cols[1:min(50, nrow(geno_cols)), ]))
sample_vals <- sample_vals[!is.na(sample_vals)]
numeric_encoding <- length(sample_vals) > 0 && all(sample_vals %in% c(-1, 0, 1, 2, NA))
text_encoding <- length(sample_vals) > 0 && all(sample_vals %in% c("a", "b", "ab", "-", NA, "NA"))
if (numeric_encoding) {
df_raw[, 3:ncol(df_raw)] <- lapply(
df_raw[, 3:ncol(df_raw), drop = FALSE],
function(col) {
col <- as.character(col)
col[col == "2"] <- "a"
col[col == "1"] <- "b"
col[col == "0"] <- "ab"
col[col == "-1"] <- "-"
col[is.na(col) | col == "NA"] <- "-"
col
})
} else if (!text_encoding) {
df_raw[, 3:ncol(df_raw)] <- lapply(
df_raw[, 3:ncol(df_raw), drop = FALSE],
function(col) {
col <- as.character(col)
col[!(col %in% c("a", "b", "ab"))] <- "-"
col
})
}
df_raw$Chromosome <- as.character(df_raw$Chromosome)
df_raw$Chromosome[is.na(df_raw$Chromosome) | df_raw$Chromosome == "NA"] <- "Unknown"
df_raw$.input_order <- seq_len(nrow(df_raw))
df_raw
}
infer_chromosome_from_marker <- function(marker_names) {
markers <- as.character(marker_names)
chrom <- rep(NA_character_, length(markers))
patterns <- list(
list(regex = "(?i)chr(\\d{1,2})[_\\-]", group = 1),
list(regex = "(?i)[_\\-]C(\\d{1,2})[_\\-]", group = 1),
list(regex = "(?i)^MSU\\d+_(\\d{1,2})_", group = 1),
list(regex = "(?i)^DTY(\\d{1,2})[\\-_]", group = 1),
list(regex = "(?i)^[A-Za-z]{1,6}(\\d{1,2})[a-zA-Z_]", group = 1),
list(regex = "(?i)^Saltol", group = NA),
list(regex = "(?i)^SCT(\\d{1,2})[_\\-]", group = 1),
list(regex = "(?i)^R(\\d{1,2})", group = 1),
list(regex = "(?i)^Pi(\\d{1,2})", group = 1)
)
for (i in seq_along(markers)) {
m <- markers[i]
for (pat in patterns) {
if (is.na(pat$group)) {
if (grepl(pat$regex, m, perl = TRUE)) { chrom[i] <- "1"; break }
} else {
mt <- regmatches(m, regexpr(pat$regex, m, perl = TRUE))
if (length(mt) > 0) {
sub_mt <- regmatches(mt, regexec(pat$regex, mt, perl = TRUE))[[1]]
if (length(sub_mt) >= pat$group + 1) {
chr_num <- as.integer(sub_mt[pat$group + 1])
if (!is.na(chr_num) && chr_num >= 1 && chr_num <= 30) {
chrom[i] <- as.character(chr_num); break
}
}
}
}
}
}
chrom
}
# ═══════════════════════════════════════════════════════════════════════════════
# CORE GENETIC FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
calculate_recombination <- function(population_type, genotype1, genotype2) {
g1 <- as.character(genotype1)
g2 <- as.character(genotype2)
keep <- !(g1 %in% c("-", "NA", NA) | g2 %in% c("-", "NA", NA))
g1 <- g1[keep]; g2 <- g2[keep]
n <- length(g1)
if (n < 4) return(NA_real_)
if (population_type == "RIL") {
hom <- !(g1 == "ab" | g2 == "ab")
g1h <- g1[hom]; g2h <- g2[hom]
if (length(g1h) < 4) return(NA_real_)
r_obs <- mean(g1h != g2h)
rf <- r_obs / (2 * (1 - r_obs))
return(min(rf, 0.4999))
} else if (population_type == "F2") {
AA_AA <- sum(g1=="a" & g2=="a"); AA_Aa <- sum(g1=="a" & g2=="ab")
AA_BB <- sum(g1=="a" & g2=="b"); Aa_AA <- sum(g1=="ab" & g2=="a")
Aa_Aa <- sum(g1=="ab" & g2=="ab"); Aa_BB <- sum(g1=="ab" & g2=="b")
BB_AA <- sum(g1=="b" & g2=="a"); BB_Aa <- sum(g1=="b" & g2=="ab")
BB_BB <- sum(g1=="b" & g2=="b")
r <- 0.25
for (iter in 1:200) {
r_old <- r; q <- 1 - r
p_parental <- q^2 + r^2; p_recomb <- 2 * r * q
norm <- p_parental + p_recomb
n_recomb_AaAa <- if (norm > 0) Aa_Aa * p_recomb / norm else 0
recomb_count <- 2*(AA_BB+BB_AA) + (AA_Aa+Aa_AA+Aa_BB+BB_Aa) + 2*n_recomb_AaAa
r_new <- max(1e-6, min(recomb_count / (2*n), 0.4999))
r <- r_new
if (abs(r - r_old) < 1e-8) break
}
return(r)
} else stop("population_type must be 'RIL' or 'F2'")
}
calculate_cM_kosambi <- function(r) {
if (is.na(r)) return(NA_real_)
r <- max(0, min(r, 0.4999))
if (r == 0) return(0)
25 * log((1 + 2*r) / (1 - 2*r))
}
detect_monomorphic_markers <- function(df) {
geno_cols_idx <- setdiff(3:ncol(df), which(colnames(df) == ".input_order"))
if (length(geno_cols_idx) == 0) return(setNames(rep(FALSE, nrow(df)), df$Marker))
is_mono <- apply(df[, geno_cols_idx, drop = FALSE], 1, function(row) {
vals <- row[!is.na(row) & !(row %in% c("-", "NA", ""))]
if (length(vals) == 0) return(FALSE)
unique_vals <- unique(vals[vals %in% c("a", "b", "ab")])
length(unique_vals) <= 1
})
names(is_mono) <- df$Marker
is_mono
}
perform_chi_square_test <- function(df, population_type) {
geno_cols_idx <- setdiff(3:ncol(df), which(colnames(df) == ".input_order"))
if (population_type == "F2") {
expected <- c(a = 0.25, ab = 0.50, b = 0.25)
valid_classes <- c("a", "ab", "b")
} else {
expected <- c(a = 0.50, b = 0.50)
valid_classes <- c("a", "b")
}
p_values <- apply(df[, geno_cols_idx, drop = FALSE], 1, function(row) {
vals <- row[!is.na(row) & row %in% valid_classes]
if (length(vals) < 10) return(1.0)
obs <- table(factor(vals, levels = valid_classes))
result <- tryCatch(suppressWarnings(chisq.test(obs, p = expected)),
error = function(e) list(p.value = 1.0))
result$p.value
})
names(p_values) <- df$Marker
p_values
}
correct_errors_and_impute_missing <- function(df) {
valid_geno <- c("a", "b", "ab")
geno_start <- which(colnames(df) == "Chromosome") + 1
if (length(geno_start) == 0 || geno_start > ncol(df)) return(df)
geno_cols_idx <- setdiff(geno_start:ncol(df), which(colnames(df) == ".input_order"))
for (j in geno_cols_idx) {
x <- as.character(df[[j]])
x[x %in% c("-", "NA", "", " ", "na")] <- NA
x[!is.na(x) & !(x %in% valid_geno)] <- NA
df[[j]] <- x
}
if (length(geno_cols_idx) > 0) {
na_col <- colMeans(is.na(df[, geno_cols_idx, drop = FALSE]))
keep_idx <- c(seq_len(geno_start - 1),
geno_cols_idx[na_col < 1],
which(colnames(df) == ".input_order"))
keep_idx <- keep_idx[keep_idx <= ncol(df)]
df <- df[, sort(unique(keep_idx)), drop = FALSE]
}
geno_cols_idx2 <- setdiff(geno_start:ncol(df), which(colnames(df) == ".input_order"))
if (length(geno_cols_idx2) > 0) {
df[, geno_cols_idx2] <- t(apply(df[, geno_cols_idx2, drop = FALSE], 1, function(row) {
nas <- is.na(row)
if (all(nas) || !any(nas)) return(row)
tbl <- table(row[!nas])
mode_val <- names(which.max(tbl))
row[nas] <- mode_val
row
}))
}
df
}
quality_control <- function(df, missing_threshold = 0.1) {
if (ncol(df) < 3 || nrow(df) == 0) return(df[0, ])
geno_cols_idx <- setdiff(3:(ncol(df)), which(colnames(df) == ".input_order"))
if (length(geno_cols_idx) == 0) return(df)
geno <- df[, geno_cols_idx, drop = FALSE]
miss_frac <- rowMeans(is.na(geno))
miss_frac[is.na(miss_frac)] <- 1
df[miss_frac <= missing_threshold, ]
}
# ═══════════════════════════════════════════════════════════════════════════════
# POSITION BUILDING
# ═══════════════════════════════════════════════════════════════════════════════
build_positions_from_input_order <- function(df, population_type, recombination_threshold) {
chromosomes <- unique(df$Chromosome[order(df$.input_order)])
geno_cols_idx <- setdiff(3:ncol(df), which(colnames(df) == ".input_order"))
positions_list <- list()
recombination_rows <- list()
for (chr in chromosomes) {
sub <- df[df$Chromosome == chr, ]
sub <- sub[order(sub$.input_order), ]
mks <- sub$Marker
if (length(mks) == 0) next
pos <- setNames(numeric(length(mks)), mks)
if (length(mks) > 1) {
for (i in 2:length(mks)) {
m_prev <- mks[i-1]; m_curr <- mks[i]
g1 <- unlist(sub[sub$Marker == m_prev, geno_cols_idx])
g2 <- unlist(sub[sub$Marker == m_curr, geno_cols_idx])
rf <- calculate_recombination(population_type, g1, g2)
if (!is.na(rf)) {
cM <- calculate_cM_kosambi(rf)
recombination_rows[[length(recombination_rows)+1]] <-
data.frame(Marker1=m_prev, Marker2=m_curr,
RecombinationFraction=rf, MapDistance_cM=cM,
Chromosome=chr, stringsAsFactors=FALSE)
pos[i] <- if (rf < recombination_threshold) pos[i-1] + abs(cM)
else pos[i-1] + 50
} else {
pos[i] <- pos[i-1] + 5
}
}
}
for (mk in names(pos)) positions_list[[mk]] <- pos[mk]
}
list(positions_list=positions_list, recombination_rows=recombination_rows)
}
scale_linkage_map <- function(linkage_map, max_total_cm, method = "proportional") {
pos_map <- linkage_map[!is.na(linkage_map$Position), ]
if (nrow(pos_map) == 0)
return(list(scaled_map=linkage_map, chrom_lengths_before=data.frame(),
chrom_lengths_after=data.frame()))
before <- pos_map %>%
group_by(Chromosome) %>%
summarise(Original_Length = max(Position, na.rm=TRUE), .groups="drop")
total <- sum(before$Original_Length, na.rm=TRUE)
if (total <= max_total_cm || total == 0)
return(list(scaled_map=linkage_map, chrom_lengths_before=before,
chrom_lengths_after=before))
scaled <- linkage_map
if (method == "proportional") {
before$sf <- max_total_cm / total
} else {
n_chr <- nrow(before)
before$sf <- ifelse(before$Original_Length > 0,
(max_total_cm/n_chr)/before$Original_Length, 1)
}
for (chr in before$Chromosome) {
sf <- before$sf[before$Chromosome == chr]
idx <- which(scaled$Chromosome == chr & !is.na(scaled$Position))
if (length(idx) > 0) {
mn <- min(scaled$Position[idx])
scaled$Position[idx] <- (scaled$Position[idx] - mn) * sf
}
}
after <- scaled %>%
filter(!is.na(Position)) %>%
group_by(Chromosome) %>%
summarise(Original_Length = max(Position, na.rm=TRUE), .groups="drop")
list(scaled_map=scaled, chrom_lengths_before=before, chrom_lengths_after=after)
}
# ═══════════════════════════════════════════════════════════════════════════════
# MASTER ANALYSIS
# ═══════════════════════════════════════════════════════════════════════════════
create_linkage_map <- function(df,
population_type = "F2",
missing_threshold = 0.1,
recombination_threshold = 0.5,
max_total_cm = 1500,
scaling_method = "proportional",
enable_chi_square = FALSE,
chi_square_alpha = 0.05,
use_bonferroni = FALSE) {
empty_result <- function(df_in = df) {
list(original_data = df_in,
recombination_data = data.frame(),
linkage_map = data.frame(Marker=character(0),
Chromosome=character(0),
Position=numeric(0)),
all_markers_positions = data.frame(),
retained_markers_list = data.frame(),
chrom_lengths_before = data.frame(),
chrom_lengths_after = data.frame(),
removal_log = data.frame(),
qc_report = "No data passed QC.")
}
df_orig <- df
if (!(".input_order" %in% colnames(df))) df$.input_order <- seq_len(nrow(df))
removal_log <- data.frame(
Marker = character(0), Chromosome = character(0),
Reason = character(0), Detail = character(0),
stringsAsFactors = FALSE
)
n_input <- nrow(df)
df <- correct_errors_and_impute_missing(df)
geno_cols_idx <- setdiff(3:ncol(df), which(colnames(df) == ".input_order"))
geno_mat <- df[, geno_cols_idx, drop = FALSE]
miss_frac <- if (ncol(geno_mat) > 0) rowMeans(is.na(geno_mat)) else rep(0, nrow(df))
miss_frac[is.na(miss_frac)] <- 1
failed_miss <- df$Marker[miss_frac > missing_threshold]
if (length(failed_miss) > 0) {
removal_log <- rbind(removal_log, data.frame(
Marker = failed_miss,
Chromosome = df$Chromosome[miss_frac > missing_threshold],
Reason = "Excess missing data",
Detail = paste0(round(miss_frac[miss_frac > missing_threshold]*100,1),
"% missing (threshold: ", round(missing_threshold*100,1), "%)"),
stringsAsFactors = FALSE
))
}
df <- df[miss_frac <= missing_threshold, ]
if (nrow(df) == 0 || ncol(df) < 3) {
res <- empty_result(df_orig); res$removal_log <- removal_log; return(res)
}
is_mono <- detect_monomorphic_markers(df)
mono_markers <- names(is_mono)[is_mono]
if (length(mono_markers) > 0) {
mono_df <- df[df$Marker %in% mono_markers, ]
geno_cols_idx2 <- setdiff(3:ncol(df), which(colnames(df) == ".input_order"))
details <- sapply(mono_markers, function(mk) {
row <- df[df$Marker == mk, geno_cols_idx2, drop = FALSE]
vals <- unlist(row)
vals <- vals[!is.na(vals) & vals %in% c("a","b","ab")]
if (length(vals) == 0) return("No informative genotypes")
tbl <- table(vals)
dominant <- names(which.max(tbl))
allele_name <- switch(dominant,
a = "Homozygous Parent 1 (AA)",
b = "Homozygous Parent 2 (BB)",
ab = "Heterozygous only (ab)",
dominant)
paste0("Fixed as: ", allele_name,
" (", round(max(tbl)/sum(tbl)*100, 1), "% of genotyped individuals)")
})
removal_log <- rbind(removal_log, data.frame(
Marker = mono_markers,
Chromosome = mono_df$Chromosome,
Reason = "Monomorphic marker",
Detail = details,
stringsAsFactors = FALSE
))
df <- df[!df$Marker %in% mono_markers, ]
}
if (nrow(df) == 0) {
res <- empty_result(df_orig); res$removal_log <- removal_log; return(res)
}
n_after_mono <- nrow(df)
n_failed_chi <- 0
chi_note <- "Chi-square filter : DISABLED"
if (enable_chi_square) {
p_values <- perform_chi_square_test(df, population_type)
alpha_used <- if (use_bonferroni) chi_square_alpha / nrow(df) else chi_square_alpha
failed_chi_markers <- names(p_values)[p_values < alpha_used]
if (length(failed_chi_markers) > 0) {
chi_df <- df[df$Marker %in% failed_chi_markers, ]
chi_details <- sapply(failed_chi_markers, function(mk) {
pv <- p_values[mk]
paste0("p = ", formatC(pv, format="e", digits=3),
" (alpha = ", formatC(alpha_used, format="e", digits=3), ")")
})
removal_log <- rbind(removal_log, data.frame(
Marker = failed_chi_markers,
Chromosome = chi_df$Chromosome,
Reason = "Segregation distortion (chi-square)",
Detail = chi_details,
stringsAsFactors = FALSE
))
df <- df[!df$Marker %in% failed_chi_markers, ]
n_failed_chi <- length(failed_chi_markers)
}
bonf_label <- if (use_bonferroni)
paste0("Bonferroni-corrected (alpha/", n_after_mono, ")")
else "Uncorrected"
chi_note <- paste0(
"Chi-square filter : ENABLED\n",
" Significance : alpha = ", chi_square_alpha, " (", bonf_label, ")\n",
" Effective alpha : ", formatC(alpha_used, format="e", digits=3), "\n",
" Markers removed : ", n_failed_chi
)
}
if (nrow(df) == 0) {
res <- empty_result(df_orig); res$removal_log <- removal_log; return(res)
}
n_after_qc <- nrow(df)
qc_msg <- paste0(
"QC Report (v6.0)\n",
"================\n",
" Markers input : ", n_input, "\n",
" Removed (missing data) : ", length(failed_miss), "\n",
" Removed (monomorphic) : ", length(mono_markers), "\n",
" Removed (chi-square) : ", n_failed_chi, "\n",
" Markers retained : ", n_after_qc, "\n\n",
chi_note, "\n\n",
" Marker ORDER preserved from input file.\n",
" First marker of each chromosome = 0 cM.\n"
)
pos_result <- build_positions_from_input_order(df, population_type, recombination_threshold)
positions_list <- pos_result$positions_list
recombination_rows <- pos_result$recombination_rows
rec_df <- if (length(recombination_rows) > 0) do.call(rbind, recombination_rows)
else data.frame()
if (length(positions_list) == 0) {
lm_empty <- df[, c("Marker","Chromosome")]
lm_empty$Position <- NA_real_
retained_list <- data.frame(Marker=df$Marker, Chromosome=df$Chromosome,
Status="Retained", stringsAsFactors=FALSE)
all_markers <- build_all_markers_positions(df_orig, removal_log, lm_empty)
return(list(original_data=df_orig, recombination_data=rec_df,
linkage_map=lm_empty,
all_markers_positions=all_markers,
retained_markers_list=retained_list,
chrom_lengths_before=data.frame(),
chrom_lengths_after=data.frame(),
removal_log=removal_log, qc_report=qc_msg))
}
pos_df <- data.frame(Marker = names(positions_list),
Position = unlist(positions_list),
stringsAsFactors = FALSE)
linkage_map <- merge(df[, c("Marker","Chromosome",".input_order")],
pos_df, by="Marker", all.x=TRUE)
linkage_map <- linkage_map[order(linkage_map$Chromosome, linkage_map$.input_order), ]
linkage_map$.input_order <- NULL
scaled <- scale_linkage_map(linkage_map, max_total_cm, scaling_method)
all_markers_positions <- build_all_markers_positions(df_orig, removal_log, scaled$scaled_map)
retained_markers_list <- scaled$scaled_map %>%
select(Marker, Chromosome, Position) %>%
mutate(Status = "Retained") %>%
arrange(Chromosome, Position)
list(original_data = df_orig,
recombination_data = rec_df,
linkage_map = scaled$scaled_map,
all_markers_positions = all_markers_positions,
retained_markers_list = retained_markers_list,
chrom_lengths_before = scaled$chrom_lengths_before,
chrom_lengths_after = scaled$chrom_lengths_after,
removal_log = removal_log,
qc_report = qc_msg)
}
build_all_markers_positions <- function(df_orig, removal_log, linkage_map) {
df_all <- df_orig[, c("Marker","Chromosome"), drop=FALSE]
df_all$Chromosome <- as.character(df_all$Chromosome)
lm_sub <- linkage_map[, c("Marker","Position"), drop=FALSE]
df_all <- merge(df_all, lm_sub, by="Marker", all.x=TRUE)
removed_markers <- unique(removal_log$Marker)
df_all$Status <- ifelse(df_all$Marker %in% removed_markers, "Removed", "Retained")
if (nrow(removal_log) > 0) {
reason_df <- removal_log[, c("Marker","Reason","Detail"), drop=FALSE]
df_all <- merge(df_all, reason_df, by="Marker", all.x=TRUE)
} else {
df_all$Reason <- NA_character_
df_all$Detail <- NA_character_
}
df_all$Position[df_all$Status == "Removed"] <- NA_real_
df_all <- df_all[order(df_all$Chromosome, df_all$Marker), ]
df_all[, c("Marker","Chromosome","Status","Position","Reason","Detail")]
}
# ═══════════════════════════════════════════════════════════════════════════════
# CYTOGENETIC PLOT DATA BUILDER (FIXED)
# ═══════════════════════════════════════════════════════════════════════════════
build_cyto_plot_data <- function(linkage_map, df_retained, selected_chromosomes) {
if (is.null(selected_chromosomes) || length(selected_chromosomes) == 0) return(list())
# Ensure linkage_map has Position column
if (is.null(linkage_map$Position)) linkage_map$Position <- NA_real_
# Get genotype columns (all columns except Marker, Chromosome, .input_order, Position, Status, Reason, Detail)
exclude_cols <- c("Marker", "Chromosome", ".input_order", "Position", "Status", "Reason", "Detail")
geno_cols <- setdiff(colnames(df_retained), exclude_cols)
# Sort chromosomes numerically if possible
chr_sorted <- tryCatch(
selected_chromosomes[order(as.numeric(selected_chromosomes))],
warning = function(w) sort(selected_chromosomes),
error = function(e) sort(selected_chromosomes)
)
result <- list()
for (chr in chr_sorted) {
# Get markers for this chromosome with valid positions
sub_map <- linkage_map[linkage_map$Chromosome == chr & !is.na(linkage_map$Position), ]
if (nrow(sub_map) == 0) next
sub_map <- sub_map[order(sub_map$Position), ]
# Get genotype data for these markers
sub_geno <- df_retained[df_retained$Marker %in% sub_map$Marker, ]
marker_data <- list()
for (i in seq_len(nrow(sub_map))) {
mk <- sub_map$Marker[i]
pos <- sub_map$Position[i]
# Get genotype row for this marker
geno_row <- sub_geno[sub_geno$Marker == mk, geno_cols, drop = FALSE]
if (nrow(geno_row) == 0 || length(geno_cols) == 0) {
marker_data[[i]] <- list(
marker = mk,
position = round(pos, 3),
allele_a = 0,
allele_b = 0,
allele_ab = 0,
allele_miss = 1
)
next
}
vals <- unlist(geno_row)
vals <- vals[!is.na(vals)]
n_total <- length(vals)
if (n_total == 0) n_total <- 1
marker_data[[i]] <- list(
marker = mk,
position = round(pos, 3),
allele_a = round(sum(vals == "a", na.rm = TRUE) / n_total, 4),
allele_b = round(sum(vals == "b", na.rm = TRUE) / n_total, 4),
allele_ab = round(sum(vals == "ab", na.rm = TRUE) / n_total, 4),
allele_miss = round(sum(!(vals %in% c("a","b","ab")), na.rm = TRUE) / n_total, 4)
)
}
if (length(marker_data) > 0) {
result[[chr]] <- list(
chromosome = chr,
max_pos = round(max(sub_map$Position, na.rm = TRUE), 2),
n_markers = nrow(sub_map),
markers = marker_data
)
}
}
result
}
# ═══════════════════════════════════════════════════════════════════════════════
# HIGH-RESOLUTION GGPLOT LINKAGE MAP
# ═══════════════════════════════════════════════════════════════════════════════
create_ggplot_linkage_map <- function(linkage_map,
selected_chromosomes,
show_labels = FALSE,
label_size = 2.5,
color_palette = "Set2",
chrom_width = 0.4,
point_size = 2.5,
chrom_spacing = 2.5,
plot_theme = "dark") {
if (is.null(selected_chromosomes) || length(selected_chromosomes) == 0)
return(ggplot() + annotate("text", x=0.5, y=0.5, label="No chromosomes selected.", size=6) + theme_void())
chr_sorted <- tryCatch(
selected_chromosomes[order(as.numeric(selected_chromosomes))],
warning = function(w) sort(selected_chromosomes),
error = function(e) sort(selected_chromosomes)
)
plot_data <- linkage_map %>%
filter(Chromosome %in% selected_chromosomes, !is.na(Position)) %>%
mutate(Chromosome = factor(Chromosome, levels = chr_sorted)) %>%
arrange(Chromosome, Position)
if (nrow(plot_data) == 0) {
return(ggplot() +
annotate("text", x=0.5, y=0.5,
label="No valid marker positions.\nTry relaxing QC parameters.",
size=5, color="#E74C3C", hjust=0.5) + theme_void())
}
n_chr <- length(chr_sorted)
chrom_stats <- plot_data %>%
group_by(Chromosome) %>%
summarise(max_pos = max(Position, na.rm=TRUE),
min_pos = min(Position, na.rm=TRUE),
num_markers = n(), .groups="drop") %>%
mutate(Chromosome = factor(Chromosome, levels = chr_sorted)) %>%
arrange(Chromosome) %>%
mutate(x_center = as.numeric(Chromosome) * chrom_spacing)
plot_data <- plot_data %>%
left_join(chrom_stats %>% select(Chromosome, x_center), by="Chromosome")
# Color palette
n_needed <- max(3, n_chr)
chrom_colors <- tryCatch({
if (n_chr <= 8) brewer.pal(n_needed, color_palette)[seq_len(n_chr)]
else colorRampPalette(brewer.pal(8, color_palette))(n_chr)
}, error = function(e) {
colorRampPalette(brewer.pal(8, "Set2"))(n_chr)
})
names(chrom_colors) <- chr_sorted
# Theme setup
if (plot_theme == "dark") {
bg_col <- "#1A1A2E"
panel_col <- "#16213E"
grid_col <- "#2D3561"
text_col <- "#E8E8F0"
axis_col <- "#8A8AB0"
strip_col <- "#0F3460"
} else if (plot_theme == "minimal_white") {
bg_col <- "#FFFFFF"
panel_col <- "#FAFAFA"
grid_col <- "#EEEEEE"
text_col <- "#222222"
axis_col <- "#666666"
strip_col <- "#F0F0F0"
} else {
bg_col <- "#F0F4FF"
panel_col <- "#FFFFFF"
grid_col <- "#D0DCF0"
text_col <- "#1E3A8A"
axis_col <- "#4A6FA5"
strip_col <- "#DBEAFE"
}
# Half-width for chromosome bars
hw <- chrom_width / 2
p <- ggplot() +
# Chromosome body (thick segment with rounded aesthetic via geom_rect)
geom_rect(data = chrom_stats,
aes(xmin = x_center - hw,
xmax = x_center + hw,
ymin = min_pos,
ymax = max_pos,
fill = as.factor(Chromosome)),
alpha = 0.18, color = NA) +
# Chromosome axis line
geom_segment(data = chrom_stats,
aes(x = x_center, xend = x_center,
y = min_pos, yend = max_pos,
color = as.factor(Chromosome)),
linewidth = chrom_width * 12,
alpha = 0.5, lineend = "round") +
# Marker tick marks (horizontal lines)
geom_segment(data = plot_data,
aes(x = x_center - hw * 1.6,
xend = x_center + hw * 1.6,
y = Position,
yend = Position,
color = as.factor(Chromosome)),
linewidth = 0.6, alpha = 0.85) +
# Marker points
geom_point(data = plot_data,
aes(x = x_center, y = Position,
color = as.factor(Chromosome)),
size = point_size, alpha = 0.92, shape = 21,
fill = "white", stroke = 0.8) +
# Chromosome name label (bottom)
geom_text(data = chrom_stats,
aes(x = x_center,
y = max_pos + max(chrom_stats$max_pos) * 0.025,
label = paste0("Chr ", Chromosome),
color = as.factor(Chromosome)),
size = 3.8, fontface = "bold", vjust = 0) +
# Marker count label
geom_text(data = chrom_stats,
aes(x = x_center,
y = max_pos + max(chrom_stats$max_pos) * 0.07,
label = paste0("n=", num_markers)),
color = axis_col, size = 3.0, vjust = 0) +
# Length label (top)
geom_text(data = chrom_stats,
aes(x = x_center,
y = min_pos - max(chrom_stats$max_pos) * 0.025,
label = paste0(round(max_pos,1), " cM"),
color = as.factor(Chromosome)),
size = 3.0, vjust = 1, fontface = "italic") +
scale_color_manual(values = chrom_colors) +
scale_fill_manual(values = chrom_colors) +
scale_y_reverse(name = "Genetic Position (cM)",
expand = expansion(mult = c(0.04, 0.15))) +
scale_x_continuous(breaks = chrom_stats$x_center,
labels = chrom_stats$Chromosome,
name = "Linkage Group",
expand = expansion(mult = 0.08)) +
guides(color = "none", fill = "none") +
labs(title = "Genetic Linkage Map",
subtitle = paste0(nrow(plot_data), " markers across ",
n_chr, " linkage groups"),
caption = "Map distances in Kosambi cM | Linkage Map Creator v6.0") +
theme_minimal(base_size = 13) +
theme(
plot.background = element_rect(fill = bg_col, color = NA),
panel.background = element_rect(fill = panel_col, color = NA),
panel.grid.major.x = element_blank(),
panel.grid.minor.x = element_blank(),
panel.grid.major.y = element_line(color = grid_col, linewidth = 0.4, linetype = "dashed"),
panel.grid.minor.y = element_line(color = grid_col, linewidth = 0.2),
axis.text.x = element_text(color = text_col, size = 11, face = "bold"),
axis.text.y = element_text(color = axis_col, size = 10),
axis.title.x = element_text(color = text_col, size = 13, face = "bold",
margin = margin(t = 12)),
axis.title.y = element_text(color = text_col, size = 13, face = "bold",
margin = margin(r = 12)),
plot.title = element_text(color = text_col, size = 18, face = "bold",
hjust = 0.5, margin = margin(b = 4)),
plot.subtitle = element_text(color = axis_col, size = 12,
hjust = 0.5, margin = margin(b = 14)),
plot.caption = element_text(color = axis_col, size = 9, hjust = 1),
plot.margin = margin(20, 30, 20, 20)
)
# Optional marker labels
if (show_labels && nrow(plot_data) <= 600) {
p <- p + ggrepel::geom_text_repel(
data = plot_data,
aes(x = x_center, y = Position,
label = Marker, color = as.factor(Chromosome)),
size = label_size,
direction = "y",
nudge_x = hw * 2.2,
segment.size = 0.25,
segment.alpha = 0.55,
max.overlaps = 25,
show.legend = FALSE,
fontface = "plain"
)
}
p
}
# ═══════════════════════════════════════════════════════════════════════════════
# RECOMBINATION HEATMAP
# ═══════════════════════════════════════════════════════════════════════════════
create_recombination_heatmap <- function(rec_data, selected_chr, top_n = 60) {
if (is.null(rec_data) || nrow(rec_data) == 0)
return(ggplot() + annotate("text",x=.5,y=.5,label="No recombination data.",size=5) + theme_void())
df <- rec_data
if (!is.null(selected_chr) && length(selected_chr) > 0)
df <- df[df$Chromosome %in% selected_chr, ]
if (nrow(df) == 0)
return(ggplot() + annotate("text",x=.5,y=.5,label="No data for selected chromosomes.",size=5) + theme_void())
# Limit pairs for readability
if (nrow(df) > top_n) df <- df[1:top_n, ]
df$pair <- paste0(df$Marker1, "\n", df$Marker2)
df$pair <- factor(df$pair, levels = rev(unique(df$pair)))
ggplot(df, aes(x = 1, y = pair, fill = RecombinationFraction)) +
geom_tile(color = "white", linewidth = 0.4) +
geom_text(aes(label = paste0(round(MapDistance_cM, 1), " cM\nr=",
round(RecombinationFraction, 3))),
size = 2.8, color = "white", fontface = "bold") +
scale_fill_gradientn(
colors = c("#0D3B66","#1A6FA5","#4ECDC4","#FFE66D","#FF6B6B","#C0392B"),
limits = c(0, 0.5),
name = "Recombination\nFraction (r)",
breaks = c(0, 0.1, 0.2, 0.3, 0.4, 0.5),
labels = c("0.0","0.1","0.2","0.3","0.4","0.5")
) +
facet_wrap(~Chromosome, scales = "free_y", ncol = 3) +
labs(title = "Adjacent Marker Recombination Fractions",
subtitle = paste0("Showing ", nrow(df), " marker pairs"),
x = NULL, y = "Marker Pair") +
theme_minimal(base_size = 11) +
theme(
axis.text.x = element_blank(),
axis.ticks.x = element_blank(),
axis.text.y = element_text(size = 8),
strip.text = element_text(face = "bold", size = 10),
legend.position = "right",
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
plot.subtitle = element_text(size = 10, hjust = 0.5, color = "gray50"),
panel.grid = element_blank(),
plot.background = element_rect(fill = "#FAFAFA", color = NA)
)
}
# ═══════════════════════════════════════════════════════════════════════════════
# MARKER DENSITY PLOT
# ═══════════════════════════════════════════════════════════════════════════════
create_marker_density_plot <- function(linkage_map, selected_chr, bin_width = 10) {
if (is.null(linkage_map) || nrow(linkage_map) == 0)
return(ggplot() + annotate("text",x=.5,y=.5,label="No data.",size=5) + theme_void())
df <- linkage_map %>%
filter(!is.na(Position))
if (!is.null(selected_chr) && length(selected_chr) > 0)
df <- df %>% filter(Chromosome %in% selected_chr)
if (nrow(df) == 0)
return(ggplot() + annotate("text",x=.5,y=.5,label="No positioned markers.",size=5) + theme_void())
chr_sorted <- tryCatch(
sort(unique(df$Chromosome), decreasing = FALSE),
warning = function(w) sort(unique(df$Chromosome))
)
df$Chromosome <- factor(df$Chromosome, levels = chr_sorted)
n_chr <- length(chr_sorted)
pal <- tryCatch(
colorRampPalette(brewer.pal(min(n_chr, 9), "Set1"))(n_chr),
error = function(e) colorRampPalette(c("#E74C3C","#3498DB","#2ECC71","#F39C12","#9B59B6"))(n_chr)
)
ggplot(df, aes(x = Position, fill = Chromosome, color = Chromosome)) +
geom_histogram(binwidth = bin_width, alpha = 0.75, linewidth = 0.3) +
geom_density(aes(y = after_stat(count) * bin_width),
alpha = 0, linewidth = 1.0, linetype = "dashed") +
facet_wrap(~Chromosome, scales = "free", ncol = ceiling(sqrt(n_chr))) +
scale_fill_manual(values = pal) +
scale_color_manual(values = pal) +
labs(title = "Marker Density Distribution",
subtitle = paste0("Bin width: ", bin_width, " cM"),
x = "Genetic Position (cM)",
y = "Number of Markers",
fill = "Chromosome",
color = "Chromosome") +
guides(fill = "none", color = "none") +
theme_minimal(base_size = 12) +
theme(
strip.text = element_text(face = "bold", size = 11,
color = "#1E3A8A", margin = margin(b=4)),
strip.background = element_rect(fill = "#DBEAFE", color = NA),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = 15, hjust = 0.5),
plot.subtitle = element_text(size = 10, hjust = 0.5, color = "gray50"),
plot.background = element_rect(fill = "#F8FAFF", color = NA),
panel.background = element_rect(fill = "#FFFFFF", color = NA)
)
}
# ═══════════════════════════════════════════════════════════════════════════════
# UI
# ═══════════════════════════════════════════════════════════════════════════════
ui <- fluidPage(
tags$head(
tags$style(HTML("
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
*, *::before, *::after { box-sizing: border-box; }
body {
background: #F0F4FF;
color: #1E293B;
font-family: 'DM Sans', sans-serif;
margin: 0; padding-bottom: 60px;
}
.navbar-default {
background: linear-gradient(135deg, #0F172A 0%, #1E3A8A 60%, #1D4ED8 100%) !important;
border: none !important;
box-shadow: 0 2px 16px rgba(0,0,0,0.35);
min-height: 52px;
}
.navbar-default .navbar-brand {
color: #E0EAFF !important;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.3px;
}
.navbar-default .navbar-nav > li > a {
color: #A5B4FC !important;
font-weight: 500;
font-size: 13px;
padding: 16px 14px;
transition: all .2s;
}
.navbar-default .navbar-nav > li > a:hover {
color: #fff !important;
background: rgba(255,255,255,0.10) !important;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:focus,
.navbar-default .navbar-nav > .active > a:hover {
color: #fff !important;
background: rgba(99,102,241,0.35) !important;
border-bottom: 3px solid #818CF8;
}
.card {
background: #fff;
border-radius: 12px;
border: 1px solid #E2E8F0;
box-shadow: 0 2px 12px rgba(30,58,138,0.07);
padding: 22px 24px;
margin-bottom: 20px;
}
.card h3 {
font-size: 16px; font-weight: 700; color: #1E3A8A;
margin: 0 0 16px; padding-bottom: 10px;
border-bottom: 2px solid #EEF2FF;
display: flex; align-items: center; gap: 8px;
}
.card h4 { font-size: 14px; font-weight: 600; color: #3B4FCC; margin: 16px 0 10px; }
.well {
background: #fff;
border: 1px solid #E2E8F0;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(30,58,138,0.07);
padding: 18px;
}
.form-control {
border: 1.5px solid #CBD5E1;
border-radius: 8px;
font-family: 'DM Sans', sans-serif;
font-size: 13px;
transition: border-color .2s;
background: #F8FAFF;
}
.form-control:focus { border-color: #6366F1; box-shadow: 0 0 0 3px rgba(99,102,241,0.15); }
.irs--shiny .irs-bar { background: linear-gradient(90deg,#6366F1,#3B82F6); }
.irs--shiny .irs-handle { background: #6366F1; border-color: #4F46E5; }
.irs--shiny .irs-from, .irs--shiny .irs-to, .irs--shiny .irs-single { background: #4F46E5; }
.btn {
border-radius: 8px;
font-family: 'DM Sans', sans-serif;
font-weight: 600;
font-size: 13px;
padding: 9px 18px;
transition: all .2s;
letter-spacing: 0.2px;
}
.btn-primary {
background: linear-gradient(135deg,#4F46E5,#2563EB);
border-color: #4338CA;
color: #fff;
}
.btn-primary:hover { background: linear-gradient(135deg,#4338CA,#1D4ED8); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79,70,229,0.4); }
.btn-success {
background: linear-gradient(135deg,#059669,#047857);
border-color: #065F46; color: #fff;
}
.btn-success:hover { background: linear-gradient(135deg,#047857,#065F46); transform: translateY(-1px); }
.btn-info {
background: linear-gradient(135deg,#0891B2,#0E7490);
border-color: #155E75; color: #fff;
}
.btn-info:hover { background: linear-gradient(135deg,#0E7490,#164E63); transform: translateY(-1px); }
.btn-warning {
background: linear-gradient(135deg,#D97706,#B45309);
border-color: #92400E; color: #fff;
}
.btn-block { width: 100%; display: block; }
.alert-info {
background: #EFF6FF; border: 1px solid #BFDBFE;
color: #1E40AF; border-radius: 10px; font-size: 13px;
}
.alert-warning {
background: #FFFBEB; border: 1px solid #FDE68A;
color: #92400E; border-radius: 10px; font-size: 13px;
}
.alert-success {
background: #ECFDF5; border: 1px solid #A7F3D0;
color: #065F46; border-radius: 10px; font-size: 13px;
}
.plot-wrapper {
background: #fff;
border-radius: 12px;
border: 1px solid #E2E8F0;
overflow: hidden;
box-shadow: 0 4px 20px rgba(30,58,138,0.08);
}
#cyto-scroll-wrapper {
overflow-x: auto; overflow-y: hidden;
background: #fff;
border-radius: 10px;
border: 1px solid #E2E8F0;
padding: 12px;
min-height: 200px;
}
#cytoCanvas {
display: block;
cursor: crosshair;
image-rendering: crisp-edges;
}
#cyto-tooltip {
position: fixed;
background: rgba(15,23,42,0.95);
color: #E0EAFF;
padding: 10px 14px;
border-radius: 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
pointer-events: none;
display: none;
z-index: 9999;
max-width: 280px;
line-height: 1.8;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
border: 1px solid rgba(99,102,241,0.4);
}
.cyto-controls {
display: flex; flex-wrap: wrap; gap: 12px;
align-items: center; margin-bottom: 14px;
padding: 12px 16px;
background: #F8FAFF;
border-radius: 8px;
border: 1px solid #E2E8F0;
}
.cyto-controls label { font-size: 12px; color: #64748B; margin: 0; font-weight: 600; }
.cyto-controls input[type=range] { width: 130px; accent-color: #6366F1; }
#cyto-legend {
display: flex; flex-wrap: wrap; gap: 16px;
margin-top: 14px; font-size: 12px;
align-items: center;
padding: 10px 14px;
background: #F8FAFF;
border-radius: 8px;
border: 1px solid #E2E8F0;
}
.legend-item { display: flex; align-items: center; gap: 7px; font-weight: 500; }
.legend-swatch { width: 18px; height: 18px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.1); }
.dataTables_wrapper { font-size: 13px; }
table.dataTable { border: 1px solid #E2E8F0 !important; border-radius: 8px; }
table.dataTable thead th {
background: linear-gradient(135deg,#1E3A8A,#1D4ED8);
color: #fff !important;
font-weight: 700;
border-color: #2D4EA0 !important;
}
table.dataTable tbody tr:hover { background: #EEF2FF !important; }
pre { font-family: 'JetBrains Mono', monospace; font-size: 12px; background: #F1F5F9; border-radius: 8px; border: 1px solid #E2E8F0; }
.footer {
position: fixed; bottom: 0; width: 100%;
background: linear-gradient(90deg, #0F172A, #1E3A8A);
color: #94A3B8; text-align: center;
padding: 9px; font-size: 11px; letter-spacing: 0.4px;
border-top: 1px solid #334155;
z-index: 100;
}
.chi-panel {
background: #F0FDF4; border: 1px solid #86EFAC;
border-radius: 8px; padding: 14px; margin-top: 10px;
}
.chi-disabled {
background: #FFF1F2; border: 1px solid #FECACA;
border-radius: 8px; padding: 12px; margin-top: 10px;
font-size: 12px; color: #991B1B;
}
.stat-badge {
display: inline-flex; align-items: center; justify-content: center;
background: linear-gradient(135deg,#EEF2FF,#DBEAFE);
border: 1px solid #BFDBFE;
border-radius: 20px;
padding: 4px 14px;
font-size: 12px; font-weight: 700; color: #1E40AF;
margin: 3px;
}
.tab-content { padding-top: 10px; }
"))
),
navbarPage(
title = "🧬 Linkage Map Creator v6.0",
id = "nav",
collapsible = TRUE,
tabPanel("πŸ“ Upload", icon = icon("upload"),
sidebarLayout(
sidebarPanel(width = 4,
div(class = "card",
h3("πŸ“‚ Upload Genotype Data"),
fileInput("file", "Choose Excel File (.xlsx)",
accept = c(".xlsx",".xls"),
buttonLabel = "Browse…",
placeholder = "No file selected"),
selectInput("population_type", "Population Type:",
choices = c("F2" = "F2", "RIL" = "RIL"),
selected = "F2"),
hr(),
div(class = "alert alert-info",
tags$b("✨ v6.0 Features:"), br(),
"β€’ High-resolution ggplot linkage map", br(),
"β€’ Recombination fraction heatmap", br(),
"β€’ Marker density distribution plot", br(),
"β€’ Cytogenetic ideogram with hover tooltips", br(),
"β€’ Allele frequency banding per chromosome", br(),
"β€’ Full Excel export with all analysis sheets"
)
)
),
mainPanel(width = 8,
div(class = "card",
h3("πŸ“Š Upload Status"),
verbatimTextOutput("upload_status")
),
div(class = "card",
h3("πŸ”¬ Data Preview (first 10 rows, converted)"),
div(style="overflow-x:auto;", dataTableOutput("preview_table"))
),
div(class = "card",
h3("🧬 Chromosome Assignments"),
div(style="overflow-x:auto;", dataTableOutput("chr_summary_table"))
)
)
)
),
tabPanel("βš™οΈ Parameters", icon = icon("sliders-h"),
sidebarLayout(
sidebarPanel(width = 4,
div(class = "card",
h3("βš™οΈ Analysis Parameters"),
sliderInput("missing_threshold", "Missing Data Threshold:",
min=0, max=1, value=0.3, step=0.05),
sliderInput("recombination_threshold", "Max Recombination Fraction (r):",
min=0, max=0.5, value=0.45, step=0.01),
hr(),
h4("πŸ§ͺ Chi-Square Filter"),
checkboxInput("enable_chi_square",
"Enable chi-square segregation distortion filter",
value = FALSE),
conditionalPanel("input.enable_chi_square == true",
div(class = "chi-panel",
numericInput("chi_square_alpha", "Significance level (Ξ±):",
min=0.001, max=0.2, value=0.05, step=0.005),
checkboxInput("use_bonferroni", "Apply Bonferroni correction", value=FALSE),
div(class="alert alert-warning", style="font-size:12px; margin-top:8px;",
"⚠️ Chi-square filtering removes markers with skewed allele ratios.")
)
),
conditionalPanel("input.enable_chi_square == false",
div(class="chi-disabled", "ℹ️ Chi-square filter is OFF.")
),
hr(),
h4("πŸ“ Map Scaling"),
numericInput("max_total_cm", "Max Total Map Length (cM):",
min=100, max=10000, value=1500, step=100),
selectInput("scaling_method", "Scaling Method:",
choices = c("Proportional"="proportional","Uniform"="uniform"),
selected = "proportional"),
hr(),
actionButton("run_analysis", "β–Ά Run Analysis",
class="btn-primary btn-block", icon=icon("play"))
)
),
mainPanel(width = 8,
div(class="card",
h3("πŸ“– Parameter Guide"),
tags$ul(
tags$li(tags$b("Missing Data Threshold:"), " Markers with > this fraction missing are excluded."),
tags$li(tags$b("Max Recombination Fraction:"), " Pairs with r β‰₯ threshold get a 50 cM gap."),
tags$li(tags$b("Monomorphic Removal:"), " Automatic β€” fixed allele markers excluded."),
tags$li(tags$b("Chi-Square Filter:"), " Optional β€” removes segregation-distorted markers."),
tags$li(tags$b("Map Scaling:"), " Proportional scales all chromosomes uniformly; Uniform equalises chromosome lengths.")
)
),
div(class="card",
h4("πŸ“ Scaling Summary"), verbatimTextOutput("scaling_summary"),
h4("Before Scaling"), verbatimTextOutput("before_scaling_stats"),
h4("After Scaling"), verbatimTextOutput("after_scaling_stats")
)
)
)
),
tabPanel("πŸ” QC Report", icon = icon("clipboard-check"),
div(class="card",
h3("πŸ“‹ Quality Control Report"),
verbatimTextOutput("qc_report_output")
),
div(class="card",
h3("πŸ—‘οΈ Removed Markers β€” Full Detail"),
div(style="overflow-x:auto;", dataTableOutput("removal_log_table")),
br(),
downloadButton("download_removal_log", "⬇ Download Removal Log (.xlsx)",
class="btn-success")
)
),
tabPanel("πŸ“Š Results", icon = icon("table"),
div(class="card",
h3("πŸ“Š Analysis Results"),
conditionalPanel("output.analysis_run",
fluidRow(
column(4, downloadButton("download_results",
"⬇ Full Excel Results",
class="btn-success btn-block")),
column(4, downloadButton("download_all_markers",
"⬇ All Markers + Positions",
class="btn-info btn-block")),
column(4, downloadButton("download_retained_markers",
"⬇ Retained Markers List",
class="btn-info btn-block"))
),
br(),
h4("βœ… Retained Markers"),
div(style="overflow-x:auto;", dataTableOutput("retained_markers_table")),
br(),
h4("πŸ“‹ All Markers β€” Status + Positions"),
div(style="overflow-x:auto;", dataTableOutput("all_markers_table")),
br(),
h4("πŸ—ΊοΈ Linkage Map Table"),
div(style="overflow-x:auto;", dataTableOutput("result_table")),
br(),
h4("πŸ“ˆ Summary Statistics"),
verbatimTextOutput("summary_stats")
),
conditionalPanel("!output.analysis_run",
p("Upload data and click 'Run Analysis' to see results.",
style="color:#94A3B8;font-style:italic;text-align:center;padding:50px;font-size:15px;")
)
)
),
tabPanel("πŸ—ΊοΈ Map Plot", icon = icon("chart-bar"),
sidebarLayout(
sidebarPanel(width = 3,
div(class="card",
h3("🎨 Plot Options"),
selectInput("gg_chromosome_select", "Chromosomes:",
choices=NULL, multiple=TRUE),
selectInput("gg_theme", "Plot Theme:",
choices = c("Dark"="dark","Blue/White"="blue",
"Minimal White"="minimal_white"),
selected = "dark"),
selectInput("gg_color_palette", "Color Palette:",
choices = c("Set1","Set2","Set3","Paired","Dark2",
"Accent","Spectral","RdYlBu","PRGn"),
selected = "Set1"),
checkboxInput("gg_show_labels", "Show Marker Labels", value=FALSE),
sliderInput("gg_label_size", "Label Size:", min=1.5, max=6, value=2.5, step=0.5),
sliderInput("gg_chrom_width", "Chromosome Width:", min=0.1, max=1.2, value=0.4, step=0.05),
sliderInput("gg_point_size", "Marker Point Size:", min=0.5, max=6, value=2.5, step=0.25),
sliderInput("gg_chrom_spacing", "LG Spacing:", min=1, max=6, value=2.5, step=0.25),
sliderInput("gg_plot_height", "Plot Height (px):", min=400, max=2000, value=700, step=50),
br(),
downloadButton("download_gg_plot", "⬇ Download PNG (300 dpi)",
class="btn-success btn-block")
)
),
mainPanel(width = 9,
div(class="card",
h3("πŸ—ΊοΈ Genetic Linkage Map"),
conditionalPanel("output.analysis_run",
div(class="plot-wrapper",
plotOutput("gg_linkage_plot", height = "auto"))
),
conditionalPanel("!output.analysis_run",
p("Run analysis first.", style="color:#94A3B8;text-align:center;padding:60px;")
)
)
)
)
),
tabPanel("πŸ“‰ Density & RF", icon = icon("chart-area"),
fluidRow(
column(12,
div(class="card",
h3("πŸ“Š Marker Density per Chromosome"),
conditionalPanel("output.analysis_run",
fluidRow(
column(6, selectInput("density_chr_select", "Chromosomes:",
choices=NULL, multiple=TRUE)),
column(3, sliderInput("density_bin", "Bin Width (cM):",
min=2, max=30, value=10, step=2)),
column(3, br(), downloadButton("download_density_plot",
"⬇ Download", class="btn-info btn-block"))
),
div(class="plot-wrapper",
plotOutput("density_plot", height="500px"))
),
conditionalPanel("!output.analysis_run",
p("Run analysis first.", style="color:#94A3B8;text-align:center;padding:60px;"))
)
)
),
fluidRow(
column(12,
div(class="card",
h3("🌑️ Adjacent Marker Recombination Heatmap"),
conditionalPanel("output.analysis_run",
fluidRow(
column(6, selectInput("heatmap_chr_select", "Chromosomes:",
choices=NULL, multiple=TRUE)),
column(3, sliderInput("heatmap_top_n", "Max Pairs to Show:",
min=10, max=200, value=60, step=10)),
column(3, br(), downloadButton("download_heatmap_plot",
"⬇ Download", class="btn-warning btn-block"))
),
div(class="plot-wrapper",
plotOutput("heatmap_plot", height="600px"))
),
conditionalPanel("!output.analysis_run",
p("Run analysis first.", style="color:#94A3B8;text-align:center;padding:60px;"))
)
)
)
),
tabPanel("🧬 Cytogenetic", icon = icon("dna"),
div(class="card",
h3("🧬 High-Resolution Cytogenetic Ideogram"),
conditionalPanel("output.analysis_run",
fluidRow(
column(8, selectInput("cyto_chromosome_select",
"Select Chromosomes:",
choices=NULL, multiple=TRUE)),
column(4, br(),
actionButton("cyto_save_png", "πŸ“· Save as PNG",
class="btn-primary btn-block"))
),
br(),
div(class="cyto-controls",
tags$label("Zoom:"),
tags$input(type="range", id="cytoZoom", min="0.5", max="3",
value="1", step="0.1",
oninput="updateCytoZoom(this.value)"),
tags$span(id="cytoZoomLabel", "1.0Γ—",
style="font-size:13px; min-width:40px; font-weight:600; color:#6366F1;")
),
div(id="cyto-scroll-wrapper",
tags$canvas(id="cytoCanvas", width="1200", height="600")
),
div(id="cyto-legend",
div(class="legend-item",
div(class="legend-swatch", id="sw-a"),
tags$span("Parent A (aa)")),
div(class="legend-item",
div(class="legend-swatch", id="sw-b"),
tags$span("Parent B (bb)")),
div(class="legend-item",
div(class="legend-swatch", id="sw-ab"),
tags$span("Heterozygous (ab)")),
div(class="legend-item",
div(class="legend-swatch",
style="background:rgba(148,163,184,0.4); border:1px dashed #94A3B8;"),
tags$span("Missing / unknown"))
),
br(),
fluidRow(
column(3, sliderInput("cyto_chrom_width", "Chromosome Width:",
min=10, max=80, value=28, step=2)),
column(3, sliderInput("cyto_chrom_spacing", "Spacing:",
min=20, max=150, value=60, step=5)),
column(3, sliderInput("cyto_px_per_cm", "px per cM:",
min=1, max=25, value=5, step=0.5)),
column(3, sliderInput("cyto_font_size", "Font Size:",
min=8, max=20, value=11, step=1))
),
fluidRow(
column(4, selectInput("cyto_color_scheme", "Color Scheme:",
choices = c("Dark Ideogram"="classic",
"Genomics Blue"="blue_orange",
"Forest"="green_purple",
"Red/Blue"="red_blue",
"Pastel"="pastel"),
selected = "classic")),
column(4, checkboxInput("cyto_show_scale", "Show cM scale bar", value=TRUE)),
column(4, checkboxInput("cyto_show_marker_names",
"Show marker names (≀200 markers)", value=FALSE))
)
),
conditionalPanel("!output.analysis_run",
p("Run analysis first.", style="color:#94A3B8;text-align:center;padding:60px;"))
)
),
tabPanel("❓ Help", icon = icon("question-circle"),
div(class="card",
h3("πŸ“– Application Guide β€” v6.0"),
h4("Required Input Format"),
div(class="alert alert-info",
tags$ul(
tags$li("Excel (.xlsx): Column 1 = 'Marker', Column 2 = 'Chromosome'"),
tags$li("Genotype columns 3+: individual sample genotype calls"),
tags$li("Accepted codes: a / b / ab / – OR numeric 2 / 1 / 0 / –1"),
tags$li("Chromosome column: integer numbers or Excel VLOOKUP formulas")
)
),
h4("Genotype Encoding Reference"),
tableOutput("encoding_table"),
h4("Analysis Workflow"),
tags$ol(
tags$li("Upload your .xlsx genotype file in the Upload tab."),
tags$li("Choose population type (F2 or RIL)."),
tags$li("Adjust QC parameters in the Parameters tab."),
tags$li("Click 'Run Analysis'."),
tags$li("View QC report, download results, and explore all visualisations.")
),
h4("Plots Available"),
tags$ul(
tags$li(tags$b("Map Plot:"), " High-resolution ggplot linkage map with chromosome ideograms."),
tags$li(tags$b("Density & RF:"), " Marker density histograms + recombination fraction heatmap."),
tags$li(tags$b("Cytogenetic:"), " HTML canvas ideogram with allele-colour banding and hover tooltips.")
)
)
)
),
div(id = "cyto-tooltip"),
tags$script(HTML("
var cytoData = null;
var cytoSettings = {};
var cytoZoomVal = 1.0;
var colorSchemes = {
classic: { a:'#1a1a2e', b:'#f5f5f5', ab:'#F59E0B', miss:'rgba(148,163,184,0.25)', border:'#475569', text:'#1E293B' },
blue_orange: { a:'#1D4ED8', b:'#F97316', ab:'#FEF08A', miss:'rgba(148,163,184,0.25)', border:'#334155', text:'#0F172A' },
green_purple: { a:'#14532D', b:'#581C87', ab:'#FEF08A', miss:'rgba(148,163,184,0.25)', border:'#334155', text:'#0F172A' },
red_blue: { a:'#991B1B', b:'#1E3A8A', ab:'#FCD34D', miss:'rgba(148,163,184,0.25)', border:'#374151', text:'#111827' },
pastel: { a:'#93C5FD', b:'#F9A8D4', ab:'#86EFAC', miss:'rgba(148,163,184,0.25)', border:'#CBD5E1', text:'#334155' }
};
function updateLegendSwatches(scheme) {
var cs = colorSchemes[scheme] || colorSchemes.classic;
var sa = document.getElementById('sw-a');
var sb = document.getElementById('sw-b');
var sab = document.getElementById('sw-ab');
if (sa) sa.style.background = cs.a;
if (sb) sb.style.background = cs.b;
if (sab) sab.style.background = cs.ab;
}
function updateCytoZoom(val) {
cytoZoomVal = parseFloat(val);
var lbl = document.getElementById('cytoZoomLabel');
if (lbl) lbl.textContent = parseFloat(val).toFixed(1) + 'Γ—';
if (cytoData) renderCytogeneticMap();
}
function renderCytogeneticMap() {
if (!cytoData || !cytoData.chromosomes) return;
var canvas = document.getElementById('cytoCanvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var chromWidth = Math.round((cytoSettings.chromWidth || 28) * cytoZoomVal);
var chromSpacing = Math.round((cytoSettings.chromSpacing || 60) * cytoZoomVal);
var pxPerCm = (cytoSettings.pxPerCm || 5) * cytoZoomVal;
var fontSize = Math.round((cytoSettings.fontSize || 11) * Math.max(0.75, cytoZoomVal));
var showScale = cytoSettings.showScale !== false;
var showNames = cytoSettings.showNames || false;
var scheme = colorSchemes[cytoSettings.colorScheme] || colorSchemes.classic;
var chrs = cytoData.chromosomes;
var n_chr = chrs.length;
if (n_chr === 0) return;
var maxPos = 0;
for (var i = 0; i < n_chr; i++) {
if (chrs[i].max_pos > maxPos) maxPos = chrs[i].max_pos;
}
var topPad = Math.round(70 * cytoZoomVal);
var botPad = Math.round(80 * cytoZoomVal);
var leftPad = Math.round(70 * cytoZoomVal);
var rightPad = Math.round(30 * cytoZoomVal);
var mapHeight = Math.ceil(maxPos * pxPerCm);
var totalW = leftPad + n_chr * chromWidth + (n_chr - 1) * chromSpacing + rightPad;
var totalH = topPad + mapHeight + botPad;
canvas.width = Math.max(totalW, 300);
canvas.height = Math.max(totalH, 300);
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
// Background gradient
var bgGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
bgGrad.addColorStop(0, '#FAFBFF');
bgGrad.addColorStop(1, '#F0F4FF');
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Horizontal grid lines
if (maxPos > 0) {
var scaleSteps = [5, 10, 20, 25, 50, 100, 150, 200];
var gridStep = 10;
for (var si = 0; si < scaleSteps.length; si++) {
if (mapHeight / (maxPos / scaleSteps[si]) >= 20) { gridStep = scaleSteps[si]; break; }
}
ctx.save();
ctx.strokeStyle = 'rgba(99,102,241,0.10)';
ctx.lineWidth = 1;
ctx.setLineDash([4, 6]);
for (var pos = 0; pos <= maxPos; pos += gridStep) {
var gy = topPad + Math.round(pos * pxPerCm);
ctx.beginPath();
ctx.moveTo(leftPad - 8, gy);
ctx.lineTo(canvas.width - rightPad, gy);
ctx.stroke();
}
ctx.setLineDash([]);
ctx.restore();
}
canvas._hitRegions = [];
for (var ci = 0; ci < n_chr; ci++) {
var chr = chrs[ci];
var chrH = Math.max(Math.ceil(chr.max_pos * pxPerCm), 10);
var x0 = leftPad + ci * (chromWidth + chromSpacing);
var y0 = topPad;
var markers = chr.markers;
var nMk = markers.length;
// Draw shadow
ctx.save();
ctx.shadowColor = 'rgba(30,58,138,0.18)';
ctx.shadowBlur = Math.round(8 * cytoZoomVal);
ctx.shadowOffsetX = Math.round(2 * cytoZoomVal);
ctx.shadowOffsetY = Math.round(2 * cytoZoomVal);
ctx.fillStyle = '#ccc';
var rShadow = Math.min(chromWidth * 0.35, 10);
ctx.beginPath();
ctx.moveTo(x0 + rShadow, y0);
ctx.lineTo(x0 + chromWidth - rShadow, y0);
ctx.quadraticCurveTo(x0 + chromWidth, y0, x0 + chromWidth, y0 + rShadow);
ctx.lineTo(x0 + chromWidth, y0 + chrH - rShadow);
ctx.quadraticCurveTo(x0 + chromWidth, y0 + chrH, x0 + chromWidth - rShadow, y0 + chrH);
ctx.lineTo(x0 + rShadow, y0 + chrH);
ctx.quadraticCurveTo(x0, y0 + chrH, x0, y0 + chrH - rShadow);
ctx.lineTo(x0, y0 + rShadow);
ctx.quadraticCurveTo(x0, y0, x0 + rShadow, y0);
ctx.closePath();
ctx.fill();
ctx.restore();
// Clip to chromosome shape
var rr = Math.min(chromWidth * 0.35, 10);
ctx.save();
ctx.beginPath();
ctx.moveTo(x0 + rr, y0);
ctx.lineTo(x0 + chromWidth - rr, y0);
ctx.quadraticCurveTo(x0 + chromWidth, y0, x0 + chromWidth, y0 + rr);
ctx.lineTo(x0 + chromWidth, y0 + chrH - rr);
ctx.quadraticCurveTo(x0 + chromWidth, y0 + chrH, x0 + chromWidth - rr, y0 + chrH);
ctx.lineTo(x0 + rr, y0 + chrH);
ctx.quadraticCurveTo(x0, y0 + chrH, x0, y0 + chrH - rr);
ctx.lineTo(x0, y0 + rr);
ctx.quadraticCurveTo(x0, y0, x0 + rr, y0);
ctx.closePath();
ctx.clip();
// Base fill
ctx.fillStyle = scheme.miss;
ctx.fillRect(x0, y0, chromWidth, chrH);
// Bands
for (var mi = 0; mi < nMk; mi++) {
var mk = markers[mi];
var yTop = y0 + Math.round(mk.position * pxPerCm);
var yBot = (mi < nMk - 1)
? y0 + Math.round(markers[mi+1].position * pxPerCm)
: y0 + chrH;
var bandH = Math.max(yBot - yTop, 1);
var dom = 'miss', maxFrac = 0;
if (mk.allele_a > maxFrac) { maxFrac = mk.allele_a; dom = 'a'; }
if (mk.allele_b > maxFrac) { maxFrac = mk.allele_b; dom = 'b'; }
if (mk.allele_ab > maxFrac) { maxFrac = mk.allele_ab; dom = 'ab'; }
ctx.fillStyle = scheme[dom] || scheme.miss;
ctx.fillRect(x0, yTop, chromWidth, bandH);
// Subtle band divider
if (mi > 0 && bandH > 3) {
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 0.8;
ctx.beginPath();
ctx.moveTo(x0, yTop);
ctx.lineTo(x0 + chromWidth, yTop);
ctx.stroke();
}
// Marker name in band
if (showNames && bandH >= fontSize + 3) {
ctx.save();
ctx.fillStyle = (dom === 'a' || dom === 'green_purple') ? '#FFFFFF' : '#0F172A';
ctx.font = 'bold ' + (fontSize - 1) + 'px JetBrains Mono, monospace';
ctx.textAlign = 'left';
ctx.fillText(mk.marker.substring(0, 14), x0 + 4,
yTop + bandH / 2 + (fontSize - 1) / 3);
ctx.restore();
}
canvas._hitRegions.push({
x: x0, y: yTop, w: chromWidth, h: bandH,
marker: mk.marker, position: mk.position,
chromosome: chr.chromosome,
a: mk.allele_a, b: mk.allele_b, ab: mk.allele_ab, miss: mk.allele_miss
});
}
ctx.restore();
// Chromosome border with gradient stroke
ctx.save();
var gradBorder = ctx.createLinearGradient(x0, 0, x0 + chromWidth, 0);
gradBorder.addColorStop(0, 'rgba(99,102,241,0.6)');
gradBorder.addColorStop(0.5, 'rgba(99,102,241,0.9)');
gradBorder.addColorStop(1, 'rgba(99,102,241,0.6)');
ctx.strokeStyle = gradBorder;
ctx.lineWidth = Math.max(1.2, cytoZoomVal * 1.2);
var rb = Math.min(chromWidth * 0.35, 10);
ctx.beginPath();
ctx.moveTo(x0 + rb, y0);
ctx.lineTo(x0 + chromWidth - rb, y0);
ctx.quadraticCurveTo(x0 + chromWidth, y0, x0 + chromWidth, y0 + rb);
ctx.lineTo(x0 + chromWidth, y0 + chrH - rb);
ctx.quadraticCurveTo(x0 + chromWidth, y0 + chrH, x0 + chromWidth - rb, y0 + chrH);
ctx.lineTo(x0 + rb, y0 + chrH);
ctx.quadraticCurveTo(x0, y0 + chrH, x0, y0 + chrH - rb);
ctx.lineTo(x0, y0 + rb);
ctx.quadraticCurveTo(x0, y0, x0 + rb, y0);
ctx.closePath();
ctx.stroke();
ctx.restore();
// Centromere-style notch
if (chrH > 30) {
var centY = y0 + Math.round(chrH * 0.45);
ctx.save();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.strokeStyle = 'rgba(99,102,241,0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x0, centY - 4);
ctx.lineTo(x0 + chromWidth / 2, centY);
ctx.lineTo(x0 + chromWidth, centY - 4);
ctx.lineTo(x0 + chromWidth, centY + 4);
ctx.lineTo(x0 + chromWidth / 2, centY);
ctx.lineTo(x0, centY + 4);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
// Label below
ctx.save();
ctx.fillStyle = '#1E3A8A';
ctx.font = 'bold ' + fontSize + 'px DM Sans, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Chr ' + chr.chromosome, x0 + chromWidth / 2,
y0 + chrH + fontSize + 10);
ctx.font = (fontSize - 1) + 'px DM Sans, sans-serif';
ctx.fillStyle = '#6366F1';
ctx.fillText('n=' + chr.n_markers, x0 + chromWidth / 2,
y0 + chrH + fontSize * 2 + 14);
ctx.restore();
// Length label above
ctx.save();
ctx.fillStyle = '#475569';
ctx.font = (fontSize - 1) + 'px DM Sans, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(chr.max_pos.toFixed(1) + ' cM', x0 + chromWidth / 2, y0 - 10);
ctx.restore();
}
// cM scale bar
if (showScale && maxPos > 0) {
var scaleSteps2 = [5, 10, 20, 25, 50, 100, 150, 200];
var step2 = 10;
for (var si2 = 0; si2 < scaleSteps2.length; si2++) {
if (mapHeight / (maxPos / scaleSteps2[si2]) >= 22) { step2 = scaleSteps2[si2]; break; }
}
ctx.save();
ctx.strokeStyle = '#6366F1';
ctx.lineWidth = 1.5;
ctx.fillStyle = '#374151';
ctx.font = (fontSize - 1) + 'px JetBrains Mono, monospace';
ctx.textAlign = 'right';
var scaleX = leftPad - 14;
ctx.beginPath();
ctx.moveTo(scaleX, topPad);
ctx.lineTo(scaleX, topPad + mapHeight);
ctx.stroke();
for (var pos2 = 0; pos2 <= maxPos; pos2 += step2) {
var ty2 = topPad + Math.round(pos2 * pxPerCm);
ctx.beginPath();
ctx.moveTo(scaleX - 5, ty2);
ctx.lineTo(scaleX, ty2);
ctx.stroke();
ctx.fillText(pos2 + '', scaleX - 7, ty2 + 4);
}
// Scale label
ctx.save();
ctx.translate(scaleX - 22, topPad + mapHeight / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillStyle = '#6366F1';
ctx.font = 'bold ' + fontSize + 'px DM Sans, sans-serif';
ctx.fillText('Genetic Position (cM)', 0, 0);
ctx.restore();
ctx.restore();
}
}
// Tooltip
document.addEventListener('mousemove', function(e) {
var canvas = document.getElementById('cytoCanvas');
if (!canvas || !canvas._hitRegions) return;
var rect = canvas.getBoundingClientRect();
var scaleX = canvas.width / rect.width;
var scaleY = canvas.height / rect.height;
var cx = (e.clientX - rect.left) * scaleX;
var cy = (e.clientY - rect.top) * scaleY;
var tip = document.getElementById('cyto-tooltip');
var found = null;
var regs = canvas._hitRegions;
for (var i = 0; i < regs.length; i++) {
var r = regs[i];
if (cx >= r.x && cx <= r.x + r.w && cy >= r.y && cy <= r.y + r.h) {
found = r; break;
}
}
if (found) {
tip.style.display = 'block';
tip.style.left = (e.clientX + 16) + 'px';
tip.style.top = (e.clientY - 12) + 'px';
tip.innerHTML =
'<b style=\"color:#818CF8\">' + found.marker + '</b><br>' +
'Chr ' + found.chromosome + ' &nbsp;|&nbsp; ' + found.position.toFixed(3) + ' cM<br>' +
'<span style=\"color:#86EFAC\">Parent A:</span> ' + (found.a * 100).toFixed(1) + '%<br>' +
'<span style=\"color:#FDA4AF\">Parent B:</span> ' + (found.b * 100).toFixed(1) + '%<br>' +
'<span style=\"color:#FCD34D\">Het (ab): </span>' + (found.ab * 100).toFixed(1) + '%<br>' +
'<span style=\"color:#94A3B8\">Missing: </span>' + (found.miss * 100).toFixed(1) + '%';
} else {
tip.style.display = 'none';
}
});
// Receive plot data from Shiny
Shiny.addCustomMessageHandler('updateCytoPlot', function(msg) {
cytoData = msg.data;
cytoSettings = msg.settings;
updateLegendSwatches(cytoSettings.colorScheme || 'classic');
cytoZoomVal = 1.0;
var zs = document.getElementById('cytoZoom');
if (zs) zs.value = '1';
var zl = document.getElementById('cytoZoomLabel');
if (zl) zl.textContent = '1.0Γ—';
renderCytogeneticMap();
});
Shiny.addCustomMessageHandler('updateCytoSettings', function(msg) {
cytoSettings = msg;
updateLegendSwatches(cytoSettings.colorScheme || 'classic');
if (cytoData) renderCytogeneticMap();
});
// Save cytogenetic PNG
document.addEventListener('click', function(e) {
if (e.target && (e.target.id === 'cyto_save_png' ||
e.target.closest && e.target.closest('#cyto_save_png'))) {
var canvas = document.getElementById('cytoCanvas');
if (!canvas) return;
var link = document.createElement('a');
link.download = 'cytogenetic_map_' + new Date().toISOString().slice(0,10) + '.png';
link.href = canvas.toDataURL('image/png', 1.0);
link.click();
}
});
")),
div(class = "footer",
"🧬 Linkage Map Creator v6.0 | High-Res Cytogenetic + ggplot Visualisation | Kosambi cM | © 2025")
)
# ═══════════════════════════════════════════════════════════════════════════════
# SERVER
# ═══════════════════════════════════════════════════════════════════════════════
server <- function(input, output, session) {
uploaded_data <- reactiveVal(NULL)
analysis_results <- reactiveVal(NULL)
upload_log <- reactiveVal("")
output$encoding_table <- renderTable({
data.frame(
`Numeric Code` = c("2", "1", "0", "-1"),
`Text Code` = c("a", "b", "ab", "-"),
`Meaning` = c("Homozygous Parent 1 (AA)",
"Homozygous Parent 2 (BB)",
"Heterozygous (Aa)",
"Missing data"),
check.names = FALSE
)
}, striped=TRUE, hover=TRUE, bordered=TRUE)
# ── Upload ──────────────────────────────────────────────────────────────────
observeEvent(input$file, {
req(input$file)
tryCatch({
df <- load_and_convert_data(input$file$datapath)
n_chr <- length(unique(df$Chromosome))
log_msg <- paste0(
"βœ“ File loaded: ", input$file$name, "\n",
" Markers : ", nrow(df), "\n",
" Individuals : ", ncol(df) - 3, "\n",
" Chromosomes : ", n_chr, "\n",
" (", paste(sort(unique(df$Chromosome)), collapse=", "), ")\n",
" Genotype codes auto-converted to a/b/ab/-\n",
" Marker order will be PRESERVED from input file.\n"
)
uploaded_data(df)
upload_log(log_msg)
showNotification("βœ“ File uploaded successfully!", type="message", duration=3)
}, error=function(e) {
upload_log(paste0("βœ— Error: ", e$message))
showNotification(paste("Upload error:", e$message), type="error")
})
})
output$upload_status <- renderText({ upload_log() })
output$preview_table <- renderDataTable({
req(uploaded_data())
df_show <- head(uploaded_data(), 10)
df_show <- df_show[, colnames(df_show) != ".input_order", drop=FALSE]
datatable(df_show, options=list(scrollX=TRUE, dom="t", pageLength=10), rownames=FALSE)
})
output$chr_summary_table <- renderDataTable({
req(uploaded_data())
df <- uploaded_data()
chr_tab <- df %>%
group_by(Chromosome) %>%
summarise(N_Markers=n(), .groups="drop") %>%
arrange(suppressWarnings(as.numeric(Chromosome)))
datatable(chr_tab, options=list(dom="t", pageLength=30), rownames=FALSE)
})
# ── Run analysis ─────────────────────────────────────────────────────────────
observeEvent(input$run_analysis, {
req(uploaded_data())
showModal(modalDialog(
title = "πŸ”¬ Building Linkage Map…",
paste0("Population: ", input$population_type, "\n",
if (input$enable_chi_square) paste0("Chi-square alpha = ", input$chi_square_alpha)
else "Chi-square: DISABLED"),
footer = NULL, easyClose = FALSE
))
tryCatch({
result <- create_linkage_map(
df = uploaded_data(),
population_type = input$population_type,
missing_threshold = input$missing_threshold,
recombination_threshold = input$recombination_threshold,
max_total_cm = input$max_total_cm,
scaling_method = input$scaling_method,
enable_chi_square = input$enable_chi_square,
chi_square_alpha = input$chi_square_alpha,
use_bonferroni = input$use_bonferroni
)
analysis_results(result)
chromosomes <- unique(result$linkage_map$Chromosome)
chr_sorted <- tryCatch(
chromosomes[order(as.numeric(chromosomes))],
warning = function(w) sort(chromosomes),
error = function(e) sort(chromosomes)
)
updateSelectInput(session, "gg_chromosome_select", choices=chr_sorted, selected=chr_sorted)
updateSelectInput(session, "cyto_chromosome_select", choices=chr_sorted, selected=chr_sorted)
updateSelectInput(session, "density_chr_select", choices=chr_sorted, selected=chr_sorted)
updateSelectInput(session, "heatmap_chr_select", choices=chr_sorted, selected=chr_sorted)
removeModal()
n_pos <- sum(!is.na(result$linkage_map$Position))
n_removed <- nrow(result$removal_log)
showNotification(
paste0("βœ“ Analysis complete! ", n_pos, " markers positioned. ",
n_removed, " removed. See QC Report."),
type="message", duration=5
)
}, error=function(e) {
removeModal()
showNotification(paste("βœ— Analysis error:", e$message), type="error", duration=10)
})
})
output$analysis_run <- reactive({ !is.null(analysis_results()) })
outputOptions(output, "analysis_run", suspendWhenHidden=FALSE)
# ── Cytogenetic canvas ────────────────────────────────────────────────────────
observe({
req(analysis_results(), input$cyto_chromosome_select)
res <- analysis_results()
lm <- res$linkage_map
df_g <- res$original_data
# Build cytogenetic plot data
cyto_data_list <- build_cyto_plot_data(lm, df_g, input$cyto_chromosome_select)
# Convert list to array for JavaScript
chromosomes_array <- list()
for (chr_name in names(cyto_data_list)) {
chromosomes_array[[length(chromosomes_array) + 1]] <- cyto_data_list[[chr_name]]
}
msg <- list(
data = list(chromosomes = chromosomes_array),
settings = list(
chromWidth = input$cyto_chrom_width,
chromSpacing = input$cyto_chrom_spacing,
pxPerCm = input$cyto_px_per_cm,
fontSize = input$cyto_font_size,
showScale = input$cyto_show_scale,
showNames = input$cyto_show_marker_names,
colorScheme = input$cyto_color_scheme
)
)
session$sendCustomMessage("updateCytoPlot", msg)
})
observeEvent(
list(input$cyto_chrom_width, input$cyto_chrom_spacing,
input$cyto_px_per_cm, input$cyto_font_size,
input$cyto_show_scale, input$cyto_show_marker_names,
input$cyto_color_scheme), {
req(analysis_results())
session$sendCustomMessage("updateCytoSettings", list(
chromWidth = input$cyto_chrom_width,
chromSpacing = input$cyto_chrom_spacing,
pxPerCm = input$cyto_px_per_cm,
fontSize = input$cyto_font_size,
showScale = input$cyto_show_scale,
showNames = input$cyto_show_marker_names,
colorScheme = input$cyto_color_scheme
))
}, ignoreInit=TRUE)
# ── QC outputs ───────────────────────────────────────────────────────────────
output$qc_report_output <- renderText({
if (is.null(analysis_results())) return("No analysis run yet.")
analysis_results()$qc_report
})
output$removal_log_table <- renderDataTable({
if (is.null(analysis_results()))
return(datatable(data.frame(Message="Run analysis first."),
options=list(dom="t"), rownames=FALSE))
log_df <- analysis_results()$removal_log
if (nrow(log_df) == 0)
return(datatable(data.frame(Message="No markers were removed."),
options=list(dom="t"), rownames=FALSE))
datatable(log_df, options=list(scrollX=TRUE, pageLength=25), rownames=FALSE,
caption=paste0(nrow(log_df), " marker(s) removed")) %>%
formatStyle("Reason",
color = styleEqual(
c("Excess missing data","Monomorphic marker","Segregation distortion (chi-square)"),
c("#DC2626","#7C3AED","#D97706")),
fontWeight = "bold")
})
# ── Results tab tables ────────────────────────────────────────────────────────
output$retained_markers_table <- renderDataTable({
req(analysis_results())
rml <- analysis_results()$retained_markers_list
if (is.null(rml) || nrow(rml) == 0)
return(datatable(data.frame(Message="No retained markers."),
options=list(dom="t"), rownames=FALSE))
datatable(rml, options=list(scrollX=TRUE, pageLength=25), rownames=FALSE) %>%
formatRound("Position", 3)
})
output$all_markers_table <- renderDataTable({
req(analysis_results())
amp <- analysis_results()$all_markers_positions
if (is.null(amp) || nrow(amp) == 0)
return(datatable(data.frame(Message="No marker data."),
options=list(dom="t"), rownames=FALSE))
datatable(amp, options=list(scrollX=TRUE, pageLength=25), rownames=FALSE) %>%
formatRound("Position", 3) %>%
formatStyle("Status",
color = styleEqual(c("Retained","Removed"),
c("#059669","#DC2626")),
fontWeight = "bold")
})
output$result_table <- renderDataTable({
req(analysis_results())
lm <- analysis_results()$linkage_map
if (nrow(lm) == 0)
return(datatable(data.frame(Message="No positioned markers."),
options=list(dom="t"), rownames=FALSE))
datatable(lm, options=list(scrollX=TRUE, pageLength=25), rownames=FALSE) %>%
formatRound("Position", 2)
})
output$summary_stats <- renderText({
req(analysis_results())
lm <- analysis_results()$linkage_map
rec <- analysis_results()$recombination_data
if (nrow(lm) == 0) return("No positioned markers.")
n_pos <- sum(!is.na(lm$Position))
n_chr <- length(unique(lm$Chromosome))
chr_lengths <- lm %>%
filter(!is.na(Position)) %>%
group_by(Chromosome) %>%
summarise(Length=max(Position,na.rm=TRUE), N_markers=n(), .groups="drop") %>%
arrange(suppressWarnings(as.numeric(Chromosome)))
total_len <- sum(chr_lengths$Length)
avg_len <- mean(chr_lengths$Length)
chr_detail <- paste(apply(chr_lengths, 1, function(x)
paste0(" Chr ", x[1], " : ", round(as.numeric(x[2]),2),
" cM (", x[3], " markers)")), collapse="\n")
log_df <- analysis_results()$removal_log
n_removed <- nrow(log_df)
n_mono <- sum(log_df$Reason == "Monomorphic marker")
n_miss <- sum(log_df$Reason == "Excess missing data")
n_chi <- sum(log_df$Reason == "Segregation distortion (chi-square)")
chi_line <- if (input$enable_chi_square)
paste0(" Removed (chi-square) : ", n_chi)
else " Chi-square filter : DISABLED"
paste0(
"LINKAGE MAP SUMMARY (v6.0)\n===========================\n",
"Population type : ", input$population_type, "\n",
"Marker order : Preserved from input file\n\n",
"Total markers input : ", nrow(analysis_results()$original_data), "\n",
" Removed (missing) : ", n_miss, "\n",
" Removed (monomorphic): ", n_mono, "\n",
chi_line, "\n",
" Total removed : ", n_removed, "\n",
"Markers positioned : ", n_pos, "\n",
"Chromosomes / LGs : ", n_chr, "\n",
"Total map length : ", round(total_len,2), " cM\n",
"Avg chromosome length : ", round(avg_len, 2), " cM\n",
"Marker pairs analysed : ", nrow(rec), "\n\n",
"PER-CHROMOSOME DETAILS:\n", chr_detail
)
})
output$scaling_summary <- renderText({
req(analysis_results())
paste0("Method : ", input$scaling_method, "\nMax cM : ", input$max_total_cm)
})
fmt_chr <- function(df) {
if (is.null(df) || nrow(df) == 0) return("No data.")
total <- sum(df$Original_Length, na.rm=TRUE)
rows <- apply(df, 1, function(x)
paste0(" Chr ", x[1], " : ", round(as.numeric(x[2]),2), " cM"))
paste0("Total : ", round(total,2), " cM\n", paste(rows, collapse="\n"))
}
output$before_scaling_stats <- renderText({
req(analysis_results()); fmt_chr(analysis_results()$chrom_lengths_before)
})
output$after_scaling_stats <- renderText({
req(analysis_results()); fmt_chr(analysis_results()$chrom_lengths_after)
})
# ── ggplot linkage map ────────────────────────────────────────────────────────
gg_plot_reactive <- reactive({
req(analysis_results(), input$gg_chromosome_select)
create_ggplot_linkage_map(
linkage_map = analysis_results()$linkage_map,
selected_chromosomes = input$gg_chromosome_select,
show_labels = input$gg_show_labels,
label_size = input$gg_label_size,
color_palette = input$gg_color_palette,
chrom_width = input$gg_chrom_width,
point_size = input$gg_point_size,
chrom_spacing = input$gg_chrom_spacing,
plot_theme = input$gg_theme
)
})
output$gg_linkage_plot <- renderPlot({
gg_plot_reactive()
}, height = function() input$gg_plot_height, res = 110)
output$download_gg_plot <- downloadHandler(
filename = function() paste0("linkage_map_", Sys.Date(), ".png"),
content = function(file) {
req(analysis_results(), input$gg_chromosome_select)
p <- create_ggplot_linkage_map(
linkage_map = analysis_results()$linkage_map,
selected_chromosomes = input$gg_chromosome_select,
show_labels = input$gg_show_labels,
label_size = input$gg_label_size,
color_palette = input$gg_color_palette,
chrom_width = input$gg_chrom_width,
point_size = input$gg_point_size,
chrom_spacing = input$gg_chrom_spacing,
plot_theme = input$gg_theme
)
n_chr <- length(input$gg_chromosome_select)
w_in <- max(7, n_chr * 1.2 + 2)
ggsave(file, p, width=w_in, height=10, dpi=300, bg="white")
}
)
# ── Density plot ──────────────────────────────────────────────────────────────
output$density_plot <- renderPlot({
req(analysis_results(), input$density_chr_select)
create_marker_density_plot(
analysis_results()$linkage_map,
input$density_chr_select,
input$density_bin
)
}, res=110)
output$download_density_plot <- downloadHandler(
filename = function() paste0("marker_density_", Sys.Date(), ".png"),
content = function(file) {
req(analysis_results(), input$density_chr_select)
p <- create_marker_density_plot(analysis_results()$linkage_map,
input$density_chr_select, input$density_bin)
ggsave(file, p, width=12, height=8, dpi=300, bg="white")
}
)
# ── Heatmap plot ──────────────────────────────────────────────────────────────
output$heatmap_plot <- renderPlot({
req(analysis_results(), input$heatmap_chr_select)
create_recombination_heatmap(
analysis_results()$recombination_data,
input$heatmap_chr_select,
input$heatmap_top_n
)
}, res=110)
output$download_heatmap_plot <- downloadHandler(
filename = function() paste0("recombination_heatmap_", Sys.Date(), ".png"),
content = function(file) {
req(analysis_results(), input$heatmap_chr_select)
p <- create_recombination_heatmap(analysis_results()$recombination_data,
input$heatmap_chr_select, input$heatmap_top_n)
ggsave(file, p, width=14, height=10, dpi=300, bg="white")
}
)
# ── Downloads ─────────────────────────────────────────────────────────────────
output$download_removal_log <- downloadHandler(
filename = function() paste0("removal_log_", Sys.Date(), ".xlsx"),
content = function(file) {
req(analysis_results())
wb <- createWorkbook()
addWorksheet(wb, "Removed_Markers")
log_df <- analysis_results()$removal_log
if (nrow(log_df) == 0) log_df <- data.frame(Message="No markers were removed.")
writeData(wb, "Removed_Markers", log_df)
saveWorkbook(wb, file, overwrite=TRUE)
}
)
output$download_all_markers <- downloadHandler(
filename = function() paste0("all_markers_positions_", Sys.Date(), ".xlsx"),
content = function(file) {
req(analysis_results())
amp <- analysis_results()$all_markers_positions
wb <- createWorkbook()
addWorksheet(wb, "All_Markers_Positions")
writeData(wb, "All_Markers_Positions", amp)
hs <- createStyle(fontColour="#FFFFFF", fgFill="#1E3A8A",
halign="CENTER", textDecoration="Bold", border="Bottom")
addStyle(wb, "All_Markers_Positions", hs, rows=1, cols=1:ncol(amp), gridExpand=TRUE)
removed_rows <- which(amp$Status == "Removed") + 1
retained_rows <- which(amp$Status == "Retained") + 1
if (length(removed_rows) > 0) addStyle(wb, "All_Markers_Positions",
createStyle(fgFill="#FEE2E2"), rows=removed_rows, cols=1:ncol(amp), gridExpand=TRUE)
if (length(retained_rows) > 0) addStyle(wb, "All_Markers_Positions",
createStyle(fgFill="#D1FAE5"), rows=retained_rows, cols=1:ncol(amp), gridExpand=TRUE)
setColWidths(wb, "All_Markers_Positions", cols=1:ncol(amp),
widths=c(20,12,10,12,30,40))
saveWorkbook(wb, file, overwrite=TRUE)
}
)
output$download_retained_markers <- downloadHandler(
filename = function() paste0("retained_markers_", Sys.Date(), ".xlsx"),
content = function(file) {
req(analysis_results())
rml <- analysis_results()$retained_markers_list
wb <- createWorkbook()
addWorksheet(wb, "Retained_Markers")
writeData(wb, "Retained_Markers", rml)
hs <- createStyle(fontColour="#FFFFFF", fgFill="#059669",
halign="CENTER", textDecoration="Bold", border="Bottom")
addStyle(wb, "Retained_Markers", hs, rows=1, cols=1:ncol(rml), gridExpand=TRUE)
if (nrow(rml) > 0) addStyle(wb, "Retained_Markers",
createStyle(fgFill="#D1FAE5"), rows=2:(nrow(rml)+1), cols=1:ncol(rml), gridExpand=TRUE)
setColWidths(wb, "Retained_Markers", cols=1:ncol(rml), widths=c(20,12,12,10))
saveWorkbook(wb, file, overwrite=TRUE)
}
)
output$download_results <- downloadHandler(
filename = function() paste0("linkage_map_v60_", Sys.Date(), ".xlsx"),
content = function(file) {
req(analysis_results())
wb <- createWorkbook()
sheets <- c("Original_Data","Recombination_Fractions","Linkage_Map",
"All_Markers_Positions","Retained_Markers","Removal_Log","QC_Report")
for (s in sheets) addWorksheet(wb, s)
orig <- analysis_results()$original_data
orig <- orig[, colnames(orig) != ".input_order", drop=FALSE]
writeData(wb, "Original_Data", orig)
writeData(wb, "Recombination_Fractions", analysis_results()$recombination_data)
writeData(wb, "Linkage_Map", analysis_results()$linkage_map)
writeData(wb, "All_Markers_Positions", analysis_results()$all_markers_positions)
writeData(wb, "Retained_Markers", analysis_results()$retained_markers_list)
writeData(wb, "Removal_Log", analysis_results()$removal_log)
writeData(wb, "QC_Report", data.frame(Report=analysis_results()$qc_report))
saveWorkbook(wb, file, overwrite=TRUE)
}
)
}
shinyApp(ui = ui, server = server)