dsplot.m 9.73 KB
function hL = dsplot(x, y, numPoints)

%DSPLOT Create down sampled plot.
%   This function creates a down sampled plot to improve the speed of
%   exploration (zoom, pan).
%
%   DSPLOT(X, Y) plots Y versus X by downsampling if there are large number
%   of elements. X and Y needs to obey the following:
%     1. X must be a monotonically increasing vector.
%     2. If Y is a vector, it must be the same size as X.
%     3. If Y is a matrix, one of the dimensions must line up with X.
%
%   DSPLOT(Y) plots the columns of Y versus their index.
%
%   hLine = DSPLOT(X, Y) returns the handles of the line. Note that the
%   lines may be downsampled, so they may not represent the full data set.
%
%   DSPLOT(X, Y, NUMPOINTS) or DSPLOT(Y, [], NUMPOINTS) specifies the
%   number of points (roughly) to display on the screen. The default is
%   50000 points (~390 kB doubles). NUMPOINTS can be a number greater than
%   500.
%
%   It is very likely that more points will be displayed than specified by
%   NUMPOINTS, because it will try to plot any outlier points in the range.
%   If the signal is stochastic or has a lot of sharp changes, there will
%   be more points on plotted on the screen.
%
%   The figure title (name) will indicate whether the plot shown is
%   downsampled or is the true representation.
%
%   The figure can be saved as a .fig file, which will include the actual
%   data. The figure can be reloaded and the actual data can be exported to
%   the base workspace via a menu.
%
%   Run the following examples and zoom/pan to see the performance.
%
%  Example 1: (with small details)
%   x  = linspace(0, 2*pi, 1000000);
%   y1 = sin(x)+.02*cos(200*x)+0.001*sin(2000*x)+0.0001*cos(20000*x);
%   dsplot(x,y1);title('Down Sampled');
%   % compare with
%   figure;plot(x,y1);title('Normal Plot');
%
%  Example 2: (with outlier points)
%   x  = linspace(0, 2*pi, 1000000);
%   y1 = sin(x) + .01*cos(200*x) + 0.001*sin(2000*x);
%   y2 = sin(x) + 0.3*cos(3*x)   + 0.001*randn(size(x));
%   y1([300000, 700000, 700001, 900000]) = [0, 1, -2, 0.5];
%   y2(300000:500000) = y2(300000:500000) + 1;
%   y2(500001:600000) = y2(500001:600000) - 1;
%   y2(800000) = 0;
%   dsplot(x, [y1;y2]);title('Down Sampled');
%   % compare with
%   figure;plot(x, [y1;y2]);title('Normal Plot');
%
%  See also PLOT.

%  Version:
%   v1.0 - first version (Aug 1, 2007)
%   v1.1 - added CreateFcn for the figure so that when the figure is saved
%          and re-loaded, the zooming and panning works. Also added a menu
%          item for saving out the original data back to the base
%          workspace. (Aug 10, 2007)
%
%  Jiro Doke
%  August 1, 2007

debugMode = false;

%--------------------------------------------------------------------------
% Error checking
error(nargchk(1, 3, nargin, 'struct'));
if nargin < 3
  % Number of points to show on the screen. It's quite possible that more
  % points will be displayed if there are outlier points
  numPoints = 50000;  % ~390 kB for doubles
end
if nargin == 1 || isempty(y)
  noXVar = true;
  y = x;
  x = [];
else
  noXVar = false;
end
myErrorCheck;
%--------------------------------------------------------------------------

if size(x, 2) > 1  % it's a row vector -> transpose
  x = x';
  y = y';
  varTranspose = true;
else
  varTranspose = false;
end

% Number of lines
numSignals = size(y, 2);

% If the number of lines is greater than the number of data points per
% line, it's possible that the user may have mistaken the matrix
% orientation.
if numSignals > size(y, 1)
  s = input(sprintf('Are you sure you want to plot %d lines? (y/n) ', ...
    numSignals), 's');
  if ~strcmpi(s, 'y')
    disp('Canceled. You may want to transpose the matrix.');
    if nargout == 1
      hL = [];
    end
    return;
  end
end

% Attempt to find outliers. Use a running average technique
filterWidth = ceil(min([50, length(x)/10])); % max window size of 50
a  = y - filter(ones(filterWidth,1)/filterWidth, 1, y);
[iOutliers, jOutliers] = find(abs(a - repmat(mean(a), size(a, 1), 1)) > ...
  repmat(4 * std(a), size(a, 1), 1));
clear a;

% Always create new figure because it messes around with zoom, pan,
% datacursors.
hFig    = figure;
figName = '';

% Create template plot using NaNs
hLine   = plot(NaN(2, numSignals), NaN(2, numSignals));
set(hLine, 'tag', 'dsplot_lines');

% Define CreateFcn for the figure
set(hFig, 'CreateFcn', @mycreatefcn);
mycreatefcn();

% Create menu for exporting data
hMenu = uimenu(hFig, 'Label', 'Data');
uimenu(hMenu, ...
  'Label'   , 'Export data to workspace.', ...
  'Callback', @myExportFcn);

% Update lines
updateLines([min(x), max(x)]);

% Deal with output argument
if nargout == 1
  hL = hLine;
end

%--------------------------------------------------------------------------
  function myExportFcn(varargin)
    % This callback allows for extracting the actual data from the figure.
    % This means that if you save this figure and load it back later, you
    % can get back the data.
    
    % Determine the variable name
    allVarNames = evalin('base', 'who');
    newVarName = genvarname('dsplotData', allVarNames);
    
    % X
    if ~noXVar
      if varTranspose
        dat.x = x';
      else
        dat.x = x;
      end
    end
    
    % Y
    if varTranspose
      dat.y = y';
    else
      dat.y = y;
    end
    
    assignin('base', newVarName, dat);
    
    msgbox(sprintf('Data saved to the base workspace as ''%s''.', ...
      newVarName), 'Saved', 'modal');
    
  end

%--------------------------------------------------------------------------
  function mycreatefcn(varargin)
    % This callback defines the custom zoom/pan functions. It is defined as
    % the CreateFcn of the figure, so it allows for saving and reloading of
    % the figure.

    if nargin > 0
      hFig = varargin{1};
    end
    hLine = findobj(hFig, 'type', 'axes');
    hLine(strmatch('legend', get(hLine, 'tag'))) = [];
    hLine = get(hLine, 'Children');
    
    % Create Zoom, Pan, Datacursor objects
    hZoom = zoom(hFig);
    hPan  = pan(hFig);
    hDc   = datacursormode(hFig);
    set(hZoom, 'ActionPostCallback', @mypostcallback);
    set(hPan , 'ActionPostCallback', @mypostcallback);
    set(hDc  , 'UpdateFcn'         , @myDCupdatefcn);

  end

%--------------------------------------------------------------------------
  function mypostcallback(obj, evd) %#ok
    % This callback that gets called when the mouse is released after
    % zooming or panning.

    % single or double-click
    switch get(hFig, 'SelectionType')
      case {'normal', 'alt'}
        updateLines(xlim(evd.Axes));

      case 'open'
        updateLines([min(x), max(x)]);

    end

  end

%--------------------------------------------------------------------------
  function updateLines(rng)
    % This helper function is for determining the points to plot on the
    % screen based on which portion is visible in the current limits.

    % find indeces inside the range
    id = find(x >= rng(1) & x <= rng(2));

    % if there are more points than we want
    if length(id) > numPoints / numSignals

      % see how many outlier points are in this range
      blah = iOutliers > id(1) & iOutliers < id(end);

      % determine indeces of points to plot. 
      idid = round(linspace(id(1), id(end), round(numPoints/numSignals)))';

      x2 = cell(numSignals, 1);
      y2 = x2;
      for iSignals = 1:numSignals
        % add outlier points
        ididid = unique([idid; iOutliers(blah & jOutliers == iSignals)]);
        x2{iSignals} = x(ididid);
        y2{iSignals} = y(ididid, iSignals);
      end

      if debugMode
        figName = ['downsampled - ', sprintf('%d, ', cellfun('length', y2))];
      else
        figName = 'downsampled';
      end

    else % no need to down sample
      figName = 'true';

      x2 = repmat({x(id)}, numSignals, 1);
      y2 = mat2cell(y(id, :), length(id), ones(1, numSignals))';

    end

    % Update plot
    set(hLine, {'xdata', 'ydata'} , [x2, y2]);
    set(hFig, 'Name', figName);

  end

%--------------------------------------------------------------------------
  function txt = myDCupdatefcn(empt, event_obj) %#ok
    % This function displays appropriate data cursor message based on the
    % display type

    pos = get(event_obj,'Position');
    switch figName
      case 'true'
        txt = {['X: ',num2str(pos(1))],...
          ['Y: ',num2str(pos(2))]};
      otherwise
        txt = {['X: ',num2str(pos(1))],...
          ['Y: ',num2str(pos(2))], ...
          'Warning: Downsampled', ...
          'May not be accurate'};
    end
  end

%--------------------------------------------------------------------------
  function myErrorCheck
    % Do some error checking on the input arguments.

    if ~isa(numPoints, 'double') || numel(numPoints) > 1 || numPoints < 500
      error('Third argument must be a scalar greater than 500');
    end
    if ~isnumeric(x) || ~isnumeric(y)
      error('Arguments must be numeric');
    end
    if length(size(x)) > 2 || length(size(y)) > 2
      error('Only 2-D data accepted');
    end
    
    % If only one input, create index vector X
    if isempty(x)
      if ismember(1, size(y))
        x = reshape(1:numel(y), size(y));
      else
        x = (1:size(y, 1))';
      end
    end
    
    if ~ismember(1, size(x))
      error('First argument has to be a vector');
    end
    if ~isequal(size(x, 1), size(y, 1)) && ~isequal(size(x, 2), size(y, 2))
      error('One of the dimensions of the two arguments must match');
    end
    if any(diff(x) <= 0)
      error('The first argument has to be a monotonically increasing vector');
    end
  end

end