前段時間在小紅書刷到了一個很有特色的熱力圖,由大佬@滾筒洗衣機創作,感覺很有意思,嘗試 MATLAB 復刻:
作者使用的是 python 代碼,趕快去瞅瞅。
復刻效果
正文部分
0.數據準備
數據需要一個用來畫熱圖的矩陣以及一個和矩陣相同列數的數組或者元胞數組。例如:
% 例子1 隨便構造數據
% 隨便構造的數據,可以換成自己的數據
clc; clear; rng(4)
Data = rand([7, 12]) + 1 + sin(linspace(0, 2*pi, 12) - pi/1.2) + (1:7).'./12;
Data = Data./max(max(Data));% 繪制小提琴圖的數據,應為列數與 Data 相同的矩陣或元胞數組
VData = mean(Data, 1) + randn([50, size(Data, 2)]).*.6;
這組數據畫出來大概長這樣:
此外假如我有兩行數據,一行是日期,一行是數值,我們也可以直接將這組數據進行處理,統計其均值來畫熱圖,然后按照每個月的數據分類畫小提琴圖,例如我們讓 chatGPT 生成一組數據:
% 對chatGPT:
% 使用matlab構造一組數據,為2018第一天到2024年最后一天的數據,要求具有季節性clc;clear
% 創建日期向量
t = datetime(2018,1,1):days(1):datetime(2024,12,31);
n = length(t);% 將日期轉換為一年中的位置(1 到 365/366)
day_of_year = day(t, 'dayofyear');
year_length = year(t); % 判斷閏年時有用
is_leap = eomday(year(t),2) == 29;% 構造季節性數據:例如正弦函數,每年重復一次(周期365)
% 基本模式:sin(2*pi * day_of_year / 365)
% 添加噪聲 + 趨勢(可選)
seasonal = 10 * sin(2*pi * day_of_year ./ 365); % 季節性(年周期)
noise = randn(1, n).*5; % 噪聲
trend = 0.01 * (1:n); % 微小上升趨勢% 最終數據
v = seasonal + noise + trend;% 可視化
plot(t, v)
xlabel('Date')
ylabel('Value')
title('Synthetic Seasonal Data (2018–2024)')Data.t = t;
Data.v = v;
save test.mat Data
這組數據大概長這樣:
我們讀取存儲的數據并進行統計:
% 例子2 已有各年份每一天數據,對其進行統計
clc; clear
testData = load('test.mat');
t = testData.Data.t;
v = testData.Data.v;
y = 2024:-1:2018;
% 構造一個矩陣,第 i 行第 j 列是第 i 年第 j 個月的數值平均值
Data = zeros(length(y), 12);
for i = 1:length(y)for m = 1:12idx = (year(t) == y(i)) & (month(t) == m);Data(i, m) = mean(v(idx));end
end
% 構造一個元胞數組,第 i 個元胞是 i 月全部數值合集
VData{12} = [];
for m = 1:12idx = (month(t) == m);VData{m} = v(idx)';
end
這組數據畫出圖來形狀如下:
當然還需要定義行標簽和列標簽:
% 矩陣每行名稱
% rowName = {'2024','2023','2022','2021',2020','2019','2018'};
rowName = compose('%d',2024:-1:2018);% 矩陣每列名稱
colName = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', ...'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'};
1.基礎樣式
基本不太用動,數據范圍及刻度位置不設置的話會自動計算,那個被注釋掉的配色就是我嘗試仿照原作者弄的配色:
% 標簽格式
fontProp = {'FontSize',16, 'FontName','Times New Roman'};% 配色
CMap = 'summer';
% CMap = interp1([0,.5,1], [214,115,144;255,238,234;107,152,191]./255, 0:.01:1);% 小提琴圖寬度
width = 0.9;% 數據范圍,以及刻度位置
% VLim = [0, 1];
% VTick = 0:.2:1;
VLim = [];
VTick = [];
完整代碼
因為繪圖部分比較長,我這里直接放一下帶著前面數據定義的完整代碼得了:
% ----------------------------------------------------------------------
% 例子1 隨便構造數據
% 隨便構造的數據,可以換成自己的數據
clc; clear; rng(4)
Data = rand([7, 12]) + 1 + sin(linspace(0, 2*pi, 12) - pi/1.2) + (1:7).'./12;
Data = Data./max(max(Data));% 繪制小提琴圖的數據,應為列數與 Data 相同的矩陣或元胞數組
VData = mean(Data, 1) + randn([50, size(Data, 2)]).*.6;% ----------------------------------------------------------------------
% % 例子2 已有各年份每一天數據,對其進行統計
% clc; clear
% testData = load('test.mat');
% t = testData.Data.t;
% v = testData.Data.v;
% y = 2024:-1:2018;
% % 構造一個矩陣,第 i 行第 j 列是第 i 年第 j 個月的數值平均值
% Data = zeros(length(y), 12);
% for i = 1:length(y)
% for m = 1:12
% idx = (year(t) == y(i)) & (month(t) == m);
% Data(i, m) = mean(v(idx));
% end
% end
% % 構造一個元胞數組,第 i 個元胞是 i 月全部數值合集
% VData{12} = [];
% for m = 1:12
% idx = (month(t) == m);
% VData{m} = v(idx)';
% end% 矩陣每行名稱
% rowName = {'2024','2023','2022','2021',2020','2019','2018'};
rowName = compose('%d',2024:-1:2018);% 矩陣每列名稱
colName = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', ...'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'};%% 格式設置 ===============================================================
% 標簽格式
fontProp = {'FontSize',16, 'FontName','Times New Roman'};% 配色
CMap = 'summer';
% CMap = interp1([0,.5,1], [214,115,144;255,238,234;107,152,191]./255, 0:.01:1);% 小提琴圖寬度
width = 0.9;% 數據范圍,以及刻度位置
% VLim = [0, 1];
% VTick = 0:.2:1;
VLim = [];
VTick = [];%% 數據預處理 ============================================================
% 如果不設置 VLim, VTick
% 則自動計算一個比較合理的刻度位置
linearTickCompactDegree = 5;
if isempty(VLim)if iscell(VData)VLim = [min(min(min(Data)), min(cellfun(@min, VData)))...max(max(max(Data)), max(cellfun(@max, VData)))];elseVLim = [min(min(min(Data)), min(min(VData)))...max(max(max(Data)), max(max(VData)))];end
end
if isempty(VTick)tXS = diff(VLim) / linearTickCompactDegree;tXN = ceil(log(tXS) / log(10));tXS = round(round(tXS / 10^(tXN-2)) / 5) * 5 * 10^(tXN-2);tVTick1 = 0:-tXS:VLim(1);tVTick2 = 0:tXS:VLim(2);VTick = unique([tVTick1, tVTick2]);VTick(VTick < VLim(1)) = [];VTick(VTick > VLim(2)) = [];
end%% 繪圖部分代碼 ============================================================
% 構造圖窗及坐標區域
fig = figure('Units','normalized', 'Position',[.1,.1,.6,.8], 'Color','w');
ax = axes('Parent',fig, 'XLim',[-sqrt(3), sqrt(3)], 'YLim',[0, 2], ...'DataAspectRatio',[1,1,1], 'Position',[.05,.2,.9,.8], ...'NextPlot','add', 'XColor','none', 'YColor','none');
colormap(CMap)% 繪制射線
w = 1*pi/3/size(Data, 2);
tt = linspace(5*pi/6 - w, pi/6 + w, size(Data, 2));
xx = cos(tt).*2; xx = [xx; xx.*0; xx.*nan(1)];
yy = sin(tt).*2; yy = [yy; yy.*0; yy.*nan(1)];
plot(ax, xx(:), yy(:), 'LineWidth',1, 'Color',[1,1,1].*.8, 'LineStyle','--')% 繪制列標簽
for i = 1:length(colName)text(ax, cos(tt(i)).*2.01, sin(tt(i)).*2.01, colName(i), ...'HorizontalAlignment','center', 'VerticalAlignment', 'bottom', ...'Rotation', tt(i)/pi*180 - 90, fontProp{:})
end% 繪制行名稱標簽
for i = 1:length(rowName)r = 2/5 + (size(Data, 1) - i + .5)*4/size(Data, 1)/5;if mod(i, 2) == 1text(ax, cos(5*pi/6).*r - 1/100, sin(5*pi/6).*r - sqrt(3)/100, rowName{i}, ...'HorizontalAlignment','right', 'Rotation',60, fontProp{:})elsetext(ax, cos(pi/6).*r + 1/100, sin(pi/6).*r - sqrt(3)/100, rowName{i}, ...'HorizontalAlignment','left', 'Rotation',-60, fontProp{:})end
end% 繪制刻度軸線
plot(ax, cos(5*pi/6).*[6/5 + 1/10, 10/5 - 1/10], ...sin(5*pi/6).*[6/5 + 1/10, 10/5 - 1/10], 'LineWidth',1, 'Color','k')
plot(ax, cos(pi/6).*[6/5 + 1/10, 10/5 - 1/10], ...sin(pi/6).*[6/5 + 1/10, 10/5 - 1/10], 'LineWidth',1, 'Color','k')% 繪制刻度和刻度標簽
for i = 1:length(VTick)r = (VTick(i) - VLim(1))./diff(VLim).*(3/5) + (6/5 + 1/10);x1 = [cos(5*pi/6).*r, cos(5*pi/6).*r + 1/100];y1 = [sin(5*pi/6).*r, sin(5*pi/6).*r + sqrt(3)/100];plot(ax, x1, y1, 'LineWidth',1, 'Color','k')x2 = [cos(pi/6).*r, cos(pi/6).*r - 1/100];y2 = [sin(pi/6).*r, sin(pi/6).*r + sqrt(3)/100];plot(ax, x2, y2, 'LineWidth',1, 'Color','k')if mod(length(VTick) - i, 2) == 0text(ax, cos(5*pi/6).*r - 1/100, sin(5*pi/6).*r - sqrt(3)/100, num2str(VTick(i)), ...'HorizontalAlignment','right', 'Rotation',60, fontProp{:})elsetext(ax, cos(pi/6).*r + 1/100, sin(pi/6).*r - sqrt(3)/100, num2str(VTick(i)), ...'HorizontalAlignment','left', 'Rotation',-60, fontProp{:})end
end% 繪制小提琴圖
maxf = 0;
for i = 1:size(Data, 2)if iscell(VData)tY = VData{i};elsetY = VData(:,i);endtY(isnan(tY)) = [];[f, yi] = ksdensity(tY);maxf = max(maxf, max(f));
end
for i = 1:size(Data, 2)if iscell(VData)tY = VData{i};elsetY = VData(:,i);endtY(isnan(tY)) = [];[f, yi] = ksdensity(tY);yyi = [min(tY), yi(yi<max(tY) & yi>min(tY)), max(tY)];ind1 = find(yi<max(tY) & yi>min(tY), 1, 'first');ind2 = find(yi<max(tY) & yi>min(tY), 1, 'last');f1 = interp1(yi((ind1 - 1):ind1), f((ind1 - 1):ind1), min(tY));f2 = interp1(yi(ind2:(ind2 + 1)), f(ind2:(ind2 + 1)), max(tY));ff = [f1, f(yi<max(tY) & yi>min(tY)), f2];xx = [ff, -ff(end:-1:1)]./maxf.*(4*pi/5/size(Data, 2)).*width./2;yy = ([yyi, yyi(end:-1:1)] - VLim(1))./diff(VLim).*3./5 + 6/5 + 1/10;xy = [cos(tt(i) - pi/2), - sin(tt(i) - pi/2);sin(tt(i) - pi/2), cos(tt(i) - pi/2)]*[xx; yy];% 繪制小提琴fill(ax, xy(1,:), xy(2,:), mean(tY), 'EdgeColor',[0,0,0], 'LineWidth',1)qt25 = quantile(tY, 0.25);qt75 = quantile(tY, 0.75);med = median(tY);ind3 = find(yi < qt25, 1, 'last');ind4 = find(yi < qt75, 1, 'last');ind5 = find(yi < med, 1, 'last');f3 = interp1(yi(ind3:(ind3 + 1)), f(ind3:(ind3 + 1)), qt25);f4 = interp1(yi(ind4:(ind4 + 1)), f(ind4:(ind4 + 1)), qt75);f5 = interp1(yi(ind5:(ind5 + 1)), f(ind5:(ind5 + 1)), med);xx = [f3, -f3, f4, -f4, f5, -f5]./maxf.*(4*pi/5/size(Data, 2)).*width./2;yy = ([qt25, qt25, qt75, qt75, med, med] - VLim(1))./diff(VLim).*3./5 + 6/5 + 1/10;xy = [cos(tt(i) - pi/2), - sin(tt(i) - pi/2);sin(tt(i) - pi/2), cos(tt(i) - pi/2)]*[xx; yy];% 繪制四分位線和中位線plot(ax, xy(1,1:2), xy(2,1:2), 'LineWidth',1, 'Color','k')plot(ax, xy(1,3:4), xy(2,3:4), 'LineWidth',1, 'Color','k')plot(ax, xy(1,5:6), xy(2,5:6), 'LineWidth',2, 'Color','k')
end% 繪制熱圖
TT = linspace(5*pi/6, pi/6, size(Data, 2) + 1);
for i = 1:size(Data, 1)for j = 1:size(Data, 2)tt = linspace(TT(j), TT(j + 1), 30);r1 = 2/5 + (i - 1)*4/size(Data, 1)/5;r2 = 2/5 + i*4/size(Data, 1)/5;xx = [cos(tt).*r1, cos(tt(end:-1:1)).*r2];yy = [sin(tt).*r1, sin(tt(end:-1:1)).*r2];fill(ax, xx, yy, Data(i,j), 'EdgeColor','w', 'LineWidth',1)end
end% 繪制最上方弧線
tt = linspace(5*pi/6, pi/6, 80);
xx = cos(tt).*2;
yy = sin(tt).*2;
plot(ax, xx(:), yy(:), 'LineWidth',1, 'Color','k')colorbar(ax, 'Position',[.5-.01,.1,.02,.2], fontProp{:});
以上即為完整代碼,數據和完整代碼已經放在以下gitee倉庫:
- https://gitee.com/slandarer/PLTreprint