Code covered by the BSD License  

Highlights from
plotLDS

from plotLDS by Sebastian Hölz
Enhance zooming and panning on large data sets by automatically downsampling data

plotLDS(varargin)
%
% varargout = plotLDS(varargin)
%
% IMPORTANT NOTE
% ==============
%
% This function will only work in MATLAB 7.3 or higher !!!
%
% DESCRIPTION
% ===========
% Function for plotting large data sets (e.g. time-series with more than 1e6 data points).
% The main benefit of this function can be seen in the fact that zooming and paning will work much smoother,
% since the data is automatically downsampled. This functionality is preserved when saving and reloading a figure.
%
% USAGE
% =====
% The usage is identical to the regular plot-command for vectors (x, y) with the following syntax supported:
%       
%       plotLDS(y)
%       plotLDS(x,y)
%       plotLDS(x,y,LineSpec)
%       plotLDS(..., PropName1, PropValue1, ..., PropNameN, PropValueN)
%       plotLDS(axis_handle, ...)
%
% Additional arguements:
%   'n_points'      Specify number of points to be used. Standard is 1e4.
%   'x_equal'       Specify if x-values are equally spaced. If not specified, this is tested inside
%                   the function, which can be time-consuming.
%   'y_equal'       Same as previous, but for x-values
%
%       plotLDS(... , 'n_points', 1e5, ...)     % Specify "n_points" as 1e5.
%       plotLDS(... , 'x_equal', 1, ...)        % Specify that x-values are equally spaced.
%
%
% HINTS & LIMITATIONS
% ===================
%
% "DeleteFcn" & "CreateFcn" are reserved for this function and should not be used otherwise for the created line.
%
% The "ActionPostCallback" (=> zoom & pan) is also used, but may also be used by the user ...
% it simply needs to be specified before the call to "plotLDS", e.g.:
% figure; h = zoom; set(h,'ActionPostCallback','disp(''hallo'')'); plotLDS(rand(1e6,1)); % Now try zooming
%
% Downsampling is simply performed by plotting only every n-th data point. Therefore, aliasing may occur !!!
%
% Data is clipped outside current axis, but is updated to the current limits after panning / zooming is finished.
%
% Using double-clicking in "zoom-in" modus might not work as expected, because the dispayed data-set
% is clipped. It is possible to return to the original data-set by using the scroll-wheel or the "zoom-out" tool.
%
% The x- & y-data is stored as application-data to the line-handle.
%
% Saved fig-files can get big, because the complete data is stored together with the decimated plot.
%
% The true x- and y-data for the line with handle "h", use ...
%   LDS = getappdata(h,'LDS_data'); x = LDS.x; y = LDS.y;
%
% EXAMPLES - Create data first, then use zoom & pan. Compare to regular plot-command !!!
% ======================================================================================
%
% y1 = rand(1,1e6); y2 = linspace(1,1e6,1e6); phi = linspace(0,2*pi,1e6);
%
% plotLDS(y1); figure; plotLDS(y2)                      % Example 1
%
% plotLDS(sin(1e4*y2))                                  % Example 2
%
% plotLDS(sin(phi),cos(phi),'.')                        % Example 3
%
% ax(1)=subplot(2,1,1); plotLDS(y1)                     % Example 4
% ax(2)=subplot(2,1,2); plotLDS(y2)
% linkaxes(ax,'x')
%
% plotLDS(y1); ax(1)=gca;                                % Example 5
% figure; plotLDS(y2); ax(2)=gca; 
% linkaxes(ax,'x') 
%
% plotLDS(y1,phi,'xdatasource','y1','ydatasource','phi')          % Example 6
%
% Example 6 demonstrates how to link data to the workspace. 
% This GREATLY reduces the memory need, but will only work for variables or strucure-fields from the base-workspace!!!
%

% AUTHOR
% ======
% Sebastian Hlz (shoelz "_AT_" ifm-geomar "_dot_" de)
%
% TODO & BUGS
% ===========
%
% There is still a memory issue preventing the linking of data to the workspace. Try the following:
%   y = rand(1e7,1); plotLDS(y,'ydatasource','y'); y(1) = 10;
% Changing the variable in the base-workspace will cause MATLAB to create a new (unwanted) copy of "y".
%
%
% VERSION
% =======
% 2.1   05.09.2008      Removed several bugs, improved performance, altered way of passing n_points
%                       to function. All occurences of "x_link" and "y_link" have been renamed to
%                       "xdatasource" and "ydatasource", respectively.
% 2.0.2 03.09.2008      Bugfix for monotonous data and linked data
% 2.0.1 02.09.2008      Minor bugfix in input-parsing
% 2.0   07.05.2008      Major rewrite
%                       Data can now be linked directly to the workspace. This is should be an enhancment
%                       concerning memory consumption, since data is not copied into LDSdata (but see bug ...).
%                       The according syntax is something like: "plotLDS(y,'ydatasource','y')"
%
% 1.3   29.04.2008      Major rewrite, after several bugs were noticed. 
%                       Handles are now stored differently and saving & reloading a figure with LDS_data now works.
%
% 1.2.1 11.03.2008      Removed bug for x = const. or y = const.
%                       Original zoom- & pan-ActionPostCallbacks are stored and evaluated after the LDS-ActionPostCallback.
%
% 1.2   25.02.2008      If x- or y-data are equal-spaced, they are only stored as [min dxy max] to save memory.
%                       Optional input "n_points": is kept as individual parameter for each line.
%
% 1.1   22.02.2008      Fixed bug for call, which creates several lines, e.g.: plotLDS(rand(1e6,2)).
%                       Will now work for monotonically increasing y-values and unstructured data as well.
%                       Will now work after saving and reloading a figure.
%
% 1.0   20.02.2008      First release
%

function varargout = plotLDS(varargin)
    
    % Check if n_points, x_equal or y_equal has been specified
    ind = find(strcmpi(varargin,'n_points'));
    if ind; n_points = varargin{ind+1}; varargin(ind:ind+1) = []; else n_points = 1e4; end
    
    ind = find(strcmpi(varargin,'x_equal'));
    if ind; x_equal = varargin{ind+1}; varargin(ind:ind+1) = []; else x_equal = []; end
    
    ind = find(strcmpi(varargin,'y_equal'));
    if ind; y_equal = varargin{ind+1}; varargin(ind:ind+1) = []; else y_equal = []; end
    
    % Check for PropertyValuePairs
    PropValPairs = {};
    for i_Prop = 1:length(varargin)
        if ischar(varargin{i_Prop}) && any(strcmpi(varargin{i_Prop}, ...
                {'DisplayName' 'Annotation' 'Color' 'EraseMode' 'LineStyle' 'LineWidth' 'Marker' 'MarkerSize' ...
                'MarkerEdgeColor' 'MarkerFaceColor' 'XData' 'YData' 'ZData' 'BeingDeleted' 'ButtonDownFcn'    ...
                'Children' 'Clipping' 'CreateFcn' 'DeleteFcn' 'BusyAction' 'HandleVisibility' 'HitTest'       ...
                'Interruptible' 'Selected' 'SelectionHighlight'  'Tag' 'Type' 'UIContextMenu' 'UserData'      ...
                'Visible' 'Parent' 'XDataMode' 'XDataSource' 'YDataSource' 'ZDataSource' ...
                'xdatasource' 'ydatasource'}))
            
            PropValPairs = varargin(i_Prop:end);
            varargin = varargin(1:i_Prop-1);
            break
        end
    end
    
    % Check if axis-handle has been specified as first or second argument
    if isscalar(varargin{1}) && ishandle(varargin{1}) && strcmp(get(varargin{1},'type'),'axes')
        PropValPairs(end+1:end+2) = {'parent', varargin{1}};
        varargin(1) = [];
    end    
    
    % Check which type of plot-command was used and determine if data is linked to base workspace
   	if length(varargin)==3; 
        x = varargin{1}; y = varargin{2}; LineSpec = varargin{3}; x_arg = 1; y_arg = 2;
    elseif length(varargin)==2 && ischar(varargin{2})
        x = [1 1 length(varargin{1})]; y = varargin{1}; LineSpec = varargin{2}; x_arg = 0; y_arg = 1;
    elseif length(varargin)==2 && isnumeric(varargin{2})
        x = varargin{1}; y = varargin{2}; LineSpec = ''; x_arg = 1; y_arg = 2;
    elseif length(varargin) == 1
        x = [1 1 length(varargin{1})]; y = varargin{1}; LineSpec = ''; x_arg = 0; y_arg = 1;
    end
    
    ind=find(strcmpi(PropValPairs,'XDataSource')); 
    if x_arg && ~isempty(ind); xdatasource = PropValPairs{ind+1}; PropValPairs(ind:ind+1) = []; else xdatasource = ''; end
    ind=find(strcmpi(PropValPairs,'YDataSource')); 
    if y_arg && ~isempty(ind); ydatasource = PropValPairs{ind+1}; PropValPairs(ind:ind+1) = []; else ydatasource = ''; end
    
    n_abs = length(y);
    
    % Plot dummy-line, the rest will be handled in the nested-functions after the creation of the line
	h = plot([min(x) min(x) max(x) max(x)],[min(y) max(y) min(y) max(y)], ...
        LineSpec,PropValPairs{:},'CreateFcn',@LDS_AddHandle);

    if nargout==1; varargout{1} = h; end

    % ===================================
    function PostZoomPanCallback(varargin)

        LDS_global = getappdata(0,'LDS_global');
        for i_fig = 1:length(LDS_global)
            
            h_fig = LDS_global(i_fig);
            LDS_fig = getappdata(LDS_global(i_fig),'LDS_fig');
            
            for i_ax = 1:length(LDS_fig.h_ax)
                
                h_ax = LDS_fig.h_ax(i_ax);
                LDS_ax = getappdata(h_ax,'LDS_ax');
                
                % Check if current axis was changed
                if all(LDS_ax.xlim==xlim(h_ax)) && all(LDS_ax.ylim==ylim(h_ax))
                    continue
                else
                    XLIM = xlim(h_ax); YLIM = ylim(h_ax); 
                    LDS_ax.xlim = XLIM;
                    LDS_ax.ylim = YLIM;
                end

                % Update lines
                for i_line = 1:length(LDS_ax.h_line)
                    
                    h_line = LDS_ax.h_line(i_line);
                    l_dat = getappdata(h_line, 'LDS_data');

                    if ~isempty(l_dat.xdatasource) && ~l_dat.x_equal; x = evalin('base',l_dat.xdatasource); else x = l_dat.x; end; x = x(:);
                    if ~isempty(l_dat.ydatasource) && ~l_dat.y_equal; y = evalin('base',l_dat.ydatasource); else y = l_dat.y; end; y = y(:);
                    
                    % Determine indices
                    if l_dat.x_monotone || l_dat.y_monotone % This is a time-series-like or depth-plot-like plot
                        if l_dat.x_monotone
                            if l_dat.x_equal
                                ind1 = max([floor((XLIM(1)-diff(XLIM)*.3-x(1))/x(2)) 1]);
                                ind2 = min([ceil((XLIM(2)+diff(XLIM)*.3-x(1))/x(2))  l_dat.n_abs]);
                            else
                                ind1 = find(x>XLIM(1)-diff(XLIM)*.3, 1, 'first');
                                ind2 = find(x<XLIM(2)+diff(XLIM)*.3, 1, 'last');
                            end
                        else
                            if l_dat.y_equal
                                ind1 = max([floor((YLIM(1)-diff(YLIM)*.3-y(1))/y(2)) 1]);
                                ind2 = min([ceil((YLIM(2)+diff(YLIM)*.3-y(1))/y(2))  l_dat.n_abs]);
                            else
                                ind1 = find(y>YLIM(1)-diff(YLIM)*.3, 1, 'first');
                                ind2 = find(y<YLIM(2)+diff(YLIM)*.3, 1, 'last');
                            end
                        end

                        if isempty(ind1); ind1 = 1; end
                        if isempty(ind2); ind2 = l_dat.n_abs; end

                        n = ind2-ind1+1;
                        d_ind = ceil(n/l_dat.n_points);
                        ind = ind1:d_ind:ind2;
                        if ind(end)<ind2; ind(end+1)=ind2; end      %#ok

                    else % this is the unstructured case ...
                       	ind_tmp = find( ...
                            (x>XLIM(1)-diff(XLIM)) & (x<XLIM(2)+diff(XLIM)) & ...
                            (y>YLIM(1)-diff(YLIM)) & (y<YLIM(2)+diff(YLIM)));

                        n = length(ind_tmp);
                        d_ind = ceil(n/l_dat.n_points);
                        ind = ind_tmp(1:d_ind:n);

                    end

                    if ishandle(l_dat.h);
                        if length(x)==3; x = ind*x(2)+x(1); else x = x(ind); end
                        if length(y)==3; y = ind*y(2)+y(1); else y = y(ind); end
                        set(l_dat.h,'xdata',x,'ydata',y); 
                    end

                end
                
                setappdata(h_ax, 'LDS_ax', LDS_ax)
            end
            
%             % Call original pan- / and zomm-callbacks
%             if nargin>0
%                 try eval(LDS_fig.pan_ActionPostCallback); end
%                 try eval(LDS_fig.zoom_ActionPostCallback); end
%             end
        end
    end


    % ==============================
    function LDS_AddHandle(h_line, varargin)

        % Get handles of axis and figure, which contain h_line
        h_ax = get(h_line,'parent');
        while ~strcmp(get(h_ax,'type'),'axes'); h_ax = get(h_ax,'Parent'); end % Make sure that parent of h_line is not a hgtransorm or hggroup
        h_fig = get(h_ax,'parent');
        
        % Prepare LDS_global, LDS_fig, LDS_ax, LDS_line
        % LDS_global:   handles of figures containing lines with LDSdata;       stored in root
        % LDS_fig:      current callbacks & handles of axes containing LDSdata; stored in figure
        % LDS_ax:       current limits and handles of lines containing LDSdata; stored in axis
        % LDSdata:      information needed for LDSplot
        try LDS_global = getappdata(0,'LDS_global');    catch LDS_global = []; end
        try LDS_fig    = getappdata(h_fig,'LDS_fig');   catch LDS_fig = []; end
        try LDS_ax     = getappdata(h_ax,'LDS_ax');     catch LDS_ax = []; end
        try LDS_line   = getappdata(h_line,'LDS_line'); catch LDS_line = []; end
        
        % LDS_global & LDS_fig
        if isempty(find(h_fig == LDS_global, 1))
            LDS_global(end+1) = h_fig;
            
            % We need to add zoom- & pan-callbacks, this also tests, if a valid Matlab-Version is used
            try
%                 LDS_fig.zoom_ActionPostCallback = get(zoom,'ActionPostCallback');
%                 set(zoom,'ActionPostCallback', @PostZoomPanCallback);
% 
%                 LDS_fig.pan_ActionPostCallback = get(pan,'ActionPostCallback');
%                 set(pan,'ActionPostCallback', @PostZoomPanCallback);

                z = zoom; p = pan;
                CallbackStack([z p],'ActionPostCallback', @PostZoomPanCallback);
                
                LDS_fig.h_ax = [];

            catch
                error('PlotLDS:OldMatlabVersion', '\n\t%s\n\t%s\n', ...
                    'Sorry, your version of Matlab does not support the required zoom-features.', ...
                    'Matlab 7.3 or higher is required to use this function.')
            end
        end

        if isempty(find(h_ax==[LDS_fig.h_ax], 1))
            LDS_fig.h_ax(end+1)	= h_ax;
        end

        % LDS_ax
        if isempty(LDS_ax)
            % Set limits to zero. Otherwise line will not be updated in call to ZoomPanFcn
            LDS_ax.xlim   = [0 0];   
            LDS_ax.ylim   = [0 0];
            
            LDS_ax.h_line = [];
        end

        if isempty(find(h_line==[LDS_ax.h_line], 1))
            LDS_ax.h_line(end+1) = h_line;
        end
               
        % LDS_data
        db = dbstack;
        if strcmp(db(2).name,'plotLDS') % Upon loading of a figure containing LDSdata, this part will be skipped
            
            incr = 2^11;
            
            % Check x-properties
            if ~x_arg
                x = [1 1 n_abs]; x_equal = 1; x_monotone = 1;
            elseif x_equal % Specified as input arguement
                x_monotone = 1;
                x = [x(1) x(2)-x(1) x(end)];
            else
                dx1 = x(2)-x(1);        x_equal    = 1;
                sign_dx1 = sign(dx1);   x_monotone = 1;
                for j = 1:incr:length(x)
                    try X=double(x(j:j+incr)); catch X=double(x(j:end)); end; X = X(:);
                    
                    if x_equal
                        X_tmp = linspace(X(1),X(end),length(X)); X_tmp = X_tmp(:);
                        if any(abs(X-X_tmp)>abs(X_tmp.*eps)); 
                            x_equal=0; 
                        end
                    end
                    
                    if x_monotone
                        if any(sign(diff(X))~=sign_dx1); x_monotone = 0; end
                    end
                    
                    if ~(x_equal || x_monotone); break; end
                end
                if x_equal; x = [x(1) dx1 x(end)]; end
            end
            if ~isempty(xdatasource) && ~x_equal; x=[]; end
                        
            % Check y-properties
            if y_equal      % Specified as input arguement
                y_monotone = 1;
                y = [y(1) y(2)-y(1) y(end)];
                
            else
                dy1 = y(2)-y(1);        y_equal    = 1;
                sign_dy1 = sign(dy1);   y_monotone = 1;
                for j = 1:incr:length(y)
                    try Y=double(y(j:j+incr)); catch Y=double(y(j:end)); end; Y = Y(:);

                    if y_equal
                        Y_tmp = linspace(Y(1),Y(end),length(Y)); Y_tmp = Y_tmp(:);
                        if any(abs(Y-Y_tmp)>abs(Y_tmp.*eps)); y_equal=0; end
                    end

                    if y_monotone
                        if any(sign(diff(Y))~=sign_dy1); y_monotone = 0; end
                    end

                    if ~(y_equal || y_monotone); break; end
                end
                if y_equal; y = [y(1) dy1 y(end)]; end
            end
            if ~isempty(ydatasource) && ~y_equal; y=[]; end
            
            unstructured = ~(x_monotone || y_monotone);

            LDS_data = ...
                struct('h',h_line,'n_points',n_points, 'n_abs', n_abs, ...
                'x',x,'x_equal',x_equal,'x_monotone',x_monotone,'xdatasource',xdatasource, ...
                'y',y,'y_equal',y_equal,'y_monotone',y_monotone,'ydatasource',ydatasource);
            
            setappdata(h_line,'LDS_data',LDS_data)
        end
        
        % Set application data
        setappdata(0,'LDS_global',LDS_global)
        setappdata(h_fig,'LDS_fig',LDS_fig)
        setappdata(h_ax,'LDS_ax',LDS_ax)

        set([h_fig h_ax h_line],'DeleteFcn',@LDS_DeleteHandle);
        PostZoomPanCallback
    end

    % ==============================
    function LDS_DeleteHandle(varargin)
        
        h = varargin{1};
        switch get(varargin{1},'type')
            case 'figure'
                LDS_global = getappdata(0,'LDS_global');
                LDS_global(LDS_global==h) = [];
                setappdata(0,'LDS_global',LDS_global)
                
            case 'axes'
                h_fig = get(h,'parent');
                LDS_fig = getappdata(h_fig,'LDS_fig');
                LDS_fig.h_ax(LDS_fig.h_ax==h) = [];
                setappdata(h_fig,'LDS_fig',LDS_fig)
                
            case 'line'
                h_ax = get(h,'parent');
                LDS_ax = getappdata(h_ax,'LDS_ax');
                LDS_ax.h_line(LDS_ax.h_line==h) = [];
                setappdata(h_ax,'LDS_ax',LDS_ax)
                
            otherwise
                disp(get(varargin{1},'type'))
        end
    end
end

Contact us at files@mathworks.com