library(shiny) library(DT) library(ggplot2) library(dplyr) library(reshape2) # ── Helper functions ────────────────────────────────────────────────────────── load_data <- function(file_path, expected_cols = NULL) { data <- read.csv(file_path, stringsAsFactors = FALSE) if (!is.null(expected_cols) && !all(expected_cols %in% colnames(data))) { stop(paste("Missing required columns. Expected:", paste(expected_cols, collapse = ", "))) } return(data) } process_kmat <- function(kmat_raw) { Kmat <- as.matrix(kmat_raw[, -1]) rownames(Kmat) <- trimws(kmat_raw[, 1]) colnames(Kmat) <- rownames(Kmat) Kmat[Kmat < 0] <- 1e-6 Kmat <- (Kmat + t(Kmat)) / 2 diag(Kmat) <- 0 return(Kmat) } relate_thinning <- function(K, criterion, threshold = 0.15, max_per_cluster = 5) { K[K < 0] <- 1e-6 diag(K) <- 0 K_binary <- K K_binary[K_binary > threshold] <- 1 K_binary[K_binary <= threshold] <- 0 size_init <- nrow(K_binary) for (i in 1:nrow(K_binary)) { index <- colSums(K_binary) index <- sort(index, decreasing = TRUE) index <- index[which(index > max_per_cluster)] if (length(index) == 0) break cluster_members <- names(which(K_binary[names(index)[1], ] == 1)) tmp <- criterion[match(cluster_members, criterion[, 1]), ] tmp <- tmp[order(tmp$Criterion, decreasing = TRUE), ] if (nrow(tmp) > max_per_cluster) { tmp_rm <- match(tmp[-c(1:max_per_cluster), 1], rownames(K_binary)) tmp_rm <- tmp_rm[!is.na(tmp_rm)] if (length(tmp_rm) > 0) K_binary <- K_binary[-tmp_rm, -tmp_rm] } } return(rownames(K_binary)) } generate_half_diallel <- function(parents, criterion, K) { n <- length(parents) if (n < 2) stop("Need at least 2 parents") total_crosses <- n * (n - 1) / 2 P1 <- character(total_crosses) P2 <- character(total_crosses) P1_Value <- numeric(total_crosses) P2_Value <- numeric(total_crosses) Mid_Parent_Value <- numeric(total_crosses) Kinship <- numeric(total_crosses) idx <- 1 for (i in 1:(n - 1)) { for (j in (i + 1):n) { P1[idx] <- parents[i] P2[idx] <- parents[j] P1_Value[idx] <- criterion$Criterion[criterion$Genotype == parents[i]] P2_Value[idx] <- criterion$Criterion[criterion$Genotype == parents[j]] Mid_Parent_Value[idx] <- (P1_Value[idx] + P2_Value[idx]) / 2 Kinship[idx] <- K[parents[i], parents[j]] idx <- idx + 1 } } crosses <- data.frame( Cross_ID = 1:total_crosses, Parent1 = P1, Parent2 = P2, P1_Criterion_Value = round(P1_Value, 4), P2_Criterion_Value = round(P2_Value, 4), Predicted_Offspring_Value = round(Mid_Parent_Value, 4), Kinship_Coefficient = round(Kinship, 6), stringsAsFactors = FALSE ) crosses$Relatedness_Category <- cut(crosses$Kinship_Coefficient, breaks = c(-Inf, 0.05, 0.125, 0.25, Inf), labels = c("Unrelated", "Distantly_Related", "Moderately_Related", "Closely_Related") ) q_breaks <- quantile(crosses$Predicted_Offspring_Value, probs = c(0, 0.25, 0.5, 0.75, 1)) if (length(unique(q_breaks)) < 5) { crosses$Performance_Category <- "Medium" } else { crosses$Performance_Category <- cut(crosses$Predicted_Offspring_Value, breaks = q_breaks, labels = c("Low", "Medium", "High", "Very_High"), include.lowest = TRUE ) } crosses <- crosses[order(crosses$Predicted_Offspring_Value, decreasing = TRUE), ] rownames(crosses) <- NULL crosses } # ── UI ─────────────────────────────────────────────────────────────────────── ui <- fluidPage( tags$head(tags$style(HTML(" :root { --accent: #58a6ff; --accent2: #3fb950; --accent3: #f78166; --surface: #161b22; --border: #30363d; --muted: #8b949e; } body { background: #0d1117 !important; color: #e6edf3 !important; font-family: 'Courier New', monospace; } .container-fluid { background: #0d1117; } .well { background: #161b22 !important; border-color: #30363d !important; } .nav-tabs > li > a { background: #0d1117; color: #8b949e; border-color: #30363d; font-family: 'Courier New', monospace; font-size: 0.85rem; } .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { background: #161b22; color: #58a6ff; border-color: #30363d #30363d #161b22; } .nav-tabs > li > a:hover { background: #161b22; color: #e6edf3; border-color: #30363d; } .tab-content { background: #0d1117; border: 1px solid #30363d; border-top: none; padding: 16px; border-radius: 0 0 6px 6px; } select, input[type=number], input[type=text] { background: #0d1117 !important; color: #e6edf3 !important; border: 1px solid #30363d !important; border-radius: 4px !important; } .selectize-input { background: #0d1117 !important; color: #e6edf3 !important; border-color: #30363d !important; } .selectize-dropdown { background: #161b22 !important; color: #e6edf3 !important; border-color: #30363d !important; } .irs--shiny .irs-bar { background: #58a6ff; border-color: #58a6ff; } .irs--shiny .irs-handle { background: #58a6ff; border-color: #58a6ff; } .irs--shiny .irs-from, .irs--shiny .irs-to, .irs--shiny .irs-single { background: #58a6ff; } .irs-grid-text { color: #8b949e; } .app-header { background: linear-gradient(135deg, #161b22 0%, #0d1117 100%); border-bottom: 1px solid var(--border); padding: 24px 32px 20px; margin-bottom: 28px; } .app-title { font-family: 'Courier New', monospace; font-size: 1.6rem; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; margin: 0; } .app-subtitle { color: var(--muted); font-size: 0.82rem; margin-top: 4px; letter-spacing: 0.5px; } .card-panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px 24px; margin-bottom: 20px; } .card-title { font-family: 'Courier New', monospace; font-size: 0.78rem; font-weight: 600; color: var(--muted); letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 14px; border-bottom: 1px solid var(--border); padding-bottom: 10px; } .stat-box { background: #0d1117; border: 1px solid var(--border); border-radius: 6px; padding: 14px 18px; text-align: center; } .stat-num { font-family: 'Courier New', monospace; font-size: 2rem; font-weight: 700; color: var(--accent); } .stat-lab { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; margin-top: 2px; } .nav-tabs { border-bottom: 1px solid var(--border); } .nav-tabs .nav-link { color: var(--muted); border: none; padding: 10px 18px; font-size: 0.84rem; font-family: 'Courier New', monospace; } .nav-tabs .nav-link.active { background: var(--surface); color: var(--accent); border-bottom: 2px solid var(--accent); border-radius: 0; } .nav-tabs .nav-link:hover { color: #e6edf3; } .form-control, .selectize-input, .shiny-input-container input { background: #0d1117 !important; color: #e6edf3 !important; border: 1px solid var(--border) !important; border-radius: 6px !important; font-family: 'Courier New', monospace !important; font-size: 0.84rem !important; } .form-label, label { color: var(--muted); font-size: 0.78rem; letter-spacing: 0.5px; text-transform: uppercase; } .btn-primary { background: var(--accent); border-color: var(--accent); font-family: 'Courier New', monospace; font-size: 0.82rem; letter-spacing: 0.5px; color: #0d1117; font-weight: 700; } .btn-primary:hover { background: #79c0ff; border-color: #79c0ff; color: #0d1117; } .btn-success { background: var(--accent2); border-color: var(--accent2); font-family: 'Courier New', monospace; font-size: 0.82rem; color: #0d1117; font-weight: 700; } .badge-unrelated { background: #238636; } .badge-distant { background: #9e6a03; } .badge-moderate { background: #da3633; } table.dataTable { background: #0d1117; color: #e6edf3; border-color: var(--border); font-size: 0.82rem; } table.dataTable thead { background: var(--surface); color: var(--muted); } table.dataTable tbody tr:hover { background: var(--surface); } .dataTables_wrapper { color: var(--muted); } .dataTables_wrapper .dataTables_filter input, .dataTables_wrapper .dataTables_length select { background: #0d1117; color: #e6edf3; border: 1px solid var(--border); border-radius: 4px; } .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { background: var(--accent); border-color: var(--accent); color: white !important; border-radius: 4px; } .dataTables_wrapper .dataTables_paginate .paginate_button:hover { background: var(--surface); color: #e6edf3 !important; border-color: var(--border); border-radius: 4px; } .step-badge { display: inline-block; background: var(--accent); color: #0d1117; border-radius: 50%; width: 22px; height: 22px; text-align: center; line-height: 22px; font-size: 0.72rem; font-weight: 700; margin-right: 8px; } .alert-info-custom { background: rgba(88,166,255,0.08); border: 1px solid rgba(88,166,255,0.3); border-radius: 6px; padding: 12px 16px; color: #79c0ff; font-size: 0.82rem; } .alert-success-custom { background: rgba(63,185,80,0.08); border: 1px solid rgba(63,185,80,0.3); border-radius: 6px; padding: 12px 16px; color: var(--accent2); font-size: 0.82rem; } .alert-warn-custom { background: rgba(210,153,34,0.1); border: 1px solid rgba(210,153,34,0.3); border-radius: 6px; padding: 12px 16px; color: #d29922; font-size: 0.82rem; } .shiny-plot-output { border-radius: 6px; overflow: hidden; } hr { border-color: var(--border); } .progress { background: var(--border); } .progress-bar { background: var(--accent); } "))), # Header div(class = "app-header", div(class = "app-title", "◈ Half-Diallel Cross Optimizer"), div(class = "app-subtitle", "GENOMIC SELECTION · KINSHIP-AWARE CROSSING · BREEDING VALUE PREDICTION") ), div(style = "padding: 0 24px;", tabsetPanel( # ── TAB 1: Upload & Configure ── tabPanel("① Upload & Configure", br(), fluidRow( column(4, div(class = "card-panel", div(class = "card-title", "Step 1 — Load Data"), div(class = "alert-info-custom", style = "margin-bottom: 14px;", "Upload your criterion CSV (columns: Genotype, Criterion) and kinship matrix CSV." ), fileInput("criterion_file", "Criterion File (.csv)", accept = ".csv", buttonLabel = "Browse", placeholder = "criterion.csv"), fileInput("kmat_file", "Kinship Matrix File (.csv)", accept = ".csv", buttonLabel = "Browse", placeholder = "kmat.csv"), hr(), div(class = "card-title", "Step 2 — Thinning Parameters"), sliderInput("threshold", "Kinship Threshold", min = 0.05, max = 0.5, value = 0.15, step = 0.01), sliderInput("max_per_cluster", "Max per Cluster", min = 1, max = 20, value = 5, step = 1), hr(), actionButton("run_btn", "▶ Run Analysis", class = "btn-primary btn-block", width = "100%"), br(), uiOutput("run_status") ) ), column(8, div(class = "card-panel", div(class = "card-title", "Data Preview & Validation"), uiOutput("data_validation"), fluidRow( column(6, tableOutput("criterion_preview")), column(6, uiOutput("kmat_preview")) ) ) ) ) ), # ── TAB 2: Summary Dashboard ── tabPanel("② Summary Dashboard", br(), uiOutput("summary_stats_ui"), br(), fluidRow( column(6, div(class = "card-panel", div(class = "card-title", "Predicted Offspring Value Distribution"), plotOutput("hist_pov", height = "260px") ) ), column(6, div(class = "card-panel", div(class = "card-title", "Kinship Coefficient Distribution"), plotOutput("hist_kinship", height = "260px") ) ) ), fluidRow( column(6, div(class = "card-panel", div(class = "card-title", "Crosses by Relatedness Category"), plotOutput("bar_relatedness", height = "260px") ) ), column(6, div(class = "card-panel", div(class = "card-title", "Value vs Kinship Scatter"), plotOutput("scatter_vk", height = "260px") ) ) ) ), # ── TAB 3: All Crosses ── tabPanel("③ All Crosses", br(), div(class = "card-panel", div(class = "card-title", "Half-Diallel Cross Table"), fluidRow( column(3, selectInput("filter_relatedness", "Filter Relatedness", choices = c("All", "Unrelated", "Distantly_Related", "Moderately_Related", "Closely_Related"), selected = "All") ), column(3, selectInput("filter_performance", "Filter Performance", choices = c("All", "Low", "Medium", "High", "Very_High"), selected = "All") ), column(3, numericInput("filter_kinship_max", "Max Kinship", value = 1, min = 0, max = 1, step = 0.01) ), column(3, style = "margin-top: 24px;", downloadButton("download_all", "⬇ Download CSV", class = "btn-success") ) ), DTOutput("crosses_table") ) ), # ── TAB 4: Top Crosses ── tabPanel("④ Top Crosses", br(), fluidRow( column(6, div(class = "card-panel", div(class = "card-title", "Top 50 — Low Kinship (K < 0.125)"), DTOutput("top_low_kinship"), br(), downloadButton("dl_low_k", "⬇ Download", class = "btn-success btn-sm") ) ), column(6, div(class = "card-panel", div(class = "card-title", "Top 50 — Medium Kinship (0.125 ≤ K < 0.25)"), DTOutput("top_med_kinship"), br(), downloadButton("dl_med_k", "⬇ Download", class = "btn-success btn-sm") ) ) ) ), # ── TAB 5: Parent Summary ── tabPanel("⑤ Parent Summary", br(), fluidRow( column(5, div(class = "card-panel", div(class = "card-title", "Parent Usage & Criterion Values"), DTOutput("parent_table"), br(), downloadButton("dl_parents", "⬇ Download", class = "btn-success btn-sm") ) ), column(7, div(class = "card-panel", div(class = "card-title", "Parent Criterion Values (Ranked)"), plotOutput("parent_bar", height = "400px") ) ) ) ) ) ) ) # ── Server ──────────────────────────────────────────────────────────────────── server <- function(input, output, session) { dark_theme <- function() { theme( plot.background = element_rect(fill = "#161b22", color = NA), panel.background = element_rect(fill = "#0d1117", color = NA), panel.grid.major = element_line(color = "#21262d", size = 0.4), panel.grid.minor = element_blank(), axis.text = element_text(color = "#8b949e", size = 9, family = "mono"), axis.title = element_text(color = "#8b949e", size = 9, family = "mono"), plot.title = element_text(color = "#e6edf3", size = 11, family = "mono", face = "bold"), legend.background = element_rect(fill = "#161b22"), legend.text = element_text(color = "#8b949e", size = 8), legend.title = element_text(color = "#8b949e", size = 8), strip.background = element_rect(fill = "#161b22"), strip.text = element_text(color = "#8b949e") ) } COLS <- c("Unrelated" = "#3fb950", "Distantly_Related" = "#d29922", "Moderately_Related" = "#f78166", "Closely_Related" = "#da3633") # ── Reactive data ── criterion_data <- reactive({ req(input$criterion_file) tryCatch(load_data(input$criterion_file$datapath, c("Genotype", "Criterion")), error = function(e) { showNotification(e$message, type = "error"); NULL }) }) kmat_data <- reactive({ req(input$kmat_file) tryCatch({ raw <- load_data(input$kmat_file$datapath) process_kmat(raw) }, error = function(e) { showNotification(e$message, type = "error"); NULL }) }) # ── Validation UI ── output$data_validation <- renderUI({ crit <- criterion_data() kmat <- kmat_data() if (is.null(crit) && is.null(kmat)) return(div(class = "alert-info-custom", "Upload files to begin validation.")) msgs <- list() if (!is.null(crit)) { crit$Genotype <- trimws(as.character(crit$Genotype)) msgs <- c(msgs, list(div(class = "alert-success-custom", paste("✓ Criterion loaded:", nrow(crit), "rows,", length(unique(crit$Genotype)), "unique genotypes")))) } if (!is.null(kmat)) { msgs <- c(msgs, list(div(class = "alert-success-custom", paste("✓ Kinship matrix loaded:", nrow(kmat), "×", ncol(kmat))))) } if (!is.null(crit) && !is.null(kmat)) { crit$Genotype <- trimws(as.character(crit$Genotype)) overlap <- sum(crit$Genotype %in% rownames(kmat)) cls <- if (overlap == 0) "alert-warn-custom" else "alert-success-custom" msgs <- c(msgs, list(div(class = cls, paste("Overlap:", overlap, "genotypes match between files")))) } do.call(tagList, msgs) }) output$criterion_preview <- renderTable({ req(criterion_data()) head(criterion_data(), 8) }, striped = TRUE, bordered = TRUE, hover = TRUE) output$kmat_preview <- renderUI({ req(kmat_data()) k <- kmat_data() div(p(style = "color: #8b949e; font-size: 0.8rem;", paste0("Matrix: ", nrow(k), " × ", ncol(k), " | Range: [", round(min(k), 4), ", ", round(max(k), 4), "]"))) }) # ── Analysis reactive ── analysis_results <- eventReactive(input$run_btn, { req(criterion_data(), kmat_data()) withProgress(message = "Running analysis...", value = 0, { crit <- criterion_data() crit$Genotype <- trimws(as.character(crit$Genotype)) kmat <- kmat_data() incProgress(0.1, detail = "Filtering genotypes...") crit_filtered <- crit[crit$Genotype %in% rownames(kmat), ] validate(need(nrow(crit_filtered) > 0, "No matching genotypes. Check genotype name formats.")) incProgress(0.3, detail = "Relatedness thinning...") keep <- tryCatch( relate_thinning(kmat, crit_filtered, input$threshold, input$max_per_cluster), error = function(e) { showNotification(paste("Thinning error:", e$message), type = "error"); NULL } ) req(keep) all_parents <- intersect(keep, rownames(kmat)) validate(need(length(all_parents) >= 2, "Need at least 2 parents after thinning.")) incProgress(0.5, detail = "Generating crosses...") crosses <- tryCatch( generate_half_diallel(all_parents, crit_filtered, kmat), error = function(e) { showNotification(paste("Cross error:", e$message), type = "error"); NULL } ) req(crosses) incProgress(0.85, detail = "Building summaries...") parent_usage <- data.frame( Genotype = all_parents, Times_Used = sapply(all_parents, function(x) sum(crosses$Parent1 == x | crosses$Parent2 == x)), Criterion_Value = sapply(all_parents, function(x) crit_filtered$Criterion[crit_filtered$Genotype == x]), stringsAsFactors = FALSE ) parent_usage <- parent_usage[order(parent_usage$Criterion_Value, decreasing = TRUE), ] incProgress(1) list(crosses = crosses, parents = all_parents, parent_usage = parent_usage, crit_filtered = crit_filtered, n_original = nrow(crit)) }) }) output$run_status <- renderUI({ res <- analysis_results() if (is.null(res)) return(NULL) div(class = "alert-success-custom", style = "margin-top: 10px;", paste0("✓ Complete: ", nrow(res$crosses), " crosses from ", length(res$parents), " parents")) }) # ── Summary stats ── output$summary_stats_ui <- renderUI({ req(analysis_results()) res <- analysis_results() cr <- res$crosses make_stat <- function(num, lab) { div(class = "stat-box", div(class = "stat-num", num), div(class = "stat-lab", lab) ) } div(class = "card-panel", div(class = "card-title", "Analysis Summary"), fluidRow( column(2, make_stat(nrow(cr), "Total Crosses")), column(2, make_stat(length(res$parents), "Parents")), column(2, make_stat(round(mean(cr$Predicted_Offspring_Value), 3), "Mean POV")), column(2, make_stat(round(max(cr$Predicted_Offspring_Value), 3), "Max POV")), column(2, make_stat(sum(cr$Kinship_Coefficient < 0.125), "K < 0.125")), column(2, make_stat(round(mean(cr$Kinship_Coefficient), 4), "Mean Kinship")) ) ) }) # ── Plots ── output$hist_pov <- renderPlot({ req(analysis_results()) cr <- analysis_results()$crosses ggplot(cr, aes(x = Predicted_Offspring_Value)) + geom_histogram(bins = 40, fill = "#58a6ff", alpha = 0.85, color = "#0d1117") + geom_vline(xintercept = mean(cr$Predicted_Offspring_Value), color = "#3fb950", linetype = "dashed", size = 0.8) + labs(x = "Predicted Offspring Value", y = "Count") + dark_theme() }, bg = "#161b22") output$hist_kinship <- renderPlot({ req(analysis_results()) cr <- analysis_results()$crosses ggplot(cr, aes(x = Kinship_Coefficient)) + geom_histogram(bins = 40, fill = "#f78166", alpha = 0.85, color = "#0d1117") + geom_vline(xintercept = 0.125, color = "#d29922", linetype = "dashed", size = 0.8) + geom_vline(xintercept = 0.25, color = "#da3633", linetype = "dashed", size = 0.8) + labs(x = "Kinship Coefficient (K)", y = "Count") + dark_theme() }, bg = "#161b22") output$bar_relatedness <- renderPlot({ req(analysis_results()) cr <- analysis_results()$crosses cnt <- as.data.frame(table(cr$Relatedness_Category)) colnames(cnt) <- c("Category", "Count") ggplot(cnt, aes(x = reorder(Category, -Count), y = Count, fill = Category)) + geom_col(alpha = 0.9, width = 0.6) + scale_fill_manual(values = COLS, guide = "none") + labs(x = "", y = "Number of Crosses") + dark_theme() + theme(axis.text.x = element_text(angle = 20, hjust = 1)) }, bg = "#161b22") output$scatter_vk <- renderPlot({ req(analysis_results()) cr <- analysis_results()$crosses ggplot(cr, aes(x = Kinship_Coefficient, y = Predicted_Offspring_Value, color = Relatedness_Category)) + geom_point(alpha = 0.35, size = 0.8) + geom_vline(xintercept = 0.125, color = "#d29922", linetype = "dashed", size = 0.6, alpha = 0.7) + scale_color_manual(values = COLS, name = "Relatedness") + labs(x = "Kinship (K)", y = "Predicted Offspring Value") + dark_theme() + theme(legend.position = "bottom") }, bg = "#161b22") output$parent_bar <- renderPlot({ req(analysis_results()) pu <- analysis_results()$parent_usage pu$Genotype <- factor(pu$Genotype, levels = pu$Genotype[order(pu$Criterion_Value)]) ggplot(pu, aes(x = Criterion_Value, y = Genotype)) + geom_col(fill = "#58a6ff", alpha = 0.8, width = 0.7) + labs(x = "Criterion Value", y = "") + dark_theme() + theme(axis.text.y = element_text(size = 7)) }, bg = "#161b22") # ── Tables ── dt_opts <- function(dom = "lfrtip") { list(pageLength = 15, dom = dom, initComplete = JS("function(settings, json) { $(this.api().table().header()).css({'background-color': '#161b22', 'color': '#8b949e'}); }")) } filtered_crosses <- reactive({ req(analysis_results()) cr <- analysis_results()$crosses if (input$filter_relatedness != "All") cr <- cr[cr$Relatedness_Category == input$filter_relatedness, ] if (input$filter_performance != "All") cr <- cr[cr$Performance_Category == input$filter_performance, ] cr <- cr[cr$Kinship_Coefficient <= input$filter_kinship_max, ] cr }) output$crosses_table <- renderDT({ req(filtered_crosses()) datatable(filtered_crosses(), options = dt_opts(), rownames = FALSE, selection = "none") %>% formatRound(c("P1_Criterion_Value", "P2_Criterion_Value", "Predicted_Offspring_Value"), 4) %>% formatRound("Kinship_Coefficient", 6) }) output$top_low_kinship <- renderDT({ req(analysis_results()) d <- analysis_results()$crosses %>% filter(Kinship_Coefficient < 0.125) %>% head(50) %>% select(Parent1, Parent2, Predicted_Offspring_Value, Kinship_Coefficient, Relatedness_Category) datatable(d, options = dt_opts("tp"), rownames = FALSE) %>% formatRound(c("Predicted_Offspring_Value", "Kinship_Coefficient"), 4) }) output$top_med_kinship <- renderDT({ req(analysis_results()) d <- analysis_results()$crosses %>% filter(Kinship_Coefficient >= 0.125, Kinship_Coefficient < 0.25) %>% head(50) %>% select(Parent1, Parent2, Predicted_Offspring_Value, Kinship_Coefficient, Relatedness_Category) datatable(d, options = dt_opts("tp"), rownames = FALSE) %>% formatRound(c("Predicted_Offspring_Value", "Kinship_Coefficient"), 4) }) output$parent_table <- renderDT({ req(analysis_results()) datatable(analysis_results()$parent_usage, options = dt_opts("tp"), rownames = FALSE) %>% formatRound("Criterion_Value", 4) }) # ── Downloads ── output$download_all <- downloadHandler( filename = function() paste0("Half_Diallel_Crosses_", Sys.Date(), ".csv"), content = function(file) write.csv(filtered_crosses(), file, row.names = FALSE) ) output$dl_low_k <- downloadHandler( filename = function() paste0("Top50_Low_Kinship_", Sys.Date(), ".csv"), content = function(file) write.csv( analysis_results()$crosses %>% filter(Kinship_Coefficient < 0.125) %>% head(50), file, row.names = FALSE) ) output$dl_med_k <- downloadHandler( filename = function() paste0("Top50_Med_Kinship_", Sys.Date(), ".csv"), content = function(file) write.csv( analysis_results()$crosses %>% filter(Kinship_Coefficient >= 0.125, Kinship_Coefficient < 0.25) %>% head(50), file, row.names = FALSE) ) output$dl_parents <- downloadHandler( filename = function() paste0("Parent_Usage_", Sys.Date(), ".csv"), content = function(file) write.csv(analysis_results()$parent_usage, file, row.names = FALSE) ) } shinyApp(ui, server)