Have you ever tried to customize the way in which tick labels appear in Matlab plot axes?
For example, setting the numerical precision of the labels, or adding some short descriptive text (for example, the units)? If you have, then I bet that you have encountered the following dilemma: Once we modify the tick labels (for discussion sake, let’s assume the Y axis, so this is done by updating the YTickLabel property), then the corresponding YTickLabelMode property changes from ‘auto’ to ‘manual’ and loses its relationship to the tick values (YTick). So, if we now zoom or pan the plot, our new labels remain unchanged although the tick values have changed, causing much panic and frustration… If we also set the tick values manually, this solves that problem but leaves us with another: now, when we zoom or pan, we no longer see any ticks or tick labels at all!
Of course, we can always trap the zoom and pan callback functions to update the tick labels dynamically while keeping the tick values automatically. This will work for these cases, but we need to do it separately for zoom and pan. Also, if we modify the axes limits explicitly (via the corresponding YLim property) or indirectly (by modifying the displayed plot data), then the callbacks are not called and the labels are not updated.
The solution – using a property change listener
A better way to solve this problem is to simply trap changes to the displayed tick values, and whenever these occur to call our dedicated function to update the labels according to the new tick values. This can be done by using UDD, or more precisely the ability to trap update events on any property (in our case, YTick). Such a mechanism was already demonstrated here in 2010, as one way to achieve continuous slider feedback. The idea is to use the built-in handle.listener function with the PropertyPostSet event, as follows:
hhAxes = handle(hAxes); % hAxes is the Matlab handle of our axes hProp = findprop(hhAxes,'YTick'); % a schema.prop object hListener = handle.listener(hhAxes, hProp, 'PropertyPostSet', @myCallbackFunction); setappdata(hAxes, 'YTickListener', hListener); |
Note that we have used setappdata to store the hListener
handle in the axes. This ensures that the listener exists for exactly as long as the axes does. If we had not stored this listener handle somewhere, then Matlab would have immediately deleted the listener hook and our callback function would not have been called upon tick value updates. Forgetting to store listener handles is a common pitfall when using them. If you take a look at the addlistener function’s code, you will see that it also uses setappdata after creating the listener, for exactly this reason. Unfortunately, addlistsner cannot always be used, and I keep forgetting under which circumstances, so I generally use handle.listener directly as above: It’s simple enough to use that I find I never really need to use the simple addlistener wrapper, but you are welcome to try it yourself.
That’s all there is to it: Whenever YTick changes its value(s), our callback function (myCallbackFunction) will automatically be called. It is quite simple to set up. While we cannot use TeX in tick labels yet (this will change in the upcoming HG2), using sprintf formatting does enable quite a bit of flexibility in formatting the labels. For example, let’s say I want my tick labels to have the format ‘%.1fV’ (i.e., always one decimal, plus the Volts units):
function myCallbackFunction(hProp,eventData) %#ok - hProp is unused hAxes = eventData.AffectedObject; tickValues = get(hAxes,'YTick'); newLabels = arrayfun(@(value)(sprintf('%.1fV',value)), tickValues, 'UniformOutput',false); set(hAxes, 'YTickLabel', newLabels); end % myCallbackFunction |
Handling duplicate tick labels
Of course, ‘%.1fV’ may not be a good format when we zoom in to such a degree that the values differ by less than 0.1 – in this case all the labels will be the same. So let’s modify our callback function to add extra decimals until the labels become distinct:
function myCallbackFunction(hProp,eventData) %#ok - hProp is unused hAxes = eventData.AffectedObject; tickValues = get(hAxes,'YTick'); %newLabels = arrayfun(@(value)(sprintf('%.1fV',value)), tickValues, 'UniformOutput',false); digits = 0; labelsOverlap = true; while labelsOverlap % Add another decimal digit to the format until the labels become distinct digits = digits + 1; format = sprintf('%%.%dfV',digits); newLabels = arrayfun(@(value)(sprintf(format,value)), tickValues, 'UniformOutput',false); labelsOverlap = (length(newLabels) > length(unique(newLabels))); % prevent endless loop if the tick values themselves are non-unique if labelsOverlap && max(diff(tickValues))< 16*eps break; end end set(hAxes, 'YTickLabel', newLabels); end % myCallbackFunction |
ticklabelformat
Based on a file that I received from an anonymous reader a few years ago, I have prepared a utility called ticklabelformat that automates much of the set-up above. Feel free to download this utility and modify it for your needs – it’s quite simple to read and follow. The usage syntax is as follows:
ticklabelformat(gca,'y','%.6g V') % sets y axis on current axes to display 6 significant digits ticklabelformat(gca,'xy','%.2f') % sets x & y axes on current axes to display 2 decimal digits ticklabelformat(gca,'z',@myCbFcn) % sets a function to update the Z tick labels on current axes ticklabelformat(gca,'z',{@myCbFcn,extraData}) % sets an update function as above, with extra data |
Addendum for HG2 (R2014b or newer releases)
As I’ve indicated in my comment response below, in HG2 (R2014b or newer) we need to listen to the HG object’s MarkedClean
event, rather than to a property change event.
I’ve updated my ticklabelformat utility accordingly, so that it works with both HG1 (pre-R2014a) and HG2 (R2014b+). Use this utility as-is, and/or look within its source code for the implementation details.
This is fantastic! I had long since given up hope of finding a solution to this problem. Thanks very much.
Brilliant!
Very useful in 3D rotordynamics graphs when
axis normal;
gives ugly results. Thank you very much 😉
Yair,
Have you looked at how MATLAB works with scientific notation format of tick labels putting the exponent only into one place on the axes? The XTickLabels are only what one can see there without exponent. Where MATLAB hides the exponent? Is there a way to change it? to enforce this format? I know only way with manually changing the ticks and adding exponent as a text object. As in this question, for example:
http://stackoverflow.com/questions/13326894/force-exponential-format-of-ticks-like-matlab-does-it-automatically
Any undocumented staff?
@Yuri – I do not know of a way to do this with the current HG system. HG axes have some related hidden properties (ExpFontAngle, ExpFontName, ExpFontSize, ExpFontStrikeThrough, ExpFontUnderline, ExpFontUnits, ExpFontWeight) but they don’t appear to have any effect as far as I can tell (maybe someone could find out).
I believe that I saw some configurable exponent properties in the new HG2. I haven’t tested HG2 on the latest releases lately, so try it (using the -hgVersion2 startup switch as mentioned in the article) and let us know what you discover.
p.s. – wow, has it been 2 full years already? – it’s about time HG2 got released…
@Yuri – as followup, customizing the exponent is indeed possible in HG2, as I have recently shown.
Just found this post yesterday and really liked it. Using this method, you can avoid the stupid identical labels that appear when you zoom in real close in a large plot. Also, the ticklabelformat function from matlabcentral can be modified quite easily to scale the tick values, which can be extremely helpful. Thanks, Yair.
I always use this method for MATLAB 2013B, it works fine.
However, it does not work for 2014b, the latest released version.
@Damayi – This is a known issue that is due to the changes in property listeners that were done for the new HG2 graphics engine in R2014b.
Do you have a resolution for this problem?
@Damayi – Yes, in HG2 we need to listen to the HG object’s
MarkedClean
event, rather than to a property change event.I’ve updated my ticklabelformat utility accordingly, so that it works with both HG1 (pre-R2014a) and HG2 (R2014b+). Use this utility as-is, and/or look within its source code for the implementation details.
Yair,
thanks for providing this kind of input and the ticklabelformat. I’m also thankful for the comments as I had no idea why the callbacks for axis properties do not work anymore with HG2. Already in HG1 some things are confusing, since somehow the event argument is of different type, depending on…well I don’t really know. But in HG2 addlistener does somehow not work on those properties.
Would it be possible to provide a fully working example callback function for HG2 (maybe in the docstring of the ticklabelformat function)?. The one given above apparently does not work (only tested with HG2). The second argument has only two properties: .Source and something else. I tried to access the ticks via – I think it was – .Source.Tick, but it did not work properly and even crashed MATLAB. All my old callbacks do not work anymore with the new HG2. Sad, that TWM does not supply easy instructions on updating customized tick labels as this is quite a substantial thing, when examining data using a plot and the pan/zoom functionality. Thanks.
Dear Yair,
do you know a way to change the XTickLabel vertical position, a.k.a its distance from the x-axis line? The generally suggested solution is to recreate the labels using the text() command, but there should be a simpler way to do it!
Do you have any suggestion?
Thanks,
Sandor
YEEEESSSS !!! It’s many time that i searched this way to have MORE digit in the graphs !!!! THAAANK YOU !!!!
Yair,
Would it be possible to add an “update” section by the blurb on the listener to describe the change with HG2? I’m specifically thinking something that references MarkedClean and YRuler. I’m fine but it might be nice for others …
Thanks,
Jim
@Jim – done 🙂
I wonder – is it possible to use images as axis tick labels?
Ron
@Ron – Tick labels are strings. If you want to use images, then set the ticklabels to empty (i.e., do not show any ticks) and programmatically add images at the relevant [negative] axes locations based on the computed tick positions. You will need to update the images and their location whenever the axes resizes or zooms or pans or changes its ticks in whichever way (details). To be honest, I’m not sure that it’s worth all this effort…
Do you know of a way to change the position of one of the tick labels so that it does not overlap with the tick labels from the neighbouring axes?
Thanks