OSCR

Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism.

Code ↔ Paper

1 match between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 1 match
  1. [1] § Results › Loss of Pex5 selectively in cortex glia disrupts sleep ↔ Sleep/DAM_2026_Das_metrics.R, lines 1088–1148 · score 0.57 · Sleep bout duration, beam crossings, Activity bouts, S1, min, day

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R · 3,769 lines · 129 KB · no license · 1 match

  1. ####read me####
  2. #Code for drosophila activity monitor analysis, modified for R from python scripts from Vaughen et al 2022
  3. #Reads .txt activity monitor files saved in subfolders for different GAL4 drivers depleting Pex5. Experiments were done in LD except for DD run of cortex glia (GMR77A03)
  4. #Code outputs similar graphs and metrics for these 4 conditions (GMR57C10, GMR77A03 (LD), GMR77A03 (DD), and GMR57C10)
  5. #outputs graphs and metrics are saved in same subfolders (ie GMR57C10)
  6. ####install packages if not installed and load libs####
  7. #install.packages("openxlsx")
  8. library(readr)
  9. library(dplyr)
  10. library(tidyr)
  11. library(lubridate)
  12. library(stringr)
  13. library(cowplot)
  14. library(grid)
  15. library(openxlsx)
  16. #install.packages("ggplot2")
  17. #install.packages("patchwork")
  18. #install.packages("gtable") # brings you to ≥ 0.3.6
  19. #install.packages(c("ggplot2","patchwork"))# good to align deps
  20. #packageVersion("gtable"); packageVersion("patchwork")
  21. ####Functions####
  22. parse_dam_file <- function(path, date_start, date_end,
  23. tz = "America/Los_Angeles",
  24. expect_tubes = 32,
  25. fill_missing = TRUE) {
  26. # Read everything as character, then coerce
  27. raw <- suppressMessages(
  28. readr::read_tsv(
  29. file = path,
  30. col_names = FALSE,
  31. col_types = readr::cols(.default = readr::col_character()),
  32. progress = FALSE,
  33. na = c("", "NA")
  34. )
  35. )
  36. req_cols <- 10 + expect_tubes
  37. if (ncol(raw) < req_cols) {
  38. stop(sprintf("%s: found %d columns; need >= %d (10 meta + %d tubes)",
  39. basename(path), ncol(raw), req_cols, expect_tubes))
  40. }
  41. nm <- c("row","date","time","c4","c5","c6","c7","group","c9","light",
  42. paste0("tube", seq_len(expect_tubes)))
  43. names(raw)[seq_along(nm)] <- nm
  44. raw <- raw[, nm]
  45. # Parse timestamp robustly and snap to exact minute boundary
  46. ts0 <- suppressWarnings(lubridate::dmy(raw$date, tz = tz) + lubridate::hms(raw$time))
  47. ts0 <- lubridate::force_tz(ts0, tz) # enforce tz
  48. ts0 <- lubridate::floor_date(ts0, unit = "minute") # snap to minute
  49. raw$ts <- ts0
  50. raw$light <- suppressWarnings(as.integer(raw$light))
  51. raw$file <- basename(path)
  52. # Coerce tube columns to integer
  53. tube_cols <- paste0("tube", seq_len(expect_tubes))
  54. raw[tube_cols] <- lapply(raw[tube_cols], function(x) {
  55. x <- trimws(x); suppressWarnings(as.integer(x))
  56. })
  57. # Trim to date window
  58. start_ts <- lubridate::ymd_hms(paste0(date_start, " 00:00:00"), tz = tz)
  59. end_ts <- lubridate::ymd_hms(paste0(date_end, " 23:59:59"), tz = tz)
  60. raw <- raw %>%
  61. dplyr::filter(!is.na(ts), ts >= start_ts, ts <= end_ts) %>%
  62. dplyr::arrange(ts)
  63. # Build an integer minute index to avoid POSIXct join issues
  64. raw <- raw %>%
  65. dplyr::mutate(min_index = as.integer(difftime(ts, min(ts), units = "mins")))
  66. if (fill_missing && nrow(raw) > 1) {
  67. full_idx <- tibble::tibble(min_index = 0:as.integer(difftime(max(raw$ts), min(raw$ts), units = "mins")))
  68. raw <- dplyr::left_join(full_idx, raw, by = "min_index")
  69. raw$is_padded <- is.na(raw$file) # rows created by padding
  70. # carry metadata/light forward
  71. raw <- raw %>%
  72. tidyr::fill(file, date, time, ts, .direction = "down") %>%
  73. tidyr::fill(light, .direction = "down")
  74. } else {
  75. raw$is_padded <- FALSE
  76. }
  77. # Long form
  78. long <- raw %>%
  79. tidyr::pivot_longer(dplyr::starts_with("tube"), names_to = "tube", values_to = "count") %>%
  80. dplyr::mutate(
  81. tube = as.integer(stringr::str_remove(tube, "tube")),
  82. count = suppressWarnings(as.integer(count)),
  83. count_na0 = dplyr::coalesce(count, 0L)
  84. )
  85. dplyr::select(long, ts, light, tube, count, count_na0, file, date, time, is_padded)
  86. }
  87. read_dam_specs <- function(specs,
  88. date_start = NULL, date_end = NULL,
  89. tz = "America/Los_Angeles",
  90. expect_tubes = 32,
  91. fill_missing = TRUE) {
  92. stopifnot(is.list(specs), length(specs) >= 1)
  93. parts <- lapply(specs, function(s) {
  94. stopifnot(is.list(s), !is.null(s$path), !is.null(s$genotype))
  95. tubes <- if (is.null(s$tubes)) 1:expect_tubes else as.integer(s$tubes)
  96. # per-spec overrides (fall back to function args if missing)
  97. ds <- if (!is.null(s$date_start)) s$date_start else date_start
  98. de <- if (!is.null(s$date_end)) s$date_end else date_end
  99. if (is.null(ds) || is.null(de)) {
  100. stop("Provide date_start/date_end either in each spec or as function defaults.")
  101. }
  102. run_id <- if (!is.null(s$run_id)) as.character(s$run_id) else basename(s$path)
  103. df1 <- parse_dam_file(s$path, ds, de, tz, expect_tubes, fill_missing)
  104. df1 %>%
  105. dplyr::filter(tube %in% tubes) %>%
  106. dplyr::mutate(
  107. genotype = as.character(s$genotype),
  108. run_id = run_id
  109. )
  110. })
  111. dplyr::bind_rows(parts)
  112. }
  113. remove_dead <- function(df,
  114. inactivity_hours = 12,
  115. quiet = FALSE,
  116. use_is_padded = TRUE,
  117. group_vars = c("genotype","tube")) {
  118. thr <- as.integer(inactivity_hours * 60) # minutes
  119. max_zero_run <- function(v) {
  120. is_zero <- (!is.na(v)) & (v == 0L)
  121. if (!any(is_zero)) return(0L)
  122. r <- rle(is_zero)
  123. max(ifelse(r$values, r$lengths, 0L))
  124. }
  125. # Ensure is_padded exists if we want to use it
  126. if (use_is_padded && !("is_padded" %in% names(df))) df$is_padded <- FALSE
  127. # For death detection, treat padded rows as NA
  128. df_dead <- df %>%
  129. dplyr::mutate(
  130. count_dead = dplyr::if_else(
  131. use_is_padded & dplyr::coalesce(is_padded, FALSE),
  132. NA_integer_,
  133. suppressWarnings(as.integer(count))
  134. )
  135. )
  136. tube_stats <- df_dead %>%
  137. dplyr::arrange(ts) %>%
  138. dplyr::group_by(dplyr::across(dplyr::all_of(group_vars))) %>%
  139. dplyr::summarise(
  140. n_minutes = sum(!is.na(count_dead)),
  141. max_zero_run_min = max_zero_run(count_dead),
  142. .groups = "drop"
  143. ) %>%
  144. dplyr::mutate(is_dead = max_zero_run_min >= thr)
  145. purged <- tube_stats %>% dplyr::filter(is_dead)
  146. kept <- tube_stats %>% dplyr::filter(!is_dead)
  147. df2 <- df %>% dplyr::semi_join(kept, by = group_vars)
  148. attr(df2, "purged") <- purged
  149. attr(df2, "tube_stats") <- tube_stats
  150. # ---- robust summary (no `.data$missingcol` lookups) ----
  151. summary_tbl <- tube_stats %>%
  152. dplyr::count(genotype, is_dead, name = "n") %>%
  153. tidyr::pivot_wider(
  154. names_from = is_dead,
  155. values_from = n,
  156. names_prefix = "removed_",
  157. values_fill = 0
  158. )
  159. # Add missing columns if pivot_wider didn't create them
  160. if (!("removed_TRUE" %in% names(summary_tbl))) summary_tbl$removed_TRUE <- 0L
  161. if (!("removed_FALSE" %in% names(summary_tbl))) summary_tbl$removed_FALSE <- 0L
  162. summary_tbl <- summary_tbl %>%
  163. dplyr::transmute(
  164. genotype,
  165. removed_TRUE,
  166. kept = removed_FALSE,
  167. total = removed_TRUE + removed_FALSE
  168. )
  169. if (!quiet) {
  170. for (i in seq_len(nrow(summary_tbl))) {
  171. g <- summary_tbl$genotype[i]
  172. r <- summary_tbl$removed_TRUE[i]
  173. k <- summary_tbl$kept[i]
  174. message(sprintf("Genotype %-12s: removed %2d, kept %2d (total %2d)", g, r, k, r + k))
  175. }
  176. }
  177. list(
  178. data = df2,
  179. purged = purged,
  180. tube_stats = tube_stats,
  181. summary = summary_tbl
  182. )
  183. }
  184. .phase_label <- function(light) dplyr::if_else(light == 1L, "Light", "Dark", missing = NA_character_)
  185. plot_daynight_activity <- function(df, outdir = "DAM_Graphs", color_map = NULL,
  186. width_in = 6, height_in = 4, dpi = 300) {
  187. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  188. sum_tube <- df |>
  189. dplyr::mutate(phase = .phase_label(light)) |>
  190. dplyr::filter(!is.na(phase)) |>
  191. dplyr::group_by(genotype, tube, phase) |>
  192. dplyr::summarise(total = sum(count_na0, na.rm = TRUE), .groups = "drop")
  193. sum_geno <- sum_tube |>
  194. dplyr::group_by(genotype, phase) |>
  195. dplyr::summarise(n = dplyr::n(), mean = mean(total), sem = stats::sd(total)/sqrt(n), .groups = "drop")
  196. if (!is.null(color_map)) sum_geno$col <- unname(color_map[sum_geno$genotype])
  197. p <- ggplot2::ggplot(sum_geno, ggplot2::aes(x = phase, y = mean, fill = genotype)) +
  198. ggplot2::geom_col(position = ggplot2::position_dodge(width = 0.6), width = 0.6, color = "black", alpha = 0.9) +
  199. ggplot2::geom_errorbar(ggplot2::aes(ymin = mean - sem, ymax = mean + sem),
  200. position = ggplot2::position_dodge(width = 0.6), width = 0.2) +
  201. ggplot2::labs(x = NULL, y = "Total activity (counts)", title = "DAM: Day vs Night Activity") +
  202. ggplot2::theme_classic(base_size = 12)
  203. if (!is.null(color_map)) {
  204. p_main <- p_main +
  205. ggplot2::scale_color_manual(values = color_map, labels = genotype_labels) +
  206. ggplot2::scale_fill_manual(values = color_map, labels = genotype_labels) +
  207. ggplot2::guides(
  208. color = ggplot2::guide_legend(label = ggplot2::label_parsed),
  209. fill = ggplot2::guide_legend(label = ggplot2::label_parsed)
  210. )
  211. }
  212. out <- file.path(outdir, sprintf("DAM_activity_day_night_%s.png", format(Sys.time(), "%Y%m%d_%H%M%S")))
  213. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  214. invisible(list(plot = p, summary = sum_geno, file = out))
  215. }
  216. flag_sleep_minutes <- function(count, sleep_block_min = 5L) {
  217. # count: integer vector with possible NA; NA breaks runs
  218. is_zero <- (!is.na(count)) & (count == 0L)
  219. r <- rle(is_zero)
  220. idx <- rep.int(seq_along(r$lengths), r$lengths)
  221. as.integer(is_zero & (r$lengths[idx] >= as.integer(sleep_block_min)))
  222. }
  223. plot_daynight_sleep <- function(df, sleep_block_min = 5L, outdir = "DAM_Graphs",
  224. color_map = NULL, width_in = 6, height_in = 4, dpi = 300) {
  225. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  226. sleep_df <- df |>
  227. dplyr::arrange(ts) |>
  228. dplyr::group_by(genotype, tube) |>
  229. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min)) |>
  230. dplyr::ungroup() |>
  231. dplyr::mutate(phase = .phase_label(light)) |>
  232. dplyr::filter(!is.na(phase))
  233. sum_tube <- sleep_df |>
  234. dplyr::group_by(genotype, tube, phase) |>
  235. dplyr::summarise(total_sleep_min = sum(sleep_flag, na.rm = TRUE), .groups = "drop")
  236. sum_geno <- sum_tube |>
  237. dplyr::group_by(genotype, phase) |>
  238. dplyr::summarise(n = dplyr::n(), mean = mean(total_sleep_min), sem = stats::sd(total_sleep_min)/sqrt(n), .groups = "drop")
  239. p <- ggplot2::ggplot(sum_geno, ggplot2::aes(x = phase, y = mean, fill = genotype)) +
  240. ggplot2::geom_col(position = ggplot2::position_dodge(width = 0.6), width = 0.6, color = "black", alpha = 0.9) +
  241. ggplot2::geom_errorbar(ggplot2::aes(ymin = mean - sem, ymax = mean + sem),
  242. position = ggplot2::position_dodge(width = 0.6), width = 0.2) +
  243. ggplot2::labs(x = NULL, y = sprintf("Total sleep (min, ≥%d-min bouts)", as.integer(sleep_block_min)),
  244. title = "DAM: Day vs Night Sleep") +
  245. ggplot2::theme_classic(base_size = 12)
  246. if (!is.null(color_map)) p_main <- p_main +
  247. ggplot2::scale_color_manual(values = color_map, labels = genotype_labels) +
  248. ggplot2::scale_fill_manual(values = color_map, labels = genotype_labels) +
  249. ggplot2::guides(
  250. color = ggplot2::guide_legend(label = ggplot2::label_parsed),
  251. fill = ggplot2::guide_legend(label = ggplot2::label_parsed)
  252. )
  253. out <- file.path(outdir, sprintf("DAM_sleep_day_night_%s.png", format(Sys.time(), "%Y%m%d_%H%M%S")))
  254. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  255. invisible(list(plot = p, summary = sum_geno, file = out))
  256. }
  257. infer_light_cycle <- function(df, zt0_hour = 6L) {
  258. tz_ts <- lubridate::tz(df$ts)
  259. if (is.null(tz_ts) || tz_ts == "") tz_ts <- "UTC"
  260. min_ts <- suppressWarnings(min(df$ts, na.rm = TRUE))
  261. max_ts <- suppressWarnings(max(df$ts, na.rm = TRUE))
  262. if (!is.finite(min_ts)) stop("infer_light_cycle(): df$ts has no finite values.")
  263. # IMPORTANT: day boundaries in the SAME tz as ts
  264. min_day <- as.Date(lubridate::with_tz(min_ts, tz_ts))
  265. max_day <- as.Date(lubridate::with_tz(max_ts, tz_ts))
  266. days <- seq.Date(min_day, max_day, by = "day")
  267. # IMPORTANT: create scheduled ON/OFF in SAME tz as ts
  268. sched <- tibble::tibble(
  269. day = days,
  270. ON_sched = lubridate::as_datetime(days, tz = tz_ts) + lubridate::hours(zt0_hour),
  271. OFF_sched = lubridate::as_datetime(days, tz = tz_ts) + lubridate::hours(zt0_hour + 12L)
  272. )
  273. x0 <- df |>
  274. dplyr::filter(!is.na(light)) |>
  275. dplyr::distinct(ts, light) |>
  276. dplyr::arrange(ts)
  277. # DD/LL/UNKNOWN: just use scheduled subjective times
  278. if (nrow(x0) == 0 || length(unique(x0$light)) == 1) {
  279. mode <- if (nrow(x0) == 0) "UNKNOWN" else if (unique(x0$light) == 0L) "DD" else "LL"
  280. per_day <- sched |> dplyr::transmute(day, ON = ON_sched, OFF = OFF_sched)
  281. return(list(
  282. mode = mode,
  283. per_day = per_day,
  284. summary = tibble::tibble(
  285. zt0_ref = stats::median(per_day$ON, na.rm = TRUE),
  286. zt12_ref = stats::median(per_day$OFF, na.rm = TRUE)
  287. )
  288. ))
  289. }
  290. # LD inference + fill missing with schedule (also tz-safe because sched is tz-safe)
  291. x <- x0 |>
  292. dplyr::mutate(prev = dplyr::lag(light),
  293. changed = !is.na(prev) & light != prev) |>
  294. dplyr::filter(changed) |>
  295. dplyr::mutate(kind = dplyr::if_else(light == 1L, "ON", "OFF"),
  296. day = as.Date(lubridate::with_tz(ts, tz_ts)))
  297. per_day_inf <- x |>
  298. dplyr::group_by(day, kind) |>
  299. dplyr::summarise(time = min(ts), .groups = "drop") |>
  300. tidyr::pivot_wider(names_from = kind, values_from = time)
  301. if (!("ON" %in% names(per_day_inf))) per_day_inf$ON <- as.POSIXct(NA, tz = tz_ts)
  302. if (!("OFF" %in% names(per_day_inf))) per_day_inf$OFF <- as.POSIXct(NA, tz = tz_ts)
  303. per_day <- sched |>
  304. dplyr::left_join(per_day_inf, by = "day") |>
  305. dplyr::mutate(
  306. ON = dplyr::coalesce(ON, ON_sched),
  307. OFF = dplyr::coalesce(OFF, OFF_sched)
  308. ) |>
  309. dplyr::select(day, ON, OFF)
  310. list(
  311. mode = "LD",
  312. per_day = per_day,
  313. summary = tibble::tibble(
  314. zt0_ref = stats::median(per_day$ON, na.rm = TRUE),
  315. zt12_ref = stats::median(per_day$OFF, na.rm = TRUE)
  316. )
  317. )
  318. }
  319. plot_average_day <- function(
  320. df,
  321. metric = c("activity","sleep"),
  322. bin_minutes = 5L,
  323. color_map = NULL,
  324. genotype_labels = NULL, # <-- NEW: named plotmath strings
  325. # appearance
  326. mean_linewidth = 1.2,
  327. sem_alpha = 0.3,
  328. # titles/labels
  329. title = NULL,
  330. x_label = "ZT (h)",
  331. y_label = NULL,
  332. title_size = 20,
  333. axis_title_size = 20,
  334. axis_text_size = 14,
  335. show_legend = TRUE,
  336. # phase shading (background)
  337. show_bg_phase_shading = TRUE,
  338. bg_light_color = "#FFF7AE", bg_light_alpha = 0.18,
  339. bg_dark_color = "#1E2430", bg_dark_alpha = 0.12,
  340. # phase bar
  341. show_phase_bar = TRUE,
  342. phase_bar_position = c("inside","below"),
  343. bar_light_color = "#FFD84D",
  344. bar_dark_color = "#22313F",
  345. bar_height_frac = 1/50, # used only for "inside"
  346. # file
  347. outdir = "DAM_Graphs",
  348. filename = NULL,
  349. width_in = 7, height_in = 4, dpi = 300
  350. ) {
  351. phase_bar_position <- match.arg(phase_bar_position)
  352. metric <- match.arg(metric)
  353. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  354. # ----- data prep -----
  355. lc <- infer_light_cycle(df, zt0_hour = 6L)
  356. tz_ts <- lubridate::tz(df$ts)
  357. if (is.null(tz_ts) || tz_ts == "") tz_ts <- "UTC"
  358. per_day <- lc$per_day
  359. mode <- lc$mode
  360. # If user didn't override, use CT for constant conditions
  361. if (is.null(x_label)) x_label <- "ZT (h)"
  362. if (mode %in% c("DD","LL") && identical(x_label, "ZT (h)")) {
  363. x_label <- "CT (h)"
  364. }
  365. df2 <- df |>
  366. dplyr::mutate(day = as.Date(lubridate::with_tz(ts, tz_ts))) |>
  367. dplyr::left_join(per_day |> dplyr::select(day, ON), by = "day") |>
  368. dplyr::mutate(
  369. zt_min = as.integer((as.numeric(difftime(ts, ON, units = "mins")) %% (24*60)))
  370. )
  371. if (metric == "sleep") {
  372. df2 <- df2 |>
  373. dplyr::arrange(ts) |>
  374. dplyr::group_by(genotype, tube) |>
  375. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min = 5L)) |>
  376. dplyr::ungroup()
  377. }
  378. df2 <- df2 |>
  379. dplyr::mutate(bin = (zt_min %/% bin_minutes) * bin_minutes)
  380. agg_tube <- df2 |>
  381. dplyr::group_by(genotype, tube, bin) |>
  382. dplyr::summarise(
  383. value = if (metric == "activity") mean(count_na0, na.rm = TRUE) else mean(sleep_flag, na.rm = TRUE),
  384. .groups = "drop"
  385. )
  386. agg_geno <- agg_tube |>
  387. dplyr::group_by(genotype, bin) |>
  388. dplyr::summarise(
  389. n = dplyr::n(),
  390. mean = mean(value),
  391. sem = stats::sd(value)/sqrt(n),
  392. .groups = "drop"
  393. ) |>
  394. dplyr::mutate(zt_h = bin/60)
  395. if (is.null(y_label)) y_label <- if (metric == "activity") "Counts / min" else "Sleep prob. / min"
  396. default_title <- sprintf("DAM average day: %s", metric)
  397. # y-range (for "inside" bar)
  398. y_min <- min(agg_geno$mean - agg_geno$sem, na.rm = TRUE)
  399. y_max <- max(agg_geno$mean + agg_geno$sem, na.rm = TRUE)
  400. yr <- y_max - y_min
  401. bar_y0 <- y_min
  402. bar_y1 <- y_min + yr * bar_height_frac
  403. # ----- main plot -----
  404. p_main <- ggplot2::ggplot(
  405. agg_geno,
  406. ggplot2::aes(x = zt_h, y = mean, color = genotype, fill = genotype)
  407. )
  408. if (show_bg_phase_shading) {
  409. p_main <- p_main +
  410. ggplot2::annotate("rect", xmin = 0, xmax = 12, ymin = -Inf, ymax = Inf,
  411. fill = bg_light_color, alpha = bg_light_alpha, linewidth = 0) +
  412. ggplot2::annotate("rect", xmin = 12, xmax = 24, ymin = -Inf, ymax = Inf,
  413. fill = bg_dark_color, alpha = bg_dark_alpha, linewidth = 0)
  414. }
  415. p_main <- p_main +
  416. ggplot2::geom_ribbon(ggplot2::aes(ymin = mean - sem, ymax = mean + sem),
  417. alpha = sem_alpha, linewidth = 0) +
  418. ggplot2::geom_line(linewidth = mean_linewidth) +
  419. ggplot2::scale_x_continuous(breaks = seq(0,24,6), limits = c(0,24), expand = c(0,0)) +
  420. ggplot2::labs(
  421. x = if (show_phase_bar && phase_bar_position == "below") NULL else x_label,
  422. y = y_label,
  423. title = if (is.null(title)) default_title else title
  424. ) +
  425. ggplot2::theme_classic(base_size = 12) +
  426. ggplot2::theme(
  427. legend.position = if (show_legend) "right" else "none",
  428. plot.title = ggplot2::element_text(hjust = 0.5, size = title_size),
  429. axis.title.x = ggplot2::element_text(size = axis_title_size),
  430. axis.title.y = ggplot2::element_text(size = axis_title_size),
  431. axis.text = ggplot2::element_text(size = axis_text_size)
  432. )
  433. # ----- legend label parsing FIX -----
  434. # We parse *in the scale*, not via guide_legend(label=label_parsed) (that often bites).
  435. # genotype_labels should be a named character vector of plotmath strings.
  436. if (!is.null(color_map)) {
  437. # If user didn't provide labels, default to plain genotype names.
  438. if (is.null(genotype_labels)) {
  439. genotype_labels <- setNames(names(color_map), names(color_map))
  440. use_parsed <- FALSE
  441. } else {
  442. # ensure same ordering / breaks across plots
  443. use_parsed <- TRUE
  444. # keep only labels that exist in the color_map ordering
  445. genotype_labels <- genotype_labels[names(color_map)]
  446. }
  447. p_main <- p_main +
  448. ggplot2::theme(
  449. legend.text = ggplot2::element_text(size = 14),
  450. legend.title = ggplot2::element_text(size = 0)
  451. )
  452. if (use_parsed) {
  453. p_main <- p_main +
  454. ggplot2::scale_color_manual(
  455. values = color_map,
  456. breaks = names(color_map),
  457. labels = parse(text = genotype_labels)
  458. ) +
  459. ggplot2::scale_fill_manual(
  460. values = color_map,
  461. breaks = names(color_map),
  462. labels = parse(text = genotype_labels)
  463. ) +
  464. ggplot2::guides(
  465. # keep ONLY line legend
  466. color = ggplot2::guide_legend(
  467. label = ggplot2::label_parsed,
  468. override.aes = list(fill = NA)
  469. ),
  470. # remove ribbon legend entirely
  471. fill = "none"
  472. )
  473. } else {
  474. p_main <- p_main +
  475. ggplot2::scale_color_manual(
  476. values = color_map,
  477. breaks = names(color_map),
  478. labels = genotype_labels
  479. ) +
  480. ggplot2::scale_fill_manual(
  481. values = color_map,
  482. breaks = names(color_map),
  483. labels = genotype_labels
  484. ) +
  485. ggplot2::guides(
  486. color = ggplot2::guide_legend(override.aes = list(fill = NA)),
  487. fill = "none"
  488. )
  489. }
  490. }
  491. # ----- phase bar handling -----
  492. if (show_phase_bar && phase_bar_position == "inside") {
  493. if (is.finite(yr) && yr > 0) {
  494. p_main <- p_main +
  495. ggplot2::annotate("rect", xmin = 0, xmax = 12, ymin = bar_y0, ymax = bar_y1,
  496. fill = bar_light_color, alpha = 1, linewidth = 0) +
  497. ggplot2::annotate("rect", xmin = 12, xmax = 24, ymin = bar_y0, ymax = bar_y1,
  498. fill = bar_dark_color, alpha = 1, linewidth = 0)
  499. }
  500. p_final <- p_main
  501. } else if (show_phase_bar && phase_bar_position == "below") {
  502. p_bar <- ggplot2::ggplot() +
  503. ggplot2::annotate("rect", xmin = 0, xmax = 12, ymin = 0, ymax = 1,
  504. fill = bar_light_color, alpha = 1, linewidth = 0) +
  505. ggplot2::annotate("rect", xmin = 12, xmax = 24, ymin = 0, ymax = 1,
  506. fill = bar_dark_color, alpha = 1, linewidth = 0) +
  507. ggplot2::scale_x_continuous(limits = c(0,24), expand = c(0,0)) +
  508. ggplot2::coord_cartesian(ylim = c(0,1), clip = "off") +
  509. ggplot2::labs(x = x_label, y = NULL) +
  510. ggplot2::theme_void(base_size = 12) +
  511. ggplot2::theme(
  512. plot.margin = ggplot2::margin(t = -6, r = 5, b = 5, l = 5),
  513. axis.title.x = ggplot2::element_text(size = axis_title_size, hjust = 0.5)
  514. )
  515. if (requireNamespace("patchwork", quietly = TRUE)) {
  516. bar_rel <- bar_height_frac / max(1e-6, (1 - bar_height_frac))
  517. p_final <- p_main / p_bar + patchwork::plot_layout(heights = c(1, bar_rel))
  518. } else if (requireNamespace("cowplot", quietly = TRUE)) {
  519. bar_rel <- bar_height_frac / max(1e-6, (1 - bar_height_frac))
  520. p_final <- cowplot::plot_grid(
  521. p_main + ggplot2::theme(axis.title.x = ggplot2::element_blank()),
  522. p_bar, ncol = 1, rel_heights = c(1, bar_rel)
  523. )
  524. } else {
  525. message("phase_bar_position='below' requested, but neither patchwork nor cowplot is installed; drawing without stacking.")
  526. p_final <- p_main
  527. }
  528. } else {
  529. p_final <- p_main
  530. }
  531. # ----- save -----
  532. if (is.null(filename)) {
  533. filename <- sprintf("DAM_avgday_%s_bin%dm.png", metric, bin_minutes)
  534. }
  535. out <- file.path(outdir, filename)
  536. ggplot2::ggsave(out, p_final, width = width_in, height = height_in, dpi = dpi)
  537. invisible(list(plot = p_final, summary = agg_geno, file = out))
  538. }
  539. build_daynight_metric_avg <- function(df,
  540. metric = c("activity","sleep"),
  541. zt0_hour = 6L,
  542. sleep_block_min = 5L) {
  543. metric <- match.arg(metric)
  544. lc <- infer_light_cycle(df, zt0_hour = zt0_hour)
  545. per_day <- lc$per_day
  546. mode <- lc$mode
  547. tz_ts <- lubridate::tz(df$ts)
  548. if (is.null(tz_ts) || tz_ts == "") tz_ts <- "UTC"
  549. df2 <- df |>
  550. dplyr::mutate(day = as.Date(lubridate::with_tz(ts, tz_ts))) |>
  551. dplyr::left_join(per_day |> dplyr::select(day, ON), by = "day") |>
  552. dplyr::mutate(
  553. zt_min = as.integer((as.numeric(difftime(ts, ON, units = "mins")) %% (24*60))),
  554. phase = dplyr::if_else(zt_min < 12*60, "Light", "Dark")
  555. )
  556. # ---- choose the per-minute value to average (DO NOT use if_else here) ----
  557. if (metric == "activity") {
  558. df2 <- df2 |>
  559. dplyr::mutate(value_min = dplyr::coalesce(count_na0, dplyr::coalesce(count, 0)))
  560. } else {
  561. df2 <- df2 |>
  562. dplyr::arrange(ts) |>
  563. dplyr::group_by(genotype, tube) |>
  564. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min = sleep_block_min)) |>
  565. dplyr::ungroup() |>
  566. dplyr::mutate(value_min = sleep_flag)
  567. }
  568. dn <- df2 |>
  569. dplyr::group_by(genotype, tube, phase) |>
  570. dplyr::summarise(
  571. value = if (metric == "activity") mean(value_min, na.rm = TRUE) * (12*60) else mean(value_min, na.rm = TRUE),
  572. .groups = "drop"
  573. )
  574. attr(dn, "mode") <- mode
  575. attr(dn, "metric") <- metric
  576. dn
  577. }
  578. plot_daynight_metric_box <- function(
  579. df,
  580. metric = c("activity","sleep"),
  581. zt0_hour = 6L,
  582. sleep_block_min = 5L,
  583. outdir = "DAM_Graphs",
  584. color_map = NULL,
  585. genotype_labels = NULL,
  586. legend_text_size = 14,
  587. legend_title_size = 0,
  588. mutant = "CG_GAL4_pex5",
  589. controls = c("pex5","CG_GAL4"),
  590. show_legend = TRUE,
  591. show_dots = TRUE,
  592. dot_size = 0.8,
  593. show_kw_stars = FALSE,
  594. show_brackets = TRUE,
  595. drop_ns = TRUE,
  596. title = NULL,
  597. x_label = NULL,
  598. y_label = NULL,
  599. phase_labels = c(Light = "Day", Dark = "Night"),
  600. width_in = 4, height_in = 4, dpi = 300,
  601. filename = NULL
  602. ) {
  603. metric <- match.arg(metric)
  604. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  605. dn <- build_daynight_metric_avg(
  606. df,
  607. metric = metric,
  608. zt0_hour = zt0_hour,
  609. sleep_block_min = sleep_block_min
  610. )
  611. mode <- attr(dn, "mode")
  612. if (is.null(x_label)) x_label <- if (mode %in% c("DD","LL")) "CT phase" else NULL
  613. default_title <- if (metric == "activity") "Activity" else "Sleep"
  614. default_y <- if (metric == "activity") "Beam crossings per 12h" else "Sleep probability / min"
  615. # stable ordering
  616. dn$phase <- factor(dn$phase, levels = names(phase_labels))
  617. if (!is.null(color_map)) dn$genotype <- factor(dn$genotype, levels = names(color_map))
  618. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  619. dn, value_col = "value",
  620. mutant = mutant, controls = controls,
  621. p_adjust = "BH"
  622. ) |>
  623. dplyr::mutate(
  624. p_use = dplyr::coalesce(p_adj, p),
  625. label = .p_to_stars(p_use)
  626. )
  627. if (drop_ns) {
  628. ctrl_vs_mut <- ctrl_vs_mut |> dplyr::filter(!is.na(label), label != "ns")
  629. }
  630. kw_only <- NULL
  631. if (show_kw_stars) {
  632. kw_only <- nonparam_stats_by_phase(dn, value_col = "value", p_adjust = "BH") |>
  633. dplyr::filter(test == "Kruskal–Wallis") |>
  634. dplyr::mutate(label = .p_to_stars(p))
  635. if (drop_ns) kw_only <- kw_only |> dplyr::filter(label != "ns")
  636. }
  637. p <- ggplot2::ggplot(dn, ggplot2::aes(x = phase, y = value, fill = genotype)) +
  638. ggplot2::geom_boxplot(
  639. position = ggplot2::position_dodge(width = 0.6),
  640. width = 0.6, alpha = 0.9, outlier.shape = NA
  641. ) +
  642. ggplot2::labs(
  643. x = x_label,
  644. y = if (is.null(y_label)) default_y else y_label,
  645. title = if (is.null(title)) default_title else title
  646. ) +
  647. ggplot2::theme_classic(base_size = 12) +
  648. ggplot2::theme(
  649. legend.position = if (show_legend) "right" else "none",
  650. legend.text = ggplot2::element_text(size = legend_text_size),
  651. legend.title = ggplot2::element_text(size = legend_title_size),
  652. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  653. axis.title.y = ggplot2::element_text(size = 20),
  654. axis.text = ggplot2::element_text(size = 14),
  655. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  656. ) +
  657. ggplot2::scale_x_discrete(labels = phase_labels) +
  658. ggplot2::scale_y_continuous(limits = c(0, NA),
  659. expand = ggplot2::expansion(mult = c(0, 0.18))) +
  660. ggplot2::coord_cartesian(clip = "off")
  661. if (show_dots) {
  662. p <- p + ggplot2::geom_point(
  663. position = ggplot2::position_jitterdodge(jitter.width = 0.04, dodge.width = 0.6),
  664. size = dot_size, alpha = 0.6, shape = 16, color = "black"
  665. )
  666. }
  667. if (!is.null(color_map)) {
  668. if (!is.null(genotype_labels)) {
  669. genotype_labels <- genotype_labels[names(color_map)]
  670. p <- p +
  671. ggplot2::scale_fill_manual(
  672. values = color_map,
  673. breaks = names(color_map),
  674. labels = parse(text = genotype_labels)
  675. ) +
  676. ggplot2::guides(fill = ggplot2::guide_legend(label = ggplot2::label_parsed))
  677. } else {
  678. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  679. }
  680. }
  681. if (show_kw_stars && !is.null(kw_only) && nrow(kw_only)) {
  682. y_max <- dn |>
  683. dplyr::group_by(phase) |>
  684. dplyr::summarise(ypos = max(value, na.rm = TRUE) * 1.08, .groups = "drop")
  685. sig_df <- kw_only |>
  686. dplyr::select(phase, label) |>
  687. dplyr::left_join(y_max, by = "phase")
  688. p <- p + ggplot2::geom_text(
  689. data = sig_df,
  690. ggplot2::aes(x = phase, y = ypos, label = label),
  691. inherit.aes = FALSE, vjust = 0, size = 8
  692. )
  693. }
  694. if (show_brackets && nrow(ctrl_vs_mut)) {
  695. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(dn$genotype)
  696. p <- .add_pairwise_brackets_from_build(
  697. p,
  698. stats_df = ctrl_vs_mut,
  699. genotype_order = genotype_order,
  700. y_pad_frac = 0.06,
  701. text_size = 5
  702. )
  703. }
  704. if (is.null(filename)) {
  705. filename <- sprintf("DAM_%s_day_night_box.png", metric)
  706. }
  707. out <- file.path(outdir, filename)
  708. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  709. invisible(list(plot = p, stats_ctrl_vs_mut = ctrl_vs_mut, data = dn, file = out, mode = mode))
  710. }
  711. plot_total_sleep_by_day <- function(
  712. df,
  713. sleep_block_min = 5L,
  714. outdir = "DAM_Graphs",
  715. color_map = NULL,
  716. show_legend = TRUE,
  717. # appearance
  718. mean_linewidth = 1.0,
  719. sem_alpha = 0.25,
  720. show_points = FALSE, # <-- NEW: turn day dots on/off
  721. point_size = 2,
  722. # labels
  723. title = NULL,
  724. x_label = "Day",
  725. y_label = NULL,
  726. title_size = 20,
  727. axis_title_size = 20,
  728. axis_text_size = 14,
  729. # day filtering (to avoid clipped/empty days)
  730. drop_empty_days = TRUE, # drop days with 0 real minutes across all tubes
  731. min_real_minutes_per_day = NULL, # e.g., 24*60 to keep only full days per tube
  732. # file
  733. filename = "DAM_sleep_total_by_day.png",
  734. width_in = 7, height_in = 4, dpi = 300
  735. ) {
  736. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  737. # Ensure is_padded exists; older dfs before fill_missing may not have it.
  738. if (!("is_padded" %in% names(df))) df$is_padded <- FALSE
  739. # Per-minute sleep flag (NA/padded minutes break runs, so flagged as 0 by design)
  740. df2 <- df %>%
  741. dplyr::arrange(ts) %>%
  742. dplyr::group_by(genotype, tube) %>%
  743. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min)) %>%
  744. dplyr::ungroup() %>%
  745. dplyr::mutate(day = as.Date(ts))
  746. # Tube/day totals AND real (non-padded) minutes per tube/day
  747. tube_day <- df2 %>%
  748. dplyr::group_by(genotype, tube, day) %>%
  749. dplyr::summarise(
  750. total_sleep_min = sum(sleep_flag, na.rm = TRUE),
  751. real_minutes = sum(!dplyr::coalesce(is_padded, FALSE)),
  752. .groups = "drop"
  753. )
  754. # (A) Optionally drop tube-days with too few real minutes (e.g., partial days)
  755. if (!is.null(min_real_minutes_per_day)) {
  756. tube_day <- dplyr::filter(tube_day, real_minutes >= as.integer(min_real_minutes_per_day))
  757. }
  758. # (B) Optionally drop entire calendar days that have 0 real minutes across all tubes
  759. if (drop_empty_days) {
  760. keep_days <- tube_day %>%
  761. dplyr::group_by(day) %>%
  762. dplyr::summarise(all_real = sum(real_minutes, na.rm = TRUE) > 0, .groups = "drop") %>%
  763. dplyr::filter(all_real) %>%
  764. dplyr::pull(day)
  765. tube_day <- dplyr::filter(tube_day, day %in% keep_days)
  766. }
  767. # Genotype mean ± SEM per day
  768. geno_day <- tube_day %>%
  769. dplyr::group_by(genotype, day) %>%
  770. dplyr::summarise(
  771. n = dplyr::n(),
  772. mean = mean(total_sleep_min),
  773. sem = stats::sd(total_sleep_min)/sqrt(n),
  774. .groups = "drop"
  775. )
  776. if (is.null(y_label)) {
  777. y_label <- sprintf("Total sleep / day (min, ≥%d-min bouts)", as.integer(sleep_block_min))
  778. }
  779. if (is.null(title)) title <- "DAM: Total Sleep per Day"
  780. p <- ggplot2::ggplot(
  781. geno_day,
  782. ggplot2::aes(x = day, y = mean, color = genotype, fill = genotype, group = genotype)
  783. ) +
  784. ggplot2::geom_ribbon(ggplot2::aes(ymin = mean - sem, ymax = mean + sem), alpha = sem_alpha, linewidth = 0) +
  785. ggplot2::geom_line(linewidth = mean_linewidth) +
  786. ggplot2::labs(x = x_label, y = y_label, title = title) +
  787. ggplot2::theme_classic(base_size = 12) +
  788. ggplot2::theme(
  789. legend.position = if (show_legend) "right" else "none",
  790. plot.title = ggplot2::element_text(hjust = 0.5, size = title_size),
  791. axis.title.x = ggplot2::element_text(size = axis_title_size),
  792. axis.title.y = ggplot2::element_text(size = axis_title_size),
  793. axis.text = ggplot2::element_text(size = axis_text_size)
  794. )
  795. if (show_points) {
  796. p <- p + ggplot2::geom_point(size = point_size)
  797. }
  798. if (!is.null(color_map)) {
  799. p <- p +
  800. ggplot2::scale_color_manual(values = color_map, labels = genotype_labels) +
  801. ggplot2::scale_fill_manual(values = color_map, labels = genotype_labels) +
  802. ggplot2::guides(
  803. color = ggplot2::guide_legend(label = ggplot2::label_parsed),
  804. fill = ggplot2::guide_legend(label = ggplot2::label_parsed)
  805. )
  806. }
  807. out <- file.path(outdir, filename)
  808. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  809. invisible(list(plot = p, summary = geno_day, data = tube_day, file = out))
  810. }
  811. build_daynight_activity_avg <- function(df) {
  812. df %>%
  813. dplyr::mutate(
  814. phase = dplyr::if_else(light == 1L, "Light", "Dark", missing = NA_character_),
  815. day = as.Date(ts),
  816. fly_id = paste(run_id, tube, sep = "_") # ✅ unique fly across experiments
  817. ) %>%
  818. dplyr::filter(!is.na(phase)) %>%
  819. dplyr::group_by(genotype, fly_id, phase, day) %>%
  820. dplyr::summarise(total_day = sum(count_na0, na.rm = TRUE), .groups = "drop") %>%
  821. dplyr::group_by(genotype, fly_id, phase) %>%
  822. dplyr::summarise(value = mean(total_day, na.rm = TRUE), days = dplyr::n(), .groups = "drop")
  823. }
  824. build_daynight_sleep_avg <- function(df, sleep_block_min = 5L) {
  825. df %>%
  826. dplyr::arrange(ts) %>%
  827. dplyr::group_by(run_id, genotype, tube) %>% # ✅ include run_id here too
  828. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min)) %>%
  829. dplyr::ungroup() %>%
  830. dplyr::mutate(
  831. phase = dplyr::if_else(light == 1L, "Light", "Dark", missing = NA_character_),
  832. day = as.Date(ts),
  833. fly_id = paste(run_id, tube, sep = "_") # ✅ unique fly across experiments
  834. ) %>%
  835. dplyr::filter(!is.na(phase)) %>%
  836. dplyr::group_by(genotype, fly_id, phase, day) %>%
  837. dplyr::summarise(total_day = sum(sleep_flag, na.rm = TRUE), .groups = "drop") %>%
  838. dplyr::group_by(genotype, fly_id, phase) %>%
  839. dplyr::summarise(value = mean(total_day, na.rm = TRUE), days = dplyr::n(), .groups = "drop")
  840. }
  841. # Minutes from ZT12 to first sleep bout in dark phase, averaged across nights per fly.
  842. # Called by plot_sleep_latency_box (was missing from original script).
  843. build_sleep_latency <- function(df,
  844. zt0_hour = 6L,
  845. sleep_block_min = 5L,
  846. summarize_days = c("mean", "median")) {
  847. summarize_days <- match.arg(summarize_days)
  848. lc <- infer_light_cycle(df, zt0_hour = zt0_hour)
  849. per_day <- lc$per_day
  850. tz_ts <- lubridate::tz(df$ts)
  851. if (is.null(tz_ts) || tz_ts == "") tz_ts <- "UTC"
  852. df2 <- df |>
  853. dplyr::mutate(
  854. day = as.Date(lubridate::with_tz(ts, tz_ts)),
  855. fly_id = dplyr::if_else(
  856. !is.na(run_id) & run_id != "",
  857. paste(run_id, tube, sep = "__"),
  858. as.character(tube)
  859. )
  860. ) |>
  861. dplyr::left_join(per_day |> dplyr::select(day, ON), by = "day") |>
  862. dplyr::mutate(
  863. zt_min = as.integer((as.numeric(difftime(ts, ON, units = "mins")) %% (24L * 60L)))
  864. ) |>
  865. dplyr::arrange(genotype, fly_id, ts) |>
  866. dplyr::group_by(genotype, fly_id) |>
  867. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min = sleep_block_min)) |>
  868. dplyr::ungroup()
  869. # Per fly-day: first sleep minute in the dark phase (ZT12 onward)
  870. lat_day <- df2 |>
  871. dplyr::filter(zt_min >= 12L * 60L, sleep_flag == 1L) |>
  872. dplyr::group_by(genotype, fly_id, day) |>
  873. dplyr::summarise(latency_min = min(zt_min, na.rm = TRUE) - 12L * 60L, .groups = "drop")
  874. # Average across nights per fly
  875. lat_day |>
  876. dplyr::group_by(genotype, fly_id) |>
  877. dplyr::summarise(
  878. value = if (summarize_days == "mean") mean(latency_min, na.rm = TRUE)
  879. else stats::median(latency_min, na.rm = TRUE),
  880. .groups = "drop"
  881. )
  882. }
  883. #### Xlsx export helper ####
  884. # Reshapes per-fly data to wide format (one column per genotype) with Mean and SEM appended.
  885. .wide_with_summary <- function(df, value_col = "value", geno_order = NULL) {
  886. if (is.null(geno_order)) geno_order <- sort(unique(df$genotype))
  887. by_geno <- lapply(geno_order, function(g) df[df$genotype == g, value_col, drop = TRUE])
  888. n_max <- max(vapply(by_geno, length, 1L))
  889. wide <- do.call(data.frame,
  890. lapply(by_geno, function(v) c(v, rep(NA_real_, n_max - length(v)))))
  891. names(wide) <- geno_order
  892. means <- vapply(by_geno, function(v) mean(v, na.rm = TRUE), numeric(1))
  893. sems <- vapply(by_geno, function(v) {
  894. n <- sum(!is.na(v)); if (n > 1) stats::sd(v, na.rm = TRUE) / sqrt(n) else NA_real_
  895. }, numeric(1))
  896. summ_df <- rbind(setNames(as.data.frame(t(means)), geno_order),
  897. setNames(as.data.frame(t(sems)), geno_order))
  898. label_col <- data.frame(fly_id = c(paste0("fly_", seq_len(n_max)), "Mean", "SEM"),
  899. stringsAsFactors = FALSE)
  900. cbind(label_col, rbind(wide, summ_df))
  901. }
  902. # Writes one phased metric (Light / Dark sections) to a sheet in wb.
  903. .write_phased_sheet <- function(wb, sheet_name, df, value_col = "value", geno_order = NULL) {
  904. if (is.null(geno_order)) geno_order <- sort(unique(df$genotype))
  905. openxlsx::addWorksheet(wb, sheet_name)
  906. phase_style <- openxlsx::createStyle(textDecoration = "bold", fgFill = "#CFE2F3")
  907. hdr_style <- openxlsx::createStyle(textDecoration = "bold", fgFill = "#D9EAD3",
  908. border = "Bottom")
  909. summ_style <- openxlsx::createStyle(fontColour = "#555555", numFmt = "0.00")
  910. cur_row <- 1L
  911. for (ph in c("Light", "Dark")) {
  912. sub <- df[df$phase == ph, ]
  913. if (nrow(sub) == 0) next
  914. openxlsx::writeData(wb, sheet_name,
  915. data.frame(Phase = ph), startRow = cur_row, startCol = 1,
  916. rowNames = FALSE, colNames = FALSE)
  917. openxlsx::addStyle(wb, sheet_name, phase_style, rows = cur_row, cols = 1)
  918. cur_row <- cur_row + 1L
  919. tbl <- .wide_with_summary(sub, value_col, geno_order)
  920. openxlsx::writeData(wb, sheet_name, tbl, startRow = cur_row, startCol = 1,
  921. rowNames = FALSE, headerStyle = hdr_style)
  922. n_rows <- nrow(tbl)
  923. openxlsx::addStyle(wb, sheet_name, summ_style,
  924. rows = cur_row + n_rows - 1L, cols = seq_len(ncol(tbl)),
  925. stack = TRUE, gridExpand = TRUE)
  926. openxlsx::addStyle(wb, sheet_name, summ_style,
  927. rows = cur_row + n_rows, cols = seq_len(ncol(tbl)),
  928. stack = TRUE, gridExpand = TRUE)
  929. cur_row <- cur_row + n_rows + 1L + 2L # header row + data + blank gap
  930. }
  931. }
  932. # Master export: collects all metric tables from one experiment block and writes S1_Data.xlsx.
  933. export_metrics_xlsx <- function(outdamdir,
  934. res_act,
  935. res_slp,
  936. res_sleep_bouts_n,
  937. res_sleep_boutdur,
  938. res_act_bouts_n,
  939. res_latency,
  940. per,
  941. filename = "S1_Data.xlsx") {
  942. wb <- openxlsx::createWorkbook()
  943. hdr_style <- openxlsx::createStyle(textDecoration = "bold", fgFill = "#D9EAD3",
  944. border = "Bottom")
  945. summ_style <- openxlsx::createStyle(fontColour = "#555555", numFmt = "0.00")
  946. geno_order <- sort(unique(res_act$data$genotype))
  947. # 1. Activity (beam crossings per 12 h, by phase)
  948. .write_phased_sheet(wb, "Activity_beam_crossings", res_act$data, "value", geno_order)
  949. # 2. Sleep duration (min/day, by phase)
  950. .write_phased_sheet(wb, "Sleep_min_per_day", res_slp$data, "value", geno_order)
  951. # 3. Sleep bout count (per 12 h, by phase)
  952. .write_phased_sheet(wb, "Sleep_bout_count", res_sleep_bouts_n$data, "value", geno_order)
  953. # 4. Sleep bout duration (mean min, by phase)
  954. .write_phased_sheet(wb, "Sleep_bout_duration_min", res_sleep_boutdur$data, "value", geno_order)
  955. # 5. Activity bout count (by phase)
  956. .write_phased_sheet(wb, "Activity_bout_count", res_act_bouts_n$data, "value", geno_order)
  957. # 6. Sleep latency (single dark-onset value per fly, no phase split)
  958. openxlsx::addWorksheet(wb, "Sleep_latency_min")
  959. lat_df <- res_latency$data |> dplyr::rename(dplyr::any_of(c(fly_id = "id")))
  960. lat_tbl <- .wide_with_summary(lat_df, "value",
  961. intersect(geno_order, lat_df$genotype))
  962. openxlsx::writeData(wb, "Sleep_latency_min", lat_tbl, startRow = 1, startCol = 1,
  963. rowNames = FALSE, headerStyle = hdr_style)
  964. n_lat <- nrow(lat_tbl)
  965. openxlsx::addStyle(wb, "Sleep_latency_min", summ_style,
  966. rows = n_lat, cols = seq_len(ncol(lat_tbl)), stack = TRUE, gridExpand = TRUE)
  967. openxlsx::addStyle(wb, "Sleep_latency_min", summ_style,
  968. rows = n_lat + 1, cols = seq_len(ncol(lat_tbl)), stack = TRUE, gridExpand = TRUE)
  969. # 7. Circadian period (one value per fly/tube)
  970. openxlsx::addWorksheet(wb, "Circadian_period_h")
  971. per_clean <- per[is.finite(per$period_h), ]
  972. per_tbl <- .wide_with_summary(per_clean, "period_h",
  973. intersect(geno_order, per_clean$genotype))
  974. openxlsx::writeData(wb, "Circadian_period_h", per_tbl, startRow = 1, startCol = 1,
  975. rowNames = FALSE, headerStyle = hdr_style)
  976. n_per <- nrow(per_tbl)
  977. openxlsx::addStyle(wb, "Circadian_period_h", summ_style,
  978. rows = n_per, cols = seq_len(ncol(per_tbl)), stack = TRUE, gridExpand = TRUE)
  979. openxlsx::addStyle(wb, "Circadian_period_h", summ_style,
  980. rows = n_per + 1, cols = seq_len(ncol(per_tbl)), stack = TRUE, gridExpand = TRUE)
  981. out_path <- file.path(outdamdir, filename)
  982. openxlsx::saveWorkbook(wb, out_path, overwrite = TRUE)
  983. message("Metrics xlsx saved: ", out_path)
  984. invisible(out_path)
  985. }
  986. .p_to_stars <- function(p) {
  987. vapply(p, function(x) {
  988. if (is.na(x)) return(NA_character_)
  989. if (x < 1e-4) "****"
  990. else if (x < 1e-3) "***"
  991. else if (x < 1e-2) "**"
  992. else if (x < 5e-2) "*"
  993. else "ns"
  994. }, character(1))
  995. }
  996. .add_pairwise_brackets_from_build <- function(
  997. p,
  998. stats_df,
  999. phase_labels = c(Light = "Day", Dark = "Night"),
  1000. genotype_order = NULL, # pass names(color_map) ideally
  1001. y_pad_frac = 0.06,
  1002. text_size = 5,
  1003. drop_ns = TRUE,
  1004. # --- NEW controls ---
  1005. bracket_anchor = c("top", "box", "fixed"),
  1006. bracket_fixed_y = NULL, # used if bracket_anchor == "fixed"
  1007. base_lift_mult = 0, # how far BELOW top (in y_step units) when anchor="top"
  1008. gap_mult = 1.2 # spacing between stacked brackets (in y_step units)
  1009. ) {
  1010. bracket_anchor <- match.arg(bracket_anchor)
  1011. if (!nrow(stats_df)) return(p)
  1012. b <- ggplot2::ggplot_build(p)
  1013. # Find first layer that looks like a boxplot layer
  1014. box_i <- which(vapply(b$data, function(x) all(c("x","upper","lower") %in% names(x)), logical(1)))
  1015. if (!length(box_i)) return(p)
  1016. boxdat <- b$data[[box_i[1]]]
  1017. # base_x: which discrete phase bucket (1,2,...)
  1018. boxdat$base_x <- round(boxdat$x)
  1019. # Map base_x -> phase factor level names (in the order used by the plot)
  1020. phase_levels <- names(phase_labels)
  1021. phase_map <- stats::setNames(phase_levels, seq_along(phase_levels))
  1022. boxdat$phase <- unname(phase_map[as.character(boxdat$base_x)])
  1023. # Determine genotype order to assign within each phase
  1024. if (is.null(genotype_order)) {
  1025. if (!is.null(p$data$genotype)) {
  1026. genotype_order <- levels(factor(p$data$genotype))
  1027. } else {
  1028. genotype_order <- sort(unique(as.character(p$data$genotype)))
  1029. }
  1030. }
  1031. # Build lookup: for each phase, sort boxes by x and assign genotypes in order
  1032. combos <- boxdat %>%
  1033. dplyr::filter(!is.na(phase)) %>%
  1034. dplyr::group_by(phase) %>%
  1035. dplyr::arrange(x, .by_group = TRUE) %>%
  1036. dplyr::mutate(genotype = genotype_order[seq_len(dplyr::n())]) %>%
  1037. dplyr::ungroup() %>%
  1038. dplyr::select(phase, genotype, x, upper) %>%
  1039. dplyr::rename(ymax_box = upper)
  1040. # Phase-wise y-top from the drawn boxes
  1041. y_top <- combos %>%
  1042. dplyr::group_by(phase) %>%
  1043. dplyr::summarise(y_top = max(ymax_box, na.rm = TRUE), .groups = "drop")
  1044. # Parse comparison "A vs B"
  1045. stats_df2 <- stats_df %>%
  1046. dplyr::mutate(
  1047. p_use = dplyr::coalesce(.data$p_adj, .data$p),
  1048. label = .p_to_stars(p_use),
  1049. g1 = sub(" vs .*", "", comparison),
  1050. g2 = sub(".* vs ", "", comparison)
  1051. ) %>%
  1052. dplyr::left_join(y_top, by = "phase")
  1053. # Drop ns labels if requested
  1054. if (drop_ns) {
  1055. stats_df2 <- stats_df2 %>% dplyr::filter(!is.na(label), label != "ns")
  1056. }
  1057. if (!nrow(stats_df2)) return(p)
  1058. # Stack brackets within each phase
  1059. yrng <- ggplot2::layer_scales(p)$y$range$range
  1060. y_span <- diff(yrng)
  1061. if (!is.finite(y_span) || y_span == 0) y_span <- 1
  1062. y_step <- y_span * y_pad_frac
  1063. stats_df2 <- stats_df2 %>%
  1064. dplyr::group_by(phase) %>%
  1065. dplyr::arrange(p_use, .by_group = TRUE) %>% # smaller p first (optional)
  1066. dplyr::mutate(rank_in_phase = dplyr::row_number()) %>%
  1067. dplyr::ungroup()
  1068. # Choose anchor
  1069. if (bracket_anchor == "top") {
  1070. # anchor a bit BELOW the top of the y-range
  1071. y_anchor <- yrng[2] - base_lift_mult * y_step
  1072. } else if (bracket_anchor == "fixed") {
  1073. if (is.null(bracket_fixed_y) || !is.finite(bracket_fixed_y)) {
  1074. stop("bracket_fixed_y must be a finite number when bracket_anchor = 'fixed'")
  1075. }
  1076. y_anchor <- bracket_fixed_y
  1077. } else {
  1078. # "box": anchor above the highest box in each phase (old behavior),
  1079. # but still give it a lift so it clears whiskers
  1080. # We'll compute per-phase anchor below when making 'ann'
  1081. y_anchor <- NA_real_
  1082. }
  1083. # Join x positions for each genotype within phase
  1084. ann <- stats_df2 %>%
  1085. dplyr::left_join(
  1086. combos %>% dplyr::select(phase, genotype, x),
  1087. by = c("phase", "g1" = "genotype")
  1088. ) %>%
  1089. dplyr::rename(x1 = x) %>%
  1090. dplyr::left_join(
  1091. combos %>% dplyr::select(phase, genotype, x),
  1092. by = c("phase", "g2" = "genotype")
  1093. ) %>%
  1094. dplyr::rename(x2 = x) %>%
  1095. dplyr::mutate(
  1096. x_min = pmin(x1, x2),
  1097. x_max = pmax(x1, x2),
  1098. y = dplyr::case_when(
  1099. bracket_anchor == "top" ~ y_anchor + (rank_in_phase - 1) * y_step * gap_mult,
  1100. bracket_anchor == "fixed" ~ y_anchor + (rank_in_phase - 1) * y_step * gap_mult,
  1101. TRUE ~ (y_top + 1.2 * y_step) + (rank_in_phase - 1) * y_step * gap_mult # "box"
  1102. )
  1103. ) %>%
  1104. dplyr::filter(is.finite(x_min), is.finite(x_max), phase %in% phase_levels)
  1105. if (!nrow(ann)) return(p)
  1106. # Draw brackets + stars
  1107. p +
  1108. ggplot2::geom_segment(
  1109. data = ann,
  1110. ggplot2::aes(x = x_min, xend = x_max, y = y, yend = y),
  1111. inherit.aes = FALSE, linewidth = 0.6
  1112. ) +
  1113. ggplot2::geom_segment(
  1114. data = ann,
  1115. ggplot2::aes(x = x_min, xend = x_min, y = y, yend = y - 0.25 * y_step),
  1116. inherit.aes = FALSE, linewidth = 0.6
  1117. ) +
  1118. ggplot2::geom_segment(
  1119. data = ann,
  1120. ggplot2::aes(x = x_max, xend = x_max, y = y, yend = y - 0.25 * y_step),
  1121. inherit.aes = FALSE, linewidth = 0.6
  1122. ) +
  1123. ggplot2::geom_text(
  1124. data = ann,
  1125. ggplot2::aes(x = (x_min + x_max) / 2, y = y + 0.15 * y_step, label = label),
  1126. inherit.aes = FALSE, size = text_size
  1127. )
  1128. }
  1129. pairwise_vs_mutant_by_phase <- function(df_long, value_col = "value",
  1130. mutant = "CG_GAL4_pex5",
  1131. controls = c("pex5","CG_GAL4"),
  1132. p_adjust = "BH") {
  1133. stopifnot(all(c("genotype","phase", value_col) %in% names(df_long)))
  1134. out <- df_long %>%
  1135. dplyr::filter(genotype %in% c(mutant, controls)) %>%
  1136. dplyr::group_by(phase) %>%
  1137. dplyr::group_modify(\(d, key) {
  1138. res <- lapply(controls, function(ctrl) {
  1139. s2 <- d %>% dplyr::filter(genotype %in% c(ctrl, mutant))
  1140. p <- tryCatch(
  1141. stats::wilcox.test(s2[[value_col]] ~ s2$genotype, exact = FALSE)$p.value,
  1142. error = function(e) NA_real_
  1143. )
  1144. tibble::tibble(
  1145. test = "Wilcoxon (control vs mutant)",
  1146. comparison = paste(ctrl, "vs", mutant),
  1147. p = p
  1148. )
  1149. })
  1150. dplyr::bind_rows(res)
  1151. }) %>%
  1152. dplyr::ungroup()
  1153. out$p_adj <- p.adjust(out$p, method = p_adjust)
  1154. out
  1155. }
  1156. plot_daynight_activity_box <- function(
  1157. df, outdir = "DAM_Graphs",
  1158. color_map = NULL,
  1159. genotype_labels = NULL, # named vector of plotmath strings
  1160. legend_text_size = 14,
  1161. legend_title_size = 0,
  1162. mutant = "CG_GAL4_pex5",
  1163. controls = c("pex5","CG_GAL4"),
  1164. show_legend = TRUE,
  1165. show_dots = TRUE,
  1166. dot_size = 0.8,
  1167. show_kw_stars = FALSE, # <- default OFF (you usually don't want KW across all 3)
  1168. show_brackets = TRUE, # control-vs-mutant brackets
  1169. drop_ns = TRUE, # don't draw ns labels/brackets
  1170. # text controls
  1171. title = NULL,
  1172. x_label = NULL,
  1173. y_label = NULL,
  1174. phase_labels = c(Light = "Day", Dark = "Night"),
  1175. width_in = 4, height_in = 4, dpi = 300
  1176. ) {
  1177. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1178. # ---- data (per-fly across runs) ----
  1179. dn <- build_daynight_activity_avg(df)
  1180. # stable ordering
  1181. dn$phase <- factor(dn$phase, levels = names(phase_labels))
  1182. if (!is.null(color_map)) dn$genotype <- factor(dn$genotype, levels = names(color_map))
  1183. # ---- stats for brackets (only ctrl vs mutant) ----
  1184. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  1185. dn, value_col = "value",
  1186. mutant = mutant, controls = controls,
  1187. p_adjust = "BH"
  1188. ) %>%
  1189. dplyr::mutate(
  1190. p_use = dplyr::coalesce(p_adj, p),
  1191. label = .p_to_stars(p_use)
  1192. )
  1193. if (drop_ns) {
  1194. ctrl_vs_mut <- ctrl_vs_mut %>% dplyr::filter(!is.na(label), label != "ns")
  1195. }
  1196. # ---- optional KW per phase (rarely needed) ----
  1197. kw_only <- NULL
  1198. if (show_kw_stars) {
  1199. kw_only <- nonparam_stats_by_phase(dn, value_col = "value", p_adjust = "BH") %>%
  1200. dplyr::filter(test == "Kruskal–Wallis") %>%
  1201. dplyr::mutate(label = .p_to_stars(p)) %>%
  1202. { if (drop_ns) dplyr::filter(., label != "ns") else . }
  1203. }
  1204. default_title <- "Activity"
  1205. default_y <- "Beam crossings per day"
  1206. default_x <- NULL
  1207. p <- ggplot2::ggplot(dn, ggplot2::aes(x = phase, y = value, fill = genotype)) +
  1208. ggplot2::geom_boxplot(
  1209. position = ggplot2::position_dodge(width = 0.6),
  1210. width = 0.6, alpha = 0.9, outlier.shape = NA
  1211. ) +
  1212. ggplot2::labs(
  1213. x = if (is.null(x_label)) default_x else x_label,
  1214. y = if (is.null(y_label)) default_y else y_label,
  1215. title = if (is.null(title)) default_title else title
  1216. ) +
  1217. ggplot2::theme_classic(base_size = 12) +
  1218. ggplot2::theme(
  1219. legend.position = if (show_legend) "right" else "none",
  1220. legend.text = ggplot2::element_text(size = legend_text_size),
  1221. legend.title = ggplot2::element_text(size = legend_title_size),
  1222. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  1223. axis.title.y = ggplot2::element_text(size = 20),
  1224. axis.text = ggplot2::element_text(size = 14),
  1225. # helps avoid clipping of annotations
  1226. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  1227. ) +
  1228. ggplot2::scale_x_discrete(labels = phase_labels) +
  1229. ggplot2::scale_y_continuous(limits = c(0, NA),expand = ggplot2::expansion(mult = c(0, 0.18))) +
  1230. # ggplot2::scale_y_continuous(expand = ggplot2::expansion(mult = c(0.02, 0.18))) +
  1231. ggplot2::coord_cartesian(clip = "off")
  1232. if (show_dots) {
  1233. p <- p + ggplot2::geom_point(
  1234. ggplot2::aes(color = NULL),
  1235. position = ggplot2::position_jitterdodge(jitter.width = 0.04, dodge.width = 0.6),
  1236. size = dot_size, alpha = 0.6, shape = 16, color = "black"
  1237. )
  1238. }
  1239. # ---- fill scale + parsed legend labels ----
  1240. if (!is.null(color_map)) {
  1241. if (!is.null(genotype_labels)) {
  1242. genotype_labels <- genotype_labels[names(color_map)]
  1243. p <- p +
  1244. ggplot2::scale_fill_manual(
  1245. values = color_map,
  1246. breaks = names(color_map),
  1247. labels = parse(text = genotype_labels)
  1248. ) +
  1249. ggplot2::guides(
  1250. fill = ggplot2::guide_legend(label = ggplot2::label_parsed)
  1251. )
  1252. } else {
  1253. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  1254. }
  1255. }
  1256. # ---- (A) KW stars above each phase (optional) ----
  1257. if (show_kw_stars && !is.null(kw_only) && nrow(kw_only)) {
  1258. y_max <- dn %>%
  1259. dplyr::group_by(phase) %>%
  1260. dplyr::summarise(ypos = max(value, na.rm = TRUE) * 1.08, .groups = "drop")
  1261. sig_df <- kw_only %>%
  1262. dplyr::select(phase, label) %>%
  1263. dplyr::left_join(y_max, by = "phase")
  1264. p <- p + ggplot2::geom_text(
  1265. data = sig_df,
  1266. ggplot2::aes(x = phase, y = ypos, label = label),
  1267. inherit.aes = FALSE, vjust = 0, size = 8
  1268. )
  1269. }
  1270. # ---- (B) ctrl vs mutant brackets (significant only) ----
  1271. if (show_brackets && nrow(ctrl_vs_mut)) {
  1272. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(dn$genotype)
  1273. p <- .add_pairwise_brackets_from_build(
  1274. p,
  1275. stats_df = ctrl_vs_mut,
  1276. genotype_order = genotype_order,
  1277. y_pad_frac = 0.06,
  1278. text_size = 5
  1279. )
  1280. }
  1281. out <- file.path(outdir, "DAM_activity_day_night_box.png")
  1282. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1283. invisible(list(plot = p, stats_ctrl_vs_mut = ctrl_vs_mut, data = dn, file = out))
  1284. }
  1285. plot_daynight_sleep_box <- function(
  1286. df, sleep_block_min = 5L,
  1287. outdir = "DAM_Graphs",
  1288. color_map = NULL,
  1289. genotype_labels = NULL, # named vector of plotmath strings
  1290. legend_text_size = 14,
  1291. legend_title_size = 0,
  1292. mutant = "CG_GAL4_pex5",
  1293. controls = c("pex5","CG_GAL4"),
  1294. show_legend = TRUE,
  1295. show_dots = TRUE,
  1296. dot_size = 0.8,
  1297. show_kw_stars = FALSE, # default OFF
  1298. show_brackets = TRUE, # control-vs-mutant brackets
  1299. drop_ns = TRUE, # don't draw ns labels/brackets
  1300. # text controls
  1301. title = NULL,
  1302. x_label = NULL,
  1303. y_label = NULL,
  1304. phase_labels = c(Light = "Day", Dark = "Night"),
  1305. width_in = 4, height_in = 4, dpi = 300
  1306. ) {
  1307. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1308. # ---- data (per-fly across runs) ----
  1309. dn <- build_daynight_sleep_avg(df, sleep_block_min = sleep_block_min)
  1310. # stable ordering
  1311. dn$phase <- factor(dn$phase, levels = names(phase_labels))
  1312. if (!is.null(color_map)) dn$genotype <- factor(dn$genotype, levels = names(color_map))
  1313. # ---- stats for brackets (only ctrl vs mutant) ----
  1314. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  1315. dn, value_col = "value",
  1316. mutant = mutant, controls = controls,
  1317. p_adjust = "BH"
  1318. ) %>%
  1319. dplyr::mutate(
  1320. p_use = dplyr::coalesce(p_adj, p),
  1321. label = .p_to_stars(p_use)
  1322. )
  1323. if (drop_ns) {
  1324. ctrl_vs_mut <- ctrl_vs_mut %>% dplyr::filter(!is.na(label), label != "ns")
  1325. }
  1326. # ---- optional KW per phase (rarely needed) ----
  1327. kw_only <- NULL
  1328. if (show_kw_stars) {
  1329. kw_only <- nonparam_stats_by_phase(dn, value_col = "value", p_adjust = "BH") %>%
  1330. dplyr::filter(test == "Kruskal–Wallis") %>%
  1331. dplyr::mutate(label = .p_to_stars(p)) %>%
  1332. { if (drop_ns) dplyr::filter(., label != "ns") else . }
  1333. }
  1334. default_title <- "Sleep"
  1335. default_y <- sprintf("Sleep (min/day; ≥%d-min bouts)", as.integer(sleep_block_min))
  1336. default_x <- NULL
  1337. p <- ggplot2::ggplot(dn, ggplot2::aes(x = phase, y = value, fill = genotype)) +
  1338. ggplot2::geom_boxplot(
  1339. position = ggplot2::position_dodge(width = 0.6),
  1340. width = 0.6, alpha = 0.9, outlier.shape = NA
  1341. ) +
  1342. ggplot2::labs(
  1343. x = if (is.null(x_label)) default_x else x_label,
  1344. y = if (is.null(y_label)) default_y else y_label,
  1345. title = if (is.null(title)) default_title else title
  1346. ) +
  1347. ggplot2::theme_classic(base_size = 12) +
  1348. ggplot2::theme(
  1349. legend.position = if (show_legend) "right" else "none",
  1350. legend.text = ggplot2::element_text(size = legend_text_size),
  1351. legend.title = ggplot2::element_text(size = legend_title_size),
  1352. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  1353. axis.title.y = ggplot2::element_text(size = 20),
  1354. axis.text = ggplot2::element_text(size = 14),
  1355. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  1356. ) +
  1357. ggplot2::scale_x_discrete(labels = phase_labels) +
  1358. ggplot2::scale_y_continuous(limits = c(0, NA),expand = ggplot2::expansion(mult = c(0, 0.18))) +
  1359. # ggplot2::scale_y_continuous(expand = ggplot2::expansion(mult = c(0.02, 0.18))) +
  1360. ggplot2::coord_cartesian(clip = "off")
  1361. if (show_dots) {
  1362. p <- p + ggplot2::geom_point(
  1363. ggplot2::aes(color = NULL),
  1364. position = ggplot2::position_jitterdodge(jitter.width = 0.04, dodge.width = 0.6),
  1365. size = dot_size, alpha = 0.6, shape = 16, color = "black"
  1366. )
  1367. }
  1368. # ---- fill scale + parsed legend labels ----
  1369. if (!is.null(color_map)) {
  1370. if (!is.null(genotype_labels)) {
  1371. genotype_labels <- genotype_labels[names(color_map)]
  1372. p <- p +
  1373. ggplot2::scale_fill_manual(
  1374. values = color_map,
  1375. breaks = names(color_map),
  1376. labels = parse(text = genotype_labels)
  1377. ) +
  1378. ggplot2::guides(
  1379. fill = ggplot2::guide_legend(label = ggplot2::label_parsed)
  1380. )
  1381. } else {
  1382. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  1383. }
  1384. }
  1385. # ---- (A) KW stars above each phase (optional) ----
  1386. if (show_kw_stars && !is.null(kw_only) && nrow(kw_only)) {
  1387. y_max <- dn %>%
  1388. dplyr::group_by(phase) %>%
  1389. dplyr::summarise(ypos = max(value, na.rm = TRUE) * 1.08, .groups = "drop")
  1390. sig_df <- kw_only %>%
  1391. dplyr::select(phase, label) %>%
  1392. dplyr::left_join(y_max, by = "phase")
  1393. p <- p + ggplot2::geom_text(
  1394. data = sig_df,
  1395. ggplot2::aes(x = phase, y = ypos, label = label),
  1396. inherit.aes = FALSE, vjust = 0, size = 8
  1397. )
  1398. }
  1399. # ---- (B) ctrl vs mutant brackets (significant only) ----
  1400. if (show_brackets && nrow(ctrl_vs_mut)) {
  1401. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(dn$genotype)
  1402. p <- .add_pairwise_brackets_from_build(
  1403. p,
  1404. stats_df = ctrl_vs_mut,
  1405. genotype_order = genotype_order,
  1406. y_pad_frac = 0.06,
  1407. text_size = 5,
  1408. drop_ns = drop_ns
  1409. )
  1410. }
  1411. out <- file.path(outdir, "DAM_sleep_day_night_box.png")
  1412. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1413. invisible(list(
  1414. plot = p,
  1415. stats_ctrl_vs_mut = ctrl_vs_mut,
  1416. data = dn,
  1417. file = out
  1418. ))
  1419. }
  1420. # Per-tube day/night totals for activity
  1421. build_daynight_activity <- function(df) {
  1422. df %>%
  1423. dplyr::mutate(phase = dplyr::if_else(light == 1L, "Light", "Dark", missing = NA_character_)) %>%
  1424. dplyr::filter(!is.na(phase)) %>%
  1425. dplyr::group_by(genotype, tube, phase) %>%
  1426. dplyr::summarise(total = sum(count_na0, na.rm = TRUE), .groups = "drop")
  1427. }
  1428. # Per-tube day/night totals for sleep (≥ sleep_block_min consecutive zeros)
  1429. flag_sleep_minutes <- function(count, sleep_block_min = 5L) {
  1430. is_zero <- (!is.na(count)) & (count == 0L)
  1431. r <- rle(is_zero); idx <- rep.int(seq_along(r$lengths), r$lengths)
  1432. as.integer(is_zero & (r$lengths[idx] >= as.integer(sleep_block_min)))
  1433. }
  1434. build_daynight_sleep <- function(df, sleep_block_min = 5L) {
  1435. df %>%
  1436. dplyr::arrange(ts) %>%
  1437. dplyr::group_by(genotype, tube) %>%
  1438. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min)) %>%
  1439. dplyr::ungroup() %>%
  1440. dplyr::mutate(phase = dplyr::if_else(light == 1L, "Light", "Dark", missing = NA_character_)) %>%
  1441. dplyr::filter(!is.na(phase)) %>%
  1442. dplyr::group_by(genotype, tube, phase) %>%
  1443. dplyr::summarise(total = sum(sleep_flag, na.rm = TRUE), .groups = "drop")
  1444. }
  1445. # Simple nonparametric stats:
  1446. # - If 2 genotypes: Wilcoxon (Mann–Whitney) per phase
  1447. # - If >2 genotypes: Kruskal–Wallis per phase; then pairwise Wilcoxon with BH correction
  1448. nonparam_stats_by_phase <- function(df_long, value_col = "total", p_adjust = "BH") {
  1449. stopifnot(all(c("genotype","phase", value_col) %in% names(df_long)))
  1450. phases <- sort(unique(df_long$phase))
  1451. out_list <- list()
  1452. for (ph in phases) {
  1453. sub <- df_long[df_long$phase == ph, , drop = FALSE]
  1454. k <- length(unique(sub$genotype))
  1455. if (k == 2) {
  1456. g <- unique(sub$genotype)
  1457. p <- tryCatch(
  1458. stats::wilcox.test(sub[[value_col]] ~ sub$genotype, exact = FALSE)$p.value,
  1459. error = function(e) NA_real_
  1460. )
  1461. out_list[[ph]] <- data.frame(
  1462. phase = ph, test = "Wilcoxon rank-sum",
  1463. comparison = paste(g, collapse = " vs "), p = p,
  1464. p_adj = NA_real_, # <- keep columns consistent
  1465. stringsAsFactors = FALSE
  1466. )
  1467. } else if (k > 2) {
  1468. kw <- tryCatch(stats::kruskal.test(sub[[value_col]] ~ sub$genotype), error = function(e) NULL)
  1469. row_kw <- data.frame(
  1470. phase = ph, test = "Kruskal–Wallis",
  1471. comparison = "all groups",
  1472. p = if (is.null(kw)) NA_real_ else kw$p.value,
  1473. p_adj = NA_real_, # <- add p_adj here
  1474. stringsAsFactors = FALSE
  1475. )
  1476. pairs <- utils::combn(sort(unique(sub$genotype)), 2, simplify = FALSE)
  1477. pw <- lapply(pairs, function(pp) {
  1478. s2 <- sub[sub$genotype %in% pp, , drop = FALSE]
  1479. p <- tryCatch(
  1480. stats::wilcox.test(s2[[value_col]] ~ s2$genotype, exact = FALSE)$p.value,
  1481. error = function(e) NA_real_
  1482. )
  1483. data.frame(
  1484. phase = ph, test = "Wilcoxon pairwise",
  1485. comparison = paste(pp, collapse = " vs "), p = p,
  1486. stringsAsFactors = FALSE
  1487. )
  1488. })
  1489. pw <- do.call(rbind, pw)
  1490. pw$p_adj <- p.adjust(pw$p, method = p_adjust)
  1491. out_list[[ph]] <- rbind(row_kw, pw) # now columns match
  1492. }
  1493. }
  1494. do.call(rbind, out_list)
  1495. }
  1496. .dodge_w <- 0.6
  1497. .jitter_w <- 0.04
  1498. .tag_zt_day <- function(df, tz = "America/Los_Angeles") {
  1499. lc <- try(infer_light_cycle(df), silent = TRUE)
  1500. per_day <- if (inherits(lc, "try-error")) NULL else lc$per_day
  1501. med_on <- if (inherits(lc, "try-error")) NA else lc$summary$lights_on_median
  1502. # Fallback if infer_light_cycle couldn't determine ON/OFF medians
  1503. if (is.null(per_day) || !is.finite(as.numeric(med_on))) {
  1504. # median time-of-day among rows with light==1
  1505. df_on <- dplyr::filter(df, light == 1L, !is.na(ts))
  1506. if (nrow(df_on)) {
  1507. tod <- as.POSIXct(format(df_on$ts, "%H:%M:%S"), format = "%H:%M:%S", tz = tz)
  1508. med_tod <- stats::median(tod, na.rm = TRUE)
  1509. # function: given a date, build that day's ON at the median time-of-day
  1510. build_on <- function(d) {
  1511. as.POSIXct(sprintf("%s %s", as.character(d), format(med_tod, "%H:%M:%S")),
  1512. tz = tz)
  1513. }
  1514. } else {
  1515. # last-resort: 09:00 local
  1516. build_on <- function(d) {
  1517. as.POSIXct(sprintf("%s 09:00:00", as.character(d)), tz = tz)
  1518. }
  1519. }
  1520. df %>%
  1521. dplyr::mutate(date = as.Date(ts)) %>%
  1522. dplyr::mutate(ON = build_on(date)) %>%
  1523. dplyr::mutate(
  1524. zt_anchor = dplyr::if_else(ts < ON, ON - lubridate::days(1), ON),
  1525. zt_day_id = as.Date(zt_anchor)
  1526. ) %>%
  1527. dplyr::select(-date, -ON)
  1528. } else {
  1529. # Normal path with per_day ON table and median fallback
  1530. df %>%
  1531. dplyr::mutate(date = as.Date(ts)) %>%
  1532. dplyr::left_join(per_day %>% dplyr::select(day, ON), by = c("date" = "day")) %>%
  1533. dplyr::mutate(ON = dplyr::coalesce(ON, med_on)) %>%
  1534. dplyr::mutate(
  1535. zt_anchor = dplyr::if_else(ts < ON, ON - lubridate::days(1), ON),
  1536. zt_day_id = as.Date(zt_anchor)
  1537. ) %>%
  1538. dplyr::select(-date, -ON)
  1539. }
  1540. }
  1541. plot_total_sleep_by_day <- function(
  1542. df, sleep_block_min = 5L, outdir = "DAM_Graphs",
  1543. color_map = NULL, show_legend = TRUE,
  1544. # appearance
  1545. mean_linewidth = 1.0, sem_alpha = 0.25, show_points = FALSE, point_size = 2,
  1546. # labels
  1547. title = NULL, x_label = NULL, y_label = NULL,
  1548. title_size = 32, axis_title_size = 18, axis_text_size = 18,
  1549. # day choices
  1550. day_mode = c("zt","calendar"),
  1551. x_mode = c("relative","date"),
  1552. # filtering
  1553. drop_empty_days = TRUE,
  1554. min_real_minutes_per_day = NULL,
  1555. # file
  1556. filename = "DAM_sleep_total_by_day.png",
  1557. width_in = 7, height_in = 4, dpi = 300
  1558. ) {
  1559. day_mode <- match.arg(day_mode)
  1560. x_mode <- match.arg(x_mode)
  1561. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1562. if (!("is_padded" %in% names(df))) df$is_padded <- FALSE
  1563. df2 <- df %>%
  1564. dplyr::arrange(ts) %>%
  1565. dplyr::group_by(genotype, tube) %>%
  1566. dplyr::mutate(sleep_flag = flag_sleep_minutes(count, sleep_block_min)) %>%
  1567. dplyr::ungroup()
  1568. # choose the day key
  1569. if (day_mode == "zt") df2 <- .tag_zt_day(df2) %>% dplyr::mutate(day_key = zt_day_id)
  1570. else df2 <- df2 %>% dplyr::mutate(day_key = as.Date(ts))
  1571. # per-tube per-day totals + real minutes (vectorized)
  1572. tube_day <- df2 %>%
  1573. dplyr::group_by(genotype, tube, day_key) %>%
  1574. dplyr::summarise(
  1575. total_sleep_min = sum(sleep_flag, na.rm = TRUE),
  1576. real_minutes = sum(!dplyr::coalesce(is_padded, FALSE)),
  1577. .groups = "drop"
  1578. )
  1579. if (!is.null(min_real_minutes_per_day)) {
  1580. tube_day <- dplyr::filter(tube_day, real_minutes >= as.integer(min_real_minutes_per_day))
  1581. }
  1582. if (drop_empty_days) {
  1583. keep_days <- tube_day %>%
  1584. dplyr::group_by(day_key) %>%
  1585. dplyr::summarise(any_real = sum(real_minutes, na.rm = TRUE) > 0, .groups = "drop") %>%
  1586. dplyr::filter(any_real) %>% dplyr::pull(day_key)
  1587. tube_day <- dplyr::filter(tube_day, day_key %in% keep_days)
  1588. }
  1589. geno_day <- tube_day %>%
  1590. dplyr::group_by(genotype, day_key) %>%
  1591. dplyr::summarise(n = dplyr::n(),
  1592. mean = mean(total_sleep_min),
  1593. sem = stats::sd(total_sleep_min)/sqrt(n),
  1594. .groups = "drop")
  1595. if (is.null(y_label)) y_label <- sprintf("Total sleep / day (min, ≥%d-min bouts)", as.integer(sleep_block_min))
  1596. if (is.null(title)) title <- if (day_mode == "zt") "DAM: Total Sleep per ZT Day" else "DAM: Total Sleep per Calendar Day"
  1597. if (is.null(x_label)) x_label <- if (x_mode == "relative") "Day" else "Day"
  1598. # relative-day mapping 1..N
  1599. day_levels <- sort(unique(geno_day$day_key))
  1600. day_map <- setNames(seq_along(day_levels), day_levels)
  1601. geno_day <- geno_day %>% dplyr::mutate(rel_day = day_map[as.character(day_key)])
  1602. # plot
  1603. if (x_mode == "relative") {
  1604. p <- ggplot2::ggplot(geno_day, ggplot2::aes(x = rel_day, y = mean, color = genotype, fill = genotype, group = genotype)) +
  1605. ggplot2::scale_x_continuous(breaks = sort(unique(geno_day$rel_day)))
  1606. } else {
  1607. p <- ggplot2::ggplot(geno_day, ggplot2::aes(x = day_key, y = mean, color = genotype, fill = genotype, group = genotype))
  1608. }
  1609. p <- p +
  1610. ggplot2::geom_ribbon(ggplot2::aes(ymin = mean - sem, ymax = mean + sem), alpha = sem_alpha, linewidth = 0) +
  1611. ggplot2::geom_line(linewidth = mean_linewidth) +ylim(0,NA)+
  1612. ggplot2::labs(x = x_label, y = y_label, title = title) +
  1613. ggplot2::theme_classic(base_size = 12) +
  1614. ggplot2::theme(
  1615. legend.position = if (show_legend) "right" else "none",
  1616. plot.title = ggplot2::element_text(hjust = 0.5, size = title_size),
  1617. axis.title.x = ggplot2::element_text(size = axis_title_size),
  1618. axis.title.y = ggplot2::element_text(size = axis_title_size),
  1619. axis.text = ggplot2::element_text(size = axis_text_size)
  1620. )
  1621. if (show_points) p <- p + ggplot2::geom_point(size = point_size)
  1622. if (!is.null(color_map)) p <- p + ggplot2::scale_color_manual(values = color_map) +
  1623. ggplot2::scale_fill_manual(values = color_map)
  1624. out <- file.path(outdir, filename)
  1625. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1626. invisible(list(plot = p, summary = geno_day, data = tube_day, file = out))
  1627. }
  1628. plot_total_activity_by_day <- function(
  1629. df, outdir = "DAM_Graphs",
  1630. color_map = NULL, show_legend = TRUE,
  1631. # appearance
  1632. mean_linewidth = 1.0, sem_alpha = 0.25, show_points = FALSE, point_size = 2,
  1633. # labels
  1634. title = NULL, x_label = NULL, y_label = "Total activity / day (counts)",
  1635. title_size = 32, axis_title_size = 18, axis_text_size = 18,
  1636. # day choices
  1637. day_mode = c("zt","calendar"),
  1638. x_mode = c("relative","date"),
  1639. # filtering
  1640. drop_empty_days = TRUE,
  1641. min_real_minutes_per_day = NULL,
  1642. # file
  1643. filename = "DAM_activity_total_by_day.png",
  1644. width_in = 7, height_in = 4, dpi = 300
  1645. ) {
  1646. day_mode <- match.arg(day_mode)
  1647. x_mode <- match.arg(x_mode)
  1648. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1649. if (!("is_padded" %in% names(df))) df$is_padded <- FALSE
  1650. df2 <- if (day_mode == "zt") .tag_zt_day(df) %>% dplyr::mutate(day_key = zt_day_id)
  1651. else df %>% dplyr::mutate(day_key = as.Date(ts))
  1652. tube_day <- df2 %>%
  1653. dplyr::group_by(genotype, tube, day_key) %>%
  1654. dplyr::summarise(
  1655. total_activity = sum(count_na0, na.rm = TRUE),
  1656. real_minutes = sum(!dplyr::coalesce(is_padded, FALSE)),
  1657. .groups = "drop"
  1658. )
  1659. if (!is.null(min_real_minutes_per_day)) {
  1660. tube_day <- dplyr::filter(tube_day, real_minutes >= as.integer(min_real_minutes_per_day))
  1661. }
  1662. if (drop_empty_days) {
  1663. keep_days <- tube_day %>%
  1664. dplyr::group_by(day_key) %>%
  1665. dplyr::summarise(any_real = sum(real_minutes, na.rm = TRUE) > 0, .groups = "drop") %>%
  1666. dplyr::filter(any_real) %>% dplyr::pull(day_key)
  1667. tube_day <- dplyr::filter(tube_day, day_key %in% keep_days)
  1668. }
  1669. geno_day <- tube_day %>%
  1670. dplyr::group_by(genotype, day_key) %>%
  1671. dplyr::summarise(n = dplyr::n(),
  1672. mean = mean(total_activity),
  1673. sem = stats::sd(total_activity)/sqrt(n),
  1674. .groups = "drop")
  1675. if (is.null(title)) title <- if (day_mode == "zt") "DAM: Total Activity per ZT Day" else "DAM: Total Activity per Calendar Day"
  1676. if (is.null(x_label)) x_label <- if (x_mode == "relative") "Day" else "Day"
  1677. # relative-day mapping 1..N
  1678. day_levels <- sort(unique(geno_day$day_key))
  1679. day_map <- setNames(seq_along(day_levels), day_levels)
  1680. geno_day <- geno_day %>% dplyr::mutate(rel_day = day_map[as.character(day_key)])
  1681. # plot
  1682. if (x_mode == "relative") {
  1683. p <- ggplot2::ggplot(geno_day, ggplot2::aes(x = rel_day, y = mean, color = genotype, fill = genotype, group = genotype)) +
  1684. ggplot2::scale_x_continuous(breaks = sort(unique(geno_day$rel_day)))
  1685. } else {
  1686. p <- ggplot2::ggplot(geno_day, ggplot2::aes(x = day_key, y = mean, color = genotype, fill = genotype, group = genotype))
  1687. }
  1688. p <- p +
  1689. ggplot2::geom_ribbon(ggplot2::aes(ymin = mean - sem, ymax = mean + sem), alpha = sem_alpha, linewidth = 0) +
  1690. ggplot2::geom_line(linewidth = mean_linewidth) +
  1691. ggplot2::labs(x = x_label, y = y_label, title = title) +ylim(0,NA)+
  1692. ggplot2::theme_classic(base_size = 12) +
  1693. ggplot2::theme(
  1694. legend.position = if (show_legend) "right" else "none",
  1695. plot.title = ggplot2::element_text(hjust = 0.5, size = title_size),
  1696. axis.title.x = ggplot2::element_text(size = axis_title_size),
  1697. axis.title.y = ggplot2::element_text(size = axis_title_size),
  1698. axis.text = ggplot2::element_text(size = axis_text_size)
  1699. )
  1700. if (show_points) p <- p + ggplot2::geom_point(size = point_size)
  1701. if (!is.null(color_map)) p <- p + ggplot2::scale_color_manual(values = color_map) +
  1702. ggplot2::scale_fill_manual(values = color_map)
  1703. out <- file.path(outdir, filename)
  1704. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1705. invisible(list(plot = p, summary = geno_day, data = tube_day, file = out))
  1706. }
  1707. #### bout functions ####
  1708. .extract_bouts <- function(flag_vec) {
  1709. f <- as.integer(flag_vec)
  1710. f[is.na(f)] <- 0L
  1711. r <- rle(f)
  1712. ends <- cumsum(r$lengths)
  1713. starts <- ends - r$lengths + 1L
  1714. idx <- which(r$values == 1L)
  1715. if (!length(idx)) {
  1716. return(tibble::tibble(start_idx = integer(), end_idx = integer(), dur_min = integer()))
  1717. }
  1718. tibble::tibble(
  1719. start_idx = starts[idx],
  1720. end_idx = ends[idx],
  1721. dur_min = r$lengths[idx]
  1722. )
  1723. }
  1724. build_daynight_bout_metric <- function(df,
  1725. metric = c("sleep","activity"),
  1726. what = c("count","duration"),
  1727. zt0_hour = 6L,
  1728. sleep_block_min = 5L,
  1729. activity_threshold = 0L,
  1730. summarize_days = c("mean","median")) {
  1731. metric <- match.arg(metric)
  1732. what <- match.arg(what)
  1733. summarize_days <- match.arg(summarize_days)
  1734. lc <- infer_light_cycle(df, zt0_hour = zt0_hour)
  1735. per_day <- lc$per_day
  1736. mode <- lc$mode
  1737. tz_ts <- lubridate::tz(df$ts)
  1738. if (is.null(tz_ts) || tz_ts == "") tz_ts <- "UTC"
  1739. df2 <- df |>
  1740. dplyr::mutate(
  1741. day = as.Date(lubridate::with_tz(ts, tz_ts)),
  1742. fly_id = dplyr::if_else(
  1743. !is.na(run_id) & run_id != "",
  1744. paste(run_id, tube, sep = "__"),
  1745. as.character(tube)
  1746. )
  1747. ) |>
  1748. dplyr::left_join(per_day |> dplyr::select(day, ON), by = "day") |>
  1749. dplyr::mutate(
  1750. zt_min = as.integer((as.numeric(difftime(ts, ON, units = "mins")) %% (24*60))),
  1751. phase = dplyr::if_else(zt_min < 12*60, "Light", "Dark")
  1752. ) |>
  1753. dplyr::arrange(genotype, fly_id, ts)
  1754. # per-minute "in-bout" flag
  1755. if (metric == "sleep") {
  1756. df2 <- df2 |>
  1757. dplyr::group_by(genotype, fly_id) |>
  1758. dplyr::mutate(flag = flag_sleep_minutes(count, sleep_block_min = sleep_block_min)) |>
  1759. dplyr::ungroup()
  1760. } else {
  1761. df2 <- df2 |>
  1762. dplyr::mutate(
  1763. count0 = dplyr::coalesce(count_na0, dplyr::coalesce(count, 0)),
  1764. flag = count0 > activity_threshold
  1765. )
  1766. }
  1767. # per fly x day x phase
  1768. day_phase <- df2 |>
  1769. dplyr::group_by(genotype, fly_id, day, phase) |>
  1770. dplyr::summarise(
  1771. value = {
  1772. bouts <- .extract_bouts(flag)
  1773. if (what == "count") {
  1774. nrow(bouts)
  1775. } else {
  1776. if (nrow(bouts) == 0) NA_real_ else mean(bouts$dur_min)
  1777. }
  1778. },
  1779. .groups = "drop"
  1780. )
  1781. # collapse across days (per fly x phase)
  1782. dn <- day_phase |>
  1783. dplyr::group_by(genotype, fly_id, phase) |>
  1784. dplyr::summarise(
  1785. value = if (summarize_days == "mean") mean(value, na.rm = TRUE) else stats::median(value, na.rm = TRUE),
  1786. .groups = "drop"
  1787. )
  1788. attr(dn, "mode") <- mode
  1789. attr(dn, "metric") <- metric
  1790. attr(dn, "what") <- what
  1791. dn
  1792. }
  1793. plot_sleep_latency_box <- function(df,
  1794. zt0_hour = 6L,
  1795. sleep_block_min = 5L,
  1796. summarize_days = c("mean","median"),
  1797. outdir = "DAM_Graphs",
  1798. filename = NULL,
  1799. color_map = NULL,
  1800. genotype_labels = NULL,
  1801. title = "Sleep latency",
  1802. y_label = "Minutes after ZT12 / CT12",
  1803. mutant = "CG_GAL4_pex5",
  1804. controls = c("pex5","CG_GAL4"),
  1805. show_legend = TRUE,
  1806. show_dots = TRUE,
  1807. dot_size = 0.8,
  1808. drop_ns = TRUE,
  1809. show_brackets = TRUE,
  1810. width_in = 4, height_in = 4, dpi = 300) {
  1811. summarize_days <- match.arg(summarize_days)
  1812. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1813. dn <- build_sleep_latency(
  1814. df,
  1815. zt0_hour = zt0_hour,
  1816. sleep_block_min = sleep_block_min,
  1817. summarize_days = summarize_days
  1818. )
  1819. # dn should have: genotype, fly_id (or tube), value
  1820. id_col <- if ("fly_id" %in% names(dn)) "fly_id" else if ("tube" %in% names(dn)) "tube" else NULL
  1821. if (is.null(id_col)) stop("build_sleep_latency() output missing an ID column (expected fly_id or tube).")
  1822. d <- dn |>
  1823. dplyr::filter(is.finite(value)) |>
  1824. dplyr::transmute(genotype, id = .data[[id_col]], value)
  1825. # stable ordering
  1826. if (!is.null(color_map)) d$genotype <- factor(d$genotype, levels = names(color_map))
  1827. # stats via phase helper by adding dummy phase
  1828. d_phase <- d |> dplyr::mutate(phase = "All")
  1829. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  1830. d_phase,
  1831. value_col = "value",
  1832. mutant = mutant,
  1833. controls = controls,
  1834. p_adjust = "BH"
  1835. ) |>
  1836. dplyr::mutate(
  1837. p_use = dplyr::coalesce(p_adj, p),
  1838. label = vapply(p_use, .p_to_stars, character(1))
  1839. )
  1840. if (drop_ns) ctrl_vs_mut <- ctrl_vs_mut |> dplyr::filter(!is.na(label), label != "ns")
  1841. # plot
  1842. p <- ggplot2::ggplot(d, ggplot2::aes(x = genotype, y = value, fill = genotype)) +
  1843. ggplot2::geom_boxplot(width = 0.6, alpha = 0.9, outlier.shape = NA) +
  1844. ggplot2::labs(x = NULL, y = y_label, title = title) +
  1845. ggplot2::theme_classic(base_size = 12) +
  1846. ggplot2::theme(
  1847. legend.position = if (show_legend) "right" else "none",
  1848. legend.text = ggplot2::element_text(size = 14),
  1849. legend.title = ggplot2::element_text(size = 0),
  1850. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  1851. axis.title.y = ggplot2::element_text(size = 20),
  1852. axis.text = ggplot2::element_text(size = 14),
  1853. axis.text.x = ggplot2::element_text(angle = 45, hjust = 1, vjust = 1),
  1854. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  1855. ) +
  1856. ggplot2::scale_y_continuous(limits = c(0, NA),
  1857. expand = ggplot2::expansion(mult = c(0, 0.18))) +
  1858. ggplot2::coord_cartesian(clip = "off")
  1859. if (show_dots) {
  1860. p <- p + ggplot2::geom_point(
  1861. position = ggplot2::position_jitter(width = 0.10, height = 0),
  1862. size = dot_size, alpha = 0.6, shape = 16, color = "black",
  1863. show.legend = FALSE
  1864. )
  1865. }
  1866. # colors + parsed x-axis labels
  1867. if (!is.null(color_map)) {
  1868. if (!is.null(genotype_labels)) {
  1869. genotype_labels <- genotype_labels[names(color_map)]
  1870. p <- p +
  1871. ggplot2::scale_fill_manual(
  1872. values = color_map,
  1873. breaks = names(color_map),
  1874. labels = parse(text = genotype_labels)
  1875. ) +
  1876. ggplot2::scale_x_discrete(
  1877. breaks = names(color_map),
  1878. labels = parse(text = genotype_labels)
  1879. ) +
  1880. ggplot2::guides(fill = ggplot2::guide_legend(label = ggplot2::label_parsed))
  1881. } else {
  1882. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  1883. }
  1884. } else if (!is.null(genotype_labels)) {
  1885. p <- p + ggplot2::scale_x_discrete(
  1886. breaks = names(genotype_labels),
  1887. labels = parse(text = genotype_labels)
  1888. )
  1889. }
  1890. # brackets (stats has phase="All")
  1891. if (show_brackets && nrow(ctrl_vs_mut)) {
  1892. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(factor(d$genotype))
  1893. p <- .add_pairwise_brackets_from_build(
  1894. p,
  1895. stats_df = ctrl_vs_mut,
  1896. genotype_order = genotype_order,
  1897. y_pad_frac = 0.06,
  1898. text_size = 5
  1899. )
  1900. }
  1901. if (is.null(filename)) filename <- "DAM_sleep_latency_box.png"
  1902. out <- file.path(outdir, filename)
  1903. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1904. invisible(list(plot = p, stats_ctrl_vs_mut = ctrl_vs_mut, data = d, file = out))
  1905. }
  1906. .plot_phase_box_mw <- function(dn,
  1907. outdir,
  1908. filename,
  1909. color_map = NULL,
  1910. genotype_labels = NULL,
  1911. phase_labels = c(Light="Day", Dark="Night"),
  1912. title = NULL,
  1913. x_label = NULL,
  1914. y_label = NULL,
  1915. show_legend = TRUE,
  1916. show_dots = TRUE,
  1917. dot_size = 0.8,
  1918. legend_text_size = 14,
  1919. legend_title_size = 0,
  1920. mutant = "CG_GAL4_pex5",
  1921. controls = c("pex5","CG_GAL4"),
  1922. show_brackets = TRUE,
  1923. drop_ns = TRUE,
  1924. width_in = 4, height_in = 4, dpi = 300) {
  1925. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  1926. mode <- attr(dn, "mode")
  1927. if (is.null(x_label)) x_label <- if (!is.null(mode) && mode %in% c("DD","LL")) "CT phase" else NULL
  1928. # stable ordering
  1929. dn$phase <- factor(dn$phase, levels = names(phase_labels))
  1930. if (!is.null(color_map)) dn$genotype <- factor(dn$genotype, levels = names(color_map))
  1931. # ---- Mann–Whitney ctrl vs mutant ----
  1932. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  1933. dn, value_col = "value",
  1934. mutant = mutant, controls = controls,
  1935. p_adjust = "BH"
  1936. ) |>
  1937. dplyr::mutate(
  1938. p_use = dplyr::coalesce(p_adj, p),
  1939. label = vapply(p_use, .p_to_stars, character(1))
  1940. )
  1941. if (drop_ns) ctrl_vs_mut <- ctrl_vs_mut |> dplyr::filter(!is.na(label), label != "ns")
  1942. # ---- plot ----
  1943. p <- ggplot2::ggplot(dn, ggplot2::aes(x = phase, y = value, fill = genotype)) +
  1944. ggplot2::geom_boxplot(
  1945. position = ggplot2::position_dodge(width = 0.6),
  1946. width = 0.6, alpha = 0.9, outlier.shape = NA
  1947. ) +
  1948. ggplot2::labs(x = x_label, y = y_label, title = title) +
  1949. ggplot2::theme_classic(base_size = 12) +
  1950. ggplot2::theme(
  1951. legend.position = if (show_legend) "right" else "none",
  1952. legend.text = ggplot2::element_text(size = legend_text_size),
  1953. legend.title = ggplot2::element_text(size = legend_title_size),
  1954. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  1955. axis.title.y = ggplot2::element_text(size = 20),
  1956. axis.text = ggplot2::element_text(size = 14),
  1957. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  1958. ) +
  1959. ggplot2::scale_x_discrete(labels = phase_labels) +
  1960. ggplot2::scale_y_continuous(limits = c(0, NA),
  1961. expand = ggplot2::expansion(mult = c(0, 0.18))) +
  1962. ggplot2::coord_cartesian(clip = "off")
  1963. if (show_dots) {
  1964. p <- p + ggplot2::geom_point(
  1965. position = ggplot2::position_jitterdodge(jitter.width = 0.04, dodge.width = 0.6),
  1966. size = dot_size, alpha = 0.6, shape = 16, color = "black"
  1967. )
  1968. }
  1969. if (!is.null(color_map)) {
  1970. if (!is.null(genotype_labels)) {
  1971. genotype_labels <- genotype_labels[names(color_map)]
  1972. p <- p +
  1973. ggplot2::scale_fill_manual(
  1974. values = color_map, breaks = names(color_map),
  1975. labels = parse(text = genotype_labels)
  1976. ) +
  1977. ggplot2::guides(fill = ggplot2::guide_legend(label = ggplot2::label_parsed))
  1978. } else {
  1979. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  1980. }
  1981. }
  1982. if (show_brackets && nrow(ctrl_vs_mut)) {
  1983. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(dn$genotype)
  1984. p <- .add_pairwise_brackets_from_build(
  1985. p,
  1986. stats_df = ctrl_vs_mut,
  1987. genotype_order = genotype_order,
  1988. y_pad_frac = 0.06,
  1989. text_size = 5
  1990. )
  1991. }
  1992. out <- file.path(outdir, filename)
  1993. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  1994. invisible(list(plot = p, stats_ctrl_vs_mut = ctrl_vs_mut, data = dn, file = out, mode = mode))
  1995. }
  1996. plot_daynight_bout_count_box <- function(df,
  1997. metric = c("sleep","activity"),
  1998. zt0_hour = 6L,
  1999. sleep_block_min = 5L,
  2000. activity_threshold = 0L,
  2001. summarize_days = c("mean","median"),
  2002. outdir = "DAM_Graphs",
  2003. filename = NULL,
  2004. color_map = NULL,
  2005. genotype_labels = NULL,
  2006. phase_labels = c(Light="Day", Dark="Night"),
  2007. title = NULL,
  2008. y_label = NULL,
  2009. mutant = "CG_GAL4_pex5",
  2010. controls = c("pex5","CG_GAL4"),
  2011. show_legend = TRUE,
  2012. show_dots = TRUE,
  2013. dot_size = 0.8,
  2014. show_brackets = TRUE,
  2015. drop_ns = TRUE,
  2016. width_in = 4, height_in = 4, dpi = 300) {
  2017. metric <- match.arg(metric)
  2018. summarize_days <- match.arg(summarize_days)
  2019. dn <- build_daynight_bout_metric(
  2020. df, metric = metric, what = "count",
  2021. zt0_hour = zt0_hour,
  2022. sleep_block_min = sleep_block_min,
  2023. activity_threshold = activity_threshold,
  2024. summarize_days = summarize_days
  2025. )
  2026. if (is.null(title)) title <- sprintf("%s bouts (count)", if (metric == "sleep") "Sleep" else "Activity")
  2027. if (is.null(y_label)) y_label <- "Bouts per 12h"
  2028. if (is.null(filename)) filename <- sprintf("DAM_%s_boutcount_day_night_box.png", metric)
  2029. .plot_phase_box_mw(
  2030. dn,
  2031. outdir = outdir,
  2032. filename = filename,
  2033. color_map = color_map,
  2034. genotype_labels = genotype_labels,
  2035. phase_labels = phase_labels,
  2036. title = title,
  2037. y_label = y_label,
  2038. mutant = mutant,
  2039. controls = controls,
  2040. show_legend = show_legend,
  2041. show_dots = show_dots,
  2042. dot_size = dot_size,
  2043. show_brackets = show_brackets,
  2044. drop_ns = drop_ns,
  2045. width_in = width_in,
  2046. height_in = height_in,
  2047. dpi = dpi
  2048. )
  2049. }
  2050. plot_daynight_bout_duration_box <- function(df,
  2051. metric = c("sleep","activity"),
  2052. zt0_hour = 6L,
  2053. sleep_block_min = 5L,
  2054. activity_threshold = 0L,
  2055. summarize_days = c("mean","median"),
  2056. outdir = "DAM_Graphs",
  2057. filename = NULL,
  2058. color_map = NULL,
  2059. genotype_labels = NULL,
  2060. phase_labels = c(Light="Day", Dark="Night"),
  2061. title = NULL,
  2062. y_label = NULL,
  2063. mutant = "CG_GAL4_pex5",
  2064. controls = c("pex5","CG_GAL4"),
  2065. show_legend = TRUE,
  2066. show_dots = TRUE,
  2067. dot_size = 0.8,
  2068. show_brackets = TRUE,
  2069. drop_ns = TRUE,
  2070. width_in = 4, height_in = 4, dpi = 300) {
  2071. metric <- match.arg(metric)
  2072. summarize_days <- match.arg(summarize_days)
  2073. dn <- build_daynight_bout_metric(
  2074. df, metric = metric, what = "duration",
  2075. zt0_hour = zt0_hour,
  2076. sleep_block_min = sleep_block_min,
  2077. activity_threshold = activity_threshold,
  2078. summarize_days = summarize_days
  2079. )
  2080. if (is.null(title)) title <- sprintf("%s bout duration", if (metric == "sleep") "Sleep" else "Activity")
  2081. if (is.null(y_label)) y_label <- "Mean bout duration (min)"
  2082. if (is.null(filename)) filename <- sprintf("DAM_%s_boutdur_day_night_box.png", metric)
  2083. .plot_phase_box_mw(
  2084. dn,
  2085. outdir = outdir,
  2086. filename = filename,
  2087. color_map = color_map,
  2088. genotype_labels = genotype_labels,
  2089. phase_labels = phase_labels,
  2090. title = title,
  2091. y_label = y_label,
  2092. mutant = mutant,
  2093. controls = controls,
  2094. show_legend = show_legend,
  2095. show_dots = show_dots,
  2096. dot_size = dot_size,
  2097. show_brackets = show_brackets,
  2098. drop_ns = drop_ns,
  2099. width_in = width_in,
  2100. height_in = height_in,
  2101. dpi = dpi
  2102. )
  2103. }
  2104. ####period functions####
  2105. # Detrend helper (optional)
  2106. .detrend <- function(y, method = c("none","linear","loess")) {
  2107. method <- match.arg(method)
  2108. y <- as.numeric(y)
  2109. if (method == "none") return(y)
  2110. t <- seq_along(y)
  2111. if (method == "linear") {
  2112. fit <- stats::lm(y ~ t)
  2113. return(y - stats::predict(fit))
  2114. } else { # loess
  2115. # lite loess with large span to capture slow drift
  2116. fit <- try(stats::loess(y ~ t, span = 0.3, family = "symmetric"), silent = TRUE)
  2117. if (inherits(fit, "try-error")) return(y)
  2118. y - stats::predict(fit)
  2119. }
  2120. }
  2121. # Bin to k-minute means (handles NAs)
  2122. .bin_series <- function(ts, val, by_min = 5L) {
  2123. stopifnot(length(ts) == length(val))
  2124. if (by_min <= 1L) return(data.frame(ts = ts, val = val, bin_min = 1L))
  2125. # Bin start at first minute boundary
  2126. origin <- min(ts, na.rm = TRUE)
  2127. bin <- as.integer(difftime(ts, origin, units = "mins")) %/% as.integer(by_min)
  2128. agg <- stats::aggregate(val ~ bin, FUN = function(z) mean(z, na.rm = TRUE))
  2129. agg_ts <- origin + (agg$bin * by_min) * 60
  2130. data.frame(ts = agg_ts, val = agg$val, bin_min = as.integer(by_min))
  2131. }
  2132. # Core estimator: ACF or Lomb
  2133. .est_period <- function(y, samp_min = 1L, min_h = 18, max_h = 30, method = c("acf","lomb")) {
  2134. method <- match.arg(method)
  2135. y <- as.numeric(y); y[is.na(y)] <- 0
  2136. if (all(y == 0)) return(c(period_h = NA_real_, strength = NA_real_))
  2137. if (method == "acf") {
  2138. a <- stats::acf(y, plot = FALSE, lag.max = 72*60/samp_min)
  2139. # acf$lags are fractions of series length; convert to minutes correctly
  2140. n <- length(y)
  2141. lags_idx <- seq_along(a$acf[,1,1]) - 1L
  2142. lags_min <- lags_idx * samp_min
  2143. lags_h <- lags_min / 60
  2144. ok <- which(lags_h >= min_h & lags_h <= max_h)
  2145. if (!length(ok)) return(c(period_h = NA_real_, strength = NA_real_))
  2146. # Avoid trivially-large low-frequency trend: use first local max in window
  2147. idx <- ok[which.max(a$acf[ok,1,1])]
  2148. c(period_h = lags_h[idx], strength = a$acf[idx,1,1])
  2149. } else {
  2150. if (!requireNamespace("lomb", quietly = TRUE)) return(c(period_h = NA_real_, strength = NA_real_))
  2151. t <- seq_along(y) * samp_min # minutes
  2152. ls <- lomb::lsp(data.frame(t = t, y = y),
  2153. from = 1/(max_h*60), to = 1/(min_h*60),
  2154. ofac = 4, type = "period", plot = FALSE)
  2155. pk <- which.max(ls$power)
  2156. c(period_h = ls$scanned[pk]/60, strength = ls$power[pk])
  2157. }
  2158. }
  2159. # Public API
  2160. analyze_periodicity <- function(df,
  2161. method = c("acf","lomb"),
  2162. min_period_h = 18, max_period_h = 30,
  2163. bin_minutes = 5L,
  2164. detrend = c("none","linear","loess")) {
  2165. method <- match.arg(method)
  2166. detrend <- match.arg(detrend)
  2167. # Build per-tube, regularly sampled series (we already have per-minute rows)
  2168. # Optionally bin to reduce noise
  2169. out <- df %>%
  2170. dplyr::arrange(ts) %>%
  2171. dplyr::group_by(genotype, tube) %>%
  2172. dplyr::summarise({
  2173. s <- .bin_series(ts, count_na0, by_min = as.integer(bin_minutes))
  2174. y <- .detrend(s$val, method = detrend)
  2175. est <- .est_period(y,
  2176. samp_min = as.integer(bin_minutes),
  2177. min_h = min_period_h, max_h = max_period_h,
  2178. method = method)
  2179. dplyr::tibble(
  2180. bin_minutes = as.integer(bin_minutes),
  2181. detrend = detrend,
  2182. method = method,
  2183. n_minutes = sum(!is.na(s$val)) * as.integer(bin_minutes),
  2184. frac_nonzero= mean(s$val > 0, na.rm = TRUE),
  2185. period_h = as.numeric(est[[1]]),
  2186. strength = as.numeric(est[[2]])
  2187. )
  2188. }, .groups = "drop")
  2189. out
  2190. }
  2191. plot_period_distribution <- function(per_tbl,
  2192. outdir = outdamdir,
  2193. color_map = NULL,
  2194. genotype_labels = NULL, # named plotmath strings
  2195. show_legend = TRUE,
  2196. trim = FALSE, # <- default FALSE per your request
  2197. width_in = 6, height_in = 4, dpi = 300,
  2198. filename = NULL) {
  2199. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  2200. # keep finite only for plotting
  2201. per_tbl <- per_tbl |>
  2202. dplyr::filter(is.finite(period_h))
  2203. # stable ordering if color_map supplied
  2204. if (!is.null(color_map)) {
  2205. per_tbl <- per_tbl |>
  2206. dplyr::mutate(genotype = factor(genotype, levels = names(color_map)))
  2207. }
  2208. # pretty x-axis labels (plotmath), independent of legend
  2209. x_scale <- ggplot2::scale_x_discrete()
  2210. if (!is.null(genotype_labels)) {
  2211. if (!is.null(color_map)) genotype_labels <- genotype_labels[names(color_map)]
  2212. x_scale <- ggplot2::scale_x_discrete(
  2213. breaks = names(genotype_labels),
  2214. labels = parse(text = genotype_labels)
  2215. )
  2216. }
  2217. # natural y-range like the original: min/max with a bit of padding
  2218. y_min <- suppressWarnings(min(per_tbl$period_h, na.rm = TRUE))
  2219. y_max <- suppressWarnings(max(per_tbl$period_h, na.rm = TRUE))
  2220. if (!is.finite(y_min) || !is.finite(y_max)) {
  2221. y_min <- 18; y_max <- 30
  2222. }
  2223. yr <- y_max - y_min
  2224. if (!is.finite(yr) || yr <= 0) yr <- 1
  2225. y_pad <- 0.08 * yr
  2226. p <- ggplot2::ggplot(per_tbl, ggplot2::aes(x = genotype, y = period_h, fill = genotype)) +
  2227. ggplot2::geom_violin(trim = trim, alpha = 0.3) +
  2228. ggplot2::geom_boxplot(width = 0.2, outlier.shape = NA, alpha = 0.9) +
  2229. ggplot2::geom_jitter(ggplot2::aes(color = genotype),
  2230. width = 0.1, alpha = 0.5, size = 1,
  2231. show.legend = FALSE) +
  2232. ggplot2::geom_hline(yintercept = 24, linetype = 2, linewidth = 0.8, alpha = 0.7) +
  2233. ggplot2::labs(x = NULL, y = "Estimated period (h)", title = "Circadian Period") +
  2234. ggplot2::theme_classic(base_size = 12) +
  2235. ggplot2::theme(
  2236. legend.position = if (show_legend) "right" else "none",
  2237. plot.title = ggplot2::element_text(hjust = 0.5)
  2238. ) +
  2239. x_scale +
  2240. ggplot2::coord_cartesian(ylim = c(y_min - y_pad, y_max + y_pad), clip = "off")
  2241. if (!is.null(color_map)) {
  2242. p <- p +
  2243. ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map)) +
  2244. ggplot2::scale_color_manual(values = color_map, breaks = names(color_map))
  2245. }
  2246. if (is.null(filename)) {
  2247. filename <- sprintf("DAM_period_distribution_%s.png", format(Sys.time(), "%Y%m%d_%H%M%S"))
  2248. }
  2249. out <- file.path(outdir, filename)
  2250. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  2251. invisible(list(plot = p, file = out))
  2252. }
  2253. plot_periodogram <- function(df, genotype, tube,
  2254. method = c("acf","lomb"),
  2255. bin_minutes = 5L,
  2256. detrend = c("none","linear","loess"),
  2257. min_period_h = 18, max_period_h = 30,
  2258. outdir = outdamdir, width_in = 4, height_in = 4, dpi = 300) {
  2259. method <- match.arg(method)
  2260. detrend <- match.arg(detrend)
  2261. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  2262. dd <- df %>% dplyr::filter(genotype == !!genotype, tube == !!tube) %>% dplyr::arrange(ts)
  2263. stopifnot(nrow(dd) > 0)
  2264. s <- .bin_series(dd$ts, dd$count_na0, by_min = as.integer(bin_minutes))
  2265. y <- .detrend(s$val, method = detrend); y[is.na(y)] <- 0
  2266. if (method == "acf") {
  2267. a <- stats::acf(y, plot = FALSE, lag.max = 72*60/as.integer(bin_minutes))
  2268. lags_idx <- seq_along(a$acf[,1,1]) - 1L
  2269. lags_h <- (lags_idx * as.integer(bin_minutes))/60
  2270. d <- data.frame(period_h = lags_h, strength = a$acf[,1,1])
  2271. d <- d[d$period_h >= min_period_h & d$period_h <= max_period_h, ]
  2272. p <- ggplot2::ggplot(d, ggplot2::aes(x = period_h, y = strength)) +
  2273. ggplot2::geom_line(linewidth = 0.8) +
  2274. ggplot2::labs(x = "Lag (h)", y = "ACF", title = sprintf("ACF periodogram — %s tube %d", genotype, tube)) +
  2275. ggplot2::theme_classic(base_size = 12)
  2276. } else {
  2277. if (!requireNamespace("lomb", quietly = TRUE)) stop("Package 'lomb' not installed.")
  2278. t <- seq_along(y) * as.integer(bin_minutes)
  2279. ls <- lomb::lsp(data.frame(t = t, y = y),
  2280. from = 1/(max_period_h*60), to = 1/(min_period_h*60),
  2281. ofac = 4, type = "period", plot = FALSE)
  2282. d <- data.frame(period_h = ls$scanned/60, power = ls$power)
  2283. p <- ggplot2::ggplot(d, ggplot2::aes(x = period_h, y = power)) +
  2284. ggplot2::geom_line(linewidth = 0.8) +
  2285. ggplot2::labs(x = "Period (h)", y = "Lomb power", title = sprintf("Lomb–Scargle — %s tube %d", genotype, tube)) +
  2286. ggplot2::theme_classic(base_size = 12)
  2287. }
  2288. out <- file.path(outdir, sprintf("DAM_periodogram_%s_tube%02d_%s.png",
  2289. genotype, as.integer(tube), format(Sys.time(), "%Y%m%d_%H%M%S")))
  2290. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  2291. invisible(list(plot = p, file = out))
  2292. }
  2293. # Estimate per-tube periods with ACF, binned to 5 min, loess detrend
  2294. plot_period_box <- function(per_tbl,
  2295. outdir = "DAM_Graphs",
  2296. filename = "DAM_period_box.png",
  2297. color_map = NULL,
  2298. genotype_labels = NULL,
  2299. mutant = "CG_GAL4_pex5",
  2300. controls = c("pex5","CG_GAL4"),
  2301. show_legend = TRUE,
  2302. show_dots = TRUE,
  2303. dot_size = 0.9,
  2304. drop_ns = TRUE,
  2305. y_label = "Estimated period (h)",
  2306. title = "Circadian period",
  2307. width_in = 4.5, height_in = 4, dpi = 300) {
  2308. dir.create(outdir, showWarnings = FALSE, recursive = TRUE)
  2309. d <- per_tbl |>
  2310. dplyr::filter(is.finite(period_h)) |>
  2311. dplyr::transmute(genotype, tube, value = period_h)
  2312. # stable ordering
  2313. if (!is.null(color_map)) d$genotype <- factor(d$genotype, levels = names(color_map))
  2314. # ---- Mann–Whitney control vs mutant ----
  2315. d_phase <- d |>
  2316. dplyr::mutate(phase = "All")
  2317. # ---- Mann–Whitney ctrl vs mutant (same helper you already trust) ----
  2318. ctrl_vs_mut <- pairwise_vs_mutant_by_phase(
  2319. d_phase,
  2320. value_col = "value",
  2321. mutant = mutant,
  2322. controls = controls,
  2323. p_adjust = "BH"
  2324. ) |>
  2325. dplyr::mutate(
  2326. p_use = dplyr::coalesce(p_adj, p),
  2327. label = vapply(p_use, .p_to_stars, character(1))
  2328. )
  2329. if (drop_ns) {
  2330. ctrl_vs_mut <- ctrl_vs_mut |> dplyr::filter(!is.na(label), label != "ns")
  2331. }
  2332. # ---- plot ----
  2333. # --- y padding so boxes aren't micro / clipped ---
  2334. y_min <- min(d$value, na.rm = TRUE)
  2335. y_max <- max(d$value, na.rm = TRUE)
  2336. yr <- y_max - y_min
  2337. if (!is.finite(yr) || yr <= 0) yr <- 0.1
  2338. y_pad <- 0.12 * yr
  2339. p <- ggplot2::ggplot(d, ggplot2::aes(x = genotype, y = value, fill = genotype)) +
  2340. # draw the 24h reference line FIRST so it doesn't cover boxes
  2341. ggplot2::geom_hline(yintercept = 24, linetype = 2, linewidth = 0.6, alpha = 0.5) +
  2342. # make box outlines visible
  2343. ggplot2::geom_boxplot(
  2344. width = 0.55,
  2345. outlier.shape = NA,
  2346. alpha = 0.6,
  2347. color = "black",
  2348. linewidth = 0.6
  2349. ) +
  2350. ggplot2::labs(x = NULL, y = y_label, title = title) +
  2351. ggplot2::theme_classic(base_size = 12) +
  2352. ggplot2::theme(
  2353. legend.position = if (show_legend) "right" else "none",
  2354. plot.title = ggplot2::element_text(hjust = 0.5, size = 26),
  2355. axis.title.y = ggplot2::element_text(size = 20),
  2356. axis.text = ggplot2::element_text(size = 14),
  2357. axis.text.x = ggplot2::element_text(angle = 45, hjust = 1, vjust = 1), # <- NEW
  2358. plot.margin = ggplot2::margin(t = 12, r = 10, b = 6, l = 6)
  2359. ) +
  2360. ggplot2::scale_y_continuous(expand = ggplot2::expansion(mult = c(0.02, 0.10))) +
  2361. ggplot2::coord_cartesian(ylim = c(y_min - y_pad, y_max + y_pad), clip = "off")
  2362. if (show_dots) {
  2363. p <- p + ggplot2::geom_point(
  2364. position = ggplot2::position_jitter(width = 0.10),
  2365. size = dot_size, alpha = 0.6, shape = 16, color = "black",
  2366. show.legend = FALSE
  2367. )
  2368. }
  2369. # colors + parsed genotype labels (legend + x-axis)
  2370. if (!is.null(color_map)) {
  2371. if (!is.null(genotype_labels)) {
  2372. genotype_labels <- genotype_labels[names(color_map)]
  2373. p <- p +
  2374. ggplot2::scale_fill_manual(values = color_map,
  2375. breaks = names(color_map),
  2376. labels = parse(text = genotype_labels)) +
  2377. ggplot2::scale_x_discrete(breaks = names(color_map),
  2378. labels = parse(text = genotype_labels)) +
  2379. ggplot2::guides(fill = ggplot2::guide_legend(label = ggplot2::label_parsed))
  2380. } else {
  2381. p <- p + ggplot2::scale_fill_manual(values = color_map, breaks = names(color_map))
  2382. }
  2383. } else if (!is.null(genotype_labels)) {
  2384. p <- p + ggplot2::scale_x_discrete(breaks = names(genotype_labels),
  2385. labels = parse(text = genotype_labels))
  2386. }
  2387. # brackets (same style helper you already use)
  2388. if (nrow(ctrl_vs_mut)) {
  2389. genotype_order <- if (!is.null(color_map)) names(color_map) else levels(factor(d$genotype))
  2390. p <- .add_pairwise_brackets_from_build(
  2391. p,
  2392. stats_df = ctrl_vs_mut,
  2393. genotype_order = genotype_order,
  2394. y_pad_frac = 0.06,
  2395. text_size = 5
  2396. )
  2397. }
  2398. out <- file.path(outdir, filename)
  2399. ggplot2::ggsave(out, p, width = width_in, height = height_in, dpi = dpi)
  2400. invisible(list(plot = p, stats_ctrl_vs_mut = ctrl_vs_mut, data = d, file = out))
  2401. }
  2402. ###GMR77A03 LD####
  2403. #### read file, purge dead flies####
  2404. setwd("/Users/jvaughen/Dropbox/Anurag_MS/sleep/")
  2405. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR77A03/Graphs/"
  2406. damcolors <- c(
  2407. pex5 = "#1B7837",
  2408. CG_GAL4 = "#2166AC",
  2409. CG_GAL4_pex5 = "magenta3"
  2410. )
  2411. genotype_labels <- c(
  2412. pex5 = "italic(Pex5^{KD})",
  2413. CG_GAL4 = "italic(CG *'>')", # or whatever you want this called
  2414. CG_GAL4_pex5 = "italic(CG *'>'*Pex5^{KD})"
  2415. )
  2416. #printing legend
  2417. df_leg <- data.frame(
  2418. x = 1:3,
  2419. y = 1,
  2420. genotype = factor(names(damcolors), levels = names(damcolors))
  2421. )
  2422. p_leg <- ggplot(df_leg, aes(x, y, fill = genotype)) +
  2423. geom_col(width = 0.6) +
  2424. scale_fill_manual(
  2425. values = damcolors,
  2426. breaks = names(damcolors),
  2427. labels = parse(text = genotype_labels[names(damcolors)])
  2428. ) +
  2429. guides(
  2430. fill = guide_legend(
  2431. title = NULL,
  2432. label = label_parsed
  2433. )
  2434. ) +
  2435. theme_void() +
  2436. theme(
  2437. legend.position = "right",
  2438. legend.text = element_text(size = 14),
  2439. legend.title = element_blank()
  2440. )
  2441. # Extract legend
  2442. leg <- cowplot::get_legend(p_leg)
  2443. # Save with WHITE background
  2444. png(
  2445. filename = "GMR77A03/DAM_legend.png",
  2446. width = 700,
  2447. height = 300,
  2448. res = 300,
  2449. bg = "white" # <-- this is the key line
  2450. )
  2451. grid.newpage()
  2452. grid.draw(leg)
  2453. dev.off()
  2454. specs <- list(
  2455. # Oct run
  2456. list(path="GMR77A03/Oct1-5.txt", run_id="Oct1-5", date_start="2025-10-01", date_end="2025-10-05",
  2457. genotype="CG_GAL4_pex5", tubes=1:10),
  2458. list(path="GMR77A03/Oct1-5.txt", run_id="Oct1-5", date_start="2025-10-01", date_end="2025-10-05",
  2459. genotype="pex5", tubes=11:21),
  2460. list(path="GMR77A03/Oct1-5.txt", run_id="Oct1-5", date_start="2025-10-01", date_end="2025-10-05",
  2461. genotype="CG_GAL4", tubes=22:32),
  2462. # Nov run 1
  2463. list(path="GMR77A03/Nov8-12.txt", run_id="Nov8-12", date_start="2025-11-08", date_end="2025-11-12",
  2464. genotype="CG_GAL4_pex5", tubes=1:10),
  2465. list(path="GMR77A03/Nov8-12.txt", run_id="Nov8-12", date_start="2025-11-08", date_end="2025-11-12",
  2466. genotype="pex5", tubes=11:21),
  2467. list(path="GMR77A03/Nov8-12.txt", run_id="Nov8-12", date_start="2025-11-08", date_end="2025-11-12",
  2468. genotype="CG_GAL4", tubes=22:32),
  2469. # Nov run 2
  2470. list(path="GMR77A03/Nov14-19.txt", run_id="Nov14-19", date_start="2025-11-14", date_end="2025-11-19",
  2471. genotype="CG_GAL4_pex5", tubes=1:10),
  2472. list(path="GMR77A03/Nov14-19.txt", run_id="Nov14-19", date_start="2025-11-14", date_end="2025-11-19",
  2473. genotype="pex5", tubes=11:21),
  2474. list(path="GMR77A03/Nov14-19.txt", run_id="Nov14-19", date_start="2025-11-14", date_end="2025-11-19",
  2475. genotype="CG_GAL4", tubes=22:32),
  2476. # Jan run
  2477. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt", run_id="Jan23-26", date_start="2025-01-23", date_end="2025-01-26",
  2478. genotype="CG_GAL4_pex5", tubes=1:10),
  2479. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt", run_id="Jan23-26", date_start="2025-01-23", date_end="2025-01-26",
  2480. genotype="pex5", tubes=11:21),
  2481. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt", run_id="Jan23-26", date_start="2025-01-23", date_end="2025-01-26",
  2482. genotype="CG_GAL4", tubes=22:32)
  2483. )
  2484. df <- read_dam_specs(specs, fill_missing=TRUE)
  2485. lc <- infer_light_cycle(df) # prints median lights-on/off
  2486. df2 <- df %>%
  2487. dplyr::group_by(run_id) %>%
  2488. dplyr::group_modify(\(d, key) remove_dead(d, inactivity_hours = 12, quiet = F)$data) %>%
  2489. dplyr::ungroup()
  2490. #make sure ok, tallying n
  2491. df2 %>%
  2492. dplyr::distinct(run_id, genotype, tube) %>%
  2493. dplyr::count(run_id, genotype, name = "n_tubes") %>%
  2494. tidyr::pivot_wider(names_from = genotype, values_from = n_tubes, values_fill = 0)
  2495. df2 %>%
  2496. dplyr::distinct(run_id, genotype, tube) %>%
  2497. dplyr::count(genotype, name = "n_total_tubes") %>%
  2498. dplyr::arrange(desc(n_total_tubes))
  2499. df2 %>%
  2500. dplyr::distinct(run_id, genotype, tube) %>%
  2501. dplyr::count(genotype, name = "n_flies_total")
  2502. # How many unique tube numbers (pooled across runs) are being used?
  2503. df2 %>%
  2504. dplyr::distinct(genotype, tube) %>%
  2505. dplyr::count(genotype, name = "n_tube_numbers")
  2506. #dead <- remove_dead(df, inactivity_hours = 12)
  2507. #View(dead$purged) # which tubes were removed. highest = dead whole time, lowest is based on threshodl. dont think mintues are acutally being treated properly
  2508. ####zt average plots####
  2509. plot_average_day(
  2510. df2, metric = "activity", bin_minutes = 5,
  2511. color_map = damcolors,
  2512. genotype_labels = genotype_labels,
  2513. mean_linewidth = 0.8, sem_alpha = 0.25,
  2514. title = "Average Activity", y_label = "Counts / min", x_label = "ZT",
  2515. title_size = 26, axis_title_size = 20, axis_text_size = 14,
  2516. show_bg_phase_shading = F,
  2517. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2518. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2519. show_phase_bar = T, phase_bar_position = "below", bar_height_frac = 0.04,
  2520. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F", show_legend = F,
  2521. outdir = outdamdir, filename = "avgday_activity1.png",
  2522. width_in = 5, height_in = 4
  2523. )
  2524. # Sleep: turn off phase bar, keep background shading
  2525. plot_average_day(
  2526. df2, metric = "sleep", bin_minutes = 5,
  2527. genotype_labels = genotype_labels,
  2528. color_map = damcolors,
  2529. mean_linewidth = 0.8, sem_alpha = 0.25,
  2530. title = "Average Sleep", y_label = "Sleep probability/ min", x_label = "ZT",
  2531. title_size = 26, axis_title_size = 20, axis_text_size = 14,show_legend = F,
  2532. show_bg_phase_shading = F,
  2533. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2534. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2535. show_phase_bar = T, bar_height_frac = 0.04, phase_bar_position="below",
  2536. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F",
  2537. outdir = outdamdir, filename = "avgday_sleep1.png",
  2538. width_in = 5, height_in = 4
  2539. )
  2540. ####boxplots+stats####
  2541. res_act <- plot_daynight_activity_box(
  2542. df2,
  2543. outdir = outdamdir,
  2544. color_map = damcolors,
  2545. genotype_labels = genotype_labels,
  2546. show_kw_stars = FALSE,
  2547. show_brackets = TRUE,
  2548. show_legend =F,
  2549. drop_ns = TRUE,
  2550. width_in = 4,
  2551. dot_size = 1,
  2552. height_in = 4
  2553. )
  2554. # Sleep boxplots + stats
  2555. res_slp <- plot_daynight_sleep_box(
  2556. df2,
  2557. sleep_block_min = 5,
  2558. outdir = outdamdir,
  2559. color_map = damcolors,
  2560. genotype_labels = genotype_labels,
  2561. title = "Sleep",
  2562. y_label = "Sleep (Minutes per day)",
  2563. show_legend = F,
  2564. show_dots = TRUE,
  2565. dot_size = 1,
  2566. width_in = 4, height_in = 4, dpi = 300
  2567. )
  2568. ####period####
  2569. per <- analyze_periodicity(df2, method = "acf", bin_minutes = 5, detrend = "loess")
  2570. plot_period_box(per, outdir = outdamdir, color_map = damcolors, genotype_labels = genotype_labels,
  2571. mutant= "CG_GAL4_pex5", controls = c("pex5","CG_GAL4"), show_legend=F, width_in = 4, height_in = 4)
  2572. ####bouts 77a03 ld####
  2573. res_sleep_bouts_n <- plot_daynight_bout_count_box(
  2574. df2,
  2575. metric = "sleep",
  2576. zt0_hour = 6L,
  2577. sleep_block_min = 5L,
  2578. outdir = outdamdir,
  2579. filename = "sleep_bout_count.png",
  2580. color_map = damcolors,
  2581. genotype_labels = genotype_labels,
  2582. mutant = "CG_GAL4_pex5",
  2583. controls = c("pex5","CG_GAL4"),
  2584. show_legend = FALSE,
  2585. dot_size = 1,
  2586. width_in = 4, height_in = 4
  2587. )
  2588. res_sleep_boutdur <- plot_daynight_bout_duration_box(
  2589. df2,
  2590. metric = "sleep",
  2591. zt0_hour = 6L,
  2592. sleep_block_min = 5L,
  2593. outdir = outdamdir,
  2594. filename = "sleep_bout_duration.png",
  2595. color_map = damcolors,
  2596. genotype_labels = genotype_labels,
  2597. mutant = "CG_GAL4_pex5",
  2598. controls = c("pex5","CG_GAL4"),
  2599. show_legend = FALSE,
  2600. dot_size = 1,
  2601. width_in = 4, height_in = 4
  2602. )
  2603. res_act_bouts_n <- plot_daynight_bout_count_box(
  2604. df2,
  2605. metric = "activity",
  2606. activity_threshold = 0L, # active minute = count>0
  2607. zt0_hour = 6L,
  2608. outdir = outdamdir,
  2609. filename = "activity_bout_count.png",
  2610. color_map = damcolors,
  2611. genotype_labels = genotype_labels,
  2612. mutant = "CG_GAL4_pex5",
  2613. controls = c("pex5","CG_GAL4"),
  2614. show_legend = FALSE,
  2615. dot_size = 1,
  2616. width_in = 4, height_in = 4
  2617. )
  2618. res_latency <- plot_sleep_latency_box(
  2619. df2,
  2620. zt0_hour = 6L,
  2621. sleep_block_min = 5L,
  2622. outdir = outdamdir,
  2623. filename = "sleep_latency.png",
  2624. color_map = damcolors,
  2625. genotype_labels = genotype_labels,
  2626. mutant = "CG_GAL4_pex5",
  2627. controls = c("pex5","CG_GAL4"),
  2628. show_legend = FALSE,
  2629. dot_size = 1,
  2630. width_in = 4, height_in = 4
  2631. )
  2632. #### Export metrics — GMR77A03 LD ####
  2633. export_metrics_xlsx(
  2634. outdamdir = outdamdir,
  2635. res_act = res_act,
  2636. res_slp = res_slp,
  2637. res_sleep_bouts_n = res_sleep_bouts_n,
  2638. res_sleep_boutdur = res_sleep_boutdur,
  2639. res_act_bouts_n = res_act_bouts_n,
  2640. res_latency = res_latency,
  2641. per = per,
  2642. filename = "S1_Data.xlsx"
  2643. )
  2644. ###GMR77A03 DD####
  2645. #### read file, purge dead flies####
  2646. #setwd("/Users/jvaughen/Dropbox/sapr/data/Dam/")
  2647. setwd("/Users/jvaughen/Dropbox/Anurag_MS/sleep/")
  2648. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR77A03/DD/Graphs/"
  2649. #this caused r nearly to crash- need to find smarter way to trim brefore combining 32 tubes across that many timepoints (and we oversampled..)
  2650. #damcolors<-c(gba1b= "#AA4499", control = "#44AA99")
  2651. damcolors <- c(
  2652. pex5 = "#1B7837",
  2653. CG_GAL4 = "#2166AC",
  2654. CG_GAL4_pex5 = "magenta3"
  2655. )
  2656. genotype_labels <- c(
  2657. pex5 = "italic(Pex5^{KD})",
  2658. CG_GAL4 = "italic(CG *'>')", # or whatever you want this called
  2659. CG_GAL4_pex5 = "italic(CG *'>'*Pex5^{KD})"
  2660. )
  2661. #printing legend
  2662. df_leg <- data.frame(
  2663. x = 1:3,
  2664. y = 1,
  2665. genotype = factor(names(damcolors), levels = names(damcolors))
  2666. )
  2667. p_leg <- ggplot(df_leg, aes(x, y, fill = genotype)) +
  2668. geom_col(width = 0.6) +
  2669. scale_fill_manual(
  2670. values = damcolors,
  2671. breaks = names(damcolors),
  2672. labels = parse(text = genotype_labels[names(damcolors)])
  2673. ) +
  2674. guides(
  2675. fill = guide_legend(
  2676. title = NULL,
  2677. label = label_parsed
  2678. )
  2679. ) +
  2680. theme_void() +
  2681. theme(
  2682. legend.position = "right",
  2683. legend.text = element_text(size = 14),
  2684. legend.title = element_blank()
  2685. )
  2686. # Extract legend
  2687. leg <- cowplot::get_legend(p_leg)
  2688. # Save with WHITE background
  2689. png(
  2690. filename = "GMR77A03/DD/DAM_legend.png",
  2691. width = 700,
  2692. height = 300,
  2693. res = 300,
  2694. bg = "white" # <-- this is the key line
  2695. )
  2696. grid.newpage()
  2697. grid.draw(leg)
  2698. dev.off()
  2699. specs <- list(
  2700. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt", run_id="Jan28-2", date_start="2025-01-28", date_end="2025-02-2",
  2701. genotype="CG_GAL4_pex5", tubes=1:10),
  2702. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt",run_id="Jan28-2", date_start="2025-01-28", date_end="2025-02-2",
  2703. genotype="pex5", tubes=11:21),
  2704. list(path="GMR77A03/23jan_26janLD_28jan-2_DD.txt", run_id="Jan28-2", date_start="2025-01-28", date_end="2025-02-2",
  2705. genotype="CG_GAL4", tubes=22:32)
  2706. )
  2707. df <- read_dam_specs(specs, fill_missing=TRUE)
  2708. lc <- infer_light_cycle(df, zt0_hour = 6L) # prints median lights-on/off
  2709. df2 <- df %>%
  2710. dplyr::group_by(run_id) %>%
  2711. dplyr::group_modify(\(d, key) remove_dead(d, inactivity_hours = 12, quiet = F)$data) %>%
  2712. dplyr::ungroup()
  2713. ####zt average plots####
  2714. plot_average_day(
  2715. df2, metric = "activity", bin_minutes = 5,
  2716. color_map = damcolors,
  2717. genotype_labels = genotype_labels,
  2718. mean_linewidth = 0.8, sem_alpha = 0.25,
  2719. title = "Average Activity", y_label = "Counts / min", x_label = "CT",
  2720. title_size = 26, axis_title_size = 20, axis_text_size = 14,
  2721. show_bg_phase_shading = F,
  2722. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2723. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2724. show_phase_bar = T, phase_bar_position = "below", bar_height_frac = 0.04,
  2725. bar_light_color = "#22313F", bar_dark_color = "#22313F", show_legend = F,
  2726. outdir = outdamdir, filename = "avgday_activity1.png",
  2727. width_in = 5, height_in = 4
  2728. )
  2729. # Sleep: turn off phase bar, keep background shading
  2730. plot_average_day(
  2731. df2, metric = "sleep", bin_minutes = 5,
  2732. genotype_labels = genotype_labels,
  2733. color_map = damcolors,
  2734. mean_linewidth = 0.8, sem_alpha = 0.25,
  2735. title = "Average Sleep", y_label = "Sleep probability/ min", x_label = "CT",
  2736. title_size = 26, axis_title_size = 20, axis_text_size = 14,show_legend = F,
  2737. show_bg_phase_shading = F,
  2738. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2739. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2740. show_phase_bar = T, bar_height_frac = 0.04, phase_bar_position="below",
  2741. bar_light_color = "#22313F", bar_dark_color = "#22313F",
  2742. outdir = outdamdir, filename = "avgday_sleep1.png",
  2743. width_in = 5, height_in = 4
  2744. )
  2745. ####boxplots+stats####
  2746. res_act <- plot_daynight_metric_box(
  2747. df2,
  2748. metric = "activity",
  2749. zt0_hour = 6L,
  2750. outdir = outdamdir,
  2751. color_map = damcolors,
  2752. genotype_labels = genotype_labels,
  2753. show_kw_stars = FALSE,
  2754. show_brackets = TRUE,
  2755. show_legend =F,
  2756. drop_ns = TRUE,
  2757. width_in = 4,
  2758. dot_size = 1,
  2759. height_in = 4
  2760. )
  2761. # Sleep boxplots + stats
  2762. res_slp <- plot_daynight_metric_box(
  2763. df2,
  2764. zt0_hour = 6L,
  2765. metric = "sleep",
  2766. sleep_block_min = 5,
  2767. outdir = outdamdir,
  2768. color_map = damcolors,
  2769. genotype_labels = genotype_labels,
  2770. title = "Sleep",
  2771. y_label = "Sleep (Minutes per day)",
  2772. show_legend = F,
  2773. show_dots = TRUE,
  2774. dot_size = 1,
  2775. width_in = 4, height_in = 4, dpi = 300
  2776. )
  2777. ####period####
  2778. per <- analyze_periodicity(df2, method = "acf", bin_minutes = 5, detrend = "loess")
  2779. plot_period_box(per, outdir = outdamdir, color_map = damcolors, genotype_labels = genotype_labels,
  2780. mutant= "CG_GAL4_pex5", controls = c("pex5","CG_GAL4"), show_legend=F, width_in = 4, height_in = 4)
  2781. #
  2782. ####bouts 77a03 dd####
  2783. res_sleep_bouts_n <- plot_daynight_bout_count_box(
  2784. df2,
  2785. metric = "sleep",
  2786. zt0_hour = 6L,
  2787. sleep_block_min = 5L,
  2788. outdir = outdamdir,
  2789. filename = "sleep_bout_count.png",
  2790. color_map = damcolors,
  2791. genotype_labels = genotype_labels,
  2792. mutant = "CG_GAL4_pex5",
  2793. controls = c("pex5","CG_GAL4"),
  2794. show_legend = FALSE,
  2795. dot_size = 1,
  2796. width_in = 4, height_in = 4
  2797. )
  2798. res_sleep_boutdur <- plot_daynight_bout_duration_box(
  2799. df2,
  2800. metric = "sleep",
  2801. zt0_hour = 6L,
  2802. sleep_block_min = 5L,
  2803. outdir = outdamdir,
  2804. filename = "sleep_bout_duration.png",
  2805. color_map = damcolors,
  2806. genotype_labels = genotype_labels,
  2807. mutant = "CG_GAL4_pex5",
  2808. controls = c("pex5","CG_GAL4"),
  2809. show_legend = FALSE,
  2810. dot_size = 1,
  2811. width_in = 4, height_in = 4
  2812. )
  2813. res_act_bouts_n <- plot_daynight_bout_count_box(
  2814. df2,
  2815. metric = "activity",
  2816. activity_threshold = 0L, # active minute = count>0
  2817. zt0_hour = 6L,
  2818. outdir = outdamdir,
  2819. filename = "activity_bout_count.png",
  2820. color_map = damcolors,
  2821. genotype_labels = genotype_labels,
  2822. mutant = "CG_GAL4_pex5",
  2823. controls = c("pex5","CG_GAL4"),
  2824. show_legend = FALSE,
  2825. dot_size = 1,
  2826. width_in = 4, height_in = 4
  2827. )
  2828. res_latency <- plot_sleep_latency_box(
  2829. df2,
  2830. zt0_hour = 6L,
  2831. sleep_block_min = 5L,
  2832. outdir = outdamdir,
  2833. filename = "sleep_latency.png",
  2834. color_map = damcolors,
  2835. genotype_labels = genotype_labels,
  2836. mutant = "CG_GAL4_pex5",
  2837. controls = c("pex5","CG_GAL4"),
  2838. show_legend = FALSE,
  2839. dot_size = 1,
  2840. width_in = 4, height_in = 4
  2841. )
  2842. #### Export metrics — GMR77A03 DD ####
  2843. export_metrics_xlsx(
  2844. outdamdir = outdamdir,
  2845. res_act = res_act,
  2846. res_slp = res_slp,
  2847. res_sleep_bouts_n = res_sleep_bouts_n,
  2848. res_sleep_boutdur = res_sleep_boutdur,
  2849. res_act_bouts_n = res_act_bouts_n,
  2850. res_latency = res_latency,
  2851. per = per,
  2852. filename = "S1_Data.xlsx"
  2853. )
  2854. ####GMR57C10 nsyb####
  2855. #### read file, purge dead flies####
  2856. setwd("/Users/jvaughen/Dropbox/Anurag_MS/sleep/")
  2857. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR57C10/Graphs/"
  2858. #this caused r nearly to crash- need to find smarter way to trim brefore combining 32 tubes across that many timepoints (and we oversampled..)
  2859. #damcolors<-c(gba1b= "#AA4499", control = "#44AA99")
  2860. damcolors <- c(
  2861. pex5 = "#1B7837",
  2862. nSyb_GAL4 = "#2166AC",
  2863. nSyb_GAL4_pex5 = "magenta3"
  2864. )
  2865. genotype_labels <- c(
  2866. pex5 = "italic(Pex5^{KD})",
  2867. nSyb_GAL4 = "italic(nSyb *'>')", # or whatever you want this called
  2868. nSyb_GAL4_pex5 = "italic(nSyb *'>'*Pex5^{KD})"
  2869. )
  2870. #printing legend
  2871. df_leg <- data.frame(
  2872. x = 1:3,
  2873. y = 1,
  2874. genotype = factor(names(damcolors), levels = names(damcolors))
  2875. )
  2876. p_leg <- ggplot(df_leg, aes(x, y, fill = genotype)) +
  2877. geom_col(width = 0.6) +
  2878. scale_fill_manual(
  2879. values = damcolors,
  2880. breaks = names(damcolors),
  2881. labels = parse(text = genotype_labels[names(damcolors)])
  2882. ) +
  2883. guides(
  2884. fill = guide_legend(
  2885. title = NULL,
  2886. label = label_parsed
  2887. )
  2888. ) +
  2889. theme_void() +
  2890. theme(
  2891. legend.position = "right",
  2892. legend.text = element_text(size = 14),
  2893. legend.title = element_blank()
  2894. )
  2895. # Extract legend
  2896. leg <- cowplot::get_legend(p_leg)
  2897. # Save with WHITE background
  2898. png(
  2899. filename = "GMR57C10/DAM_legend.png",
  2900. width = 700,
  2901. height = 300,
  2902. res = 300,
  2903. bg = "white" # <-- this is the key line
  2904. )
  2905. grid.newpage()
  2906. grid.draw(leg)
  2907. dev.off()
  2908. specs <- list(
  2909. list(path="GMR57C10/April16-20.txt", run_id="April16-20", date_start="2025-04-16", date_end="2025-04-20",
  2910. genotype="nSyb_GAL4_pex5", tubes=1:10),
  2911. list(path="GMR57C10/April16-20.txt", run_id="April16-20", date_start="2025-04-16", date_end="2025-04-20",
  2912. genotype="pex5", tubes=11:21),
  2913. list(path="GMR57C10/April16-20.txt", run_id="April16-20", date_start="2025-04-16", date_end="2025-04-20",
  2914. genotype="nSyb_GAL4", tubes=22:32)
  2915. )
  2916. df <- read_dam_specs(specs, fill_missing=TRUE)
  2917. lc <- infer_light_cycle(df) # prints median lights-on/off
  2918. df2 <- df %>%
  2919. dplyr::group_by(run_id) %>%
  2920. dplyr::group_modify(\(d, key) remove_dead(d, inactivity_hours = 12, quiet = F)$data) %>%
  2921. dplyr::ungroup()
  2922. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR57C10/Graphs/"
  2923. ####zt average plots####
  2924. plot_average_day(
  2925. df2, metric = "activity", bin_minutes = 5,
  2926. color_map = damcolors,
  2927. genotype_labels = genotype_labels,
  2928. mean_linewidth = 0.8, sem_alpha = 0.25,
  2929. title = "Average Activity", y_label = "Counts / min", x_label = "ZT",
  2930. title_size = 26, axis_title_size = 20, axis_text_size = 14,
  2931. show_bg_phase_shading = F,
  2932. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2933. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2934. show_phase_bar = T, phase_bar_position = "below", bar_height_frac = 0.04,
  2935. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F", show_legend = F,
  2936. outdir = outdamdir, filename = "avgday_activity1.png",
  2937. width_in = 5, height_in = 4
  2938. )
  2939. # Sleep: turn off phase bar, keep background shading
  2940. plot_average_day(
  2941. df2, metric = "sleep", bin_minutes = 5,
  2942. genotype_labels = genotype_labels,
  2943. color_map = damcolors,
  2944. mean_linewidth = 0.8, sem_alpha = 0.25,
  2945. title = "Average Sleep", y_label = "Sleep probability/ min", x_label = "ZT",
  2946. title_size = 26, axis_title_size = 20, axis_text_size = 14,show_legend = F,
  2947. show_bg_phase_shading = F,
  2948. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  2949. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  2950. show_phase_bar = T, bar_height_frac = 0.04, phase_bar_position="below",
  2951. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F",
  2952. outdir = outdamdir, filename = "avgday_sleep1.png",
  2953. width_in = 5, height_in = 4
  2954. )
  2955. ####boxplots+stats####
  2956. res_act <- plot_daynight_activity_box(
  2957. df2,
  2958. outdir = outdamdir,
  2959. color_map = damcolors,
  2960. genotype_labels = genotype_labels,
  2961. mutant = "nSyb_GAL4_pex5",
  2962. controls = c("pex5","nSyb_GAL4"),
  2963. show_kw_stars = FALSE,
  2964. show_brackets = TRUE,
  2965. show_legend =F,
  2966. drop_ns = TRUE,
  2967. width_in = 4,
  2968. dot_size = 1,
  2969. height_in = 4
  2970. )
  2971. # Sleep boxplots + stats
  2972. res_slp <- plot_daynight_sleep_box(
  2973. df2,
  2974. mutant = "nSyb_GAL4_pex5",
  2975. controls = c("pex5","nSyb_GAL4"),
  2976. sleep_block_min = 5,
  2977. outdir = outdamdir,
  2978. color_map = damcolors,
  2979. genotype_labels = genotype_labels,
  2980. title = "Sleep",
  2981. y_label = "Sleep (Minutes per day)",
  2982. show_legend = F,
  2983. show_dots = TRUE,
  2984. dot_size = 1,
  2985. width_in = 4, height_in = 4, dpi = 300
  2986. )
  2987. ####period + bouts GMR57C10####
  2988. per <- analyze_periodicity(df2, method = "acf", bin_minutes = 5, detrend = "loess")
  2989. plot_period_box(per, outdir = outdamdir, color_map = damcolors, genotype_labels = genotype_labels,
  2990. mutant= "nSyb_GAL4_pex5", controls = c("pex5","nSyb_GAL4"), show_legend=F, width_in = 4, height_in = 4)
  2991. ####bouts nsyb####
  2992. res_sleep_bouts_n <- plot_daynight_bout_count_box(
  2993. df2,
  2994. metric = "sleep",
  2995. zt0_hour = 6L,
  2996. sleep_block_min = 5L,
  2997. outdir = outdamdir,
  2998. filename = "sleep_bout_count.png",
  2999. color_map = damcolors,
  3000. genotype_labels = genotype_labels,
  3001. mutant = "nSyb_GAL4_pex5",
  3002. controls = c("pex5","nSyb_GAL4"),
  3003. show_legend = FALSE,
  3004. dot_size = 1,
  3005. width_in = 4, height_in = 4
  3006. )
  3007. res_sleep_boutdur <- plot_daynight_bout_duration_box(
  3008. df2,
  3009. metric = "sleep",
  3010. zt0_hour = 6L,
  3011. sleep_block_min = 5L,
  3012. outdir = outdamdir,
  3013. filename = "sleep_bout_duration.png",
  3014. color_map = damcolors,
  3015. genotype_labels = genotype_labels,
  3016. mutant = "nSyb_GAL4_pex5",
  3017. controls = c("pex5","nSyb_GAL4"),
  3018. show_legend = FALSE,
  3019. dot_size = 1,
  3020. width_in = 4, height_in = 4
  3021. )
  3022. res_act_bouts_n <- plot_daynight_bout_count_box(
  3023. df2,
  3024. metric = "activity",
  3025. activity_threshold = 0L, # active minute = count>0
  3026. zt0_hour = 6L,
  3027. outdir = outdamdir,
  3028. filename = "activity_bout_count.png",
  3029. color_map = damcolors,
  3030. genotype_labels = genotype_labels,
  3031. mutant = "nSyb_GAL4_pex5",
  3032. controls = c("pex5","nSyb_GAL4"),
  3033. show_legend = FALSE,
  3034. dot_size = 1,
  3035. width_in = 4, height_in = 4
  3036. )
  3037. res_latency <- plot_sleep_latency_box(
  3038. df2,
  3039. zt0_hour = 6L,
  3040. sleep_block_min = 5L,
  3041. outdir = outdamdir,
  3042. filename = "sleep_latency.png",
  3043. color_map = damcolors,
  3044. genotype_labels = genotype_labels,
  3045. mutant = "nSyb_GAL4_pex5",
  3046. controls = c("pex5","nSyb_GAL4"),
  3047. show_legend = FALSE,
  3048. dot_size = 1,
  3049. width_in = 4, height_in = 4
  3050. )
  3051. #### Export metrics — GMR57C10 (nSyb) ####
  3052. export_metrics_xlsx(
  3053. outdamdir = outdamdir,
  3054. res_act = res_act,
  3055. res_slp = res_slp,
  3056. res_sleep_bouts_n = res_sleep_bouts_n,
  3057. res_sleep_boutdur = res_sleep_boutdur,
  3058. res_act_bouts_n = res_act_bouts_n,
  3059. res_latency = res_latency,
  3060. per = per,
  3061. filename = "S1_Data.xlsx"
  3062. )
  3063. ####GMR56F03 (EG-GAL4)####
  3064. #### read file, purge dead flies####
  3065. #setwd("/Users/jvaughen/Dropbox/sapr/data/Dam/")
  3066. setwd("/Users/jvaughen/Dropbox/Anurag_MS/sleep/")
  3067. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR56F03/Graphs/"
  3068. #this caused r nearly to crash- need to find smarter way to trim brefore combining 32 tubes across that many timepoints (and we oversampled..)
  3069. #damcolors<-c(gba1b= "#AA4499", control = "#44AA99")
  3070. damcolors <- c(
  3071. pex5 = "#1B7837",
  3072. EG_GAL4 = "#2166AC",
  3073. EG_GAL4_pex5 = "magenta3"
  3074. )
  3075. genotype_labels <- c(
  3076. pex5 = "italic(Pex5^{KD})",
  3077. EG_GAL4 = "italic(EG *'>')", # or whatever you want this called
  3078. EG_GAL4_pex5 = "italic(EG *'>'*Pex5^{KD})"
  3079. )
  3080. #printing legend
  3081. df_leg <- data.frame(
  3082. x = 1:3,
  3083. y = 1,
  3084. genotype = factor(names(damcolors), levels = names(damcolors))
  3085. )
  3086. p_leg <- ggplot(df_leg, aes(x, y, fill = genotype)) +
  3087. geom_col(width = 0.6) +
  3088. scale_fill_manual(
  3089. values = damcolors,
  3090. breaks = names(damcolors),
  3091. labels = parse(text = genotype_labels[names(damcolors)])
  3092. ) +
  3093. guides(
  3094. fill = guide_legend(
  3095. title = NULL,
  3096. label = label_parsed
  3097. )
  3098. ) +
  3099. theme_void() +
  3100. theme(
  3101. legend.position = "right",
  3102. legend.text = element_text(size = 14),
  3103. legend.title = element_blank()
  3104. )
  3105. # Extract legend
  3106. leg <- cowplot::get_legend(p_leg)
  3107. # Save with WHITE background
  3108. png(
  3109. filename = "GMR56F03/DAM_legend.png",
  3110. width = 700,
  3111. height = 300,
  3112. res = 300,
  3113. bg = "white" # <-- this is the key line
  3114. )
  3115. grid.newpage()
  3116. grid.draw(leg)
  3117. dev.off()
  3118. specs <- list(
  3119. list(path="GMR56F03/May8-11.txt", run_id="May8-11", date_start="2025-05-08", date_end="2025-05-11",
  3120. genotype="EG_GAL4_pex5", tubes=1:10),
  3121. list(path="GMR56F03/May8-11.txt", run_id="May8-11", date_start="2025-05-08", date_end="2025-05-11",
  3122. genotype="pex5", tubes=11:21),
  3123. list(path="GMR56F03/May8-11.txt", run_id="May8-11", date_start="2025-05-08", date_end="2025-05-11",
  3124. genotype="EG_GAL4", tubes=22:32)
  3125. )
  3126. df <- read_dam_specs(specs, fill_missing=TRUE)
  3127. lc <- infer_light_cycle(df) # prints median lights-on/off
  3128. df2 <- df %>%
  3129. dplyr::group_by(run_id) %>%
  3130. dplyr::group_modify(\(d, key) remove_dead(d, inactivity_hours = 12, quiet = F)$data) %>%
  3131. dplyr::ungroup()
  3132. outdamdir<-"/Users/jvaughen/Dropbox/Anurag_MS/sleep/GMR56F03/Graphs/"
  3133. ####zt average plots####
  3134. plot_average_day(
  3135. df2, metric = "activity", bin_minutes = 5,
  3136. color_map = damcolors,
  3137. genotype_labels = genotype_labels,
  3138. mean_linewidth = 0.8, sem_alpha = 0.25,
  3139. title = "Average Activity", y_label = "Counts / min", x_label = "ZT",
  3140. title_size = 26, axis_title_size = 20, axis_text_size = 14,
  3141. show_bg_phase_shading = F,
  3142. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  3143. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  3144. show_phase_bar = T, phase_bar_position = "below", bar_height_frac = 0.04,
  3145. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F", show_legend = F,
  3146. outdir = outdamdir, filename = "avgday_activity1.png",
  3147. width_in = 5, height_in = 4
  3148. )
  3149. # Sleep: turn off phase bar, keep background shading
  3150. plot_average_day(
  3151. df2, metric = "sleep", bin_minutes = 5,
  3152. genotype_labels = genotype_labels,
  3153. color_map = damcolors,
  3154. mean_linewidth = 0.8, sem_alpha = 0.25,
  3155. title = "Average Sleep", y_label = "Sleep probability/ min", x_label = "ZT",
  3156. title_size = 26, axis_title_size = 20, axis_text_size = 14,show_legend = F,
  3157. show_bg_phase_shading = F,
  3158. bg_light_color = "#FFF7AE", bg_light_alpha = 0.22,
  3159. bg_dark_color = "#1E2430", bg_dark_alpha = 0.22,
  3160. show_phase_bar = T, bar_height_frac = 0.04, phase_bar_position="below",
  3161. bar_light_color = "#FFF7AE", bar_dark_color = "#22313F",
  3162. outdir = outdamdir, filename = "avgday_sleep1.png",
  3163. width_in = 5, height_in = 4
  3164. )
  3165. ####boxplots+stats####
  3166. res_act <- plot_daynight_activity_box(
  3167. df2,
  3168. outdir = outdamdir,
  3169. color_map = damcolors,
  3170. genotype_labels = genotype_labels,
  3171. mutant = "EG_GAL4_pex5",
  3172. controls = c("pex5","EG_GAL4"),
  3173. show_kw_stars = FALSE,
  3174. show_brackets = TRUE,
  3175. show_legend =F,
  3176. drop_ns = TRUE,
  3177. width_in = 4,
  3178. dot_size = 1,
  3179. height_in = 4
  3180. )
  3181. # Sleep boxplots + stats
  3182. res_slp <- plot_daynight_sleep_box(
  3183. df2,
  3184. mutant = "EG_GAL4_pex5",
  3185. controls = c("pex5","EG_GAL4"),
  3186. sleep_block_min = 5,
  3187. outdir = outdamdir,
  3188. color_map = damcolors,
  3189. genotype_labels = genotype_labels,
  3190. title = "Sleep",
  3191. y_label = "Sleep (Minutes per day)",
  3192. show_legend = F,
  3193. show_dots = TRUE,
  3194. dot_size = 1,
  3195. width_in = 4, height_in = 4, dpi = 300
  3196. )
  3197. ####circadian####
  3198. per <- analyze_periodicity(df2, method = "acf", bin_minutes = 5, detrend = "loess")
  3199. plot_period_box(per, outdir = outdamdir, color_map = damcolors, genotype_labels = genotype_labels,
  3200. mutant= "EG_GAL4_pex5", controls = c("pex5","EG_GAL4"), show_legend=F, width_in = 4, height_in = 4)
  3201. ####bouts####
  3202. res_sleep_bouts_n <- plot_daynight_bout_count_box(
  3203. df2,
  3204. metric = "sleep",
  3205. zt0_hour = 6L,
  3206. sleep_block_min = 5L,
  3207. outdir = outdamdir,
  3208. filename = "sleep_bout_count.png",
  3209. color_map = damcolors,
  3210. genotype_labels = genotype_labels,
  3211. mutant = "EG_GAL4_pex5",
  3212. controls = c("pex5","EG_GAL4"),
  3213. show_legend = FALSE,
  3214. dot_size = 1,
  3215. width_in = 4, height_in = 4
  3216. )
  3217. res_sleep_boutdur <- plot_daynight_bout_duration_box(
  3218. df2,
  3219. metric = "sleep",
  3220. zt0_hour = 6L,
  3221. sleep_block_min = 5L,
  3222. outdir = outdamdir,
  3223. filename = "sleep_bout_duration.png",
  3224. color_map = damcolors,
  3225. genotype_labels = genotype_labels,
  3226. mutant = "EG_GAL4_pex5",
  3227. controls = c("pex5","EG_GAL4"),
  3228. show_legend = FALSE,
  3229. dot_size = 1,
  3230. width_in = 4, height_in = 4
  3231. )
  3232. res_act_bouts_n <- plot_daynight_bout_count_box(
  3233. df2,
  3234. metric = "activity",
  3235. activity_threshold = 0L, # active minute = count>0
  3236. zt0_hour = 6L,
  3237. outdir = outdamdir,
  3238. filename = "activity_bout_count.png",
  3239. color_map = damcolors,
  3240. genotype_labels = genotype_labels,
  3241. mutant = "EG_GAL4_pex5",
  3242. controls = c("pex5","EG_GAL4"),
  3243. show_legend = FALSE,
  3244. dot_size = 1,
  3245. width_in = 4, height_in = 4
  3246. )
  3247. res_latency <- plot_sleep_latency_box(
  3248. df2,
  3249. zt0_hour = 6L,
  3250. sleep_block_min = 5L,
  3251. outdir = outdamdir,
  3252. filename = "sleep_latency.png",
  3253. color_map = damcolors,
  3254. genotype_labels = genotype_labels,
  3255. mutant = "EG_GAL4_pex5",
  3256. controls = c("pex5","EG_GAL4"),
  3257. show_legend = FALSE,
  3258. dot_size = 1,
  3259. width_in = 4, height_in = 4
  3260. )
  3261. #### Export metrics — GMR56F03 (EG-GAL4) ####
  3262. export_metrics_xlsx(
  3263. outdamdir = outdamdir,
  3264. res_act = res_act,
  3265. res_slp = res_slp,
  3266. res_sleep_bouts_n = res_sleep_bouts_n,
  3267. res_sleep_boutdur = res_sleep_boutdur,
  3268. res_act_bouts_n = res_act_bouts_n,
  3269. res_latency = res_latency,
  3270. per = per,
  3271. filename = "S1_Data.xlsx"
  3272. )

DAM_2026_Das_metrics.R at commit a60091d, no license · at the source

Overview

Authors: Anurag Das1,2, Irma Magaly Rivas-Serna3, Ankur Kumar1,4, Lakpa Sherpa1,4, Kerui Huang1, Hia Kalita5, Marlene Dorneich-Hayes1,4, Ruiqi Liu1,5, Vera C Mazurak3, John P Vaughen6, Hua Bai1
  1. Department of Genetics, Development, and Cell Biology, Iowa State University, Ames, Iowa, United States of America
  2. Interdepartmental Neuroscience PhD Program, Iowa State University, Ames, Iowa, United States of America
  3. Department of Agriculture, Food, and Nutritional Science, University of Alberta, Edmonton, Canada
  4. Interdepartmental Genetics & Genomics PhD Program, Iowa State University, Ames, Iowa, United States of America
  5. Interdepartmental MCDB PhD Program, Iowa State University, Ames, Iowa, United States of America
  6. Department of Anatomy, University of California San Francisco, San Francisco, California, United States of America
Institutions: Iowa State University (United States); University of Alberta (Canada); University of California, San Francisco (United States)
Journal: PLoS biology, volume 24, issue 7, article e3003901
Dates: received 28 March 2026; accepted 29 June 2026; published online 15 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pbio.3003901 · PMID 42455861 · PMCID PMC13387613 · OpenAlex W4411639224
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: drosophila (organism), cellular / molecular (subfield)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Evoked potentials, Connectivity, fMRI & imaging
MeSH: Circadian Rhythm*, Lipid Metabolism*, Neuroglia*, Peroxisomes*, Sleep*, Animals, Brain, Circadian Clocks, Drosophila melanogaster, Drosophila Proteins, Neurons, Peroxisome-Targeting Signal 1 Receptor, Protein Transport (* major topic)
Topic: Circadian rhythm and melatonin (Endocrine and Autonomic Systems, Neuroscience), according to OpenAlex
Funding: NIH HHS (OT2 OD030544, P40 OD018537); National Science Foundation (NSF CAREER 2046984); National Institute on Aging (R01AG075156); NIDDK NIH HHS (U2C DK119886, U2C DK119889); NIA NIH HHS (R01 AG075156); Sandler Foundation; Simons Foundation; Hevolution (HF- GRO-23-1199062-14); NHGRI NIH HHS (U41 HG000739)
Citations: not cited yet (Europe PMC); 108 references in the paper

Abstract

Peroxisomes are critical organelles that detoxify cellular waste while also catabolizing and anabolizing lipids. How peroxisomes coordinate protein import and support metabolic functions across complex tissues and timescales remains poorly understood in vivo. Using the Drosophila brain, we discover a striking enrichment of peroxisomes in the neuronal soma and the cortex glia that enwrap them. Unexpectedly, import of peroxisomal proteins into cortex glia, but not neurons, oscillated across time and peaked in the early morning. Rhythmic peroxisomal import in cortex glia autonomously required the circadian clock and Peroxin 5 (Pex5; peroxisomal biogenesis factor 5 homolog), with import persistently elevated in clock mutants. Notably, reducing Pex5 in cortex glia, but not neurons, caused hyperactivity and reduced total sleep. Moreover, brain lipid metabolism was dramatically altered upon Pex5 knockdown, with glia impacting sphingolipids and triacylglycerols, and neurons impacting phospholipids. The cell-type specificity of these Pex5 phenotypes highlights unique roles for peroxisomal import in both sleep and lipid metabolism in the brain.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 1 match between paragraphs and lines of code.

Zenodo 20637355

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data Availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: cowplot (2 files), ggplot2 (2 files), tidyverse (2 files), broom (1 file), circlize (1 file), ComplexHeatmap (1 file), emmeans (1 file), ggpubr (1 file), patchwork (1 file), rstatix (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
3 files

jvaughen/das

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: a60091d58874746a4ea01594d3a18f26f69e6ddc, 10 June 2026
Languages: R (2)
Size: 14 files, 2 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: cowplot (2 files), ggplot2 (2 files), tidyverse (2 files), broom (1 file), circlize (1 file), ComplexHeatmap (1 file), emmeans (1 file), ggpubr (1 file), patchwork (1 file), rstatix (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 4 scripts, each with its path and the digest of its content;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability

Lipidomics raw data generated is available at the NIH Common Fund’s National Metabolomics Data Repository (NMDR) website, the Metabolomics Workbench, https://www.metabolomicsworkbench.org, where it has been assigned Project ID #7642. The data can be accessed directly via http://dx.doi.org/10.21228/M8XK2W. Code generated and processed lipidomics data (in ng/brain and rel % composition/brain) are accessible at https://zenodo.org/records/20637355. All other relevant data can be found within the manuscript and supplemental files.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Authors: added John P Vaughen (0000-0002-7141-1857); Hua Bai (0000-0003-2221-7545); removed John P Vaughen; Hua Bai

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 13 MeSH terms, 9 funders, 108 references, 21 RRIDs.

Cite

This paper

Das, A., Rivas-Serna, I. M., Kumar, A., Sherpa, L., Huang, K., Kalita, H., Dorneich-Hayes, M., Liu, R., Mazurak, V. C., Vaughen, J. P., & Bai, H. (2026). Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism. PLoS biology, 24(7), e3003901. https://doi.org/10.1371/journal.pbio.3003901

BibTeX

@article{das2026peroxisomal,
author = {Das, Anurag and Rivas-Serna, Irma Magaly and Kumar, Ankur and Sherpa, Lakpa and Huang, Kerui and Kalita, Hia and Dorneich-Hayes, Marlene and Liu, Ruiqi and Mazurak, Vera C and Vaughen, John P and Bai, Hua},
title = {{Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism}},
journal = {PLoS biology},
year = {2026},
month = jul,
volume = {24},
number = {7},
pages = {e3003901},
publisher = {PLOS},
issn = {1544-9173},
doi = {10.1371/journal.pbio.3003901},
url = {https://doi.org/10.1371/journal.pbio.3003901},
pmid = {42455861},
pmcid = {PMC13387613}
}

RIS

TY - JOUR
AU - Das, Anurag
AU - Rivas-Serna, Irma Magaly
AU - Kumar, Ankur
AU - Sherpa, Lakpa
AU - Huang, Kerui
AU - Kalita, Hia
AU - Dorneich-Hayes, Marlene
AU - Liu, Ruiqi
AU - Mazurak, Vera C
AU - Vaughen, John P
AU - Bai, Hua
TI - Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism
T2 - PLoS biology
J2 - PLoS Biol
PY - 2026
DA - 2026/07/15
VL - 24
IS - 7
SP - e3003901
SN - 1544-9173
PB - PLOS
DO - 10.1371/journal.pbio.3003901
UR - https://doi.org/10.1371/journal.pbio.3003901
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pbio.3003901",
"type": "article-journal",
"title": "Peroxisomal import is circadian in glia and regulates sleep and lipid metabolism",
"container-title": "PLoS biology",
"author": [
{
"family": "Das",
"given": "Anurag"
},
{
"family": "Rivas-Serna",
"given": "Irma Magaly"
},
{
"family": "Kumar",
"given": "Ankur"
},
{
"family": "Sherpa",
"given": "Lakpa"
},
{
"family": "Huang",
"given": "Kerui"
},
{
"family": "Kalita",
"given": "Hia"
},
{
"family": "Dorneich-Hayes",
"given": "Marlene"
},
{
"family": "Liu",
"given": "Ruiqi"
},
{
"family": "Mazurak",
"given": "Vera C"
},
{
"family": "Vaughen",
"given": "John P"
},
{
"family": "Bai",
"given": "Hua"
}
],
"container-title-short": "PLoS Biol",
"volume": "24",
"issue": "7",
"page": "e3003901",
"DOI": "10.1371/journal.pbio.3003901",
"PMID": "42455861",
"PMCID": "PMC13387613",
"ISSN": "1544-9173",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pbio.3003901",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
15
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: rstatix, broom, circlize, 7 other tools
[2] doi:10.1073/pnas.2609132123 [code]
A human lysosomal storage disorder toolkit for decoding proteome landscapes in cortical-like and dopaminergic-like induced neurons.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: rstatix, broom, circlize, 6 other tools, cellular / molecular
[3] doi:10.1073/pnas.2605750123
Light- and temperature-sensitive seizures are regulated by spatially distinct cortex glial populations in the central nervous system.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: drosophila, cellular / molecular, 7 references
[4] doi:10.1038/s41467-026-74753-y [code]
A human-specific microRNA controls the timing of excitatory synaptogenesis.
Journal: Nature communications
In common: rstatix, circlize, emmeans, 6 other tools, cellular / molecular
[5] doi:10.1038/s41386-026-02406-1 [code]
Functional genomic profiling of schizophrenia-associated genes reveals key microglial regulators.
Journal: Neuropsychopharmacology : official publication of the American College of Neuropsychopharmacology
In common: broom, circlize, emmeans, 5 other tools, cellular / molecular
[6] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: rstatix, circlize, ComplexHeatmap, 5 other tools, cellular / molecular
[7] doi:10.1038/s41467-026-76232-w [code]
Th17 effector cytokines induce shared and distinct microglial and endothelial cell responses in a mouse model for post-streptococcal encephalitis.
Journal: Nature communications
In common: rstatix, circlize, ComplexHeatmap, 5 other tools, cellular / molecular
[8] doi:10.1038/s41514-026-00391-9 [code]
Region-specific transcriptional signatures of brain aging in the absence of neuropathology at the single-cell level.
Journal: npj aging
In common: broom, circlize, ComplexHeatmap, 5 other tools, cellular / molecular
[9] doi:10.1016/j.isci.2026.115573 [code]
Female cortical cellular mosaicism underlies shared MeCP2 and PCB impacted gene pathways.
Journal: iScience
In common: broom, circlize, ComplexHeatmap, 5 other tools, cellular / molecular
[10] doi:10.1016/j.xcrm.2026.102651 [code]
Integrative CSF profiling identifies disease-specific immune responses in leptomeningeal disease.
Journal: Cell reports. Medicine
In common: broom, circlize, ComplexHeatmap, 5 other tools, cellular / molecular

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.