引言
这篇文章我们将使用 MATLAB 复现在 2024高教社杯全国大学生数学建模竞赛C题论文展示 中绘制的双重饼图。具体来说:我们将一步一步讲解如何读取 2024 高教社杯全国大学生数学建模竞赛 C 题附件 1.xlsx,并根据附件 1.xlsx 中提供的数据绘制如下的双重饼图: 
绘制的思路如下:创建两个不同大小的坐标轴,分别绘制外圈和内圈饼图,然后通过控制坐标轴的 Position 实现大小嵌套。
基本数据处理
📂 Excel 表格的读取
MATLAB 中提供多种方法来读取 Excel 表格,包括 readtable、xlsread、xlsreadtable 等。其中 readtable 是 MATLAB R2013b 引入的现代函数,返回表格数据类型。
matlab
T = readtable(filename, 'Sheet', sheet, 'Range', range)
优势:
- 自动识别列名
- 支持混合数据类型
- 更好的数据组织
- 支持更多选项
- 支持中文列名
所以这里我们选用 readtable 函数来读取 Excel 表格。因为在附件 1.xlsx 中的列名存在中文,所以在读取附件 1.xlsx 时,我们需要指定 'VariableNamingRule' 属性为 'preserve'。
matlab
data = readtable('附件1.xlsx', 'VariableNamingRule', 'preserve'); % 读取Excel,保留中文列名
head(data(:, 1:3), 5) % 查看前3列前5行数据
💻 运行结果
bash>>head(data(:, 1:3), 5) 地块名称 地块类型 地块面积/亩 _______ _________ __________ {'A1'} {'平旱地'} 80 {'A2'} {'平旱地'} 55 {'A3'} {'平旱地'} 35 {'A4'} {'平旱地'} 72 {'A5'} {'平旱地'} 68
⚠️ 注意 在读取附件 1.xlsx 时,一定要记得先把附件 1.xlsx 复制到 MATLAB 工作目录下。 2024高教社杯全国大学生数学建模竞赛真题下载链接
📊 数据处理
我们需要计算每种类型地的总面积,同时需要把相同类型的地块分组保存起来,方便后续的绘制双重饼图。
第一步:确定要提取的地块类型和绘制顺序
matlab
target_names = {'水浇地','山坡地', '梯田', '平旱地'}; % 要提取的地块类型(也是绘制顺序)
第二步:分组保存地块类型并计算总面积
接下来我们使用 strcmp 函数来判断每个地块的类型,然后根据类型将地块分组保存起来,并使用 sum 函数来计算每个类型的总面积。
matlab
inner_vals = zeros(1, length(target_names)); % 预分配:各类型总面积
outer_vals_cell = cell(length(target_names), 1); % 预分配:各类型面积数组
for i = 1:length(target_names)
idx = strcmp(data.("地块类型"), target_names{i}); % 匹配该类型的行
outer_vals_cell{i} = data.("地块面积/亩")(idx); % 保存该类型各块面积
inner_vals(i) = sum(outer_vals_cell{i}); % 计算该类型总面积
end
outer_vals = vertcat(outer_vals_cell{:}); % 拼成外圈饼图的数据
注意点
- 预先分配内存:预先分配内存,避免在循环中动态分配内存,提高效率。
- 中文列名的访问 :因为列名是中文,所以在使用
strcmp函数时,需要使用双引号来包裹列名(data.("地块类型")),不能直接使用点号来访问列名。- 垂直拼接 :使用
vertcat函数将所有类型的面积数值数组垂直拼接起来,方便后续的绘制。
这样我们就得到了需要的数据,可以开始绘制双重饼图了。
绘制双重饼图
🎨 定义颜色
对于内外圈饼图,我们需要采用不同方式来定义颜色
内圈饼图颜色
对于内圈饼图,我们直接使用矩阵定义不同的颜色来表示不同的地块类型。
matlab
inner_colors = [ % 内圈颜色:每种地块类型一种颜色(RGB)
0.88, 0.48, 0.44; % 红色/粉色
0.58, 0.48, 0.72; % 紫色
0.20, 0.65, 0.38; % 绿色
0.96, 0.80, 0.20; % 黄色
];
如果你有其他颜色方案,也可以根据需要修改 inner_colors 矩阵。或者你想知道更多科研绘图的颜色方案,可以参考 科研色彩探索平台,里面有非常多的顶级期刊的配色方案。
外圈饼图颜色
对于外圈饼图,我们采用渐变色来定义,渐变的公式如下:
Color=blend_factor×base_col+(1−blend_factor)×1,1,1
其中 base_col 为对应的内圈饼图的颜色, blend_factor 为渐变因子,取值范围为 0.9 到 0.4,按相同类型地块的数量进行线性采样。最后我们将所有类型的渐变色垂直拼接起来,方便后续的绘制。。
matlab
outer_colors = cell(1, length(inval)); % 预分配:存放各类型渐变色
for i = 1:length(inval)
n_sub = length(outer_vals{i}); % 该类型的地块数量
base_col = inner_colors(i, :); % 以对应内圈颜色为基色
blend_factor = linspace(0.9, 0.4, n_sub)'; % 渐变因子 0.9→0.4 线性采样
outer_colors{i} = blend_factor * base_col + (1 - blend_factor) * [1 1 1]; % 向白色渐变
end
outer_colors = vertcat(outer_colors{:}); % 拼接所有渐变色
⭕ 绘制外圈饼图
绘制前,我们需要先创建一个 figure 对象,设置其颜色为白色,位置为 [100, 100, 600, 600],并创建一个坐标轴 ax_outer,设置其位置为 [0.1, 0.1, 0.8, 0.8],作为外圈饼图的坐标轴。并设置 'DataAspectRatio' 属性为 [1 1 1],确保饼图的大小是等比例的。
matlab
figure('Color', 'w', 'Position', [100, 100, 600, 600]); % 新建白色窗口(600×600)
ax_outer = axes('Position', [0.1 0.1 0.8 0.8], 'DataAspectRatio', [1 1 1]); % 外圈坐标轴(占窗口80%)
!TIP
Position属性详解
'Position', [100, 100, 600, 600]表示figure对象:
- 左边缘距离电脑屏幕最左侧的距离为 100 像素;
- 图形窗口的底边缘距离电脑屏幕最底部的距离为 100 像素;
- 宽度为 600,高度为 600。
'Position', [0.1, 0.1, 0.8, 0.8]表示ax_outer坐标轴:
- 左边缘距离图形窗口左侧边缘的距离,占整个窗口宽度的 10%;
- 坐标轴的底边缘距离图形窗口底部边缘的距离,占整个窗口高度的 10%;
- 坐标轴本身的总宽度占整个窗口宽度的 80%;
- 总高度占整个窗口高度的 80%。
然后我们在坐标轴 ax_outer 上使用 pie 函数来绘制外圈饼图。使用 repmat 来使每个饼图的标签为空字符串,从而隐藏标签。
matlab
pie(ax_outer,outer_vals, repmat({''}, size(outer_vals))) % 绘制外圈饼图并隐藏标签
设置外圈饼图颜色和边框
接下来我们来设置外圈饼图的颜色和边框。使用 findobj 函数来找到 ax_outer 坐标轴上的所有类型的 patch 对象(即多边形对象),在这里就是我们刚刚绘制的饼图,然后使用 set 函数来设置颜色和边框颜色。
FaceColor:设置多边形的填充颜色。EdgeColor:设置多边形的边框颜色。LineWidth:设置多边形的边框宽度。
matlab
h_outer = findobj(ax_outer, 'Type', 'patch'); % 获取饼图的多边形对象
set(h_outer, {'FaceColor'}, num2cell(flipud(outer_colors), 2)); % 设置填充颜色(patch顺序倒序需翻转)
set(h_outer, 'EdgeColor', 'white', 'LineWidth', 0.6); % 设置白色边框
⚠️ 注意
- MATLAB 的
pie函数返回的图形对象(patch)顺序是倒序的(第一个对象对应最后一个数据),因此设置颜色时必须用flipud()翻转颜色矩阵,否则颜色分组会错位。- 我们使用
num2cell()将颜色矩阵转换为元胞数组,把颜色矩阵按行(维度 2)进行切分,把每一行(即每一个 RGB 颜色)单独打包进一个 cell 单元里,最终变成一个由多个 RGB 数组组成的元胞数组。
我们可以先查看目前的效果,如下所示: 
接下来我们要在另一个坐标轴 ax_inner 上绘制内圈饼图,直接覆盖外圈饼图,形成我们想要的双重饼图的效果。
⭕ 绘制内圈饼图
我们定义内圈饼图的坐标轴 ax_inner,通过 inner_size 定义内圈饼图的大小,并根据大小设置偏移量 offset,来确保内圈饼图的中心与外圈饼图的中心重合。计算的示意图如下:
matlab
inner_size = 0.56; % 内圈相对大小,调小可让外圈更宽
offset = (1 - inner_size) / 2; % 偏移量:使内外圈中心重合
ax_inner = axes('Position', [offset offset inner_size inner_size],'DataAspectRatio', [1 1 1]); % 内圈坐标轴
现在我们在坐标轴 ax_inner 上使用 pie 函数来绘制内圈饼图。使用 repmat 来使每个饼图的标签为空字符串,从而隐藏标签。并设置颜色为定义的内圈饼图的颜色,边框颜色为白色,边框宽度为 1.2。
matlab
h = pie(ax_inner,inner_vals,repmat({''}, size(inner_vals))); % 绘制内圈饼图,保存句柄便于加标签
h_inner = findobj(ax_inner, 'Type', 'patch'); % 获取内圈多边形对象
set(h_inner, {'FaceColor'}, num2cell(flipud(inner_colors), 2)); % 设置颜色(倒序需翻转)
set(h_inner, 'EdgeColor', 'white', 'LineWidth', 1.2); % 设置白色边框

pie的函数句柄h的奇数项是多边形对象,偶数项是标签对象。我们这里将内圈饼图的函数句柄赋值保存下来,方便添加标签。
现在我们来查看一下效果:
添加标签
现在离我们想要的效果已经很近了,我们只需要添加标签即可。但是 MATLAB 中的 pie 的标签的位置是自动的,我们需要手动调整标签的位置,才能使标签位于扇形内。下面我们对于每一个扇形进行处理。
📍 标签的位置
我们先使用 pie 的函数句柄 h 获取第 i 个扇形的多边形对象的 x 坐标和 y 坐标。
matlab
patch_obj = h(2*i - 1); % 第i个扇形对象(h的奇数项是多边形)
x = patch_obj.XData; % 扇形顶点x坐标
y = patch_obj.YData; % 扇形顶点y坐标
我们可以画出这个多边形来查看效果:
matlab
plot(x,y,'r--o')
结果如下:
可以看见,MATLAB 就是通过绘制这些散点围成的多边形来实现绘制扇形的。
接下来我们计算扇形的中心位置。首先我们计算扇形的中心角度,计算方法如下:
- 我们先去掉 x 坐标和 y 坐标都为 0 的点,因为这些点在圆心,我们不需要计算。
- 我们计算剩余点的平均点的角度,即扇形的中心角度。
代码如下:
matlab
mask = ~(x == 0 & y == 0); % 剔除圆心(0,0)处的点
x_arc = x(mask); % 弧线上各点x坐标
y_arc = y(mask); % 弧线上各点y坐标
mid_angle = atan2(mean(y_arc), mean(x_arc)); % 弧线平均角度≈扇形中心角
接下来因为 pie 在坐标系中默认半径为 1,所以我们令标签所在的位置的半径为 r_internal = 0.55,来保证标签位于扇形内。最后使用极坐标来计算标签的位置。
matlab
r_internal = 0.55; % 标签半径(保证位于扇形内)
text_x = r_internal * cos(mid_angle); % 极坐标转直角坐标
text_y = r_internal * sin(mid_angle);
✍️ 写标签
标签属性位于 pie 函数句柄 h 的偶数项,所以我们需要使用 h(2*i) 来获取标签对象。使用 sprintf 来格式化标签字符串,包括标签的名称和百分比(对于 sprintf 的用法可以查看文章 MATLAB 基本语法指南)。最后使用 set 设置标签属性:
- String:标签字符串
- Position:标签的位置
- HorizontalAlignment:标签的水平对齐方式
- VerticalAlignment:标签的垂直对齐方式
- Color:标签的颜色
- FontSize:标签的字体大小
matlab
text_obj = h(2*i); % 第i个标签对象(h的偶数项是标签)
str_label = sprintf('%s\n%.1f%%', target_names{i}, ...
inner_vals(i)/total_val*100);% 标签内容:类型名+面积占比
set(text_obj, ...
'String', str_label, ...
'Position', [text_x, text_y, 0], ... % 将标签放到扇形中心
'HorizontalAlignment', 'center', ...
'VerticalAlignment', 'middle', ...
'Color', 'black', ...
'FontSize', 8);
📜 完整的循环过程
matlab
r_internal = 0.55; % 标签半径(扇形内)
total_val = sum(inner_vals); % 总面积,用于计算百分比
for i = 1:length(inner_vals)
patch_obj = h(2*i - 1); % 第i个扇形对象
x = patch_obj.XData; % 顶点x坐标
y = patch_obj.YData; % 顶点y坐标
mask = ~(x == 0 & y == 0); % 剔除圆心点
x_arc = x(mask);
y_arc = y(mask);
mid_angle = atan2(mean(y_arc), mean(x_arc)); % 扇形中心角
text_x = r_internal * cos(mid_angle); % 标签位置(极坐标转直角坐标)
text_y = r_internal * sin(mid_angle);
text_obj = h(2*i); % 第i个标签对象
str_label = sprintf('%s\n%.1f%%', target_names{i}, ...
inner_vals(i)/total_val*100); % 名称+面积占比
set(text_obj, ... % 设置标签样式与位置
'String', str_label, ...
'Position', [text_x, text_y, 0], ...
'HorizontalAlignment', 'center', ...
'VerticalAlignment', 'middle', ...
'Color', 'black', ...
'FontSize', 8);
end
现在我们来看看效果:

这样就已经实现了我们想要的效果。
不足
2024高教社杯全国大学生数学建模竞赛C题论文展示中的双重饼图:

我们在外围饼图颜色的变化没有完美复现出来。现在我还无法知道如何使用 MATLAB 实现完全一样的外围颜色效果。如果有深入研究的大佬,欢迎指导。
作者:majorArcs 原文链接:k-hengzhou.github.io/posts/MATLA... 完整源码下载