Code covered by the BSD License  

Highlights from
PLOT2AXES

image thumbnail
from PLOT2AXES by Jiro Doke
Plot a set of data with two different axes.

plot2axes(varargin)
function [ax, h] = plot2axes(varargin)

%PLOT2AXES Graphs one set of data with two sets of axes
%
%   PLOT2AXES(X, Y, 'Param1', 'Value1', ...) plots X versus Y with
%   secondary axes.  The following parameters are accepted [default
%   values]:
%
%     xloc ['top']: location of secondary X-axis
%     yloc ['right']: location of secondary Y-axis
%     xscale [1]: scaling factor for secondary X-axis (scalar)
%     yscale [1]: scaling factor for secondary Y-axis (scalar)
%
%           xscale and yscale can also be an anonymous function that
%           describes the relationship between the 2 axes, such as the
%           equation relating Celsius and Fahrenheit: @(x) 5/9*(x-32)
%
%     xlim [NaN NaN] : xlim in the primary axes (secondary is adjusted
%                      accordingly). The default is naturally selected by
%                      the plotting function.
%     ylim [NaN NaN] : ylim in the primary axes (secondary is adjusted
%                      accordingly). The default is naturally selected by
%                      the plotting function.
%
%   PLOT2AXES(@FUN, ...) uses the plotting function @FUN instead of PLOT to
%   produce the plot.  @FUN should be a function handle to a plotting
%   function, e.g. @plot, @semilogx, @semilogy, @loglog ,@stem, etc. that
%   accepts the syntax H = FUN(...).  Optional arguments accepted by these
%   plotting functions are also allowed (e.g. PLOT2AXES(X, Y, 'r*', ...))
%
%   [AX, H] = PLOT2AXES(...) returns the handles of the primary and
%   secondary axes (in that order) in AX, and the handles of the graphic
%   objects in H.
%
%   Right-click on the axes to bring up a context menu for adding grids to
%   the axes.
%
%   The actual data is plotted in the primary axes.  The primary axes lie
%   on top of the secondary axes.  After the execution of this function,
%   the primary axes become the current axes.  If the next plot replaces
%   the axes, the secondary axes are automatically deleted.
%
%   When you zoom and pan, the secondary axes will automatically adjust
%   itself (only available on R2006b or later). For older releases of
%   MATLAB, there will be a "Fix Axes" menu, which let's you adjust the
%   limits.
%
%   PLOT2AXES('FixAxes') fixes the secondary limits of all figures created
%   using plot2axes.
%
%   Example 1:
%     x = 0:.1:1;
%     y = x.^2 + 0.1*randn(size(x));
%     [ax, h] = plot2axes(x, y, 'ro', 'yscale', 25.4);
%     title('Length vs Time');
%     ylabel(ax(1), 'inch');
%     ylabel(ax(2), 'millimeter');
%     xlabel('time (sec)');
%
%   Example 2:
%     x = 1:10;
%     y = 50*rand(size(x));
%     [ax, h] = plot2axes(x, y, 'ro', 'yscale', @(x) 5/9*(x-32), ...
%         'xscale', 2.20);
%     xlabel(ax(1), 'kg'); ylabel(ax(1), 'Fahrenheit');
%     xlabel(ax(2), 'lb'); ylabel(ax(2), 'Celcius');
%
% See also PLOTYY, function_handle.

% VERSIONS:
%   v1.0 - first version.
%   v1.1 - added option to specify X and Y limits.
%   v1.2 - remove tick labels for secondary axes if no scaling factors are
%          specified. Also, fixed bug in matching the scaling type (linear
%          or log).
%   v1.3 - added the 'Fix Axes' menu for adjusting the secondary axes
%          limits after zooming.
%   v1.4 - added the option for specifying an equation for xscale and
%          yscale. (June 2005)
%   v1.5 - fixed problem plotting on uipanel, where the parent of the axes
%          is not a figure. (Feb 2006)
%   v2.0 - added auto adjust of axes limits (works R2006b and later). now
%          works with nonlinear scaling. added ability to add grids
%          interactively. use of function handles instead of strings.
%          (Dec 2009)
%
% Copyright 2009 The MathWorks, Inc.

if nargin < 1
  error('Not enough input arguments');
end

if nargin == 1 && strcmpi(varargin{1}, 'FixAxes')
  figsH = findobj('Type', 'figure');
  if ~isempty(figsH)
    for iFig = 1:length(figsH)
      fixAxes(figsH(iFig));
    end
  end
  return;
end

% Default options
opts.xloc = 'top';
opts.yloc = 'right';
opts.xscale = 1;
opts.yscale = 1;
opts.xlim = [NaN NaN];
opts.ylim = [NaN NaN];

var = varargin;

% Check to see if the first argument is a function handle
if isa(var{1}, 'function_handle');
  func   = var{1};
  var(1) = '';
else
  func = @plot;
end

% Parse optional arguments
idx = 1;
while idx <= length(var)
  param = var{idx};
  
  % Optional parameters must be CHARS
  if ~ischar(param)
    idx = idx + 1;
    continue;
  end
  
  param = lower(param);
  
  % Process only if it's one of the parameters
  if ~ismember(param, {'xloc','yloc','xscale','yscale','xlim','ylim'})
    idx = idx + 1;
    continue;
  end
  
  % Make sure that there is a corresponding value
  if length(var) <= idx
    error('plot2axes:InvalidParamValuePair', ...
      'Optional parameters must come in param-value pairs');
  end
  
  % Validate the values
  val = var{idx+1};
  switch param
    case 'xloc'
      if ~ischar(val) || ~ismember(lower(val), {'top', 'bottom'})
        myerror(upper(param));
      end
      opts.xloc = lower(val);
      
    case 'yloc'
      if ~ischar(val) || ~ismember(lower(val), {'left', 'right'})
        myerror(upper(param)');
      end
      opts.yloc = lower(val);
      
    case {'xscale', 'yscale'}
      if (~isnumeric(val) && ~isa(val, 'function_handle')) || ...
          numel(val) > 1
        myerror(upper(param));
      end
      opts.(param) = val;
      
    case {'xlim', 'ylim'}
      if ~isnumeric(val) || ~isequal(size(val), [1 2])
        myerror(upper(param));
      end
      opts.(param) = val;
      
  end
  
  % Remove the arguments from the list
  var(idx:idx+1) = '';
  
end

% Determine the axes to plot
ax1 = newplot;
nextplot = get(ax1, 'NextPlot');

figH = gcf;

% Plot data
h = feval(func, var{:});

set(ax1, 'Box', 'off', 'Color', 'none');

% Create secondary axes on top of primary axes
ax2 = axes(...
  'Box'     , 'off', ...
  'Parent'  , get(ax1, 'Parent'), ...
  'Hittest', 'off');

% Link the Position property
hl = linkprop([ax1 ax2], 'Position');
set(ax1, 'UserData', hl);

opts.ax1 = ax1;
opts.ax2 = ax2;

% Create context menu for grid control
hMenu = uicontextmenu('Callback', @contextMenuCallback);
uimenu('Parent', hMenu, 'Label', 'Left Grid', ...
  'Callback', @contextMenuCallback);
uimenu('Parent', hMenu, 'Label', 'Right Grid', ...
  'Callback', @contextMenuCallback);
uimenu('Parent', hMenu, 'Label', 'Bottom Grid', ...
  'Callback', @contextMenuCallback);
uimenu('Parent', hMenu, 'Label', 'Top Grid', ...
  'Callback', @contextMenuCallback);

set([ax1, ax2], 'UIContextMenu', hMenu);

%--------------------------------------------------------------------------
% Apply options - scaling is done in "fixAxes" subfunction
%--------------------------------------------------------------------------
if strcmpi(opts.xloc, 'top') % xloc
  set(ax1, 'XAxisLocation', 'bottom');
  set(ax2, 'XAxisLocation', 'top');
else
  set(ax1, 'XAxisLocation', 'top');
  set(ax2, 'XAxisLocation', 'bottom');
end

if strcmpi(opts.yloc, 'right') % yloc
  set(ax1, 'YAxisLocation', 'left');
  set(ax2, 'YAxisLocation', 'right');
else
  set(ax1, 'YAxisLocation', 'right');
  set(ax2, 'YAxisLocation', 'left');
end

if ~all(isnan(opts.xlim))  % xlim
  set(ax1, 'xlim', opts.xlim);
end
if ~all(isnan(opts.ylim))  % ylim
  set(ax1, 'ylim', opts.ylim);
end

set(ax2, 'xscale', get(ax1, 'xscale'), ...
  'yscale', get(ax1, 'yscale'));

%--------------------------------------------------------------------------
% Create DeleteProxy objects (an invisible text object) so that the other
% axes will be deleted properly.  <inspired by PLOTYY>
%--------------------------------------------------------------------------
DeleteProxy(1) = text(...
  'Parent'          , ax1, ...
  'Visible'         , 'off', ...
  'HandleVisibility', 'off');
DeleteProxy(2) = text(...
  'Parent'          , ax2, ...
  'Visible'         , 'off', ...
  'HandleVisibility', 'off', ...
  'UserData'        , DeleteProxy(1));
set(DeleteProxy(1), ...
  'UserData'        , DeleteProxy(2));

set(DeleteProxy, ...
  'DeleteFcn'       , @DelFcn);

%--------------------------------------------------------------------------
% Switch the order of axes, so that the secondary axes are under the
% primary axes, and that the primary axes become the current axes.
%--------------------------------------------------------------------------
% get list of figure children. ax1 and ax2 must exist in this list
ch = get(get(ax1, 'Parent'), 'Children');
i1 = find(ch == ax1);       % find where ax1 is
i2 = find(ch == ax2);       % find where ax2 is
ch([i1, i2]) = [ax2; ax1];  % swap ax1 and ax2

% assign the new list of children and set current axes to primary
set(get(ax1, 'Parent'), 'Children', ch);
set(figH, 'CurrentAxes', ax1);

% Restore NextPlot property (just in case it was modified)
set([ax1, ax2], 'NextPlot', nextplot);

setappdata(ax1, 'p2a', opts);

fixAxes(figH, struct('Axes', ax1));

try
  % Try setting Action Callbacks for zoom and pan
  % (only available in R2006b or later)
  hz = zoom;
  hp = pan;
  set(hz, 'ActionPostCallback', @fixAxes);
  set(hp, 'ActionPostCallback', @fixAxes);
  
catch %#ok<CTCH>
  % It must be an older release of MATLAB (pre-R2006b)
  
  % Create 'Fix Axes' button for adjusting the secondary axes limits after
  % zooming
  hMenu = findobj(figH, 'Type', 'uimenu', 'Label', 'Fix Axes');
  if strcmpi(get(figH, 'Menubar'), 'figure') && ...
      (isempty(hMenu) || ~ishandle(hMenu))
    hMenu = uimenu('Parent', figH, 'Label', 'Fix Axes');
    uimenu('Parent', hMenu, 'Label', 'Fix Axes Limits', ...
      'Callback', @fixAxes);
  end
  zoom off;
  pan off;
  
end

if nargout
  ax = [ax1, ax2];
end


function DelFcn(obj, edata) %#ok<INUSD>
%--------------------------------------------------------------------------
% DelFcn - automatically delete both axes
%--------------------------------------------------------------------------

try %#ok<TRYNC>
  set(get(obj, 'UserData'), ...
    'DeleteFcn', 'try;delete(get(gcbo, ''UserData''));end');
  set(obj, 'UserData', ...
    get(get(obj, 'UserData'), 'Parent'));
  delete(get(obj,'UserData'));
end


function fixAxes(obj, edata)
%--------------------------------------------------------------------------
% fixAxes - fix the scaling of the axes
%--------------------------------------------------------------------------

% This is only for pre-R2006b (called from a menu)
if strcmp(get(obj, 'type'), 'uimenu')
  fixAxes(ancestor(obj, 'figure'));
  return;
end

if nargin == 1
  axs = findobj(obj, 'type', 'axes');
  for iAx = 1:length(axs)
    fixAxes(obj, struct('Axes', axs(iAx)));
  end
  
else
  
  opts = getappdata(edata.Axes, 'p2a');
  
  if ~isempty(opts)
    if ishandle(opts.ax1) && ishandle(opts.ax2)
      if isa(opts.xscale, 'function_handle')
        fh = opts.xscale;
        if islinearANDincreasing(fh, xlim(opts.ax1))
          set(opts.ax2, 'XLim', fh(xlim(opts.ax1)));
          set(opts.ax2, 'XTickLabelMode', 'auto');
          set(opts.ax2, 'XTickMode', 'auto');
        else
          set(opts.ax2, 'xlim', xlim(opts.ax1));
          set(opts.ax2, 'XTick', get(opts.ax1, 'XTick'));
          set(opts.ax2, 'XTickLabel', num2str(fh(get(opts.ax1, 'XTick'))',3));
        end
      else
        xlim(opts.ax2, opts.xscale * xlim(opts.ax1));
        if opts.xscale == 1
          set(opts.ax2, 'XTickLabel', '');
        end
      end
      if isa(opts.yscale, 'function_handle')
        fh = opts.yscale;
        if islinearANDincreasing(fh, ylim(opts.ax1))
          set(opts.ax2, 'YLim', fh(ylim(opts.ax1)));
          set(opts.ax2, 'YTickLabelMode', 'auto');
          set(opts.ax2, 'YTickMode', 'auto');
        else
          set(opts.ax2, 'YLim', ylim(opts.ax1));
          set(opts.ax2, 'YTick', get(opts.ax1, 'YTick'));
          set(opts.ax2, 'YTickLabel', num2str(fh(get(opts.ax1, 'YTick'))',3));
        end
      else
        ylim(opts.ax2, opts.yscale * ylim(opts.ax1));
        if opts.yscale == 1
          set(opts.ax2, 'YTickLabel', '');
        end
      end
    end
  end
end


function x = islinearANDincreasing(fcn,lims)
%--------------------------------------------------------------------------
% islinearANDincreasing - checks to see if FUN is a linear and increasing
%                         function
%--------------------------------------------------------------------------

% Create linearly spaced vector
vals = linspace(lims(1), lims(2), 20);

% Evaluate using "fun"
newvals = fcn(vals);

% Get the differences between values
delta = diff(newvals);

% Check to see if they are increasing
if ~all(delta>0)
  x = false;
  return;
end

delta_mean = mean(delta);
delta_stdev = std(delta);

% If all differences are the "same" (within 3 standard deviations), we will
% assume that they are equally spaced
x = all(delta < delta_mean+3*delta_stdev & delta > delta_mean-3*delta_stdev);


function contextMenuCallback(hObject, edata) %#ok<INUSD>
%--------------------------------------------------------------------------
% contextMenuCallback - manage grid display
%--------------------------------------------------------------------------

opts = getappdata(gca, 'p2a');

% If this is the top level context menu, update the checks based on whether
% the grid is on or not. This makes sure that if the grid was turned on
% programmatically, the check marks would accurately represent the state of
% the grids.
if strcmp(get(hObject, 'Type'), 'uicontextmenu')
  lgridMenu = findobj(hObject, 'Label', 'Left Grid');
  rgridMenu = findobj(hObject, 'Label', 'Right Grid');
  bgridMenu = findobj(hObject, 'Label', 'Bottom Grid');
  tgridMenu = findobj(hObject, 'Label', 'Top Grid');
  if strcmpi(opts.yloc, 'right')
    [lAx, rAx] = deal(opts.ax1, opts.ax2);
  else
    [lAx, rAx] = deal(opts.ax2, opts.ax1);
  end
  if strcmpi(opts.xloc, 'top')
    [bAx, tAx] = deal(opts.ax1, opts.ax2);
  else
    [bAx, tAx] = deal(opts.ax2, opts.ax1);
  end
  
  % Store in the UserData which axes and grid to turn on/off
  if strcmpi(get(lAx, 'YGrid'), 'on')
    set(lgridMenu, 'Checked', 'on');
    set(lgridMenu, 'UserData', {lAx, 'ygrid', 'off'});
  else
    set(lgridMenu, 'Checked', 'off');
    set(lgridMenu, 'UserData', {lAx, 'ygrid', 'on'});
  end
  if strcmpi(get(rAx, 'YGrid'), 'on')
    set(rgridMenu, 'Checked', 'on');
    set(rgridMenu, 'UserData', {rAx, 'ygrid', 'off'});
  else
    set(rgridMenu, 'Checked', 'off');
    set(rgridMenu, 'UserData', {rAx, 'ygrid', 'on'});
  end
  if strcmpi(get(bAx, 'XGrid'), 'on')
    set(bgridMenu, 'Checked', 'on');
    set(bgridMenu, 'UserData', {bAx, 'xgrid', 'off'});
  else
    set(bgridMenu, 'Checked', 'off');
    set(bgridMenu, 'UserData', {bAx, 'xgrid', 'on'});
  end
  if strcmpi(get(tAx, 'XGrid'), 'on')
    set(tgridMenu, 'Checked', 'on');
    set(tgridMenu, 'UserData', {tAx, 'xgrid', 'off'});
  else
    set(tgridMenu, 'Checked', 'off');
    set(tgridMenu, 'UserData', {tAx, 'xgrid', 'on'});
  end
  
else
  % The UserData contains all the necessary information to perform the
  % appropriate action.
  ud = get(hObject, 'UserData');
  % ud{1} - axes handle
  % ud{2} - xgrid/ygrid
  % ud{3} - on/off
  
  set(hObject, 'Checked', ud{3});
  set(ud{1}, ud{2}, ud{3});
  
end

function myerror(param)

error('plot2axes:InvalidParameterValue', ...
  'Invalid value for parameter %s', param);

Contact us at files@mathworks.com