add data label to a grouped bar chart in matlab - matlab

I'd like to add data to a grouped bar chart in matlab. However, I can't place each data in the top of each bar. Using this question for usual bar and this one, I tried the following code for grouped chart, but xpos and ypos is not correct. Any help is appreciated.
a=[0.92,0.48,0.49];
b=[0.74,0.60,0.30];
c=[0.70,0.30,0.10];
X=[a;b;c];
hbar = bar(X, 'grouped');
for i=1:length(hbar)
XDATA=get(hbar(i),'XData')';
YDATA=get(hbar(i),'YData')';
labels = cellstr(num2str(YDATA));
ygap=0.01;
for j=1:size(XDATA,2)
xpos=XDATA(i,1);
ypos=YDATA(i,1)+ygap;
t=[num2str(YDATA(1,j),3)];text(xpos,ypos,t,'Color','k','HorizontalAlignment','left','Rotation',90)
end
end

There are two main errors in your code:
the definition of the inner loop: XDATA is a (N x 1) array therefore the inner loop makes only one iteration since size(XDATA,2) is 1. This makes your labels added on the center bar of each group
in the innser loop you first set the variable t as the label (t=[num2str(YDATA(1,j),3)];; then you use the same variable as output of the text function (t = text(xpos,ypos,labels{i});; then you use that variable in another call to text but now it contains the handle to the label and no longer the label string. This generate an error.
To proper add the labels you have to modify your code in order to identify the X position of the label.
You have to retreive the position of each bar within the groups: the X position of each bar is given by its XDATA value, plus its offset with respect to the center of the group. The offset value is stored in the XOffset property of the bar (notice: this is an hidden / undocumented property).
This is a possible implementation:
% Generate some data
bar_data=rand(4,4)
% Get the max value of data (used ot det the YLIM)
mx=max(bar_data(:))
% Draw the grouped bars
hbar=bar(bar_data)
% Set the axes YLIM (increaed wrt the max data value to have room for the
% label
ylim([0 mx*1.2])
grid minor
% Get the XDATA
XDATA=get(hbar(1),'XData')';
% Define the vertical offset of the labels
ygap=mx*0.1;
% Loop over the bar's group
for i=1:length(hbar)
% Get the YDATA of the i-th bar in each group
YDATA=get(hbar(i),'YData')';
% Loop over the groups
for j=1:length(XDATA)
% Get the XPOS of the j-th bar
xpos=XDATA(j);
% Get the height of the bar and increment it with the offset
ypos=YDATA(j)+ygap;
% Define the labels
labels=[num2str(YDATA(j),3)];
% Add the labels
t = text(xpos+hbar(i).XOffset,ypos,labels,'Color','k','HorizontalAlignment','center','Rotation',90)
end
end
Hope this helps,
Qapla'

Related

How to set arbitrary colors for bars in a 3D bar plot?

Say that I have a matrix Z with some values, and I want to illustrate it by a plotting the values in Z by height. The first solution comes to mind is a surface, but using surf and similar functions with small matrices doesn't look good.
So I thought about using something like a 3D bar plot with bar3. But the problem is that this function always sets the color by the group and not by height, and I can't get it to do so.
Here is an example:
Z = peaks(5);
subplot 121
surf(Z)
title('Surface look bad')
subplot 122
bar3(Z)
title('The color is not by height')
I tried to look for the color properties in the handles returned by bar3 (like CData and FaceColor) but got lost with all the values and how they relate to the bars themselves.
Ultimately, I would like to have a general solution that for 2 matrices Z and C I can create a 3D bar plot with bars in height given by Z and color given by C.
How can I do so?
The function bar3 returns a surface object, one for each group (i.e. one for each color), so all the bars in one group are essentially plotted as one 'broken' surface. This is explained very good in this answer, so I won't repeat it here.
Instead, I'll get to the solution for this specific problem. The relevant property of the surface is CData. When we create the bar plot, each surface's CData is assigned with a matrix in some size (we'll get to this) that is all equal one value. A different value for each surface. This is how the figure as a whole translates its color map to the color of the groups.
As written above (and elaborated in the linked answer), each group represented by a surface, so it takes a whole matrix to define the color at each point of the surface. The first thing we want to do is to get this matrix size:
Z = peaks(5);
bar_h = bar3(Z);
% we take only the first one, but they are all the same size:
cdata_sz = size(bar_h(1).CData)
cdata_sz =
30 4
CData has always 4 columns (see here why), and the number of rows is always 6*number of groups. This is because it takes 5 vertices to create one closed rectangle with an area object (the last vertex is like the first one) and one line is for spacing between the bars with NaNs, so they will look separated.
Next, we need to enlarge our original colormap (which is the same size of Z) to fit CData in the right way. Essentially, we just want to repeat the same value for all vertices that belong to the same bar. Assuming Z is also our color data (i.e. we color by height) we do:
z_color = repelem(Z,6,4)
Now we need to split our z_color to different cells in the number of our groups. Each cell will contain the coloring data for one surface object:
z_color = mat2cell(z_color,cdata_sz(1),ones(1,size(Z,2))*cdata_sz(2));
And finally, we apply the new color data to the bar plot:
set(bar_h,{'CData'},z_color.')
As a bonus, if we want to remove all zero values from our bar, it can be done easily by setting them to NaN:
Z(abs(Z)<eps) = nan;
C(isnan(Z)) = nan; % if we use a colormap C different from Z
All the above could be boiled down to this handy function:
function bar_h = Cbar3(Z,C,b,y)
% Z - The data
% C - CData (if other then Z values)
% b - Minimum absolute value to keep colored
% y - y-axis values to order the data by
if nargin<2, C = Z; end
if nargin<3 || isempty(b), b = 0; end
Z(abs(Z)<b) = nan;
C(isnan(Z)) = nan;
if nargin<4
bar_h = bar3(Z);
else
bar_h = bar3(y,Z);
end
cdata_sz = size(bar_h(1).CData);
z_color = repelem(C,6,4);
z_color = mat2cell(z_color,...
cdata_sz(1),ones(1,size(Z,2))*cdata_sz(2));
set(bar_h,{'CData'},z_color.')
end
Example of usage:
subplot 121
Z = peaks(30);
Cbar3(Z,Z,0.5);
pbaspect auto
shading flat % just to get a cleaner look
title('Cbar3 using height as color')
subplot 122
Cbar3(Z,rand(size(Z)),0.5);
pbaspect auto
shading flat % just to get a cleaner look
title('Cbar3 using random as color')
Result:
This is a partial answer.
The case of using the bar height as color is covered by the official MATLAB documentation. Essentially the example code boils down to:
function q45423394
hB = bar3(peaks(25)); colorbar;
for indE = 1:numel(hB)
hB(indE).CData = hB(indE).ZData;
end
All you need to do afterwards is make sure that the colormap is the one you want.
While I find EBH's solution aesthetically more pleasing, here there is a simpler solution: interpolation
z = peaks(5);
[x,y]=meshgrid(1:0.1:size(z,1),1:0.1:size(z,2));
zn=interp2(z,x,y,'nearest');
% plot it
surf(zn,'edgecolor','none','facecolor','interp')

Choosing specific line to legend in different matlab runs

I have made a simulation that calculates trajectories of objects and plot it.
The figure looks like this:
figure(1)
plot(ArrayRT1,ArrayRT2);
hold on
plot(ArrayRD1,ArrayRD2);
plot(ArrayRM1,ArrayRM2);
title('Interception Trajectory')
xlabel('Downrange (Kft)')
ylabel('Altitude (Kft) ')
grid on
Where:
plot(ArrayRT1,ArrayRT2) - 1st object trajectory
plot(ArrayRD1,ArrayRD2) - 2nd object trajectory
plot(ArrayRM1,ArrayRM2) - 3rd object trajectory
Now, I run the same simulation without closing the figure with different initial conditions to check how they affect the trajectories so basically after the 2nd run I will have 6 lines on my graph.
How can I choose when I make legend to show legend only for 4 lines:
1,2,3 from first run and the 6th (the 3rd from the 2nd run)
Thank you.
Option 1
Use the syntax legend(subset,___) to set a legend only to specific objects in you axes. This requires getting the handles to all these objects. You can do that by assigning then to an array of handles, as in the following example:
x = 1:10;
% plotting all the lines:
figure(1)
hold on
p(1) = plot(x,2*x);
p(2) = plot(x,3*x);
p(3) = plot(x,4*x);
p(4) = plot(x,2*x+1);
p(5) = plot(x,3*x+1);
p(6) = plot(x,4*x+1);
hold off
% set the legend to a subset of the lines
legend(p([1:3 6]),{'Line 1', 'Line 2','Line 3','Line 6'})
Alternatively, you can 'tag' the lines to whom you want to attach a legend and use findobj to locate their handles, as done in option 2 below.
Option 2
You can set the property DisplayName for your plots to something like "no legend" (or any other string) and then use a loop to turn it off for these specific plots. Here is an example:
x = 1:10;
% plotting all the lines:
figure(1)
hold on
plot(x,2*x,'DisplayName','Line 1');
plot(x,3*x,'DisplayName','Line 2');
plot(x,4*x,'DisplayName','Line 3');
plot(x,2*x+1,'DisplayName','no legend'); % tag for no legend
plot(x,3*x+1,'DisplayName','no legend');% tag for no legend
plot(x,4*x+1,'DisplayName','Line 6');
hold off
% set the legend off for all lines with 'no legend'
set_leg_off = findobj('DisplayName','no legend');
for k = 1:numel(set_leg_off)
set_leg_off(k).Annotation.LegendInformation.IconDisplayStyle = 'off';
end
% show the legend
legend show
Note that:
You don't need to set the DisplayName for all the lines, only for those you want to remove from the legend. However, if you just write legend show it will ignore them when counting the data lines, so if you omit the DisplayName only for line 6, it will give it the label "data1".
You can use other property like tag to mark the non-legend lines (or any other property that will distinguish between the line you want to plot and those you don't), and then if you decide later to show them they won't appear with the label "no legend". Just remember to correct the findobj call to the property you use.
Keep in mind that changing object's tag or DisplayName does not effect the appearance of them in the legend, this is only a way to mark them for the findobj function, so you can loop only on them and turn the legend off. If you want to turn the legend on later, you need to use this loop again.
In both cases, the result is:

Stacked bar from Table in matlab

I want to create a stacked bar chart from a table. Here there is a MWE of the type of table I am looking at:
clear all;
country1=rand(5,1);
country2=rand(5,1);
country3=rand(5,1);
country4=rand(5,1);
country5=rand(5,1);
date=1990:1994;
T=table(date',country1,country2,country3,country4,country5);
T.Properties.VariableNames{1}='date';
T.Total=sum(T{:,2:end},2);
T{:,2:end} = T{:,2:end}./T.Total;
A = table2array(T);
A(:,[1,end])=[];
A=sort(A,2);
TT=array2table(A,'VariableNames',{'country1','country2','country3','country4','country5'});
TT.Date=T.date;
TT.Total=T.Total;
T_new=table(TT.Date, TT.country1,TT.country2,TT.country3,TT.country4,TT.country5,TT.Total);
T_new.Properties.VariableNames=T.Properties.VariableNames;
T_new.World=sum(T{:,2:4},2);
T_new.World=1-(T_new.country4+T_new.country5);
T_new(:,[2:4,end-1])=[];
T_new
date country4 country5 World
____ ________ ________ _______
1990 0.2933 0.29471 0.41199
1991 0.31453 0.34511 0.34035
1992 0.22595 0.29099 0.48307
1993 0.26357 0.33336 0.40306
1994 0.28401 0.28922 0.42677
Type of Stacked BAR
====================
Based on the T_new table I want to create a stacked bar graph. In the 'x' axis the chart should show the dates (1990,1991 etc) and for each date should be one stacked bar. So, for example, for 1990 there is should be one bar stacking the values 0.2933 0.29471 0.41199
Ideally, in the stack bar I want also to include the labels of (country1, country2, world) for the correspending values.
How I can do that in matlab ?
You can do the following:
bar(T_new{:,1},T_new{:,2:end},'stacked')
legend(T_new.Properties.VariableNames(2:end))
The code you've provided contains an erron at line:
T{:,2:end} = T{:,2:end}./T.Total
Error using ./
Matrix dimensions must agree.
Error in stacked_bars (line 14)
T{:,2:end} = T{:,2:end}./T.Total;
since T{:,2:end} is a (5 x 6) matrix and T.Total is a (5 x 1) array
You can fix it replacing that line with, for example:
T{:,2:end}=bsxfun(#rdivide,T{:,2:end},T.Total)
Once fixed the error, an alternative way (with respect to the already posted answer) to plot the labels could be to use the text function to draw a string in each of the stackedbars.
You can identify the x and y coordinate of the point in which to draw the string this way:
x: for each set of bars, is the corresponding date (you need to shift that value a little on the left in order to centre the text with respect to the bar since text uses the x coordinate as starting point
y: for the first label (the lower) could be simply the half of the height of the bar; from the second bar on, you need to add the height of the previous ones
A possible implementation of this approach could be the following:
% Get the T_new data
x=table2array(T_new)
x=x(:,2:end)
% Ientify the number of bars
n_s_bars=size(x,2)
% Open a Figure for the plot
figure(123)
% Plot the stacked bars
bar(T_new{:,1},T_new{:,2:end},'stacked')
% Get the names of the table variables
v_names=T_new.Properties.VariableNames
% Loop over the dates
for i=1:length(date)
% Create the label string:
% country_x (or world)
% percentage
str=sprintf('%s\n%f',v_names{2},x(i,1))
% Print the label in the center of the first bar
tx=text(date(i)-.3,x(i,1)/2,str,'Color',[1 1 1])
% Loop over the bars, starting from the second bar
for j=2:n_s_bars
% Create the label string:
% country_x (or world)
% percentage
str=sprintf('%s\n%f',v_names{j+1},x(i,j))
% Print the label in the center of the first bar
tx=text(date(i)-.3,sum(x(i,1:j-1))+x(i,j)/2,str)
end
end
Over the loops, the following image is generated:
Hope this helps,
Qapla'

how to decrease the legend width in matlab

I am using the matlab to plot some project figures, see the blow figure. Now I am trying to cut the legend width so that the line won't look so wide. I tried these command as suggest by Benoit_11:
[~,icons,~,~] = legend(leg,'location','northwest');
hline = icons(2);
linedata = get(hline,'xdata');
newdata = [linedata(1)+0.2 linedata(2)];
set(hline,'xdata',newdata,'linewidth',1)
I am using the for loop to plot these figures because I have multiple figures to analysis at the same time. Now I can change the length of the legend line right now. But I got another problem: if I have different length of legend text, even if I set the same starting point and end point, I will get different length for the line in the end (you can see that from the figures). I tried to modify icon(1) but always got the error. Any suggestions?
There are 2 things you are not doing right with your code (aside the fact that you use size as the handles to the legend...that's risky because size is a built-in function):
1) Calling legend with only 1 argument returns a handle to the legend object and getting its position actually gives you the position of the box enclosing the legend, i.e. the text + the line.
2) Using this line:
p(3) = p(3) - 0.06;
does modify the position, however you would need to set the new position of the legend with something like the following for the changes to be effective:
set(HandleToLegend,'Position',p)
To come back to your question, the trick is to assign many outputs during the call to legend; you can then modify specific elements of the legend object.
Actually we only need 1 of the 4 output arguments, called icons in the docs so I'll stick with the notation. Then, we can get the XData property of the line and modify it as we want. The XData is actually a 2-element vector:
[StartingPoint EndingPoint]
so changing one or the other (or both) will change the length of the line displayed in the legend box.
Here is the whole code with comments; I changed the length and linewidth of the line in the 2nd plot to highlight the changes.
clear
clc
close all
x = 1:10;
y = rand(1,10);
figure;
%// Default case
subplot(1,2,1)
plot(x,y);
legend('First plot','Location','NorthWest');
title('Before','FontSize',18);
%// With modifications
subplot(1,2,2)
plot(x,y);
title('After','FontSize',18);
%//========================
%// Change the legend here
%//========================
%// The "icons" output is what you want
[~,icons,~,~] = legend('First plot','Location','NorthWest');
%// icons(1) is the text of the current element in the legend Here its 'First plot'
i_1 = get(icons(1)); %// access the properties with this command.
%// icons(2) is the line associated with that text. Here the blue line.
i_2 = get(icons(2));
%// Mhh I don't know what icons(3) represents haha sorry about that.
i_3 = get(icons(3));
%// Get the actual line
hline = icons(2);
%// Fetch its XData property
LineData = get(hline,'XData')
%// Play with those 2 elements to see the output change.
NewData = [LineData(1)+.2 LineData(2)-.01];
%// Apply the changes
set(hline,'XData',NewData,'LineWidth',3)
Which gives the following:
You need to set the value of the Position property, you can just change the vector p. p does not affect the plot at all, it's just a vector of numbers. You have to modify it, then apply it back to the plot, using
set(size,'Position',p)
There does seem to be a minimum width of the legend however.

Horizontal and vertical boxplots in same axes in Matlab

I am trying to plot a set of horizontal and vertical boxplots on the same axes in Matlab R2011b. Using the usual hold on command does not seem to work; only the second set of boxplots is shown. My code is as follows:
bv = boxplot(x,yGrp,'orientation','vertical');
hold on
bh = boxplot(y,xGrp,'orientation','horizontal','position',yPos);
yGrp and xGrp, the grouping index variables are specifically set to be in the range of the y and x datasets, respectively, so that the two plots should naturally have a similar set of values. That is, my x variables spans the range 0-0.05 and my y-variable spans the range 0-1, so yGrp contains a set of categorical numbers between 0 and 1 and xGrp contains bin numbers spanning the range 0-0.05. Similarly, yPos is chosen to span the expected 0-1 range of the vertical axes.
Removing the 'position' argument in the second boxplot call or trying to use simple integer variables does not help. If plotting in separate windows my boxplots look good, but I cannot combine them. What am I doing wrong?
The problem is in axes limits (xlim and ylim). Briefly your 1st boxplots exist but hidden out of axes.
When you plot the second boxplot, it set its own limits ignoring the 1st boxplot. In addition, boxplot by default sets position (y values for horizontal orientation) as 1:number_of_groups, but label them according to your groups. So the ylim will be [0.5 number_of_groups+0.5].
Since your x values are between 0 and 0.05, they are not visible.
As a solution set the limits manually considering this boxplot behavior:
x = rand(10,1)/20;
xg = randi(2,10,1)/40;
y = rand(10,1);
yg = randi(2,10,1)/2;
bv = boxplot(x,xg,'orientation','vertical');
xlim manual
hold on
bh = boxplot(y,yg,'orientation','horizontal');
hold off
xlim([0 2.5])
ylim([0 2.5])
When you set position parameter it determines the values instead of 1:number_of_groups. You can set them close to the range of x.