I previously asked the question "How to zoom subplots together?", and have been using the excellent answer since then.
I'm now plotting just two sets of time-series data, and I need to continue to zoom as above, but now I need to also pan one plot relative to the other (I'm doing eyeball correlation). The data comes from 2 independent instruments with different start times and different clock settings.
In use, I zoom using the 'Zoom to Rectangle' toolbar button, and I scroll using the "Pan/Zoom" button.
How may I best scroll one plot in X relative to the other? Ideally, I'd also like to capture and display the time difference. I do not need to scroll vertically in Y.
I suspect I may need to stop using the simple "sharex=" "sharey=" method, but am not certain how best to proceed.
Thanks, in advance, to the great StackOverflow community!
I hacked the above solution until it did want I think I want.
# File: ScrollTest.py # coding: ASCII """ Interatively zoom plots together, but permit them to scroll independently. """ from matplotlib import pyplot import sys def _get_limits( ax ): """ Return X and Y limits for the passed axis as [[xlow,xhigh],[ylow,yhigh]] """ return [list(ax.get_xlim()), list(ax.get_ylim())] def _set_limits( ax, lims ): """ Set X and Y limits for the passed axis """ ax.set_xlim(*(lims)) ax.set_ylim(*(lims)) return def pre_zoom( fig ): """ Initialize history used by the re_zoom() event handler. Call this after plots are configured and before pyplot.show(). """ global oxy oxy = [_get_limits(ax) for ax in fig.axes] # :TODO: Intercept the toolbar Home, Back and Forward buttons. return def re_zoom(event): """ Pyplot event handler to zoom all plots together, but permit them to scroll independently. Created to support eyeball correlation. Use with 'motion_notify_event' and 'button_release_event'. """ global oxy for ax in event.canvas.figure.axes: navmode = ax.get_navigate_mode() if navmode is not None: break scrolling = (event.button == 1) and (navmode == "PAN") if scrolling: # Update history (independent of event type) oxy = [_get_limits(ax) for ax in event.canvas.figure.axes] return if event.name != 'button_release_event': # Nothing to do! return # We have a non-scroll 'button_release_event': Were we zooming? zooming = (navmode == "ZOOM") or ((event.button == 3) and (navmode == "PAN")) if not zooming: # Nothing to do! oxy = [_get_limits(ax) for ax in event.canvas.figure.axes] # To be safe return # We were zooming, but did anything change? Check for zoom activity. changed = None zoom = [[0.0,0.0],[0.0,0.0]] # Zoom from each end of axis (2 values per axis) for i, ax in enumerate(event.canvas.figure.axes): # Get the axes # Find the plot that changed nxy = _get_limits(ax) if (oxy[i] != nxy): # This plot has changed changed = i # Calculate zoom factors for j in [0,1]: # Iterate over x and y for each axis # Indexing: nxy[x/y axis][lo/hi limit] # oxy[plot #][x/y axis][lo/hi limit] width = oxy[i][j] - oxy[i][j] # Determine new axis scale factors in a way that correctly # handles simultaneous zoom + scroll: Zoom from each end. zoom[j] = [(nxy[j] - oxy[i][j]) / width, # lo-end zoom (oxy[i][j] - nxy[j]) / width] # hi-end zoom break # No need to look at other axes if changed is not None: for i, ax in enumerate(event.canvas.figure.axes): # change the scale if i == changed: continue for j in [0,1]: width = oxy[i][j] - oxy[i][j] nxy[j] = [oxy[i][j] + (width*zoom[j]), oxy[i][j] - (width*zoom[j])] _set_limits(ax, nxy) event.canvas.draw() # re-draw the canvas (if required) pre_zoom(event.canvas.figure) # Update history return # End re_zoom() def main(argv): """ Test/demo code for re_zoom() event handler. """ import numpy x = numpy.linspace(0,100,1000) # Create test data y = numpy.sin(x)*(1+x) fig = pyplot.figure() # Create plot ax1 = pyplot.subplot(211) ax1.plot(x,y) ax2 = pyplot.subplot(212) ax2.plot(x,y) pre_zoom( fig ) # Prepare plot event handler pyplot.connect('motion_notify_event', re_zoom) # for right-click pan/zoom pyplot.connect('button_release_event',re_zoom) # for rectangle-select zoom pyplot.show() # Show plot and interact with user # End main() if __name__ == "__main__": # Script is being executed from the command line (not imported) main(sys.argv) # End of file ScrollTest.py
Ok, here's my stab at it. This works, but there might be a simpler approach. This solution uses some matplotlib event-handling to trigger a new
set_xlim() every time it notices the mouse in motion. The trigger event
'motion_notify_event' could be eliminated if dynamic synchronous zooming isn't required.
Bonus: this works for any number of subplots.
from matplotlib import pyplot import numpy x = numpy.linspace(0,10,100) y = numpy.sin(x)*(1+x) fig = pyplot.figure() ax1 = pyplot.subplot(121) ax1.plot(x,y) ax2 = pyplot.subplot(122) ax2.plot(x,y) ax1.old_xlim = ax1.get_xlim() # store old values so changes ax2.old_xlim = ax2.get_xlim() # can be detected def re_zoom(event): zoom = 1.0 for ax in event.canvas.figure.axes: # get the change in scale nx = ax.get_xlim() ox = ax.old_xlim if ox != nx: # of axes that have changed scale zoom = (nx-nx)/(ox-ox) for ax in event.canvas.figure.axes: # change the scale nx = ax.get_xlim() ox = ax.old_xlim if ox == nx: # of axes that need an update mid = (ox + ox)/2.0 dif = zoom*(ox - ox)/2.0 nx = (mid - dif, mid + dif) ax.set_xlim(*nx) ax.old_xlim = nx if zoom != 1.0: event.canvas.draw() # re-draw the canvas (if required) pyplot.connect('motion_notify_event', re_zoom) # for right-click pan/zoom pyplot.connect('button_release_event', re_zoom) # for rectangle-select zoom pyplot.show()