Search code examples
matlabmatlab-figure

Legend only affects to one of the linked axes


The following MVCE reproduces a problem that I am experiencing when displaying a legend in a figure that has several axes objects.

In order to plot an elevation profile of the terrain I use two axes:

  • ax(1) for the sky in the background
  • ax(2) for the terrain in the foreground

Vertical profile, legend

During the plotting tasks I work with each set of axes independently and at the end of the process I link ax(1) and ax(2) using linkaxes(ax, 'xy') so that they remain synchronized dimensionally.

Finally, I add a legend in the 'southoutside' location but only the foreground axes are automatically shrinked to allow space for the legend, the background axes stay unchanged, thus interfering with the intended layout, as shown in the following image:

Vertical profile, legend, bad axes

The very purpose of using linkaxes was to prevent this undesired behavior.

I would be grateful if someone could shed some light on how to solve this problem.


Update

In response to gnovice comment:

The reason why I am using two axes is because I thought it was the only possible way to combine different colormaps in the same figure. Note that for the sky I use sky_map and for the terrain I use the demcmap function which sets a different colormap.


Code

% Create figure.
figure;

% Create background axes for the sky.
ax(1) = axes;

% Sky data for background.
x_bg = [0, 0, 10, 10];
y_bg = [0, 10, 10, 0];

% Fill sky with color gradient on y-axis.
fill(x_bg, y_bg, y_bg);

% Generate custom sky colormap.
sky_map = interp1([0, 1], [135, 206, 235; 255, 255, 255]./255, linspace(0, 1, 255));

% Apply sky colormap.
colormap(ax(1), sky_map);

% Create foreground axes for the terrain.
ax(2) = axes;

% Terrain data.
x = [0, 0:10, 10];
y = [0, 5*rand(size(0:10)), 0];

% Fill terrain.
fill(x, y, y);

% Sets the colormap and color axis limits based on the elevation data
% limits.
demcmap(y);

% Link x-axis and y-axis of ax(1) and ax(2).
linkaxes(ax, 'xy');

% Hide ax(1).
set(ax(1), 'Visible', 'off');

% Transparent background for ax(2).
set(ax(2), 'Color', 'none');

% Display title.
title('Vertical Profile');

% Display legend underneath.
legend('Terrain', 'Location', 'southoutside');

Solution

  • Method 1: Using two axes and linkprop.

    Quoting excaza's comment:

    linkaxes only syncs axes limits. You'll need to sync properties (like Position) with linkprop

    Add linkprop(ax, 'Position'); before calling legend.

    Result:

    Method 1

    As a side note, if you wish to learn more, I suggest reading "Using linkaxes vs. linkprop" from Undocumented Matlab, specially to solve the problem of linkaxes being overriden by each other.


    Code:

    % Create figure.
    figure;
    
    % Create background axes for the sky.
    ax(1) = axes;
    
    % Sky data for background.
    x_bg = [0, 0, 10, 10];
    y_bg = [0, 10, 10, 0];
    
    % Fill sky with color gradient on y-axis.
    fill(x_bg, y_bg, y_bg);
    
    % Generate custom sky colormap.
    sky_map = interp1([0, 1], [135, 206, 235; 255, 255, 255]./255, linspace(0, 1, 255));
    
    % Apply sky colormap.
    colormap(ax(1), sky_map);
    
    % Create foreground axes for the terrain.
    ax(2) = axes;
    
    % Terrain data.
    x = [0, 0:10, 10];
    y = [0, 5*rand(size(0:10)), 0];
    
    % Fill terrain.
    fill(x, y, y);
    
    % Sets the colormap and color axis limits based on the elevation data
    % limits.
    demcmap(y);
    
    % Link x-axis and y-axis limits of ax(1) and ax(2).
    linkaxes(ax, 'xy');
    
    % Link Position property of ax(1) and ax(2).
    linkprop(ax, 'Position');
    
    % Hide ax(1).
    set(ax(1), 'Visible', 'off');
    
    % Transparent background for ax(2).
    set(ax(2), 'Color', 'none');
    
    % Display title.
    title('Vertical Profile');
    
    % Display legend underneath.
    legend('Terrain', 'Location', 'southoutside');
    

    Method 2: Using one axes and modifying 'FaceVertexCData'

    Quoting gnovice's comments 1 and 2:

    I have to ask... why are you using two axes? Couldn't you plot the two filled polygons on the same axes, then modify the ZData property of each patch to ensure they are layered properly?

    Having multiple objects that need their own colormaps is tricky, but you can usually get around it by defining object colors so that they are RGB colors instead of colormap indices. For example, the fill command you use is really just creating patch objects, so for the sky background you could just make a patch object directly, paying special attention to how you define the C input argument. The sky could be made without a colormap, so the terrain could use it.

    For the sky part, replace fill with patch and set hold on. And then modify the 'FaceVertexCData' property with an array of true color RGB values (see Patch Properties).

    In my code below, the true color array is called CData and is defined by:

    % True color array.
    CData = [sky_map(1, :); sky_map(end, :); sky_map(end, :); sky_map(1, :)];
    

    which looks like this:

    CData =
    
        0.5294    0.8078    0.9216
        1.0000    1.0000    1.0000
        1.0000    1.0000    1.0000
        0.5294    0.8078    0.9216
    

    then use set with the patch handle h1:

    % Apply sky colormap to 'FaceVertexCData' property.
    set(h1, 'FaceVertexCData', CData);
    

    Result:

    Method 2


    Code:

    % Create figure.
    figure;
    
    % Sky data for background.
    x_bg = [0, 0, 10, 10];
    y_bg = [0, 10, 10, 0];
    
    % Sky indexed colors.
    c_bg = [0, 1, 1, 0];
    
    % Patch sky using default colormap (parula).
    h1 = patch(x_bg, y_bg, c_bg);
    
    % Generate custom sky colormap.
    sky_map = interp1([0, 1], [135, 206, 235; 255, 255, 255]./255, linspace(0, 1, 255));
    
    % True color array.
    CData = [sky_map(1, :); sky_map(end, :); sky_map(end, :); sky_map(1, :)];
    
    % Apply sky colormap to 'FaceVertexCData' property.
    set(h1, 'FaceVertexCData', CData);
    
    % Retain sky patch in the current axes.
    hold on;
    
    % Terrain data.
    x = [0, 0:10, 10];
    y = [0, 5*rand(size(0:10)), 0];
    
    % Fill terrain.
    h2 = fill(x, y, y);
    
    % Sets the colormap and color axis limits based on the elevation data
    % limits.
    demcmap(y);
    
    % Display title.
    title('Vertical Profile');
    
    % Display legend underneath.
    legend(h2, 'Terrain', 'Location', 'southoutside');
    

    Differences

    Note that the results generated with Method 1 and Method 2 are very similar but not the same:

    Method 1:

    • Slightly smaller figure.
    • Tick marks are visible.
    • Gray color for legend backgound.

    Method 2:

    • Slightly bigger figure.
    • Tick marks are not visible.
    • White color for legend backgound.