Code covered by the BSD License  

Highlights from
X-Axis Brackets

image thumbnail

X-Axis Brackets

by

 

03 Jul 2013 (Updated )

Draws a set of brackets below the x-axis to group data.

xAxisBrackets(locations, labels, ax, sortFlag)
function [hLeft, hBottom, hRight, varargout] = xAxisBrackets(locations, labels, ax, sortFlag)
%xAxisBrackets draws a set of brackets below the x-axis.
%   xAxisBrackets(LOCATIONS) draws a set of brackets below the current axis.
%   The locations of the brackets are specified in LOCATIONS, a vector with the
%   format [xStart1, xEnd1, xStart2, xEnd2,....]. Each bracket is drawn
%   from xStart to xEnd.
%
%   xAxisBrackets(LOCATIONS,LABELS) labels each bracket. LABELS is a cell
%   array half as long as the vector LOCATIONS so that each bracket has one
%   label. 
%
%   xAxisBrackets(LOCATIONS,LABELS,AX) draws the brackets on the axis
%   specified by AX instead of the current axis.
%
%   xAxisBrackets(LOCATIONS,LABELS,AX,'ordered') plots brackets in the
%   order they are listed in LOCATIONS instead of trying to optimize the
%   positions. Brackets are still arranged in tiers so that they do not
%   overlap.
%
%   [hLeft, hBottom, hRight, hLabels] = xAxisBrackets(LOCATIONS) returns
%   vectors of handles to the left, bottom, and right lines in the bracket
%   and to the text of the label if there is one.
%
%   Copyright (c) Molly J. Rossow <molly@mollyrossow.com> 7/2/2012
%
%   Examples:
%   figure();
%   plot(-6:-1:-20);
%   locations = [6,8,1,2,8,14,8,15,4,5];
%   labels = {'one','two','three','four','five'};
%   xAxisBrackets(locations,labels,gca);
%
%   figure();
%   plot(-6:-1:-20);
%   locations = [6,8,1,2,8,14,8,15,4,5];
%   labels = {'one','two','three','four','five'};
%   [hLeft, hBottom, hRight] = xAxisBrackets(locations,'',[],'ordered');
%   set(hBottom,'Color','m')



% set default input arguments
switch nargin
    case 1
        labels = '';
        ax = gca;
        sortFlag = '';
    case 2
        ax = gca;
        sortFlag = '';
    case 3
        sortFlag = '';
end

%check if ax exists
if isempty(ax)
    ax = gca;
end
% check if y axis is reversed
if strcmp(get(ax,'ydir'),'reverse')
    error(['Brackets cannot be drawn on plots where the y axis direction'...
        'is reversed. Please use set(ax,''ydir'',''normal'') or replot your'...
        'data.']);
end


% sort the positions
if ~strcmp(sortFlag,'ordered')
    [sortedLocations, labels] = arrangePositions(locations,true,labels);
else
    [sortedLocations, labels] = arrangePositions(locations,false,labels);
end

sPos = size(sortedLocations);
nTiers = sPos(1); % number of tiers of labels

% get axis postion
posAx = get(ax,'Position');
outPosAx = get(ax, 'OuterPosition');
difPosAx = posAx - outPosAx;

% get axis x and y limits
axLimits = axis(ax);
yMin = axLimits(3);

% Get font size
set(ax,'FontUnits','normalized')
sizeFont = get(ax,'FontSize');
set(ax,'FontUnits','points')

% Some constants that look nice
posVerticalShiftPerTier = 4*sizeFont;

% calculate new location of the axis to make room for the brackets.
% location of axes
% verticalOffset = nTiers * posVerticalShiftPerTier + xLabelOffset + xTickOffset;
verticalOffset = nTiers * posVerticalShiftPerTier + difPosAx(2);
xAxPos = posAx(1);
widthAx = posAx(3);
heightAx = posAx(4) - verticalOffset;
yAxPos = posAx(2) +  verticalOffset;

%check if the new axis height is less than 0
if heightAx < 0
    heightAx = 0.01;
end

% resize axes to make room for the brackets.
set(ax,'Position',[xAxPos, yAxPos, widthAx, heightAx]);

% restore the axis limits
axis(ax,axLimits);

% draw each bracket and add the label
labelIndex = 1;
height = figureyDist2AxyDist(posVerticalShiftPerTier,ax)/2; % height of each bracket
% initialize handles vectors
nBrackets = length(locations)/2;
hLeft = zeros(1,nBrackets);
hBottom = zeros(1,nBrackets);
hRight = zeros(1,nBrackets);
hText = zeros(1,nBrackets);

for tier = 1:nTiers % for each tier of brackets
    depth =  yMin - figureyDist2AxyDist(difPosAx(2),ax)/2-2*tier*height; % find the y position of the bracket
    for k = 1:length(sortedLocations{tier}) % for each pair of location in that bracket.
        pos = sortedLocations{tier}{k};
        if isempty(labels) % draw brackets without labels
            [hLeft(labelIndex),hBottom(labelIndex),hRight(labelIndex)] ...
                = drawBracket(pos(1),pos(2),depth,height,ax);
        else % draw brackets with labels
            [hLeft(labelIndex),hBottom(labelIndex),hRight(labelIndex),...
                hText(labelIndex)] = drawBracket(pos(1),pos(2)...
                ,depth,height,ax,labels(labelIndex));
        end
        labelIndex = labelIndex + 1;
    end
end

% return label handles if there are labels
if ~isempty(labels)
    varargout{1} = hText;
end

end


function  [positionArray, sortedLabels] = arrangePositions(positions,s,labels)
%arrangePositionsSorted arranges bracket positions into tiers for display
%with xAxisBrackets
%    [POSITIONARRAY] = arrangePositions(POSITIONS) arranges the positions in
%    the vector positions with the format [xStart1, xEnd1, xStart2,
%    xEnd2,....]. POSITIONARRAY is a cell array. Each row of POSITIONARRAY
%    corresponds to a tier of brackets in the final plot. Each row contains
%    another cell array of vectors, each vector is of the form [xStart1,
%    xEnd1].
%
%    [POSITIONARRAY] = arrangePositions(POSITIONS,S) sorts the position in
%    POSITIONS before arranging them into tiers if S true.
%
%    [POSITIONARRAY, S, SORTEDLABELS] = arrangePositions(POSITIONS,LABELS)
%    sorts the strings in the cell array labels. sortPositions also returns
%    SORTEDLABELS, a cell array of string, the bracket labels

% deal with optional inputs
switch nargin
    case 1
        s = true;
        labels = '';
        sortedLabels = '';
    case 2
        labels = '';
        sortedLabels = '';
    case 3
        % check if the number of labels corresponds to the number of
        % locations. (if there are labels)
        nBrackets = length(positions)/2;
        if ~isempty(labels)
            if (length(labels) ~= nBrackets)
                error(['The number of labels does not match the number of ' ...
                    'locations. Please provide ', num2str(nBrackets),' labels'])
            end
        end
end

%check if there are an even number of locations
lenInPos = length(positions);
if mod(lenInPos,2) ~= 0
    error('The length of locations is not even');
end

%convert to two column matrix
posPairs = [positions(1:2:end);positions(2:2:end)]';

%order Xstart and Xstop so the smallest value is first
posPairs = [min(posPairs,[],2),max(posPairs,[],2)];

% sort bracket pairs in order of length of bracket
if s
    dif = abs(posPairs(:,1) - posPairs(:,2)); %find distance between x and y
    [sortedVec, index] = sortrows([dif, posPairs]); % sort based on distance
    sortedVec = sortedVec(:,2:end); % remove distance
    if ~isempty(labels)
        sortedLabels = labels(index);
    else
        sortedLabels = '';
    end
else
    sortedVec = posPairs;
    sortedLabels = labels;
end

% create cell array of bracket pairs.
positionArray = {};
s = size(sortedVec);
count = 1;
posPairsIndex = 1;
currentRow = {};
coordPair = sortedVec(posPairsIndex,:);
% if there is only one bracket
if s(1) == 1
    positionArray{1}{1} = sortedVec;
end
while count < s(1) % loop until all pairs of coordinates have been assigned
    currentRow = {};
    % next position pair from input
    while and(~isOverlap(currentRow,coordPair), (count <s(1)))
        currentRow{end + 1} = coordPair;
        posPairsIndex = posPairsIndex + 1;
        coordPair = sortedVec(posPairsIndex,:);
        count = count + 1;
    end
    %posPairsIndex = posPairsIndex + 1;
    positionArray{end + 1,1} = currentRow;
end

% add last pair
if ~isempty(currentRow)
    if ~isOverlap(currentRow, coordPair)
        currentRow{end + 1} = coordPair;
        positionArray{end,1} = currentRow;
    else
        
        positionArray{end,1} = currentRow;
        currentRow = {};
        currentRow{end + 1} = coordPair;
        positionArray{end + 1,1} = currentRow;
    end
end
end

function [hLeft, hBottom, hRight, hText] = drawBracket(x1,x2,y,height,varargin)
%drawBracket draws an individual bracket. 
%   drawBracket(X1,X2,Y,HEIGHT) draws a bracket on the current axis from
%   (X1,Y) to (X2,Y) with a height of HEIGHT. X1, X2, Y, and HEIGHT are in
%   data units. 
%
%   drawBracket(X1,X2,Y,HEIGHT,AX) draws a bracket on axis AX instead of the 
%   current axis.
%
%   drawBracket(X1,X2,Y,HEIGHT,AX,LABLE) adds a text label to the bracket.
% 
%   drawBracket returns handles to the left, bottom, and
%   right lines in the bracket and to the text of the label if there is 
%   one. 

% set default input arguments
ax = gca;
label = '';
if nargin >= 5
    if ~isempty(varargin{1})
        ax = varargin{1};
    end
    
end
if nargin == 6
    if ~isempty(varargin{2})
        label = char(varargin{2});
    end
end
    
axes(ax)
% get axes limits so they can be restored later
xLim = get(ax,'XLim');
yLim = get(ax,'YLim');

% find y coordinates for verticle lines
y1 = min([y, y + height]);
y2 = max([y, y + height]);

% draw lines
% the order here is important for the resize function
hLeft = line([x1,x1],[y1,y2]);
hBottom = line([x1,x2],[y1,y1]);
hRight = line([x2,x2],[y1,y2]);

% makes lines outside of axes box visible
set(hLeft,'Clipping','off')
set(hBottom,'Clipping','off')
set(hRight,'Clipping','off')
set(gca,'XLim',xLim)
set(gca,'YLim',yLim)

% add label
if ~isempty(label)
    hText = text(0,0,label); %make temporary label
    set(hText,'HorizontalAlignment','center')
    fontS = get(ax,'FontSize');
    set(hText,'FontSize',fontS)
    ext = get(hText,'Extent');
    % find x and y coordinates of label
    textX = (((x2-x1) / 2) + x1);
    textY = y1 - ext(4)/2;
    set(hText,'Position',[textX, textY]) % move label
end

%restore axis limits
set(gca,'XLim',xLim)
set(gca,'YLim',yLim)

end

function dyAx = figureyDist2AxyDist(dyFig,ax)
%figureyDist2AxyDist converts a vertical distance in normalized units to
%data units.
%    DYAX = figureyDist2AxyDist(DYFIG,AX) is the equivalent in data units
%    to DYFIG on axis AX.


% get y axis limits
yVec = get(ax,'YLim');
yMin = yVec(1);
yMax = yVec(2) ;

% get get the axis height in figure units.
set(ax,'Units','Normalized');
vecPos = get(ax,'Position');
heightAx = vecPos(4);

% convert units.
yScale =  heightAx / (yMax - yMin);
dyAx = dyFig / yScale;
end

function out  = isOverlap(allRows,row)
%isOverlap determines if a vector overlaps any of an array of vectors
%    OUT  = isOverlap(ALLROWS,ROW) ROW is a two element vector, ALLROWS is
%    a cell array of two element vectors.  isOverlap returns true if the
%    range defined by ROW overlaps the range of any of the vectors in
%    ALLROWS. 

    out = false;
     
     for  r = 1:length(allRows)
         vec = allRows{r};
         % if the first value of row is in the range vec
         if and((row(1) >= vec(1)), row(1) <= vec(2)) 
             out = true;
         % if the second value of row is in the range vec
         elseif  and((row(2) >= vec(1)), row(2) <= vec(2))
             out = true;
         %if the firs value of row is less than the range of vec and the
         %second valueo of row is greater than the range of vec.
         elseif and(row(1) <= vec(1), row(2) >= vec(2)) 
             out = true;
         end
     end
end



Contact us