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