Code covered by the BSD License  

Highlights from
dtmcursor

image thumbnail
from dtmcursor by Jonathan Lister
DTMCURSOR implements a cross hair with the ability to set and measure values from x and/or y datums.

dtmcursor
classdef dtmcursor < hgsetget
    %dtmcursor implements a crosshair with the ability to set and measure values from x or y datums.
    %
    % Syntax:
    %    obj = dtmcursor(hAxes)%
    %
    % Description:
    %     Datum Cursor (DTMCURSOR) provides a graphical evaluation tool that
    % produces a crosshair that displays its location in a figure annotation
    % for quick inspection of data values. The user can define x and/or y
    % datums and measurement lines to calculate and annotate differences. The
    % difference is displayed either above or to the right of each measurement
    % line.
    %
    % Initialization Input:
    %     hAxes = the handle of the axes for the dtmcursor to be implemented on
    %
    % Output:
    %     an instance of this class
    %
    % Examples:
    %     plot(1:100)
    %     dtmcursor(gca)
    %
    % Keypress Actions:
    %     Shift+x: Set the X-axis Datum (a dashed vertical line)
    %           x: Creates a X-axis measurement line (a dotted vertical line) and
    %              displays its Delta_x from the X-axis Datum.
    %     Shift+y: Set the Y-axis Datum (a dashed horizontal line)
    %           y: Creates a Y-axis measurement line (a dotted horizontal line) and
    %              displays its Delta_y from the Y-axis Datum.
    %     Shift+r: Clears all datum and measurement lines (and their texts)
    %              off of the axes
    %           r: Clears only the measurement lines off of the axes
    %     Shift+n: Toggles the SnapToNearest feature on/off
    %     Shift+c: Toggles the Crosshair lines and SnapToNearest feature on/off 
    %
    % Bugs and suggestions:
    %    Please send to jonathan lister (mechanical.engineer78 at gmail.com)
    %
    % Change log:
    %    2013-07-16: first version
    %    2013-07-18: added properties ycolor, scalefactor, SnapToNearest
    %    2013-07-22: Fixed bug that kept delta-y measurents from having a
    %                background color the same color as the line being
    %                measured. Added keyboard command Shift+n to toggle the
    %                SnapToNearest feature on/off
    %    2013-07-29: Changed labels to use TargetAxes' color
    %                Added Shift+n and Shift+c Keypress commands
    %                Y delta labels now show their association with a line
    %                   by their edge and font color instead of background
    %                Datums and measurement lines can now be moved by the
    %                   mouse.
    %
    % Todo: Implement property-value pair input and checking
    
    properties
        TargetAxes
        TargetFig
        Color          = 'r'
        Label
        LabelFormatStr = '%10s = %10.4f %10s = %10.4f'
        yline
        ydatum         = []
        ymline         = []
        xline
        xdatum         = []
        xmline         = []
        cbstack        = []
        SnapToNearest  = true;
        scalefactor    = {1 0 'x' 'y'}; %used when utilizing add'l Y axes
        ycolor         = 'w';
        
    end
    
    methods
        function this = dtmcursor(varargin)
            this.TargetAxes = varargin{1};
            this.TargetFig = ancestor(this.TargetAxes,'figure');
            
            %get the axes limits
            x_lim = xlim(this.TargetAxes);
            y_lim = ylim(this.TargetAxes);
            
            %place crosshair in the middle of the axes intitially
            x1 = mean(x_lim);
            y1 = mean(y_lim);
            
            %draw the vertical portion of the crosshair
            this.xline = line([x1 x1],y_lim, ...
                'Color',this.Color,...
                'EraseMode','xor',...
                'tag','xhvl');
            
            %draw the horizontal portion of the crosshair
            this.yline = line(x_lim,[y1 y1],...
                'Color',this.Color,...
                'EraseMode','xor',...
                'tag','xhhl');
            
            th = text(x_lim(1),y_lim(1),sprintf(this.LabelFormatStr,x1,y1),...
                'BackgroundColor','none','tag','xhlbl');
            set(th,'Units','normalized')
            pos = get(th,'Position');
            set(th,'Position',pos - [0 .07 0])
            this.Label = th;     
            
            %set callbacks that enable functionality
            set(this.TargetFig,'WindowButtonMotionFcn',@this.Update,...
                'KeyPressFcn',@this.KeyPressFcn,...
                'WindowButtonDownFcn',@this.BtnDwnFcn,...
                'WindowButtonUpFcn',@this.BtnUpFcn);
        end
        
        function Update(this,varargin)
            try                
                %prevent axes from resizing
                ax = axis(this.TargetAxes);               
                axis(ax);
                
                cp = get(this.TargetAxes,'CurrentPoint');
                xval = cp(1,1);
                yval = cp(1,2);
                x_lim = xlim(this.TargetAxes);
                y_lim = ylim(this.TargetAxes);
                
                if this.isOverAxes                                      
                    if this.SnapToNearest
                        [xdata, ydata, dx, dy, color] = this.parseLines;
                        cp = get(this.TargetAxes,'CurrentPoint');
                        [~,xval,yval,c] = closestpoint(cp(1,1:2),xdata,ydata,dx,dy,color);
                        this.ycolor = c;
                    end
                    
                    sf = {1 0 'X' 'Y'};
                    xparam = sf{3};
                    yparam = sf{4};
                    
                    set(this.xline,...
                        'XData',[xval xval],...
                        'YData',y_lim)
                    
                    set(this.yline,...
                        'XData',x_lim,...
                        'YData',[yval yval])
                    
                    
                    yscaled = (yval-sf{2})/sf{1};
                    set(this.Label,....
                        'string',sprintf(this.LabelFormatStr,xparam,xval,yparam,yscaled));
                end
                
            catch err %#ok<*NASGU>
                
                this.exit
                
            end
        end
        
        function KeyPressFcn(this,varargin)
            %Keypress funciton
            try
            if this.isOverAxes
                cc = get(this.TargetFig,'CurrentCharacter');
                switch cc
                    case 'Y'
                        
                        x = get(this.yline,'xdata');
                        y = get(this.yline,'ydata');
                        
                        if isempty(this.ydatum)
                            %draw y datum (a horizontal line)
                            this.ydatum = line(x,y,'Color',this.Color,...
                                'LineStyle','-',...
                                'LineWidth',2,...
                                'tag','ydatum');
                            ac = get(this.TargetAxes,'Color');
                            th = text(x(end),y(1),'Y',...
                                'tag','ydtmtxt',...
                                'BackgroundColor',ac,...
                                'EdgeColor',this.Color);
                            units = get(th,'units');
                            set(th,'Units','normalized')
                            pos = get(th,'Position');
                            set(th,'Position',pos + [0.02 0 0])
                            set(th,'units',units)
                            set(this.ydatum,'UserData',{th});
                        else
                            % update datum location on plot
                            set(this.ydatum,'XData',x,'YData',y)
                            ud = get(this.ydatum,'UserData');
                            th = ud{1};
                            pos = get(th,'Position');
                            pos(2) = y(1);
                            set(th,'Position',pos);
                        end
                        
                        if ~isempty(this.ymline)
                            %redo all the y-delta's
                            this.redoDY                            
                        end
                        
                    case 'y' %add y measurment (a horizontal line)
                        if ~isempty(this.ydatum)
                            x = get(this.yline,'xdata');
                            y = get(this.yline,'ydata');
                            yd = get(this.ydatum,'ydata');
                            mlines = this.ymline;
                            
                            mline = line(x,y,'Color',this.Color,...
                                'LineStyle','-',...
                                'LineWidth',2,...                               
                                'tag','ymline');
                            
                            mlines = [mlines,mline];
                            this.ymline = mlines;
                            sf = this.scalefactor;
                            delta = y(1) - yd(1);
                            delta = delta/sf{1};
                            
                            ac = get(this.TargetAxes,'Color');
                               
                            th = text(x(end),y(end),['$\Delta_y = ' num2str(delta) '$'],...
                                'Interpreter','Latex',...
                                'EdgeColor',this.ycolor,...
                                'Color',this.ycolor,...
                                'BackgroundColor',ac,...
                                'tag','mtext',...
                                'FontSize',14);
                            
                            set(mline,'UserData',{th, sf, this.ycolor})

                        else
                            msgbox(['Please set the y-datum first.'
                                'Type shift + y to set it.    '])
                        end
                        
                    case 'X'
                        
                        x = get(this.xline,'xdata');
                        y = get(this.xline,'ydata');
                        
                        if isempty(this.xdatum)
                            % draw vertical datum line
                            this.xdatum = line(x,y,'Color',this.Color,...
                                'LineStyle','-',...
                                'LineWidth',2,...
                                'tag','xdatum');
                            ac = get(this.TargetAxes,'Color');
                            th = text(x(1),y(end),'X',...
                                'tag','xdtmtxt',...
                                'BackgroundColor',ac,...
                                'EdgeColor',this.Color);
                            units = get(th,'units');
                            set(th,'Units','normalized')
                            pos = get(th,'Position');
                            set(th,'Position',pos + [0 .04 0])
                            set(this.xdatum,'UserData',th)
                            set(th,'units',units)
                        else
                            % update datum location on plot
                            set(this.xdatum,'XData',x,...
                                'YData',y,...
                                'EdgeColor',this.Color,...
                                'LineWidth',2)
                            th = get(this.xdatum,'UserData');
                            pos = get(th,'Position');
                            pos(1) = x(1);
                            set(th,'Position',pos);
                        end
                        
                        
                        if ~isempty(this.xmline)
                            %redo all the x-delta's
                            this.redoDX
                        end
                        
                    case 'x' %add horizontal measurment (a vertical line)
                        if ~isempty(this.xdatum)
                            x = get(this.xline,'xdata');
                            y = get(this.xline,'ydata');
                            xd = get(this.xdatum,'xdata');
                            mlines = this.xmline;
                            mline = line(x,y,'Color',this.Color,...
                                'LineStyle','-',...
                                'LineWidth',2,...
                                'tag','xmline');
                            mlines = [mlines,mline];
                            this.xmline = mlines;
                            delta = x(1) - xd(1);
                            y_lim = ylim(this.TargetAxes);
                            y_add = .03*(y_lim(2) - y_lim(1));
                            ac = get(this.TargetAxes,'Color');
                            th = text(x(end),y(end)+y_add,...
                                ['$\Delta_x = ' num2str(delta) '$'],...
                                'Interpreter','Latex',...
                                'EdgeColor',this.Color,...
                                'BackgroundColor',ac,...
                                'tag','mtext',...
                                'FontSize',14);
                            set(mline,'UserData',th)
                        else
                            msgbox(['Please set the x-datum first.'
                                'Type shift + x to set it.    '])
                        end
                        
                    case 'r'
                        a = findall(this.TargetFig,'Tag','xmline');
                        b = findall(this.TargetFig,'Tag','ymline');
                        c = findall(this.TargetFig,'Tag','mtext');
                        delete([a; b; c])
                        this.xmline = [];
                        this.ymline = [];
                    case 'R'
                        a = findall(this.TargetAxes,'Tag','xmline');
                        b = findall(this.TargetAxes,'Tag','ymline');
                        c = findall(this.TargetAxes,'Tag','mtext');
                        d = findall(this.TargetAxes,'Tag','ydatum');
                        e = findall(this.TargetAxes,'Tag','xdatum');
                        f = findall(this.TargetAxes,'Tag','xdtmtxt');
                        g = findall(this.TargetAxes,'Tag','ydtmtxt');
                        delete([a; b; c; d; e; f; g])
                        this.xmline = [];
                        this.ymline = [];
                        this.ydatum = [];
                        this.xdatum = [];
                    case 'N'
                        %toggle snap to nearest on or off
                        if this.SnapToNearest
                            %it was on turn it off
                            this.SnapToNearest = false;
                        else
                            %it was off, turn it on
                            this.SnapToNearest = true;
                        end
                    case 'C'
                        %toggle cursor display on/off
                        %also toggle SnapToNearest
                        vis = get(this.xline,'Visible');
                        if strcmpi(vis,'on')
                            set([this.xline this.yline],'visible','off')
                            this.SnapToNearest = false;
                        else
                            set([this.xline this.yline],'visible','on')
                            this.SnapToNearest = true;
                        end
                            
                end
            end
            catch err
                this.exit
            end
            
        end
        
        function redoDX(this,varargin)
            if ~isempty(this.xmline)
                %redo all the delta's
                n = numel(this.xmline);
                lines = this.xmline;
                xd = get(this.xdatum,'XData');
                for i=1:n
                    th = get(lines(i),'UserData');
                    x = get(lines(i),'XData');
                    delta = x(1) - xd(1);
                    set(th,'String',['$\Delta_x = ' num2str(delta) '$'])
                end
            end        
        end
        
        function redoDY(this,varargin)
            if ~isempty(this.ymline)
                %redo all the delta's
                n = numel(this.ymline);
                lines = this.ymline;
                yd = get(this.ydatum,'YData');
                for i=1:n
                    ud = get(lines(i),'UserData');
                    th = ud{1};
                    sf = ud{2};
                    y = get(lines(i),'YData');
                    delta = (y(1) - yd(1))/sf{1};

                    set(th,'String',['$\Delta_y = ' num2str(delta) '$'])
                end

            end            
        end
        
        function bool = isOverAxes(this,varargin)
            %test to see if pointer is in axes bounds
            % inspired by dualcursor or crosshair
            try
                set(this.TargetFig,'units','normalized');
                axpos = get(this.TargetAxes,'position');
                figcp = get(this.TargetFig,'Currentpoint');
                axlim = axpos(1) + axpos(3);
                aylim = axpos(2) + axpos(4);
                bool = true;
                if or(figcp(1) > (axlim+.01), figcp(1) < (axpos(1)-.01)),
                    bool = false;
                elseif or(figcp(2) > (aylim+.01), figcp(2) < (axpos(2)-.01)),
                    bool = false;
                end
            catch err
                this.exit
            end
            
        end
        
        function exit(this,varargin)
            try %#ok<TRYNC>
                a = findall(this.TargetFig,'Tag','xmline');
                b = findall(this.TargetFig,'Tag','ymline');
                c = findall(this.TargetFig,'Tag','mtext');
                d = findall(this.TargetFig,'Tag','ydatum');
                e = findall(this.TargetFig,'Tag','xdatum');
                f = findall(this.TargetAxes,'Tag','xdtmtxt');
                g = findall(this.TargetAxes,'Tag','ydtmtxt');
                delete([a; b; c; d; e; f; g])
                
                try
                    delete(this.xline)
                    delete(this.yline)
                    delete(this.Label)
                catch err
                    % do nothing
                end
                
                %             stack = this.cbstack;
                %             stack.revert('prior to init');
                delete(this)
            end
        end
        
        function BtnDwnFcn(this,varargin)
            %inspired by dualcursor 
            obj = gco;
            tag = get(obj,'Tag');
            if ~isempty(tag)
                switch tag
                    case {'mtext','xhlbl'}
                        set(obj,'EraseMode','xor')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.dragText)
                    case {'xmline','xdatum'}
                        set(obj,'EraseMode','xor')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.dragLine)
                        this.redoDX
                    case {'ymline','ydatum'}
                        set(obj,'EraseMode','xor')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.dragLine)
                        this.redoDY
                end
            end
        end
        
        function BtnUpFcn(this,varargin)
            %inspired by dualcursor
            obj = gco;
            tag = get(obj,'Tag');
            if ~isempty(tag)
                switch tag
                    case {'mtext','xhlbl'}
                        set(obj,'EraseMode','Normal')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.Update)
                        
                    case {'xmline','xdatum'}
                        set(obj,'EraseMode','Normal')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.Update)
                        this.redoDX
                        
                    case {'ymline','ydatum'}
                        set(obj,'EraseMode','Normal')
                        set(this.TargetFig,'WindowButtonMotionFcn',@this.Update)
                        this.redoDY
                end
            end
        end
        
        function dragText(this,varargin)
            %inspired by dualcursor
            co = gco;
            if ~isempty(co)
                tag = get(co,'Tag');
                switch tag
                    case {'mtext','xhlbl'}
                        axh = this.TargetAxes;
                        cp = get(axh,'CurrentPoint');
                        pt = cp(1,[1 2]);
                        
                        %Put into normalized units
                        ax = axis;
                        axis(ax); %keep from resizing
                        lim = localObjbounds(axh);
                        ax(isinf(ax)) = lim(isinf(ax));
                        
                        pt(1) = (pt(1) - ax(1))/(ax(2)-ax(1));
                        pt(2) = (pt(2) - ax(3))/(ax(4)-ax(3));
                        
                        units = get(co,'Units'); %save units
                        set(co,'Units','normalized','Position', [pt 0])
                        set(co,'Units',units) %restore units
                        drawnow
                end
                
            end
        end
        
        function dragLine(this,varargin)
            axh = this.TargetAxes;
            cp = get(axh,'CurrentPoint');
            co = gco;
            if ~isempty(co) && this.isOverAxes
                tag = get(co,'Tag');
                switch tag
                    case {'ymline','ydatum'}
                        %drag horizontal line
                        y = cp(1,2);
                        set(co,'YData',[y y])
                        ud = get(co,'UserData');
                        th = ud{1};
                        pos = get(th,'Position');
                        pos(2) = y;
                        set(th,'Position',pos)
                        drawnow
                    case {'xmline','xdatum'}
                        %drag vertical line
                        x = cp(1,1);
                        set(co,'XData',[x x])
                        th = get(co,'UserData');
                        pos = get(th,'Position');
                        pos(1) = x;
                        set(th,'Position',pos)
                        drawnow
                end
                
            end
        end 
        
        function [xdata, ydata, dx, dy, color] = parseLines(this,varargin)
            %inspired by D'Errico's selectdata
         
            % save this to restore later
            axissize = axis;
            
            % make sure the axes stay fixed
            axis(axissize)
            
            % find closest point on line
            
            % extract xdata and ydata from the specified axes
            % get the children of the axes
            % modified for TIGER and use of findobj
            try
            hc = findobj(this.TargetAxes,'type','line',...
                         '-not','Tag','xmline',...
                         '-not','Tag','ymline',...
                         '-not','Tag','ydatum',...
                         '-not','Tag','xdatum',...
                         '-not','Tag','xhhl',...
                         '-not','Tag','xhvl');
            catch err
                hc = gca;
            end
            
            % strip out xdata and ydata of each line (produces cell arrays)
            xdata = get(hc,'xdata');
            ydata = get(hc,'ydata');
            color = get(hc,'color');
            
            %nolines = size(hc,1);
            
            %if it is a double I want it to be a cell of 1X1 containing the double
            if isa(xdata,'double')
                xdata  = {xdata'};
                ydata  = {ydata'};
                color  = {color};
                %sfdata = {sfdata'};
            end
            
            % dx, dy to scale the distance
            dx = (axissize(2) - axissize(1));
            dy = (axissize(4) - axissize(3));
            
        end
        
    end %Methods
    
end % Class

%-----------------------------------------------------------------------------------------------------
% Helper functions

%% from selectdata by D'Errico
function [pointslist,xselect,yselect,c] = closestpoint(xy,xdata,ydata,dx,dy,color)
% find the single closest point to xy, in scaled units
if ~iscell(xdata)
    % just one line/curve to consider
    D = sqrt(((xdata - xy(1))/dx).^2 + ((ydata - xy(2))/dy).^2);
    [junk,pointslist] = min(D(:)); %#ok
    xselect = xdata(pointslist);
    yselect = ydata(pointslist);
    c = color;
else
    % there is more than one line/curve
    Dmin = inf;
    pointslist = cell(size(xdata));
    for i = 1:numel(xdata);
        D = sqrt(((xdata{i} - xy(1))/dx).^2 + ((ydata{i} - xy(2))/dy).^2);
        [mind,ind] = min(D(:));
        
        if mind < Dmin
            % searching for the closest point
            Dmin = mind;
            
            pointslist = ind;
            xselect = xdata{i}(ind);
            yselect = ydata{i}(ind);
                  c = color{i};
        end
    end
end

end % subfunction end


%% from dualcursor by Scott Hirsch
function lim = localObjbounds(axh)
% Get x limits of all data in axes axh
kids = get(axh,'Children');
xmin = Inf; xmax = -Inf;
ymin = Inf; ymax = -Inf;
for ii=1:length(kids)
    try % Pass through if can't get data.  hopefully we hit at least one
        xd = get(kids(ii),'XData');
        xmin = min([xmin min(xd(:))]);
        xmax = max([xmax max(xd(:))]);
        
        yd = get(kids(ii),'YData');
        ymin = min([ymin min(yd(:))]);
        ymax = max([ymax max(yd(:))]);
    catch err %#ok<*NASGU>
        
    end
end
% fail safe option, in case things went really bad
xmin(xmin==Inf) = 0;  xmax(xmax==-Inf) = 1;
ymin(ymin==Inf) = 0;  ymax(ymax==-Inf) = 1;

lim = [xmin xmax ymin ymax];
end

Contact us