Back to Article
Descriptives
Download Source

Descriptives

Individual, behavioural, and environmental determinants of personal light exposure in daily life: a multi-country wearable and experience-sampling study

Author

Johannes Zauner

Last modified:

April 17, 2026

Preface

This document contains descriptive plots and tables of the dataset.

Setup

In [1]:
Setup
library(tidyverse)
Warning: package 'ggplot2' was built under R version 4.5.2
Warning: package 'tibble' was built under R version 4.5.2
Warning: package 'tidyr' was built under R version 4.5.2
Warning: package 'readr' was built under R version 4.5.2
Warning: package 'purrr' was built under R version 4.5.2
Warning: package 'dplyr' was built under R version 4.5.2
Warning: package 'lubridate' was built under R version 4.5.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.2     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(melidosData)
library(LightLogR)
Warning: package 'LightLogR' was built under R version 4.5.2
library(magick)
Warning: package 'magick' was built under R version 4.5.2
Linking to ImageMagick 6.9.13.29
Enabled features: cairo, fontconfig, freetype, heic, lcms, pango, raw, rsvg, webp
Disabled features: fftw, ghostscript, x11
library(cowplot)

Attaching package: 'cowplot'

The following object is masked from 'package:lubridate':

    stamp
library(ggridges)
library(gt)
Warning: package 'gt' was built under R version 4.5.2

Attaching package: 'gt'

The following object is masked from 'package:cowplot':

    as_gtable
library(rnaturalearth)
Warning: package 'rnaturalearth' was built under R version 4.5.2
library(rnaturalearthdata)

Attaching package: 'rnaturalearthdata'

The following object is masked from 'package:rnaturalearth':

    countries110
library(legendry)
library(ggrepel)
Warning: package 'ggrepel' was built under R version 4.5.2
library(sf)
Warning: package 'sf' was built under R version 4.5.2
Linking to GEOS 3.13.0, GDAL 3.8.5, PROJ 9.5.1; sf_use_s2() is TRUE
library(patchwork)

Attaching package: 'patchwork'

The following object is masked from 'package:cowplot':

    align_plots
library(glue)
library(rlang)
Warning: package 'rlang' was built under R version 4.5.2

Attaching package: 'rlang'

The following objects are masked from 'package:purrr':

    flatten, flatten_chr, flatten_dbl, flatten_int, flatten_lgl,
    flatten_raw, invoke, splice
library(gtsummary)
Warning: package 'gtsummary' was built under R version 4.5.2
source("scripts/helpers.R")
source("scripts/Fig2.R")
source("scripts/Fig3.R")

Load data

In [2]:
load("data/metrics_glasses.RData")
load("data/metrics_chest.RData")
demographics <- load_data("demographics") |> flatten_data()
chronotype <- load_data("chronotype") |> flatten_data()
sleep <- load_data("sleepdiaries") |> flatten_data()
Warning in flatten_data(load_data("sleepdiaries")): Not all labels across all
sites were identical. Labels used from: BAUA; sites that have other labels:
RISE
wearlog <- load_data("wearlog") |> flatten_data()
Warning in flatten_data(load_data("wearlog")): Not all labels across all sites
were identical. Labels used from: BAUA; sites that have other labels: MPI
load("data/preprocessed_glasses_1.RData")
load("data/preprocessed_chest_1.RData")
load("data/preprocessed_glasses_2.RData")
load("data/preprocessed_chest_2.RData")

Table 1: Participant and site characteristics

Prepare data

Site and city

In [3]:
tbl1_country <-
  melidos_countries |> 
  enframe("site", "country")

tbl1_city <-
  melidos_cities |> 
  enframe("site", "city")

Participant N

In [4]:
tbl1_partN <-
  demographics |> 
    count(site, name = "partN") |> 
    add_row(site = "Overall", demographics |> count(name = "partN"))

glasses_partN <- 
  light_glasses_processed1 |> summarize(Id = unique(Id), .groups = "drop")

glasses_partN <-
  glasses_partN |> 
    count(site, name = "partN_glasses") |> 
    add_row(site = "Overall", glasses_partN |> count(name = "partN_glasses"))

chest_partN <- 
  light_chest_processed1 |> summarize(Id = unique(Id), .groups = "drop")

chest_partN <-
  chest_partN |> 
    count(site, name = "partN_chest") |> 
    add_row(site = "Overall", chest_partN |> count(name = "partN_chest"))

tbl1_partN <- 
tbl1_partN |> 
  left_join(glasses_partN, by = "site") |> 
  left_join(chest_partN, by = "site") |> 
    replace_na(list(partN_chest = 0))

Participant-days N

In [5]:
glasses_daysN <- 
  light_glasses_processed1 |> 
  add_Date_col(group.by = TRUE) |>
  summarize(Date = unique(Date), .groups = "drop")

tbl1_daysN <-
  glasses_daysN

glasses_daysN <-
  glasses_daysN |> 
    count(site, name = "daysN_glasses") |> 
    add_row(site = "Overall", glasses_daysN |> count(name = "daysN_glasses"))

chest_daysN <- 
  light_chest_processed1  |> 
  add_Date_col(group.by = TRUE) |> 
  summarize(Date = unique(Date), .groups = "drop")

tbl1_daysN <-
  tbl1_daysN |> full_join(chest_daysN, by = names(tbl1_daysN))

chest_daysN <-
  chest_daysN |> 
    count(site, name = "daysN_chest") |> 
    add_row(site = "Overall", chest_daysN |> count(name = "daysN_chest"))

tbl1_daysN <- 
tbl1_daysN |> 
  count(site, name = "daysN") |> 
  add_row(site = "Overall", tbl1_daysN |> count(name = "daysN")) |> 
  left_join(glasses_daysN, by = "site") |> 
  left_join(chest_daysN, by = "site") |> 
    replace_na(list(daysN_chest = 0))

Weekend/weekdays N

In [6]:
glasses_weekdayN <-
  light_glasses_processed1 |> 
  add_Date_col(group.by = TRUE) |>
  mutate(Weekend = wday(Date, week_start = 1) >5) |> 
  distinct(site, Id, Date, Weekend) 

tbl1_weekdayN <-
  glasses_weekdayN

glasses_weekdayN <-
  glasses_weekdayN |> 
  ungroup(Date) |> 
  count(Weekend)|> 
    ungroup() |> 
    count(site, Weekend, name = "weekdayN_glasses", wt = n) |> 
    pivot_wider(id_cols = site, 
                names_from = Weekend, 
                values_from = weekdayN_glasses) |> 
    rename_with(\(x) x |> replace_values("FALSE" ~ "weekdayN_glasses", "TRUE" ~ "weekendN_glasses"))

glasses_weekdayN <-
  glasses_weekdayN |> 
    add_row(site = "Overall", glasses_weekdayN |> summarize(across(-site, sum)))

chest_weekdayN <-
  light_chest_processed1 |> 
  add_Date_col(group.by = TRUE) |>
  mutate(Weekend = wday(Date, week_start = 1) >5) |> 
  distinct(site, Id, Date, Weekend) 

tbl1_weekdayN <-
  tbl1_weekdayN |> full_join(chest_weekdayN, by = names(tbl1_weekdayN))

chest_weekdayN <-
  chest_weekdayN |> 
  ungroup(Date) |> 
  count(Weekend)|> 
    ungroup() |> 
    count(site, Weekend, name = "weekdayN_chest", wt = n) |> 
    pivot_wider(id_cols = site, 
                names_from = Weekend, 
                values_from = weekdayN_chest) |> 
    rename_with(\(x) x |> replace_values("FALSE" ~ "weekdayN_chest", "TRUE" ~ "weekendN_chest"))

chest_weekdayN <-
  chest_weekdayN |> 
    add_row(site = "Overall", chest_weekdayN |> summarize(across(-site, sum)))

tbl1_weekdayN <-
tbl1_weekdayN |> 
  ungroup(Date) |> 
  count(Weekend) |> 
  ungroup() |> 
  count(site, Weekend, name = "weekdayN", wt = n) |> 
  pivot_wider(id_cols = site, 
                names_from = Weekend, 
                values_from = weekdayN) |> 
    rename_with(\(x) x |> replace_values("FALSE" ~ "weekdayN", "TRUE" ~ "weekendN"))

tbl1_weekdayN <-
tbl1_weekdayN |> 
  add_row(site = "Overall", tbl1_weekdayN |> summarize(across(-site, sum))) |> 
  left_join(glasses_weekdayN, by = "site") |> 
  left_join(chest_weekdayN, by = "site") |> 
    replace_na(list(weekdayN_chest = 0))

Geolocation coordinates

In [7]:
tbl1_location <-
  melidos_coordinates |> 
  enframe("site", "coordinates") |> 
  mutate(coordinates = map(coordinates, format_coordinates) |> unlist())

Photoperiod

In [8]:
phot_glasses <- 
light_glasses_processed1 |> 
  add_Date_col(group.by = TRUE) |> 
  select(Id, Date, photoperiod) |> 
  distinct(site, Id, Date, .keep_all = TRUE)
Adding missing grouping variables: `site`
phot_chest <- 
light_chest_processed1 |> 
  add_Date_col(group.by = TRUE) |> 
  select(Id, Date, photoperiod) |> 
  distinct(site, Id, Date, .keep_all = TRUE)
Adding missing grouping variables: `site`
phot_dates <- 
  full_join(
    phot_glasses,
    phot_chest,
    by = names(phot_glasses)
  )

tbl1_photoperiod <-
  phot_dates |> 
  ungroup(Date, Id) |> 
  summarize(
    phot = median(photoperiod),
    phot_p05 = quantile(photoperiod, p=0.05),
    phot_p95 = quantile(photoperiod, p=0.95)
  ) |> 
  rbind(phot_dates |> 
  ungroup() |> 
  summarize(
    site = "Overall",
    phot = median(photoperiod),
    phot_p05 = quantile(photoperiod, p=0.05),
    phot_p95 = quantile(photoperiod, p=0.95)
  ))

Data collection dates

In [9]:
tbl1_dates <- 
phot_dates |> 
  ungroup(Id, Date) |> 
  summarize(date_median = median(Date),
            date_min = min(Date),
            date_max = max(Date)
            ) |> 
  rbind(phot_dates |> 
  ungroup() |> 
  summarize(
    site = "Overall",
    date_median = median(Date),
            date_min = min(Date),
            date_max = max(Date)
  ))

Age, gender, employment status, chronotype

In [10]:
partial_demographics <- 
demographics |> 
  left_join(chronotype |> select(Id, meq_type, meq, msf_sc, sjl), by = "Id") |> 
  mutate(across(c(sex, gender), forcats::fct_drop),
         meq_type = meq_type |> 
           fct_collapse(
             Morning = c("Definitely morning type", "Moderately morning type"),
             Evening = c("Definitely evening type", "Moderately evening type"),
                        ),
         employment_status = employment_status |> 
           fct_collapse(
             `Fully` = c("Full time employed", "Not employed but studying or in training", "Studying and employed"),
             `Partially` = c("Part time employed", "Marginally employed (Minijob)"),
             None = c("Not employed")
                        )
         ) |> 
  select(-c(Id, native_language, language_specification, comments))

factor_vars <- c("sex", "gender", "employment_status", "meq_type")

tbl1_demographics <-
  partial_demographics |> 
  reframe(
    .by = site,
    age_median = median(age) |> round(),
    age_min = min(age),
    age_max = max(age),
    across(c(meq),
           .fns = list(
             median = ~median(.x, na.rm =TRUE)|> round(),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE)|> round(),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)|> round()
           )), 
    across(c(sjl, msf_sc),
           .fns = list(
             median = ~median(.x, na.rm =TRUE),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)
           )), 
      across(all_of(factor_vars),
             \(x) {
               column <- cur_column()
               count(cur_data(), pick(column)) |> 
                 pivot_wider(names_from = cur_column(), 
                             values_from = "n", 
                             names_prefix = paste0(cur_column(), "_"))
             })
  ) |> 
  rbind(
    partial_demographics |> 
    reframe(
    site = "Overall",
    age_median = median(age) |> round(),
    age_min = min(age),
    age_max = max(age),
    across(c(meq),
           .fns = list(
             median = ~median(.x, na.rm =TRUE)|> round(),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE)|> round(),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)|> round()
           )), 
    across(c(sjl, msf_sc),
           .fns = list(
             median = ~median(.x, na.rm =TRUE),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)
           )), 
      across(all_of(factor_vars),
             \(x) {
               column <- cur_column()
               count(cur_data(), pick(column)) |> 
                 pivot_wider(names_from = cur_column(), 
                             values_from = "n", 
                             names_prefix = paste0(cur_column(), "_"))
             })
  )) |> 
  unnest(all_of(factor_vars))
Warning: There were 2 warnings in `reframe()`.
The first warning was:
ℹ In argument: `across(...)`.
ℹ In group 1: `site = "BAUA"`.
Caused by warning:
! `cur_data()` was deprecated in dplyr 1.1.0.
ℹ Please use `pick()` instead.
ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.

Sleep duration

In [11]:
tbl1_sleep <- 
sleep |> 
  mutate(daytype = daytype2 |> fct_relabel(\(x) x |> str_extract("work|free"))) |> 
  select(site, sleep_duration, daytype) |> 
  summarize(
    across(where(is.difftime),
           .fns = list(
             median = ~median(.x, na.rm =TRUE),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)
           )), 
      across(where(is.factor),
             \(x) {
               column <- cur_column()
               count(cur_data(), pick(column)) |> 
                 pivot_wider(names_from = cur_column(), 
                             values_from = "n", 
                             names_prefix = paste0(cur_column(), "_"))
             }),
    .by = site
  ) |> 
  rbind(
    sleep |> 
  mutate(daytype = daytype2 |> fct_relabel(\(x) x |> str_extract("work|free"))) |> 
  select(site, sleep_duration, daytype) |> 
  summarize(
    site = "Overall",
    across(where(is.difftime),
           .fns = list(
             median = ~median(.x, na.rm =TRUE),
             p05 = ~quantile(.x, p=0.05, na.rm = TRUE),
             p95 = ~quantile(.x, p=0.95, na.rm = TRUE)
           )), 
      across(where(is.factor),
             \(x) {
               column <- cur_column()
               count(cur_data(), pick(column)) |> 
                 pivot_wider(names_from = cur_column(), 
                             values_from = "n", 
                             names_prefix = paste0(cur_column(), "_"))
             })
  )
  ) |> 
  unnest(where(is_tibble))

Non-wear time

In [12]:
tbl1_wear <- 
light_glasses_processed1 |> 
  rbind(light_glasses_processed1) |>
  filter(!is.implicit) |>
  distinct(site, Id, Datetime, .keep_all = TRUE) |>
  group_by(site, Id, nonwear = wear == "off" & sleep != "sleepprep") |>
  mutate(
    nonwear = replace_na(nonwear, FALSE)
  ) |>
  durations() |>  
  pivot_wider(id_cols = c(site, Id), names_from = "nonwear", values_from = duration) |> 
  rowwise() |> 
  mutate(total = sum(dplyr::c_across(c(`FALSE`,`TRUE`)), na.rm = TRUE) |> as.duration()) |> 
  group_by(site) |> 
  summarize(
    nonwear = sum(`TRUE`, na.rm = TRUE) |> as.duration(),
    total = sum(total, na.rm = TRUE) |> as.duration(),
    nonwear_pct = nonwear/total
  ) |> 
  rbind(
    light_glasses_processed1 |> 
  rbind(light_glasses_processed1) |>
  filter(!is.implicit) |>
  distinct(site, Id, Datetime, .keep_all = TRUE) |>
  group_by(site, Id, nonwear = wear == "off" & sleep != "sleepprep") |>
  mutate(
    nonwear = replace_na(nonwear, FALSE)
  ) |>
  durations() |>  
  pivot_wider(id_cols = c(site, Id), names_from = "nonwear", values_from = duration) |> 
  rowwise() |> 
  mutate(total = sum(dplyr::c_across(c(`FALSE`,`TRUE`)), na.rm = TRUE) |> as.duration()) |> 
  ungroup() |> 
  summarize(
    site = "Overall",
    nonwear = sum(`TRUE`, na.rm = TRUE) |> as.duration(),
    total = sum(total, na.rm = TRUE) |> as.duration(),
    nonwear_pct = nonwear/total
  )
  )

Days included in calculations

In [13]:
glasses_validdaysN <- 
  light_glasses_processed2 |> 
  add_Date_col(group.by = TRUE) |>
  summarize(Date = unique(Date), .groups = "drop")

tbl1_validdaysN <-
  glasses_validdaysN

glasses_validdaysN <-
  glasses_validdaysN |> 
    count(site, name = "validdaysN_glasses") |> 
    add_row(site = "Overall", glasses_validdaysN |> count(name = "validdaysN_glasses"))

chest_validdaysN <- 
  light_chest_processed1  |> 
  add_Date_col(group.by = TRUE) |> 
  summarize(Date = unique(Date), .groups = "drop")

tbl1_validdaysN <-
  tbl1_validdaysN |> full_join(chest_validdaysN, by = names(tbl1_validdaysN))

chest_validdaysN <-
  chest_validdaysN |> 
    count(site, name = "validdaysN_chest") |> 
    add_row(site = "Overall", chest_validdaysN |> count(name = "validdaysN_chest"))

tbl1_validdaysN <- 
tbl1_validdaysN |> 
  count(site, name = "validdaysN") |> 
  add_row(site = "Overall", tbl1_validdaysN |> count(name = "validdaysN")) |> 
  left_join(glasses_validdaysN, by = "site") |> 
  left_join(chest_validdaysN, by = "site") |> 
    replace_na(list(validdaysN_chest = 0))

Combine data

In [14]:
tbl1_data <-
  tbl1_country |> 
  add_row(site = "Overall") |> 
    left_join(by = "site", tbl1_city) |> 
    left_join(by = "site", tbl1_location) |> 
    left_join(by = "site", tbl1_partN) |> 
    left_join(by = "site", tbl1_daysN) |> 
    left_join(by = "site", tbl1_weekdayN) |> 
    left_join(by = "site", tbl1_wear) |> 
    left_join(by = "site", tbl1_validdaysN) |> 
    left_join(by = "site", tbl1_dates) |> 
    left_join(by = "site", tbl1_photoperiod) |> 
    left_join(by = "site", tbl1_demographics) |> 
    left_join(by = "site", tbl1_sleep)
In [15]:
tbl1_data_formatted <- 
tbl1_data |> 
  gt() |> 
  cols_merge(starts_with("partN"), pattern = "{1}<br>(**g:{2}**, c:{3})") |> 
  cols_merge(starts_with("daysN"), pattern = "{1}d<br>(**g:{2}d**, c:{3}d)") |> 
  cols_merge(starts_with("date_"), pattern = "{1}<br>({2}, {3})") |> 
  cols_merge(starts_with("validdaysN"), pattern = "{1}d<br>(**g:{2}d**, c:{3}d)") |> 
  cols_merge(starts_with("phot"), pattern = "{1}<br>({2}, {3})") |> 
  cols_merge(starts_with("age"), pattern = "{1}yr<br>({2}yr, {3}yr)") |> 
  cols_merge(starts_with("meq_type_"), pattern = "<<{1}m/>>{2}i<</{3}e>>") |> 
  cols_merge(c("meq_median","meq_p05", "meq_p95"), pattern = "{1}<br>({2}, {3})") |> 
  cols_merge(starts_with("sjl"), pattern = "{1}<br>({2}, {3})") |> 
  cols_merge(starts_with("sex_"), pattern = "{1}F/{2}M") |> 
  cols_merge(starts_with("gender_"), pattern = "{1}W/{2}M<</{3}NB>>") |> 
  cols_merge(starts_with("nonwear"), pattern = "{1} ({2})") |> 
  cols_merge(starts_with("msf_sc"), pattern = "{1}<br>({2}, {3})") |> 
  cols_merge(starts_with("sleep_duration"), pattern = "{1}\n({2}, {3})") |> 
  cols_merge(starts_with("employment_status"), pattern = "{1}F<</{2}P>><</{3}N>>") |> 
  cols_merge(c(weekdayN, weekendN), pattern = "{1}wd / {2}we") |> 
  cols_merge(c(daytype_free, daytype_work), pattern = "{2}work / {1}free") |> 
  cols_hide(c(starts_with("weekdayN_"), starts_with("weekendN_"), daytype_NA)) |> 
  fmt_percent(nonwear_pct, decimals = 1) |> 
  fmt_integer(c("meq_median","meq_p05", "meq_p95")) |> 
  fmt_date(starts_with("date_"), date_style = "y.mn.day") |> 
  fmt(starts_with("msf_sc"), fns = \(x) x |> strptime(format = "%H:%M:%S") |> format(format = "%H:%M")) |> 
  fmt_duration(all_of(c("total")),
               input_units = "secs", max_output_units = 2
               ) |> 
  fmt_duration(all_of(c("nonwear", "phot", "phot_p05", "phot_p95", 
                        "sjl_median", "sjl_p05", "sjl_p95", 
                        "sleep_duration_median", "sleep_duration_p05", "sleep_duration_p95")),
               input_units = "secs", max_output_units = 2
               ) |> 
  cols_move(nonwear, after = total) |> 
  cols_move(daytype_free, after = weekdayN) |> 
  cols_move(c(meq_median, sjl_median), after = meq_type_Morning) |> 
  cols_label_with(everything(), fn= str_to_sentence) |> 
  as.data.frame() |> 
  select(site:partN, daysN, weekdayN, daytype_free, total, nonwear, validdaysN, 
         date_median, phot, age_median, sex_Female, gender_Woman,
         employment_status_Fully, meq_type_Morning, msf_sc_median, meq_median, sjl_median, 
         sleep_duration_median)

tbl1_transposed <- 
tbl1_data_formatted |> 
  t()
  
colnames(tbl1_transposed) <- tbl1_transposed[1,]

tbl1_transposed <- 
tbl1_transposed |> 
  as_tibble(rownames = "description") |> 
  mutate(
    description = replace_values(
      description,
      "site" ~ "Institution",
      "country" ~ "Country",
      "city" ~ "City",
      "coordinates" ~ "Coordinates",
    "partN" ~ "Participants",
    "daysN" ~ "Participant-days",
    "weekdayN" ~ "Weekday / weekend",
    "daytype_free" ~ "Workday / free",
    "date_median" ~ "Collection dates",
    "total" ~ "Participant time",
    "nonwear" ~ "Nonwear time (%)",
    "validdaysN" ~ "Complete days (>80% data)",
    "phot" ~ "Photoperiod",
    "age_median" ~ "Age",
    "meq_median" ~ "Chronotype (MEQ-score)",
    "sex_Female" ~ "Sex",
    "gender_Woman" ~ "Gender",
    "employment_status_Fully" ~ "Employment status",
    "sleep_duration_median" ~ "Sleep duration",
    "meq_type_Morning" ~ "Chronotype (MEQ-based)",
    "sjl_median" ~ "Social jetlag",
    "msf_sc_median" ~ "Chronotype (MCTQ-score)"
    ),
    type = c(rep("Site specifics", 4),rep("Data collection", 9), rep("Participant information", 9))
  ) |> 
  relocate(Overall, all_of(names(melidos_cities) |> sort()))

Finish table

In [16]:
tbl1 <- 
tbl1_transposed |> 
  group_by(type) |> 
  gt(rowname_col = "description") |> 
  # cols_move(Overall, 1) |> 
  sub_missing() |> 
  gt_multiple(names(melidos_cities), style_tab) |> 
  tab_style(style = cell_text(align = "center"), 
            locations = list(cells_body(),
                             cells_column_labels())) |> 
  tab_style(style = cell_text(weight = "bold", ), 
            locations = list(cells_row_groups(),
                             cells_column_labels())) |> 
  fmt_markdown() |> 
  tab_footnote(
    "g: glasses-mounted measurement device; c: chest-level measurement device",
    locations = cells_stub(c("Participants", "Participant-days", "Complete days (>80% data)"))
  ) |> 
  tab_footnote(
    "Median (minimum, maximum)",
    locations = cells_stub(c("Collection dates", "Age"))
  ) |> 
  tab_footnote(
    "Median (5% percentile, 95% percentile)",
    locations = cells_stub(c("Photoperiod", "Chronotype (MCTQ-score)", 
                             "Chronotype (MEQ-score)", "Social jetlag",
                             "Sleep duration"))
  ) |> 
  tab_footnote(
    "w: weeks, d: days, h: hours, m: minutes, s: seconds",
    locations = cells_stub(c("Participant time", "Participant-days", "Nonwear time (%)", 
                             "Complete days (>80% data)", "Photoperiod", "Social jetlag",
                             "Sleep duration"))
  ) |> 
  tab_footnote(
    "F: female, M: male(sex)/man(gender), W: woman, NB: non-binary",
    locations = cells_stub(c("Sex", "Gender"))
  ) |> 
  tab_footnote(
    "F: Fully employed OR Studying and employed OR Not employed but studying or in training; P: Part time employed OR Marginally employed (Minijob); N: Unemployed",
    locations = cells_stub(c("Employment status"))
  ) |> 
  tab_footnote(
    "Following the MEQ score; m: morning type; i: intermediary type; e: evening type",
    locations = cells_stub(c("Chronotype (MEQ-based)"))
  ) |> 
  tab_footnote(
    "MEQ: Morningness-Eveningness-Questionnaire (circadian preference); MCTQ: Munich Chronotype Questionnaire (midsleep on free days, sleep corrected",
    locations = cells_stub(c("Chronotype (MEQ-score)", "Chronotype (MCTQ-score)"))
  ) |> 
  site_conv_gt(rev = FALSE)

tbl1
Overall Borås (SE) Delft (NL) Dortmund (DE) Tübingen (DE) Munich (DE) Madrid (ES) Izmir (TR) San José (CR) Kumasi (GH)
Site specifics
Institution Overall RISE THUAS BAUA MPI TUM FUSPCEU IZTECH UCR KNUST
Country Sweden The Netherlands Germany Germany Germany Spain Turkey Costa Rica Ghana
City Borås Delft Dortmund Tübingen Munich Madrid Izmir San Pedro, San José Kumasi
Coordinates 57.7°N, 12.9°E 52°N, 4.4°E 51.5°N, 7.4°E 48.5°N, 9.1°E 48.1°N, 11.6°E 40.4°N, 3.7°W 38.3°N, 26.6°E 9.9°N, 84.1°W 6.7°N, 1.6°W
Data collection
Participants1 191
(g:143, c:157)
17
(g:14, c:17)
20
(g:13, c:15)
24
(g:19, c:22)
26
(g:26, c:0)
10
(g:10, c:10)
23
(g:23, c:22)
17
(g:17, c:17)
39
(g:6, c:39)
15
(g:15, c:15)
Participant-days1,2 1480d
(g:1136d, c:1249d)
137d
(g:107d, c:137d)
125d
(g:107d, c:125d)
176d
(g:145d, c:163d)
208d
(g:208d, c:0d)
80d
(g:80d, c:80d)
182d
(g:182d, c:174d)
140d
(g:140d, c:140d)
312d
(g:48d, c:312d)
120d
(g:119d, c:118d)
Weekday / weekend 1109wd / 371we 103wd / 34we 93wd / 32we 132wd / 44we 156wd / 52we 60wd / 20we 136wd / 46we 106wd / 34we 233wd / 79we 90wd / 30we
Workday / free 824work / 451free 77work / 40free 70work / 31free 95work / 54free 115work / 66free 38work / 32free 95work / 54free 99work / 38free 180work / 87free 55work / 49free
Participant time2 139w 22h 12w 6d 13w 2d 17w 6d 25w 2d 9w 4d 22w 4d 17w 2d 5w 5d 14w 1d
Nonwear time (%)2 5w 4d (4.1%) 3d 11h (3.8%) 4d 4h (4.5%) 4d 14h (3.7%) 1w 1d (4.6%) 2d 19h (4.1%) 4d 23h (3.1%) 3d 19h (3.2%) 2d 15h (6.5%) 5d 22m (5.0%)
Complete days (>80% data)1,2 1416d
(g:811d, c:1249d)
137d
(g:78d, c:137d)
125d
(g:78d, c:125d)
174d
(g:106d, c:163d)
150d
(g:150d, c:0d)
80d
(g:60d, c:80d)
179d
(g:127d, c:174d)
140d
(g:102d, c:140d)
312d
(g:30d, c:312d)
119d
(g:80d, c:118d)
Collection dates3 25/04/03
(23/08/14, 25/10/20)
25/05/30
(25/03/03, 25/10/20)
25/06/05
(25/02/21, 25/10/14)
25/07/08
(25/06/10, 25/10/20)
23/10/12
(23/08/14, 23/11/13)
24/06/24
(24/05/13, 24/07/29)
24/11/27
(24/10/07, 25/02/10)
25/03/21
(24/12/23, 25/06/02)
25/07/25
(25/06/16, 25/09/07)
24/11/28
(24/10/07, 25/02/03)
Photoperiod4,2 13h 14m
(10h 37m, 18h 11m)
14h 48m
(11h 48m, 20h 29m)
13h 59m
(11h 52m, 18h 5m)
17h 52m
(11h 45m, 18h 13m)
12h 3m
(10h 32m, 15h 24m)
17h 12m
(16h 27m, 17h 26m)
11h 49s
(10h 21m, 12h 13m)
13h 5m
(10h 31m, 15h 33m)
13h 20m
(13h 24s, 13h 28m)
12h 32m
(12h 29m, 12h 41m)
Participant information
Age3 28yr
(18yr, 68yr)
38yr
(21yr, 68yr)
30yr
(19yr, 59yr)
36yr
(18yr, 62yr)
27yr
(22yr, 46yr)
28yr
(20yr, 29yr)
31yr
(20yr, 56yr)
24yr
(21yr, 32yr)
34yr
(22yr, 52yr)
23yr
(18yr, 25yr)
Sex5 105F/86M 6F/11M 8F/12M 13F/11M 14F/12M 6F/4M 15F/8M 11F/6M 24F/15M 8F/7M
Gender5 106W/84M/1NB 6W/11M 9W/10M/1NB 13W/11M 14W/12M 6W/4M 15W/8M 11W/6M 24W/15M 8W/7M
Employment status6 163F/26P/2N 16F/1N 11F/9P 21F/3P 23F/3P 9F/1P 21F/2P 15F/2P 34F/5P 13F/1P/1N
Chronotype (MEQ-based)7 62m/94i/30e 8m/4i/5e 3m/9i/3e 8m/13i/3e 9m/9i/8e 8i/2e 5m/17i/1e 2m/10i/5e 16m/20i/3e 11m/4i
Chronotype (MCTQ-score)4,8 04:02
(02:07, 06:14)
02:55
(02:04, 05:08)
04:31
(03:04, 05:36)
03:47
(02:17, 04:49)
04:33
(02:40, 06:04)
05:07
(04:11, 06:03)
04:48
(03:10, 06:52)
05:28
(03:34, 06:49)
03:42
(02:16, 05:30)
02:19
(01:21, 03:25)
Chronotype (MEQ-score)4,8 54
(35, 70)
58
(34, 71)
49
(38, 61)
52
(40, 70)
49
(33, 68)
44
(38, 58)
52
(42, 67)
52
(32, 60)
58
(40, 69)
62
(51, 72)
Social jetlag4,2 1h
(0s, 2h 45m)
37m 30s
(−59s, 3h 4m)
45m
(5m 15s, 1h 49m)
1h 16m
(30m, 2h 9m)
53m 44s
(0s, 1h 51m)
56m 15s
(30m, 2h 24m)
1h 22m
(4m 5s, 3h 18m)
1h 4m
(20m, 2h 2m)
1h 4m
(0s, 2h 36m)
15m
(0s, 1h 18m)
Sleep duration4,2 7h 35m (5h 9m, 10h 16m) 7h 25m (5h 39m, 9h 44m) 7h 53m (5h 45m, 10h) 7h 55m (5h 40m, 10h 16m) 7h 26m (4h 31m, 9h 15m) 7h 48m (5h, 10h 46m) 7h 49m (5h 52m, 11h 40m) 7h 50m (4h 44m, 11h 4m) 7h 5m (5h 5m, 9h 18m) 7h 37m (5h 45s, 10h 43m)
1 g: glasses-mounted measurement device; c: chest-level measurement device
2 w: weeks, d: days, h: hours, m: minutes, s: seconds
3 Median (minimum, maximum)
4 Median (5% percentile, 95% percentile)
5 F: female, M: male(sex)/man(gender), W: woman, NB: non-binary
6 F: Fully employed OR Studying and employed OR Not employed but studying or in training; P: Part time employed OR Marginally employed (Minijob); N: Unemployed
7 Following the MEQ score; m: morning type; i: intermediary type; e: evening type
8 MEQ: Morningness-Eveningness-Questionnaire (circadian preference); MCTQ: Munich Chronotype Questionnaire (midsleep on free days, sleep corrected
gtsave(tbl1, "tables/tbl1.png", vwidth = 1200)
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpQ91dJy/file16314522c84db.html screenshot completed

Reduced table

In [17]:
demographics |> 
  mutate(sex = sex |> fct_drop(),
         Female = sex == "Female",
         Participants = TRUE) |> 
  tbl_summary(by = site, include = c(Participants, Female, age),
              statistic = list(Participants ~ "{n}",
                               age ~ "{mean} ±{sd}"
                               )) |> 
  add_overall() |> 
  as_gt() |> 
  as.data.frame() |> 
  select(variable, stat_0:last_col()) |> 
  rename_with(\(x) c("Variable", "Overall", names(melidos_cities) |> sort())) |> 
  mutate(Variable = replace_values(Variable, "age" ~ "Age")) |> 
  gt() |> 
  gt_multiple(names(melidos_cities), style_tab) |> 
  site_conv_gt() |> 
  tab_header(md("**MeLiDos sites and demographics**")) |> 
  cols_width(
    Overall ~ px(90),
    Variable ~ px(100),
    everything() ~ px(85)) |> 
  tab_style(
    style = cell_text(weight = "bold"),
    cells_column_labels()
  ) |> 
  gtsave("tables/tbl1_short.png")
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpQ91dJy/file163143a87779d.html screenshot completed

Table 2: Metric distribution

Prepare data

In [18]:
load("data/H1_results.RData")
Warning in check_dep_version(dep_pkg = "TMB"): package version mismatch: 
glmmTMB was built with TMB package version 1.9.19
Current TMB package version is 1.9.21
Please re-install glmmTMB from source or restore original 'TMB' package (see '?reinstalling' for more information)
In [19]:
metrics <-
metrics |> 
  select(name, metric_type, data,  response, engine) |> 
  filter(!is.na(response)) |>
  mutate(scaling = recode_values(response,
                                 "log_zero_inflated(metric)" ~ "log_10_zero_inflated",
                                 default = "identity") |> 
           replace_when(
             engine == "glmmTMB" ~ "log_10_zero_inflated"
           )) |> 
  select(-c(response, engine))

Calculate site metrics

In [20]:
tbl2_data <- 
metrics |> 
  mutate(data = map2(data, name, 
                    \(data, name) if(name != "dose") return(data) else {
                      data |> mutate(metric = metric/1000)
                    }),
         summary = map(data, 
                      \(x) {
                        x |> 
                          summarize(
                            .by = site,
                            median = median(metric, na.rm = TRUE),
                            mean = mean(metric, na.rm = TRUE),
                            sd = sd(metric, na.rm = TRUE),
                            p25 = quantile(metric, na.rm = TRUE, p = 0.25),
                            p75 = quantile(metric, na.rm = TRUE, p = 0.75),
                            n = sum(!is.na(metric))
                          )
                          }
                      ),
         summary_overall = map(data, 
                      \(x) {
                        x |> 
                          summarize(
                            site = "Overall",
                            median = median(metric, na.rm = TRUE),
                            mean = mean(metric, na.rm = TRUE),
                            sd = sd(metric, na.rm = TRUE),
                            p25 = quantile(metric, na.rm = TRUE, p = 0.25),
                            p75 = quantile(metric, na.rm = TRUE, p = 0.75),
                            n = sum(!is.na(metric))
                          )
                          }
         ),
         summary = map2(summary, summary_overall, rbind)
  ) |> 
  select(-summary_overall)

Add plots

In [21]:
tbl2_data <- 
tbl2_data |>
  mutate(plot = map2(data, scaling, ridges_function2),
         type2 = metric_type
         ) |> 
  arrange(metric_type, name)

Constructing table

In [22]:
tbl2_data_formatted <- 
tbl2_data |>
  select(-data) |>
  unnest(summary) |> 
  pivot_wider(id_cols = c(name:scaling, type2), values_from = median:n, names_from = site) |> 
  rename_with(\(y) str_remove(y, "median_")) |> 
  group_by(metric_type) |> 
  gt(rowname_col = "name") |> 
  gt_multiple(c(names(melidos_cities), "Overall"), merge_desc_columns) |> 
  cols_hide(type2) |> 
  fmt_number(columns = !starts_with("n_"), rows = type2 %in% c("dynamics", "spectrum")) |> 
  fmt_number(columns = !starts_with("n_"), rows = type2 %in% c("exposure history"),
             decimals = 2) |> 
  fmt_number(columns = !starts_with("n_"), rows = type2 %in% c("level"),
             decimals = 1) |> 
  fmt(columns = !c(starts_with("n_"), name:scaling, type2),
      rows = type2 %in% c("timing", "duration"), 
      fns = \(x) {
        x |> hms::hms(hours = _) |> strptime(format = "%H:%M:%S") |> format(format = "%H:%M")
        }
      ) |> 
  as.data.frame() |> 
  select(1:Overall)

Finish table

In [23]:
tbl2 <- 
tbl2_data_formatted |> 
  mutate(metric_type = metric_type |> str_to_title(),
         across(c(name, scaling),
                \(x) x |> str_to_title() |>  str_replace_all("_", " ")),
         Unit = c(rep("Hrs:Mins (duration)", 5),
                  rep(NA, 2),
                  "klx·h",
                  rep("lx", 3),
                  NA,
                  rep("HH:MM (time-of-day)", 5)
                  ),
         .after = name) |> 
  group_by(metric_type) |> 
  gt(rowname_col = "name") |> 
  cols_hide(type2) |> 
  sub_values(values = "Mder", replacement = "MDER") |> 
  tab_header("Metric descriptive summary (glasses)") |> 
  gt_multiple(names(melidos_cities), style_tab) |> 
  cols_move(Overall, scaling) |> 
  cols_label(
    scaling = "Scaling"
  ) |> 
  tab_footnote(
    md("**Median** (25% percentile, 75% percentile), *Mean ± standard deviation* , <span style = 'color:grey'>n=observations</span>")
  )|> 
  tab_footnote(
    "Scaling refers to the distributional characteristics of the data and the scaling of the Plot column. Metric-values are on a linear scale. Log 10 zero inflated: logarithmic scaling (base 10), but also contains zero values (offset +0.1); Identity: linear scale",
    locations = cells_column_labels(scaling)
  ) |>
  cols_add(Plot = tbl2_data$plot |> 
             gt::ggplot_image(
                       height = gt::px(80), 
                       aspect_ratio = 2.4
                     )) |> 
  cols_label(
    Plot = "Distribution"
  ) |> 
  tab_footnote(
    md("Red horizontal lines indicate the median of a site. If the scaling column indicates *Log 10 zero inflated* then values were transformed accordingly before plotting"),
    locations = cells_column_labels(Plot)
  ) |>
  fmt_markdown() |> 
  cols_width(
             c(Overall, matches(names(melidos_cities))) ~ px(120),
             Plot ~ px(200),
             everything() ~ px(100)) |> 
  sub_missing() |> 
    tab_style(style = cell_text(weight = "bold"),
            locations = list(
              cells_column_labels(),
              cells_row_groups(),
              cells_title()
            )) |> 
  cols_move(scaling, UCR) |> 
  site_conv_gt(rev = FALSE)
Picking joint bandwidth of 0.178
Picking joint bandwidth of 0.178
Picking joint bandwidth of 0.161
Picking joint bandwidth of 0.161
Picking joint bandwidth of 0.108
Picking joint bandwidth of 0.108
Picking joint bandwidth of 0.0385
Picking joint bandwidth of 0.0385
Picking joint bandwidth of 0.134
Picking joint bandwidth of 0.134
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.193
Picking joint bandwidth of 0.193
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.107
Picking joint bandwidth of 0.107
Picking joint bandwidth of 0.0374
Picking joint bandwidth of 0.0374
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.552
Picking joint bandwidth of 0.5
Picking joint bandwidth of 0.5
Picking joint bandwidth of 0.676
Picking joint bandwidth of 0.676
Picking joint bandwidth of 0.769
Picking joint bandwidth of 0.769
Picking joint bandwidth of 0.578
Picking joint bandwidth of 0.578
Warning: There were 14 warnings in `dplyr::mutate()`.
The first warning was:
ℹ In argument: `Plot = gt::ggplot_image(tbl2_data$plot, height = gt::px(80),
  aspect_ratio = 2.4)`.
Caused by warning:
! Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
ℹ Run `dplyr::last_dplyr_warnings()` to see the 13 remaining warnings.
In [24]:
tbl2
Metric descriptive summary (glasses)
Unit Overall Borås (SE) Delft (NL) Dortmund (DE) Tübingen (DE) Munich (DE) Madrid (ES) Izmir (TR) San José (CR) Kumasi (GH) Scaling1 Distribution2
Duration
Duration above 1000 Hrs:Mins (duration) 00:39
(00:11, 01:33)
01:09 ±01:28
n=811
01:17
(00:30, 03:23)
02:00 ±01:54
n=78
01:18
(00:29, 02:08)
01:31 ±01:27
n=78
01:02
(00:19, 02:32)
01:46 ±02:13
n=106
00:37
(00:07, 01:24)
01:05 ±01:21
n=150
00:38
(00:09, 01:38)
01:08 ±01:18
n=60
00:27
(00:05, 00:53)
00:37 ±00:46
n=127
00:29
(00:05, 01:03)
00:43 ±00:50
n=102
00:19
(00:09, 00:43)
00:33 ±00:34
n=30
00:42
(00:14, 01:15)
00:57 ±00:57
n=80
Log 10 zero inflated
Duration above 250 wake Hrs:Mins (duration) 02:20
(00:50, 04:32)
02:56 ±02:27
n=755
04:03
(02:16, 06:14)
04:10 ±02:27
n=74
03:17
(01:58, 05:12)
03:35 ±02:23
n=66
03:36
(01:21, 05:32)
03:53 ±02:51
n=91
01:53
(00:42, 04:12)
02:46 ±02:37
n=146
02:43
(01:02, 04:16)
02:55 ±02:14
n=60
02:43
(00:54, 04:43)
03:01 ±02:19
n=113
01:31
(00:41, 03:25)
02:08 ±01:54
n=98
01:47
(00:42, 02:42)
01:52 ±01:21
n=27
01:07
(00:24, 02:36)
01:43 ±01:45
n=80
Log 10 zero inflated
Duration below 10 pre-Sleep Hrs:Mins (duration) 01:43
(00:53, 02:30)
01:43 ±01:02
n=780
02:07
(01:24, 02:37)
01:56 ±00:56
n=76
01:40
(00:52, 02:39)
01:41 ±01:03
n=70
01:39
(00:48, 02:17)
01:35 ±00:58
n=101
01:44
(00:57, 02:21)
01:47 ±01:05
n=145
01:39
(00:43, 02:28)
01:40 ±01:07
n=59
01:36
(00:50, 02:38)
01:43 ±01:04
n=121
01:24
(00:46, 02:12)
01:32 ±00:59
n=100
02:06
(01:14, 02:36)
01:50 ±00:59
n=28
01:58
(01:17, 02:40)
01:55 ±00:55
n=80
Log 10 zero inflated
Duration below 1 sleep Hrs:Mins (duration) 07:06
(05:56, 08:15)
07:01 ±02:00
n=790
07:06
(05:51, 08:14)
07:03 ±01:53
n=78
07:10
(05:55, 08:04)
06:54 ±01:55
n=72
07:05
(05:55, 08:15)
07:00 ±01:54
n=101
06:54
(05:51, 07:45)
06:47 ±01:31
n=150
05:51
(04:41, 07:03)
05:58 ±01:58
n=60
08:05
(07:10, 09:08)
07:59 ±02:03
n=120
06:45
(05:07, 08:15)
06:39 ±02:06
n=99
06:50
(06:06, 07:30)
06:28 ±02:00
n=30
07:30
(06:30, 08:50)
07:31 ±02:13
n=80
Log 10 zero inflated
Period above 250 Hrs:Mins (duration) 00:38
(00:17, 01:13)
00:56 ±01:05
n=811
00:56
(00:29, 01:35)
01:14 ±01:04
n=78
00:48
(00:24, 01:20)
01:09 ±01:14
n=78
01:00
(00:21, 01:44)
01:23 ±01:47
n=106
00:35
(00:11, 01:06)
00:50 ±00:54
n=150
00:35
(00:17, 01:41)
01:06 ±01:12
n=60
00:38
(00:16, 01:05)
00:44 ±00:35
n=127
00:33
(00:14, 00:56)
00:42 ±00:41
n=102
00:28
(00:14, 00:40)
00:29 ±00:19
n=30
00:23
(00:15, 00:49)
00:38 ±00:45
n=80
Log 10 zero inflated
Dynamics
Interdaily stability 0.31
(0.25, 0.38)
0.32 ±0.10
n=141
0.34
(0.30, 0.37)
0.33 ±0.08
n=13
0.25
(0.24, 0.37)
0.28 ±0.09
n=13
0.27
(0.22, 0.33)
0.28 ±0.08
n=18
0.33
(0.26, 0.41)
0.34 ±0.10
n=26
0.24
(0.22, 0.28)
0.26 ±0.06
n=10
0.35
(0.32, 0.48)
0.38 ±0.10
n=23
0.28
(0.24, 0.39)
0.32 ±0.10
n=17
0.36
(0.32, 0.45)
0.37 ±0.11
n=6
0.30
(0.26, 0.34)
0.30 ±0.06
n=15
Identity
Intradaily variability 1.32
(0.96, 1.54)
1.28 ±0.38
n=141
1.02
(0.81, 1.41)
1.09 ±0.35
n=13
1.27
(0.96, 1.61)
1.30 ±0.40
n=13
1.43
(1.09, 1.61)
1.31 ±0.40
n=18
1.21
(0.85, 1.42)
1.16 ±0.36
n=26
1.54
(1.20, 1.72)
1.41 ±0.43
n=10
1.27
(1.06, 1.53)
1.27 ±0.39
n=23
1.36
(0.95, 1.54)
1.31 ±0.39
n=17
1.37
(1.24, 1.50)
1.37 ±0.21
n=6
1.41
(1.31, 1.66)
1.42 ±0.35
n=15
Identity
Exposure History
Dose klx·h 4.77
(1.78, 11.67)
9.77 ±15.76
n=811
8.29
(3.21, 19.76)
18.59 ±28.29
n=78
9.11
(3.85, 17.40)
15.09 ±19.83
n=78
6.76
(2.12, 14.67)
13.00 ±21.85
n=106
3.62
(1.33, 8.68)
7.12 ±9.36
n=150
6.49
(1.89, 14.08)
9.11 ±10.30
n=60
3.32
(1.51, 6.39)
5.96 ±8.45
n=127
4.23
(1.52, 9.99)
7.05 ±8.55
n=102
2.19
(1.25, 3.84)
3.10 ±2.42
n=30
5.40
(1.77, 13.22)
9.24 ±10.56
n=80
Log 10 zero inflated
Level
Mean lx 5.1
(2.8, 9.2)
7.5 ±9.4
n=811
7.6
(4.5, 11.6)
9.1 ±7.1
n=78
6.5
(3.9, 11.7)
8.2 ±6.5
n=78
7.7
(3.1, 13.6)
11.9 ±18.6
n=106
5.2
(3.2, 8.1)
7.2 ±6.4
n=150
8.7
(3.9, 14.7)
12.0 ±12.0
n=60
3.5
(1.5, 6.2)
4.2 ±3.3
n=127
5.7
(3.6, 7.9)
7.6 ±7.0
n=102
4.4
(3.7, 6.9)
5.3 ±2.9
n=30
2.2
(0.8, 4.1)
3.0 ±2.9
n=80
Log 10 zero inflated
Brightest 10h mean lx 114.4
(45.1, 250.8)
222.7 ±515.4
n=811
252.7
(104.5, 462.0)
415.0 ±545.6
n=78
181.3
(88.9, 315.6)
280.4 ±341.3
n=78
168.0
(74.8, 357.2)
433.9 ±1,217.9
n=106
104.7
(40.0, 210.4)
185.0 ±240.2
n=150
156.9
(63.4, 264.2)
208.4 ±191.4
n=60
104.0
(25.7, 218.5)
139.5 ±136.0
n=127
83.2
(54.8, 151.5)
121.3 ±102.3
n=102
69.4
(47.3, 165.7)
102.8 ±72.3
n=30
46.2
(9.7, 104.4)
86.9 ±127.6
n=80
Log 10 zero inflated
Darkest 10h mean lx 0.1
(0.0, 0.2)
0.2 ±0.5
n=811
0.1
(0.0, 0.2)
0.2 ±0.2
n=78
0.1
(0.0, 0.2)
0.2 ±0.2
n=78
0.1
(0.0, 0.2)
0.2 ±0.3
n=106
0.1
(0.1, 0.3)
0.3 ±0.5
n=150
0.3
(0.1, 0.7)
0.5 ±0.6
n=60
0.0
(0.0, 0.1)
0.0 ±0.1
n=127
0.1
(0.0, 0.5)
0.5 ±0.9
n=102
0.1
(0.1, 0.3)
0.4 ±1.1
n=30
0.0
(0.0, 0.1)
0.1 ±0.1
n=80
Log 10 zero inflated
Spectrum
MDER 0.71
(0.63, 0.78)
0.70 ±0.12
n=725
0.78
(0.69, 0.86)
0.78 ±0.17
n=61
0.72
(0.66, 0.78)
0.71 ±0.14
n=74
0.74
(0.65, 0.82)
0.74 ±0.11
n=93
0.64
(0.58, 0.72)
0.65 ±0.10
n=139
0.69
(0.63, 0.75)
0.69 ±0.09
n=52
0.66
(0.61, 0.72)
0.67 ±0.09
n=118
0.69
(0.61, 0.79)
0.71 ±0.12
n=91
0.69
(0.64, 0.74)
0.69 ±0.07
n=28
0.74
(0.70, 0.81)
0.73 ±0.13
n=69
Identity
Timing
Brightest 10h midpoint HH:MM (time-of-day) 13:45
(12:54, 14:59)
13:58 ±01:50
n=811
13:14
(12:35, 14:09)
13:20 ±01:18
n=78
14:03
(13:15, 14:59)
14:04 ±01:55
n=78
13:56
(13:09, 15:08)
14:10 ±01:34
n=106
13:42
(12:54, 14:45)
13:57 ±01:35
n=150
14:48
(13:13, 16:02)
14:37 ±02:05
n=60
13:57
(13:15, 15:44)
14:22 ±01:54
n=127
14:12
(13:26, 15:27)
14:19 ±02:04
n=102
12:19
(11:50, 13:42)
12:38 ±01:26
n=30
12:48
(12:05, 13:36)
13:04 ±01:50
n=80
Identity
Darkest 10h midpoint HH:MM (time-of-day) 02:48
(01:51, 03:47)
02:46 ±01:55
n=811
01:56
(01:21, 02:37)
01:59 ±01:01
n=78
02:52
(01:59, 03:24)
02:25 ±02:55
n=78
02:24
(01:39, 03:12)
02:27 ±01:23
n=106
03:05
(02:05, 04:08)
03:08 ±01:35
n=150
02:33
(01:50, 04:22)
03:12 ±02:06
n=60
03:21
(02:40, 04:50)
03:14 ±01:58
n=127
03:32
(02:35, 04:20)
03:25 ±01:27
n=102
01:44
(01:04, 02:57)
02:07 ±01:30
n=30
02:19
(01:31, 02:59)
02:00 ±02:17
n=80
Identity
First timing above 250 HH:MM (time-of-day) 09:08
(08:01, 10:39)
09:27 ±02:28
n=778
07:59
(06:57, 08:42)
08:09 ±01:33
n=78
09:06
(08:17, 09:58)
09:22 ±02:05
n=75
08:41
(07:19, 10:16)
09:00 ±02:32
n=99
09:43
(08:11, 10:58)
09:39 ±02:31
n=147
09:33
(07:54, 11:06)
09:33 ±03:28
n=59
09:36
(08:40, 11:30)
10:07 ±02:26
n=120
10:02
(09:05, 11:54)
10:21 ±02:40
n=96
08:08
(07:12, 09:16)
08:22 ±01:27
n=30
08:39
(08:00, 09:54)
09:12 ±01:47
n=74
Identity
Last timing above 250 HH:MM (time-of-day) 18:02
(16:13, 19:37)
17:49 ±02:36
n=778
18:43
(17:39, 19:47)
18:43 ±01:42
n=78
18:26
(17:24, 19:52)
18:21 ±02:04
n=75
19:32
(17:59, 20:40)
18:49 ±02:40
n=99
17:29
(15:32, 18:51)
17:02 ±02:41
n=147
19:46
(18:34, 20:35)
19:24 ±02:09
n=59
17:52
(16:42, 19:18)
18:01 ±02:19
n=120
18:37
(16:45, 19:37)
18:13 ±02:16
n=96
15:23
(12:59, 16:36)
15:17 ±02:44
n=30
15:51
(14:29, 17:19)
15:30 ±02:13
n=74
Identity
Mean timing above 250 HH:MM (time-of-day) 13:29
(12:27, 14:35)
13:30 ±01:49
n=778
13:20
(12:24, 14:18)
13:21 ±01:22
n=78
13:56
(13:09, 15:04)
13:59 ±01:25
n=75
13:50
(12:32, 14:54)
13:54 ±01:56
n=99
13:16
(12:16, 14:28)
13:22 ±01:46
n=147
13:58
(12:56, 15:32)
14:07 ±02:03
n=59
13:51
(13:02, 14:27)
13:51 ±01:33
n=120
13:50
(12:58, 14:52)
14:04 ±01:33
n=96
11:26
(10:15, 12:37)
11:19 ±01:40
n=30
12:10
(11:05, 13:01)
12:04 ±01:37
n=74
Identity
Median (25% percentile, 75% percentile), Mean ± standard deviation , n=observations
1 Scaling refers to the distributional characteristics of the data and the scaling of the Plot column. Metric-values are on a linear scale. Log 10 zero inflated: logarithmic scaling (base 10), but also contains zero values (offset +0.1); Identity: linear scale
2 Red horizontal lines indicate the median of a site. If the scaling column indicates Log 10 zero inflated then values were transformed accordingly before plotting
gtsave(tbl2, "tables/tbl2.png", vwidth = 1750)
file:////var/folders/9p/326_k3kx43qbn_cyl1rqfhb00000gn/T//RtmpQ91dJy/file1631441c2b12f.html screenshot completed

Figure 1: Overview

Panel A: Protocol

In [25]:
path <- "assets/2026-03-30_MeLiDos_Protocol.png"
P_A <- 
ggdraw() +
  draw_image(path)
P_A

Panel B: Overview

In [26]:
P_B <-
  light_glasses_processed2 |> 
  rbind(light_chest_processed2) |> 
  distinct(site, Id, Datetime) |>
  mutate(site = fct(site, levels = names(melidos_cities))) |>
  site_conv_mutate() |> 
  gg_overview(Id.colname = site, gap.data = tibble(),
              color = site) +
  scale_color_manual(values = melidos_colors) +
  guides(color = "none") +
  labs(y = NULL, x = "Collection dates")
P_B

Panel C: Location

In [27]:
world <- ne_countries(scale = "medium", returnclass = "sf")
    
    countries_colors <- tibble(
      country = melidos_countries,
      color   = melidos_colors,
      stringsAsFactors = FALSE
    )
    
    world$color <- ifelse(
      world$name %in% countries_colors$country,
      countries_colors$country[match(world$name, countries_colors$country)],
      NA
    )
    
    # 3) Locations in geographic CRS (EPSG:4326)
    location_info <- tibble(
      country  = melidos_countries,
      location = melidos_cities,
      lat      = melidos_coordinates |> map(1) |> unlist(),
      lon      = melidos_coordinates |> map(2) |> unlist(),
      color    = melidos_countries,
      stringsAsFactors = FALSE
    ) |> 
      rowwise() |> 
      mutate(
        coordinate_string = format_coordinates(c(lat,lon)),
        label = 
          paste0(location, ", ", country, " (", coordinate_string, ")")
        )
    
    locations <- st_as_sf(location_info, coords = c("lon", "lat"), crs = 4326)
    
    # 4) Project both layers to a planar CRS
    # robinson_crs <- paste0("+proj=", map_projection)
    robinson_crs <- "+proj=eqc"
    world_proj     <- st_transform(world,    crs = robinson_crs)
    locations_proj <- st_transform(locations, crs = robinson_crs)
    
    bb <- st_bbox(world_proj)
    y_offset <- 0.08 * (bb["ymax"] - bb["ymin"]) 
    
    # 5) Plot
P_C <-
      ggplot() +
      geom_sf(data = world_proj, aes(fill = color),
              color = NA, size = 0.25, alpha = 0.5, show.legend = FALSE) +
      geom_sf(data = locations_proj, aes(fill = color),
              shape = 21, color = "black", size = 3, stroke = 0.2) +
      geom_label_repel(
        data = locations_proj,
        stat = "sf_coordinates",
        label.size = 0,
        aes(label = label, fill = color, geometry = geometry),
        color = c(rep("black", 8), "white"),
        min.segment.length = 0.75,
        size = 3,
        alpha = 0.8,
        box.padding = 0.35,    # space between labels
        max.overlaps = 20,
        point.padding = 0.25,  # space from point to label
        force = 2.5,             # stronger repulsion
        force_pull = 0.25,      # weaker pull back to point
        seed = 123
      ) +
      scale_fill_manual(values = 
                          countries_colors |> 
                          select(country, color) |> 
                          deframe()) +
      theme_cowplot() +
      theme(legend.position = "none") +
      theme_sub_axis(line = element_blank()) +
      labs(x = NULL, y = NULL) +
      coord_sf(expand = FALSE)
P_C

Panel D: Doubleplot

In [28]:
source("scripts/Brown_bracket.R")
Loading required package: ggtext
In [29]:
P_D_data <-
      light_glasses_processed2 |>
      ungroup() |>
      mutate(across(where(is.POSIXct),
                    \(x) {
                      date(x) <- "2025-04-03"
                      x }),
             across(c(dawn, dusk),
                    LightLogR:::datetime_to_circular)
             ) |>
      aggregate_Datetime(
        unit = "15 mins",
        type = "floor",
        numeric.handler = \(x) median(x, na.rm = TRUE),
        upper95 = quantile(MEDI, 0.975, na.rm = TRUE),
        upper75 = quantile(MEDI, 0.875, na.rm = TRUE),
        upper50 = quantile(MEDI, 0.75, na.rm = TRUE),
        lower50 = quantile(MEDI, 0.25, na.rm = TRUE),
        lower75 = quantile(MEDI, 0.125, na.rm = TRUE),
        lower95= quantile(MEDI, 0.025, na.rm = TRUE),
      ) |> 
        mutate(
          across(c(dawn, dusk), LightLogR:::circular_to_hms),
          across(c(dawn, dusk), 
                 \(x) {
                      x <- x |> strptime("%H:%M:%S", tz = tz(Datetime))
                      date(x) <- "2025-04-03"
                      x |> as.POSIXct() }),
               )

P_D_data_site <- 
    light_glasses_processed2 |>
    ungroup(Id) |> 
    select(site, Datetime, MEDI) |> 
    aggregate_Date(
        unit = "15 mins",
        type = "floor",
        numeric.handler = \(x) {
          median(x, na.rm = TRUE)
          },
        date.handler = \(x) median(x, na.rm = TRUE),
        upper95 = quantile(MEDI, 0.975, na.rm = TRUE),
        upper75 = quantile(MEDI, 0.875, na.rm = TRUE),
        upper50 = quantile(MEDI, 0.75, na.rm = TRUE),
        lower50 = quantile(MEDI, 0.25, na.rm = TRUE),
        lower75 = quantile(MEDI, 0.125, na.rm = TRUE),
        lower95= quantile(MEDI, 0.025, na.rm = TRUE),
      ) |> 
    mutate(
      across(where(is.POSIXct),
                    \(x) {
                      date(x) <- "2025-04-03"
                      x })
    )

P_D_data_site <- 
  P_D_data_site |> 
  rbind(
    P_D_data_site |> 
    mutate(
      across(where(is.POSIXct),
                    \(x) {
                      date(x) <- "2025-04-04"
                      x })
    )  
    )

# site_color <- ggsci::pal_jco()(1)
site_color <- "black"

P_D <- 
P_D_data |> 
      mutate(sleep = replace_values(sleep, "wake" ~ NA),
             ) |> 
      gg_doubleplot(geom = "blank",
                    facetting = FALSE,
                    # jco_color = FALSE,
                    x.axis.label = "Local time (HH:MM)",
                    y.axis.label = "Melanopic EDI (lx)",
                    aes_fill = site
                    ) |> 
      gg_photoperiod(fill = "darkblue", alpha = 0.1) |> 
      gg_states(sleep, aes_fill = sleep, ymax = -0.1, fill = "red", alpha = 1,
                on.top = TRUE) +
      geom_ribbon(aes(ymin = lower95, ymax = upper95), alpha = 0.4) +
      geom_ribbon(aes(ymin = lower75, ymax = upper75), alpha = 0.4) +
      geom_ribbon(aes(ymin = lower50, ymax = upper50), alpha = 0.4) +
      geom_line(data = P_D_data_site |>       site_conv_mutate(),
                aes(y=MEDI, colour = site), linewidth = 1) +
      geom_line(aes(y = MEDI), linewidth = 2) +
      map(c(1,10,250), 
          \(x) geom_hline(aes(yintercept = x), col = "grey", linetype = "dashed")
      ) +
      scale_color_manual(values = melidos_colors) +
      coord_cartesian(ylim = c(0, 100000)) +
      guides(y = guide_axis_stack(Brown_bracket, "axis"), fill = "none",
             color = "none") +
      # labs(x = NULL)
      labs(
        caption = glue(
          "<i>daytime</i>, <i>evening</i>, and <i>sleep</i> indicate 
          recommendations for healthy light exposure (Brown et al., 2022). 
          <br><b>Median</b> with <b style = 'color:{alpha(site_color, 
          alpha = 0.75)}'>50%</b>, <b style = 'color:{alpha(site_color, 
          alpha = 0.5)}'>75%</b>, or <b style = 'color:{alpha(site_color, 
          alpha = 0.25)}'>95%</b> of data. The horizontal bars show
          <b style = 'color:red'>average sleep times</b>. The vertical bars show 
          <b style = 'color:{alpha('darkblue', alpha = 0.4)}'>average nighttimes</b>."
        )
      ) +
      theme(plot.caption = ggtext::element_markdown())
Scale for colour is already present.
Adding another scale for colour, which will replace the existing scale.
P_D

Panel E: Photoperiods

In [30]:
tz <- light_glasses_processed2 |> pull(Datetime) |> tz()

photoperiods <- metrics_chest$data[[3]] |> pull(photoperiod)

photoperiods_range <- photoperiods |> range() |> round(1)

photoperiods_seq <- 
      inject(seq(!!!photoperiods_range, by = 1) |> 
               round(1))

P_E_data <-
      metrics_glasses$data[[3]] |> 
      rbind(metrics_chest$data[[3]]) |> 
      distinct(site, Id, Date, .keep_all = TRUE) |> 
      mutate(site = site |> fct_rev()) |> 
      site_conv_mutate()

P_E <- 
P_E_data |>   
      ggplot(aes(x=photoperiod)) +
      geom_boxplot(aes(y= site, col = site), width = 0.25,
                   position = position_nudge(y = -0.25)) +
      geom_density_ridges(aes(y=site, fill = site),
                          bandwidth = 0.25,
                          alpha = 0.8) +
      scale_x_continuous(breaks = 10:21) +
      scale_color_manual(values = melidos_colors) +
      scale_fill_manual(values = melidos_colors) +
      labs( 
        y = NULL, 
        x = "Photoperiod (hours)"
        ) +
      theme_cowplot() +
      guides(
        fill = "none", color = "none") +
      coord_cartesian(xlim = photoperiods_range) +
      theme(axis.title.x = ggtext::element_markdown())
P_E

Plot composition

In [31]:
P_A / 
  ((P_C + P_B) + plot_layout(widths = c(3,2))) / 
  ((P_E + P_D) + plot_layout(widths = c(2,3))) + 
  plot_layout(heights = c(1.3,1,1)) +
  plot_annotation(tag_levels = "A")

ggsave("figures/Fig1.pdf", height = 10, width = 10.5, scale = 1.5)

Figure 2: Site plots

In [32]:
Fig2_data <-
      light_glasses_processed2 |>
      ungroup(Id) |>
      mutate(across(where(is.POSIXct),
                    \(x) {
                      date(x) <- "2025-04-03"
                      x }),
             across(c(dawn, dusk),
                    LightLogR:::datetime_to_circular)
             ) |>
      aggregate_Datetime(
        unit = "15 mins",
        type = "floor",
        numeric.handler = \(x) median(x, na.rm = TRUE),
        upper95 = quantile(MEDI, 0.975, na.rm = TRUE),
        upper75 = quantile(MEDI, 0.875, na.rm = TRUE),
        upper50 = quantile(MEDI, 0.75, na.rm = TRUE),
        lower50 = quantile(MEDI, 0.25, na.rm = TRUE),
        lower75 = quantile(MEDI, 0.125, na.rm = TRUE),
        lower95= quantile(MEDI, 0.025, na.rm = TRUE),
      ) |> 
        mutate(
          across(c(dawn, dusk), LightLogR:::circular_to_hms),
          across(c(dawn, dusk), 
                 \(x) {
                      x <- x |> strptime("%H:%M:%S", tz = tz(Datetime))
                      date(x) <- "2025-04-03"
                      x |> as.POSIXct() }),
               )
In [33]:
# Fig2 <- 
Fig2_data |> 
      mutate(sleep = replace_values(sleep, "wake" ~ NA),
             ) |> 
      site_conv_mutate(rev = FALSE) |> 
      gg_doubleplot(geom = "blank",
                    facetting = FALSE,
                    jco_color = FALSE,
                    x.axis.label = "Local time (HH:MM)",
                    y.axis.label = "Melanopic EDI (lx)",
                    aes_fill = site
                    ) |> 
      gg_photoperiod() |> 
      gg_states(sleep, aes_fill = sleep, ymax = -0.1, fill = "red", alpha = 1,
                on.top = TRUE) +
      facet_wrap(~site, nrow = 3, ncol = 3, dir = "lt") + 
      geom_ribbon(aes(ymin = lower95, ymax = upper95, fill = site), alpha = 0.4) +
      geom_ribbon(aes(ymin = lower75, ymax = upper75, fill = site), alpha = 0.4) +
      geom_ribbon(aes(ymin = lower50, ymax = upper50, fill = site), alpha = 0.4) +
      geom_line(aes(y = MEDI, col = site), linewidth = 0.5, layout = "fixed") +
      geom_line(aes(y = MEDI), linewidth = 1.5) +
      map(c(1,10,250), 
          \(x) geom_hline(aes(yintercept = x), col = "grey", linetype = "dashed")
      ) +
      scale_color_manual(values = melidos_colors) +
      scale_fill_manual(values = melidos_colors) +
      coord_cartesian(ylim = c(0, 100000)) +
      guides(y = guide_axis_stack(Brown_bracket, "axis"), fill = "none",
             color = "none") +
      # labs(x = NULL)
      labs(
        caption = glue(
          "<i>daytime</i>, <i>evening</i>, and <i>sleep</i> indicate 
          recommendations for healthy light exposure (Brown et al., 2022). 
          <br><b>Median</b> with <b style = 'color:{alpha(site_color, 
          alpha = 0.75)}'>50%</b>, <b style = 'color:{alpha(site_color, 
          alpha = 0.5)}'>75%</b>, or <b style = 'color:{alpha(site_color, 
          alpha = 0.25)}'>95%</b> of data. The horizontal bars show
          <b style = 'color:red'>average sleep times</b>. The vertical bars show 
          <b style = 'color:grey'>average nighttimes</b>."
        )
      ) +
      theme(plot.caption = ggtext::element_markdown(),
            panel.spacing.x = unit(30, "pt"))

ggsave("figures/Fig2.pdf", height = 8, width = 12, scale = 1.5)
In [34]:
walk(names(melidos_cities), 
     \(x) Fig2_data |> 
            Fig2_plot_individual(x) |> 
            ggsave(plot = _, 
                   filename = str_c("figures/Fig2_individual/Fig2_",x ,".pdf"), 
                   width = 8, 
                   height = 5
                   ) 
     )

Figure 3: Metric plots

In [35]:
Fig3_data <- 
tbl2_data |> 
  mutate(name = 
           name |> 
           str_to_sentence() |> 
           str_replace_all("_", " ") |> 
           str_replace("(1000$|250 |250$|10 |1 )",
                       "\\1 lx mel EDI ") |> 
           str_replace("Mder", "MDER"),
         name = name |> 
           replace_when(
             metric_type == "duration" ~ str_c(name, " (HH:MM)"),
             metric_type == "level" ~ str_c(name, " (lx; from geometric mean)"),
             metric_type == "exposure history" ~ str_c(name, " (klx·h)"),
             metric_type == "timing" ~ str_c(name, " (HH:MM)")
           ),
         data = data |> map2(metric_type, \(x,y) {
           if(y %in% c("duration", "timing")) {
           x |> mutate(metric = metric*60*60)
         } else x
         })
         )

Fig3_data <- 
  Fig3_data |> 
  mutate(
    plot = pmap(list(data, scaling, metric_type, name), Fig3_plot)
  )
In [36]:
Fig3 <-
  Fig3_data |> 
  filter_out(name == "Darkest 10h mean (lx; from geometric mean)") |> 
  pull(plot) |> 
  reduce(\(plots, plot) `+`(plots, plot))
Fig3 + plot_annotation(tag_levels = "A") &
  theme(
    plot.tag = element_text(face = "bold")
  )
Picking joint bandwidth of 1390
Picking joint bandwidth of 1390
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 912
Picking joint bandwidth of 912
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.179
Picking joint bandwidth of 2.75
Picking joint bandwidth of 2.75
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.183
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1990
Picking joint bandwidth of 1990
Picking joint bandwidth of 1800
Picking joint bandwidth of 1800
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

ggsave("figures/Fig3.pdf", width = 10, height = 10, scale = 2)
Picking joint bandwidth of 1390
Picking joint bandwidth of 1390
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 912
Picking joint bandwidth of 912
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.179
Picking joint bandwidth of 2.75
Picking joint bandwidth of 2.75
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.183
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1990
Picking joint bandwidth of 1990
Picking joint bandwidth of 1800
Picking joint bandwidth of 1800
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
In [37]:
walk2(Fig3_data$plot, 
      LETTERS[1:17], 
      \(x,y) ggsave(str_c("figures/Fig3_individual/Fig3_", 
                          y, 
                          ".pdf"), 
                    x+ theme_sub_axis_left(text = element_text()), 
                    width = 3.5,
                    height = 5)
      )
Picking joint bandwidth of 1390
Picking joint bandwidth of 1390
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2970
Warning: Removed 56 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1370
Warning: Removed 31 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2190
Warning: Removed 21 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 912
Picking joint bandwidth of 912
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.0425
Picking joint bandwidth of 0.179
Picking joint bandwidth of 0.179
Picking joint bandwidth of 2.75
Picking joint bandwidth of 2.75
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.13
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.183
Picking joint bandwidth of 0.107
Picking joint bandwidth of 0.107
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 0.0374
Warning: Removed 86 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 1990
Picking joint bandwidth of 1990
Picking joint bandwidth of 1800
Picking joint bandwidth of 1800
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2430
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2770
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Removed 33 rows containing non-finite outside the scale range
(`stat_boxplot()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).
Picking joint bandwidth of 2080
Warning: Removed 33 rows containing non-finite outside the scale range
(`stat_density_ridges()`).

Figure: latitude x photoperiod

In [38]:
span_phot <- tibble(Datetime = as.POSIXct(ymd("2025-01-01"), tz = "UTC") + ddays(0:364))

phot_potential <-
0:60 |> 
  map(
   \(x) span_phot |> 
        extract_photoperiod(c(x,0)) |> 
        group_by(lat) |> 
        summarize(min = min(photoperiod), 
                  max = max(photoperiod))
) |> list_rbind()

phot_potential_range <- 
  phot_potential |> summarize(min = min(min), max = max(max)) |> unlist()

P_E_data |> 
  mutate(site = fct_rev(site)) |> 
  # site_conv_mutate(rev = TRUE) |>
  ggplot(aes(y = lat)) + 
  geom_jitter(aes(col = site, x = photoperiod), width = 0, height = 0.5, alpha = 0.4) +
  geom_density_ridges(aes(y=lat, fill = site, x = photoperiod),
                      scale = 2,
                          position = position_nudge(y = 1),
                          bandwidth = 0.15,
                          alpha = 0.8) +
  geom_ribbon(data = phot_potential, aes(xmin = 0, xmax = min), fill = "black") +
  geom_ribbon(data = phot_potential, aes(xmin = max, xmax = 24), fill = "black") +
  annotate("text", y = 3, x = 15.2, label = "possible photoperiods", color = "white",
           hjust = 0) +
  annotate("curve", y = 3, x = 15,
           xend = 13.3, yend = 5.1,
           curvature = -0.1,
           arrow = arrow(type = "closed",
                         length = unit(0.2, "cm")),
           color = "white") +
  theme_cowplot() + 
  guides(color = "none") +
  coord_cartesian(ylim = c(0, 63), expand = FALSE, xlim = phot_potential_range) +
  scale_color_manual(values = melidos_colors) +
  scale_fill_manual(values = melidos_colors) +
  labs(color = "Site", x = "Photoperiod (hr)", y = "absolute Latitude (°)",
       caption = "A jitter of height 0.5 was applied to points.\nPhotoperiod is defined as the timespan of daily sun elevation above -6°") +
  theme( 
    # panel.background = element_rect(fill = "black"),
        legend.position = "inside",
        legend.text = element_text(colour = "white"),
        legend.position.inside = c(0.65,0.4))

ggsave("figures/photoperiod_potential_ridges.pdf", width = 6, height = 6)

Session Info

In [39]:
sessionInfo()
R version 4.5.0 (2025-04-11)
Platform: aarch64-apple-darwin20
Running under: macOS 26.3.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/Berlin
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] ggtext_0.1.2            gtsummary_2.5.0         rlang_1.2.0            
 [4] glue_1.8.0              patchwork_1.3.2         sf_1.1-0               
 [7] ggrepel_0.9.8           legendry_0.2.4          rnaturalearthdata_1.0.0
[10] rnaturalearth_1.2.0     gt_1.3.0                ggridges_0.5.7         
[13] cowplot_1.2.0           magick_2.9.1            LightLogR_0.10.0       
[16] melidosData_1.0.3       lubridate_1.9.5         forcats_1.0.1          
[19] stringr_1.6.0           dplyr_1.2.1             purrr_1.2.2            
[22] readr_2.2.0             tidyr_1.3.2             tibble_3.3.1           
[25] ggplot2_4.0.2           tidyverse_2.0.0        

loaded via a namespace (and not attached):
 [1] Rdpack_2.6.6        DBI_1.3.0           sandwich_3.1-1     
 [4] magrittr_2.0.5      otel_0.2.0          e1071_1.7-17       
 [7] compiler_4.5.0      mgcv_1.9-4          systemfonts_1.3.2  
[10] vctrs_0.7.3         pkgconfig_2.0.3     fastmap_1.2.0      
[13] labeling_0.4.3      promises_1.5.0      rmarkdown_2.31     
[16] markdown_2.0        tzdb_0.5.0          nloptr_2.2.1       
[19] ragg_1.5.2          xfun_0.57           litedown_0.9       
[22] jsonlite_2.0.0      later_1.4.8         R6_2.6.1           
[25] stringi_1.8.7       RColorBrewer_1.1-3  boot_1.3-31        
[28] numDeriv_2016.8-1.1 estimability_1.5.1  Rcpp_1.1.1-1       
[31] knitr_1.51          zoo_1.8-15          base64enc_0.1-6    
[34] Matrix_1.7-3        splines_4.5.0       timechange_0.4.0   
[37] glmmTMB_1.1.14      tidyselect_1.2.1    rstudioapi_0.18.0  
[40] yaml_2.3.12         TMB_1.9.21          websocket_1.4.4    
[43] processx_3.8.7      suntools_1.1.0      lattice_0.22-7     
[46] withr_3.0.2         S7_0.2.1-1          coda_0.19-4.1      
[49] evaluate_1.0.5      units_1.0-1         proxy_0.4-29       
[52] xml2_1.5.2          circular_0.5-2      pillar_1.11.1      
[55] KernSmooth_2.23-26  renv_1.1.4          reformulas_0.4.4   
[58] generics_0.1.4      chromote_0.5.1      hms_1.1.4          
[61] commonmark_2.0.0    scales_1.4.0        minqa_1.2.8        
[64] class_7.3-23        emmeans_2.0.3       tools_4.5.0        
[67] webshot2_0.1.2      lme4_2.0-1          mvtnorm_1.3-7      
[70] fs_2.0.1            grid_4.5.0          rbibutils_2.4.1    
[73] cards_0.7.1         nlme_3.1-168        cli_3.6.6          
[76] textshaping_1.0.5   bigD_0.3.1          gtable_0.3.6       
[79] ggsci_5.0.0         sass_0.4.10         digest_0.6.39      
[82] classInt_0.4-11     htmlwidgets_1.6.4   farver_2.1.2       
[85] htmltools_0.5.9     lifecycle_1.0.5     gridtext_0.1.6     
[88] MASS_7.3-65