前言

  在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);                        % 绘制标记的真实数量

  得到了绘制标记点的位置后,需要将标记平移到该位置。本文以标记图形的重心作为参考,计算重心到该位置的位移,再到坐标进行更新。
  计算多边形的重心方法如下:

  1. 计算面积 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 1n1,则该多边形的面积 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=1n(xiyi+1xi+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)
  2. 计算重心的坐标 ( 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=1n(xi+xi+1)(xiyi+1xi+1yi)Cy=6A1i=1n(yi+yi+1)(xiyi+1xi+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函数,只针对边界处的标记点设置成不裁剪模式。首先,需要判断标记点是否处于边界处,判断准则有两个:

  1. 重心点位于绘图区内;
  2. 标记图形的任意点超出绘图区。

  基于此准则,将平移标记点部分的最后一个代码块修改如下(回调函数部分的也是如此,只不过该部分在上一小节中已经提前改好了)。其中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')%保存图像

运行代码后得到下面的结果图。
在这里插入图片描述

自定义标记函数的使用

  自定义标记函数的使用非常简单,一共有两个步骤。

  1. 定义点和连接顺序
      首先,创建自定义标记的点坐标和连接顺序(不需要首尾相连),下面提供了上文中自定义多边形标记的数据。
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

总结

  这只是一个基础的示例,实际中还会有更具体的、更细致的要求,这就需要再做额外调整;另外本人也仍在学习中,这只是个人的学习笔记,可能还有一些不足之处,欢迎指正。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部