如何在ggplot2中正确填充geom_line与geom_step之间的区域?
动态血压监测数据可视化:阶梯线间区域正确填充问题
问题描述
尝试复现动态血压监测(ABPM)常用可视化图表,包含阶梯线及线间填充区域。已成功构建阶梯线,但使用stat_difference()填充阶梯线上方区域时无法保留阶梯结构,效果不符合预期。
最小可复现代码
data <- structure(list(Date = structure(c(1750165200, 1750166100, 1750167000, 1750167900, 1750168800, 1750169700, 1750170600, 1750171500, 1750172400, 1750173300, 1750174200, 1750175100, 1750176000, 1750176900, 1750177800, 1750178700, 1750179600, 1750180500, 1750181400, 1750182300, 1750183200, 1750184100, 1750185000, 1750185900, 1750186800, 1750187700, 1750188600, 1750189500, 1750190400, 1750191300, 1750192200, 1750193100, 1750194000, 1750194900, 1750195800, 1750196700, 1750197600, 1750198500, 1750199400, 1750200300, 1750201200, 1750202100, 1750203000, 1750203900, 1750204800, 1750205700, 1750206600, 1750207500, 1750208400, 1750209300, 1750210200, 1750211100, 1750212000, 1750213800, 1750215600, 1750217400, 1750219200, 1750221000, 1750222800, 1750224600, 1750226400, 1750228200, 1750230000, 1750231800, 1750233600, 1750235400, 1750237200, 1750238100, 1750239000, 1750239900, 1750240800, 1750241700, 1750242600, 1750243500, 1750244400, 1750245300, 1750246200, 1750247100, 1750248000, 1750248900, 1750249800, 1750250700), class = c("POSIXct", "POSIXt"), tzone = ""), SBP_Patient = c(154, 150, 130, 145, 136, 140, 145, 148, 152, 140, 160, 150, 150, 145, 139, 120, 125, 132, 145, 135, 150, 157, 154, 146, 145, 165, 170, 120, 115, 168, 150, 148, 162, 146, 154, 167, 170, 145, 152, 148, 152, 149, 145, 140, 152, 140, 164, 150, 145, 130, 120, 110, 112, 110, 124, 128, 100, 90, 98, 100, 110, 105, 95, 105, 115, 119, 130, 125, 130, 142, 140, 140, 142, 146, 145, 140, 145, 132, 145, 150, 152, 160), DBP_Patient = c(95, 90, 98, 89, 95, 90, 95, 86, 85, 90, 100, 85, 89, 82, 85, 80, 80, 87, 85, 83, 85, 90, 95, 95, 85, 95, 105, 90, 80, 95, 90, 92, 98, 85, 85, 95, 98, 90, 95, 95, 97, 89, 80, 82, 82, 80, 98, 95, 82, 80, 74, 70, 70, 60, 68, 80, 70, 65, 70, 75, 80, 70, 62, 75, 75, 80, 80, 75, 80, 80, 98, 89, 82, 84, 80, 85, 83, 80, 89, 90, 95, 95), SBP_Threshold = c(135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 120, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135), DBP_Threshold = c(85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85), Period = structure(c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), levels = c("Wake", "Sleep"), class = "factor")), row.names = c(NA, -82L), class = c("tbl_df", "tbl", "data.frame")) ggplot(data, aes(x = Date)) + ggh4x::stat_difference(aes(ymin = SBP_Threshold, ymax = SBP_Patient), alpha = 0.5, levels = c("SBP Above", "SBP Normal"), show.legend = F) + ggh4x::stat_difference(aes(ymin = DBP_Threshold, ymax = DBP_Patient), alpha = 0.5, levels = c("DBP Above", "DBP Normal"), show.legend = F) + geom_step(aes(y = SBP_Threshold, group = 1), color = "grey50") + geom_step(aes(y = DBP_Threshold, group = 1), color = "grey50") + geom_line(aes(y = SBP_Patient, group = 1, color = "Systolic")) + geom_line(aes(y = DBP_Patient, group = 1, color = "Diastolic")) + scale_color_manual(values = c("Systolic" = "red", "Diastolic" = "blue3")) + scale_fill_manual(values = c("SBP Above" = "red", "SBP Normal" = "transparent", "DBP Above" = "blue3", "DBP Normal" = "transparent")) + scale_x_datetime(date_breaks = "1 hour", date_labels = "%H") + labs(y = "Arterial blood pressure (mmHg)", x = "Hour", color = NULL) + theme_bw()
解决方案
stat_difference()默认使用线性插值生成填充区域,无法匹配geom_step()的阶梯结构。需要手动构造阶梯形状的阈值数据,再结合geom_ribbon()实现正确填充:
步骤1:扩展数据为阶梯格式
首先生成包含阶梯节点的数据集,每个阈值会保持到下一个时间点:
library(dplyr) library(tidyr) # 扩展阈值数据,生成阶梯节点 step_data <- data %>% select(Date, SBP_Threshold, DBP_Threshold) %>% mutate( next_Date = lead(Date), # 复制当前行的阈值到下一个时间点 SBP_Threshold_step = SBP_Threshold, DBP_Threshold_step = DBP_Threshold ) %>% # 合并原行和扩展的阶梯行 pivot_longer(cols = c(Date, next_Date), names_to = "type", values_to = "Date_step") %>% filter(!is.na(Date_step)) %>% select(Date_step, SBP_Threshold_step, DBP_Threshold_step) %>% arrange(Date_step) # 合并患者数据与阶梯阈值数据,按时间匹配 filled_data <- data %>% select(Date, SBP_Patient, DBP_Patient) %>% full_join(step_data, by = c("Date" = "Date_step")) %>% arrange(Date) %>% # 填充缺失的患者血压值(用前值填充,匹配阶梯的时间点) fill(SBP_Patient, DBP_Patient, .direction = "down") %>% # 计算填充的上下限:仅保留血压超过阈值的部分 mutate( SBP_fill_ymax = pmax(SBP_Patient, SBP_Threshold_step), SBP_fill_ymin = pmin(SBP_Patient, SBP_Threshold_step), DBP_fill_ymax = pmax(DBP_Patient, DBP_Threshold_step), DBP_fill_ymin = pmin(DBP_Patient, DBP_Threshold_step), # 标记是否需要填充(血压超过阈值时) SBP_above = SBP_Patient > SBP_Threshold_step, DBP_above = DBP_Patient > DBP_Threshold_step )
步骤2:绘制带正确填充的图表
使用geom_ribbon()绘制填充区域,再叠加阶梯线和患者血压线:
ggplot() + # 填充SBP超过阈值的区域 geom_ribbon(data = filter(filled_data, SBP_above), aes(x = Date, ymin = SBP_Threshold_step, ymax = SBP_Patient), fill = "red", alpha = 0.5) + # 填充DBP超过阈值的区域 geom_ribbon(data = filter(filled_data, DBP_above), aes(x = Date, ymin = DBP_Threshold_step, ymax = DBP_Patient), fill = "blue3", alpha = 0.5) + # 绘制阶梯状阈值线 geom_step(data = data, aes(x = Date, y = SBP_Threshold), color = "grey50") + geom_step(data = data, aes(x = Date, y = DBP_Threshold
相关产品推荐
相关产品推荐

