5

Taking an example from SO, I'd like to adapt the axis ticks dependent on the current view. This is the default behavior, unless one sets a self defined number of ticks,

The resulting behavior is demonstrated in the picture below. On the left side the default behavior, on the right side the figure with self defined ticks. When rotating the plot with the self defined Z ticks, their number won't be adapted to the currently available space (see right bottom figure).

Is there a simple, general solution for that without some fancy things like getting current angle by camva()? I'd wish to not scale the data itself, since it's a big dataset, but to use self defined ticks and tick-labels. It needs to work with MATLAB Version: 8.0.0.783 (R2012b).

enter image description here

Code

%# sample graph vertices and edges (similar to your data)
[adj,XYZ] = bucky;
[r, c] = find(adj);
edges = [r c];      %# M-by-2 matrix holding the vertex indices
points = XYZ';      %# 3-by-N matrix of points X/Y/Z coordinates

%# build a list of separated lines
e = edges';
e(end+1,:) = 1;
e = e(:);
p = points(:,e);
p(:,3:3:end) = NaN;

figure
line(p(1,:), p(2,:), p(3,:));
view(3)

% Now the same with self defined ticks
figure
line(p(1,:), p(2,:), p(3,:));
view(3)
z = points(3, :);
fac = 3.14159265359;
tickstep = (max(z)-min(z))/9;
ticklabels_ = min(z):tickstep:max(z);
set(gca, 'ZTick', ticklabels_)
set(gca, 'ZTickLabel', sprintf('%.3f|',ticklabels_))
Community
  • 1
  • 1
embert
  • 7,336
  • 10
  • 49
  • 78
  • 1
    Usually you would write a callback such as in these answers http://stackoverflow.com/questions/4940561/does-matlab-execute-a-callback-when-a-plot-is-zoomed-resized-redrawn but I'm not sure how you adjust the view and which functions and their callbacks are available to you. – bdecaf Oct 30 '14 at 07:35
  • @embert: what do you want to happen in such case (when the plot is rotated as in the bottom right example with custom tick marks)? Do you want it to revert to automatic tick spacing and labels? If not how do you want to specify the manual spacing? – Amro Nov 02 '14 at 16:54

2 Answers2

6

As indicated by @bdecaf in the comments, you could write an event handler for when the z-ticks change, in order to customize the tick labels accordingly. This is using undocumented functionality.

This is convenient because we still let MATLAB automatically decide on the best number of ticks to use depending on the screen space available for the axis, we just customize the format of the labels displayed.

Example:

function customize_ticks_example()
    % data
    [adj,XYZ] = bucky;
    [r,c] = find(adj);
    edges = [r c];
    points = XYZ';
    e = edges';
    e(end+1,:) = 1;
    e = e(:);
    p = points(:,e);
    p(:,3:3:end) = NaN;

    % plot
    hFig = figure;
    line(p(1,:), p(2,:), p(3,:), ...
        'LineWidth',2, 'Marker','.', 'MarkerSize',20);
    ax = handle(gca);
    view(3), grid on, box on
    xlabel x, ylabel y, zlabel z
    rotate3d on

    % listen to changes on ZTick property
    ev = handle.listener(ax, findprop(ax,'ZTick'), ...
        'PropertyPostSet', @onZTickChange);
    setappdata(hFig, 'my_ztick_listener', ev);
end

function onZTickChange(~,e)
    % get the new 'ZTick', and format them as needed
    labels = num2str(e.NewValue(:),'%g $');

    % update the 'ZTickLabel'
    set(double(e.AffectedObject), 'ZTickLabel',labels)
end

animation

Of course the labels don't have to be numerical, you could use any custom labels as long as you have some kind of scale along which you can interpolate values to figure what label to use.


EDIT:

an easier way to set the event listener:

ev = addlistener(gca, 'ZTick', 'PostSet', @onZTickChange);

(in this case, there is no need to store ev inside the GUI with setappdata. This syntax bind the listener to the lifecycle of the GUI object).


EDIT 2:

In response to comments, here is another example:

function example_manual_ticks
    %% some 3d plot
    hFig = figure;
    sphere
    view(3), grid on, box on
    xlabel x, ylabel y, zlabel z

    %% create a hidden copy of the axis
    hax1 = gca;
    hax2 = copyobj(hax1, hFig);
    set(hax2, 'Visible','off', 'Color','none', 'HitTest','off', ...
        'XLimMode','manual', 'YLimMode','manual', 'ZLimMode','manual')
    delete(get(hax2, 'Children'))
    uistack(hax2, 'bottom')

    % sync axes on 3d rotation
    hlink = linkprop([hax1,hax2], {'CameraPosition','CameraUpVector'});
    setappdata(hax1, 'my_axes_linkprop', hlink);
    rotate3d on

    % respnd to changes in ZTick axis property
    ev = addlistener(hax2, 'ZTick', 'PostSet',@(o,e) onZTickChange(o,e,hax1));

    %% animation
    el = 90 .* sin(linspace(0,2*pi,100) + asin(1/3));
    for n=1:numel(el)
        view(-37.5, el(n))
        drawnow
    end
end
function onZTickChange(~,e,ax)
    % determine number of ticks
    num_ticks = numel(e.NewValue);
    new_num_ticks = num_ticks - 1;

    % interpolate new ticks along the axis limits
    limits = get(ax, 'ZLim');
    zticks = linspace(limits(1), limits(2), new_num_ticks);
    zticks_labels = num2str(zticks(:), '%.2f ($)');

    % update ticks
    set(ax, 'ZTick',zticks, 'ZTickLabel',zticks_labels)
    drawnow
end

animation2

The idea is to create a hidden copy of the axis, kept in sync with the original one on 3D rotation. This second axis will have automatic tick marks, while we customize the ticks of first axis as we wish (similar to before we register a callback on the hidden axis for when the ticks change).

Again I'm using this hidden axis as a way to let MATLAB deal with the task of determining the best number of ticks given the view (this is a lot easier than messing with camera angles and matrix transformations in order to find out the length of the projected axis in pixels).

In the example above, I'm simply setting the number of ticks one less than the automatic number ({2,4,10} instead of {3,5,11}), but you could use any kind of mapping you want.

Community
  • 1
  • 1
Amro
  • 123,847
  • 25
  • 243
  • 454
  • some relevant links about plot ticks: http://www.mathworks.com/matlabcentral/answers/92565-how-do-i-control-axis-tick-labels-limits-and-axes-tick-locations, http://www.mathworks.com/matlabcentral/answers/103188-how-do-i-format-tick-labels – Amro Nov 03 '14 at 00:13
  • the addlistener syntax is working, amro. handle.listener did not in my matlab version. Do you also have an idea of how to control how matlab arranges the ticks? For example in matplotlib (a python package) there are [locators](http://matplotlib.org/api/ticker_api.html) which can be used. Your animation for instance is using 1, 3, 5 or 11 ticks. Could that be changed to e. g. {2, 4, 6, 8}? – embert Nov 03 '14 at 06:39
  • `handle.listener` uses undocumented UDD system, I thought that worked in every version (perhaps not the latest R2014b with HG2 ?). Anyway the regular `addlistener` is equivalent, so using that is also fine... – Amro Nov 03 '14 at 17:27
  • as for controlling the number of ticks like that, I don't think MATLAB support such customization, not without getting deep into camera properties and projection transformations (the problem is that there is no way to get the length of the axis side in pixels from the current rotated view, which is needed if we want to use some rule for determining number of ticks, say like 1 z-tick mark every 10 vertical pixels)... Anyway there is a workaround, see my new example. – Amro Nov 03 '14 at 17:31
  • Unfortunatelly this does not work for 2014b+. After searching, I found [Yair commenting on this issue here](http://undocumentedmatlab.com/blog/setting-axes-tick-labels-format). I tried the provided [FEX](http://www.mathworks.com/matlabcentral/fileexchange/36254-ticklabelformat) using a `ticks = eventData.Source.Tick` and `eventData.Source.TickLabel = formatted_ticks` syntax in the callback function supplied to the `ticklabelformat` function of the FEX, but it does not work properly. Hard to guess what's a reliable way to it now. Any experience with HG2 already, Amro? – embert Jan 23 '15 at 14:12
0

In 2015b the PostSet property is not supported anymore (https://de.mathworks.com/matlabcentral/answers/231377-problem-with-addlistener-to-xlim).

If the size is changed manually the WindowMouseRelease and SizeChanged properties can be used. I've adapted the code from https://stackoverflow.com/a/26705913/2295776, but the animation would not work in this example anymore:

% Create sphere
figure1 = figure;
axes1 = axes('Parent',figure1);
hold(axes1,'on');
sphere;
view(axes1, 3);

% create a hidden copy of the axis
axesCopy = copyobj(axes1, figure1);

% sync axes on 3d rotation
Link = linkprop([axes1, axesCopy],{'CameraUpVector', 'CameraPosition', 'CameraTarget', 'XLim', 'YLim', 'ZLim'});
setappdata(figure1, 'StoreTheLink', Link);
rotate3d on

set(axes1, 'Visible','off', 'Color','none', ...
        'XLimMode','auto', 'YLimMode','auto', 'ZLimMode','auto')
set(axesCopy, ...
        'XLimMode','manual', 'YLimMode','manual', 'ZLimMode','manual')
delete(get(axesCopy, 'Children'))
grid(axesCopy,'on');
uistack(axesCopy, 'bottom')

% respond to changes in XTick and YTick axis properties
addlistener(figure1,'WindowMouseRelease', @(src, evnt) onTickChange(axes1, axesCopy));
addlistener(figure1,'SizeChanged', @(src, evnt) onTickChange(axes1, axesCopy));
onTickChange(axes1, axesCopy);

%% functions
function onTickChange(axes, axesCopy)
    zticks_orig = zticks(axes);
    ZTickLabels = num2str(zticks_orig(:), '%.2f ($)');

    % update ticks
    set(axesCopy, 'ZTick', zticks(axes), 'ZTickLabel', ZTickLabels);
    drawnow;
end
Martin Stolpe
  • 135
  • 1
  • 6