dsplot.m 9.25 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