0

The below code example is modified from the basis on Matplotlib: Finding out xlim and ylim after zoom

Basically, in that example, I want to have a dual x axis; and I want the second (doubled) x axis to behave as the original one. So, the link uses a callback to do that. However:

  • While I only pan the plot, everything is fine - the dual copy is synchronized to the original X axis
  • Once I zoom - something goes wrong, and apparently, the wrong range is applied to the dual axis - EVEN IF when printing the ranges in the callback, gives the expected range for the drawing after the zoom
  • Once I start dragging after zoom, the two axes are dragged correspondingly - however, since the initial condition from the previous step is wrong, so is this part
  • Now, I've added an extra call to the callback, on button release - so once I release the button from the drag move motion, THEN the two axes get synchronized again ??!!

Here is an animated gif (Matplotlib 3.1.1, Python 3.7.4 on MSYS2/MINGW64 on Windows 10):

enter image description here

I truly, truly don't understand this. How come, when I just pan/drag the plot, ax.get_xlim() gives the right numbers, and ax22.set_xlim() applies them - but when I zoom the plot, ax.get_xlim() gives the right numbers, but ax22.set_xlim() does NOT apply them (or rather, applies them, but late - that is, applies the data from previous request in the current request) ????!!! What is this sorcery?!

And how can I get the dual ax22 to be synchronized with the original ax axis, regardless if I drag or zoom in interactive mode?

The code:

#!/usr/bin/env python3

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt

#
# Some toy data
x_seq = [x / 100.0 for x in range(1, 100)]
y_seq = [x**2 for x in x_seq]

#
# Scatter plot
fig, ax = plt.subplots(1, 1)
ax.plot(x_seq, y_seq)

# https://stackoverflow.com/questions/31803817/how-to-add-second-x-axis-at-the-bottom-of-the-first-one-in-matplotlib
ax22 = ax.twiny() # instantiate a second axes that shares the same y-axis
# Move twinned axis ticks and label from top to bottom
ax22.xaxis.set_ticks_position("bottom")
ax22.xaxis.set_label_position("bottom")
# Offset the twin axis below the host
ax22.spines["bottom"].set_position(("axes", -0.06))

ax22.set_xlim(*ax.get_xlim())

#
# Declare and register callbacks
def on_xlims_change(axes):
  print("updated xlims: ", ax.get_xlim())
  ax22.set_xlim(*ax.get_xlim())

ax.callbacks.connect('xlim_changed', on_xlims_change)
fig.canvas.mpl_connect('button_release_event', on_xlims_change)

#
# Show
plt.show()

sdbbs
  • 4,270
  • 5
  • 32
  • 87
  • 1
    You don't need any callback. Just leave it out and it works fine. That is, both axes are zoomed simultaneously. – ImportanceOfBeingErnest Oct 16 '19 at 11:49
  • Thanks @ImportanceOfBeingErnest - you're right for this specific case; however, I chose a callback, because ultimately I want to have the second axis show a different range, as in https://stackoverflow.com/questions/58409802 - and if I just do `newxlim = (ax.get_xlim()[0]*655, ax.get_xlim()[1]*655) ; ax22.set_xlim(*newxlim)`, then the relationship between axes will not persist after a zoom without a callback. I chose a callback with both axes showing the same range here simply to illustrate that there is a problem. – sdbbs Oct 16 '19 at 12:05
  • 1
    Even if the axes have different limits it should still work without callback?! The problem is that if the axes is zoomed *and* in addition you set the limits you will "overzoom". – ImportanceOfBeingErnest Oct 16 '19 at 12:16
  • Thanks @ImportanceOfBeingErnest - that helped a lot! It turns out, what I really wanted, is that the dual axis - in different range - to also follow the ticks of the original axis, across both pan and zoom; and that requires a callback, simply to calculate and apply new set of ticks; I have posted an answer below, which does what I wanted to do in the first place. – sdbbs Oct 16 '19 at 12:53

1 Answers1

0

Thanks to @ImportanceOfBeingErnest - I think I have an example now, that behaves the way I want - also for differing axes' ranges:

Figure_1

Basically, without a callback, and with setting labels manually, all works as @ImportanceOfBeingErnest mentioned - except, when zooming, the old set of labels will remain (and so, when you zoom in, you might see 10 ticks on the original axis, but only 1 tick on the dual); so here, the callback is just used to "follow" the original axis labels:

#!/usr/bin/env python3

import matplotlib
print("matplotlib.__version__ {}".format(matplotlib.__version__))
import matplotlib.pyplot as plt

#
# Some toy data
x_seq = [x / 100.0 for x in range(1, 100)]
y_seq = [x**2 for x in x_seq]

#
# Scatter plot
fig, ax = plt.subplots(1, 1)
ax.plot(x_seq, y_seq)

# https://stackoverflow.com/questions/31803817/how-to-add-second-x-axis-at-the-bottom-of-the-first-one-in-matplotlib
ax22 = ax.twiny() # instantiate a second axes that shares the same y-axis
# Move twinned axis ticks and label from top to bottom
ax22.xaxis.set_ticks_position("bottom")
ax22.xaxis.set_label_position("bottom")
# Offset the twin axis below the host
ax22.spines["bottom"].set_position(("axes", -0.06))

factor = 655
old_xlims = ax.get_xlim()
new_xlims = (factor*old_xlims[0], factor*old_xlims[1])
old_tlocs = ax.get_xticks()
new_tlocs = [i*factor for i in old_tlocs]
print("old_xlims {} new_xlims {} old_tlocs {} new_tlocs {}".format(old_xlims, new_xlims, old_tlocs, new_tlocs))
ax22.set_xticks(new_tlocs)
ax22.set_xlim(*new_xlims)

def on_xlims_change(axes):
  old_tlocs = ax.get_xticks()
  new_tlocs = [i*factor for i in old_tlocs]
  ax22.set_xticks(new_tlocs)

ax.callbacks.connect('xlim_changed', on_xlims_change)

#
# Show
plt.show()

sdbbs
  • 4,270
  • 5
  • 32
  • 87