classdef tmark < hgsetget
%TMARK Tickmark annotation class for 2-D plots.
%
%Usage: t = tmark([x,y],varargin)
% t = tmark(ginput(1),'color','r','label','Peak 1')
%
%Tmarks are user positioned annotation objects whos position's remain fixed
%in axes units when plot axes are rescaled (unlike standard annotation
%objects whos positions are not updated as a result of axis rescaling).
%Tmarks are intended to be used to label features like peak positions in
%spectroscopic data. The functionality is like the "Pin to Axes" function
%except tmarks are available from the command line and in scripts. Tmarks
%can be created either from the command line or interactively using
%tmulti.m. Once created tmarks properties may be edited via either a
%context menu (right click on the mark) or via the commaqnd line using the
%utility routines tedit.m, tfind.m and trestore.m, or using the set
%formalism as for other types of handle object.
%
%Known Limitations:
% Currently when a figure containing tmarks is saved to disk and then
%reloaded the tmarks become 'dormant' (i.e. they will no longer be
%repositioned as a result of axis rescaling). The utility routine
%trestore.m will revive the tmarks subject to some limitations - in
%particular tmarks in figures containing more than one subplot may end up
%being attached to the wrong axis. See the help info in trestore.m for
%details and a workaround for this problem.
%
%Example
%
% %Plot some data
% x = 0:0.01:2*pi;
% plot(x,sin(x))
% ylim([-1.5 1.5])
%
% %Add a tmark from the command line.
%
% tm = tmark([pi,sin(pi)],'color','r','label','Tmark')
%
% %Modify the settings for the mark. Editing is also available by right
% %clicking on the mark with the mouse.
%
% set(tm,'resolution',0.001) %increase the resolution
% set(tm,'textonly',1) %display only the text label
%
% %Create marks interactively on the current figure using ginput. Left click
% %to create marks. Press escape to exit.
%
% tm = tmulti %tm will be a cell array of tmark handles.
%
%End example
%
%
%Required Inputs
%===============
% pos .................... [x,y] coordinate of mark in experimental units
%
%Optional Inputs
%===============
% label .................. Text label to be displayed in addition to the
% x-coordinate of the mark.
% resolution ............. The numeric resolution of the displayed
% position. For examples, a resolution of 1
% will display the position rounded to the
% nearest integer, a value of 0.02 will give the
% position rounded to the nearest 0.02. When
% tmarks are created they script picks the
% initial resolution based upon the xaxis range
% as reported by xlim.
% textonly ............... If set to 1 only the label text is displayed
% (no x-axis value). Default = 0 (label and
% x position displated).
% color .................. The color of the mark.
% fontsize ............... Font size for the mark.
% hdlAx .................. The handle of theaxis to which the tick mark
% is attached.
%
%Outputs
%=======
% TM ..................... Tickmark object (tmark)
%
%See also: tedit tfind trestore tmulti
% $Author: jcaspar $
% $Date: 2009/11/06 23:33:47 $
% $Revision: 1.6 $
properties (Hidden)
locked = 0; %used to prevent access of multiple listeners at same time
listenerX %handle of listener for x-axis change
listenerY %handle of listener for y-axis change
string = ''
arrowx
arrowy
linestyle = '-';
textrotation = 90;
verticalalignment = 'middle';
horizontalalignment = 'center';
end
properties (SetAccess = private)
hdl %handle of annotation object
end
properties (SetAccess = public)
hdlAx %handle of axis to which mark is attached
pos = [0,0]; %mark location in real x,y coordinates (from ginput)
label = ''; %label text
color = [0,0,0];
resolution = 1;
textonly = 0;
fontsize = 9;
end
events
end
methods
%Class constructor
function TM = tmark(pos,varargin)
TM.hdlAx = [];
if ~isempty(get(0,'currentfigure'))
if ~isempty(get(get(0,'currentfigure'),'currentaxes'))
TM.hdlAx = gca;
end
end
TM.pos = pos;
%confirm active axis
if ~isempty(varargin)
for k=1:2:size(varargin,2)
switch lower(varargin{k})
case 'axis'
TM.hdlAx = varargin{k+1};
end
end
end
if isempty(TM.hdlAx)
%reloading data
return
end
%figure out default resolution
TM.resolution = 10.^floor(log10(diff(xlim(TM.hdlAx))/100));
if ~isempty(varargin)
for k=1:2:size(varargin,2)
switch lower(varargin{k})
case 'resolution'
TM.resolution = varargin{k+1};
case 'color'
TM.color = varargin{k+1};
case 'label'
TM.label = varargin{k+1};
case 'axis'
TM.hdlAx = varargin{k+1};
case 'textonly'
TM.textonly = varargin{k+1};
if TM.textonly == 1
TM.textrotation = 0;
TM.linestyle = 'none'
else
TM.textrotation = 90;
TM.linestyle = '-';
end
case 'fontsize'
TM.fontsize = varargin{k+1};
end
end
end
%create uicontextmenu
mnu = uicontextmenu;
m1 = uimenu(mnu,'label','Resolution ');
m11 = uimenu(m1,'label','10',...
'callback',@(src,evt)tmarkresolution(gco,10));
m12 = uimenu(m1,'label','1',...
'callback',@(src,evt)tmarkresolution(gco,1));
m13 = uimenu(m1,'label','0.1',...
'callback',@(src,evt)tmarkresolution(gco,0.1));
m14 = uimenu(m1,'label','0.01',...
'callback',@(src,evt)tmarkresolution(gco,0.01));
m15 = uimenu(m1,'label','Other',...
'callback',@(src,evt)tmarkresolution(gco));
m2 = uimenu(mnu,'label','Color');
m21 = uimenu(m2,'label','Black','foregroundcolor','k',...
'callback',@(src,evt)tmarkcolor(gco,'k'));
m22 = uimenu(m2,'label','Red','foregroundcolor','r',...
'callback',@(src,evt)tmarkcolor(gco,'r'));
m23 = uimenu(m2,'label','Green','foregroundcolor',[0,0.75,0],...
'callback',@(src,evt)tmarkcolor(gco,[0,0.75,0]));
m24 = uimenu(m2,'label','Blue','foregroundcolor','b',...
'callback',@(src,evt)tmarkcolor(gco,'b'));
m25 = uimenu(m2,'label','Cyan','foregroundcolor','c',...
'callback',@(src,evt)tmarkcolor(gco,'c'));
m26 = uimenu(m2,'label','Magenta','foregroundcolor','m',...
'callback',@(src,evt)tmarkcolor(gco,'m'));
m27 = uimenu(m2,'label','Yellow','foregroundcolor','y',...
'callback',@(src,evt)tmarkcolor(gco,'y'));
m28 = uimenu(m2,'label','Custom','foregroundcolor','k',...
'callback',@(src,evt)tmarkcolor(gco,uisetcolor));
m3 = uimenu(mnu,'label','Fontsize');
m31 = uimenu(m3,'label',' 7',...
'callback',@(src,evt)tmarkfontsize(gco,7));
m32 = uimenu(m3,'label',' 9',...
'callback',@(src,evt)tmarkfontsize(gco,9));
m33 = uimenu(m3,'label','10',...
'callback',@(src,evt)tmarkfontsize(gco,10));
m34 = uimenu(m3,'label','12',...
'callback',@(src,evt)tmarkfontsize(gco,12));
m35 = uimenu(m3,'label','14',...
'callback',@(src,evt)tmarkfontsize(gco,14));
m36 = uimenu(m3,'label','16',...
'callback',@(src,evt)tmarkfontsize(gco,16));
m9 = uimenu(mnu,'label','Font...',...
'callback',@(src,evt)myfont(gco));
m4 = uimenu(mnu,'label','Label',...
'callback',@(src,evt)tmarkrelabel(gco));
m5 = uimenu(mnu,'label','Text only',...
'callback',@(src,evt)tmarktxtonly(src,gco));
m6 = uimenu(mnu,'label','Reposition');
m61 = uimenu(m6,'label','Cursor',...
'callback',@(src,evt)tmarkmove(gco,'cursor'));
m62 = uimenu(m6,'label','Numeric',...
'callback',@(src,evt)tmarkmove(gco,'numeric'));
% m7 = uimenu(mnu,'label','Restore ','separator','on',...
% 'callback',@(src,evt)mnurestore(gco));
m8 = uimenu(mnu,'label','Delete','separator','on',...
'callback',@(src,evt)delete(gco));
TM = MakeArrows(TM);
TM = MakeLabel(TM);
TM.hdl = annotation(get(TM.hdlAx,'parent'),...
'textarrow',TM.arrowx,TM.arrowy,...
'string',TM.string,...
'textrotation',TM.textrotation,...
'linestyle',TM.linestyle,...
'color',TM.color,...
'fontsize',TM.fontsize,...
'tag','tmark',...
'horizontalalignment',TM.horizontalalignment,...
'verticalalignment',TM.verticalalignment,...
'headstyle','none',...
'uicontextmenu',mnu);
local_modifyuserdata(TM.hdl,'tmark',TM) %store tmark for future access
set(TM.hdl,'deleteFcn',@(src,evt)tmark.deleteMark(TM));
TM.listenerX = addlistener(TM.hdlAx, 'XLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
TM.listenerY = addlistener(TM.hdlAx, 'YLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
end
%Restore functionality of a tmark reloaded from disk
function TM = tmarkrestore(TM,hdl,hdlAx)
TM.hdl = hdl;
TM.hdlAx = hdlAx;
TM.listenerX = addlistener(TM.hdlAx, 'XLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
TM.listenerY = addlistener(TM.hdlAx, 'YLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
end
%Set commands
function TM = set.pos(TM,value)
TM.pos = value;
if ~isempty(TM.hdl)
TM = MakeArrows(TM);
set(TM.hdl,'x',TM.arrowx,'y',TM.arrowy);
TM = MakeLabel(TM);
set(TM.hdl,'string',TM.string);
end
end
function TM = set.hdlAx(TM,value)
TM.hdlAx = value;
end
function TM = set.label(TM,value)
TM.label = value;
if ~isempty(TM.hdl)
TM = MakeLabel(TM);
set(TM.hdl,'string',TM.string)
end
end
function TM = set.color(TM,value)
TM.color = value;
if ~isempty(TM.hdl)
set(TM.hdl,'color',value,'textcolor',value)
end
end
function TM = set.linestyle(TM,value)
TM.linestyle = value;
if ~isempty(TM.hdl)
set(TM.hdl,'linestyle',value)
end
end
function TM = set.horizontalalignment(TM,value)
TM.horizontalalignment = value;
if ~isempty(TM.hdl)
set(TM.hdl,'horizontalalignment',value)
end
end
function TM = set.verticalalignment(TM,value)
TM.verticalalignment = value;
if ~isempty(TM.hdl)
set(TM.hdl,'verticalalignment',value)
end
end
function TM = set.textrotation(TM,value)
TM.textrotation = value;
if ~isempty(TM.hdl)
set(TM.hdl,'textrotation',value)
end
end
function TM = set.resolution(TM,value)
TM.resolution = value;
if ~isempty(TM.hdl)
TM = MakeLabel(TM);
set(TM.hdl,'string',TM.string);
end
end
function TM = set.textonly(TM,value)
TM.textonly = value;
if value == 1
TM.textrotation = 0;
TM.linestyle = '-';
TM.verticalalignment = 'bottom';
else
TM.textrotation = 90;
TM.linestyle = '-';
TM.verticalalignment = 'middle';
end
if ~isempty(TM.hdl)
TM = MakeLabel(TM);
set(TM.hdl,'string',TM.string);
end
end
function TM = set.fontsize(TM,value)
TM.fontsize = value;
if ~isempty(TM.hdl)
set(TM.hdl,'fontsize',TM.fontsize);
end
end
function TM = set(TM,varargin)
if ~isempty(varargin)
for k=1:2:size(varargin,2)
switch lower(varargin{k})
case 'pos'
TM.pos = varargin{k+1};
case 'hdlax'
TM.hdlAx = varargin{k+1};
delete(TM.listenerX);
delete(TM.listenerY);
TM.listenerX = addlistener(TM.hdlAx, 'XLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
TM.listenerY = addlistener(TM.hdlAx, 'YLim', 'PostSet',...
@(src,evt)tmark.axisrescale(TM,src,evt));
case 'label'
TM.label = varargin{k+1};
if ~isempty(TM.hdl)
set(TM.hdl,'string',TM.string) %???
end
case 'color'
TM.color = varargin{k+1};
case 'resolution'
TM.resolution = varargin{k+1};
case 'textonly'
TM.textonly = varargin{k+1};
case 'fontsize'
TM.fontsize = varargin{k+1};
end
end
end
end
%Class destructor
function delete(TM)
try delete(TM.listenerX);catch;end
try delete(TM.listenerY);catch;end
try delete(TM.hdl);catch;end
end
function sobj = saveobj(obj)
sobj.hdl = [];
sobj.hdlAx = [];
sobj.locked = 0;
sobj.listenerX = [];
sobj.listenerY = [];
sobj.string = obj.string;
sobj.arrowx = obj.arrowx;
sobj.arrowy = obj.arrowy;
sobj.linestyle = obj.linestyle;
sobj.textrotation = obj.textrotation;
sobj.pos = obj.pos;
sobj.label = obj.label;
sobj.color = obj.color;
sobj.resolution = obj.resolution;
sobj.textonly = obj.textonly;
sobj.fontsize = obj.fontsize;
end
end
methods (Static)
%Load tmark from disk. Due to dependence of tmark on handles to
%graphic objects, a reloaded tmark is not fully functional. Use
%tmarkrestore (or utility trestore.m) to re-enable loaded tmarks.
function sobj = loadobj(obj)
sobj = tmark(obj.pos,'axis',[]);
sobj.hdlAx = [];
sobj.locked = 0;
sobj.listenerX = [];
sobj.listenerY = [];
sobj.string = obj.string;
sobj.arrowx = obj.arrowx;
sobj.arrowy = obj.arrowy;
sobj.linestyle = obj.linestyle;
sobj.textrotation = obj.textrotation;
sobj.label = obj.label;
sobj.color = obj.color;
sobj.resolution = obj.resolution;
sobj.textonly = obj.textonly;
sobj.fontsize = obj.fontsize;
end
%relocate tmark as a result of axis rescaling
function axisrescale(TM,src,evt) %#ok<INUSD>
while TM.locked == 1
%wait until object is free
end
TM.locked = 1; %lock it
try
TM = MakeArrows(TM);
set(TM.hdl,'x',TM.arrowx,'y',TM.arrowy);
TM.locked = 0;
catch ME
TM.locked = 0;
getReport(ME)
disp('error in axis rescale')
end
end
%handle deletion when user deletes the annotation mark directly
function deleteMark(TM)
try delete(TM.listenerX);catch;end
try delete(TM.listenerY);catch;end
try delete(TM);catch;end
end
end
end
%===================================================================
%Utility functions
%===================================================================
%Convert experimental coords to annotation units
function TM = MakeArrows(TM)
[ax,ay] = local_dsxy2figxy(TM.hdlAx, TM.pos(1), TM.pos(2));
ax(2) = ax(1);
ay(2) = ay(1) + 0.03;
ay = fliplr(ay);
TM.arrowx = ax;
TM.arrowy = ay;
end
%Convert label to final string for display
function TM = MakeLabel(TM)
xtext = num2str(local_round2n(TM.pos(1),TM.resolution),'%g');
if isempty(TM.label)
%create text for mark using only the peak position
n = numel(xtext) + 2;
s = char(ones(1,(2*n)+1)*32);%space padding
TM.string = sprintf('%s%s',s,xtext);
else
%label includes additional text
if TM.textonly == 0
%text and peak position in label
n = numel(TM.label) + numel(xtext) + 1;
s = char(ones(1,(2*n) + 5)*32);%space padding
TM.string = sprintf('%s%s (%s)',s,TM.label,xtext);
else
%use labe only with no position info
n = numel(TM.label) + numel(xtext) + 1;
s = char(ones(1,2*n)*32);%space padding
s = '';
TM.string = sprintf('%s%s',s,TM.label);
end
end
end
%Edit the label via context menu
function tmarkrelabel(h)
ud = get(h,'userdata');
txtin = get(ud.tmark,'label');
prompt = {'Enter peakmark label:'};
dlg_title = 'Re-Label Peakmark';
num_lines = 1;
def = {txtin};
txtin = inputdlg(prompt,dlg_title,num_lines,def);
if isempty(txtin)
%cancel
return
end
set(ud.tmark,'label',txtin{1});
end
%Edit the label position via context menu
function tmarkmove(h,mtd)
ud = get(h,'userdata');
switch mtd
case 'cursor'
[x,y] = ginput(1);
case 'numeric'
pos = get(ud.tmark,'pos');
prompt = {'Enter X Position:','Enter Y Position:'};
dlg_title = 'Relocate Peakmark';
num_lines = 1;
def = {num2str(pos(1)),num2str(pos(2))};
answer = inputdlg(prompt,dlg_title,num_lines,def);
if isempty(answer)
%cancel
return
end
x = str2num(answer{1});
y = str2num(answer{2});
end
set(ud.tmark,'pos',[x,y]);
end
%Restore functionality of a tmark in a figure reloaded from disk
function mnurestore(h)
%this routine is not active - too unreliable due to bug described below.
l = legend;
if ~isempty(l)
%legend is on; legend interferes with restoration of tmarks because
%for unknown reasons the legend becomes the gca. Workaround is to
%turn off the legend. This trick does not work if there are
%multiple subplots on the figure.
legend('off');
end
ud = get(h,'userdata');
hdlAx = get(gcf,'currentaxes');
tmarkrestore(ud.tmark,h,hdlAx);
if ~isempty(l)
legend('show');
end
end
%Edit the fontsize via context menu
function tmarkfontsize(h,fsize)
ud = get(h,'userdata');
set(ud.tmark,'fontsize',fsize);
end
%Edit the font settings via context menu
function myfont(h)
ud = get(h,'userdata');
f = uisetfont;
if ~isstruct(f)
return
end
set(h,'fontname',f.FontName);
set(h,'fontweight',f.FontWeight);
set(h,'fontangle',f.FontAngle);
set(ud.tmark,'fontsize',f.FontSize);
end
%Edit the display resolution via context menu
function tmarkresolution(h,res)
ud = get(h,'userdata');
if ~ismember('res',who)
%get value from user
def = num2str(get(ud.tmark,'resolution'));
prompt = {'Enter resolution:'};
dlg_title = 'Set Resolution';
num_lines = 1;
def = {def};
answer = inputdlg(prompt,dlg_title,num_lines,def);
if isempty(answer)
%cancel
return
end
res = str2num(answer{1});
end
set(ud.tmark,'resolution',res);
end
%Edit the color via context menu
function tmarkcolor(h,markcolor)
ud = get(h,'userdata');
set(ud.tmark,'color',markcolor);
end
%Edit the text only property via context menu
function tmarktxtonly(src,h)
ud = get(h,'userdata');
c = get(src,'checked');
if strcmp(c,'off')
set(src,'checked','on');
set(ud.tmark,'textonly',1);
else
set(src,'checked','off');
set(ud.tmark,'textonly',0);
end
end
%Coordinate transform
function varargout = local_dsxy2figxy(varargin)
% dsxy2figxy -- Transform point or position from axis to figure coords
% Transforms [axx axy] or [xypos] from axes hAx (data) coords into coords
% wrt GCF for placing annotation objects that use figure coords into data
% space. The annotation objects this can be used for are
% arrow, doublearrow, textarrow
% ellipses (coordinates must be transformed to [x, y, width, height])
% Note that line, text, and rectangle anno objects already are placed
% on a plot using axes coordinates and must be located within an axes.
% Usage: Compute a position and apply to an annotation, e.g.,
% [axx axy] = ginput(2);
% [figx figy] = getaxannopos(gca, axx, axy);
% har = annotation('textarrow',figx,figy);
% set(har,'String',['(' num2str(axx(2)) ',' num2str(axy(2)) ')'])
%
% Based on an old version of the dsxy2figxy.m which is a Matlab
% example located at:
%
% \Matlab\R209b\help\techdoc\creating_plots\examples\dsxy2figxy.m
%
% Code has been modified by JVC to correctly treat logarithmic x and y-axis
% scales and x-axes plotted in reverse mode (i.e. high-to-low). Never
% got around to fixing reversed y-axes yet.
% Obtain arguments (only limited argument checking is performed).
% Determine if axes handle is specified
if length(varargin{1})== 1 && ishandle(varargin{1}) && ...
strcmp(get(varargin{1},'type'),'axes')
hAx = varargin{1};
varargin = varargin(2:end);
else
hAx = gca;
end;
% Parse either a position vector or two 2-D point tuples
if length(varargin) == 1 % Must be a 4-element POS vector
pos = varargin{1};
else
[x,y] = deal(varargin{:}); % Two tuples (start & end points)
end
% Get limits
axun = get(hAx,'Units');
set(hAx,'Units','normalized'); % Need normaized units to do the xform
axpos = get(hAx,'Position'); %left,bottom, width, height
axlim = axis(hAx); % Get the axis limits [xlim ylim (zlim)]
axwidth = diff(axlim(1:2));
axheight = diff(axlim(3:4));
%Transform data from figure space to data space
if exist('x','var') % Transform a and return pair of points
if strcmp(get(hAx,'xscale'),'linear')
if strcmp(get(hAx,'xdir'),'reverse')
varargout{1} = (axlim(2) - x)*axpos(3)/axwidth + axpos(1)
else
% ((x - xmin) * width) / (width + left)
varargout{1} = (x - axlim(1))*axpos(3)/axwidth + axpos(1);
end
else
varargout{1} = (log10(x/axlim(1)) / log10(axlim(2)/axlim(1))) * axpos(3) + axpos(1);
end
if strcmp(get(hAx,'yscale'),'linear')
varargout{2} = (y-axlim(3))*axpos(4)/axheight + axpos(2);
else
varargout{2} = (log10(y/axlim(3)) / log10(axlim(4)/axlim(3))) * axpos(4) + axpos(2);
end
else % Transform and return a position rectangle
pos(1) = (pos(1)-axlim(1))/axwidth*axpos(3) + axpos(1);
pos(2) = (pos(2)-axlim(3))/axheight*axpos(4) + axpos(2);
pos(3) = pos(3)*axpos(3)/axwidth;
pos(4) = pos(4)*axpos(4)/axheight;
varargout{1} = pos;
end
% Restore axes units
set(hAx,'Units',axun)
end
%Utility to get userdata from peak mark
function local_modifyuserdata(hdl,myfield, mydata)
%modifyuserdata Update userdata for an object without destroying existing contents
%
%Usage: modifyuserdata(hdl,myfield, mydata)
% modifyuserdata(hdl,myfield,'-delete')
%
%Inputs
%======
% hdl .................... Handle(s) for userdata
% myfield ................ Field to update. If userdata contains the
% named field its value is modified to the
% specified value. If the field does not exist
% it is created and set to the specified value.
% mydata ................. Data to store. If this argument is set to
% '-delete' then the specified field is removed
% from userdata. Requesting deletion of a field
% which does not exist will leave userdata
% unchanged.
%
%Outputs
%=======
% None
% $Author: jcaspar $
% $Date: 2009/11/06 23:33:47 $
% $Revision: 1.6 $
for k=1:numel(hdl)
u = get(hdl(k),'userdata');
if isfield(u,myfield)
%existing field, need to update
if strcmp(mydata,'-delete')
%remove field
u = rmfield(u,myfield);
else
u.(myfield) = mydata;
end
else
%new field
if strcmp(mydata,'-delete')
%asking to delete field, so do not create it
else
u.(myfield) = mydata;
end
end
set(hdl(k),'userdata',u)
end
end
%Utility function to round to specified display resolution
function r = local_round2n(x,m)
%round2n Generalized rounding function. Rounds to closest multiple of m.
%
%Usage: r = round2n(x,m)
% r = round2n(x,3) %round x to nearest multiple of 3
% r = round2n(x,0.05) %round to nearest multiple of 0.05
%
%Required Inputs
%===============
% x ...................... Value to be rounded
% m ...................... Multiple to round to. If m=1 or if m is
% absent the function defaults to standard
% rounding (round)
%
%Outputs
%=======
% r ...................... Rounded output
%
%See also: round
% $Author: jcaspar $
% $Date: 2009/11/06 23:33:47 $
% $Revision: 1.6 $
if nargin == 1
%simple rounding
r = round(x);
return
end
r = m .* round(x ./ m);
end