---
title: "Nedo et al. 2026 - Data Analysis"
author: "rjw"
output: html_document
editor_options: 
  chunk_output_type: console
---

```{r setup, include=FALSE}
## Clear objects in memory
rm(list = ls(all = TRUE))

require("knitr")
knitr::opts_chunk$set(echo = TRUE)

## Core
library(tidyverse)
library(gridExtra)
library(grid)
library(plotly)
library(htmlwidgets)

## Stats
library(caTools)
library(lattice)
library(corrplot)
library(multcomp)
library(agricolae)
library(conover.test)
library(rcompanion)
library(multcompView)

## Plotting extras
library(ggpmisc)
library(RColorBrewer)

## Output
library(writexl)

## Color palette (colorblind-safe)
cbPalette <- c("#999999", "#E69F00", "#56B4E9", "#009E73",
               "#F0E442", "#0072B2", "#D55E00", "#CC79A7")

## Directories — add subfolders "data" and "figures" inside your working directory
setwd("~/path_to_your_analysis_dir/")

dat_dir <- paste0(getwd(), "/data/")
fig_dir <- paste0(getwd(), "/figures/")

## Timestamp for versioned output file names
timestamp <- format(Sys.time(), "%Y%m%d")

## Desired factor orders (used throughout)
desired_order_t <- c(
  "CH_Prolate", "CH_Oblate",
  "CH_Volume", "Network_Volume", "Network_Density", "Infection_Depth",
  "Mean_Hyphal_Length", "Peak_Branch_Level", "Final_Branch_Level",
  "Vascular_Overlap_Rank"
)

desired_order_g <- c(
  "Ki3", "Mo17", "CML247", "M37W",
  "Il14H", "Tx303", "B73", "Hp301"
)

## Trait filtering classification (used in Sections 3 and 5):
##   traits_to_filter  — require Infection_Cut_Off filtering before analysis
##   traits_no_filter  — use all data regardless of cut-off
traits_to_filter <- c(
  "CH_Prolate", "CH_Oblate", "CH_Volume",
  "Network_Volume", "Network_Density"
)
traits_no_filter <- c(
  "Infection_Depth", "Peak_Branch_Level",
  "Final_Branch_Level", "Vascular_Overlap_Rank"
)
```

```{r data}
# ==============================================================================
# 1. Read and format data
# ==============================================================================

## --- 1a. Read raw data files ---

trait_data1 <- readxl::read_xlsx(paste0(dat_dir, "Nedo_etal_traitdata_d.xlsx"),
                                  col_names = TRUE)
dim(trait_data1)

trait_data2 <- readxl::read_xlsx(paste0(dat_dir, "Nedo_etal_leveldata_d.xlsx"))
dim(trait_data2)

## --- 1b. Summarize branching patterns per infection network ---

# Count branches at each hierarchical level per network
branch_counts <- trait_data2 %>%
  group_by(Infection_Network, Genotype, level) %>%
  filter(level != "NA") %>%
  summarise(n_branches = n(), .groups = "drop")

# Convert to wide matrix, normalize rows to proportional frequencies, then back to long
branch_matrix <- branch_counts %>%
  pivot_wider(
    names_from  = level,
    values_from = n_branches,
    values_fill = 0
  ) %>%
  column_to_rownames("Infection_Network") %>%
  dplyr::select(-Genotype) %>%
  as.matrix()

branch_matrix_norm <- branch_matrix / rowSums(branch_matrix)

branch_frequencies <- branch_matrix_norm %>%
  as.data.frame() %>%
  rownames_to_column("Infection_Network") %>%
  pivot_longer(
    cols      = -Infection_Network,
    names_to  = "level",
    values_to = "frequency"
  )

branch_counts_with_freq <- branch_counts %>%
  left_join(
    branch_frequencies %>% mutate(level = as.numeric(level)),
    by = c("Infection_Network", "level")
  )

# Mean filament length at each level per network
branch_lengths <- trait_data2 %>%
  group_by(Infection_Network, Genotype, level) %>%
  filter(level != "NA") %>%
  summarise(mean_l = mean(length), .groups = "drop")

# Combine branching counts/frequencies with mean lengths
branch_data <- branch_counts_with_freq %>%
  left_join(branch_lengths, by = c("Infection_Network", "Genotype", "level")) %>%
  group_by(Infection_Network) %>%
  mutate(prop_mean_l = mean_l / sum(mean_l, na.rm = TRUE)) %>%
  ungroup()

# Summary: average level of peak branching and peak length across all networks
branch_data %>%
  group_by(Infection_Network) %>%
  summarise(
    max_level            = max(level, na.rm = TRUE),
    level_max_n_branches = level[which.max(n_branches)],
    max_n_branches       = max(n_branches, na.rm = TRUE),
    level_max_mean_l     = level[which.max(mean_l)],
    max_mean_l           = max(mean_l, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  summarise(
    level_max_n_branches_mean = mean(level_max_n_branches, na.rm = TRUE),
    level_max_mean_l_mean     = mean(level_max_mean_l, na.rm = TRUE)
  )

# Per-network: level at which branching and length peak
sample_peaks1 <- branch_data %>%
  group_by(Infection_Network, Genotype) %>%
  summarise(
    Final_Branch_Level = max(level),
    peak_branch_level  = level[which.max(n_branches)],
    .groups = "drop"
  )

sample_peaks2 <- branch_data %>%
  group_by(Infection_Network, Genotype) %>%
  summarise(
    peak_length_level = level[which.max(mean_l)],
    .groups = "drop"
  )

sample_peaks <- sample_peaks1 %>% left_join(sample_peaks2)

# Correlation: peak branch level vs. final branch level
cor.test(sample_peaks$peak_branch_level,
         sample_peaks$Final_Branch_Level,
         method = "pearson")

# Correlation: peak branch level vs. peak length level
cor.test(sample_peaks$peak_branch_level,
         sample_peaks$peak_length_level,
         method = "pearson")

summary(lm(peak_length_level ~ peak_branch_level, data = sample_peaks))

# Plot: peak branch level vs. peak length level
peak_plot <- ggplot(sample_peaks,
                    aes(x = peak_branch_level, y = peak_length_level)) +
  geom_point() +
  geom_abline(intercept = 0, slope = 1, linetype = "dashed") +
  geom_hline(yintercept = 0, linetype = "solid", color = "gray50") +
  geom_vline(xintercept = 0, linetype = "solid", color = "gray50") +
  geom_smooth(method = "lm", se = FALSE) +
  stat_poly_eq(
    formula = y ~ x,
    aes(label = paste(..eq.label.., ..rr.label.., sep = "~~~")),
    parse   = TRUE,
    label.x = 0.1,
    label.y = 0.9,
    size    = 5
  ) +
  theme_minimal() +
  theme(
    axis.text.x  = element_text(size = 15, color = "black"),
    axis.text.y  = element_text(size = 15, color = "black"),
    axis.title.x = element_text(size = 14, color = "black",
                                margin = margin(t = 8)),
    axis.title.y = element_text(size = 14, color = "black",
                                margin = margin(r = 8)),
    plot.title   = element_blank(),
    aspect.ratio = 1
  ) +
  labs(x = "Peak Branch Level", y = "Peak Branch Length")
peak_plot

ggsave(
  filename = paste0(fig_dir, "peakbranch_vs_length_", timestamp, ".pdf"),
  plot     = peak_plot,
  device   = cairo_pdf,
  width    = 6,
  height   = 6,   # square to match aspect.ratio = 1
  units    = "in"
)

## --- 1c. Summarize filament lengths ---

# Proportion of filaments below 5 µm (used as lower-bound filter threshold)
length_lb5micron <- trait_data2 %>%
  filter(!is.na(length)) %>%
  summarise(lower_percentile = mean(length <= 5))

# Boxplot of ranked lengths by genotype (filaments >= 5 µm)
plot_ranked_lengths <- trait_data2 %>%
  filter(!is.na(length), length >= 5) %>%
  mutate(
    Genotype      = factor(Genotype, levels = desired_order_g),
    length_rounded = round(length, 0),
    length_rank    = rank(length_rounded, ties.method = "average")
  ) %>%
  ggplot(aes(x = Genotype, y = length_rank)) +
  geom_jitter(width = 0.1, alpha = 0.4, size = 0.5, color = "gray70") +
  geom_boxplot(
    outlier.shape = NA, color = "black", fill = "transparent",
    size = 0.7, alpha = 0.5, width = 0.5
  ) +
  theme_classic() +
  theme(
    strip.text   = element_text(face = "bold"),
    axis.text.x  = element_text(angle = 90, vjust = 0.5, hjust = 1),
    axis.title.x = element_text(margin = margin(t = 10)),
    axis.title.y = element_text(margin = margin(r = 10))
  ) +
  labs(y = "Rank of Length", x = "Genotype")
plot_ranked_lengths

ggsave(
  filename = paste0(fig_dir, "length_rank_boxplot_", timestamp, ".pdf"),
  plot     = plot_ranked_lengths,
  width    = 5,
  height   = 3,
  device   = cairo_pdf
)

# Summary statistics for retained filaments (>= 5 µm lower bound)
trait_data2 %>%
  filter(!is.na(length)) %>%
  summarise(
    length_lb      = quantile(length, length_lb5micron$lower_percentile, na.rm = TRUE),
    n_total        = n(),
    n_below_lb     = sum(length <= length_lb),
    n_kept         = sum(length > length_lb),   # fixed: compare against threshold, not count
    length_min     = min(length[length > length_lb], na.rm = TRUE),
    length_mean    = mean(length[length > length_lb], na.rm = TRUE),
    length_median  = median(length[length > length_lb], na.rm = TRUE),
    length_max     = max(length[length > length_lb], na.rm = TRUE)
  )

# Per-network length summaries (filaments >= 5 µm)
length_summary <- trait_data2 %>%
  filter(!is.na(length), length >= 5) %>%
  group_by(Infection_Network, Genotype) %>%
  summarise(
    n_kept        = n(),
    length_min    = min(length),
    length_p01    = quantile(length, 0.01),
    length_mean   = mean(length),
    length_median = median(length),
    length_p99    = quantile(length, 0.99),
    length_max    = max(length),
    total_length  =sum(length),
    .groups = "drop"
  ) %>%
  mutate(
    length_p99_p01 = length_p99 - length_p01,
    length_max_min = length_max - length_min
  ) %>%
  arrange(desc(length_max_min))

length_summary %>%
  mutate(total_length_mm = total_length / 1000) %>%
  group_by(Genotype) %>%
  summarise(
    n_networks = n(),
    min        = min(total_length_mm,            na.rm = TRUE),
    q25        = quantile(total_length_mm, 0.25, na.rm = TRUE),
    median     = median(total_length_mm,         na.rm = TRUE),
        mean     = mean(total_length_mm,         na.rm = TRUE),
    q75        = quantile(total_length_mm, 0.75, na.rm = TRUE),
    max        = max(total_length_mm,            na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(across(where(is.numeric) & !n_networks, ~ round(., 2))) %>%
  mutate(Genotype = factor(Genotype, levels = desired_order_g)) %>%
  arrange(Genotype)

# Merge length summaries with peak branching data
trait_data2_s <- length_summary %>%
  left_join(sample_peaks1, by = c("Infection_Network", "Genotype")) %>%
  mutate(across(c(length_min, length_p01, length_mean, length_median,
                  length_p99, length_max, length_p99_p01,
                  peak_branch_level, Final_Branch_Level), as.numeric))

## --- 1d. Lesion resistance data ---

lesion_data <- trait_data1 %>%
  dplyr::select(Genotype, Lesion_Profile, Lesion_Profile_sub, SLB_BLUP, SLB_Rank, Lesion_2mmsq_freq)

## --- 1e. Combine all trait data ---

## Helper: before each join, drop any columns from the left-hand table that
## also exist in the right-hand table (excluding the join keys), so the
## right-hand version always lands cleanly without .x / .y suffixes.
safe_left_join <- function(x, y, by) {
  to_drop <- setdiff(intersect(names(x), names(y)), by)
  x %>%
    dplyr::select(-any_of(to_drop)) %>%
    dplyr::left_join(y, by = by)
}

trait_data_c <- trait_data1 %>%
  mutate(across(
    c(CH_Prolate, CH_Oblate, CH_Volume, Network_Volume, Network_Density,
      Infection_Depth, Mean_Hyphal_Length, Peak_Branch_Level,
      Final_Branch_Level, Vascular_Overlap_Rank),
    as.numeric
  )) %>%
  safe_left_join(
    trait_data2_s %>% dplyr::select(Infection_Network, Genotype, Final_Branch_Level),
    by = c("Infection_Network", "Genotype")
  ) %>%
  safe_left_join(lesion_data, by = "Genotype") %>%
  mutate(Genotype = factor(Genotype, levels = desired_order_g))
```

```{r sample_counts}
# ==============================================================================
# 2. Summarize sample counts by Infection_Cut_Off filtering level
# ==============================================================================

# Count networks per genotype before any filtering
PINs_Before <- trait_data_c %>%
  group_by(Genotype) %>%
  summarise(PINs_Before = n_distinct(Infection_Network), .groups = "drop")

# Count after excluding "Major" cut-off infections
PINs_NoMajor <- trait_data_c %>%
  filter(Infection_Cut_Off != "Major") %>%
  group_by(Genotype) %>%
  summarise(PINs_NoMajor = n_distinct(Infection_Network), .groups = "drop")

# Count after excluding both "Major" and "Moderate"
PINs_NoMajorModerate <- trait_data_c %>%
  filter(!Infection_Cut_Off %in% c("Major", "Moderate")) %>%
  group_by(Genotype) %>%
  summarise(PINs_NoMajorModerate = n_distinct(Infection_Network), .groups = "drop")

# Combined summary table
sample_comparison <- PINs_Before %>%
  full_join(PINs_NoMajor,         by = "Genotype") %>%
  full_join(PINs_NoMajorModerate, by = "Genotype") %>%
  replace_na(list(PINs_Before = 0, PINs_NoMajor = 0, PINs_NoMajorModerate = 0))

print(sample_comparison)
colSums(sample_comparison[, 2:4])
```

```{r correlations_and_associations}
# ==============================================================================
# 3. Pairwise trait correlations and associations with resistance rank
# ==============================================================================

trait_cols <- desired_order_t   # all traits used in downstream analyses

## --- 3a. Pairwise Spearman correlations ---
## Traits in traits_to_filter use only non-Major infections;
## trait pairs entirely within traits_no_filter use all data.

cor_matrix <- matrix(
  NA,
  nrow     = length(desired_order_t),
  ncol     = length(desired_order_t),
  dimnames = list(desired_order_t, desired_order_t)
)

for (i in seq_along(desired_order_t)) {
  for (j in i:length(desired_order_t)) {
    trait1       <- desired_order_t[i]
    trait2       <- desired_order_t[j]
    needs_filter <- (trait1 %in% traits_to_filter) | (trait2 %in% traits_to_filter)

    data_subset <- if (needs_filter) {
      trait_data_c %>% filter(!Infection_Cut_Off %in% c("Major"))
    } else {
      trait_data_c
    }

    cor_val <- cor(data_subset[[trait1]], data_subset[[trait2]],
                   use = "complete.obs", method = "spearman")
    cor_matrix[i, j] <- cor_val
    cor_matrix[j, i] <- cor_val
  }
}

# Clean up names for display
rownames(cor_matrix) <- gsub("_", " ", rownames(cor_matrix))
colnames(cor_matrix) <- gsub("_", " ", colnames(cor_matrix))

corrplot(cor_matrix, method = "color", type = "upper",
         tl.col = "black", tl.srt = 45,
         addCoef.col = "black", number.cex = 0.7)

cairo_pdf(paste0(fig_dir, "traits_pairwisecorrelation_all_", timestamp, ".pdf"),
          width = 8, height = 8)
corrplot(cor_matrix, method = "color", type = "upper",
         tl.col = "black", tl.srt = 45,
         addCoef.col = "black", number.cex = 0.7)
dev.off()

## --- 3b. Trait associations with resistance level (SLB_BLUP) ---
## Each trait is regressed against genotype-median trait values.
## Traits in traits_to_filter are filtered before aggregation.

compute_associations <- function(data,
                                 traits,
                                 response      = c("SLB_BLUP", "Lesion_Profile", "Lesion_2mmsq_freq")[1],   ## choose respose variable
                                 filter_trait  = TRUE,
                                 filter_types  = c("Major", "Moderate"),
                                 agg_fun       = median) {

  results <- lapply(traits, function(trait) {

    clean_data <- if (filter_trait && trait %in% traits_to_filter) {
      data %>%
        dplyr::filter(!Infection_Cut_Off %in% filter_types) %>%
        dplyr::filter(!is.na(.data[[response]]), !is.na(.data[[trait]]))
    } else {
      data %>%
        dplyr::filter(!is.na(.data[[response]]), !is.na(.data[[trait]]))
    }

    agg_data <- clean_data %>%
      dplyr::group_by(.data[[response]]) %>%
      dplyr::summarise(
        Trait_value = agg_fun(.data[[trait]], na.rm = TRUE),
        .groups = "drop"
      )

    if (nrow(agg_data) < 3) {
      return(data.frame(Trait = trait, R2 = NA, P_Value = NA, N = nrow(agg_data)))
    }

    fit         <- lm(as.formula(paste(response, "~ Trait_value")), data = agg_data)
    fit_summary <- summary(fit)

    data.frame(
      Trait   = trait,
      R2      = fit_summary$r.squared,
      P_Value = coef(fit_summary)["Trait_value", "Pr(>|t|)"],
      N       = nrow(agg_data)
    )
  })

  results_df               <- do.call(rbind, results)
  results_df$P_Value_FDR   <- p.adjust(results_df$P_Value, method = "fdr")
  results_df
}

# Without infection cut-off filtering
all_data_results          <- compute_associations(trait_data_c, trait_cols,
                                                  filter_trait = FALSE,
                                                  agg_fun      = median)
all_data_results$Dataset  <- "All Data"

# Filtering Major infections for relevant traits
filtered_results          <- compute_associations(trait_data_c, trait_cols,
                                                  filter_trait = TRUE,
                                                  filter_types = c("Major"),
                                                  agg_fun      = median)
filtered_results$Dataset  <- "Filtered (Major)"

output_table <- dplyr::bind_rows(all_data_results, filtered_results) %>%
  dplyr::select(Dataset, Trait, R2, P_Value, P_Value_FDR, N) %>%
  dplyr::arrange(Dataset, Trait) %>%
  dplyr::mutate(
    Significance = dplyr::case_when(
      P_Value_FDR < 0.001 ~ "***",
      P_Value_FDR < 0.01  ~ "**",
      P_Value_FDR < 0.05  ~ "*",
      TRUE                ~ "ns"
    )
  )

print(output_table)
```

```{r descriptive_stats}
# ==============================================================================
# 4. Descriptive statistics
# ==============================================================================

## Convex hull shape ratios (Major infections excluded)
trait_data_c_noMajor <- trait_data_c %>%
  filter(Infection_Cut_Off != "Major") %>%
  mutate(
    R_diff    = CH_Prolate - CH_Oblate,
    Rpo_ratio = CH_Prolate / CH_Oblate,
    Rop_ratio = CH_Oblate  / CH_Prolate
  )

trait_data_c_noMajor %>%
  summarise(
    min_R_diff    = min(R_diff,    na.rm = TRUE),
    max_R_diff    = max(R_diff,    na.rm = TRUE),
    min_Rpo_ratio = min(Rpo_ratio, na.rm = TRUE),
    max_Rpo_ratio = max(Rpo_ratio, na.rm = TRUE),
    min_Rop_ratio = min(Rop_ratio, na.rm = TRUE),
    max_Rop_ratio = max(Rop_ratio, na.rm = TRUE)
  )
```

```{r genotype_comparisons}
# ==============================================================================
# 5. Genotype comparisons — Kruskal-Wallis with Conover-Iman post-hoc test
# ==============================================================================

## --- 5a. Reshape trait data to long format ---

trait_long <- trait_data_c %>%
  pivot_longer(cols = all_of(trait_cols), names_to = "Trait", values_to = "Value") %>%
  mutate(
    Trait    = factor(Trait,    levels = desired_order_t),
    Genotype = factor(Genotype, levels = desired_order_g)
  )

## Raw distributions: bar chart and density histogram (all data, no filtering)

ggplot(trait_long, aes(x = Genotype, y = Value)) +
  geom_bar(stat = "identity", position = "dodge") +
  facet_wrap(~Trait, scales = "free_y", ncol = 3) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1)) +
  labs(title = "Faceted Plot of Traits Across Samples",
       x = "Samples", y = "Trait Value")

ggsave(paste0(fig_dir, "trait_values_raw_data_", timestamp, ".pdf"),
       width = 10, height = 6, dpi = 300)

ggplot(trait_long, aes(x = Value)) +
  geom_histogram(aes(y = ..density..), bins = 20,
                 fill = "gray70", color = "black", alpha = 0.6) +
  geom_density(aes(y = ..density..), color = "blue", fill = "blue", alpha = 0.3) +
  facet_wrap(~Trait, scales = "free", ncol = 3) +
  theme_minimal() +
  labs(title = "Trait Value Distributions",
       x = "Trait Value", y = "Density")

ggsave(paste0(fig_dir, "trait_distributions_raw_data_", timestamp, ".pdf"),
       width = 10, height = 6, dpi = 300)

## --- 5b. KW + Conover-Iman post-hoc with compact letter display ---
## For traits in traits_to_filter, Major infections are excluded before testing.
## Group letters are assigned at alpha = 0.01 (Bonferroni-adjusted).

compute_KW_CI <- function(data,
                          factor_col,
                          sample_col,
                          filter_trait  = TRUE,
                          filter_types  = c("Major", "Moderate")) {

  trait_cols_local <- setdiff(names(data),
                              c(sample_col, factor_col, "Infection_Cut_Off"))

  results_list           <- list()
  raw_data_with_stats_list <- list()

  for (col in trait_cols_local) {

    temp_df <- if (filter_trait && col %in% traits_to_filter) {
      data %>%
        dplyr::filter(!.data[["Infection_Cut_Off"]] %in% filter_types) %>%
        dplyr::select(all_of(c(factor_col, sample_col, col))) %>%
        dplyr::filter(!is.na(.data[[col]]), !is.na(.data[[factor_col]]))
    } else {
      data %>%
        dplyr::select(all_of(c(factor_col, sample_col, col))) %>%
        dplyr::filter(!is.na(.data[[col]]), !is.na(.data[[factor_col]]))
    }

    y     <- temp_df[[col]]
    group <- temp_df[[factor_col]]

    if (length(unique(group)) < 2 || length(y) == 0) next

    # Kruskal-Wallis test
    kw_test <- kruskal.test(y ~ group)

    # Conover-Iman pairwise post-hoc (Bonferroni-adjusted)
    pairwise_result <- conover.test::conover.test(
      x = y, g = group, method = "bonferroni", kw = FALSE, alpha = 0.1
    )

    # Compact letter display via multcompLetters
    pvals        <- pairwise_result$P.adjusted
    names(pvals) <- gsub(" - ", "-", pairwise_result$comparisons)
    letters_obj  <- multcompLetters(pvals, threshold = 0.01)

    letters_df <- tibble(
      !!factor_col := names(letters_obj$Letters),
      groups        = letters_obj$Letters
    )

    # Median and IQR per group
    summary_df <- temp_df %>%
      group_by(!!sym(factor_col)) %>%
      summarise(
        Median = median(!!sym(col), na.rm = TRUE),
        Q1     = quantile(!!sym(col), 0.25, na.rm = TRUE),
        Q3     = quantile(!!sym(col), 0.75, na.rm = TRUE),
        .groups = "drop"
      )

    plot_data        <- left_join(summary_df, letters_df, by = factor_col)
    plot_data$Trait  <- col
    plot_data$KW_pval <- kw_test$p.value
    results_list[[col]] <- plot_data

    # Append letters and KW p-value to raw data
    raw_data <- temp_df %>%
      left_join(letters_df, by = factor_col) %>%
      mutate(
        Trait    = col,
        Value    = .data[[col]],
        KW_pval  = kw_test$p.value
      ) %>%
      dplyr::select(all_of(c(factor_col, sample_col, "Trait", "Value",
                              "groups", "KW_pval")))

    raw_data_with_stats_list[[col]] <- raw_data
  }

  list(
    summary_data        = bind_rows(results_list),
    raw_data_with_stats = bind_rows(raw_data_with_stats_list)
  )
}

## Run KW + Conover-Iman (Major infections excluded for relevant traits)
plot_data_kw <- compute_KW_CI(
  data         = trait_data_c %>% dplyr::select(Infection_Network, Genotype,
                                                 Infection_Cut_Off,
                                                 all_of(trait_cols)),
  factor_col   = "Genotype",
  sample_col   = "Infection_Network",
  filter_trait = TRUE,
  filter_types = c("Major")
)

# Apply factor ordering
plot_data_kw[[1]] <- plot_data_kw[[1]] %>%
  mutate(
    Trait    = factor(Trait,    levels = desired_order_t),
    Genotype = factor(Genotype, levels = desired_order_g)
  )
plot_data_kw[[2]] <- plot_data_kw[[2]] %>%
  mutate(
    Trait    = factor(Trait,    levels = desired_order_t),
    Genotype = factor(Genotype, levels = desired_order_g)
  )

# ## Optional: save summary and raw KW data to Excel
# write_xlsx(plot_data_kw[[1]],
#            path = paste0(dat_dir, "trait_histdat_bytraitgtype_KWCI_", timestamp, ".xlsx"))
# write_xlsx(plot_data_kw[[2]],
#            path = paste0(dat_dir, "trait_boxplotdat_bytraitgtype_KWCI_", timestamp, ".xlsx"))

## --- 5c. Consistent group color palette ---
all_group_levels <- sort(unique(plot_data_kw[[2]]$groups))
group_colors     <- RColorBrewer::brewer.pal(n = length(all_group_levels), name = "Set2")
names(group_colors) <- all_group_levels

## --- 5d. Faceted bar chart (median + IQR) ---
ggplot(plot_data_kw[[1]], aes(x = Genotype, y = Median, fill = groups)) +
  geom_bar(stat = "identity", color = "black") +
  geom_errorbar(aes(ymin = Q1, ymax = Q3), width = 0.2, color = "black") +
  geom_text(aes(label = groups, y = 0), vjust = -0.5, size = 3, fontface = "bold") +
  labs(title = "KW-CI Test Results",
       x = "Genotype", y = "Median Value with IQR") +
  facet_wrap(~Trait, scales = "free_y", ncol = 3) +
  scale_fill_manual(values = group_colors) +
  theme_minimal() +
  theme(
    legend.position  = "none",
    strip.text       = element_text(face = "bold"),
    axis.text.x      = element_text(angle = 90, vjust = 0.5, hjust = 1),
    axis.title.y     = element_text(margin = margin(r = 10))
  )

ggsave(paste0(fig_dir, "trait_hist_bytraitgtype_KWCI_", timestamp, ".pdf"),
       width = 10, height = 6, dpi = 300)

## --- 5e. Faceted boxplot overview (all traits) ---
## Overview figure — no n annotation; n varies by trait due to filtering.
ggplot(plot_data_kw[[2]], aes(x = Genotype, y = Value, fill = groups)) +
  geom_boxplot(
    width         = 0.6,
    outlier.shape = 16,
    outlier.size  = 0.7,
    outlier.alpha = 0.4,
    linewidth     = 0.4
  ) +
  labs(x = "Genotype", y = "Value", fill = "KW-CI") +
  facet_wrap(~Trait, scales = "free_y", ncol = 3,
             labeller = labeller(Trait = function(x) gsub("_", " ", x))) +
  scale_fill_manual(values = group_colors) +
  theme_classic() +
  theme(
    legend.position    = "right",
    strip.text         = element_text(face = "bold", size = 9),
    strip.background   = element_blank(),
    axis.text.x        = element_text(angle = 90, vjust = 0.5, hjust = 1, size = 9),
    axis.text.y        = element_text(size = 9),
    axis.title.x       = element_text(size = 10, margin = margin(t = 6)),
    axis.title.y       = element_text(size = 10, margin = margin(r = 6)),
    panel.grid.major.y = element_line(color = "gray90", linewidth = 0.3),
    panel.spacing      = unit(0.6, "cm")
  )

ggsave(paste0(fig_dir, "trait_boxplots_bytraitgtype_KWCI_", timestamp, ".pdf"),
       width = 10, height = 6, dpi = 300)

## --- 5f. Individual boxplots per trait (fixed panel size, with n annotations) ---
## Both panel width and height are locked so all trait PDFs have an identical
## data area — safe to assemble in Illustrator without rescaling.
## Per-genotype n is drawn at the top of each box column (after trait-appropriate
## filtering), giving sample size transparency without affecting axis dimensions.

lock_panel_size <- function(p,
                            panel_width  = unit(7, "cm"),
                            panel_height = unit(5, "cm")) {
  gt          <- ggplotGrob(p)
  panel_rows  <- unique(gt$layout$t[gt$layout$name == "panel"])
  panel_cols  <- unique(gt$layout$l[gt$layout$name == "panel"])
  gt$widths[panel_cols]  <- panel_width
  gt$heights[panel_rows] <- panel_height
  gt
}

## Pre-compute n per trait × genotype from the already-filtered raw data.
## Reflects the same filtering applied in compute_KW_CI (Major removed for
## traits_to_filter), so reported n matches what is actually plotted.
n_labels <- plot_data_kw[[2]] %>%
  group_by(Trait, Genotype) %>%
  summarise(n = n(), .groups = "drop")

plot_df <- plot_data_kw[[2]]
traits  <- levels(plot_df$Trait)   # use factor levels to preserve desired_order_t

for (trait_name in traits) {

  trait_data_tmp <- plot_df %>% filter(Trait == trait_name)

  n_labels_tmp <- n_labels %>%
    filter(Trait == trait_name) %>%
    mutate(Genotype = factor(Genotype, levels = desired_order_g))

  trait_label <- gsub("_", " ", trait_name)   # y-axis title

  p <- ggplot(trait_data_tmp, aes(x = Genotype, y = Value, fill = groups)) +
    geom_boxplot(
      width         = 0.6,
      outlier.shape = 16,
      outlier.size  = 0.9,
      outlier.alpha = 0.5,
      linewidth     = 0.5
    ) +
    ## n annotation: pinned to top of panel area, one label per genotype
    geom_text(
      data        = n_labels_tmp,
      aes(x = Genotype, y = Inf, label = paste0("n=", n)),
      vjust       = 1.5,
      size        = 2.8,
      color       = "gray45",
      inherit.aes = FALSE
    ) +
    scale_fill_manual(values = group_colors) +
    labs(x = "Genotype", y = trait_label, fill = "KW-CI") +
    theme_classic() +
    theme(
      ## Axis text and titles
      axis.text.x        = element_text(size = 11, color = "black",
                                        angle = 90, vjust = 0.5, hjust = 1),
      axis.text.y        = element_text(size = 11, color = "black"),
      axis.title.x       = element_text(size = 12, color = "black",
                                        margin = margin(t = 6)),
      axis.title.y       = element_text(size = 12, color = "black",
                                        margin = margin(r = 6)),
      ## Subtle horizontal grid lines aid value reading without visual clutter
      panel.grid.major.y = element_line(color = "gray90", linewidth = 0.4),
      ## Legend just outside top-right corner of panel
      legend.position      = c(1.02, 1),
      legend.justification = c(0, 1),
      legend.title         = element_text(size = 9, color = "black"),
      legend.text          = element_text(size = 8, color = "black"),
      legend.key.size      = unit(0.4, "cm"),
      ## Top margin provides headroom for n labels; right margin for legend
      plot.margin          = margin(t = 18, r = 55, b = 8, l = 8),
      plot.title           = element_blank()
    )

  p          <- lock_panel_size(p)
  safe_trait <- gsub("[^A-Za-z0-9_]", "_", trait_name)

  ggsave(
    filename = paste0(fig_dir, "trait_boxplot_", safe_trait, "_KWCI_", timestamp, ".pdf"),
    plot     = p,
    width    = 5,
    height   = 4,
    dpi      = 300
  )
}

## --- 5g. Scaled trait profiles for a subset of genotypes ---
## Purpose: compare the overall trait profile shape across selected genotypes.
## Design: individual points (raw data, honest about small n) + median line per
## genotype, all overlaid in a single panel. Min-max scaling within each trait
## puts all traits on a common 0-1 axis. Connecting lines aid profile reading;
## scatter of raw points conveys sample size and spread without overstating it.

selected_genotypes <- c("Ki3", "Mo17", "CML247")

traits_5g <- c("CH_Prolate", "CH_Oblate", "Network_Density",
               "Infection_Depth", "Mean_Hyphal_Length",
               "Peak_Branch_Level", "Vascular_Overlap_Rank")

desired_order_t_v2 <- c(
  "Prolate Score", "Oblate Score",
  "Network Density", "Infection Depth",
  "Peak Branch Level", "Mean Hyphal Length",
  "Vascular Overlap Rank"
)

## Genotype colors (colorblind-safe; consistent across the figure series)
genotype_colors <- c(
  "Ki3"    = "#0072B2",
  "Mo17"   = "#E69F00",
  "CML247" = "#009E73"
)

## Helper to rename traits consistently
rename_traits_5g <- function(x) {
  x %>%
    mutate(
      Trait = gsub("_", " ", as.character(Trait)),
      Trait = gsub("CH Prolate", "Prolate Score", Trait),
      Trait = gsub("CH Oblate",  "Oblate Score",  Trait),
      Trait = factor(Trait, levels = desired_order_t_v2)
    )
}

## Main data: filter, scale within each trait across all three genotypes, rename
plot_5g <- plot_data_kw[[2]] %>%
  filter(Genotype %in% selected_genotypes,
         Trait    %in% traits_5g) %>%
  mutate(Genotype = factor(Genotype, levels = selected_genotypes)) %>%
  group_by(Trait) %>%
  mutate(Value_scaled = (Value - min(Value, na.rm = TRUE)) /
                        (max(Value, na.rm = TRUE) - min(Value, na.rm = TRUE))) %>%
  ungroup() %>%
  rename_traits_5g()

## Median profile per genotype x trait (for connecting line)
median_5g <- plot_5g %>%
  group_by(Genotype, Trait) %>%
  summarise(Median_scaled = median(Value_scaled, na.rm = TRUE), .groups = "drop")

## Significance labels derived from the Conover-Iman compact letter display.
## For each trait, extract the CLD letter string for each of the three selected
## genotypes, then test all pairwise combinations: if any pair shares no letter
## at all they are significantly different (Bonferroni-adjusted alpha = 0.01).
## Trait is labelled "*" if at least one such pair exists, "ns" otherwise.
sig_labels_5g <- plot_data_kw[[2]] %>%
  filter(Genotype %in% selected_genotypes,
         Trait    %in% traits_5g) %>%
  dplyr::select(Trait, Genotype, groups) %>%
  distinct() %>%                          # one row per Trait x Genotype
  group_by(Trait) %>%
  summarise(
    any_sig = {
      g     <- groups                     # vector of letter strings, one per genotype
      pairs <- combn(seq_along(g), 2)     # all pairwise index combinations
      any(apply(pairs, 2, function(idx) {
        ## Two genotypes differ if their letter strings share no character
        letters_i <- strsplit(g[idx[1]], "")[[1]]
        letters_j <- strsplit(g[idx[2]], "")[[1]]
        !any(letters_i %in% letters_j)
      }))
    },
    .groups = "drop"
  ) %>%
  mutate(sig = ifelse(any_sig, "*", "ns")) %>%
  rename_traits_5g()

ggplot() +
  ## Raw points: jittered slightly on y only to avoid overplotting;
  ## x position is kept exact so points sit clearly above their trait label
  geom_point(
    data  = plot_5g,
    aes(x = Trait, y = Value_scaled, color = Genotype),
    position = position_jitter(width = 0, height = 0.015, seed = 42),
    size  = 1.4,
    alpha = 0.45,
    shape = 16
  ) +
  ## Median profile line: connects medians across traits per genotype
  geom_line(
    data  = median_5g,
    aes(x = Trait, y = Median_scaled, color = Genotype, group = Genotype),
    linewidth = 0.8
  ) +
  ## Median point on top of line for clarity
  geom_point(
    data  = median_5g,
    aes(x = Trait, y = Median_scaled, color = Genotype),
    size  = 2.5,
    shape = 16
  ) +
  ## Significance labels just below the data area, above the x-axis tick labels.
  ## Placed at y = -0.07 with clip = "off" so they sit outside the panel without
  ## affecting the y-axis scale. "ns" shown in gray, significant in black.
  geom_text(
    data        = sig_labels_5g,
    aes(x       = Trait,
        y       = -0.07,
        label   = sig,
        color   = ifelse(sig == "ns", "gray60", "black")),
    size        = 3,
    inherit.aes = FALSE,
    show.legend = FALSE
  ) +
  scale_color_manual(values = c(genotype_colors,
                                "black"  = "black",
                                "gray60" = "gray60"),
                     breaks = names(genotype_colors)) +
  scale_y_continuous(limits = c(-0.07, 1.1),
                     breaks = c(0, 0.5, 1),
                     labels = c("0", "0.5", "1")) +
  coord_cartesian(clip = "off") +
  labs(y = "Scaled value (0-1)", color = "Genotype") +
  theme_classic() +
  theme(
    legend.position      = c(1.02, 1),
    legend.justification = c(0, 1),
    legend.title         = element_text(size = 11, color = "black"),
    legend.text          = element_text(size = 10,  color = "black"),
    legend.key.size      = unit(0.5, "cm"),
    plot.margin          = margin(t = 18, r = 80, b = 8, l = 8),
    axis.text.x          = element_text(size = 9, color = "black",
                                        angle = 45, vjust = 1, hjust = 1),
    axis.text.y          = element_text(size = 9, color = "black"),
    axis.title.x         = element_blank(),
    axis.title.y         = element_text(size = 10, color = "black",
                                        margin = margin(r = 6)),
    panel.grid.major.y   = element_line(color = "gray90", linewidth = 0.4),
    panel.grid.major.x   = element_line(color = "gray93", linewidth = 0.3)
  )

ggsave(paste0(fig_dir, "trait_profile_selectgtype_", timestamp, ".pdf"),
       width = 6, height = 4, device = cairo_pdf)
```
