MATLAB | 超多样式聚类分析树状图任你选择~~

文摘   其他   2024-02-24 09:01   山东  
请尊重原创劳动成果
转载请注明本文链接
及文章作者:slandarer

这几天写了一个代码很长的聚类分析树状图绘图工具函数(上一期文章立的flag总算实现了),能够比较轻松的绘制以下图形:

工具基本已经成型了,未来有需求未来有空再加哈哈哈,要求MATLAB至少需要17b版本(理论上是17b,实际上可能需要更新的版本),先讲解基本使用方法,工具函数代码放在最后,建议去gitee仓库或者fileexchange下载,因为未来有更新的话比较方便修改。先讲讲基本用法:


STree 使用教程

工具函数名为 STree ,是在 dendrogram 函数的基础上写出来的,因此必须要先下载 Statistics and Machine Learning Toolbox 工具箱后才可以使用:

1-1 基本使用

% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

fig1 = figure('Units''normalized''Position', [.05,.3,.7,.4], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

ST.draw()
set(gca,'XColor','none','YColor','none')

和 dendrogram 函数绘制的树状图长相差不多的树状图就绘制出来啦!当然这样看起来有些单调,大家可以设置以下属性来修饰:

  • ClustGap -- 分类间隙
  • BranchColor -- 不同类树枝添加颜色
  • BranchHighlight -- 树枝高亮
  • ClassHighlight -- 分类高亮
  • ClassLabel -- 分类文本信息

就简单的在ST.draw()之前设置就好,比如大概这样:

% ... ... 一堆其他代码
% ... ... 一堆其他代码
% ... ... 一堆其他代码

ST.ClustGap = 'on';
ST.draw()

为了更直观,我们续写之前的代码新开一个窗口来绘图:

fig2 = figure('Units''normalized''Position', [.05,.3,.7,.4], 'Color''w');
ST.ax = gca;               % 更换坐标区域        -- change the axes
ST.ClustGap = 'on';        % 每个类之间加入间隙   -- insert gap between each clust
ST.BranchColor = 'on';     % 为不同类树枝添加颜色 -- set the branch's color of each clust
ST.BranchHighlight = 'on'% 增添树枝高亮         -- add highlight for each clust's branches
ST.ClassHighlight = 'on';  % 分类高亮            -- add class highlight
ST.ClassLabel = 'on';      % 分类文本信息         -- add class-label

ST.SampleName = compose('slan%d'1:size(Data,1));
ST.ClassName = compose('Class-%c'64 + (1:N));

% 调整字体 -- adjust font
ST.SampleFont = {'FontSize'10'FontName''Times New Roman'};
ST.ClassFont = {'FontSize'14'FontName''Times New Roman''FontWeight''bold'};
ST.draw()
set(gca,'XColor','none','YColor','none')

还是同一个m文件,再新开一个窗口展示一下如何换颜色:

% change color
fig3 = figure('Units''normalized''Position', [.05,.3,.7,.4], 'Color''w');
ST.ax = gca;% 更换坐标区域  -- change the axes
ST.CData = [0.3569    0.0784    0.0784
    0.6784    0.4471    0.1725
    0.1020    0.3882    0.5176
    0.1725    0.4196    0.4392
    0.2824    0.2275    0.2902];
ST.draw()
set(gca,'XColor','none','YColor','none')

1-2 树枝样式

我们可以通过设置Layout属性设置树枝样式,比如:

% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

fig1 = figure('Units''normalized''Position', [.05,.3,.7,.4], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

ST.Layout = 'bezier';

ST.draw()
set(gca,'XColor','none','YColor','none')

树枝就会变成用贝塞尔曲线插值的样式。写段代码展示一下全部可选择样式:

% STree_demo2 
% + layout : 'rectangular'(default) / 'rounded' / 'slanted' / 'ellipse' / 'bezier'

% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

fig = figure('Units''normalized''Position', [.05,.1,.7,.8], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

% 每个类之间加入间隙 -- insert gap between each clust
ST.ClustGap = 'on';
delete(gca);

% 所有类型树枝展示 -- all kinds of branch(layout)
layoutSet = {'rectangular''rounded''slanted''ellipse''bezier'};
for i = 1:length(layoutSet)
    % 创建坐标区域并修改绘图坐标区域 -- create subplots and change the parent of the tree object
    ax = axes('Parent', fig, 'Position', [1/40, (5-i)/5+1/201-1/201/5-1/15], 'XColor''none''YColor''none');
    ST.ax = ax; 

    % 改变树枝类型 -- change the layout
    ST.Layout = layoutSet{i};

    % 修改绘图范围以便方便添加text信息
    % -- change the X-limit and the Y-limit to add the following text
    ST.XLim = [0,1];
    ST.YLim = [0,1];
    ST.draw();

    text(01, layoutSet{i}, 'FontName''Times New Roman',...
        'FontSize'16'FontWeight''bold','Color', [1,1,1].*.2);
end

1-3 树的朝向

树有四种朝向,通过设置Orientation来完成: 再写段代码把四个朝向展示在一个图里:

% STree_demo3 
% + Orientation : 'left' / 'right' / 'top' / 'bottom'

% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

fig = figure('Units''normalized''Position', [.05,.1,.9,.8], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

% 每个类之间加入间隙 -- insert gap between each clust
ST.ClustGap = 'on';
delete(gca);

% 创建坐标区域并修改绘图坐标区域 -- create subplots and change the parent of the tree object
axR = axes('Parent', fig, 'Position', [1/201/301/4-1/201-1/15], 'XColor''none''YColor''none');
ST.ax = axR;

% 改变树枝类型 -- change the layout
ST.Layout = 'bezier';
% 改变方向 -- change the orientation
ST.Orientation = 'right';
ST.draw()




%% =========================================================================
axL = axes('Parent', fig, 'Position', [3/41/301/4-1/201-1/15], 'XColor''none''YColor''none');
ST.ax = axL;
ST.Orientation = 'left';
ST.draw()

axT = axes('Parent', fig, 'Position', [1/41/102/41/2-1/10-1/40], 'XColor''none''YColor''none');
ST.ax = axT;
ST.Orientation = 'top';
ST.draw()

axB = axes('Parent', fig, 'Position', [1/41/2+1/402/41/2-1/10-1/40], 'XColor''none''YColor''none');
ST.ax = axB;
ST.Orientation = 'bottom';
ST.draw()

1-4 树的大小范围和旋转

通过设置XLim, YLim, TLim属性来完成,其中TLim是旋转范围,如果两个值相等就是不形变的旋转:

% STree_demo4
% + position
% + rotation

% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

ST.ClustGap = 'on';        % 每个类之间加入间隙   -- insert gap between each clust
ST.BranchColor = 'on';     % 为不同类树枝添加颜色 -- set the branch's color of each clust
ST.BranchHighlight = 'on'% 增添树枝高亮         -- add highlight for each clust's branches
ST.Label = 'off';          % 关闭样本标签         -- close sample label

ST.XLim = [1,3];           % 改变X坐标范围 -- change X-limit
ST.YLim = [1,2];           % 改变Y坐标范围 -- change Y-limit
ST.TLim = [pi/6,pi/6];     % 围绕0点旋转pi/6(当TLim 两个数值相等时,围绕(0,0)点做不形变的旋转)
                           % Rotate pi/6 around point 0 (when TLim has two equal values, rotate around point (0,0) without deformation)

close all

fig1 = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w');       
ST.ax = gca;
ST.Orientation = 'left';
ST.draw()
% exportgraphics(fig1, '.\gallery\demo4_position_rotation_left.png')

%% 
fig2 = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w');       
ST.ax = gca;
ST.Orientation = 'top';
ST.draw()
% exportgraphics(fig2, '.\gallery\demo4_position_rotation_top.png')

1-5 扇形的树

就是设置TLim属性两个值不相等:展示一下四种朝向的树生成的[pi/6,pi/2]范围的扇形树状图:

% STree_demo5
% + Fan-shaped rotation


% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

ST.ClustGap = 'on';        % 每个类之间加入间隙   -- insert gap between each clust
ST.BranchColor = 'on';     % 为不同类树枝添加颜色 -- set the branch's color of each clust
ST.BranchHighlight = 'on'% 增添树枝高亮         -- add highlight for each clust's branches
ST.Label = 'off';          % 关闭样本标签         -- close sample label
ST.Layout = 'bezier';      % 改变树枝类型         -- change the layout

ST.XLim = [1,3];           % 改变X坐标范围 -- change X-limit
ST.TLim = [pi/6,pi/2];     % 绘制pi/6到pi/2范围的扇形树状图 -- Draw a dendrogram of the range from pi/6 to pi/2
close all

OrientationSet = {'left''right''top''bottom'};
for i = 1:4
    tempFig = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w'); 
    ST.ax = gca;
    ST.Orientation = OrientationSet{i};
    ST.draw()

    set(gca,'XColor','none','YColor','none')
    % exportgraphics(tempFig, ['.\gallery\demo5_Fan_shaped_rotation_',OrientationSet{i},'.png'])
end

1-6 圆面(left朝向)

展示一下X坐标是否从0开始,以及树枝类型不同导致的差别:

% STree_demo6
% + Larger angle range [1]
% + Different x coordinate ranges


% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

% 改变方向 -- change the orientation
ST.Orientation = 'left';

ST.ClustGap = 'on';        % 每个类之间加入间隙   -- insert gap between each clust
ST.BranchColor = 'on';     % 为不同类树枝添加颜色 -- set the branch's color of each clust
ST.BranchHighlight = 'on'% 增添树枝高亮         -- add highlight for each clust's branches
ST.Label = 'on';           % 关闭样本标签         -- close sample label
ST.Layout = 'bezier';      % 改变树枝类型         -- change the layout
ST.ClassHighlight = 'on';  % 分类高亮            -- add class highlight
ST.ClassLabel = 'on';      % 分类文本信息         -- add class-label

ST.TLim = [0,2*pi];
close all
layoutSet = {'rectangular''rounded''slanted''ellipse''bezier'};


% 调整各个元素半径 -- adjust the radius of each elementa
% 样本文本 类弧形内侧 类弧形外侧 类文本
% Sample text, inner side of class arc, outer side of class arc, class text
ST.RTick = [1+1/401.221.271.35];
ST.XLim = [0,3];
for i = 1:length(layoutSet)
    tempFig = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w'); 
    ST.ax = gca;
    ST.Layout = layoutSet{i};
    ST.draw()

    set(gca,'XColor','none','YColor','none')
    % exportgraphics(tempFig, ['.\gallery\demo6_left_full_fan_XLim_0_3_',layoutSet{i},'.png'])
end

%% ========================================================================
% 调整各个元素半径 -- adjust the radius of each elementa
% 样本文本 类弧形内侧 类弧形外侧 类文本
% Sample text, inner side of class arc, outer side of class arc, class text
ST.RTick = [1+1/401.261.311.37];
ST.XLim = [1,3];
for i = 1:length(layoutSet)
    tempFig = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w'); 
    ST.ax = gca;
    ST.Layout = layoutSet{i};
    ST.draw()

    set(gca,'XColor','none','YColor','none')
    % exportgraphics(tempFig, ['.\gallery\demo6_left_full_fan_XLim_1_3_',layoutSet{i},'.png'])
end

X 0-3范围

X 1-3范围


1-7 圆面(right朝向)

树枝类型不同导致的差别:

% STree_demo7
% + Larger angle range [2]


% 随机生成数据 -- random data
% rng(10)
Data = rand(75,3);

% 分类数 -- clust number
N = 5;

% 创建聚类树状图对象 -- create tree(dendrogram) object
Z = linkage(Data, 'average');
ST = STree(Z, 'MaxClust', N);

% 改变方向 -- change the orientation
ST.Orientation = 'right';

ST.ClustGap = 'on';        % 每个类之间加入间隙   -- insert gap between each clust
ST.BranchColor = 'on';     % 为不同类树枝添加颜色 -- set the branch's color of each clust
ST.BranchHighlight = 'on'% 增添树枝高亮         -- add highlight for each clust's branches
ST.Label = 'on';           % 关闭样本标签         -- close sample label
ST.Layout = 'bezier';      % 改变树枝类型         -- change the layout
ST.ClassHighlight = 'on';  % 分类高亮            -- add class highlight
ST.ClassLabel = 'on';      % 分类文本信息         -- add class-label

ST.TLim = [0,2*pi];
close all
layoutSet = {'rectangular''rounded''slanted''ellipse''bezier'};


% 调整各个元素半径 -- adjust the radius of each elementa
% 样本文本 类弧形内侧 类弧形外侧 类文本
% Sample text, inner side of class arc, outer side of class arc, class text
ST.RTick = [1+1/401.41.51.6];
ST.XLim = [2.5,4];
for i = 1:length(layoutSet)
    tempFig = figure('Units''normalized''Position', [.05,.1,.5,.7], 'Color''w'); 
    ST.ax = gca;
    ST.Layout = layoutSet{i};
    ST.draw()

    set(gca,'XColor','none','YColor','none')
    % exportgraphics(tempFig, ['.\gallery\demo7_right_full_fan_XLim_0_3_',layoutSet{i},'.png'])
end

SMatrix 使用教程

这个函数是我专门写出来用来配合STree来添加热图用的,给了三个使用实例,基本上能用到的函数都提到了:

2-1 带聚类树状图的热图

% STree_SMatrix_demo1

% 随便捏造了点数据 -- made up some data casually
X1 = randn(20,20) + [(linspace(-1,2.5,20)').*ones(1,8),(linspace(.5,-.7,20)').*ones(1,5),(linspace(.9,-.2,20)').*ones(1,7)];
X2 = randn(20,25) + [(linspace(-1,2.5,20)').*ones(1,10),(linspace(.5,-.7,20)').*ones(1,8),(linspace(.9,-.2,20)').*ones(1,7)];
% 求相关系数矩阵 -- get the correlation matrix
Data = corr(X1,X2);
% rowName and colName
rowName = {'FREM2','ALDH9A1','RBL1','AP2A2','HNRNPK','ATP1A1','ARPC3','SMG5','RPS27A',...
          'RAB8A','SPARC','DDX3X','EEF1D','EEF1B2','RPS11','RPL13','RPL34','GCN1','FGG','CCT3'};
colName = compose('slan%d'1:25);

% 分类配色 -- Color schemes for each clust
CList = [0.1490    0.4039    0.4980
    0.3882    0.3608    0.4471
    0.5373    0.2157    0.3098
    0.7686    0.4353    0.2431];


fig1 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
% 左侧聚类树状图 -- left Cluster Tree
Z1 = linkage(Data, 'average');
ST1 = STree(Z1, 'MaxClust'3);
ST1.Orientation = 'left';
ST1.XLim = [-.25,-.05];
ST1.YLim = [0,1.2];
ST1.Label = 'off';
ST1.BranchColor = 'on';
ST1.BranchHighlight = 'on';
ST1.ClassHighlight = 'on';
ST1.RTick = [0,1,1.2,0];
ST1.CData = CList;
ST1.draw()
% 右侧聚类树状图 -- right Cluster Tree
Z2 = linkage(Data.', 'average');
ST2 = STree(Z2, 'MaxClust'3);
ST2.Orientation = 'top';
ST2.XLim = [0,1];
ST2.YLim = [1.25,1.45];
ST2.Label = 'off';
ST2.BranchColor = 'on';
ST2.BranchHighlight = 'on';
ST2.ClassHighlight = 'on';
ST2.RTick = [0,1,1.2,0];
ST2.CData = CList;
ST2.draw() 

% -------------------------------------------------------------------------
% 创建热图对象 -- create heatmap object
SM = SMatrix(Data);

% 添加分组信息 -- Add grouping information
SM.RowName = rowName;
SM.ColName = colName;
SM.RowOrder = ST1.order;
SM.RowClass = ST1.class;
SM.ColOrder = ST2.order;
SM.ColClass = ST2.class;

% 设置文本和字体 -- Set Text and Font
SM.LeftLabel = 'off';
SM.RightLabel = 'on';
SM.BottomLabelFont = {'FontSize'12'FontName''Times New Roman'};
SM.RightLabelFont = {'FontSize'12'FontName''Times New Roman'};

% 设置位置 -- set position
SM.XLim = [0,1];
SM.YLim = [0,1.2];
SM.draw()

% 修饰坐标区域 -- Decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1], 'XLim', [-.5,1.3]);
CB = colorbar;
CB.Position(4) = CB.Position(4).*0.75;
CB.Position(4) = CB.Position(4).*0.75;
% exportgraphics(fig1, '.\gallery\STree_SMatrix_demo1_1.png')



%% ========================================================================
fig2 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');
ST1.ax = gca;
ST2.ax = gca;
SM.ax = gca;

% SM.Colormap = slanCM(141, 64);

% 每个类之间加入间隙 -- insert gap between each clust
ST1.ClustGap = 'on';
ST2.ClustGap = 'on';
SM.ClustGap = 'on';

ST1.draw() 
ST2.draw() 
SM.draw()


% 修饰坐标区域 -- Decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1], 'XLim', [-.5,1.3]);
CB = colorbar;
CB.Position(4) = CB.Position(4).*0.75;
CB.Position(4) = CB.Position(4).*0.75;
% exportgraphics(fig2, '.\gallery\STree_SMatrix_demo1_2.png')

2-2 环形热图

其中提到的slanCM可以去fileexchange官网下载,也可以去文末gitee仓库获取全部文件,该函数的介绍在这:MATLAB | MATLAB配色不够用?全网最全的colormap补充包来啦!

% STree_SMatrix_demo2

% 随便捏造了点数据 -- made up some data casually
rng(5)
X = randn(100,80) + [(linspace(-1,2.5,100)').*ones(1,15),(linspace(.5,-.7,100)').*ones(1,15),...
                  (linspace(.1,-.7,100)').*ones(1,15),(linspace(.9,-.2,100)').*ones(1,15),...
                  (linspace(-.1,.7,100)').*ones(1,10),(linspace(-.9,-.2,100)').*ones(1,10)];
Y = randn(100,8) + [(linspace(-1,2.5,100)').*ones(1,2),(linspace(.5,-.7,100)').*ones(1,3),(linspace(-1,-2.5,100)').*ones(1,3)];
% 求相关系数矩阵 -- get the correlation matrix
Data = corr(X,Y);
% rowName and colName
rowName = compose('slan%d'1:80);
colName = compose('var%d'1:8);
% 分类配色 -- Color schemes for each clust
CList = [0.1490    0.4039    0.4980
    0.3882    0.3608    0.4471
    0.5373    0.2157    0.3098
    0.7686    0.4353    0.2431];

fig1 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
% 内侧聚类树状图 -- inner Cluster Tree
Z1 = linkage(Data, 'average');
ST1 = STree(Z1, 'MaxClust'4);
ST1.Orientation = 'left';
ST1.XLim = [0,1];
ST1.TLim = [pi/2,2*pi+pi/4];
ST1.Label = 'off';
ST1.BranchColor = 'on';
ST1.CData = CList;
ST1.draw()
% 径向聚类树状图 -- radial Cluster Tree
Z2 = linkage(Data.', 'average');
ST2 = STree(Z2, 'MaxClust'2);
ST2.Orientation = 'top';
ST2.XLim = [1,2];
ST2.TLim = [pi/4,pi/4];
ST2.YLim = [0,0.3];
ST2.Label = 'off';
ST2.BranchColor = 'on';
ST2.RTick = [0,1,1.2,0];
ST2.CData = CList;
ST2.draw() 

% -------------------------------------------------------------------------
% 创建热图对象 -- create heatmap object
SM = SMatrix(Data);

% 添加分组信息 -- Add grouping information
SM.RowName = rowName;
SM.ColName = colName;
SM.RowOrder = ST1.order;
SM.RowClass = ST1.class;
SM.ColOrder = ST2.order;
SM.ColClass = ST2.class;

% 设置文本和字体 -- Set Text and Font
SM.LeftLabel = 'off';
SM.RightLabel = 'on';
SM.BottomLabelFont = {'FontSize'12'FontName''Times New Roman'};
SM.RightLabelFont = {'FontSize'12'FontName''Times New Roman'};

% 设置位置 -- set position
SM.XLim = [1,2];
SM.TLim = [pi/2,2*pi+pi/4];
SM.draw()

% 修饰坐标区域 -- Decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1], 'XLim', [-2.2,2.3]);

exportgraphics(fig1, '.\gallery\STree_SMatrix_demo2_1.png')





%% ========================================================================
fig2 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');
ST1.ax = gca;
ST2.ax = gca;
SM.ax = gca;

% SM.Colormap = slanCM(141, 64);

% 每个类之间加入间隙 -- insert gap between each clust
ST1.ClustGap = 'on';
ST2.ClustGap = 'on';
SM.ClustGap = 'on';

% see slanCMdisplay
% and Zhaoxu Liu / slandarer (2024). 200 colormap, MATLAB Central File Exchange.
% https://www.mathworks.com/matlabcentral/fileexchange/120088-200-colormap
SM.Colormap = slanCM(13664);

ST1.draw() 
ST2.draw() 
SM.draw()


% 修饰坐标区域 -- Decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1], 'XLim', [-2.2,2.3]);

exportgraphics(fig2, '.\gallery\STree_SMatrix_demo2_2.png')

2-3 倾斜45°角热图

STreeSMatrix构造的对象其中会有一些名字以Hdl结尾的属性,实际上都是图形对象,可以直接对线条粗细呀,文字颜色之类的一系列信息进行修改,可自行研究,未来有机会可能会集成进一个set方法中:

% STree_SMatrix_demo3

% 随机生成数据
X=randn(20,20)+[(linspace(-1,2.5,20)').*ones(1,8),(linspace(.5,-.7,20)').*ones(1,5),(linspace(.9,-.2,20)').*ones(1,7)];
Data=corr(X);
% 变量名列表
NameList=compose('Sl-%d',1:20);
% 分类配色 -- Color schemes for each clust
CList = [0.1490    0.4039    0.4980
    0.3882    0.3608    0.4471
    0.5373    0.2157    0.3098
    0.7686    0.4353    0.2431];


fig1 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');

% 创建聚类树状图对象 -- create tree(dendrogram) object
% 左侧聚类树状图 -- left Cluster Tree
Z1 = linkage(Data, 'average');
ST1 = STree(Z1, 'MaxClust'3);
ST1.Orientation = 'left';
ST1.XLim = [-.25,-.05];
ST1.YLim = [0,1];
ST1.TLim = [-pi/4,-pi/4];
ST1.Label = 'off';
ST1.BranchColor = 'on';
ST1.BranchHighlight = 'on';
ST1.ClassHighlight = 'on';
ST1.RTick = [0,1,1.2,0];
ST1.CData = CList;
ST1.draw()

% 创建热图对象 -- create heatmap object
SM = SMatrix(Data);

% 添加分组信息 -- Add grouping information
SM.ColName = NameList;
SM.RowOrder = ST1.order;
SM.RowClass = ST1.class;
SM.ColOrder = ST1.order;
SM.ColClass = ST1.class;

% 设置文本和字体 -- Set Text and Font
SM.LeftLabel = 'off';
SM.BottomLabel = 'off';
SM.TopLabel = 'on';
SM.TopLabelFont = {'FontSize'12'FontName''Times New Roman''Rotation'45};

% 设置位置 -- set position
SM.XLim = [0,1];
SM.YLim = [0,1];
SM.TLim = [-pi/4,-pi/4];
SM.draw()

% 修饰句柄 -- decorate handles
% 清除热图下半部分 -- Clear the bottom half of the heatmap
tData = triu(ones(size(Data)),1);
tInd = find(tData(:) == 1);
for i = 1:length(tInd)
    set(SM.heatmapHdl{tInd(i)}, 'Visible''off')
end

% 修饰坐标区域 -- decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1],'XLim', [-.15,1.45]);
CB = colorbar();
CB.Location = 'southoutside';

exportgraphics(fig1, '.\gallery\STree_SMatrix_demo3_1.png')





%% ========================================================================
fig2 = figure('Units''normalized''Position', [.05,.1,.6,.8], 'Color''w');
ST1.ax = gca;
SM.ax = gca;

% SM.Colormap = slanCM(141, 64);

% 每个类之间加入间隙 -- insert gap between each clust
ST1.ClustGap = 'on';
SM.ClustGap = 'on';

% see slanCMdisplay
% and Zhaoxu Liu / slandarer (2024). 200 colormap, MATLAB Central File Exchange.
% https://www.mathworks.com/matlabcentral/fileexchange/120088-200-colormap
SM.Colormap = slanCM(14164);

ST1.draw() 
SM.draw()

% 修饰句柄 -- decorate handles
% 清除热图下半部分 -- Clear the bottom half of the heatmap
tData = triu(ones(size(Data)),1);
tInd = find(tData(:) == 1);
for i = 1:length(tInd)
    set(SM.heatmapHdl{tInd(i)}, 'Visible''off')
end


% 修饰坐标区域 -- decorate axes
set(gca, 'XColor''none''YColor''none',...
    'DataAspectRatio', [1,1,1],'XLim', [-.15,1.45]);
CB = colorbar();
CB.Location = 'southoutside';

exportgraphics(fig2, '.\gallery\STree_SMatrix_demo3_2.png')

工具函数完整代码

代码都是几百行,编写不易,点个赞叭!!!

3-1 STree 完整代码

classdef STree < handle
% Copyright (c) 2024, Zhaoxu Liu / slandarer
% =========================================================================
% @author : slandarer
% 公众号  : slandarer随笔
% 知乎    : slandarer
% -------------------------------------------------------------------------
% Zhaoxu Liu / slandarer (2024). STree dendrogram 
% (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), 
% MATLAB Central File Exchange.
    properties
        ax, Z, T, cutoff, CData, Parent, class, lineClass, hClass, H
        MaxClust    = 3
        Layout      = 'rectangular'% 'rectangular'(default) / 'rounded' / 'slanted'
                                     % 'ellipse' / 'bezier' 
        Orientation = 'top';         % 'top'    -- Top to bottom
                                     % 'bottom' -- Bottom to top
                                     % 'left' -- Left to right
                                     % 'right' -- Right to left
        oriXLim, oriYLim
        XLim, YLim, TLim = [0,0];
        ClustGap        = 'off';
        BranchColor     = 'off';
        BranchHighlight = 'off';
        Label           = 'on' ;
        LabelColor      = 'off'% uncompleted
        ClassHighlight  = 'off';
        ClassLabel      = 'off';

        branchHdl   , sampleLabelHdl, classLabelHdl
        branchHLTHdl, sampleHLTHdl  , classHLTHdl
        SampleName  , ClassName     , WTick
        % 样本文本 类弧形内侧 类弧形外侧 类文本
        RTick = [1+1/401.221.271.35];
        SampleFont = {'FontSize'10'FontName''Times New Roman'};
        ClassFont = {'FontSize'14'FontName''Times New Roman''FontWeight''bold'};

        lineSet, order, oriXSet, oriYSet, oriWSet, oriHSet, newXSet, newYSet, newWSet, newHSet
        branchHLTXSet, sampleHLTXSet, classHLTXSet
        branchHLTYSet, sampleHLTYSet, classHLTYSet
        branchHLTWSet, sampleHLTWSet, classHLTWSet
        branchHLTHSet, sampleHLTHSet, classHLTHSet
        arginList  = {'Parent''Layout''CData''XLim''YLim''TLim',...
            'SampleName''ClassName''Orientation''MaxClust''RTick',...
            'SampleFont''ClassFont''ClustGap''BranchColor''BranchHighlight',...
            'Label''LabelColor''ClassHighlight''ClassLabel'};
    end
% 构造函数 =================================================================
    methods
        function obj = STree(varargin)
            % 获取基本数据 -------------------------------------------------
            if isa(varargin{1}, 'matlab.graphics.axis.Axes')
                obj.ax = varargin{1}; varargin(1) = [];
            else  
            end
            obj.Z = varargin{1}; varargin(1) = [];
            % 获取其他信息 -------------------------------------------------
            for i = 1:2:(length(varargin)-1)
                tid = ismember(obj.arginList, varargin{i});
                if any(tid)
                obj.(obj.arginList{tid}) = varargin{i+1};
                end
            end
            if isempty(obj.ax) && (~isempty(obj.Parent)), obj.ax=obj.Parent; end
            if isempty(obj.ax), obj.ax=gca; end
            % 基础配色 -----------------------------------------------------
            if isempty(obj.CData)
                colorList = [204    61    36
                             243   197    88
                             109   174   144
                              48   180   204
                               0    79   122]./255;
                N = size(colorList, 1);
                colorList = colorList(mod((1:obj.MaxClust)-1, N)+1,:);
                colorList = colorList.*(.9.^(floor(((1:obj.MaxClust)-1)./N).'));
                obj.CData = colorList;
            end
            % 基础命名 -----------------------------------------------------
            if isempty(obj.SampleName)
                obj.SampleName = compose('slan%d'1:(size(obj.Z,1)+10));
                obj.ClassName = compose('Class-%c'64 + (1:obj.MaxClust));
            end
        end
% Copyright (c) 2024, Zhaoxu Liu / slandarer
% =========================================================================
% @author : slandarer
% 公众号  : slandarer随笔
% 知乎    : slandarer
% -------------------------------------------------------------------------
% Zhaoxu Liu / slandarer (2024). STree dendrogram 
% (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), 
% MATLAB Central File Exchange.
        function draw(obj)
            obj.ax.NextPlot = 'add';
            % 各类数据准备 =================================================
            % 数据处理、绘制树状图、提取图形、关闭图窗 ------------------------
            tempFigure = figure();
            N = obj.MaxClust;
            obj.T = cluster(obj.Z, 'maxclust', N);
            obj.cutoff = median([obj.Z(end-(N-1),3), obj.Z(end-(N-2),3)]);
            
            [obj.lineSet, ~, obj.order] = dendrogram(obj.Z, 0'Orientation', obj.Orientation);
            obj.oriXSet = reshape([obj.lineSet(:).XData], 4, []).';
            obj.oriYSet = reshape([obj.lineSet(:).YData], 4, []).';
            if strcmpi(obj.Orientation, 'top') || strcmpi(obj.Orientation, 'bottom'
                obj.oriWSet = obj.oriXSet; obj.oriHSet = obj.oriYSet;
            else
                obj.oriWSet = obj.oriYSet; obj.oriHSet = obj.oriXSet;
            end
            obj.class = obj.T(obj.order);
            close(tempFigure)
            % 计算高亮高度 -------------------------------------------------
            WSet = [obj.oriWSet(:,1:2); obj.oriWSet(:,3:4)];
            HSet = [obj.oriHSet(:,1:2); obj.oriHSet(:,3:4)];
            BSet = (HSet(:,1)-obj.cutoff).*(HSet(:,2)-obj.cutoff)<0;
            obj.H = (HSet(BSet,1)+HSet(BSet,2))./2;
            obj.hClass = obj.class(round(WSet(BSet,1)));
            
            % 预生成树枝配色 -----------------------------------------------
            obj.lineClass = all(obj.oriHSet < obj.cutoff,2).*obj.class(round((obj.oriWSet(:,2)+obj.oriWSet(:,3))./2));
            % 生成间隙 -----------------------------------------------------
            gap = find(diff(obj.class)~=0)+.5;
            obj.WTick = 1:length(obj.class);
            if strcmpi(obj.ClustGap, 'on')
                for i = length(gap):-1:1
                    obj.oriWSet(obj.oriWSet>gap(i)) = obj.oriWSet(obj.oriWSet>gap(i))+1;
                    obj.WTick(obj.WTick>gap(i)) = obj.WTick(obj.WTick>gap(i))+1;
                end
            end

            obj.newWSet = [];
            obj.newHSet = [];
            % 修改树枝形状 =================================================
            switch obj.Layout
                case 'rectangular'
                    for i = 1:size(obj.oriWSet,1)
                        obj.newWSet(i,:) = [linspace(obj.oriWSet(i,1), obj.oriWSet(i,2), 30),...
                                            linspace(obj.oriWSet(i,2), obj.oriWSet(i,3), 30),...
                                            linspace(obj.oriWSet(i,3), obj.oriWSet(i,4), 30)];
                        obj.newHSet(i,:) = [linspace(obj.oriHSet(i,1), obj.oriHSet(i,2), 30),...
                                            linspace(obj.oriHSet(i,2), obj.oriHSet(i,3), 30),...
                                            linspace(obj.oriHSet(i,3), obj.oriHSet(i,4), 30)];
                    end
                case 'rounded'  
                    tX = [-1.*ones(1,15),...
                          cos(linspace(pi,pi/2,20)).*.3-.7,...
                          linspace(-.7,.7,15),...
                          cos(linspace(pi/2,0,20)).*.3+.7,...
                          1.*ones(1,15)];
                    tY = [linspace(0,.7,15),...
                          sin(linspace(pi,pi/2,20)).*.3+.7,...
                          1.*ones(1,15),...
                          sin(linspace(pi/2,0,20)).*.3+.7,...
                          linspace(.7,0,15)];
                    obj.newWSet = [obj.oriWSet(:,1),...
                        tX.*(obj.oriWSet(:,4)-obj.oriWSet(:,1))./2 + (obj.oriWSet(:,4)+obj.oriWSet(:,1))./2,...
                        obj.oriWSet(:,4)];
                    obj.newHSet = [obj.oriHSet(:,1),...
                        tY.*(obj.oriHSet(:,2)-max(obj.oriHSet(:,[1,4]), [], 2)) + max(obj.oriHSet(:,[1,4]), [], 2),...
                        obj.oriHSet(:,4)];
                case 'slanted'
                    for i = 1:size(obj.oriWSet,1)
                        tWId = obj.Z(:,1:2) == (i+length(obj.class)); tW1 = [];
                        if all(tWId(:,1) == 0) && all(tWId(:,2) == 0)
                            tW = mean(obj.oriWSet(i,2:3));
                        elseif all(tWId(:,1) == 0)
                            tW1 = obj.oriWSet(tWId(:,2),1);
                            tW2 = obj.oriWSet(tWId(:,2),4);
                        elseif all(tWId(:,2) == 0)
                            tW1 = obj.oriWSet(tWId(:,1),1);
                            tW2 = obj.oriWSet(tWId(:,1),4);
                        end
                        if ~isempty(tW1)
                        if abs(tW1 - mean(obj.oriWSet(i,2:3))) > abs(tW2 - mean(obj.oriWSet(i,2:3)))
                            tW = tW2;
                        else
                            tW = tW1;
                        end
                        end
                        obj.newWSet(i,:) = [linspace(obj.oriWSet(i,1), tW, 30),...
                                            linspace(tW, obj.oriWSet(i,4), 30)];
                        obj.newHSet(i,:) = [linspace(obj.oriHSet(i,1), mean(obj.oriHSet(i,2:3)), 30),...
                                            linspace(mean(obj.oriHSet(i,2:3)), obj.oriHSet(i,4), 30)];
                    end
                case 'ellipse'
                    tT = linspace(pi,0,30);
                    t01 = linspace(0,1,25);
                    obj.newWSet = [obj.oriWSet(:,1).*ones(1,25),...
                        cos(tT).*(obj.oriWSet(:,4)-obj.oriWSet(:,1))./2 + (obj.oriWSet(:,4)+obj.oriWSet(:,1))./2,...
                        obj.oriWSet(:,4).*ones(1,25)];
                    obj.newHSet = [obj.oriHSet(:,1) + t01.*(max(obj.oriHSet(:,[1,4]), [], 2) - obj.oriHSet(:,1)),...
                        sin(tT).*(obj.oriHSet(:,2)-max(obj.oriHSet(:,[1,4]), [], 2)) + max(obj.oriHSet(:,[1,4]), [], 2),...
                        max(obj.oriHSet(:,[1,4]), [], 2) + t01.*(obj.oriHSet(:,4) - max(obj.oriHSet(:,[1,4]), [], 2))];
                case 'bezier'
                    obj.newWSet = zeros(size(obj.oriWSet,1), 60);
                    obj.newHSet = zeros(size(obj.oriHSet,1), 60);
                    for i = 1:size(obj.oriHSet,1)
                        pntsL = [obj.oriWSet(i,[1,2]),...
                                 mean(obj.oriWSet(i,[2,3]));
                                 obj.oriHSet(i,[1,2]),...
                                 obj.oriHSet(i,2)].';
                        pntsR = [mean(obj.oriWSet(i,[2,3])),...
                                 obj.oriWSet(i,[3,4]); 
                                 obj.oriHSet(i,[3,3]),...
                                 obj.oriHSet(i,4)].';
                        pntsL = bezierCurve(pntsL, 30);
                        pntsR = bezierCurve(pntsR, 30);
                        obj.newWSet(i,:) = [pntsL(:,1).', pntsR(:,1).'];
                        obj.newHSet(i,:) = [pntsL(:,2).', pntsR(:,2).'];
                    end
            end
            % 高亮区域计算 -------------------------------------------------
            classNum = unique(obj.class, 'stable');
            for i = 1:obj.MaxClust
                tX = [obj.WTick(find(obj.class == classNum(i), 1'first')) - .5,...
                      obj.WTick(find(obj.class == classNum(i), 1'last')) + .5];
                obj.branchHLTWSet(i,:) = [linspace(tX(1), tX(2), 50), tX(2).*ones(1,50),...
                                          linspace(tX(2), tX(1), 50), tX(1).*ones(1,50)];
                obj.branchHLTHSet(i,:) = [obj.H(classNum(i) == obj.hClass).*ones(1,50),...
                                          linspace(obj.H(classNum(i) == obj.hClass), 050),...
                                          zeros(1,50),...
                                          linspace(0, obj.H(classNum(i) == obj.hClass), 50)];
                obj.classHLTWSet(i,:) = [linspace(tX(1), tX(2), 50), tX(2).*ones(1,50),...
                                         linspace(tX(2), tX(1), 50), tX(1).*ones(1,50)];
                maxH = max(max(obj.oriHSet));
                minH = min(min(obj.oriHSet));
                diffH = maxH - minH;
                obj.classHLTHSet(i,:) = [-diffH.*(obj.RTick(2)-1).*ones(1,50),...
                                          linspace(-diffH.*(obj.RTick(2)-1), -diffH.*(obj.RTick(3)-1), 50),...
                                         -diffH.*(obj.RTick(3)-1).*ones(1,50),...
                                          linspace(-diffH.*(obj.RTick(3)-1), -diffH.*(obj.RTick(2)-1), 50)];
            end


            % 数据转换 =====================================================
            if  strcmpi(obj.Orientation, 'left'
                maxH = max(max(obj.newHSet));
                obj.newHSet = maxH - obj.newHSet;
                obj.branchHLTHSet = maxH - obj.branchHLTHSet;
                obj.classHLTHSet = maxH - obj.classHLTHSet;
            elseif strcmpi(obj.Orientation, 'bottom')
                obj.newHSet =  - obj.newHSet;
                obj.branchHLTHSet =  - obj.branchHLTHSet;
                obj.classHLTHSet =  - obj.classHLTHSet;
            end
            if strcmpi(obj.Orientation, 'top') || strcmpi(obj.Orientation, 'bottom'
                obj.newXSet = obj.newWSet; obj.newYSet = obj.newHSet;
                obj.branchHLTXSet = obj.branchHLTWSet; obj.branchHLTYSet = obj.branchHLTHSet;
                obj.classHLTXSet = obj.classHLTWSet; obj.classHLTYSet = obj.classHLTHSet;
            else
                obj.newXSet = obj.newHSet; obj.newYSet = obj.newWSet;
                obj.branchHLTXSet = obj.branchHLTHSet; obj.branchHLTYSet = obj.branchHLTWSet;
                obj.classHLTXSet = obj.classHLTHSet; obj.classHLTYSet = obj.classHLTWSet;
            end
            % 原始X,Y范围获取 ----------------------------------------------
            if strcmp(obj.ClustGap,'on')
                gap = 1;
            else
                gap = .5;
            end
            if strcmpi(obj.Orientation, 'top') || strcmpi(obj.Orientation, 'bottom'
                obj.oriXLim = [min(min(obj.newXSet)) - gap, max(max(obj.newXSet)) + gap];
                obj.oriYLim = [min(min(obj.newYSet)), max(max(obj.newYSet))];
            else
                obj.oriXLim = [min(min(obj.newXSet)), max(max(obj.newXSet))];
                obj.oriYLim = [min(min(obj.newYSet)) - gap, max(max(obj.newYSet)) + gap];
            end
            % X,Y范围调整 --------------------------------------------------
            if ~isempty(obj.XLim)
                obj.newXSet = (obj.newXSet - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                obj.branchHLTXSet = (obj.branchHLTXSet - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                obj.classHLTXSet = (obj.classHLTXSet - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
            else
                obj.XLim = obj.oriXLim;
            end
            if ~isempty(obj.YLim)
                obj.newYSet = (obj.newYSet - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                obj.branchHLTYSet = (obj.branchHLTYSet - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                obj.classHLTYSet = (obj.classHLTYSet - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
            else
                obj.YLim = obj.oriYLim;
            end
            % 旋转 --------------------------------------------------------
            if abs(obj.TLim(1)-obj.TLim(2)) < eps
                rotateMat = [cos(obj.TLim(1)), -sin(obj.TLim(1));
                             sin(obj.TLim(1)),  cos(obj.TLim(1))];
                % 旋转树枝
                tNewXYSet = rotateMat*[obj.newXSet(:).'; obj.newYSet(:).'];
                obj.newXSet = reshape(tNewXYSet(1,:), size(obj.newXSet,1), []);
                obj.newYSet = reshape(tNewXYSet(2,:), size(obj.newYSet,1), []);
                % 旋转树枝高亮
                tBranchHLTXYSet = rotateMat*[obj.branchHLTXSet(:).'; obj.branchHLTYSet(:).'];
                obj.branchHLTXSet = reshape(tBranchHLTXYSet(1,:), size(obj.branchHLTXSet,1), []);
                obj.branchHLTYSet = reshape(tBranchHLTXYSet(2,:), size(obj.branchHLTYSet,1), []);
                % 旋转类高亮
                tClassHLTXYSet = rotateMat*[obj.classHLTXSet(:).'; obj.classHLTYSet(:).'];
                obj.classHLTXSet = reshape(tClassHLTXYSet(1,:), size(obj.classHLTXSet,1), []);
                obj.classHLTYSet = reshape(tClassHLTXYSet(2,:), size(obj.classHLTYSet,1), []);
            else
                % 旋转树枝
                tNewTSet = (obj.newYSet - obj.YLim(1))./diff(obj.YLim).*diff(obj.TLim) + obj.TLim(1);
                tNewRSet = obj.newXSet;
                obj.newXSet = cos(tNewTSet).*tNewRSet;
                obj.newYSet = sin(tNewTSet).*tNewRSet;
                % 旋转树枝高亮
                tBranchHLTTSet = (obj.branchHLTYSet - obj.YLim(1))./diff(obj.YLim).*diff(obj.TLim) + obj.TLim(1);
                tBranchHLTRSet = obj.branchHLTXSet;
                obj.branchHLTXSet = cos(tBranchHLTTSet).*tBranchHLTRSet;
                obj.branchHLTYSet = sin(tBranchHLTTSet).*tBranchHLTRSet;
                % 旋转类高亮
                tClassHLTTSet = (obj.classHLTYSet - obj.YLim(1))./diff(obj.YLim).*diff(obj.TLim) + obj.TLim(1);
                tClassHLTRSet = obj.classHLTXSet;
                obj.classHLTXSet = cos(tClassHLTTSet).*tClassHLTRSet;
                obj.classHLTYSet = sin(tClassHLTTSet).*tClassHLTRSet;
            end
            if obj.TLim(1) ~= 0 || obj.TLim(2) ~= 0
                obj.ax.DataAspectRatio = [1,1,1];
            end


            % 图形重绘 =====================================================
            % 绘制树枝 -----------------------------------------------------
            colorList = [obj.CData];
            if strcmpi(obj.BranchColor, 'off')
                colorList = colorList.*0;
            end
            for i = 1:obj.MaxClust
                obj.branchHdl{i} = plot(obj.ax, obj.newXSet(classNum(i) == obj.lineClass,:).',...
                    obj.newYSet(classNum(i) == obj.lineClass,:).', 'Color', colorList(i,:), 'LineWidth'.8);
            end
            obj.branchHdl{obj.MaxClust+1} = plot(obj.ax, obj.newXSet(0 == obj.lineClass,:).',...
                obj.newYSet(0 == obj.lineClass,:).', 'Color', [0,0,0], 'LineWidth'.7);
            % 绘制树枝高亮 -------------------------------------------------
            for i = 1:obj.MaxClust
                obj.branchHLTHdl{i}=fill(obj.ax, obj.branchHLTXSet(i,:), obj.branchHLTYSet(i,:), obj.CData(i,:), 'EdgeColor''none''FaceAlpha'.25);
            end
            if strcmpi(obj.BranchHighlight,'off')
                for i = 1:obj.MaxClust
                    set(obj.branchHLTHdl{i},'Visible','off');
                end
            end
            % 绘制类高亮 ---------------------------------------------------
            for i = 1:obj.MaxClust
                obj.classHLTHdl{i}=fill(obj.ax, obj.classHLTXSet(i,:), obj.classHLTYSet(i,:), obj.CData(i,:), 'EdgeColor''none''FaceAlpha'.9);
            end
            if strcmpi(obj.ClassHighlight,'off')
                for i = 1:obj.MaxClust
                    set(obj.classHLTHdl{i},'Visible','off');
                end
            end
            % 绘制样本标签 -------------------------------------------------
            if abs(obj.TLim(1)-obj.TLim(2)) < eps
                rotateMat = [cos(obj.TLim(1)), -sin(obj.TLim(1));
                             sin(obj.TLim(1)),  cos(obj.TLim(1))];
                switch obj.Orientation
                    case 'left'
                        tY = (obj.WTick - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                        tX = ones(size(tY)).*abs(diff(obj.XLim)).*(obj.RTick(1)-1) + obj.XLim(2);
                        tXY = rotateMat*[tX;tY];
                        tT = obj.TLim(1)/pi*180;
                        if mod(tT,360)>90 && mod(tT,360)<270
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-180'HorizontalAlignment''right', obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT, obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tY = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                            tX = abs(diff(obj.XLim)).*(obj.RTick(4)-1) + obj.XLim(2);
                            tXY = rotateMat*[tX;tY];
                            if mod(tT,360)>180 && mod(tT,360)<360
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+180-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------    
                    case 'right'
                        tY = (obj.WTick - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                        tX = - ones(size(tY)).*abs(diff(obj.XLim)).*(obj.RTick(1)-1) + obj.XLim(1);
                        tXY = rotateMat*[tX;tY];
                        tT = obj.TLim(1)/pi*180;
                        if mod(tT,360)>90 && mod(tT,360)<270
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-180, obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT, 'HorizontalAlignment''right', obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tY = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
                            tX = - abs(diff(obj.XLim)).*(obj.RTick(4)-1) + obj.XLim(1);
                            tXY = rotateMat*[tX;tY];
                            if mod(tT,360)>180 && mod(tT,360)<360
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+180-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------
                    case 'top'
                        tX = (obj.WTick - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                        tY = - ones(size(tX)).*abs(diff(obj.YLim)).*(obj.RTick(1)-1) + obj.YLim(1);
                        tXY = rotateMat*[tX;tY];
                        tT = obj.TLim(1)/pi*180;
                        if mod(tT,360)>180 && mod(tT,360)<360
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90-180'HorizontalAlignment''right',obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90, obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tX = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                            tY = - abs(diff(obj.YLim)).*(obj.RTick(4)-1) + obj.YLim(1);
                            tXY = rotateMat*[tX;tY];
                            if mod(tT,360)>90 && mod(tT,360)<270
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+180'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT, 'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------
                    case 'bottom'
                        tX = (obj.WTick - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                        tY = ones(size(tX)).*abs(diff(obj.YLim)).*(obj.RTick(1)-1) + obj.YLim(2);
                        tXY = rotateMat*[tX;tY];
                        tT = obj.TLim(2)/pi*180;
                        if mod(tT,360)>180 && mod(tT,360)<360
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90-180, obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90'HorizontalAlignment''right', obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tX = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                            tY = abs(diff(obj.YLim)).*(obj.RTick(4)-1) + obj.YLim(2);
                            tXY = rotateMat*[tX;tY];
                            if mod(tT,360)>90 && mod(tT,360)<270
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+180'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT, 'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end
                end



            % =============================================================
            else
                rotateMat1 = [cos(obj.TLim(1)), -sin(obj.TLim(1));
                              sin(obj.TLim(1)),  cos(obj.TLim(1))];
                rotateMat2 = [cos(obj.TLim(2)), -sin(obj.TLim(2));
                              sin(obj.TLim(2)),  cos(obj.TLim(2))];
                tT3 = obj.TLim(1) - diff(obj.TLim).*(obj.RTick(4)-1);
                tT4 = obj.TLim(2) + diff(obj.TLim).*(obj.RTick(4)-1);
                rotateMat3 = [cos(tT3), -sin(tT3);
                              sin(tT3),  cos(tT3)];
                rotateMat4 = [cos(tT4), -sin(tT4);
                              sin(tT4),  cos(tT4)];
                tT3 = tT3/pi*180;
                tT4 = tT4/pi*180;
                switch obj.Orientation
                    case 'left'
                        tT = (obj.WTick - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.TLim) + obj.TLim(1);
                        tT = tT./pi.*180;
                        tR = ones(size(tT)).*abs(diff(obj.XLim)).*(obj.RTick(1)-1) + obj.XLim(2);
                        for i = 1:length(tT)
                            if mod(tT(i),360)<90 || mod(tT(i),360)>270
                                obj.sampleLabelHdl{i} = text(obj.ax, tR(i).*cos(tT(i)*pi/180), tR(i).*sin(tT(i)*pi/180), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT(i), obj.SampleFont{:});
                            else
                                obj.sampleLabelHdl{i} = text(obj.ax, tR(i).*cos(tT(i)*pi/180), tR(i).*sin(tT(i)*pi/180), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT(i)+180'HorizontalAlignment''right', obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tT = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.TLim) + obj.TLim(1);
                            tT = tT./pi.*180;
                            tR = abs(diff(obj.XLim)).*(obj.RTick(4)-1) + obj.XLim(2);
                            if mod(tT,360)<180
                                obj.classLabelHdl{i} = text(obj.ax, tR.*cos(tT*pi/180), tR.*sin(tT*pi/180), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tR.*cos(tT*pi/180), tR.*sin(tT*pi/180), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------
                    case 'right'
                        tT = (obj.WTick - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.TLim) + obj.TLim(1);
                        tT = tT./pi.*180;
                        tR = - ones(size(tT)).*abs(diff(obj.XLim)).*(obj.RTick(1)-1) + obj.XLim(1);
                        for i = 1:length(tT)
                            if mod(tT(i),360)<90 || mod(tT(i),360)>270
                                obj.sampleLabelHdl{i} = text(obj.ax, tR(i).*cos(tT(i)*pi/180), tR(i).*sin(tT(i)*pi/180), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT(i), 'HorizontalAlignment''right', obj.SampleFont{:});
                            else
                                obj.sampleLabelHdl{i} = text(obj.ax, tR(i).*cos(tT(i)*pi/180), tR(i).*sin(tT(i)*pi/180), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT(i)+180, obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tT = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.TLim) + obj.TLim(1);
                            tT = tT./pi.*180;
                            tR = - abs(diff(obj.XLim)).*(obj.RTick(4)-1) + obj.XLim(1);
                            if mod(tT,360)<180
                                obj.classLabelHdl{i} = text(obj.ax, tR.*cos(tT*pi/180), tR.*sin(tT*pi/180), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT-90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tR.*cos(tT*pi/180), tR.*sin(tT*pi/180), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT+90'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------
                    case 'top'
                        tX = (obj.WTick - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                        tY = - ones(size(tX)).*abs(diff(obj.YLim)).*(obj.RTick(1)-1);
                        tXY = rotateMat1*[tX;tY];
                        tT = obj.TLim(1)/pi*180;
                        if mod(tT,360)>180 && mod(tT,360)<360
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90-180'HorizontalAlignment''right',obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90, obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tX = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                            tY = 0;
                            tXY = rotateMat3*[tX;tY];
                            if mod(tT3,360)>90 && mod(tT3,360)<270
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT3+180'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT3, 'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end

                    % -----------------------------------------------------
                    case 'bottom'
                        abs(diff(obj.YLim))
                        tX = (obj.WTick - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                        tY = ones(size(tX)).*abs(diff(obj.YLim)).*(obj.RTick(1)-1);
                        tXY = rotateMat2*[tX;tY];
                        tT = obj.TLim(2)/pi*180;
                        if mod(tT,360)>180 && mod(tT,360)<360
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90-180, obj.SampleFont{:});
                            end
                        else
                            for i = 1:length(tX)
                                obj.sampleLabelHdl{i} = text(obj.ax, tXY(1,i), tXY(2,i), obj.SampleName{obj.order(i)},...
                                    'FontSize'12'Rotation', tT-90'HorizontalAlignment''right', obj.SampleFont{:});
                            end
                        end
                        for i = 1:obj.MaxClust
                            tX = (mean(obj.WTick(obj.class == classNum(i))) - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
                            tY = 0;
                            tXY = rotateMat4*[tX;tY];
                            if mod(tT4,360)>90 && mod(tT4,360)<270
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT4+180'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            else
                                obj.classLabelHdl{i} = text(obj.ax, tXY(1), tXY(2), obj.ClassName{i},...
                                    'FontSize'12'Rotation', tT4, 'Color', colorList(i,:), 'HorizontalAlignment''center', obj.ClassFont{:});
                            end
                        end
                end
            end
            if strcmpi(obj.ClassLabel, 'off')
                for i = 1:obj.MaxClust
                    set(obj.classLabelHdl{i}, 'Visible''off');
                end
            end
            if strcmpi(obj.Label, 'off')
                for i = 1:length(obj.sampleLabelHdl)
                    set(obj.sampleLabelHdl{i}, 'Visible''off')
                end
            end
            axis tight
            % 部分功能函数 -------------------------------------------------
            function pnts=bezierCurve(pnts,N)
                t=linspace(0,1,N);
                p=size(pnts,1)-1;
                coe1=factorial(p)./factorial(0:p)./factorial(p:-1:0);
                coe2=((t).^((0:p)')).*((1-t).^((p:-1:0)'));
                pnts=(pnts'*(coe1'.*coe2))';
            end
        end
    end
% Copyright (c) 2024, Zhaoxu Liu / slandarer
% =========================================================================
% @author : slandarer
% 公众号  : slandarer随笔
% 知乎    : slandarer
% -------------------------------------------------------------------------
% Zhaoxu Liu / slandarer (2024). STree dendrogram 
% (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), 
% MATLAB Central File Exchange.
end

3-2 SMatrix 完整代码

classdef SMatrix < handle
% Copyright (c) 2024, Zhaoxu Liu / slandarer
% =========================================================================
% @author : slandarer
% 公众号  : slandarer随笔
% 知乎    : slandarer
% -------------------------------------------------------------------------
% Zhaoxu Liu / slandarer (2024). STree dendrogram 
% (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), 
% MATLAB Central File Exchange.
    properties
        ax, H, XLim, YLim, TLim = [0,0
        oriXLim, oriYLim, XSet, YSet, Colormap, Parent, sInd
        LeftLabelFont = {'FontSize'10'FontName''Times New Roman'}
        RightLabelFont = {'FontSize'10'FontName''Times New Roman'}
        TopLabelFont = {'FontSize'10'FontName''Times New Roman'}
        BottomLabelFont = {'FontSize'10'FontName''Times New Roman'}
        RowOrder, RowClass, RowName
        ColOrder, ColClass, ColName

        heatmapHdl 

        maxH, ClustGap = 'off';
        XTick, YTick

        TopLabel     =  'off'
        BottomLabel  =  'on' 
        LeftLabel    =  'on' 
        RightLabel   =  'off'
        topLabelHdl, bottomLabelHdl, leftLabelHdl, rightLabelHdl

        dfColor1=[0.9686    0.9882    0.9412;    0.9454    0.9791    0.9199;    0.9221    0.9700    0.8987;    0.8988    0.9609    0.8774;
                  0.8759    0.9519    0.8560;    0.8557    0.9438    0.8338;    0.8354    0.9357    0.8115;    0.8152    0.9276    0.7892;
                  0.7909    0.9180    0.7685;    0.7545    0.9039    0.7523;    0.7180    0.8897    0.7361;    0.6816    0.8755    0.7199;
                  0.6417    0.8602    0.7155;    0.5962    0.8430    0.7307;    0.5507    0.8258    0.7459;    0.5051    0.8086    0.7610;
                  0.4596    0.7873    0.7762;    0.4140    0.7620    0.7914;    0.3685    0.7367    0.8066;    0.3230    0.7114    0.8218;
                  0.2837    0.6773    0.8142;    0.2483    0.6378    0.7929;    0.2129    0.5984    0.7717;    0.1775    0.5589    0.7504;
                  0.1421    0.5217    0.7314;    0.1066    0.4853    0.7132;    0.0712    0.4488    0.6950;    0.0358    0.4124    0.6768;
                  0.0314    0.3724    0.6364;    0.0314    0.3319    0.5929;    0.0314    0.2915    0.5494;    0.0314    0.2510    0.5059]
        dfColor2=[0.6196    0.0039    0.2588;    0.6892    0.0811    0.2753;    0.7588    0.1583    0.2917;    0.8283    0.2354    0.3082;
                  0.8706    0.2966    0.2961;    0.9098    0.3561    0.2810;    0.9490    0.4156    0.2658;    0.9660    0.4932    0.2931;
                  0.9774    0.5755    0.3311;    0.9887    0.6577    0.3690;    0.9930    0.7266    0.4176;    0.9943    0.7899    0.4707;
                  0.9956    0.8531    0.5238;    0.9968    0.9020    0.5846;    0.9981    0.9412    0.6503;    0.9994    0.9804    0.7161;
                  0.9842    0.9937    0.7244;    0.9526    0.9810    0.6750;    0.9209    0.9684    0.6257;    0.8721    0.9486    0.6022;
                  0.7975    0.9183    0.6173;    0.7228    0.8879    0.6325;    0.6444    0.8564    0.6435;    0.5571    0.8223    0.6448;
                  0.4698    0.7881    0.6460;    0.3868    0.7461    0.6531;    0.3211    0.6727    0.6835;    0.2553    0.5994    0.7139;
                  0.2016    0.5261    0.7378;    0.2573    0.4540    0.7036;    0.3130    0.3819    0.6694;    0.3686    0.3098    0.6353]

        arginList = {'Parent''Layout''Colormap''XLim''YLim''TLim',...
            'RowName''ColName''Font''Parent',...
            'RowOrder''RowClass''RowName''ColOrder''ColClass''ColName',...
            'TopLabelFont' , 'BottomLabelFont''LeftLabelFont''RightLabelFont',...
            'ClustGap'};

    end

    methods
        function obj = SMatrix(varargin)
            % 获取基本数据 -------------------------------------------------
            if isa(varargin{1}, 'matlab.graphics.axis.Axes')
                obj.ax = varargin{1}; varargin(1) = [];
            else  
            end
            obj.H = varargin{1}; varargin(1) = [];
            % 获取其他信息 -------------------------------------------------
            for i = 1:2:(length(varargin)-1)
                tid = ismember(obj.arginList, varargin{i});
                if any(tid)
                obj.(obj.arginList{tid}) = varargin{i+1};
                end
            end
            if isempty(obj.ax) && (~isempty(obj.Parent)), obj.ax=obj.Parent; end
            if isempty(obj.ax), obj.ax=gca; end
            obj.maxH=max(max(abs(obj.H)));
            % 设置配色 ----------------------------------------------------
            if isempty(obj.Colormap)
                if any(any(obj.H<0))
                    obj.Colormap=obj.dfColor2;
                    % tX=linspace(0,1,size(obj.Colormap,1));
                    % tXi=linspace(0,1,256);
                    % tR=interp1(tX,obj.Colormap(:,1),tXi);
                    % tG=interp1(tX,obj.Colormap(:,2),tXi);
                    % tB=interp1(tX,obj.Colormap(:,3),tXi);
                    % obj.Colormap=[tR(:),tG(:),tB(:)];
                else
                    obj.Colormap=obj.dfColor1(end:-1:1,:);
                end
            end
            % 分类情况 -----------------------------------------------------
            if isempty(obj.RowClass), obj.RowClass = ones(1size(obj.H, 1)); end
            if isempty(obj.ColClass), obj.ColClass = ones(1size(obj.H, 2)); end
            if isempty(obj.RowOrder), obj.RowOrder = 1:size(obj.H, 1); end
            if isempty(obj.ColOrder), obj.ColOrder = 1:size(obj.H, 2); end
            if isempty(obj.RowName), obj.RowName = compose('row%d'1:size(obj.H, 1)); end
            if isempty(obj.ColName), obj.ColName = compose('col%d'1:size(obj.H, 2)); end
        end

        function draw(obj)
            obj.ax.NextPlot = 'add';
            % 配色设置 -----------------------------------------------------
            colormap(obj.ax, obj.Colormap)
            colorbar(obj.ax)
            if any(any(obj.H < 0))
                try caxis(obj.ax, obj.maxH.*[-1,1]), catchend
                try clim(obj.ax, obj.maxH.*[-1,1]), catchend
            else
                try caxis(obj.ax, obj.maxH.*[0,1]), catch,end
                try clim(obj.ax, obj.maxH.*[0,1]), catch,end
            end
            % 原始X,Y范围获取 ----------------------------------------------
            gapRow = find(diff(obj.RowClass)~=0)+.5;
            gapCol = find(diff(obj.ColClass)~=0)+.5;
            obj.XSet = 1:size(obj.H, 2);
            obj.YSet = 1:size(obj.H, 1);
            if strcmpi(obj.ClustGap, 'on')
                for i = length(gapRow):-1:1
                    obj.YSet(obj.YSet>gapRow(i)) = obj.YSet(obj.YSet>gapRow(i))+1;
                end
                for i = length(gapCol):-1:1
                    obj.XSet(obj.XSet>gapCol(i)) = obj.XSet(obj.XSet>gapCol(i))+1;
                end
            end
            if abs(obj.TLim(1)-obj.TLim(2)) < eps
                obj.XTick = [obj.XSet(1)-.75, obj.XSet, obj.XSet(end)+.75]; 
                obj.YTick = [obj.YSet(1)-.75, obj.YSet, obj.YSet(end)+.75];
            else
                obj.XTick = [obj.XSet(1)-.75, obj.XSet, obj.XSet(end)+.75];
                obj.YTick = [obj.YSet(1)-.5, obj.YSet, obj.YSet(end)+.5];
            end
            if strcmp(obj.ClustGap,'on')
                gap = 1;
            else
                gap = .5;
            end
            obj.oriXLim = [1 - gap, max(max(obj.XSet)) + gap];
            obj.oriYLim = [1 - gap, max(max(obj.YSet)) + gap];
            if isempty(obj.XLim), obj.XLim = obj.oriXLim; end
            if isempty(obj.YLim), obj.YLim = obj.oriYLim; end
            % 坐标放缩 -----------------------------------------------------
            obj.sInd = reshape(1size(obj.H, 2)*size(obj.H, 1), size(obj.H));
            baseX = [linspace(-1,1,30), ones(1,30), linspace(1,-1,30), -ones(1,30)].*.5;
            baseY = [-ones(1,30), linspace(-1,1,30), ones(1,30), linspace(1,-1,30)].*.5;
            [obj.XSet, obj.YSet] = meshgrid(obj.XSet, obj.YSet);
            obj.XSet = obj.XSet(:) + baseX;
            obj.YSet = obj.YSet(:) + baseY;
            obj.XSet = (obj.XSet - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
            obj.YSet = (obj.YSet - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
            obj.XTick = (obj.XTick - obj.oriXLim(1))./diff(obj.oriXLim).*diff(obj.XLim) + obj.XLim(1);
            obj.YTick = (obj.YTick - obj.oriYLim(1))./diff(obj.oriYLim).*diff(obj.YLim) + obj.YLim(1);
            % 坐标旋转 -----------------------------------------------------
            [XTick_B, YTick_B] = rotate(obj.XTick(2:end-1), obj.YTick(1) + obj.XTick(2:end-1).*0, obj.YLim, obj.TLim);
            [XTick_T, YTick_T] = rotate(obj.XTick(2:end-1), obj.YTick(end) + obj.XTick(2:end-1).*0, obj.YLim, obj.TLim);
            [XTick_L, YTick_L] = rotate(obj.XTick(1) + obj.YTick(2:end-1).*0, obj.YTick(2:end-1), obj.YLim, obj.TLim);
            [XTick_R, YTick_R] = rotate(obj.XTick(end) + obj.YTick(2:end-1).*0, obj.YTick(2:end-1), obj.YLim, obj.TLim);
            [obj.XSet, obj.YSet] = rotate(obj.XSet, obj.YSet, obj.YLim, obj.TLim);
            
            function [X2,Y2] = rotate(X1,Y1,YLim,TLim)
                if abs(TLim(1)-TLim(2)) < eps
                    rotateMat = [cos(TLim(1)), -sin(TLim(1));
                                 sin(TLim(1)),  cos(TLim(1))];
                    tXYSet = rotateMat*[X1(:).'; Y1(:).'];
                    X2 = reshape(tXYSet(1,:), size(X1,1), []);
                    Y2 = reshape(tXYSet(2,:), size(Y1,1), []);
                else
                    tTSet = (Y1 - YLim(1))./diff(YLim).*diff(TLim) + TLim(1);
                    tRSet = X1;
                    X2 = cos(tTSet).*tRSet;
                    Y2 = sin(tTSet).*tRSet;
                end
            end
            if obj.TLim(1) ~= 0 || obj.TLim(2) ~= 0
                obj.ax.DataAspectRatio = [1,1,1];
            end

            % 图形绘制 -----------------------------------------------------
            HH = obj.H(obj.RowOrder, obj.ColOrder);
            for i = 1:size(obj.XSet,1)
                obj.heatmapHdl{i} = fill(obj.ax, obj.XSet(i,:), obj.YSet(i,:), HH(i == obj.sInd),'EdgeColor','w','LineWidth',.5);
            end
            if abs(obj.TLim(1)-obj.TLim(2)) < eps
                tT = obj.TLim(1)/pi*180;
                for i = 1:length(XTick_B)
                    if mod(tT,360)>45 && mod(tT,360)<225
                        obj.bottomLabelHdl{i}=text(obj.ax, XTick_B(i), YTick_B(i), [obj.ColName{obj.ColOrder(i)}], 'FontSize'12,...
                        'Rotation', tT+45+180'HorizontalAlignment''left', obj.BottomLabelFont{:});
                    else
                        obj.bottomLabelHdl{i}=text(obj.ax, XTick_B(i), YTick_B(i), [obj.ColName{obj.ColOrder(i)}], 'FontSize'12,...
                        'Rotation'45+tT, 'HorizontalAlignment''right', obj.BottomLabelFont{:});
                    end
                end
                for i = 1:length(XTick_T)
                    if mod(tT,360)>45 && mod(tT,360)<225
                        obj.topLabelHdl{i}=text(obj.ax, XTick_T(i), YTick_T(i), [obj.ColName{obj.ColOrder(i)}], 'FontSize'12,...
                        'Rotation', tT+45+180'HorizontalAlignment''right', obj.TopLabelFont{:});
                    else
                        obj.topLabelHdl{i}=text(obj.ax, XTick_T(i), YTick_T(i), [obj.ColName{obj.ColOrder(i)}], 'FontSize'12,...
                        'Rotation'45+tT, 'HorizontalAlignment''left', obj.TopLabelFont{:});
                    end
                end
                for i = 1:length(XTick_L)
                    if mod(tT,360)>90 && mod(tT,360)<270
                        obj.leftLabelHdl{i}=text(obj.ax, XTick_L(i), YTick_L(i), [obj.RowName{obj.RowOrder(i)}], 'FontSize'12,...
                        'Rotation', tT+180'HorizontalAlignment''left', obj.LeftLabelFont{:});
                    else
                        obj.leftLabelHdl{i}=text(obj.ax, XTick_L(i), YTick_L(i), [obj.RowName{obj.RowOrder(i)}], 'FontSize'12,...
                        'Rotation', tT, 'HorizontalAlignment''right', obj.LeftLabelFont{:});
                    end
                end
                for i = 1:length(XTick_R)
                    if mod(tT,360)>90 && mod(tT,360)<270
                        obj.rightLabelHdl{i}=text(obj.ax, XTick_R(i), YTick_R(i), [obj.RowName{obj.RowOrder(i)}], 'FontSize'12,...
                        'Rotation', tT+180'HorizontalAlignment''right', obj.RightLabelFont{:});
                    else
                        obj.rightLabelHdl{i}=text(obj.ax, XTick_R(i), YTick_R(i), [obj.RowName{obj.RowOrder(i)}], 'FontSize'12,...
                        'Rotation', tT, 'HorizontalAlignment''left', obj.RightLabelFont{:});
                    end
                end
            else
                tT1 = obj.TLim(1)/pi*180;
                tT2 = obj.TLim(2)/pi*180;
                for i = 1:length(XTick_B)
                    if mod(tT1,360)>180
                        obj.bottomLabelHdl{i}=text(obj.ax, XTick_B(i), YTick_B(i), [' ',obj.ColName{obj.ColOrder(i)},' '], 'FontSize'12,...
                        'Rotation', tT1+90'HorizontalAlignment''right', obj.BottomLabelFont{:});
                    else
                        obj.bottomLabelHdl{i}=text(obj.ax, XTick_B(i), YTick_B(i), [' ',obj.ColName{obj.ColOrder(i)},' '], 'FontSize'12,...
                        'Rotation', tT1-90'HorizontalAlignment''left', obj.BottomLabelFont{:});
                    end
                end
                for i = 1:length(XTick_T)
                    if mod(tT2,360)>180
                        obj.topLabelHdl{i}=text(obj.ax, XTick_T(i), YTick_T(i), [' ',obj.ColName{obj.ColOrder(i)},' '], 'FontSize'12,...
                        'Rotation', tT2+90'HorizontalAlignment''left', obj.TopLabelFont{:});
                    else
                        obj.topLabelHdl{i}=text(obj.ax, XTick_T(i), YTick_T(i), [' ',obj.ColName{obj.ColOrder(i)},' '], 'FontSize'12,...
                        'Rotation', tT2-90'HorizontalAlignment''right', obj.TopLabelFont{:});
                    end
                end
                tT = (obj.YTick(2:end-1) - obj.YLim(1))./diff(obj.YLim).*diff(obj.TLim) + obj.TLim(1);
                tT = tT./pi.*180;
                RR = obj.XTick(end);
                RL = obj.XTick(1);
                for i = 1:length(tT)
                    if mod(tT(i),360)<90 || mod(tT(i),360)>270
                        obj.leftLabelHdl{i} = text(obj.ax, RL.*cos(tT(i)*pi/180), RL.*sin(tT(i)*pi/180), [obj.RowName{obj.RowOrder(i)}],...
                            'FontSize'12'Rotation', tT(i), 'HorizontalAlignment''right', obj.LeftLabelFont{:});
                    else
                        obj.leftLabelHdl{i} = text(obj.ax, RL.*cos(tT(i)*pi/180), RL.*sin(tT(i)*pi/180), [obj.RowName{obj.RowOrder(i)}],...
                            'FontSize'12'Rotation', tT(i)+180, obj.LeftLabelFont{:});
                    end
                end
                for i = 1:length(tT)
                    if mod(tT(i),360)<90 || mod(tT(i),360)>270
                        obj.rightLabelHdl{i} = text(obj.ax, RR.*cos(tT(i)*pi/180), RR.*sin(tT(i)*pi/180), [obj.RowName{obj.RowOrder(i)}],...
                            'FontSize'12'Rotation', tT(i), obj.RightLabelFont{:});
                    else
                        obj.rightLabelHdl{i} = text(obj.ax, RR.*cos(tT(i)*pi/180), RR.*sin(tT(i)*pi/180), [obj.RowName{obj.RowOrder(i)}],...
                            'FontSize'12'Rotation', tT(i)+180'HorizontalAlignment''right', obj.RightLabelFont{:});
                    end
                end
            end

            if strcmpi(obj.TopLabel,'off'),for i = 1:length(obj.topLabelHdl), set(obj.topLabelHdl{i}, 'Visible''off'); endend
            if strcmpi(obj.BottomLabel,'off'),for i = 1:length(obj.bottomLabelHdl), set(obj.bottomLabelHdl{i}, 'Visible''off'); endend
            if strcmpi(obj.LeftLabel,'off'),for i = 1:length(obj.leftLabelHdl), set(obj.leftLabelHdl{i}, 'Visible''off'); endend
            if strcmpi(obj.RightLabel,'off'),for i = 1:length(obj.rightLabelHdl), set(obj.rightLabelHdl{i}, 'Visible''off'); endend
        end
    end
% Copyright (c) 2024, Zhaoxu Liu / slandarer
% =========================================================================
% @author : slandarer
% 公众号  : slandarer随笔
% 知乎    : slandarer
% -------------------------------------------------------------------------
% Zhaoxu Liu / slandarer (2024). STree dendrogram 
% (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), 
% MATLAB Central File Exchange.
end

未经允许本代码请勿作商业用途,引用的话可以引用我file exchange上的链接,可使用如下格式:

Zhaoxu Liu / slandarer (2024). STree dendrogram (https://www.mathworks.com/matlabcentral/fileexchange/160048-stree-dendrogram), MATLAB Central File Exchange. Retrieved February 23, 2024.

若转载请保留以上file exchange链接及本文链接!!!!!

本文全部代码已同时上传gitee仓库,若懒得一一获取代码和工具,可以去以下gitee仓库获取全部代码:

  • https://gitee.com/slandarer/matlab-stree-dendrogram


slandarer随笔
slandarer个人公众号,目前主要更新MATLAB相关内容。
 最新文章