I am using ggplot2 in R and I am trying to add arrowheads to the errorbars on my plot only where the errorbar goes above or below the upper_limit/lower_limit in the plot below (see code for limit definitions). I have played around with geom_segment and can add arrowheads but they are in the incorrect position. I had a similar issue previously with errorbars and points not aligning but the same fix e.g., with group does not seem to work here (see previous question and answer here. Does anyone have any suggestion on how to fix this please?
Code to produce plot with incorrect positioning of arrowheads:
# Define custom_colors as a named vector
custom_colors <- c("HR" = "#97D9E3", "OR" = "#A59BEE", "RD" = "#FDB633", "cont" = "#F6A4B7")
custom_shapes <- c("Obs" = 15, "Between-family" = 24, "Within-family" = 25, "1SMR" = 16, "2SMR" = 18)
pd <- position_dodge(width = 1.0)
line_data <- data.frame(
xintercept = seq(0.5, 10.5, by = 1) # Adjust this based on the number of outcomes
)
upper_limit <- 3.5
lower_limit <- -2.3
n_methods <- 5
dodge_width <- 1.0
df_upper_arrows <- df_filtered %>%
filter(UCI > upper_limit) %>%
mutate(dodged_x = as.numeric(as.factor(Outcome)) +
(as.numeric(Method) - 1.5) * dodge_width / n_methods)
# Subset for arrows below lower limit
df_lower_arrows <- df_filtered %>%
filter(LCI < lower_limit) %>%
mutate(dodged_x = as.numeric(as.factor(Outcome)) +
(as.numeric(Method) - 1.5) * dodge_width / n_methods)
# Create the combined plot
p <- ggplot(df_filtered, aes(x = Outcome)) +
geom_vline(data = line_data, aes(xintercept = xintercept),
color = "white", linetype = 1) +
geom_hline(yintercept = 0, color = "#646363", linetype = "dashed") +
# Points for cont and RD on the second y-axis
geom_point(data = df_filtered,
aes(y = Effect_estimate, shape = Method, color = effect_type, fill = effect_type),
size = 3, stroke=0, position = pd) +
# Confidence intervals for cont and RD on the second y-axis
geom_errorbar(data = df_filtered,
aes(ymin = pmax(LCI, lower_limit), ymax = pmin(UCI, upper_limit), color = effect_type, group = interaction(Method, drop=FALSE)),
width = 0, position = pd, size = 0.5) +
# Arrows for points where UCI exceeds the upper limit
geom_segment(data = df_upper_arrows,
aes(x = dodged_x, xend = dodged_x,
y = upper_limit, yend = upper_limit + 0.2,
color = effect_type, group = interaction(Method, drop = FALSE)),
arrow = arrow(length = unit(0.1, "cm"), type = "open"),
size = 0.5) +
# Arrows for points where LCI is below the lower limit
geom_segment(data = df_lower_arrows,
aes(x = dodged_x, xend = dodged_x,
y = lower_limit, yend = lower_limit - 0.2,
color = effect_type, group = interaction(Method, drop = FALSE)),
arrow = arrow(length = unit(0.1, "cm"), type = "open"),
size = 0.5) +
# Customize the theme
theme_minimal(base_size = 15) +
theme(panel.background = element_rect(fill = "gray90", color = NA), # Gray background
panel.grid.major.x = element_blank(), # No major vertical gridlines
panel.grid.minor.x = element_blank(), # Optional: Hide minor vertical gridlines
panel.grid.major.y = element_line(color = "white"), # Keep y major gridlines
panel.grid.minor.y = element_line(color = "white", linewidth = 0.5),
legend.position = "right",
axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(y = "Effect Estimate (OR)", x = "Outcome", color = "Effect Type", fill = "Effect Type", shape = "Method") +
scale_fill_manual(values = custom_colors, na.translate = FALSE, labels = c("cont" = "Continuous")) + # Use custom colors for effect types
scale_color_manual(values = custom_colors, na.translate = FALSE, labels = c("cont" = "Continuous")) + # Use custom colors for effect types
scale_shape_manual(values = custom_shapes, labels = c("Obs" = "Observational")) +
# Secondary y-axis for cont and RD
scale_y_continuous(limits = c(-2.5,3.8), labels = c("0.14", "1", "7.39", "54.60"), sec.axis = sec_axis(trans = ~ ., name = "Effect Estimate (Continuous, RD)")) +
guides(shape = guide_legend(override.aes = list(fill = "black", stroke = 0)))
# Print the plot
print(p)
The dataset is here:
Exposure,Outcome,Method,Model,Effect_estimate,LCI,UCI,p_value,units,effect_type,SD
Loneliness,Self harm,Obs,logistic,0.2695129,0.23552845,0.3053514,6.33E-53,yes/no,OR,NA
Loneliness,Self harm,Between-family,logistic,0.1931246,0.01703334,0.3673559,3.00E-02,yes/no,OR,NA
Loneliness,Self harm,Within-family,logistic,0.2278867,0.05307844,0.4048337,1.00E-02,yes/no,OR,NA
Loneliness,Self harm,1SMR,ivreg,0.31,0.08,0.54,7.51E-03,yes/no,RD,NA
Loneliness,Self harm,2SMR,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Suicide attempt,Obs,logistic,0.2764618,0.23044892,0.3242825,8.76E-31,yes/no,OR,NA
Loneliness,Suicide attempt,Between-family,logistic,0.2787536,0.04139269,0.519828,2.00E-02,yes/no,OR,NA
Loneliness,Suicide attempt,Within-family,logistic,0.2671717,0.02530587,0.510545,3.00E-02,yes/no,OR,NA
Loneliness,Suicide attempt,1SMR,ivreg,0.15,-0.00743,0.31,6.00E-02,yes/no,RD,NA
Loneliness,Suicide attempt,2SMR,IVW,0.3838154,0.16435286,0.6009729,5.57E-04,NA,OR,NA
Loneliness,Depression diagnosis,Obs,logistic,0.4048337,0.38738983,0.4199557,0.00E+00,yes/no,OR,NA
Loneliness,Depression diagnosis,Between-family,logistic,0.4248816,0.34830486,0.5010593,4.64E-28,yes/no,OR,NA
Loneliness,Depression diagnosis,Within-family,logistic,0.3521825,0.2764618,0.4265113,8.17E-20,yes/no,OR,NA
Loneliness,Depression diagnosis,1SMR,ivreg,0.35,0.15,0.55,5.18E-04,yes/no,RD,NA
Loneliness,Depression diagnosis,2SMR,IVW,0.3283796,0.15228834,0.50515,2.74E-04,NA,OR,NA
Loneliness,Anxiety diagnosis,Obs,logistic,0.2787536,0.26007139,0.2966652,1.73E-180,yes/no,OR,NA
Loneliness,Anxiety diagnosis,Between-family,logistic,0.3031961,0.21748394,0.3909351,5.56E-12,yes/no,OR,NA
Loneliness,Anxiety diagnosis,Within-family,logistic,0.2278867,0.1430148,0.3138672,1.93E-07,yes/no,OR,NA
Loneliness,Anxiety diagnosis,1SMR,ivreg,0.13,-0.04,0.29,1.30E-01,yes/no,RD,NA
Loneliness,Anxiety diagnosis,2SMR,IVW,0.2810334,-0.07572071,0.6394865,1.20E-01,NA,OR,NA
Loneliness,Depression trait,Obs,linear,0.5815217,0.55706522,0.6032609,0.00E+00,score ranges from 0 to 27,cont,3.68
Loneliness,Depression trait,Between-family,linear,0.6956522,0.60326087,0.7880435,1.20E-48,score ranges from 0 to 27,cont,3.68
Loneliness,Depression trait,Within-family,linear,0.4809783,0.38315217,0.576087,1.39E-22,score ranges from 0 to 27,cont,3.68
Loneliness,Depression trait,1SMR,ivreg,3.5326087,2.07065217,4.9918478,2.16E-06,score ranges from 0 to 27,cont,3.68
Loneliness,Depression trait,2SMR,NA,NA,NA,NA,NA,NA,NA,3.68
Loneliness,Anxiety trait,Obs,linear,0.4705882,0.44705882,0.4941176,0.00E+00,score ranges from 0 to 21,cont,3.4
Loneliness,Anxiety trait,Between-family,linear,0.5735294,0.48235294,0.6676471,2.61E-33,score ranges from 0 to 21,cont,3.4
Loneliness,Anxiety trait,Within-family,linear,0.3735294,0.27647059,0.4676471,2.51E-14,score ranges from 0 to 21,cont,3.4
Loneliness,Anxiety trait,1SMR,ivreg,2.2970588,1.14117647,3.4529412,9.81E-05,score ranges from 0 to 21,cont,3.4
Loneliness,Anxiety trait,2SMR,NA,NA,NA,NA,NA,NA,NA,3.4
Loneliness,Positive affect,Obs,linear,-0.7027027,-0.71621622,-0.6891892,0.00E+00,rating ranges from 1 to 6,cont,0.74
Loneliness,Positive affect,Between-family,linear,-0.7567568,-0.83783784,-0.6891892,5.58E-85,rating ranges from 1 to 6,cont,0.74
Loneliness,Positive affect,Within-family,linear,-0.5945946,-0.67567568,-0.527027,9.56E-52,rating ranges from 1 to 6,cont,0.74
Loneliness,Positive affect,1SMR,ivreg,-2.3783784,-3.24324324,-1.5135135,7.98E-08,rating ranges from 1 to 6,cont,0.74
Loneliness,Positive affect,2SMR,IVW,-0.472973,-0.62162162,-0.3243243,3.37E-10,NA,cont,0.74
Loneliness,Meaning in Life,Obs,linear,-0.5180723,-0.54216867,-0.4939759,0.00E+00,rating ranges from 1 to 5,cont,0.83
Loneliness,Meaning in Life,Between-family,linear,-0.5903614,-0.6746988,-0.5060241,4.59E-40,rating ranges from 1 to 5,cont,0.83
Loneliness,Meaning in Life,Within-family,linear,-0.4096386,-0.4939759,-0.313253,2.67E-19,rating ranges from 1 to 5,cont,0.83
Loneliness,Meaning in Life,1SMR,ivreg,-1.1084337,-2.09638554,-0.1204819,3.00E-02,rating ranges from 1 to 5,cont,0.83
Loneliness,Meaning in Life,2SMR,NA,NA,NA,NA,NA,NA,NA,0.83
Loneliness,Wellbeing spectrum,Obs,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Wellbeing spectrum,Between-family,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Wellbeing spectrum,Within-family,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Wellbeing spectrum,1SMR,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Wellbeing spectrum,2SMR,IVW,-0.28,-0.32,-0.23,4.55E-33,NA,cont,NA
Loneliness,Life satisfaction,Obs,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Life satisfaction,Between-family,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Life satisfaction,Within-family,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Life satisfaction,1SMR,NA,NA,NA,NA,NA,NA,NA,NA
Loneliness,Life satisfaction,2SMR,IVW,-0.47,-0.69,-0.24,4.28E-05,NA,cont,NA
Here is one possible option which uses just one geom_segment
and instead of using separate dataframes adds some new columns to your data to draw the segments:
library(tidyverse)
df_filtered <- df_filtered |>
mutate(
Method = factor(Method),
y = case_when(
UCI > upper_limit ~ LCI,
LCI < lower_limit ~ UCI,
.default = NA
),
yend = case_when(
UCI > upper_limit ~ Inf,
LCI < lower_limit ~ -Inf,
.default = NA
),
x = as.numeric(factor(Outcome)) +
scales::rescale(
as.numeric(Method),
# 4 / 5 = (#(Method) - 1) / #(Method)
to = c(-1, 1) * (dodge_width / 2) * 4 / 5
)
)
ggplot(df_filtered, aes(x = Outcome)) +
geom_vline(
data = line_data, aes(xintercept = xintercept),
color = "white", linetype = 1
) +
geom_hline(yintercept = 0, color = "#646363", linetype = "dashed") +
geom_point(
aes(
y = Effect_estimate, shape = Method,
color = effect_type, fill = effect_type,
group = Method
),
size = 3, stroke = 0, position = pd, na.rm = TRUE
) +
geom_errorbar(
aes(
ymin = LCI,
ymax = UCI,
color = effect_type,
group = Method
),
width = 0, position = pd, size = 0.5, na.rm = TRUE
) +
geom_segment(
aes(
x = x, xend = x,
y = y, yend = yend,
color = effect_type
),
arrow = arrow(length = unit(.1, "cm"), type = "open"),
size = 0.5, na.rm = TRUE
) +
theme_minimal(base_size = 15) +
theme(
panel.background = element_rect(fill = "gray90", color = NA), # Gray background
panel.grid.major.x = element_blank(), # No major vertical gridlines
panel.grid.minor.x = element_blank(), # Optional: Hide minor vertical gridlines
panel.grid.major.y = element_line(color = "white"), # Keep y major gridlines
panel.grid.minor.y = element_line(color = "white", linewidth = 0.5),
legend.position = "right",
axis.text.x = element_text(angle = 45, hjust = 1)
) +
labs(
y = "Effect Estimate (OR)", x = "Outcome",
color = "Effect Type", fill = "Effect Type", shape = "Method"
) +
scale_fill_manual(
values = custom_colors, na.translate = FALSE,
labels = c("cont" = "Continuous")
) + # Use custom colors for effect types
scale_color_manual(
values = custom_colors, na.translate = FALSE,
labels = c("cont" = "Continuous")
) + # Use custom colors for effect types
scale_shape_manual(
values = custom_shapes,
labels = c("Obs" = "Observational")
) +
scale_y_continuous(
limits = c(-2.5, 3.8), labels = c("0.14", "1", "7.39", "54.60"),
sec.axis = sec_axis(
trans = ~.,
name = "Effect Estimate (Continuous, RD)"
)
) +
guides(shape = guide_legend(
override.aes = list(
fill = "black",
stroke = 0
)
))