| library(shiny) |
| library(openxlsx) |
| library(igraph) |
| library(DT) |
| library(ggplot2) |
| library(dplyr) |
| library(RColorBrewer) |
| library(ggrepel) |
| library(scales) |
|
|
| |
| |
| |
|
|
| 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 |
| } |
|
|
| |
| |
| |
|
|
| 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, ] |
| } |
|
|
| |
| |
| |
|
|
| 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) |
| } |
|
|
| |
| |
| |
|
|
| 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")] |
| } |
|
|
| |
| |
| |
|
|
| build_cyto_plot_data <- function(linkage_map, df_retained, selected_chromosomes) { |
| if (is.null(selected_chromosomes) || length(selected_chromosomes) == 0) return(list()) |
| |
| |
| if (is.null(linkage_map$Position)) linkage_map$Position <- NA_real_ |
| |
| |
| exclude_cols <- c("Marker", "Chromosome", ".input_order", "Position", "Status", "Reason", "Detail") |
| geno_cols <- setdiff(colnames(df_retained), exclude_cols) |
| |
| |
| 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) { |
| |
| 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), ] |
| |
| |
| 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] |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
|
|
| 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") |
|
|
| |
| 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 |
|
|
| |
| 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" |
| } |
|
|
| |
| hw <- chrom_width / 2 |
|
|
| p <- ggplot() + |
|
|
| |
| 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) + |
|
|
| |
| 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") + |
|
|
| |
| 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) + |
|
|
| |
| 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) + |
|
|
| |
| 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) + |
|
|
| |
| 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) + |
|
|
| |
| 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) |
| ) |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| |
|
|
| 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()) |
|
|
| |
| 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) |
| ) |
| } |
|
|
| |
| |
| |
|
|
| 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 <- 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 + ' | ' + 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 <- 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) |
|
|
| |
| 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) |
| }) |
|
|
| |
| 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) |
|
|
| |
| observe({ |
| req(analysis_results(), input$cyto_chromosome_select) |
| res <- analysis_results() |
| lm <- res$linkage_map |
| df_g <- res$original_data |
| |
| |
| cyto_data_list <- build_cyto_plot_data(lm, df_g, input$cyto_chromosome_select) |
| |
| |
| 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) |
|
|
| |
| 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") |
| }) |
|
|
| |
| 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) |
| }) |
|
|
| |
| 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") |
| } |
| ) |
|
|
| |
| 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") |
| } |
| ) |
|
|
| |
| 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") |
| } |
| ) |
|
|
| |
| 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) |