目录
前言
在MATLAB中,使用plot、scatter和patch等函数进行绘图时,很多时候都需要使用标记(Marker)对数据点进行标记,以便更清晰地展示数据。在MATLAB只能使用默认的标记形状(如下图),但有时觉得MATLAB自带的标记不好看或者不够用时就需要自定义标记形状,而MATLAB中并没有提供自定义标记这样一个功能或者函数。
本文章提供了一个可以自定义标记形状的函数,只需要输入标记形状的点坐标和连接方式即可绘制出与MATLAB自带标记基本一致的自定义标记,包括随着绘图区的缩放,标记在屏幕上的大小不会改变,以及绘图区边缘的标记可以超出绘图区等功能,同时可以设置该标记的颜色、磅数、线宽等基本属性。
自定义标记函数
自定义标记的主函数如下,该函数目前只能绘制没有交叉线的图形。前5个参数为必须输入,包括ax绘图区对象,曲线的x和y坐标,标记形状的点坐标GPoints(第一列为x坐标,第二列为y坐标),形状点的连接顺序(不需要首尾相连)GSeq。
其他可设置参数为填充的面颜色faceColor,填充的线颜色edgeColor,形状的大小pounds,形状的线宽度lineWidth,形状的数量NMarker(近似数量, 采用等间距, 如不能整除则四舍五入,输入0则表示绘制所有点的标记)。同时,若不需要则以空数组[ ]代替, 如不需要形状磅数pounds, 则pounds = [ ]。
function setMarker(ax, x, y, GPoints, GSeq, faceColor, edgeColor, pounds, lineWidth, NMarker)
% 自定义设置标记形状(目前只能绘制没有交叉线的图形), 前5个参数为必须输入, 其他参数为可选参数
% 若不需要则以空数组[]代替, 如不需要形状磅数pounds, 则pounds = []
%
% ax 绘图区
% x,y 绘图的数据点
% GPoints 形状的点坐标
% GSeq 形状点的连接顺序(不需要首尾相连)
% faceColor 填充的面颜色 默认:none
% edgeColor 填充的线颜色 默认:k
% pounds 形状的大小(磅数) 默认:6
% lineWidth 形状的线宽度 % 默认:0.5
% NMarker 形状的数量(近似数量, 采用等间距, 如不能整除则四舍五入)
% 默认:0, 0则代表全部点都绘制
if nargin < 6, faceColor = 'none'; end
if nargin < 7, edgeColor = 'k'; end
if nargin < 8, pounds = 6; end
if nargin < 9, lineWidth = 0.5; end
if nargin < 10, NMarker = 0; end
if isempty(faceColor), faceColor = 'none'; end
if isempty(edgeColor), edgeColor = 'k'; end
if isempty(pounds), pounds = 6; end
if isempty(lineWidth), lineWidth = 0.5; end
if isempty(NMarker), NMarker = 0; end
if NMarker == 0, NMarker = length(x); end
hold(ax,"on")
NG = length(GSeq); % 点数量
% 将图形的比例拉正
xl = xlim;
yl = ylim;
lenx = xl(2)-xl(1); % x轴的长度
leny = yl(2)-yl(1); % y轴的长度
yRatio = leny/lenx; % 纵横比比例
GPoints(:,2) = GPoints(:,2)*yRatio; % 修正坐标
% 获取绘图区的每单位在屏幕上的长度(磅)
unitLength = getUnitLength();
% 计算水平距离矩阵
W = zeros(NG);
for i = 1:NG
for j = 1:NG
if i ~= j
W(i,j) = sqrt((GPoints(i,1)-GPoints(j,1))^2);
end
end
end
% 缩放比例
scale = 2*pounds/(max(max(W))*unitLength);
% 缩放
GPoints = GPoints*scale;
PM = 1:round(length(x)/NMarker):length(x); % 需要绘制的点位置
NMarker = length(PM); % 绘制标记的真实数量
% 重心坐标
[Cx, Cy] = polygon_centroid(GPoints);
% 初始化所有标记的坐标和连接顺序
GPointsAll = zeros(NMarker*NG,2);
GSeqAll = zeros(NMarker,NG);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标和连接顺序
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
GSeqAll(k,:) = GSeq + NG*(k-1);
%%% 判断当前标记点是否处于边界 %%%
xkk = GPointsNew(:, 1); % 当前标记点的x坐标
ykk = GPointsNew(:, 2); % 当前标记点的y坐标
% 标记形状坐标判断
BLeft = xkk - xl(1); % 左边界
BRight = xkk - xl(2); % 右边界
BDown = ykk - yl(1); % 下边界
BUp = ykk - yl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - xl(1); % 左边界
BRight2 = xl(2)-xk; % 右边界
BDown2 = yk - yl(1); % 下边界
BUp2 = yl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>=0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
% 绘制标记点
pa = patch('Faces', GSeqAll, 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'lineWidth',lineWidth);
% 设置绘图区边缘图形不裁剪
pa2 = patch('Faces', GSeqAll(BJudge,:), 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth, 'Clip', 'off');
%%%% 控制图形放大缩小 %%%%
zm = zoom(ax);
zm.Enable = 'on'; % 启用缩放
% 初始化xy轴范围
xl = xlim;
yl = ylim;
setappdata(ax,'lxl',xl)
setappdata(ax,'lyl',yl)
% 绘图区缩放回调函数
set(zm, 'ActionPostCallback', @(src, event) updateZoomInfo(ax,x,y,PM,NG,NMarker,pa,pa2));
end
自定义标记函数的说明
纵横比调整
在自定义形状时,如果x轴和y轴的刻度一致,则绘制出来的图形与实际一致。大多数情况下,MATLAB绘制图形时一般x轴和y轴的刻度并不一致,比如x轴一格长度为2,y轴一格长度为500,这时绘制出来的图形看起来会呈现上下被压扁的性质。比如,原本需要绘制的形状是下图这个四边形,但是因为x轴和y轴的刻度不一致,会出现绘制出来的形状只呈现出一条线(下面第二张图)。
为了避免这种情况,首先就要对图形比例进行修正,即获取x轴和y轴刻度之间的比例,对标记图形的x或者y坐标进行拉伸或者收缩。
% 将图形的比例拉正
xl = xlim;
yl = ylim;
lenx = xl(2)-xl(1); % x轴的长度
leny = yl(2)-yl(1); % y轴的长度
yRatio = leny/lenx; % 纵横比比例
GPoints(:,2) = GPoints(:,2)*yRatio; % 修正坐标
下面是调整后的效果图。
将图形大小按磅数设置
放缩了x轴或者y轴的比例后,会出现标记图形过大或者过小的情况,这时就需要对标记的坐标进行放缩。在MATLAB中,标记的大小(MarkerSize)通常以磅数为单位,默认大小为6磅。同样参考MATLAB的方式,我们绘制图形也可以用磅数设置图形大小,但并不知道绘图区单位长度对应的磅数大小是多少。因此,首先要计算绘图区中的单位磅数。
下面的函数中,Position(3)对应的就是x轴的总磅数,除x轴的总长度即可得到绘图区中的单位磅数。在上图中,绘图区中的单位磅数为50.9091磅。
function unitLength = getUnitLength()
% 获取绘图区的每单位在屏幕上的长度(磅)
% 获取图形窗口的大小和位置
Po = get(gcf, 'Position'); % 返回的单位是点 (points),每个点约为 1/72 英寸
figWidth = Po(3); % 图形的宽度
% X轴范围
xl = xlim;
rangeX = xl(2) - xl(1);
% 绘图区的每单位在屏幕上的长度(磅)
unitLength = figWidth/rangeX;
end
得到了绘图区中的单位磅数后就可以以某一个标准(比如图形的水平长度、竖直长度和最长边的长度等)对图形的坐标进行放缩。本文采用图形的水平长度L为标准,将长度L乘单位长度unitLength可以得到图形当前的磅数,由于MATLAB中图形的大小是以半径为参考的,故需要在前面乘2。
% 获取绘图区的每单位在屏幕上的长度(磅)
unitLength = getUnitLength();
% 水平长度矩阵
L = max(GPoints(:,1)) - min(GPoints(:,1));
% 缩放比例
scale = 2*pounds/(L*unitLength);
% 缩放
GPoints = GPoints*scale;
设置磅数pounds = 6,可以得到下面的效果图(红色为自定义标记,黑色为MATLAB自带的三角形标记),可以看到,自定义标记与MATLAB自带标记的大小一致。
平移标记点
已知标记点初始的点坐标和连接顺序后,还需要将标记点平移到相应的位置。
NMarker为需要绘制的标记点数量,为得到标记点的位置,需要将整个数据区间均分成NMarker份。但是,输入的NMarker并不一定能均分,因此文章采用一种四舍五入的方法,即下面的round(length(x)/NMarker),以及更新NMarker的数量。
% 计算绘制的点位置
PM = 1:round(length(x)/NMarker):length(x); % 需要绘制的点位置
NMarker = length(PM); % 绘制标记的真实数量
得到了绘制标记点的位置后,需要将标记平移到该位置。本文以标记图形的重心作为参考,计算重心到该位置的位移,再到坐标进行更新。
计算多边形的重心方法如下:
- 计算面积
A
A
A
假设一个多边形的顶点坐标为 ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x n , y n ) (x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n) (x1,y1),(x2,y2),…,(xn,yn),连接顺序为 1 − n − 1 1-n-1 1−n−1,则该多边形的面积 A A A为
A = 1 2 ∣ ∑ i = 1 n ( x i y i + 1 − x i + 1 y i ) ∣ A = \frac{1}{2} \left| \sum_{i=1}^{n} (x_i y_{i+1} - x_{i+1} y_i) \right| A=21 i=1∑n(xiyi+1−xi+1yi) 其中 ( x n + 1 , y n + 1 ) (x_{n+1}, y_{n+1}) (xn+1,yn+1)被定义为 ( x 1 , y 1 ) (x_1, y_1) (x1,y1)。 - 计算重心的坐标
(
C
x
,
C
y
)
(C_x, C_y)
(Cx,Cy)
重心 C C C的坐标 ( C x , C y ) (C_x, C_y) (Cx,Cy)可通过以下公式计算:
C x = 1 6 A ∑ i = 1 n ( x i + x i + 1 ) ( x i y i + 1 − x i + 1 y i ) C y = 1 6 A ∑ i = 1 n ( y i + y i + 1 ) ( x i y i + 1 − x i + 1 y i ) C_x = \frac{1}{6A} \sum_{i=1}^{n} (x_i + x_{i+1})(x_i y_{i+1} - x_{i+1} y_i) \\ C_y = \frac{1}{6A} \sum_{i=1}^{n} (y_i + y_{i+1})(x_i y_{i+1} - x_{i+1} y_i) Cx=6A1i=1∑n(xi+xi+1)(xiyi+1−xi+1yi)Cy=6A1i=1∑n(yi+yi+1)(xiyi+1−xi+1yi)根据上面的计算过程,计算重心的函数polygonCentroid如下:
function [Cx, Cy] = polygonCentroid(points)
% 计算形状的重心坐标
% points 一个n x 2 的矩阵, 表示n个点的坐标, 第一列为x值, 第二列为y值
% 点的数量
n = size(points, 1);
% 闭合多边形
points = [points; points(1, :)];
% 初始化面积和重心坐标
A = 0;
Cx = 0;
Cy = 0;
% 计算面积和重心坐标
for i = 1:n
xi = points(i, 1);
yi = points(i, 2);
xi1 = points(i+1, 1);
yi1 = points(i+1, 2);
% 计算有向面积
A_temp = xi * yi1 - xi1 * yi;
A = A + A_temp;
% 计算重心坐标
Cx = Cx + (xi + xi1) * A_temp;
Cy = Cy + (yi + yi1) * A_temp;
end
A = A / 2; % 最终面积取绝对值
Cx = Cx / (6 * A);
Cy = Cy / (6 * A);
end
得到了重心坐标后,接着进行平移。GPointsAll和GSeqAll用于存储所有标记点的坐标和连接方式,方便一次性绘图,在循环中绘图会影响绘图速度。
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标和连接顺序
GPointsAll = zeros(NMarker*NG,2);
GSeqAll = zeros(NMarker,NG);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标和连接顺序
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
GSeqAll(k,:) = GSeq + NG*(k-1);
end
绘制标记点
绘制标记点采用patch函数,patch函数其中一个用法如下:
- patch(‘Faces’,F,‘Vertices’,V) 创建一个或多个多边形,其中 V 指定顶点的值,F 定义要连接的顶点。
FaceColor、EdgeColor和LineWidth分别表示面的颜色、线的颜色和线的宽度。此外,patch是MATLAB中绘制多边形非常强大的函数,其他用途可以参考下面这几篇文章。
【MATLAB学习笔记】绘图——errorbar误差图+patch误差填充图
【有限元学习笔记】二维四边形单元位移云图可视化(有限元后处理)——MATLAB程序
【有限元学习笔记】二维四边形单元应力动画云图可视化(高斯点应力外推、应力绕节点平均)——MATLAB程序
% 绘制标记点
pa = patch('Faces', GSeqAll, 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth);
边界标记点不裁剪
此外,注意到处于边界处的标记点(红色)被边界裁剪了,而MATLAB自带的标记点(黑色)并没有被裁剪。
要想实现这个效果只需要将patch中的Clipping属性设置为 “off” 即可。
% 绘制标记点
pa = patch('Faces', GSeqAll, 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth, 'Clipping', 'off');
拓展功能——标记点自适应绘图区的缩放
绘图区缩放回调函数
要想标记点自适应绘图区的缩放而变化,就需要采用回调函数对一些参数进行更改。简单来说,回调函数就是用户在执行某一项操作时自动执行的函数。
下面利用回调函数监控了绘图区的缩放,当绘图区进行缩放时就会自动执行updateZoomInfo这个函数。setappdata函数可以将变量数据动态保存,方便在updateZoomInfo函数中使用这个数据,下面代码中保存了x轴范围xl和y轴范围yl。
%%%% 控制图形放大缩小 %%%%
zm = zoom(ax);
zm.Enable = 'on'; % 启用缩放
% 初始化xy轴范围
xl = xlim;
yl = ylim;
setappdata(ax,'lxl',xl)
setappdata(ax,'lyl',yl)
% 绘图区缩放回调函数
set(zm, 'ActionPostCallback', @(src, event) updateZoomInfo(ax,x,y,PM,NG,NMarker,pa,pa2));
回调函数updateZoomInfo的代码如下:
function updateZoomInfo(ax,x,y,PM,NG,NMarker,pa,pa2)
% 监控绘图区缩放并自动更新标记图形的回调函数
%
% ax 绘图区
% x,y 绘图的数据点
% PM 需要绘制的点位置
% NG % 点数量
% NMarker 形状的数量
% pa patch对象
% pa2 边缘点的patch对象
% 当前xy轴范围
cxl = xlim(ax);
cyl = ylim(ax);
% 获取上一次缩放的xy轴范围
lxl = getappdata(ax,'lxl');
lyl = getappdata(ax,'lyl');
% 保存当前的xy轴范围
setappdata(ax,'lxl',cxl)
setappdata(ax,'lyl',cyl)
% 计算xy轴放大因子
xScaleFactor = (lxl(2) - lxl(1))/(cxl(2) - cxl(1));
yScaleFactor = (lyl(2) - lyl(1))/(cyl(2) - cyl(1));
% 获取当前的节点坐标和连接顺序
GPoints = pa.Vertices;
GSeq = pa.Faces;
GPoints = GPoints(1:NG,:);
% 缩放坐标
GPoints(:,1) = GPoints(:,1)/xScaleFactor;
GPoints(:,2) = GPoints(:,2)/yScaleFactor;
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标
GPointsAll = zeros(NMarker*NG,2);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
%%% 判断当前标记点是否处于边界 %%%
% 标记形状坐标判断
BLeft = GPointsNew(:, 1) - cxl(1); % 左边界
BRight = GPointsNew(:, 1) - cxl(2); % 右边界
BDown = GPointsNew(:, 2) - cyl(1); % 下边界
BUp = GPointsNew(:, 2) - cyl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - cxl(1); % 左边界
BRight2 = cxl(2)-xk; % 右边界
BDown2 = yk - cyl(1); % 下边界
BUp2 = cyl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
% 更新坐标
pa.Vertices = GPointsAll;
% 设置绘图区边缘图形不裁剪
pa2.Vertices = GPointsAll;
pa2.Faces = GSeq(BJudge,:);
end
标记点大小自适应
在缩放绘图区时,自定义标记的大小并不会自己改变,而MATLAB自带的标记大小会自动更新,如下图。
要想实现这种效果就需要在回调函数中计算当前x轴和y轴的放大比例,在对标记点的坐标进行缩放。其中getappdata表示获取上一次缩放的x轴和y轴范围,setappdata保存当前的x轴和y轴范围。计算得到了放大因子后,对标记图形的x坐标和y坐标分别缩放即可。
% 当前xy轴范围
cxl = xlim(ax);
cyl = ylim(ax);
% 获取上一次缩放的xy轴范围
lxl = getappdata(ax,'lxl');
lyl = getappdata(ax,'lyl');
% 保存当前的xy轴范围
setappdata(ax,'lxl',cxl)
setappdata(ax,'lyl',cyl)
% 计算xy轴放大因子
xScaleFactor = (lxl(2) - lxl(1))/(cxl(2) - cxl(1));
yScaleFactor = (lyl(2) - lyl(1))/(cyl(2) - cyl(1));
% 获取当前的节点坐标和连接顺序
GPoints = pa.Vertices;
GSeq = pa.Faces;
GPoints = GPoints(1:NG,:);
% 缩放坐标
GPoints(:,1) = GPoints(:,1)/xScaleFactor;
GPoints(:,2) = GPoints(:,2)/yScaleFactor;
得到了缩放后的坐标后,需要重新计算新的平移位置。其中%%% 判断当前标记点是否处于边界 %%%部分见下一小节。
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标
GPointsAll = zeros(NMarker*NG,2);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
%%% 判断当前标记点是否处于边界 %%%
% 标记形状坐标判断
BLeft = GPointsNew(:, 1) - cxl(1); % 左边界
BRight = GPointsNew(:, 1) - cxl(2); % 右边界
BDown = GPointsNew(:, 2) - cyl(1); % 下边界
BUp = GPointsNew(:, 2) - cyl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - cxl(1); % 左边界
BRight2 = cxl(2)-xk; % 右边界
BDown2 = yk - cyl(1); % 下边界
BUp2 = cyl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
最后,更新一下patch中的点数据。
% 更新坐标
pa.Vertices = GPointsAll;
放大后的效果图如下。可见,随着绘图区的缩放,自定义标记在屏幕上的大小并不会发生改变,与MATLAB自带的标记一致。
标记点裁剪自适应
将原本patch中的Clipping属性设置为 “off”,虽然在初始图形中显示为不裁剪,但是在图形放大时会出现标记点偏离绘图区很多的情况,如下图。
因此,不能直接将原来的patch函数中的Clipping属性设置为 “off” ,而是需要创建一个新的patch函数,只针对边界处的标记点设置成不裁剪模式。首先,需要判断标记点是否处于边界处,判断准则有两个:
- 重心点位于绘图区内;
- 标记图形的任意点超出绘图区。
基于此准则,将平移标记点部分的最后一个代码块修改如下(回调函数部分的也是如此,只不过该部分在上一小节中已经提前改好了)。其中BJudge用于判断该标记点是否处于边界处,若处于边界处则为true,反之则为false。
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标和连接顺序
GPointsAll = zeros(NMarker*NG,2);
GSeqAll = zeros(NMarker,NG);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标和连接顺序
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
GSeqAll(k,:) = GSeq + NG*(k-1);
%%% 判断当前标记点是否处于边界 %%%
% 标记形状坐标判断
BLeft = GPointsNew(:, 1) - xl(1); % 左边界
BRight = GPointsNew(:, 1) - xl(2); % 右边界
BDown = GPointsNew(:, 2) - yl(1); % 下边界
BUp = GPointsNew(:, 2) - yl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - xl(1); % 左边界
BRight2 = xl(2)-xk; % 右边界
BDown2 = yk - yl(1); % 下边界
BUp2 = yl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>=0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
上面代码中的judgeSign为判断数组中的元素是否为同号的函数,如果是则返回ture,反之则返回false。
function flag = judgeSign(arr)
% 判断数组中所有元素是否同号(包含0也算同号)
% 计算数组每个元素的符号
signs = sign(arr(arr~=0));
% 获取唯一符号
uniqueSigns = unique(signs);
% 判断是否只有一个唯一符号,并且该符号不为零
if length(uniqueSigns) == 1 && uniqueSigns ~= 0
flag = true; % 同号
else
flag = false; % 不同号或包含零
end
end
得到BJudge数组后,将边界处的标记点的’Clipping’设置为’off’。
% 设置绘图区边缘图形不裁剪
pa2 = patch('Faces', GSeqAll(BJudge,:), 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth, 'Clipping', 'off');
最后,在回调函数中更新点数据和连接顺序。
% 设置绘图区边缘图形不裁剪
pa2.Vertices = GPointsAll;
pa2.Faces = GSeq(BJudge,:);
效果图如下,可见图形放大时不会导致出现标记点偏离绘图区很多的情况了。
示例
基本绘图
下面这是基本的绘图代码,具体细节可以参考前面的文章(点击转跳)。
clc;clear;close all
set(0,'defaultfigurecolor','w');
%% 数据
x = -1:1:10; % 产生0到1, 步长为0.01的序列
y = 2*x.^3 + 1; % y为x一次函数
%% 绘图
f = figure(1);
ax = gca; % 将当前坐标区实例化
plot(x,y,'-k','LineWidth',1.3)
xlim([-1,10])
set(gca,'FontName','Times New Roman','FontSize',13)
xlabel('\fontname{宋体} 位移\fontname{Times New Roman} \it x/\rm mm')
ylabel('\fontname{宋体} 力\fontname{Times New Roman} \it y/\rm N')
% 去除上边框、右边框刻度线
box off % 取消边框
ax1 = axes('Position',get(ax,'Position'),'XAxisLocation','top',...
'YAxisLocation','right','Color','none','XColor','k','YColor','k'); % 设置坐标区
set(ax1,'XTick', [],'YTick', []); % 去掉xy轴刻度
hold off
% 保存图片
print(f,'-dpng','FigName','-r600')%保存图像
运行代码后得到下面的结果图。
自定义标记函数的使用
自定义标记函数的使用非常简单,一共有两个步骤。
- 定义点和连接顺序
首先,创建自定义标记的点坐标和连接顺序(不需要首尾相连),下面提供了上文中自定义多边形标记的数据。
GPoints = [-1,-1;
0, 1;
1,-1;
0,-0.5];
GSeq = 1:4;
2. 定义标记属性
接着定义该形状的一些属性,不需要定义的属性则以[ ]代替。
faceColor = 'r';
edgeColor = 'r';
pounds = 6;
lineWidth = [];
NMarker = [];
setMarker(ax, x, y, GPoints, GSeq, faceColor, edgeColor, pounds, lineWidth, NMarker)
效果图如下,最右侧的标记被右框线覆盖了一部分,解决办法可以查看下面这篇文章。
【MATLAB学习笔记】绘图——去除上、右边框刻度后图被框线覆盖解决方案
- 更换形状
形状可以自己定义,下面是一个不同于前面的形状,同样也可以绘制成功。
GPoints = [-1,-1;
1, -0.3;
0, 0;
1,0.7;
-1,1];
GSeq = 1:5;
总代码
总代码如下,后续还会继续更新一些MATLAB绘图的技巧和细节,制作不易,别忘了关注和点赞喔。
主函数
clc;clear;close all
set(0,'defaultfigurecolor','w');
%% 数据
x = -1:1:10; % 产生0到1, 步长为0.01的序列
y = 2*x.^3 + 1; % y为x一次函数
%%% 形状1
GPoints = [-1,-1;
0, 1;
1,-1;
0,-0.5];
GSeq = 1:4;
% %%% 形状2
% GPoints = [-1,-1;
% 1, -0.3;
% 0, 0;
% 1,0.7;
% -1,1];
% GSeq = 1:5;
%% 绘图
f = figure(1);
ax = gca; % 将当前坐标区实例化
plot(x,y,'-k','LineWidth',1.3)
xlim([-1,10])
faceColor = 'r';
edgeColor = 'r';
pounds = 6;
lineWidth = [];
NMarker = [];
setMarker(ax, x, y, GPoints, GSeq, faceColor, edgeColor, pounds, lineWidth, NMarker)
set(gca,'FontName','Times New Roman','FontSize',13)
xlabel('\fontname{宋体} 位移\fontname{Times New Roman} \it x/\rm mm')
ylabel('\fontname{宋体} 力\fontname{Times New Roman} \it y/\rm N')
% 去除上边框、右边框刻度线
box off % 取消边框
ax1 = axes('Position',get(ax,'Position'),'XAxisLocation','top',...
'YAxisLocation','right','Color','none','XColor','k','YColor','k'); % 设置坐标区
set(ax1,'XTick', [],'YTick', []); % 去掉xy轴刻度
hold off
% 保存图片
print(f,'-dpng','图14','-r600')%保存图像
自定义标记函数
function setMarker(ax, x, y, GPoints, GSeq, faceColor, edgeColor, pounds, lineWidth, NMarker)
% 自定义设置标记形状(目前只能绘制没有交叉线的图形), 前5个参数为必须输入, 其他参数为可选参数
% 若不需要则以空数组[]代替, 如不需要形状磅数pounds, 则pounds = []
%
% ax 绘图区
% x,y 绘图的数据点
% GPoints 形状的点坐标
% GSeq 形状点的连接顺序(不需要首尾相连)
% faceColor 填充的面颜色 默认:none
% edgeColor 填充的线颜色 默认:k
% pounds 形状的大小(磅数) 默认:6
% lineWidth 形状的线宽度 % 默认:0.5
% NMarker 形状的数量(近似数量, 采用等间距, 如不能整除则四舍五入)
% 默认:0, 0则代表全部点都绘制
if nargin < 6, faceColor = 'none'; end
if nargin < 7, edgeColor = 'k'; end
if nargin < 8, pounds = 6; end
if nargin < 9, lineWidth = 0.5; end
if nargin < 10, NMarker = 0; end
if isempty(faceColor), faceColor = 'none'; end
if isempty(edgeColor), edgeColor = 'k'; end
if isempty(pounds), pounds = 6; end
if isempty(lineWidth), lineWidth = 0.5; end
if isempty(NMarker), NMarker = 0; end
if NMarker == 0, NMarker = length(x); end
hold(ax,"on")
NG = length(GSeq); % 点数量
% 将图形的比例拉正
xl = xlim;
yl = ylim;
lenx = xl(2)-xl(1); % x轴的长度
leny = yl(2)-yl(1); % y轴的长度
yRatio = leny/lenx; % 纵横比比例
GPoints(:,2) = GPoints(:,2)*yRatio; % 修正坐标
% 获取绘图区的每单位在屏幕上的长度(磅)
unitLength = getUnitLength();
% 水平长度矩阵
L = max(GPoints(:,1)) - min(GPoints(:,1));
% 缩放比例
scale = 2*pounds/(L*unitLength);
% 缩放
GPoints = GPoints*scale;
% 计算绘制的点位置
PM = 1:round(length(x)/NMarker):length(x); % 需要绘制的点位置
NMarker = length(PM); % 绘制标记的真实数量
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标和连接顺序
GPointsAll = zeros(NMarker*NG,2);
GSeqAll = zeros(NMarker,NG);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标和连接顺序
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
GSeqAll(k,:) = GSeq + NG*(k-1);
%%% 判断当前标记点是否处于边界 %%%
% 标记形状坐标判断
BLeft = GPointsNew(:, 1) - xl(1); % 左边界
BRight = GPointsNew(:, 1) - xl(2); % 右边界
BDown = GPointsNew(:, 2) - yl(1); % 下边界
BUp = GPointsNew(:, 2) - yl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - xl(1); % 左边界
BRight2 = xl(2)-xk; % 右边界
BDown2 = yk - yl(1); % 下边界
BUp2 = yl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>=0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
% 绘制标记点
pa = patch('Faces', GSeqAll, 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth);
% 设置绘图区边缘图形不裁剪
pa2 = patch('Faces', GSeqAll(BJudge,:), 'Vertices', GPointsAll, 'FaceColor', faceColor,...
'EdgeColor', edgeColor,'LineWidth',lineWidth, 'Clipping', 'off');
%%%% 控制图形放大缩小 %%%%
zm = zoom(ax);
zm.Enable = 'on'; % 启用缩放
% 初始化xy轴范围
xl = xlim;
yl = ylim;
setappdata(ax,'lxl',xl)
setappdata(ax,'lyl',yl)
% 绘图区缩放回调函数
set(zm, 'ActionPostCallback', @(src, event) updateZoomInfo(ax,x,y,PM,NG,NMarker,pa,pa2));
end
回调函数
function updateZoomInfo(ax,x,y,PM,NG,NMarker,pa,pa2)
% 监控绘图区缩放并自动更新标记图形的回调函数
%
% ax 绘图区
% x,y 绘图的数据点
% PM 需要绘制的点位置
% NG % 点数量
% NMarker 形状的数量
% pa patch对象
% pa2 边缘点的patch对象
% 当前xy轴范围
cxl = xlim(ax);
cyl = ylim(ax);
% 获取上一次缩放的xy轴范围
lxl = getappdata(ax,'lxl');
lyl = getappdata(ax,'lyl');
% 保存当前的xy轴范围
setappdata(ax,'lxl',cxl)
setappdata(ax,'lyl',cyl)
% 计算xy轴放大因子
xScaleFactor = (lxl(2) - lxl(1))/(cxl(2) - cxl(1));
yScaleFactor = (lyl(2) - lyl(1))/(cyl(2) - cyl(1));
% 获取当前的节点坐标和连接顺序
GPoints = pa.Vertices;
GSeq = pa.Faces;
GPoints = GPoints(1:NG,:);
% 缩放坐标
GPoints(:,1) = GPoints(:,1)/xScaleFactor;
GPoints(:,2) = GPoints(:,2)/yScaleFactor;
% 重心坐标
[Cx, Cy] = polygonCentroid(GPoints);
% 初始化所有标记的坐标
GPointsAll = zeros(NMarker*NG,2);
% 边界点判断初始化
BJudge = false(NMarker,1);
for k = 1:NMarker
% 取出当前点坐标
xk = x(PM(k));
yk = y(PM(k));
% 平移
displacement = [xk-Cx,yk-Cy];
% 更新坐标
GPointsNew = GPoints;
GPointsNew = GPointsNew + repmat(displacement,size(GPoints,1),1);
% 保存所有标记点的坐标
GPointsAll(NG*(k-1)+1:NG*k,:) = GPointsNew;
%%% 判断当前标记点是否处于边界 %%%
% 标记形状坐标判断
BLeft = GPointsNew(:, 1) - cxl(1); % 左边界
BRight = GPointsNew(:, 1) - cxl(2); % 右边界
BDown = GPointsNew(:, 2) - cyl(1); % 下边界
BUp = GPointsNew(:, 2) - cyl(2); % 上边界
% 标记中心坐标判断
BLeft2 = xk - cxl(1); % 左边界
BRight2 = cxl(2)-xk; % 右边界
BDown2 = yk - cyl(1); % 下边界
BUp2 = cyl(2) - yk; % 上边界
% 判断形状坐标是否同号以及中心点是否在绘图区
if (~judgeSign(BLeft) || ~judgeSign(BRight) || ~judgeSign(BDown) || ~judgeSign(BUp)) && ...
all([BLeft2,BRight2,BDown2,BUp2]>0)
BJudge(k) = true; % 记录处于边界的标记点
end
end
% 更新坐标
pa.Vertices = GPointsAll;
% 设置绘图区边缘图形不裁剪
pa2.Vertices = GPointsAll;
pa2.Faces = GSeq(BJudge,:);
end
其他函数
- 计算标记图形重心函数
function [Cx, Cy] = polygonCentroid(points)
% 计算形状的重心坐标
% points 一个n x 2 的矩阵, 表示n个点的坐标, 第一列为x值, 第二列为y值
% 点的数量
n = size(points, 1);
% 闭合多边形
points = [points; points(1, :)];
% 初始化面积和重心坐标
A = 0;
Cx = 0;
Cy = 0;
% 计算面积和重心坐标
for i = 1:n
xi = points(i, 1);
yi = points(i, 2);
xi1 = points(i+1, 1);
yi1 = points(i+1, 2);
% 计算有向面积
A_temp = xi * yi1 - xi1 * yi;
A = A + A_temp;
% 计算重心坐标
Cx = Cx + (xi + xi1) * A_temp;
Cy = Cy + (yi + yi1) * A_temp;
end
A = A / 2; % 最终面积取绝对值
Cx = Cx / (6 * A);
Cy = Cy / (6 * A);
end
- 获取绘图区单位长度磅数函数
function unitLength = getUnitLength()
% 获取绘图区的每单位在屏幕上的长度(磅)
% 获取图形窗口的大小和位置
Po = get(gcf, 'Position'); % 返回的单位是点 (points),每个点约为 1/72 英寸
figWidth = Po(3); % 图形的宽度
% X轴范围
xl = xlim;
rangeX = xl(2) - xl(1);
% 绘图区的每单位在屏幕上的长度(磅)
unitLength = figWidth/rangeX;
end
- 判断数组中所有元素是否同号函数
function flag = judgeSign(arr)
% 判断数组中所有元素是否同号(包含0也算同号)
% 计算数组每个元素的符号
signs = sign(arr(arr~=0));
% 获取唯一符号
uniqueSigns = unique(signs);
% 判断是否只有一个唯一符号,并且该符号不为零
if length(uniqueSigns) == 1 && uniqueSigns ~= 0
flag = true; % 同号
else
flag = false; % 不同号或包含零
end
end
总结
这只是一个基础的示例,实际中还会有更具体的、更细致的要求,这就需要再做额外调整;另外本人也仍在学习中,这只是个人的学习笔记,可能还有一些不足之处,欢迎指正。
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 【MATLAB学习笔记】绘图——自定义标记(Marker)形状,实现与MATLAB自带标记基本一致的功能(自适应缩放、自适应裁剪)
发表评论 取消回复