1

I have a plot with mutliple lines and I want to display the legend below the box (southoutside). The problem is that currently my legend is too long to fit on a single line. Therefore the question is how do I get a line break in my legend?

Currently I generate the legend as follows:

hLegend = legend([l1,l2,l3,l4], 'This is a very, very long legend text', 'Test2', ...
                                'A bit longer', 'This is quite long');
set(hLegend,'Fontsize',8,'Location', 'southoutside', 'Orientation','horizontal');

then this occurs:

enter image description here

As you can see I have four lines (there might come more) and the first one has a very long name.

I want to keep the orientation this way to reduce figure space needed and I want to put an automatic line break if the legend exceeds the picture width (i.e. before l3 or l4, here illustrated by the yellow or purple line).

Any ideas on this? I am using a plot width of 15.75 cm.

Edit

Thanks a lot for the answers so far. Although both of the answers provide some opportunities in splitting the legend into two lines, my main problem still occurs. If assuming now that the plot had more then four lines, lets say 20 and I want to have the legend southside horizontal in a way that it uses the least space, is there a way to split the legend not within one legend text, but after one entry. I generated a new figure generally depicting what I am looking for (its made in Paint so it really just shows the general idea).

enter image description here

Edit 2

The columnlegend package available in the Matlab File Exchange unfortunately does not support legends outside of the figure (at least the options are not specified in the description it only names the following possible locations: 'NorthWest', 'NorthEast', 'SouthEast', 'SouthWest'

Help is appreciated.

KiW
  • 593
  • 3
  • 20
  • See: [`columnlegend`](https://www.mathworks.com/matlabcentral/fileexchange/27389-simonhenin-columnlegend) – sco1 Sep 13 '16 at 14:42
  • @excaza, I want the legend outside of the plot, columnlegend does not provide that option, from the description: possible values: 'NorthWest', 'NorthEast', 'SouthEast', 'SouthWest' – KiW Sep 13 '16 at 20:06
  • The latest version added them. – sco1 Sep 13 '16 at 20:53
  • @excaza, sorry to bother again but I run into problems using columnlegend, how do you format the legend string so that it takes it ? I dont get it to work. Help is still very appreciated – KiW Sep 14 '16 at 18:02
  • Related: https://stackoverflow.com/questions/54095402/matlab-pie-chart-with-2-split-legends-r2017b/54118321#54118321 – EBH Jan 13 '19 at 15:21

2 Answers2

5

Intro:

Here's a proof-of-concept of legend text wrapping, using some undocumented outputs of legend and the MATLAB -> python interface. I will first show the code and then provide a brief explanation of why/how it works.

This is done in MATLAB 2016a.

Code:

function q39456339
%% Definitions:
MAX_LENGTH_IN_CHARS = 20;
OPTION = 2;
%% Plot something:
x = 1:10;
figure('Position',[450 400 800 270]); 
plot(x,x,x,2*x,x,10-x,x,20-2*x);
%% Using python's TextWrapper to wrap entries:
% web(fullfile(docroot, 'matlab/matlab_external/call-python-from-matlab.html'))
switch OPTION
  case 1
    [~,hT] = legend({'This is a very, very long legend text', 'Test2', 'A bit longer', ...
      'This is quite long'},'Location', 'SouthOutside', 'Orientation','Horizontal',...
      'Fontsize',8,'Box','Off');
    texts = hT(arrayfun(@(x)isa(x,'matlab.graphics.primitive.Text'),hT));
    wrapLegendTexts(texts,MAX_LENGTH_IN_CHARS);
  case 2
    hL = legend({'This is a very, very long legend text', 'Test2', 'A bit longer', ...
      'This is quite long'},'Location', 'SouthOutside', 'Orientation','Horizontal',...
      'Fontsize',8,'Interpreter','tex');
    TEX_NEWLINE = '\newline';
    addNewlinesThroughPython(hL, MAX_LENGTH_IN_CHARS, TEX_NEWLINE);
end

end

%% Helper functions:
function wrapLegendTexts(textObjs,maxlen)
  tw = py.textwrap.TextWrapper(pyargs('width', int32(maxlen)));
  for ind1 = 1:numel(textObjs)
    wrapped = cellfun(@char,cell(wrap(tw,textObjs(ind1).String)), 'UniformOutput', false);
    textObjs(ind1).Text.String = reshape(wrapped,[],1);
  end
end

function addNewlinesThroughPython(hLeg, maxlen, newlineStr)
  tw = py.textwrap.TextWrapper(pyargs('width', int32(maxlen)));
  for ind1 = 1:numel(hLeg.PlotChildren)
    hLeg.PlotChildren(ind1).DisplayName = char(...
      py.str(newlineStr).join(wrap(tw,hLeg.PlotChildren(ind1).DisplayName)));
  end
end

Result:

Option 1: Wrapping with option 1, maxlen 20

Option 2: Wrapping with option 2, maxlen 20

Explanation (Option 1):

First, let's look at the signature of legend:

>> dbtype legend 1

1     function [leg,labelhandles,outH,outM] = legend(varargin)

We can see that the 2nd output returns some sort of handles. When we investigate further:

arrayfun(@class, hT, 'UniformOutput', false)

ans = 

    'matlab.graphics.primitive.Text'
    'matlab.graphics.primitive.Text'
    'matlab.graphics.primitive.Text'
    'matlab.graphics.primitive.Text'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'
    'matlab.graphics.primitive.Line'

And:

hT(1)

ans = 

  Text (This is a very, very long legend text) with properties:

                 String: 'This is a very, very long legend text'
               FontSize: 9
             FontWeight: 'normal'
               FontName: 'Helvetica'
                  Color: [0 0 0]
    HorizontalAlignment: 'left'
               Position: [0.0761 0.5128 0]
                  Units: 'data'

  Show all properties

Aha! This is the first legend text entry. We see several interesting properties in the above list (more here), but what we care about is String.

Then it's a question of how to wrap said string. Fortunately, this is exactly the example provided in the MATLAB documentation for using the python interface, so I will not go into any details of that. Here's a link to the docs of python's textwrap. The correct version of the page (selectable by a dropdown on the top left) should correspond to your local python version (see output of pyversion).

The rest of my code is just a wrapper around the python interface, to process all legend entries.

Explanation (Option 2):

Here we don't use any extra outputs of legend, and instead modify hLeg.PlotChildren.DisplayName. This property doesn't accept cell arrays of strings (the way for multi-line strings are usually defined in MATLAB), so we need to insert newline "marks" based on syntax the interpreter recognizes (..or character 10 - the ASCII value of a "newline", as shown in excaza's answer). Finding the correct positions for the line break is still done using python, but this time the strings are joined (with the newline mark in between) instead of being converted to a cell column.

Notes:

  • The 1st option probably provides more control at the expense of some additional required tweaking. One may need to play around with the Texts' Position parameters after wrapping the strings to make the legend look a bit nicer
  • Assigning the 2nd output of legend changes it behavior slightly (you can see it from the overlapping legend entries in the top figure).
Graham
  • 7,431
  • 18
  • 59
  • 84
Dev-iL
  • 23,742
  • 7
  • 57
  • 99
4

For an automated approach that does not require a local Python installation you can specify a maximum character width and use a regular expression to wrap your text strings accordingly.

For example:

function testcode
x = 1:10;
y1 = x;
y2 = 2*x;
y3 = 3*x;
y4 = 4*x;

l = plot(x, y1, x, y2, x, y3, x, y4);
maxwidth = 20; % Maximum character width of each legend string line
ls = {'This is a very very long legend text', 'Test2', 'A bit longer', 'This is quite long'};
ls = cellfun(@(x)wrapstr(x,maxwidth), ls, 'UniformOutput', false);
legend([l(1),l(2),l(3),l(4)], ls, 'Location', 'SouthOutside', 'Orientation', 'horizontal');
end

function [output] = wrapstr(s, width)
% Split input string s into:
%   \S\S{width-1,}: sequences of 1 non-whitespace character followed by 
%                   width-1 or more non-whitespace characters OR
%   .{1, width}: sequences of 1 to width of any character. 
%
%   (?:\\s+|$): Each group is followed by either whitespace or the end of the string
exp = sprintf('(\\S\\S{%u,}|.{1,%u})(?:\\s+|$)', width-1, width);
tmp = regexp(s, exp, 'match');
output = strjoin(deblank(tmp), '\n');
end

Which produces:

yay


The regexp matches Steve Eddin's FEX submission: linewrap

sco1
  • 12,154
  • 5
  • 26
  • 48
  • 2
    The follow up question obviously is, is whether this can be automated with respect to legend width – Adriaan Sep 12 '16 at 19:07
  • @excaza thanks for your answer. I edited my question, I forgot to tell the general orientation is horizontal - would really appreciate some more help. – KiW Sep 12 '16 at 19:36