how to place a legend in best corner?

Is there a way to place legend in the best corner of subplot. I do not like it when the legend is in middle of whitespace.

 Accepted Answer

dpb
dpb on 6 Mar 2019
Edited: dpb on 7 Mar 2019
Give this a try as a first cut...written for a one-line plot as is and takes the legend, axis handles.
Generalized, it could handle simply a figure handle and find the axes and legend from it and retrieve all the X,YData.
But, illustrates the logic idea of above; seemed to work here for a test case where drew the legend on a line and then moved the legend to not clash, but that's the extent of testing.
function flag=legendclash(hAx,hLg,x,y)
% Returns T if data in line x,y cross legend box in axes hAx
AxPosn=hAx.Position; % get axis position vector
Axbot=AxPosn(2); Axtop=Axbot+AxPosn(4); % compute bounding box
Axlft=AxPosn(1); Axrgt=Axlft+AxPosn(3); % dimensions for axis
LgPosn=hLg.Position; % and same for the legend
LGbot=LgPosn(2); LGtop=LGbot+LgPosn(4);
LGlft=LgPosn(1); LGrgt=LGlft+LgPosn(3);
if strfind(hAx.YDir,'normal')
yscaled=interp1(hAx.YLim,[Axbot Axtop],y); % compute scaled plotted values
else
yscaled=interp1(hAx.YLim,[Axtop Axbot],y); % compute scaled plotted values
end
if strfind(hAx.XDir,'normal')
xscaled=interp1(hAx.xlim,[Axlft Axrgt],x); % in the axis bounding box
else
xscaled=interp1(hAx.xlim,[Axrgt Axlft],x); % in the axis bounding box
end
% test if data intersects inside the legend bounding box
flag=any(iswithin(yscaled(:),LGbot,LGtop) & iswithin(xscaled(:),LGlft,LGrgt));
end
iswithin is my oft-used utlity function that is just "syntax sugar" but makes for simpler high-level code...
function flg=iswithin(x,lo,hi)
% returns T for values within range of input
% SYNTAX:
% [log] = iswithin(x,lo,hi)
% returns T for x between lo and hi values, inclusive
flg= (x>=lo) & (x<=hi);
NOTA BENE: ERRATUM
Corrected swapped indices in axis position calculation and inserted missing function keyword
ADDENDUM
Added logic to handle reversed axes

12 Comments

ADDENDUM
One refinement might be to also return a "density of occlusion" figure which could be the ratio of the number of hits in the legend area to the legend area. That would be data by which one might be able to pick a winner in case there were no corners that didn't have at least some hits.
I got this code to work for a single plot in a figure, but I am making 2x2 subplots. It doesn't work and I am assuming positions are not correct for subplots. How do I modify this code to get corresponding positions on subplots?
function flag=legendclash(hAx,hL)
% Returns T if data in line x,y cross legend box in axes hAx
AxPosn=hAx.Position; % get axis position vector
Axbot=AxPosn(1); Axtop=Axbot+AxPosn(3); % compute bounding box
Axlft=AxPosn(2); Axrgt=Axlft+AxPosn(4); % dimensions for axis
LgPosn=hL.Position; % and same for the legend
LGbot=LgPosn(2); LGtop=LGbot+LgPosn(4);
LGlft=LgPosn(1); LGrgt=LGlft+LgPosn(3);
lineData = findobj(hAx,'Type','line');
X = {lineData.XData};
Y = {lineData.YData};
for idx = 1:numel(X)
y = Y{idx};
x = X{idx};
yscaled=interp1(hAx.YLim,[Axbot Axtop],y); % compute scaled plotted values
xscaled=interp1(hAx.XLim,[Axlft Axrgt],x); % in the axis bounding box
% test if data intersects inside the legend bounding box
flag = any(iswithin(yscaled(:),LGbot,LGtop) & iswithin(xscaled(:),LGlft,LGrgt));
if flag == true
break
end
end
function flg=iswithin(x,lo,hi)
% returns T for values within range of input
% SYNTAX:
% [log] = iswithin(x,lo,hi)
% returns T for x between lo and hi values, inclusive
flg= (x>=lo) & (x<=hi);
end
I don't believe there's any difference in a subplot at all, they're just axes, too. Did you pass the proper handles the function expects?
OH! I see I picked out a wrong line from the CommandHistory when I created the function in the Answer window (I just did the testing from command window on the fly then wrote a function after the fact for the Answer)
The position indices in the axes position calculation are swapped -- with the fix in the Answer function looks like it works ok here for subplot() as well for the simple cases I tested.
It would have been pure accident to have worked correctly for a regular axis as well with the swapped indices...
Somewhat minor comment on coding style--in
if flag == true
the "== true" portion is totally redundant; write just
if flag
instead to test a logical.
I can get it to work for "Test Code" but not for "My Code". Is it because I flip and rescale the y-axis? If so, how do I compensate for this...
axis(subplots(sp),'ij');
ylim(subplots(sp),[0,y_lim]);
dpb
dpb on 7 Mar 2019
Edited: dpb on 7 Mar 2019
Well, 'flipping' the axis is a wrinkle, indeed. That probably does something to the position vectors and the data, yes...
I'll see if I can find a few minutes to poke around and see what happens in that case but I don't see anything fundamentally that appears should change the general idea -- you've got to find the area in the axes taken up by the legend in axes coordinates and then see what of the plotted data also falls in that region.
OK, indeed, one needs to swap the top/bottom axes positions in the interpolation to invert the data to match the axes for the position comparison.
I added the patch to the original function in a brute-force fashion; it would be more elegant to just swap the two in a vector rather than duplicate the code, but "any port in a storm" for a quick fix...
...
AxVertical=[Axbot Axtop];
if strfind(hAx.YDir,'reverse'), AxVertical=flip(AxVertical); end
AxHorizontal=[Axlft Axrgt];
if strfind(hAx.XDir,'reverse'), AxHorizontal=flip(AxHorizontal); end
yscaled=interp1(hAx.YLim,AxVertical,y);
xscaled=interp1(hAx.XLim,AxHorizontal,x);
...
meghannmarie
meghannmarie on 7 Mar 2019
Edited: meghannmarie on 7 Mar 2019
That seemed to work on small sample. I will try to incorporate function into my code to place the legend in "best" corner!
dpb
dpb on 7 Mar 2019
Edited: dpb on 7 Mar 2019
BTW, iswithin is vectorized so you can probably get quite a ways along towards vectorizing the whole function if you would cell2mat the XData, YData cell arrays you're creating.
The one kicker is that interp1 isn't vectorized for both X,Y inputs if the multiple lines on a plot aren't the same X. There are other ways to do the linear transformation that don't rely on interp1, it's just an easy one-liner for throwaway code/demonstration.
Unfortunately, I've got too many other pressing deadlines to be able to spend much more time on such niceties, however, at the moment. This might eventually make a pretty nice FEX sybmission but I doubt I'll ever find the time to refine it sufficiently to release it to the wild..
Will be interested to hear how this works for you in the end...interesting problem!
I guess another way to do this would be to transform back to real x,y coordinates the coordinates of the legend box and then could do the comparison in the original data space.
However, you'd still have to deal with the inverted axes becasuse the inversion is done internally to the graphics display routines, not with either the axes limits or the data itself; those remain unchanged.
There might be some hidden properties of some use for that problem, but I've not gone handle-diving to see what might be buried away useful for the purpose.
Just updated my code and ready to run it on big dataset!
lgd = gobjects(4,1);
for sp = 1:length(subplots)
gdem = plot(means{sp},gdem_depths,'-r','Parent',subplots(sp),'LineWidth',2);
lgd(sp) = legend([obs(sp) gdem std],'MOODS','GDEM','95% of time');
count = 1; loc = {'northeast', 'southeast','northwest','southwest','best'};
flag = true;
while flag && count <= numel(loc)
set(lgd(sp),'Location', loc{count});
flag = legendclash(subplots(sp),lgd(sp));
count = count + 1;
end %while flag && count <= numel(loc)
end %for sp = 1:length(subplots)
Let us know how it goes...

Sign in to comment.

More Answers (1)

Try
legend(___,'Location','best')
ML tries to avoid the most data it can if it can find a location that the legend doesn't occlude anything.
It generally is "ok", but may not be what one might choose visually; the logic certainly isn't perfect.
Other than that, it's "pick a spot and go!" in that all other named choices are fixed locations relative to the axes or you set the actual position yourself programmatically (and then the "best" logic reverts to your own devices so you can test your skills against those of TMW :) )..

2 Comments

meghannmarie
meghannmarie on 6 Mar 2019
Edited: meghannmarie on 6 Mar 2019
I am using best right now, but I want the legend in the corner.
What I need to do is place the legend in a fixed corner and check for conficts with data. If there is a conflict, then move to next corner. If conflicts with all four corners, then use 'best'.
I cannot figure out how to check for the conficts with the plotted lines and the legend in the subplots.
I am about to plot 1000+ graphics, so it would be nice to get this to work. Picking a spot manually is out of the question.
I know of no builtin functionality, sorry.
Best I can think of is that you can retrieve the position of the legend where you choose to put it. Those are, by default, normalized dimensions within the axes object.
Then you would have to scale the plotted data to the same set of normalized position values of the axis and check if there is an intersection of values of the x,y normalized data within that area of the legend.
I have meetings in town here shortly so don't have time to try to see if can think of just what such code would look like at the moment, but I think that's the general idea.
I don't know if you can see the internals of the legend function any more or not; seems like TMW has made it almost totally opaque now so you may not be able to go look at what code for collision avoidance is inside it any longer.
Probably at least worth of a look to see if you can find anything on the FEX that somebody else may have already done...

Sign in to comment.

Categories

Find more on Creating, Deleting, and Querying Graphics Objects in Help Center and File Exchange

Products

Release

R2017b

Tags

Asked:

on 6 Mar 2019

Commented:

dpb
on 8 Mar 2019

Community Treasure Hunt

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

Start Hunting!