Code covered by the BSD License  

Highlights from
textbp: text with legend-style "best" placement

image thumbnail
from textbp: text with legend-style "best" placement by Peter Mao
automatically locates text annotation to minimize figure obscuration

textbp(string,varargin)
function ht = textbp(string,varargin)
% TEXTBP  implements 'best' location for text, a la legend
%    TEXTBP uses a modified LSCAN algorithm from the old MATLAB
%    LEGEND command to place text such that it minimizes the
%    obscuration of data points.
%
%    TEXTBP(STRING) is the simplest use of this function.  Any text
%    properties can be passed in by the same methods implemented in
%    the MATLAB TEXT builtin function. ie, following the STRING
%    with (PropertyName,PropertyValue) pairs. 
%
%    HT = TEXTBP(STRING) returns the handle to the text object

TOL = 50; % Max # of data points we are allowed to obscure

% first get the size of the text in plot-normalized units
h_temp = text(0,0,string,'units','normalized',varargin{:});
extent = get(h_temp,'Extent');
width = extent(3);
height = extent(4);
delete(h_temp);

% do the hard work
pos = tscan(gca,width,height,TOL);
% if everything went fine, then put the text onto the plot
if (pos ~= -1)
  ht_local = text(pos(1),pos(2),string,'units','normalized',...
		  'Vert','bottom',varargin{:});
end
% export the text object handle, if requested.
if nargout > 0,
  ht = ht_local;
end

%-------MODIFIED VERSION OF LSCAN FOLLOWS------
function [Pos]=tscan(ha,wdt,hgt,tol,stickytol,hl)
%TSCAN  Scan for good text location.
%   TSCAN is used by TEXTBP to determine a "good" place for
%   the text to appear. TSCAN returns either the
%   position (in figure normalized units) the text should
%   appear, or a -1 if no "good" place is found.
%
%   TSCAN searches for the best place on the graph according
%   to the following rules.
%       1. Text must obscure as few data points as possible.
%          Number of data points the text may cover before plot
%          is "squeezed" can be set with TOL. The default is a 
%          large number so to enable squeezing, TOL must 
%          be set. A negative TOL will force squeezing.
%       2. Regions with neighboring empty space are better.
%       3. Bottom and Left are better than Top and Right.
%      x 4. If a legend already exists and has been manually placed,
%      x    then try to put new legend "close" to old one. 
%
%   TSCAN(HA,WDT,HGT,TOL,STICKYTOL,HL) returns a 2 element
%   position vector. WDT and HGT are the Width and Height of
%   the legend object in figure normalized units. TOL
%   and STICKYTOL are tolerances for covering up data points.
%   HL is the handle of the current legend or -1 if none exist. 
%
%   TSCAN(HA,WDT,HGT,TOL) allows up to TOL data
%   points to be covered when selecting the best
%   text location.
%
%changes from LSCAN
%1. existing text bracketing fixed
%2. sticky references removed for clarity
%3. returns position in plot normalized units, not figure
%normalized (0,0 is LL axis, not LL of figure)

% data point extraction for-loop modified to handle histograms
% properly (Peter Mao, 6/21/11).

% modified from LSCAN by Peter Mao 6/16/06.

%   Drea Thomas     5/7/93
%   Copyright 1984-2005 The MathWorks, Inc.
%   $Revision: 1.1 $  $Date: 2006/06/19 21:08:48 $
%   $Revision: 1.2 $  $Date: 2011/06/21 09:41 $

% Defaults
debug=0;
if debug>1
  holdstatOFF = ~ishold;%%
  hold on;%%
end
% Calculate tile size

% save old units
%% this part makes text walk across screen with repeated use!!
%axoldunits = get(ha,'units');
%set(ha,'units','normalized')
%cap=get(ha,'Position'); %[fig]
%set(ha,'units',axoldunits);
cap = [0 0 1 1]; %'position' in [norm] units

xlim=get(ha,'Xlim'); %[data]
ylim=get(ha,'Ylim'); %[data]
H=ylim(2)-ylim(1);
W=xlim(2)-xlim(1);

dh=.03*H;
dw=.03*W;   % Scale so legend is away from edge of plot
H=.94*H;
W=.94*W;
xlim=xlim+[dw -dw];
ylim=ylim+[dh -dh];

Hgt=hgt/cap(4)*H; %[data]
Wdt=wdt/cap(3)*W;
Thgt=H/round(-.5+H/Hgt);
Twdt=W/round(-.5+W/Wdt);

% Get data, points and text
% legend, not included here, is a child of gcf with 'tag' 'legend'
Kids=get(ha,'children');
Xdata=[];Ydata=[];
for i=1:size(Kids),
  Xtemp = [];
  Ytemp = [];
  if strcmp(get(Kids(i),'type'),'line'),
    Xtemp = get(Kids(i),'Xdata');
    Ytemp = get(Kids(i),'Ydata');
  elseif strcmp(get(Kids(i),'type'),'patch'), % for histograms
    % X/Ydata from patch are LL,UL,UR,LR for each bar.  
    % loop below fills in the edges of the bar with fake data
    Xtemp0 = get(Kids(i),'Xdata');
    Ytemp0 = get(Kids(i),'Ydata');
    Xstart = Xtemp0(1,:);
    Yend   = Ytemp0(2,:);
    for jj=1:length(Xstart)
      thisY = Yend(jj):-Thgt:0;
      thisX = repmat(Xstart(jj),1,length(thisY));
      Xtemp = [Xtemp, thisX];
      Ytemp = [Ytemp, thisY];
    end
  elseif strcmp(get(Kids(i),'type'),'text'),
    tmpunits = get(Kids(i),'units');
    set(Kids(i),'units','data')
    %        tmp=get(Kids(i),'Position');
    ext=get(Kids(i),'Extent');
    set(Kids(i),'units',tmpunits);
    %        Xdata=[Xdata,[tmp(1) tmp(1)+ext(3)]];
    %        Ydata=[Ydata,[tmp(2) tmp(2)+ext(4)]];
    Xtemp = [ext(1) ext(1) ext(1)+ext(3) ext(1)+ext(3)*.5 ext(1)+ext(3)];
    Ytemp = [ext(2) ext(2)+ext(4) ext(2) ext(2)+ext(4)*.5 ext(2)+ext(4)];
  end
  Xdata=[Xdata; Xtemp(:)];
  Ydata=[Ydata; Ytemp(:)];
end
if debug>1, plot(Xdata,Ydata,'r.'); end

%   Determine # of data points under each "tile"

i=1;j=1;
for yp=ylim(1):Thgt/2:(ylim(2)-Thgt),
    i=1;
    for xp=xlim(1):Twdt/2:(xlim(2)-Twdt),
       pop(j,i) = ...
           sum(sum((Xdata >= xp).*(Xdata<=xp+Twdt).*(Ydata>=yp).*(Ydata<=yp+Thgt)));    
%       line([xp xp],[ylim(1) ylim(2)]);
       i=i+1;   
    end
%    line([xlim(1) xlim(2)],[yp yp]);
    j=j+1;
end

% Cover up fewest points.

minpop = min(min(pop));
if debug, disp(sprintf('minimally covering tile convers %d points',minpop)); end
if minpop > tol,
    Pos=-1;
    warning('Raise TOL in calling function to %d',minpop);
    return
end

%%%%%%%%%%%%%%%%%%%%%%
%                    %
%sticky stuff removed%
%                    %
%%%%%%%%%%%%%%%%%%%%%%

popmin = pop == min(min(pop));

if sum(sum(popmin))>1,     % Multiple minima in # of points

  [a,b]=size(pop);
  if min(a,b)>1, %check over all tiles
    
    % Look at adjacent tiles and see if they are empty
    % adds in h/v nearest neighbors, double add if on an edge
    pop=[pop(2,:)',pop(1:(a-1),:)']'+[pop(2:(a),:)',pop((a-1),:)']'+...
	[pop(:,2),pop(:,1:(b-1))]+[pop(:,2:b),pop(:,(b-1))] + pop;
    % LSCAN had two calls to the line above w/o the trailing "+ pop"
    popx=popmin.*(pop==min(pop(popmin)));
    if sum(sum(popx))>1, % prefer bottom left to top right
      flag=1;i=1;j=1;
      while flag,
	if flag == 2,
	  if popx(i,j) == 1,
	    popx=popx*0;popx(i,j)=1;
	    flag = 0;
	  else
	    popx=popx*0;popx(i,j+1)=1;
	    flag = 0;
	  end
	else
	  if popx(i,j)==1,
	    flag = 2;
	    popx=popx*0;popx(i,j)=1;
	  else
	    j=j+1;
	    if j==b+1,
	      j=1;i=i+1;
	    end
	    if i==a+1, % my add'n
	      i=1;
	      flag=2;
	    end
	  end
	end
      end
    end
  else % only one tile
    popx=popmin*0;popx(1,1)=1;
  end
else   % Only 1 minima in # covered points
  popx=popmin;
end

   %recover i,j location that we want to use
   i=find(max(popx));i=i(1); 
   j=find(max(popx'));j=j(1);

Pos=[((i-1)/(W/Twdt*2/.94)+.03)*cap(3)+cap(1),((j-1)/(H/Thgt*2/.94)+.03)*cap(4)+cap(2)];

if debug, disp(sprintf('(i,j) = (%d,%d)',i,j)); end
if debug>1
  if holdstatOFF
    hold off
  end
end

Contact us