How abiotic factors relate to dTc. - SWC - ET - VPD
2026-04-08 10:54:51.380201
2026-04-08
soil |>
ggplot(aes(x = ts, y = swc_mean, color = depth_label)) +
geom_line(alpha = 0.7, linewidth = 0.4) +
labs(
x = "Time",
y = "SWC (%)",
color = "Depth (cm)",
title = "SWC averaged across sensors at each depth (covered pits)"
) +
theme(
strip.text = element_text(size = 12),
legend.position = "bottom"
)
p_par <- ggplot(flux, aes(x = ts, y = par)) +
geom_line()
ggplotly(p_par)
p_dtc_30 <- plot_ly(dtc_long) |>
add_lines(
x = ~ts,
y = ~dtc,
color = ~roi,
name = ~roi
) |>
layout(
yaxis = list(title = "dTc (°C)")
)
p_swc_30 <- plot_ly(soil) |>
add_lines(
x = ~ts,
y = ~swc_mean,
color = ~depth_label,
name = ~depth_label
) |>
layout(
yaxis = list(title = "SWC (%)")
)
p_vpd_30 <- plot_ly(flux) |>
add_lines(
x = ~ts,
y = ~vpd,
name = "VPD (30 min)"
) |>
layout(yaxis = list(title = "VPD (kPa)"))
p_et_30 <- plot_ly(flux) |>
add_lines(
x = ~ts,
y = ~et,
name = "ET (30 min)"
) |>
layout(yaxis = list(title = "ET (kg m-2 s-1)"))
p_par_30 <- plot_ly(flux) |>
add_lines(
x = ~ts,
y = ~par,
name = "PAR (30 min)"
) |>
layout(yaxis = list(title = "PAR (µmol m-2 s-1)"))
p_precip_30 <- plot_ly(flux) |>
add_lines(
x = ~ts,
y = ~precip,
name = "Precip"
) |>
layout(yaxis = list(title = "Precip (mm)"))
# CHANGED: include precip in the subplot because the title and notes both frame
# rain pulses as part of the interpretation.
fig_30min <- subplot(
p_dtc_30,
p_et_30,
p_par_30,
p_vpd_30,
p_swc_30,
p_precip_30,
nrows = 6,
shareX = TRUE,
titleY = TRUE
) |>
layout(
xaxis = list(title = "Time"),
title = "30-min dTc, SWC, VPD, ET, and Precip"
)
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
fig_30min
One important thing I noticed from the 30-min time series:
When the site is dry, ET tends to peak early in the morning (7-8am).
After rain events, ET peaks later, sometimes close to midday (9am-11am).
This shifting peak timing suggests we need to be careful about aligning ET with dTc, since dTc reliably peaks around midday. This shift might influence relationships under wet vs dry conditions.
In this section I’m trying to figure out how to aggregate climate variables—ET, VPD, and SWC—so they can be compared meaningfully with daily midday dTc. The main question is:
Should I compare dTc to full-day values, daytime values (PAR-based), or midday values (10:00–14:00)?
For ET, this choice matters because ET has strong diurnal patterns and the timing of the ET peak can shift depending on soil moisture. Currently i define daytime is defined using a PAR threshold >1. Midday is defined the same way as in the dTc calculations (10:00–14:00).
The goal here is to run a quick diagnostic and see which aggregation window (full day, daytime, or midday) is best aligned with daily dTc patterns.
I do the same for VPD, since VPD also has a pronounced diurnal cycle.
For both ET and VPD i set up time-series and pairwise plots to compare each aggregation against midday dTc.
# CHANGED: ET summaries are prepared in the load chunk now, so this section can
# focus on comparing aggregation windows instead of recalculating them.
p_et_total <- make_plotly_daily_metric(
daily_et,
y_var = "et_total_day",
series_name = "Total ET (24h)",
y_title = LAB_ET_DAILY
)
p_et_daytime <- make_plotly_daily_metric(
daily_et,
y_var = "et_total_daytime",
series_name = "Total ET (daytime)",
y_title = LAB_ET_DAILY
)
p_et_midday <- make_plotly_daily_metric(
daily_et,
y_var = "et_total_midday",
series_name = "Total ET (midday)",
y_title = LAB_ET_DAILY
)
fig_et <- subplot(
p_et_30,
p_et_total,
p_et_daytime,
p_et_midday,
nrows = 4,
shareX = TRUE,
titleY = TRUE
) |>
layout(
xaxis = list(title = "Date"),
title = "Comparison of ET daily totals"
)
fig_et
et_dtc <- daily_et |>
left_join(dtc_midday, by = "date") |>
drop_na()
et_dtc_long <- et_dtc |>
pivot_longer(
cols = c(et_total_day, et_total_daytime, et_total_midday),
names_to = "et_agg",
values_to = "et"
)
et_dtc_aug <- et_dtc_long |>
augment_species_daily_fit(x_var = "et", agg_var = "et_agg")
p_et_species <- plot_agg_vs_dtc(
data = et_dtc_aug,
x_var = "et",
agg_var = "et_agg",
x_label = "Daily ET (kg m-2 day-1)",
title_text = "Midday dTc vs ET \nROIs colored; species-level regression in black"
)
p_et_species
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Notes on ET
Time series of total ET over 24 hours and total ET daytime look very similar. The daytime version is slightly lower in magnitude but follows almost the exact same pattern. Midday ET (10:00–14:00), on the other hand, is much coarser and naturally smaller in magnitude because it only covers a tight window, though the overall daily trend is still visible.
# CHANGED: use the same helper as ET so the VPD aggregation comparison stays in
# the same format and only needs one plotting style to maintain.
p_vpd_day <- make_plotly_daily_metric(
daily_vpd,
y_var = "vpd_mean_day",
series_name = "24h Mean VPD",
y_title = "VPD (kPa)"
)
p_vpd_daytime <- make_plotly_daily_metric(
daily_vpd,
y_var = "vpd_mean_daytime",
series_name = "Daytime Mean VPD",
y_title = "VPD (kPa)"
)
p_vpd_midday <- make_plotly_daily_metric(
daily_vpd,
y_var = "vpd_mean_midday",
series_name = "Midday Mean VPD",
y_title = "VPD (kPa)"
)
fig_vpd <- subplot(
p_vpd_30,
p_vpd_day,
p_vpd_daytime,
p_vpd_midday,
nrows = 4,
shareX = TRUE,
titleY = TRUE
) |>
layout(
xaxis = list(title = "Date"),
title = "Comparison of VPD Means by Aggregation Window"
)
fig_vpd
# Join VPD aggregations with midday dTc
vpd_dtc <- daily_vpd |>
left_join(dtc_midday, by = "date") |>
drop_na()
vpd_dtc_long <- vpd_dtc |>
pivot_longer(
cols = starts_with("vpd_mean"),
names_to = "vpd_agg",
values_to = "vpd"
)
vpd_dtc_aug <- vpd_dtc_long |>
augment_species_daily_fit(x_var = "vpd", agg_var = "vpd_agg")
p_vpd_species <- plot_agg_vs_dtc(
data = vpd_dtc_aug,
x_var = "vpd",
agg_var = "vpd_agg",
x_label = "VPD (kPa)",
title_text = "Midday dTc vs VPD Aggregations\nIndividual ROIs Colored; Species-Level Pooled Regression in Black"
)
p_vpd_species
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Notes on VPD
The midday mean VPD is slightly smoother with fewer fluctuations, but the overall patterns across the three are nearly the same.
Notes on Choosing Daily Aggregation Windows for ET, VPD, SWC, and dTc Comparisons
Working through the daily comparisons of evapotranspiration (ET), VPD, soil water content (SWC), and midday dTc. I want to be consistent about which daily aggregation makes the most sense for each variable.
# CHANGED: reuse the daily tables created in the load chunk so this summary
# figure stays aligned with the pairwise comparisons below.
p_daily_dtc <- plot_ly(dtc_midday) |>
add_lines(
x = ~date,
y = ~dtc_midday,
color = ~roi,
name = ~roi,
line = list()
) |>
add_markers(
x = ~date,
y = ~dtc_midday,
color = ~roi, # <-- ensures matching colors
marker = list(size = 5),
showlegend = FALSE # hides markers from legend
) |>
layout(
yaxis = list(title = "Midday dTc (°C)")
)
p_daily_swc <- plot_ly(daily_swc) |>
add_lines(
x = ~date,
y = ~swc_day_mean,
color = ~depth_label,
name = ~depth_label
) |>
add_markers(
x = ~date,
y = ~swc_day_mean,
marker = list(size = 3, color = "black"),
showlegend = FALSE
) |>
layout(
yaxis = list(title = "SWC (%)")
)
p_precip_daily <- plot_ly(daily_precip) |>
add_bars(
x = ~date,
y = ~precip_total,
name = "Daily Precip",
marker = list(line = list(width = 0))
) |>
layout(
yaxis = list(title = "Total Daily Precip (mm)"),
xaxis = list(title = "Date"),
title = "Precipitation",
bargap = 0.1
)
fig_daily <- subplot(
p_daily_dtc,
p_daily_swc,
p_et_daytime,
p_vpd_daytime,
p_precip_daily,
nrows = 5,
shareX = TRUE,
titleY = TRUE
) |>
layout(
xaxis = list(title = "Date"),
title = "Daily Midday dTc vs Daily Mean SWC"
)
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
fig_daily
# CHANGED: keep the paired-response plotting logic in one helper and let SWC
# reuse it from a long table so depth can be faceted in one condensed plot.
pair_plot <- function(
df,
xvar,
xlab,
group_vars = character(),
facet_var = NULL,
facet_ncol = 1
) {
df_all <- df |>
filter(dtc_midday < 15) |>
bind_rows(
df |>
group_by(across(all_of(c(group_vars, "species", "date")))) |>
summarise(
x = mean(.data[[xvar]], na.rm = TRUE),
dtc_midday = mean(dtc_midday, na.rm = TRUE),
roi = "all",
.groups = "drop"
)
) |>
mutate(x = ifelse(roi == "all", x, .data[[xvar]]))
# --- NEW COLOR PALETTE LOGIC ---
# 1. Grab all the actual ROI names (excluding the 'all' group we just made)
actual_rois <- unique(df$roi)
# 2. Sort them into Piñon and Juniper arrays based on the first letter
p_names <- sort(grep("^p", actual_rois, ignore.case = TRUE, value = TRUE))
j_names <- sort(grep("^j", actual_rois, ignore.case = TRUE, value = TRUE))
# 3. Generate color gradients for however many of each tree you have
# Piñons get a gradient of light blue to dark blue
p_cols <- colorRampPalette(c("#9ecae1", "#084594"))(length(p_names))
# Junipers get a gradient of yellow to golden-orange (so they are visible!)
j_cols <- colorRampPalette(c("#fed976", "#cc4c02"))(length(j_names))
# 4. Tie the specific colors to the specific tree names
roi_pal <- setNames(c(p_cols, j_cols), c(p_names, j_names))
# -------------------------------
p <- ggplot() +
geom_point(
data = df_all |> filter(roi != "all"),
aes(x = x, y = dtc_midday, color = roi),
alpha = 0.5,
size = 0.7
) +
geom_smooth(
data = df_all |> filter(roi != "all"),
aes(x = x, y = dtc_midday, color = roi),
method = "lm", alpha = 0.5, se = FALSE, linewidth = 0.3
) +
geom_smooth(
data = df_all |> filter(roi == "all"),
aes(x = x, y = dtc_midday),
method = "lm", se = TRUE, color = "black", linewidth = 1.4
) +
stat_cor(
data = df_all |> filter(roi == "all"),
aes(x = x, y = dtc_midday),
method = "pearson",
label.x.npc = 0.97,
label.y.npc = 0.97,
hjust = 1,
size = 3
) +
# Add the generated palette to the plot!
scale_color_manual(values = roi_pal) +
labs(
x = xlab,
y = "Midday ∆T (°C)",
color = "ROI"
) +
theme_bw() +
theme(
strip.text = element_text(size = 10),
legend.position = "none"
) +
ylim(NA,15)
if (!is.null(facet_var)) {
p <- p +
facet_wrap(
vars(!!rlang::sym(facet_var)),
scales = "free_x",
ncol = facet_ncol,
strip.position = "right"
) +
theme(strip.text.y = element_text(angle = -90)) # Keeps side labels readable
}
p
}
swc_dtc_long <- swc_dtc |>
pivot_longer(
cols = starts_with("swc_"),
names_to = "depth_label",
names_pattern = "swc_(.*)_day_mean",
values_to = "swc_day_mean"
) |>
mutate(
depth_label = factor(depth_label, levels = c("5cm", "10cm", "30cm"))
)
p_swc_dtc_pairs <- pair_plot(
swc_dtc_long,
xvar = "swc_day_mean",
xlab = "Daily Mean SWC (%)",
group_vars = "depth_label",
facet_var = "depth_label"
)
p_vpd <- pair_plot(vpd_dtc, "vpd_mean_daytime", "Daytime Mean VPD (kPa)")
p_et <- pair_plot(et_dtc, "et_total_daytime", "Daytime Total ET (kg m-2 day-1)")
p_vpd_et_swc_dtc_pair <- ( p_vpd / p_et/p_swc_dtc_pairs) +
plot_annotation( title = "Pairwise SWC, VPD, and ET vs Midday dTc", tag_levels = "a") +
plot_layout(axis_titles = "collect")+
plot_layout(heights = c(1,1,4))
p_vpd_et_dtc_pair <- p_vpd/p_et +
plot_layout(axis_titles = "collect")
ggsave(filename = here("output/research_days/p_vpd_et_swc_dtc_pair.png"),p_vpd_et_swc_dtc_pair, width = 7, height = 10, units = "in",dpi = 300 )
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
#ggsave(filename = here("output/research_days/p_swc_dtc_pairs.png"),p_swc_dtc_pairs, width = 7, height = 5, units = "in" )
Time series of P and J smoothed? with precip and vpd and swc
plot_min_date <- min(dtc_midday$date, na.rm = TRUE)
plot_max_date <- max(dtc_midday$date, na.rm = TRUE)
dtc_midday <- dtc_midday |>
mutate(
species = factor(species, levels = c("piñon", "juniper"))
)
daily_swc <- daily_swc |>
filter(date >= plot_min_date, date <= plot_max_date) |>
mutate(
depth = factor(depth, levels = sort(unique(depth)))
)
# Main panel: midday ΔTc
p_dtc_midday <- dtc_midday |>
ggplot(aes(x = date, y = dtc_midday, color = species)) +
geom_point(alpha = 0.28, size = 1.2) +
geom_smooth(
method = "loess",
span = 0.25,
se = TRUE,
linewidth = 1.2
) +
labs(
y = expression("Midday ∆T (°C)"),
colour = "Species",
x = "Date"
) +
scale_x_date(
limits = c(plot_min_date, plot_max_date),
date_breaks = "1 month",
date_labels = "%b"
) +
theme_bw() +
scale_color_manual(values = SPECIES_COLS)
p_daily_et <- daily_et |> ggplot(aes(x = date, y = et_total_daytime)) + geom_line()
#----------------------------
# 2) Midday VPD context
#----------------------------
p_vpd <- daily_vpd |>
filter(date >= plot_min_date, date <= plot_max_date) |>
ggplot(aes(x = date, y = vpd_mean_midday)) +
geom_line(linewidth = 0.8) +
labs(
title = "Midday Vapor Pressure Deficit",
y = "VPD (kPa)",
x = "Date"
) +
scale_x_date(
limits = c(plot_min_date, plot_max_date),
date_breaks = "1 month",
date_labels = "%b"
) +
theme_bw()+
theme(legend.position = "none")
#----------------------------
# 3) Soil water content context
#----------------------------
max_precip <- max(daily_precip$precip_total, na.rm = TRUE)
max_swc <- max(daily_swc$swc_day_mean, na.rm = TRUE)
# If max_precip is 40 and max_swc is 20, factor is 2.
scale_factor <- max_precip / max_swc
# 2. Build the combined plot
p_combined_swc_precip <- ggplot() +
# Primary Axis (Left): Precipitation as columns
geom_col(
data = daily_precip |> filter(date >= plot_min_date, date <= plot_max_date),
aes(x = date, y = precip_total),
fill = "blue",
alpha = 0.7
) +
# Secondary Axis (Right): SWC as lines (scaled by the factor)
geom_line(
data = daily_swc,
aes(x = date, y = swc_day_mean * scale_factor, color = depth),
linewidth = 0.8
) +
# Define the Dual Y-Scales
scale_y_continuous(
name = "Precip. (mm)",
sec.axis = sec_axis(~ . / scale_factor, name = "SWC (%)")
) +
scale_x_date(
limits = c(plot_min_date, plot_max_date),
date_breaks = "1 month",
date_labels = "%b"
) +
# Using a nice color palette for soil depths (browns/earth tones often work well)
scale_color_viridis_d(option = "plasma", end = 0.8) +
labs(
x = "Date",
color = "Soil Depth (cm)"
) +
theme_bw()
p_combined_swc_precip
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_col()`).
#----------------------------
# Combine
#----------------------------
p_context <- (p_dtc_midday / p_vpd / p_combined_swc_precip) +
plot_layout(
guides = "collect",
axes = "collect"
) +
plot_annotation(tag_levels = 'a') &
labs(title = NULL, x = NULL) &
theme(legend.position = "bottom")
p_context
## `geom_smooth()` using formula = 'y ~ x'
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_col()`).
ggplotly(p_dtc_midday)
## `geom_smooth()` using formula = 'y ~ x'
## Warning in is.na(u): is.na() applied to non-(list or vector) of type
## 'expression'
#ggsave(filename = here("output/research_days/dtc_flux_context.png"), plot = p_context, height = 7, width = 10, units = "in", dpi = 300)
# CHANGED: replace the old placeholder with a compact sanity check of the
# daily joined inputs used above so the chunk stays runnable and informative.
list(
swc_dtc = swc_dtc |>
select(date, species, roi, starts_with("swc_")) |>
head(),
vpd_dtc = vpd_dtc |>
select(date, species, roi, vpd_mean_daytime, dtc_midday) |>
head(),
et_dtc = et_dtc |>
select(date, species, roi, et_total_daytime, dtc_midday) |>
head()
)
## $swc_dtc
## # A tibble: 6 × 6
## date species roi swc_5cm_day_mean swc_10cm_day_mean swc_30cm_day_mean
## <date> <chr> <chr> <dbl> <dbl> <dbl>
## 1 2025-04-08 juniper j22 7.76 9.77 11.9
## 2 2025-04-08 juniper j23 7.76 9.77 11.9
## 3 2025-04-08 piñon p2 7.76 9.77 11.9
## 4 2025-04-08 piñon p21 7.76 9.77 11.9
## 5 2025-04-09 juniper j22 7.83 9.95 11.9
## 6 2025-04-09 juniper j23 7.83 9.95 11.9
##
## $vpd_dtc
## # A tibble: 6 × 5
## date species roi vpd_mean_daytime dtc_midday
## <date> <chr> <chr> <dbl> <dbl>
## 1 2025-04-08 juniper j22 1.48 5.24
## 2 2025-04-08 juniper j23 1.48 3.94
## 3 2025-04-08 piñon p2 1.48 4.08
## 4 2025-04-08 piñon p21 1.48 4.18
## 5 2025-04-09 juniper j22 1.85 1.22
## 6 2025-04-09 juniper j23 1.85 -0.523
##
## $et_dtc
## # A tibble: 6 × 5
## date species roi et_total_daytime dtc_midday
## <date> <chr> <chr> <dbl> <dbl>
## 1 2025-04-08 juniper j22 0.483 5.24
## 2 2025-04-08 juniper j23 0.483 3.94
## 3 2025-04-08 piñon p2 0.483 4.08
## 4 2025-04-08 piñon p21 0.483 4.18
## 5 2025-04-09 juniper j22 0.376 1.22
## 6 2025-04-09 juniper j23 0.376 -0.523
# CHANGED: summarize the all/month/week diurnal curves through shared helpers so
# the SE calculations and plot styling stay consistent.
dtc_long <- dtc_long |>
mutate(
date = as_date(ts),
month = month(ts, label = TRUE, abbr = TRUE),
week = floor_date(ts, unit = "week", week_start = 1),
time_of_day = as_hms(format(ts, "%H:%M:%S"))
)
dtc_diurnal_all <- summarise_dtc_diurnal(dtc_long)
plot_dtc_diurnal(
data = dtc_diurnal_all,
title_text = "Mean diurnal ΔTc pattern by species",
breaks_vec = hms::as_hms(sprintf("%02d:00:00", seq(0, 23, by = 2)))
)
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_ribbon()`).
dtc_diurnal_month <- summarise_dtc_diurnal(dtc_long, group_vars = "month")
plot_dtc_diurnal(
data = dtc_diurnal_month,
title_text = "Mean diurnal ΔTc pattern by species and month",
facet_var = "month",
ncol = 3,
breaks_vec = hms::as_hms(sprintf("%02d:00:00", seq(0, 23, by = 4)))
)
## Warning: Removed 10 rows containing missing values or values outside the scale range
## (`geom_ribbon()`).
dtc_diurnal_week <- summarise_dtc_diurnal(dtc_long, group_vars = "week")
plot_dtc_diurnal(
data = dtc_diurnal_week,
title_text = "Mean diurnal ΔTc pattern by species and week",
facet_var = "week",
ncol = 2,
breaks_vec = hms::as_hms(c("00:00:00", "06:00:00", "12:00:00", "18:00:00"))
)
## Warning: Removed 27 rows containing missing values or values outside the scale range
## (`geom_ribbon()`).