未记录事实核验 · 状态:维护中

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适用于具备R基础的读者,使用随机模拟数据练习10种图表、主题定制、分面、多图布局及交互式输出;ERP来源和2024年报告期均为假设情境。当前facet_grid示例只按季度分行,并未演示注释所称的两个变量行列分面;箱线图的“五数概括”表述也需注意,默认须端不必等于样本最小值和最大值。工具对比表仍含“完全可重复”“版本管理差”“需要额外工具”等未提供条件或依据的概括,不宜用于正式选型、容量规划或采购判断。中文输出依赖字体配置、网络访问及输出设备,设计原则文档仅为尚未提供的可选延伸。本次仅作结构及正文一致性审阅,未执行代码或进行外部事实核验。 已取消无法确定目标的可点击引用「待创建:数据可视化设计原则」,保留原提及;Missing link target: 待创建:数据可视化设计原则

R语言数据可视化——ggplot2深度实战

abstract 学习目标

  • 深入理解ggplot2的图层语法(Grammar of Graphics)
  • 掌握10种常见业务图表的实现方式
  • 学会颜色方案与主题定制,输出商业可用图表
  • 了解多图组合与交互式图表的实现路径

quote 核心洞察

ggplot2最重要的思想是:图表是数据属性到视觉属性的映射。当你想改一张图时,不要问"怎么改这个函数",而要问"我想把哪个数据属性映射到哪个视觉通道"。这个问题想清楚了,代码自然就写出来了。


一、图层语法:ggplot2的核心哲学

ggplot2基于Leland Wilkinson的《The Grammar of Graphics》,把图表分解为可组合的图层:

一张图 = 数据(data)
       + 映射(aes)        — 数据变量 → 视觉通道(x轴/y轴/颜色/形状/大小)
       + 几何对象(geom)    — 点/线/柱/面等视觉元素
       + 统计变换(stat)    — 计数/平滑/分箱等数据变换
       + 坐标系(coord)     — 直角/极坐标/地图投影
       + 标度(scale)       — 颜色/大小/坐标轴的刻度控制
       + 分面(facet)       — 按变量分成多个子图
       + 主题(theme)       — 非数据元素的视觉风格

最小可行的ggplot2代码:

library(ggplot2)
library(dplyr)

# 生成示例数据
set.seed(42)
sales_data <- tibble(
  month = rep(1:12, 3),
  city = rep(c("北京","上海","广州"), each = 12),
  sales = c(
    rnorm(12, 100, 15),
    rnorm(12, 120, 20),
    rnorm(12, 80, 10)
  ) %>% abs() %>% round()
)

# 三行画出一张折线图
ggplot(sales_data, aes(x = month, y = sales, color = city)) +
  geom_line() +
  geom_point()

注意+号:ggplot2用+叠加图层,而不是tidyverse的%>%管道符。


二、10种常见业务图表实现

2.1 分布类图表

直方图:查看数值分布

ggplot(sales_data, aes(x = sales)) +
  geom_histogram(bins = 20, fill = "#2196F3", color = "white", alpha = 0.8) +
  geom_vline(aes(xintercept = mean(sales)), color = "red", linetype = "dashed", size = 1) +
  labs(title = "月度销售额分布", x = "销售额(万元)", y = "频次") +
  theme_minimal()

密度图:平滑分布曲线(适合多组对比)

ggplot(sales_data, aes(x = sales, fill = city)) +
  geom_density(alpha = 0.4) +
  scale_fill_manual(values = c("#E91E63","#2196F3","#4CAF50")) +
  labs(title = "各城市销售额分布对比", fill = "城市") +
  theme_minimal()

箱线图:查看分布的五数概括

ggplot(sales_data, aes(x = city, y = sales, fill = city)) +
  geom_boxplot(alpha = 0.7, outlier.color = "red") +
  geom_jitter(width = 0.2, alpha = 0.3, size = 1) +  # 叠加散点显示原始数据
  stat_summary(fun = mean, geom = "point", shape = 23, size = 3, fill = "white") +  # 均值点
  scale_fill_brewer(palette = "Set2") +
  labs(title = "各城市销售额箱线图", x = "城市", y = "销售额") +
  theme_minimal() +
  theme(legend.position = "none")

2.2 比较类图表

柱状图:类别对比

# 汇总数据
city_total <- sales_data %>%
  group_by(city) %>%
  summarise(total = sum(sales))

# 基础柱状图
ggplot(city_total, aes(x = reorder(city, total), y = total, fill = city)) +
  geom_col(width = 0.6) +
  geom_text(aes(label = total), hjust = -0.2, size = 4) +  # 标签
  coord_flip() +  # 横向柱状图
  scale_fill_brewer(palette = "Blues", direction = -1) +
  labs(title = "各城市年度销售总额", x = NULL, y = "销售额") +
  theme_minimal() +
  theme(legend.position = "none")

分组柱状图:多维对比

# 按季度分组
sales_data %>%
  mutate(quarter = paste0("Q", ceiling(month / 3))) %>%
  group_by(city, quarter) %>%
  summarise(total = sum(sales), .groups = "drop") %>%
  ggplot(aes(x = quarter, y = total, fill = city)) +
  geom_col(position = "dodge", width = 0.7) +
  scale_fill_brewer(palette = "Set1") +
  labs(title = "各城市季度销售对比", x = "季度", y = "销售额", fill = "城市") +
  theme_minimal()

堆叠柱状图:展示构成

sales_data %>%
  mutate(quarter = paste0("Q", ceiling(month / 3))) %>%
  group_by(city, quarter) %>%
  summarise(total = sum(sales), .groups = "drop") %>%
  ggplot(aes(x = quarter, y = total, fill = city)) +
  geom_col(position = "fill") +  # position="fill"变为100%堆叠
  scale_y_continuous(labels = scales::percent) +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "各季度城市销售占比", y = "占比") +
  theme_minimal()

2.3 趋势类图表

折线图:时间序列趋势

ggplot(sales_data, aes(x = month, y = sales, color = city, group = city)) +
  geom_line(size = 1.2) +
  geom_point(size = 3) +
  geom_smooth(method = "loess", se = TRUE, alpha = 0.15) +  # 添加趋势线
  scale_x_continuous(breaks = 1:12, labels = paste0(1:12, "月")) +
  scale_color_manual(values = c("#E91E63","#2196F3","#FF9800")) +
  labs(title = "各城市月度销售趋势", x = "月份", y = "销售额", color = "城市") +
  theme_minimal()

堆叠面积图:展示各月总量与城市构成(非跨月累计)

ggplot(sales_data, aes(x = month, y = sales, fill = city)) +
  geom_area(alpha = 0.6, position = "stack") +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "各城市月度销售构成与合计", fill = "城市") +
  theme_minimal()

2.4 相关类图表

散点图:两变量关系

# 生成用户行为数据
set.seed(100)
user_behavior <- tibble(
  session_duration = rnorm(500, 8, 3) %>% abs(),
  pages_viewed = round(rnorm(500, 5, 2) %>% abs()),
  converted = rbinom(500, 1, prob = plogis(-1 + 0.2 * session_duration + 0.1 * pages_viewed)),
  user_type = sample(c("新用户","老用户"), 500, replace = TRUE)
)

ggplot(user_behavior, aes(x = session_duration, y = pages_viewed,
                           color = factor(converted), size = converted)) +
  geom_point(alpha = 0.5) +
  geom_smooth(aes(group = 1), method = "lm", color = "gray40", se = FALSE) +
  scale_color_manual(values = c("#9E9E9E","#E91E63"), labels = c("未转化","已转化")) +
  scale_size_continuous(range = c(1, 4)) +
  labs(title = "会话时长 vs 浏览页数", x = "会话时长(分钟)",
       y = "浏览页数", color = "转化状态") +
  theme_minimal() +
  guides(size = "none")

热力图:矩阵数据

# 用户行为热力图
expand.grid(hour = 0:23, weekday = c("周一","周二","周三","周四","周五","周六","周日")) %>%
  as_tibble() %>%
  mutate(
    activity = case_when(
      weekday %in% c("周六","周日") & hour %in% 10:22 ~ rnorm(n(), 80, 15),
      weekday %in% c("周一","周五") & hour %in% c(8,9,18,19) ~ rnorm(n(), 70, 10),
      TRUE ~ rnorm(n(), 30, 10)
    ) %>% abs() %>% round()
  ) %>%
  ggplot(aes(x = hour, y = weekday, fill = activity)) +
  geom_tile(color = "white", size = 0.3) +
  scale_fill_gradient2(low = "#FFFFFF", mid = "#FFC107", high = "#E91E63",
                       midpoint = 50, name = "活跃度") +
  scale_x_continuous(breaks = seq(0, 23, 3), labels = paste0(seq(0, 23, 3), "时")) +
  labs(title = "用户活跃时间热力图", x = "小时", y = NULL) +
  theme_minimal() +
  theme(panel.grid = element_blank())

三、颜色方案与主题定制

3.1 内置颜色体系

# RColorBrewer:经典配色;并非所有色板都色盲友好,应按色板及使用场景选择
library(RColorBrewer)
display.brewer.all()  # 查看所有配色

# 定性色板(适合类别)
scale_color_brewer(palette = "Set1")    # 最多9种鲜亮颜色
scale_color_brewer(palette = "Set2")    # 8种柔和颜色
scale_color_brewer(palette = "Dark2")   # 8种深色

# 序列色板(适合渐变数值)
scale_fill_distiller(palette = "Blues")   # 蓝色系
scale_fill_distiller(palette = "YlOrRd") # 黄橙红渐变

# viridis:感知均匀,色盲友好,印刷友好
scale_fill_viridis_c()    # 连续变量
scale_fill_viridis_d()    # 离散变量

3.2 自定义品牌配色

# 定义公司品牌色
brand_colors <- c(
  "主色" = "#1A73E8",
  "辅色1" = "#34A853",
  "辅色2" = "#FBBC04",
  "辅色3" = "#EA4335"
)

# 在图中使用
scale_color_manual(values = brand_colors)
scale_fill_manual(values = unname(brand_colors))

3.3 主题定制

# 自定义商业报告主题
theme_business <- function() {
  theme_minimal(base_family = "Noto Sans SC", base_size = 12) +
  theme(
    # 标题样式
    plot.title = element_text(face = "bold", size = 14, color = "#1A1A2E"),
    plot.subtitle = element_text(size = 11, color = "#666666", margin = margin(b = 10)),
    plot.caption = element_text(size = 9, color = "#999999", hjust = 0),

    # 坐标轴
    axis.text = element_text(color = "#444444"),
    axis.title = element_text(color = "#444444", size = 11),
    axis.line = element_line(color = "#E0E0E0"),

    # 网格线
    panel.grid.major = element_line(color = "#F0F0F0"),
    panel.grid.minor = element_blank(),

    # 图例
    legend.position = "bottom",
    legend.title = element_text(face = "bold"),

    # 背景
    plot.background = element_rect(fill = "white", color = NA),
    panel.background = element_rect(fill = "white", color = NA),

    # 外边距
    plot.margin = margin(20, 20, 20, 20)
  )
}

# 应用自定义主题
ggplot(sales_data, aes(x = month, y = sales, color = city)) +
  geom_line(size = 1.2) +
  labs(
    title = "月度销售趋势报告",
    subtitle = "模拟情境:2024年各城市销售额月度对比",
    caption = "数据来源:随机生成的模拟数据;假设业务场景为内部ERP销售报告",
    x = "月份", y = "销售额(万元)", color = "城市"
  ) +
  theme_business()

四、分面:一条命令出多张子图

# facet_wrap:按单个变量分面
ggplot(sales_data, aes(x = month, y = sales)) +
  geom_line(color = "#2196F3", size = 1) +
  geom_area(fill = "#2196F3", alpha = 0.15) +
  facet_wrap(~ city, ncol = 3) +  # 按城市分面,每行3图
  scale_x_continuous(breaks = c(3, 6, 9, 12)) +
  labs(title = "各城市月度销售趋势") +
  theme_minimal()

# facet_grid:按两个变量分面(行列矩阵)
sales_data %>%
  mutate(quarter = paste0("Q", ceiling(month / 3))) %>%
  ggplot(aes(x = city, y = sales, fill = city)) +
  geom_boxplot() +
  facet_grid(rows = vars(quarter)) +  # 按季度分行
  theme_minimal() +
  theme(legend.position = "none")

五、多图组合

library(patchwork)

p1 <- ggplot(city_total, aes(x = reorder(city, total), y = total)) +
  geom_col(fill = "#2196F3") +
  coord_flip() +
  labs(title = "A) 城市总销售额") +
  theme_minimal()

p2 <- ggplot(sales_data, aes(x = month, y = sales, color = city)) +
  geom_line(size = 1) +
  labs(title = "B) 月度趋势") +
  theme_minimal()

p3 <- ggplot(sales_data, aes(x = sales, fill = city)) +
  geom_density(alpha = 0.5) +
  labs(title = "C) 分布对比") +
  theme_minimal()

# patchwork的布局语法
(p1 | p2) / p3  # 上两图并排,下一图全宽

# 更精细的布局控制
p1 + p2 + p3 + plot_layout(widths = c(1, 2, 1), ncol = 3) +
  plot_annotation(
    title = "销售数据综合分析报告",
    caption = "模拟数据;假设报告期截至2024年12月"
  )

六、交互式图表

6.1 plotly:一键转为交互图

library(plotly)

# 方法1:把ggplot对象转为plotly
p <- ggplot(sales_data, aes(x = month, y = sales, color = city,
                              text = paste0("城市:", city, "<br>月份:", month, "月<br>销售额:", sales))) +
  geom_line() +
  geom_point()

ggplotly(p, tooltip = "text")  # tooltip指定悬停显示内容

# 方法2:直接用plotly语法(更精细控制)
plot_ly(sales_data,
  x = ~month, y = ~sales, color = ~city,
  type = "scatter", mode = "lines+markers",
  hovertemplate = "月份: %{x}月<br>销售额: %{y}万<extra></extra>"
) %>%
  layout(
    title = "各城市月度销售趋势",
    xaxis = list(title = "月份"),
    yaxis = list(title = "销售额(万元)"),
    hovermode = "x unified"
  )

6.2 echarts4r:ECharts的R接口

library(echarts4r)

sales_data %>%
  group_by(city) %>%
  e_charts(x = month) %>%
  e_line(sales, name = city) %>%
  e_tooltip(trigger = "axis") %>%
  e_title("各城市月度销售趋势") %>%
  e_legend(bottom = 0) %>%
  e_x_axis(name = "月份") %>%
  e_y_axis(name = "销售额")

七、保存与导出

# 保存为高分辨率图片(用于印刷或PPT)
ggsave(
  filename = "sales_trend.png",
  plot = last_plot(),  # 或指定plot变量
  width = 12,
  height = 7,
  dpi = 300,           # 印刷用300dpi,网页用72-96dpi
  units = "in",
  bg = "white"
)

# 保存为SVG(可缩放,适合网页)
ggsave("sales_trend.svg", width = 12, height = 7)

# 保存为PDF(向量图,最适合报告)
ggsave("sales_trend.pdf", width = 12, height = 7, device = cairo_pdf)

tip 中文字体显示的一种处理方案

在macOS/Linux上,中文显示取决于可用字体、字体配置和输出设备;默认配置可能出现缺字。可尝试以下方案,Google字体下载需要网络访问,也可加载本地字体:

library(showtext)
showtext_auto()  # 启用showtext自动处理
font_add_google("Noto Sans SC", "noto")  # 或加载本地字体
# 之后使用theme_minimal(base_family = "noto"),或theme(text = element_text(family = "noto"))

八、ggplot2与Tableau/PowerBI的对比

维度 ggplot2 Tableau PowerBI
学习曲线 陡峭,需要编程基础 平缓,拖拽操作 中等
定制程度 极高,像素级控制 中等 中等
可重复性 完全可重复(代码即文档) 工作簿,版本管理差 同左
自动化 天然支持(脚本化) 需要额外工具 需要额外工具
数据量 需结合可用内存、数据处理方式与绘图复杂度评估 假设比较百万行场景,仍需按数据源、硬件与图表复杂度测试,不能保证流畅 是否需要高级许可应按容量、功能及部署需求判断,不能仅按数据量断定
适合场景 统计报告、学术发表、自动化报告 业务BI看板 企业报表

建议:两者不是对立关系。用Tableau/PowerBI做实时BI看板,用ggplot2做深度统计分析报告。


相关文档

tip 立即可用的技巧

在任何ggplot2图表末尾加上+ theme_minimal(),可以立刻去掉灰色背景,让图表更干净。这是提升图表专业感成本最低的一步。