How to draw the below type of graph in MATLAB?

I'm trying to draw a graph of the form below.
But I can't figure out how to draw it.
Attached data here.
I want to hear from the experts.
Thank you.

3 comentarios

dpb
dpb el 10 de Sept. de 2019
Nice plot design -- unfortunately, MATLAB doesn't have builtin solution. Best idea I have otomh would be to create 12 axes for each independent month and plot with barh() then add yet another overall to plot the overall monthly average.
How about attaching a .mat file with the data so folks can play around with it instead of having to create their own? Might get a few takers-on for the challenge...
Changho LEE
Changho LEE el 10 de Sept. de 2019
Editada: Changho LEE el 10 de Sept. de 2019
The data file is attached here.
Changho LEE
Changho LEE el 11 de Sept. de 2019
The advice made good results.
Thank you.

Iniciar sesión para comentar.

 Respuesta aceptada

Cris LaPierre
Cris LaPierre el 10 de Sept. de 2019
I didn't worry about importing your data specifically since it appears to be a summary and not the raw data anyway.
% import bar plot data from spreadsheet
opts = detectImportOptions('data.xlsx','Range',[1 1 18 18]);
data = readtable('data.xlsx',opts);
% import line plot data
ave = readmatrix('data.xlsx',"Range","B20");
% Create x vector of 13 months to give room for Dec bar plot
d = datetime(2019,1,1,'Format',"MMM") + calmonths(0:12);
% Plot ave data
plot(d(1:end-1),ave,'k--','LineWidth',1.5)
ylim([0 16])
% Plot 13th month separately to give room for Dec bar plot
hold on
plot(d(end),0)
hold off
% Turn off x-label for Jan 2020
ax = gca;
ax.XTickLabel{13} = "";
% Use position of current plot to determine placement of bar plots
p = get(gca,'position'); % [left bottom width height]
dx = [0 p(3).*days(diff(d))/365]; % width of each month
% Create bin data for bar plots
edges = 1:16;
% plot bar plots by positioning a new axes on top of the current axis
for i = 1:length(d)-1
a = axes('Position',[p(1)+sum(dx(1:i)) p(2) .75*dx(i+1) p(4)]);
b = barh(a,edges,data{:,i+1});
axis off
end
Not perfect, but it should be able to get your started.
ChanghoLee_combinePlots.png

8 comentarios

Adam Danz
Adam Danz el 10 de Sept. de 2019
Hi Cris, I see the y axis lim for the large plot is [0 16] but the bin edges for the bar charts are [1,16]. Is the y axis limit for the subplots the same as the [0,16]? Also, the xlim for each subplot differs which means the bar heights aren't comparable between subplots.
Cris LaPierre
Cris LaPierre el 11 de Sept. de 2019
Editada: Cris LaPierre el 11 de Sept. de 2019
The y axis on the bar plots still starts at 0 even though the edges start at 1, but it's a valid critique. Best to ensure X and Y lims are consistant. That can be accomplished by adding an explicit xlim and ylim when creating the bar plots.
I hardcoded the limits using the values contained in the data and looking at the original image. This was just a quick attempt. They can be replaced with code by someone with more interest in creating a dynamic solution.
Also noticed that barh centers the bar around the x value while the original places the bar between the edges. That can be fixed by adding an offset of 0.5 when plotting. I've left the axis on here to show that x is the same in all bar plots, and y aligns with y axis of line plot for all.
That doesn't look particularly good, so here it is w/o the axes.
I noticed the original also had the line in front of the bar plots. It's possible to do that as well (use uistack).
Here's the updated code:
% import bar plot data from spreadsheet
opts = detectImportOptions('ChanghoLEE_data.xlsx','Range',[1 1 18 18]);
data = readtable('ChanghoLEE_data.xlsx',opts);
% import line plot data
ave = readmatrix('ChanghoLEE_data.xlsx',"Range","B20");
% Create x vector of 13 months to give room for Dec bar plot
d = datetime(2019,1,1,'Format',"MMM") + calmonths(0:12);
% Plot ave data
plot(d(1:end-1),ave,'k--','LineWidth',1.5)
ylim([0 16])
% Plot 13th month separately to give room for Dec bar plot
hold on
plot(d(end),0)
hold off
% Turn off x-label for Jan 2020
ax = gca;
ax.XTickLabel{13} = "";
% Use position of current plot to determine placement of bar plots
p = get(gca,'position'); % [left bottom width height]
dx = [0 p(3).*days(diff(d))/365]; % width of each month
% Create bin data for bar plots
edges = 1:16;
% plot bar plots by positioning a new axes on top of the current axis
for i = 1:length(d)-1
a = axes('Position',[p(1)+sum(dx(1:i)) p(2) .75*dx(i+1) p(4)]);
barh(a,edges+.5,data{:,i+1});
ylim([0 16])
xlim([0 14])
axis off
end
% Bring line in front of bar plots
uistack(ax,'top')
set(ax,'color','none')
Adam Danz
Adam Danz el 11 de Sept. de 2019
Nice. I don't think I've used barh before. Thanks for the info.
dpb
dpb el 11 de Sept. de 2019
Agreed....nice fixup for determining the axes positions. I've often had troubles generalizing those; the "divide by" trick is good--logical, but somehow I've missed the boat several times before I'm thinking now.
Cris LaPierre
Cris LaPierre el 11 de Sept. de 2019
Editada: Cris LaPierre el 11 de Sept. de 2019
You could do something similar with histogram with some slight modifications.
  1. Set the right edge to inf for the '16-' group
  2. set all NaN counts to 0
  3. include a 0 edge and corresponding 0 count so the histogram has a 0 reference line from y=0:16.
% Create bin data for bar plots
edges = [1:16 inf];
% plot bar plots by positioning a new axes on top of the current axis
for i = 1:length(d)-1
a = axes('Position',[p(1)+sum(dx(1:i)) p(2) .75*dx(i+1) p(4)]);
counts = data{:,i+1};
counts(isnan(counts))=0;
histogram(a,'BinEdges',[0 edges],"BinCounts",[0; counts],'Orientation',"horizontal");
ylim([0 16])
xlim([0 14])
axis off
end
Adam Danz
Adam Danz el 11 de Sept. de 2019
Editada: Adam Danz el 11 de Sept. de 2019
Yeah, that was my approach in my answer except that I added 1 instead of inf to the final bin edge since the other bins were in increments of 1. But inf is a better idea in case there are outlers.
The other big difference between our answers it that your bar axes are 'off' and on top of the summary axis whereas my histogram axes are plotted first and then the summary axis is on top so that there is only 1 invisible axis.
Cris LaPierre
Cris LaPierre el 11 de Sept. de 2019
Good point. I thought about doing it that way, but went with the summary axis first so that it could set where I needed to place the histograms. Had to hunt for the uistack fxn because of that. I'd never used it before.
Changho LEE
Changho LEE el 11 de Sept. de 2019
This graph is part of my teacher's research in 1975.
His research was so beautiful that I wanted to make it MATLAB and use it for other research.
You made it so perfect and beautiful.
It looks really elegant.
This is a wonderful thing I never thought of.
Thank you very much for creating such a wonderful work of art.
Thank you very much.

Iniciar sesión para comentar.

Más respuestas (1)

Adam Danz
Adam Danz el 10 de Sept. de 2019
Editada: Adam Danz el 11 de Sept. de 2019
"Feb" in your excel file has a $ character which needs fixed. If you have many excel sheets, you can fix it from within the code but I assumed it's fixed prior to running this.
The first section loads the data and prepares it for histogram().
The 2nd section defines the plot layout. You can change the margin size etc.
The 3rd section does the plotting and saves each axis handle.
The 4th section cleans up the axes and does all of the cosmetics.
The 5th section overlays an additonal axis at the same scale as the underlying subplots (as dpb suggested) and plots the mean curve.
See comments for details.
data = readtable('data.xlsx');
edgesStr = data{:,1}; %save edges to an array
edges = str2double(cellfun(@(x)regexp(x,'^\d+','match'),edgesStr(1:end-2))); %edges, numeric
data(:,1) = []; %remove edges from data table
data = fillmissing(data,'constant', 0); %replace NaNs with 0
% Determine subplot dimensions (all in normalized units)
nSubplots = size(data,2); %number of subplots needed
LRmargins = [.1,.08]; %left and right figure margins
TBmargins = [.1, .1]; %top and bottom figure margins
subplotWidth = (1-sum(LRmargins))/nSubplots; %width of each subplot
subplotHeight = 1-sum(TBmargins); %height of each subplot
subplotPos = linspace(LRmargins(1),1-LRmargins(2)-subplotWidth, nSubplots); % lateral position of each subplot
% Loop through each subplot, create subplot and plot the data
figure();
binEdges = [edges(:).',edges(end)+1];
sph = gobjects(1,nSubplots); %store subplot handles
for i = 1:nSubplots
sph(i) = axes('Units', 'Normalize', 'Position', [subplotPos(i),TBmargins(1),subplotWidth,subplotHeight]);
histogram(sph(i),'BinEdges', binEdges, 'BinCounts', data{1:17,i},'Orientation','Horizontal');
end
% Clean up
maxBarHeight = max(data{1:17,:},[],'all'); %max bar height
% remove the x axis ticks for all subplots
set(sph,'XTick',[])
% Set the y tick for the left most subplot as the bin lables
set(sph,'YTick', binEdges(1:end-1))
set(sph(2:end), 'YTickLabel',[])
% Make sure all axes have the same ylim
set(sph,'YLim',binEdges([1,end-1]))
% Make sure all axes have same scale
set(sph,'XLim',[0,maxBarHeight])
% Set the month labels along the horizontal, bottom axis
arrayfun(@(x,y)set(x.XLabel,'String',y),sph,data.Properties.VariableNames);
% Label bins
ylabel(sph(1),'Days')
% overlay summary axis
sph2 = axes('Position',[subplotPos(1),TBmargins(1),1-sum(LRmargins),subplotHeight]);
% plot mean curve
plot(data{end,:}, 'r-s','Linewidth',1.5)
% Scale the axis to match the range of subplots
xlim([.5,12.5])
ylim(ylim(sph(1)))
% Turn off axis
axis(sph2, 'off')
190910 235121-Figure 1.png

9 comentarios

Adam Danz
Adam Danz el 10 de Sept. de 2019
Each subplot has an x axis limit from [0, maxBarHeight] in case you need to interpret the 'height' of each bar.
dpb
dpb el 10 de Sept. de 2019
Looks just pretty good, Adam. I see you used histogram which can't handle categorical data as I was thinking to use with barh. But, it will take datetime; would that save some labeling machinations to use actual months, maybe? Now I've closed the doc, not sure if takes a duration or not...just a "for curious".
Adam Danz
Adam Danz el 10 de Sept. de 2019
Editada: Adam Danz el 10 de Sept. de 2019
Thanks dpb,
When binCounts is specified, doesn't histogram() act as a bar chart?
I only used the months in the xlabel() for the subplots so I just pulled them from the headers in the excel file. No need for datetime or duration inputs to histogram.
Cris LaPierre
Cris LaPierre el 11 de Sept. de 2019
Hey Adam. I'm just noticing your histograms only contain data between 3 and 7. I thought maybe you'd only loaded the first few rows of data, but it doesn't appear to be the case. Was that intentional?
Changho LEE
Changho LEE el 11 de Sept. de 2019
It was a nice graph.
Your idea has made better results.
Thank you.
Adam Danz
Adam Danz el 11 de Sept. de 2019
Editada: Adam Danz el 11 de Sept. de 2019
@Cris, Good catch!
I was incorrectly indexing a table using row and column indices. This resulted in eliminating some of the data.
% Replaced this
[nanRow, nanCol] = find(isnan(data{:,:})); %find NaNs in data
data{nanRow,nanCol} = 0; % replace NaNs with 0 so histrogram() doesn't throw error
% with this
data = fillmissing(data,'constant', 0); %replace NaNs with 0
I see what it is. It's this line of code:
data{nanRow,nanCol} = 0;
This is interpreted as 'access the specified rows for each listed column/variable'. Rows 3-7 are the only rows that never have a NaN, so are never specified in nanRow and therefore don't get changed to 0.
You get the same result doing this:
data{unique(nanRow),unique(nanCol)}=0
or for that matter,
data{unique(nanRow),:}=0
For what it's worth, this wouldn't work for an array either. You can read more here. You would need to use linear indexing instead, which is not supported for tables.
There are a bunch of ways around this. Here are some options:
1. Use a for loop to change NaN to 0 for each NaN
for i = 1:length(nanRow)
data{nanRow(i),nanCol(i)} = 0 % replace NaNs with 0 so histrogram() doesn't throw error
end
2. Chanage NaN to 0 for each variable
for i = 1:width(data)
data{isnan(data{:,i}),i} = 0 % replace NaNs with 0 so histrogram() doesn't throw error
end
3. Extract to an array, use linear indexing, then assign back to table.
d=data{:,:};
d(isnan(d(:)))=0;
data{:,:}=d
Adam Danz
Adam Danz el 11 de Sept. de 2019
Editada: Adam Danz el 11 de Sept. de 2019
Yeah, I would have rather used your last option which should be implemented for tables too.
But instead I used option 4, fillmissing, which is a one-liner that does work with tables.
Cris LaPierre
Cris LaPierre el 11 de Sept. de 2019
Ah, much better :)

Iniciar sesión para comentar.

Categorías

Más información sobre Creating, Deleting, and Querying Graphics Objects en Centro de ayuda y File Exchange.

Productos

Versión

R2019a

Preguntada:

el 10 de Sept. de 2019

Comentada:

el 11 de Sept. de 2019

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by