You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在ggplot2的coord_polar中绘制跨零度的geom_segment问题求助

问题:极坐标环形图中跨零度的线段绘制异常

我正在修改一个环形条形图教程,制作年度环形日历并按季节划分。但冬季对应的geom_segment在极坐标图中跨越零度,从12月到2月的线段会沿逆时针绘制,而非顺时针,导致和其他季节的线段相交。如果从1月开始划分季节,线段和标签就不会相交,所以确定跨零是问题根源。

效果对比

  • 移除冬季后的正确效果:
    移除冬季后的正确线段效果
  • 包含冬季时的异常效果(线段相交):
    包含冬季时的异常线段效果

我的代码

library(ggplot2)
library(tidyverse)
library(viridis)
library(forcats)
library(data.table)
library(dplyr)


# Create dataset
data <- data.frame(
  month=factor(c("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")),
  group=factor(c( rep('WI', 3), rep('SP', 3), rep('SU', 3), rep('FA', 3))) ,
  value1=1.5,
  value2=5,
  value3=1.5,
  value4=10,
  value5=20,
  value6=30
)

# Transform data in a tidy format (long format)
data <- data %>% gather(key = "observation", value="value", -c(1,2))

# Get the name and the y position of each label
label_data <- data %>% group_by(month = factor(month, levels = unique(month))) %>% summarize(tot=sum(value))
number_of_bar <- nrow(label_data)
label_data <- rownames_to_column(label_data)
names(label_data)[names(label_data) == "rowname"] <- "id"
label_data$id <- as.numeric(label_data$id)
angle <- 90 - 360 * (label_data$id-0.5) /number_of_bar     # 调整角度让标签在柱子中心
label_data$hjust <- ifelse(angle < -90, 1, 0)
label_data$month <- c("J","F","M","A","M","J","J","A","S","O","N","D")

group <- c("WI","SP","SU","FA")
start <- c(12,3,6,9)
end <- c(2,5,8,11)

base_data <- data.frame(group,start,end)
base_data <- base_data %>% 
  mutate(title = (start + end)/2)
# base_data <- base_data[-1,] # 注释掉此行会移除冬季线段和标签,保留则出现问题

# prepare a data frame for grid (scales)
grid_data <- base_data
grid_data$end <- grid_data$end[ c( nrow(grid_data), 1:nrow(grid_data)-1)] + 1
grid_data$start <- grid_data$start - 1
grid_data <- grid_data[-1,]

p <- ggplot(data) +      
  
  # Add the stacked bar
  geom_bar(aes(x=factor(month), y=value, fill=observation), stat="identity", alpha=0.5) +
  scale_fill_viridis_d() +
  
  ylim(-30,70) +
  theme_minimal() +
  theme(
    legend.position = "none",
    axis.text = element_blank(),
    axis.title = element_blank(),
    panel.grid = element_blank(),
    plot.margin = unit(rep(-1,4), "cm") 
  ) +
  coord_polar() +
  
  # Add labels on top of each bar
  geom_text(data=label_data, aes(x=id, y=63.5, label=month, hjust=hjust), color="black", fontface="bold",alpha=0.6, size=4, inherit.aes = FALSE ) +
  
  # Add base line information
  geom_segment(data=base_data, aes(x = start, y = -5, xend = end, yend = -5), colour = "black", alpha=0.8, size=0.6 , inherit.aes = FALSE )  +
  geom_text(data=base_data, aes(x = title, y = -10, label=group), colour = "black", alpha=0.8, size=4, fontface="bold", inherit.aes = FALSE) 
  
p

解决方案

问题核心是极坐标下,当x起始值大于结束值时,geom_segment会默认走逆时针的短路径。冬季的start=12、end=2正好触发这个逻辑,导致线段反向绘制。解决办法是把跨零的线段拆成两段:一段从12到13(对应环形的0点),另一段从1到2,强制线段沿顺时针方向连接。

修改步骤

  1. 调整base_data的生成逻辑,自动识别并拆分跨零的季节段:
# 处理跨零的季节段
base_data <- data.frame(group,start,end) %>%
  mutate(
    # 标记是否跨零
    cross_zero = start > end,
    # 跨零季节的标题位置需要调整,让标签落在顺时针区间中间
    title = ifelse(cross_zero, (start + end + 12)/2, (start + end)/2)
  )

# 拆分跨零的行
split_rows <- base_data %>%
  filter(cross_zero) %>%
  summarise(
    group = group,
    start = c(start, 1),
    end = c(13, end), # 13对应环形的0位置,实现从12到0的连接
    cross_zero = TRUE,
    title = first(title)
  )

# 合并拆分后的行和非跨零行
base_data <- bind_rows(
  base_data %>% filter(!cross_zero),
  split_rows
)
  1. 季节标签需要去重,避免拆分后重复显示:
# 替换原有的geom_text代码
geom_text(data=base_data %>% distinct(group, title), aes(x = title, y = -10, label=group), colour = "black", alpha=0.8, size=4, fontface="bold", inherit.aes = FALSE)

完整修改后的代码

library(ggplot2)
library(tidyverse)
library(viridis)
library(forcats)
library(data.table)
library(dplyr)


# Create dataset
data <- data.frame(
  month=factor(c("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec")),
  group=factor(c( rep('WI', 3), rep('SP', 3), rep('SU', 3), rep('FA', 3))) ,
  value1=1.5,
  value2=5,
  value3=1.5,
  value4=10,
  value5=20,
  value6=30
)

# Transform data in a tidy format (long format)
data <- data %>% gather(key = "observation", value="value", -c(1,2))

# Get the name and the y position of each label
label_data <- data %>% group_by(month = factor(month, levels = unique(month))) %>% summarize(tot=sum(value))
number_of_bar <- nrow(label_data)
label_data <- rownames_to_column(label_data)
names(label_data)[names(label_data) == "rowname"] <- "id"
label_data$id <- as.numeric(label_data$id)
angle <- 90 - 360 * (label_data$id-0.5) /number_of_bar     # 调整角度让标签在柱子中心
label_data$hjust <- ifelse(angle < -90, 1, 0)
label_data$month <- c("J","F","M","A","M","J","J","A","S","O","N","D")

group <- c("WI","SP","SU","FA")
start <- c(12,3,6,9)
end <- c(2,5,8,11)

# 处理跨零的季节段
base_data <- data.frame(group,start,end) %>%
  mutate(
    cross_zero = start > end,
    title = ifelse(cross_zero, (start + end + 12)/2, (start + end)/2)
  )

# 拆分跨零的行
split_rows <- base_data %>%
  filter(cross_zero) %>%
  summarise(
    group = group,
    start = c(start, 1),
    end = c(13, end),
    cross_zero = TRUE,
    title = first(title)
  )

# 合并数据
base_data <- bind_rows(
  base_data %>% filter(!cross_zero),
  split_rows
)

# prepare a data frame for grid (scales)
grid_data <- base_data %>% filter(!cross_zero) # 网格数据不需要拆分后的行
grid_data$end <- grid_data$end[ c( nrow(grid_data), 1:nrow(grid_data)-1)] + 1
grid_data$start <- grid_data$start - 1
grid_data <- grid_data[-1,]

p <- ggplot(data) +      
  
  # Add the stacked bar
  geom_bar(aes(x=factor(month), y=value, fill=observation), stat="identity", alpha=0.5) +
  scale_fill_viridis_d() +
  
  ylim(-30,70) +
  theme_minimal() +
  theme(
    legend.position = "none",
    axis.text = element_blank(),
    axis.title = element_blank(),
    panel.grid = element_blank(),
    plot.margin = unit(rep(-1,4), "cm") 
  ) +
  coord_polar() +
  
  # Add labels on top of each bar
  geom_text(data=label_data, aes(x=id, y=63.5, label=month, hjust=hjust), color="black", fontface="bold",alpha=0.6, size=4, inherit.aes = FALSE ) +
  
  # Add base line information
  geom_segment(data=base_data, aes(x = start, y = -5, xend = end, yend = -5), colour = "black", alpha=0.8, size=0.6 , inherit.aes = FALSE )  +
  # 季节标签去重,只显示一次
  geom_text(data=base_data %>% distinct(group, title), aes(x = title, y = -10, label=group), colour = "black", alpha=0.8, size=4, fontface="bold", inherit.aes = FALSE) 
  
p

修改后,冬季线段会沿顺时针方向连接12月到2月,不会和其他季节线段相交,季节标签也会正确显示在对应区间的中间位置。


内容的提问来源于stack exchange,提问作者ElizaBeso000

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 07:18:12