Using LimitsChangedFcn to sync axes of different x-scales

6 views (last 30 days)
Spencer Chen
Spencer Chen on 18 Aug 2021
Edited: Adam Danz on 28 Mar 2022
Trying out the newly added LimitsChangedFcn callback in the XAxis of Axes objects in R2021a. I'm running into callback re-entry problems that I need some help with.
I want to sync the xlim of multiple axes with different x-scales such that when zoom and pan are executed, all axes shift and scale correspondingly. I attached a sample script of what I'm trying to achieve. This script sets up so that the bottom axis shift and scale accordingly to zoom and pan on the top axis.
The problem occurs when I want to set up so that zoom and pan of the bottom axis will also update the top axis accordingly. That is with the following line uncommeted from my script:
% ax(2).XAxis.LimitsChangedFcn = {@syncxlim, ax};
With both axes set up to update each other on LimitsChangedFcn callback, they infinitely re-activate the callback. That is change in ax(1) activates the callback to update limits on ax(2), which then activates the callback again to update limits on ax(1), etc, etc, etc.
Any suggestions on how I can get this working?
Thanks, heaps.
Blessings,
Spencer
  1 Comment
Adam Danz
Adam Danz on 28 Mar 2022
Hi Spencer, I just ran into your question today (7 months later) and I enjoy the topic. You may have figured out a solution by now but I've added perhaps a different solution.

Sign in to comment.

Answers (1)

Adam Danz
Adam Danz on 28 Mar 2022
Edited: Adam Danz on 28 Mar 2022
> Sync the xlim of multiple axes with different x-scales such that when zoom and pan are executed all axes shift and scale correspondingly
This solution uses the scaling logic used in this answer but extends it to affect all axes which requires adding another step to prevent recursion. To escape recursion, when the current xlims match the computed xlims within a threshold of error, the xlims setting is stopped.
Critically, the updated xlim calculations use the original/initial xlim data to maintain scales rather using the current xlim values. This prevents the gradual accumulation of error.
For some background info on LimitChangedFcn, see this Community Highlight.
Generate figure
Adapted from OP's code, this uses 3 axes with different x-axis ranges.
figure;
tiledlayout(3,1);
ax(1) = nexttile;
plot(ax(1),1:0.1:11, rand(1,101));
xline(ax(1),6,'m-','LineWidth',2)
axis(ax(1),'tight') % for demo purposes only
ax(2) = nexttile;
plot(ax(2),0:0.5:20, rand(1,41));
xline(ax(2),10,'m-','LineWidth',2)
axis(ax(2),'tight') % for demo purposes only
ax(3) = nexttile;
plot(ax(3),50:50:1000, rand(1,20));
xline(ax(3),525,'m-','LineWidth',2)
axis(ax(3),'tight') % for demo purposes only
% Render plots before triggering callback
drawnow();
Store the original x-limits and x-limit-ranges. This must be done after setting the initial desired xlimits for the axes.
% Original xlims
xLimits = vertcat(ax.XLim);
% Original x axis ranges
xRanges = diff(xLimits,1,2);
ax(1).XAxis.LimitsChangedFcn = {@syncxlim, ax(:), xLimits, xRanges};
ax(2).XAxis.LimitsChangedFcn = {@syncxlim, ax(:), xLimits, xRanges};
ax(3).XAxis.LimitsChangedFcn = {@syncxlim, ax(:), xLimits, xRanges};
Define the LimitsChangedFcn
function syncxlim(src, event, axs, xLimits, xRanges)
% Responds to changes to x-axis limits in axes listed in axs.
% Updates xlims to maintain original scales.
% axs: nx1 vector of axes handles
% xLimits: nx2 matrix of original [min,max] xlims
% xRanges: nx1 vector of original axis ranges
% Index of axes that just changed
axIdx = axs == src.Parent;
% Compute the new xlims for axes that weren't just changed
normLowerLim = (event.NewLimits(1) - xLimits(axIdx,1)) / xRanges(axIdx);
newLowerLimits = normLowerLim * xRanges(~axIdx) + xLimits(~axIdx,1);
newUpperLimits = newLowerLimits + diff(event.NewLimits) .* xRanges(~axIdx)./ xRanges(axIdx);
newXLimits = [newLowerLimits, newUpperLimits];
% Only update if the new XLimits significantly differ from current xlims
allCurrentXLims = cell2mat(get(axs(~axIdx),'xlim'));
if any(abs(allCurrentXLims - newXLimits) > (1E-8 * xRanges(~axIdx)),'all')
set(axs(~axIdx), {'xlim'}, mat2cell(newXLimits, ones(sum(~axIdx),1), 2))
drawnow
pause(0.05) % needed to prevent reentry
end

Products


Release

R2021a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!