Loren on the Art of MATLAB

Turn ideas into MATLAB

Axes Limits – Scream Louder, I Can’t Hear You! 6

Posted by Loren Shure,

Today's guest post comes from Sean de Wolski, one of Loren's fellow Application Engineers. You might recognize him from MATLAB answers and the pick of the week blog!

I arrived at work early one day and immediately got a call from Brett, who doesn't face the same commute I do, wondering why his listeners were not working on axes' 'XLim', 'YLim' and 'ZLim'.

This seemed a little surprising, surely a listener pointed at the wrong property? Here's a minimal working example

% Plot something
ax = gca;
% Add listener to axes' 'XLim' 'PostSet' event (so it fires after axes
% change).  Update the title so it's a visible change.
addlistener(ax,'XLim','PostSet',@(~,~)title('Listener Fired'));

% Change Plot

Huh? No Title! What happened?

What's interesting is that it allowed me to set the listener on the 'XLim'. Usually, if a property is not 'SetObservable', it would error.

    % Add listener to non-SetObservable property.
    addlistener(ax,'TightInset','PostSet',@(~,~)disp('hello world'))
catch ME
While adding a PostSet listener, property 'TightInset' in class 'matlab.graphics.axis.Axes' is not defined to be SetObservable.

So what's happening?

When MATLAB versions R2014b and newer are automatically calculating the limits, the limits frequently change several times as MATLAB inspects each member of the axes and updates the limits. Allowing listeners to be triggered during the automatic calculation (even if no listener was attached) would have caused performance degradation for all plot updates even if there are no listeners. Thus the ability to trigger listeners with automatically calculated property value changes was disabled for performance reasons.

So how do we listen for limit changes?

The answer I provided to Brett was to write my own events class that has events for each of the limits changing.

classdef LimitsChangedNotifier < handle
% This class has events of limits changes to be used for updating when axes
% limits change.

        % Events for each axes limit property

Now every time I update the plot, or call x/y/zlim('auto'), I would notify afterward.

% Build the events class
limnotifier = LimitsChangedNotifier;

% Attach a listener to it
addlistener(limnotifier,'XLimChanged',@(~,~)title('Listener Fired'));

% Draw a new plot and let any listeners know you did

Note: By doing this, I retain control of letting the listeners know the limits changed. This wouldn't work if the user did this on their own, say within plottools.

For a more detailed and advanced explanation of events and listeners, please see this documentation page.

Get the MATLAB code

Published with MATLAB® R2015b


Comments are closed.

6 CommentsOldest to Newest

Adam replied on : 1 of 6
Very interesting. Is there a list anywhere of which properties are observable (or rather can have listeners attached) in builtin graphics classes like axes? Since I was interested I had a quick mess around with the metaclass and this does give a list of properties that are 'SetObservable' though it is a little messy. This list contained 165 properties though (I also filtered out ones without public set access so there were slightly more), some of which I have never heard of. But it does also contain 'XLim', 'YLim' etc. So as your code and comments above suggest, these properties are actually SetObservable but cannot have listeners attached. I've only ever really used listeners with custom classes rather than graphics objects, but they would be useful assuming I wouldn't keep running into examples where the properties appear to allow listeners, but actually don't!
Yaroslav replied on : 2 of 6
Hi, The offered solution seems to be a bit overkill. Defining a whole class—just to manually notify a listener—may result in performance issues, code obfuscation, design complexity, etc. A simpler way (since we are doing it manually anyways) would be to call xlim: % Change Plot plot(sin(1:0.1:100)) xlim(ax.XLim); % <-- calling xlim invokes the XLim listener This will invoke the listener without changing the new x-axis limits. Kind regards,
Brett replied on : 3 of 6
Sean, Thanks for the interesting post. I have to admit, I'm still a bit puzzled though. If I have to _manually_ trigger a callback with a |notify| command, that seems no more useful than a |disp(ax.XLim)| call. In fact, in your example above, the limnotifier has no association with _ax_, and has no notion of the current value of the XLim of ax. (And as Yaroslav points out, calling ax.XLim explicitly will successfully trigger the desired action anyway.) So two things: * How do I trigger a callback without an explicit call to notify or ax.XLim (by plotting a different range of data, for instance)? * How do I get that callback to reflect the current value of the axes XLim, so I can do something like this: plot(humps); ax = gca; addlistener(ax,... 'XLim','PostSet',... @(~,x) title(sprintf('XLIM: [%d, %d]',x.AffectedObject.XLim))); and trigger the title to update with a plot(ax,1:10) command the same way that it would with a call to ax.XLim = [1,10]; ? Brett
Sean de Wolski replied on : 4 of 6
@Adam: As far as I know, there is no comprehensive list. I'll put in an enhancement request for that. @Yaroslav: You're right, setting
ax.XLim = ax.XLim
does fire the listener because 'AbortSet' is not true for the 'XLim' property. Thus, your solution is probably better (!). It also takes care of Brett's second question below. @Brett: 1) To my knowledge, this is not currently possible. 2) You can always create your own event data for any event you make. Here's an example class that I would use with my above example: Custom Event Data Class
classdef (ConstructOnLoad) CustomEventData < event.EventData
% CustomEventData class
% More information here:
% https://www.mathworks.com/help/releases/R2015b/matlab/matlab_oop/events-and-listeners--syntax-and-techniques.html#brb6i_k    
        function obj = CustomEventData(AffectedObject,Message)
            obj.AffectedObject = AffectedObject;
            obj.Message = Message;
And using it:
% Build the events class
limnotifier = LimitsChangedNotifier;

% Attach a listener to it

% Draw a new plot and let any listeners know you did
notify(limnotifier,'XLimChanged',CustomEventData(gca,'Updated With plot()'))
  CustomEventData with properties:

    AffectedObject: [1x1 Axes]
           Message: 'Updated With plot()'
            Source: [1x1 LimitsChangedNotifier]
         EventName: 'XLimChanged
Brett replied on : 5 of 6
Sean, In my (procedural) brain, if I have to “notify” the listener that something is happening, it’s not really “listening” in the sense that I want it to. And I don’t necessarily want to “make” events. I want to call events that are pre-made, and have those calls trigger an action that I can define, and that is aware of the effects of the event. Moreover, the plot thickens: In your blog discussion, you say that XLim- and YLim- listeners were disable for performance reasons; hence this does not trigger the callback: plot(1:10);plot(1:100); But consider this (two puzzles in one!): clear;close all; ax = gca; addlistener(ax,'XLim','PostSet',@(~,~)title('Listener Fired')); imshow('peppers.png') pause(1) title('') pause(1) imshow('peppers.png') Why does the listener fire in the first place, and why does it re-fire in the second? Curiouser and curiouser!
bshoelso replied on : 6 of 6
@Adam: I'm also unaware of any pre-built lists of listenable properties. However, this snippet should allow you to query listenability: function [observable,nonObservable] = getListenable(hndl) % Return an alphabetical list of Observable properties in handle HNDL % % Brett Shoelson, PhD % brett.shoelson@mathworks.com % 11/11/2015 % %%% EXAMPLE Property Listener: % f = figure; % d = dir; % a = addlistener(f,... % 'ApplicationData','PostSet',... % @(~,~) disp('appdatachanged')); %%% This triggers the callback: % f.ApplicationData.test = d; %%% This does not. WHY? % setappdata(f,'test',d) % Copyright 2015 The MathWorks, Inc. tmp = metaclass(hndl); properties = tmp.PropertyList; names = {properties(:).Name}'; observability = {properties(:).SetObservable}'; observability = [observability{:}]'; observable = sort(names(observability)); if nargout > 1 nonObservable = sort(names(~observability)); end Hope that helps! Brett