哑铃图:让数据对比一目了然【Dumbbell Chart】

没错,当我祭出 "哑铃" 阵列,你当如何破解,哈哈哈哈...此时,你可以适当怀疑笔者的精神状态了。但话说回来,如果稍加想象,把上图竖起来,"大致" 就是我要分享的 "哑铃图" 了。😑

哑铃图的优劣

哑铃图(Dumbbell Plot)是一种用于对比两组数据差异的可视化图表,通过线段连接两个数据点,形似哑铃。每种数据可视化的方法,都有其优劣,它也不例外,一个简单的总结:

✅ 优点
  1. 直观对比:突出两个时间点或组别之间的数值变化(如实验组和处理组的数据对比)。
  2. 简洁清晰:适合展示少量类别(5-10组),避免柱状图或折线图的密集重叠问题。
  3. 强调差异:线段长度和方向能快速传递增长/下降趋势,适合呈现差距明显的场景。
❌ 劣势
  1. 数据量限制:类别过多时易导致视觉混乱,超过15组通常不适用。
  2. 信息单一:仅展示两点关系,无法呈现多时间点趋势或复杂多维数据。
  3. 设计敏感:颜色、线段粗细若搭配不当可能误导解读(如浅色易弱化差异感知)。

笔者:可是很多时候优势即劣势。

接下来的内容,笔者会介绍如何在 R 中,利用 ggplot2,一步一步实现哑铃图

准备工作

需要的 R Package
r 复制代码
library(tidyr) # 数据处理
library(dplyr) # 数据处理
library(ggplot2) # 画图用的
library(patchwork) # 拼图用的
示例数据
r 复制代码
raw <- tibble(
  labels = c(
    "Spirituality, faith and religion",
    "自由而独立",
    "Hobbies and recreation",
    "身心健康",
    "COVID-19",
    "宠物",
    "Nature and the outdoors"
  ),
  Dem = c(8, 6, 13, 13, 8, 5, 5),
  Rep = c(22, 12, 7, 9, 5, 2, 3)
)

一个基础的哑铃图

数据预处理

因为要用 ggplot2 进行可视化,所以必须要把数据格式转换为其需要的形式:长格式

r 复制代码
df_long <- df %>%
  pivot_longer(-labels)
绘图

很简单,想象一下,就是一条直线,两端挂着 "大大" 的点(线 + 点),线的长度代表两组的差异,点的颜色代表组别:

r 复制代码
df_long %>%
  ggplot(aes(x = value, y = labels)) +
  geom_line(aes(group = labels), color = "#E7E7E7", linewidth = 3.5) +
  geom_point(aes(color = name), size = 5) +
  theme_minimal(base_size = 20) +
  theme(
    legend.position = "none",
    axis.text.y = element_text(color = "black"),
    axis.text.x = element_text(color = "#989898"),
    axis.title = element_blank(),
    panel.grid = element_blank()
  ) +
  scale_color_manual(values = c("#436685", "#BF2F24")) +
  scale_x_continuous(labels = scales::percent_format(scale = 1))

代码不是很多,就可以实现 哑铃图,但是,但是我们需要加一些细节,让它展示的信息更加直观,比如:

  1. y轴排序 :默认情况下,ggplot() 会对用于轴的字符向量按字母顺序排序,然后以逆字母顺序显示y轴值。但是我想按两组对比的差值降序排序。所以,我需要计算差距值,然后将y轴标签转换为factor
  2. 文本标签 :在点的左边或者右边添加具体数值标签;默认情况下,图的右侧会有一个颜色图例,但在上面的代码中,通过theme(legend.position = "none",),移除了图例,实际上,对于数据的展示来说,这是不可取的,所以,我想通过文本标签的方式,在第一行数据点的上方,加上可以提示组别的标签。
  3. 组间差值 :我需要一个单独的图,可视化组间差值,使用 patchwork 将其与主图拼在一起。

加亿点点细节

数据重构
r 复制代码
df <- raw %>% # 上边的数据
  # 计算差值
  mutate(gap = Rep - Dem) %>%
  group_by(labels) %>%
  mutate(max = max(Dem, Rep)) %>%
  ungroup() %>%
  # 排序
  mutate(labels = forcats::fct_reorder(labels, abs(gap)))

# 转换为长数据
df_long <- df %>%
  pivot_longer(
    c(Dem, Rep)
  )
df

数据大概长这样:

重绘
r 复制代码
nudge_value <- .6
p_main <-
  df_long %>%
  # 前几行和之前一样
  ggplot(aes(x = value, y = labels)) +
  geom_line(aes(group = labels), color = "#E7E7E7", linewidth = 3.5) +
  geom_point(aes(color = name), size = 5) +
  # 添加数字标签,使用 ifelse,确定标签位置
  geom_text(aes(label = value, color = name),
    size = 5,
    nudge_x = if_else(
      df_long$value == df_long$max,
      nudge_value,
      -nudge_value
    ),
    hjust = if_else(
      df_long$value == df_long$max,
      0,
      1
    ),
  ) +
  # 自制图例
  geom_text(aes(label = name, color = name),
    data = . %>% filter(gap == max(gap)),
    nudge_y = .5,
    fontface = "bold",
    size = 5
  ) +
  theme_minimal(base_size = 20) +
  theme(
    legend.position = "none",
    axis.text.y = element_text(color = "black"),
    axis.text.x = element_text(color = "#989898"),
    axis.title = element_blank(),
    panel.grid = element_blank()
  ) +
  labs(x = "%", y = NULL) +
  scale_color_manual(values = c("#436685", "#BF2F24")) +
  coord_cartesian(ylim = c(1, 7.5)) +
  scale_x_continuous(labels = scales::percent_format(scale = 1))

p_main

差值示意图

构建数据
r 复制代码
df_gap <-
  df %>%
  mutate(
    label = forcats::fct_reorder(labels, abs(gap)),
    gap_party_max = if_else(
      Rep == max,
      "R",
      "D"
    ),
    gap_label =
      paste0("+", abs(gap), gap_party_max) %>%
      forcats::fct_inorder()
  )
df_gap
绘图
r 复制代码
p_gap <-
  df_gap %>%
  ggplot(aes(x = gap, y = labels)) +
  geom_text(aes(x = 0, label = gap_label, color = gap_party_max), fontface = "bold", size = 5) +
  annotate("text",
    x = 0, y = 7.5, label = "Diff",
    fontface = "bold", size = 7
  ) +
  theme_void() +
  coord_cartesian(xlim = c(-.05, 0.05), ylim = c(1, 7.5)) +
  theme(
    plot.margin = margin(l = 0, r = 0, b = 0, t = 0),
    panel.background = element_rect(fill = "#EFEFE3", color = "#EFEFE3"),
    legend.position = "none"
  ) +
  scale_color_manual(values = c("#436685", "#BF2F24"))

p_gap

拼接:成品

r 复制代码
p_whole <-
  p_main + p_gap + plot_layout(
    design =
      c(
        area(l = 0, r = 45, t = 0, b = 1), # 主图区域
        area(l = 46, r = 52, t = 0, b = 1) # 差值图区域
      )
  )

p_whole


怎么样,看起来还行吧!

简单总结一下:制作基本图形相当容易,但添加细节以提高可读性则需要更多的调整

相关推荐
Mapmost13 小时前
【数据可视化艺术·实战篇】视频AI+人流可视化:如何让数据“动”起来?
人工智能·信息可视化·实时音视频·数字孪生·demo
IT北辰1 天前
数据分析实战案例:使用 Pandas 和 Matplotlib 进行居民用水
信息可视化
奈斯ing2 天前
【prometheus+Grafana篇】Prometheus与Grafana:深入了解监控架构与数据可视化分析平台
信息可视化·grafana·prometheus
穆易青2 天前
2025.04.14【Animation】| 动画式生信数据可视化
信息可视化
派可数据BI可视化2 天前
数据中台、BI业务访谈(三):如何选择合适的访谈对象
大数据·信息可视化·数据挖掘·数据分析·商业智能bi
架构文摘JGWZ2 天前
性能炸裂的数据可视化分析工具:DataEase!
信息可视化·开源软件·工具
CUGLin2 天前
空间信息可视化——WebGIS前端实例(二)
前端·信息可视化
小白—人工智能2 天前
数据可视化 —— 柱形图应用(大全)
python·信息可视化·数据可视化
CUGLin2 天前
空间信息可视化——WebGIS前端实例(一)
前端·信息可视化
爱奇艺技术产品团队3 天前
助力用户增长数据可视化分析:天玑个性化数据大盘
信息可视化·数据挖掘·数据分析