三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

R语言绘制热力地图复合气泡饼图:空间数据多维度可视化实战

R语言绘制热力地图复合气泡饼图:空间数据多维度可视化实战

1. 从数据到洞察:为什么需要热力地图复合气泡饼图?

在数据分析的日常工作中,我们常常面临一个挑战:如何在一张图上,同时清晰地展示多个维度的信息?比如,我们有一组地理坐标数据,每个坐标点代表一个城市或区域,我们不仅想知道这些区域的“热度”分布(例如,某项指标的整体强度),还想了解每个区域内不同类别的构成比例。这时候,传统的单一图表就显得捉襟见肘了。

热力地图擅长展示连续变量的空间密度或强度变化,颜色深浅直观地反映了数值大小。气泡图则可以通过气泡的大小来编码另一个连续变量。而饼图,是展示构成比例的经典选择。那么,有没有一种方法,能将这三者巧妙地融合在一起呢?答案是肯定的,这就是我们今天要探讨的“热力地图复合气泡饼图”。

想象一下这样的场景:你是一家全国性连锁零售品牌的数据分析师,手头有各个门店的经纬度坐标、月度总销售额以及各类商品(如食品、服装、电子产品)的销售额占比。你希望在一张中国地图上,一眼看出:

  1. 哪些区域整体销售“热度”高(热力底色)。
  2. 哪些门店的销售规模大(气泡大小)。
  3. 每家门店的主力商品构成是什么(气泡内的饼图)。

这种复合图表,将地理信息、规模信息和结构信息三者合一,极大地提升了数据表达的效率和洞察的深度。它特别适用于地理空间数据分析、生态学、社会学、商业智能等领域,任何需要在空间背景下同时比较“强度”和“成分”的场景,它都能大显身手。

而实现这一强大可视化效果的利器,正是R语言及其强大的图形生态系统。尤其是ggplot2包,以其“图形语法”的哲学,为我们搭建这种复杂但精美的图表提供了坚实的基础。结合geom_scatterpie这个专门用于绘制散点饼图的扩展包,我们就能轻松地将饼图“放置”在地图上的指定坐标点,再叠加热力图层,最终生成我们想要的复合图表。

2. 构建复合图表的基石:核心工具包解析

在动手绘制之前,我们必须先理解并准备好所需的工具。R语言的强大,很大程度上源于其丰富的扩展包生态。对于热力地图复合气泡饼图,我们需要以下几类工具包协同工作。

2.1 图形语法核心:ggplot2

ggplot2是R语言中最著名、应用最广泛的数据可视化包。它的核心思想是“图形语法”:一张图是由数据、几何对象、美学映射、标度、坐标系、分面等图层叠加而成的。这种图层化的思想,正是我们构建复合图表的理论基石。

ggplot2中,我们通过ggplot()函数初始化一个图形对象,然后使用+号不断地添加图层(geom_*函数)和修饰(scale_*,theme_*,labs等)。例如,geom_tile()geom_raster()可以生成热力图的色块,geom_point()可以生成散点(气泡),而我们要用的geom_scatterpie()则是一个特殊的几何对象,它将在指定坐标绘制饼图。

理解ggplot2的另一个关键是“美学映射”(aes)。我们将数据框中的变量,通过aes()函数映射到图形的视觉属性上,如x轴位置、y轴位置、颜色、大小、填充色等。在我们的复合图中,经纬度会映射到x和y,某个强度指标会映射到热力填充色,总规模指标会映射到气泡(饼图)的大小,而分类占比则映射到饼图各扇区的填充色。

2.2 散点饼图专家:geom_scatterpie 与 scatterpie

geom_scatterpie并不是ggplot2自带的几何对象,它来自scatterpie这个扩展包。这个包的核心功能就是在ggplot2的框架内,在指定的(x, y)坐标点上绘制饼图。

它的工作原理非常直观:你需要提供一个数据框,其中必须包含用于定位的x和y列(通常是经纬度),以及一系列数值列,这些数值列代表了饼图各个部分的绝对数值(注意,不是百分比)。geom_scatterpie()会读取这些列,在每一个(x, y)点上,根据这些数值列的比例绘制一个饼图。

一个关键参数是pie_scale,它控制着所有饼图的整体大小。而每个饼图的半径,则可以通过aes(r = ...)进行映射,从而实现气泡大小的效果——这里的r代表半径,我们可以将其映射到另一个表示总规模的变量(例如各数值列之和)。这样,饼图的大小就编码了总规模信息。

2.3 空间背景的提供者:地图数据与 sf/ggspatial

我们的图表需要一张地图作为背景,以提供地理空间参考。在R中获取地图数据有多种途径:

  • mapsmapdata:提供了世界、国家、美国州界等基础地图数据,格式通常是数据框。
  • ggplot2::map_data()函数:一个便捷函数,可以从maps包中提取数据并转化为ggplot2友好的数据框格式。
  • sf(Simple Features) 包:这是现代R语言空间数据分析的事实标准。sf对象是一种更强大、更标准化的空间矢量数据格式,支持复杂的空间运算。ggplot2可以通过geom_sf()函数直接绘制sf对象。
  • ggspatial:它在ggplot2sf的基础上,提供了更便捷的地图绘制功能,如添加比例尺、指北针、从在线资源(如OpenStreetMap, Stamen)获取底图等,能让我们的地图看起来更专业。

对于入门级应用,使用map_data()获取基础地图数据就足够了。但如果需要进行更复杂的空间操作或使用更精美的在线底图,sfggspatial是更好的选择。

2.4 数据塑形助手:dplyr 与 tidyr

在准备数据的过程中,我们经常需要对数据进行筛选、汇总、变形。dplyr包提供了一套直观易用的“动词”函数来完成这些任务,如filter()(筛选)、select()(选择列)、mutate()(创建新列)、summarise()(汇总)、group_by()(分组)以及强大的管道操作符%>%

tidyr包则擅长处理数据的“整洁”变形,特别是pivot_longer()pivot_wider()函数,可以轻松地在长格式和宽格式之间转换数据。我们的饼图数据通常需要是宽格式(每一行是一个观测点,每一列是一个分类的数值),而其他数据处理可能要求长格式,tidyr在这之间的转换至关重要。

3. 实战演练:一步步绘制全国零售门店销售复合图

理论说得再多,不如亲手做一遍。让我们以一个模拟的全国零售门店数据集为例,完整地走一遍从数据准备到图表生成的流程。假设我们有一个包含100家门店的数据。

3.1 第一步:模拟与准备数据

首先,我们创建模拟数据。我们需要为每个门店生成:随机的经纬度(模拟在中国范围内的分布)、总销售额、以及三大品类(食品、服装、电子产品)的销售额。

# 加载必要的包 library(ggplot2) library(scatterpie) library(dplyr) library(tidyr) library(maps) library(ggspatial) # 用于添加地图元素 # 设置随机种子以保证结果可复现 set.seed(123) # 模拟100家门店的数据 n_stores <- 100 # 1. 模拟经纬度(大致在中国范围内) # 经度范围:73°E ~ 135°E, 纬度范围:18°N ~ 54°N store_data <- data.frame( store_id = 1:n_stores, # 使用均匀分布模拟,更真实的模拟可以用正态分布或从城市列表中抽样 longitude = runif(n_stores, 73, 135), latitude = runif(n_stores, 18, 54) ) # 2. 模拟总销售额(万元)和品类占比 # 假设总销售额在50到2000万元之间 store_data$total_sales <- runif(n_stores, 50, 2000) # 模拟每个品类的销售额(基于总销售额和随机比例) # 首先生成每个门店三个品类的随机比例(使用狄利克雷分布更合理,这里简化用均匀分布) for (i in 1:n_stores) { # 生成三个随机数作为比例的基数 props <- runif(3, 0.5, 2) # 归一化为比例 props <- props / sum(props) # 计算各品类销售额 store_data$food_sales[i] <- store_data$total_sales[i] * props[1] store_data$clothing_sales[i] <- store_data$total_sales[i] * props[2] store_data$electronics_sales[i] <- store_data$total_sales[i] * props[3] } # 查看前几行数据 head(store_data)

现在,我们有了一个宽格式的数据框store_data,每一行是一家门店,列包括位置信息和各品类的绝对销售额。这正是geom_scatterpie()所需要的格式。

注意geom_scatterpie()要求用于绘制饼图的各组成部分的列是绝对数值,而不是百分比。它会自动计算比例。如果你的原始数据是百分比,需要先乘以一个总量(如总销售额)转换回绝对数值。

3.2 第二步:创建基础热力图层(销售热度)

热力图层旨在展示整体销售强度的空间分布。一种简单的方法是使用二维核密度估计,但这需要大量的点。对于门店数据,我们可以采用将地图网格化后计算网格内销售总和的方法来模拟“热度”。

首先,我们需要获取中国地图的轮廓数据,并创建一个覆盖中国的网格。

# 获取中国地图数据(不含南海诸岛等细节,如需详细地图需使用更专业的包如`chinamap`) china_map <- map_data("world", region = "China") # 定义网格大小(经纬度步长) grid_step <- 2 # 度 # 创建覆盖中国范围的网格点 lon_range <- range(china_map$long, na.rm = TRUE) lat_range <- range(china_map$lat, na.rm = TRUE) lon_seq <- seq(from = floor(lon_range[1]), to = ceiling(lon_range[2]), by = grid_step) lat_seq <- seq(from = floor(lat_range[1]), to = ceiling(lat_range[2]), by = grid_step) # 生成所有网格中心点 grid_points <- expand.grid(lon_center = lon_seq, lat_center = lat_seq) # 将门店数据匹配到最近的网格中心(简化处理,实际应用可用空间连接) # 这里我们计算每个网格内所有门店的总销售额作为该网格的热度值 # 这是一个简化的近似,更精确的方法需要使用空间连接(如sf::st_join) heat_data <- grid_points heat_data$grid_sales <- 0 # 初始化热度值 # 简单循环匹配(仅用于演示,数据量大时效率低,应使用向量化或空间索引方法) for (i in 1:nrow(grid_points)) { lon_c <- grid_points$lon_center[i] lat_c <- grid_points$lat_center[i] # 找出落在当前网格(中心点±grid_step/2)内的门店 in_grid <- store_data$longitude >= (lon_c - grid_step/2) & store_data$longitude < (lon_c + grid_step/2) & store_data$latitude >= (lat_c - grid_step/2) & store_data$latitude < (lat_c + grid_step/2) if (any(in_grid)) { heat_data$grid_sales[i] <- sum(store_data$total_sales[in_grid]) } } # 移除完全没有销售额的网格点,减少绘图负担 heat_data <- heat_data[heat_data$grid_sales > 0, ] # 查看热力数据 head(heat_data)

现在,heat_data数据框包含了网格中心坐标和对应的总销售额(热度)。我们可以用geom_tile()来绘制热力图,将grid_sales映射到填充色。

3.3 第三步:叠加气泡饼图图层(门店规模与构成)

接下来,我们在热力图层之上,添加代表每家门店的气泡饼图。我们将使用geom_scatterpie()

# 开始构建ggplot对象 p <- ggplot() + # 1. 首先绘制中国地图轮廓(背景) geom_polygon(data = china_map, aes(x = long, y = lat, group = group), fill = "gray95", color = "gray60", linewidth = 0.2) + # 2. 绘制热力图层(注意:热力图层在地图轮廓之上,但在饼图之下) geom_tile(data = heat_data, aes(x = lon_center, y = lat_center, fill = grid_sales), width = grid_step, height = grid_step, alpha = 0.7) + # alpha设置透明度,避免遮盖地图轮廓 # 设置热力颜色梯度,这里使用从黄到红的渐变色 scale_fill_gradient(low = "yellow", high = "red", name = "区域销售热度\n(万元)") + # 3. 绘制散点饼图图层 geom_scatterpie(data = store_data, aes(x = longitude, y = latitude, r = total_sales/500), # r控制饼图大小,除以500是为了缩放 cols = c("food_sales", "clothing_sales", "electronics_sales"), # 指定构成饼图的列 color = "grey40", # 饼图边缘颜色 alpha = 0.85) + # 饼图整体透明度 # 为饼图各扇区设置填充色 scale_fill_manual(name = "品类构成", values = c(food_sales = "#4DAF4A", # 绿色代表食品 clothing_sales = "#377EB8", # 蓝色代表服装 electronics_sales = "#E41A1C"), # 红色代表电子产品 labels = c("食品", "服装", "电子产品")) + # 4. 调整坐标轴和主题 coord_fixed(ratio = 1.2) + # 固定纵横比,使地图不变形 labs(x = "经度", y = "纬度", title = "全国零售门店销售情况热力-气泡饼图", subtitle = "热力底色:区域总销售热度 | 气泡大小:门店总销售额 | 饼图:品类销售额构成") + theme_minimal() + theme(legend.position = "right", plot.title = element_text(hjust = 0.5, face = "bold"), plot.subtitle = element_text(hjust = 0.5)) # 打印图形 print(p)

代码关键点解析:

  1. 图层顺序:我们按照geom_polygon(地图背景)→geom_tile(热力层)→geom_scatterpie(饼图层)的顺序叠加。这确保了饼图在最上层,不会被热力色块完全遮盖。
  2. geom_scatterpie参数
    • aes(r = total_sales/500):这是实现“气泡”效果的关键。r美学映射到饼图的半径。我们将total_sales除以一个系数(这里是500)来缩放半径,避免饼图过大或过小。这个系数需要根据你的数据范围和图形尺寸反复调整,直到视觉效果最佳。
    • cols = c(...):指定数据框中哪些列是饼图的组成部分。这些列的值应该是数值型。
    • coloralpha:控制饼图边缘线的颜色和整体透明度。设置透明度可以让底下的热力图层隐约可见,增强图表的层次感。
  3. scale_fill_*冲突:注意,我们使用了两个scale_fill_*函数。scale_fill_gradient作用于geom_tile的热力填充色,而scale_fill_manual作用于geom_scatterpie的饼图扇区填充色。ggplot2能够很好地处理这种为不同几何对象设置不同填充标度的情况。
  4. coord_fixed:对于地图,保持经纬度的正确比例(纵横比)非常重要,否则地图会严重变形。ratio参数需要根据地图的经纬度范围调整,1.2是一个常用的近似值,你可以通过尝试找到最合适的值。

运行上述代码,你应该能得到一张初步的复合图表。热力底色展示了销售密集的热区,气泡的大小显示了门店的规模,而每个气泡内的饼图则清晰揭示了该门店的商品销售结构。

4. 进阶优化与美化:让图表更具表现力

第一版的图表已经传达了核心信息,但在美观度和信息传达效率上还有很大的提升空间。下面我们从几个方面进行优化。

4.1 优化热力图层:使用核密度估计与更好的色阶

之前我们用网格求和模拟热度,方法比较粗糙。对于点数据,使用核密度估计(Kernel Density Estimation, KDE)来生成平滑的热力分布是更专业的选择。我们可以使用ggplot2geom_density_2d_filled()stat_density_2d()结合geom_raster()来实现。

同时,热力的颜色梯度也可以优化。viridisplasmamagma等色盲友好、感知均匀的配色方案是更好的选择,它们可以通过scale_fill_viridis_c()函数轻松应用。

# 方法:使用stat_density_2d计算密度,并用geom_raster绘制 # 首先,用stat_density_2d生成一个密度估计的数据框 # 注意:这里使用门店位置和总销售额作为权重,使热力反映销售总额而不仅仅是门店数量 kde_data <- ggplot_build( ggplot(store_data, aes(x = longitude, y = latitude, weight = total_sales)) + stat_density_2d(aes(fill = after_stat(density)), geom = "raster", contour = FALSE, n = 200) )$data[[1]] # 提取计算出的栅格数据 # 重新绘图 p_advanced <- ggplot() + # 中国地图背景 geom_polygon(data = china_map, aes(x = long, y = lat, group = group), fill = "gray97", color = "gray70", linewidth = 0.25) + # 使用KDE生成的热力图层(更平滑) geom_raster(data = kde_data, aes(x = x, y = y, fill = density), alpha = 0.6) + # 使用viridis配色 scale_fill_viridis_c(option = "plasma", name = "销售密度热度", guide = guide_colorbar(barwidth = 0.8, barheight = 10)) + # 气泡饼图图层(保持不变) geom_scatterpie(data = store_data, aes(x = longitude, y = latitude, r = total_sales/500), cols = c("food_sales", "clothing_sales", "electronics_sales"), color = "grey30", alpha = 0.9) + scale_fill_manual(name = "品类构成", values = c(food_sales = "#66C2A5", # 更柔和的颜色 clothing_sales = "#8DA0CB", electronics_sales = "#FC8D62"), labels = c("食品", "服装", "电子产品")) + # 添加比例尺和指北针(使用ggspatial) annotation_scale(location = "bl", width_hint = 0.3, style = "ticks") + annotation_north_arrow(location = "tr", which_north = "true", style = north_arrow_fancy_orienteering) + # 调整坐标和主题 coord_sf(xlim = range(china_map$long), ylim = range(china_map$lat), datum = NA) + # 使用coord_sf并限制范围 labs(x = NULL, y = NULL, # 移除坐标轴标签,让地图更干净 title = "全国零售门店销售深度洞察图", subtitle = "平滑热力层:加权销售密度 | 气泡饼图:门店规模与品类结构", caption = "数据来源:模拟数据 | 制图工具:R ggplot2") + theme_void() + # 使用极简主题 theme(plot.title = element_text(hjust = 0.5, size = 16, face = "bold", margin = margin(b=5)), plot.subtitle = element_text(hjust = 0.5, size = 11, color = "gray40", margin = margin(b=15)), plot.caption = element_text(hjust = 1, size = 9, color = "gray60", margin = margin(t=10)), legend.title = element_text(size = 10, face = "bold"), legend.text = element_text(size = 9), panel.background = element_rect(fill = "aliceblue", colour = NA), # 添加淡蓝色背景模拟海洋 plot.margin = unit(c(0.5, 0.5, 0.5, 0.5), "cm")) print(p_advanced)

优化点说明:

  1. 平滑热力stat_density_2d结合geom_raster生成了基于核密度估计的平滑热度表面,weight = total_sales参数使得密度估计考虑了每家店的销售额权重,结果更准确。
  2. 专业配色scale_fill_viridis_c(option = "plasma")提供了美观且色盲友好的渐变色。
  3. 地图元素annotation_scaleannotation_north_arrow为地图添加了比例尺和指北针,使其更专业。
  4. 主题美化theme_void()移除了所有坐标轴线和标签,让视觉焦点完全集中在数据和地图上。通过panel.background设置了淡蓝色背景,模拟海洋,使陆地轮廓更突出。对标题、图例的字体和边距进行了精细调整。

4.2 处理饼图重叠与视觉混乱:抽样与过滤

当数据点非常密集时,饼图会严重重叠,导致图表无法阅读。有几种策略可以应对:

  • 过滤小规模点:只显示规模大于某个阈值的门店。这能突出重点,减少杂乱。
    store_data_filtered <- store_data %>% filter(total_sales > 300) # 只显示销售额大于300万的门店 # 然后在绘图时使用 store_data_filtered
  • 抽样显示:在密集区域随机抽取部分点显示。
    library(dplyr) # 假设我们只想显示最多50个点,可以随机抽样 set.seed(456) store_data_sampled <- store_data %>% sample_n(min(50, nrow(.)))
  • **调整饼图透明度(alpha)和边缘线(colorlinewidth):降低透明度、加深边缘线颜色、加粗边缘线,可以在重叠时提供更好的区分度。
    geom_scatterpie(..., alpha = 0.7, color = "black", linewidth = 0.3)
  • 使用pie_scale全局缩放:如果所有饼图都太大,可以调整geom_scatterpie()pie_scale参数(默认为1),整体缩小饼图尺寸。
    geom_scatterpie(..., pie_scale = 0.8) # 缩小到80%

在实际项目中,通常需要结合使用以上几种方法。我的经验是,先过滤,再调整视觉参数。优先保证核心信息(主要区域、大规模点)的清晰展示,牺牲一些细节是可接受的。

4.3 添加交互性与可读性:plotly动态化

静态图虽然精美,但交互性可以极大提升探索数据的能力。我们可以使用plotly包将ggplot2图形轻松转化为交互式图表。

library(plotly) # 假设 p_advanced 是我们优化后的ggplot对象 interactive_plot <- ggplotly(p_advanced) # 可以进一步定制plotly的悬停文本(tooltip) # 我们需要为store_data创建一个包含自定义文本的列 store_data_for_plotly <- store_data %>% mutate(hover_text = paste( sprintf("<b>门店ID:</b> %d<br>", store_id), sprintf("<b>位置:</b> (%.2fE, %.2fN)<br>", longitude, latitude), sprintf("<b>总销售额:</b> %.1f 万元<br>", total_sales), sprintf("<b>食品:</b> %.1f 万元 (%.1f%%)<br>", food_sales, 100*food_sales/total_sales), sprintf("<b>服装:</b> %.1f 万元 (%.1f%%)<br>", clothing_sales, 100*clothing_sales/total_sales), sprintf("<b>电子产品:</b> %.1f 万元 (%.1f%%)", electronics_sales, 100*electronics_sales/total_sales) )) # 重新绘制ggplot,但使用新的数据并添加text美学(plotly会识别) p_for_plotly <- ggplot() + # ... [之前的图层,但热力图层和地图背景保持不变] ... geom_scatterpie(data = store_data_for_plotly, # 使用包含悬停文本的数据 aes(x = longitude, y = latitude, r = total_sales/500, text = hover_text), # 添加text映射 cols = c("food_sales", "clothing_sales", "electronics_sales"), color = "grey30", alpha = 0.9) + # ... [其他图层和主题设置] ... theme(legend.position = "none") # plotly有时会重复图例,可以先隐藏 interactive_plot <- ggplotly(p_for_plotly, tooltip = "text") # 指定tooltip来自text美学 # 隐藏plotly默认的模式栏(如果需要) interactive_plot <- interactive_plot %>% config(displayModeBar = FALSE) interactive_plot

现在,当鼠标悬停在任何一个气泡饼图上时,都会弹出一个信息框,详细显示该门店的ID、位置、总销售额以及各品类的销售额和占比。这使图表从一个静态的展示工具,变成了一个动态的数据探索工具。

5. 避坑指南与性能调优

在制作这种复杂图表的过程中,你肯定会遇到一些“坑”。以下是我在实际项目中总结出的常见问题及解决方案。

5.1 饼图大小失控与比例尺难题

问题r美学映射的变量范围过大或过小,导致饼图要么巨大无比覆盖整个地图,要么小到看不见。手动除以一个常数(如/500)很笨拙,且难以在不同数据集间复用。

解决方案:使用标度变换函数进行标准化。ggplot2scale_radius()scale_size_area()(用于geom_point)可以自动处理气泡大小的映射,但geom_scatterpier美学默认没有对应的标度函数。我们可以手动计算一个合理的半径。

一个稳健的方法是,将规模变量映射到面积(Area ∝ r²),并限制在一个合理的像素范围内。例如,我们希望最大饼图的半径对应地图宽度的某个比例(比如1/30)。

# 计算一个自适应的半径 map_width <- diff(range(china_map$long)) max_radius_desired <- map_width / 30 # 期望的最大半径(经度单位) # 假设我们希望面积与 total_sales 成正比 # 面积 A = π * r² ∝ total_sales # 所以 r ∝ sqrt(total_sales) # 我们将其缩放到 [0, max_radius_desired] 区间内 store_data <- store_data %>% mutate(radius_scaled = sqrt(total_sales)) # 先开方 max_sqrt <- max(store_data$radius_scaled, na.rm = TRUE) store_data$radius_scaled <- store_data$radius_scaled / max_sqrt * max_radius_desired # 在绘图中使用 radius_scaled geom_scatterpie(data = store_data, aes(x = longitude, y = latitude, r = radius_scaled), ...)

这种方法确保了饼图大小与数据成比例,并且能自适应不同数据集和图形尺寸。

5.2 图形渲染缓慢与内存占用过高

问题:当数据点很多(比如上千个),或者热力网格非常精细时,图形渲染会变得极其缓慢,保存为高分辨率图片时可能内存不足。

解决方案

  1. 数据聚合:这是最有效的方法。对于门店数据,可以将地理位置非常接近的点在绘图前进行聚合,用其加总数据代表一个“超级门店”。可以使用空间聚类算法(如DBSCAN)或简单的网格聚合。
  2. 简化几何对象:对于地图背景,使用简化后的低精度Shapefile。rnaturalearth包提供的ne_countries(scale = "medium")scale = "small"数据比高精度数据轻量得多。
  3. 降低热力图分辨率:减少stat_density_2dgeom_tile的网格数量(n参数或网格步长grid_step)。
  4. 抽样绘制:如前所述,在密集区域只绘制一部分点。
  5. 使用raggCairo设备保存图片:在保存高质量图片时,使用ragg::agg_png()Cairo::CairoPNG()代替默认的png(),它们通常更快且内存效率更高。
    library(ragg) agg_png("composite_map.png", width = 3000, height = 2000, res = 300) print(p_advanced) dev.off()

5.3 图例冲突与自定义

问题:当图表中有多个填充色标度(热力图的连续色标和饼图的分类色标)时,ggplot2会自动生成两个图例。但有时它们的位置、标题或样式需要调整。

解决方案:使用guides()函数和theme()函数中的legend.*参数进行精细控制。

p_final <- p_advanced + guides( fill = guide_colorbar( # 控制热力填充色图例 title = "销售密度", barwidth = unit(0.5, "cm"), barheight = unit(3, "cm"), title.position = "top" ), fill2 = guide_legend( # 注意:饼图的填充图例在内部被命名为`fill2`,因为第一个`fill`已被热力图占用 title = "产品品类", nrow = 1, # 图例项排成一行 title.position = "top", keywidth = unit(0.8, "cm"), keyheight = unit(0.8, "cm") ) ) + theme( legend.position = "bottom", # 将两个图例都放在底部 legend.box = "horizontal", # 水平排列图例 legend.box.just = "center", legend.spacing.x = unit(1, "cm") # 调整两个图例之间的间距 )

注意geom_scatterpie创建的图例,在存在多个fill标度时,其图例键(legend key)默认可能是正方形而不是圆形。要将其改为圆形,可能需要更复杂的修改,例如在绘制完成后,使用gridcowplot包对图形对象进行后期编辑,这超出了基础调整的范围。一个折中的办法是接受方形图例,或者考虑使用geom_point绘制气泡,再用其他方式添加分类信息(如分面或颜色)。

5.4 坐标系统与地图投影

问题:我们一直使用coord_fixedcoord_sf(datum=NA),这实际上是未经投影的地理坐标系(经纬度)。对于中国全图,变形尚可接受。但如果聚焦某个特定区域(如省份),或者需要更精确的距离/面积表示,就需要使用适当的地图投影。

解决方案:使用sf包处理空间数据,并通过coord_sf()指定投影。例如,使用阿尔伯斯等积圆锥投影(Albers)适用于中国地区。

library(sf) library(rnaturalearth) # 获取中国sf格式地图数据 china_sf <- ne_countries(scale = "medium", country = "china", returnclass = "sf") # 将门店数据转换为sf对象 stores_sf <- st_as_sf(store_data, coords = c("longitude", "latitude"), crs = 4326) # WGS84 # 定义阿尔伯斯投影(参数需调整) crs_albers <- "+proj=aea +lat_1=25 +lat_2=47 +lon_0=105" # 绘图 ggplot() + geom_sf(data = china_sf, fill = "gray95", color = "gray60") + geom_sf(data = stores_sf, aes(size = total_sales, color = food_sales/total_sales)) + # 示例:用颜色表示食品占比 coord_sf(crs = crs_albers) + # 应用投影 # ... 其他自定义 ...

使用投影后,饼图的形状在地图上可能会因为投影变形而不再是完美的圆形(在边缘地区),这是正常现象。geom_scatterpie在非笛卡尔坐标系下可能表现不稳定,此时可能需要考虑其他可视化方案,或者接受轻微的变形。

绘制热力地图复合气泡饼图是一个将数据转化为空间洞察的强力过程。从最初的数据整理、工具包选择,到核心图层的叠加、视觉参数的精细调整,再到最后对重叠、性能、图例等棘手问题的处理,每一步都需要结合数据特点和对图形语法的理解。我个人的体会是,这种复杂图表成功的关键在于层次分明重点突出。热力层是背景,提供全局趋势;气泡饼图是前景,展示个体细节。一定要通过颜色、大小、透明度的调节,确保前景信息不会被背景淹没,同时背景又能有效衬托前景。

最后一个小技巧:在最终出图前,务必在不同的设备上(尤其是投影仪)预览你的图表。有些在电脑屏幕上看起来不错的颜色对比,在大屏幕上可能变得难以区分。多测试,多调整,才能做出既专业又实用的数据可视化作品。

← 返回列表