Lasso select points from matplotlib plot: colours change when selecting - python

I am trying to select points using a lasso tool from a matplotlib scatter plot. A demo is shown here:
https://matplotlib.org/3.1.0/gallery/widgets/lasso_selector_demo_sgskip.html
In the example, all the scatter points have the same color. I have a scatter plot with varying colour, although that should not make a difference. Here is my code:
xvals = [pt[0] for pt in list(positions.values())]
yvals = [pt[1] for pt in list(positions.values())]
nr_colors=10
subplot_kw = dict(xlim=(min(xvals), max(xvals)), ylim=(min(yvals), max(yvals)), autoscale_on=False)
fig,ax = plt.subplots(subplot_kw=subplot_kw)
pts = ax.scatter(x=xvals,y=yvals,c=coloring,s=sizing/15000,cmap=plt.cm.get_cmap("RdYlGn"))
selector = lassoselector.SelectFromCollection(ax, pts)
def accept(event):
if event.key == "enter":
selector.pointslist.append(selector.ind)
print("Appended a cluster!")
elif event.key == "1":
selector.disconnect()
ax.set_title("")
fig.canvas.draw()
plt.close()
print("Analyzing clusters...")
vals=[]
for selected_rows in selector.pointslist:
vals.append(np.mean(whereUsed.T.values[selected_rows],axis=0))
diff = vals[0]-vals[1]
df = pd.DataFrame({"Difference":diff})
df.index = dimension_value_names
df.sort_values(by="Difference",ascending=True).plot(kind="barh",legend=False,title="Cluster 1 - Cluster 2",figsize=(10,15))
fig.canvas.mpl_connect("key_press_event", accept)
ax.set_title("Press enter to accept selected points, press 1 to close")
plt.show()
As you can see the code is quite similar to the example. Only difference is that my pts object contains many colors instead of just one. So, when accessing the colours in the class through:
self.fc = collection.get_facecolors()
I should get my colour list. For some reason, this is not working and self.fc only contains one colour (blue) when I start drawing. Turning the whole image to blue and a lighter shaded blue, as in the example. How can I keep my original colours and just fade out the selection I'm not using?

I think it is a bug.
If you run it in interactive mode it does not change the facecolors to blue. In the sense of first creating the figure with the scatter, and then initialising the selector.
It seams that the facecolors from the collection are not populated until the figure is actually drawn, if the selector is called before the figure is drawn, then the get_facecolors() method will return the default color (blue).
I did a workaround that may not be pretty but it does the trick. Basically I included a optional parameter to the SelectFromCollection class such that instead of getting the facecolors from the collection, it uses the parameter. I included the whole code below for reference.
The main change to the class is the line:
if facecolors is not None: self.fc = facecolors
'''
import numpy as np
from matplotlib.widgets import LassoSelector
from matplotlib.path import Path
class SelectFromCollection(object):
# Select indices from a matplotlib collection using `LassoSelector`.
#
# Selected indices are saved in the `ind` attribute. This tool fades out the
# points that are not part of the selection (i.e., reduces their alpha
# values). If your collection has alpha < 1, this tool will permanently
# alter the alpha values.
#
# Note that this tool selects collection objects based on their *origins*
# (i.e., `offsets`).
#
# Parameters
# ----------
# ax : :class:`~matplotlib.axes.Axes`
# Axes to interact with.
#
# collection : :class:`matplotlib.collections.Collection` subclass
# Collection you want to select from.
#
# alpha_other : 0 <= float <= 1
# To highlight a selection, this tool sets all selected points to an
# alpha value of 1 and non-selected points to `alpha_other`.
#
def __init__(self, ax, collection, alpha_other=0.3, facecolors=None):
self.canvas = ax.figure.canvas
self.collection = collection
self.alpha_other = alpha_other
self.xys = collection.get_offsets()
self.Npts = len(self.xys)
# Ensure that we have separate colors for each object
self.fc = collection.get_facecolors()
if len(self.fc) == 0:
raise ValueError('Collection must have a facecolor')
elif len(self.fc) == 1:
self.fc = np.tile(self.fc, (self.Npts, 1))
if facecolors is not None: self.fc = facecolors
self.lasso = LassoSelector(ax, onselect=self.onselect)
self.ind = []
def onselect(self, verts):
path = Path(verts)
self.ind = np.nonzero(path.contains_points(self.xys))[0]
self.fc[:, -1] = self.alpha_other
self.fc[self.ind, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def disconnect(self):
self.lasso.disconnect_events()
self.fc[:, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
if __name__ == '__main__':
import matplotlib.pyplot as plt
# Fixing random state for reproducibility
np.random.seed(19680801)
data = np.random.rand(100, 100)
x_2d = np.zeros((100,100))
y_2d = np.zeros((100,100))
for r_ in range(100):
x_2d[r_,:] = np.arange(100)
y_2d[:,r_] = np.arange(100)
fig, ax = plt.subplots()
pts = ax.scatter(x_2d.flatten(), y_2d.flatten(), c=data.flatten(), cmap=plt.cm.jet)
facecolors = plt.cm.jet(data.flatten())
selector = SelectFromCollection(ax, pts, facecolors=facecolors)
def accept(event):
if event.key == "enter":
print("Selected points:")
print(selector.xys[selector.ind])
selector.disconnect()
ax.set_title("")
fig.canvas.draw()
fig.canvas.mpl_connect("key_press_event", accept)
ax.set_title("Press enter to accept selected points.")
plt.show()
'''

Related

Python Matplotlib Multicursor: Show value under cursor in legend

I am using Multicursor to get a cursor on every graph.
I want to show the value of the datapoint, which is hit by the cursor, inside a legend during hovering over the graphs, like this
Actually I have thought that this is a standard feature of matplotlib respectively Multicursor, but it seems not. Did someone already something like this or do I have to implement it by my own.
I already found this post matplotlib multiple values under cursor, but this could be just the beginning for the implementation I want.
I have developed a solution.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import MultiCursor
from bisect import bisect_left
fig = plt.figure(figsize=(15, 8))
# create random graph with 60 datapoints, 0 till 59
x = list(range(0,60))
axes_list = []
def createRandomGraph(ax,x):
y = np.random.randint(low=0, high=15, size=60)
data.append(y)
ax.plot(x,y, marker='.')
def take_closest(myList, myNumber):
"""
Assumes myList is sorted. Returns closest value to myNumber.
If two numbers are equally close, return the smallest number.
"""
pos = bisect_left(myList, myNumber)
if pos == 0:
return myList[0]
if pos == len(myList):
return myList[-1]
before = myList[pos - 1]
after = myList[pos]
if after - myNumber < myNumber - before:
return after, pos
else:
return before, pos-1
def show_Legend(event):
#get mouse coordinates
mouseXdata = event.xdata
# the value of the closest data point to the current mouse position shall be shown
closestXValue, posClosestXvalue = take_closest(data[0], mouseXdata)
i = 1
for ax in axes_list:
datalegend = ax.text(1.05, 0.5, data[i][posClosestXvalue], fontsize=7,
verticalalignment='top', bbox=props, transform=ax.transAxes)
ax.draw_artist(datalegend)
# this remove is required because otherwise after a resizing of the window there is
# an artifact of the last label, which lies behind the new one
datalegend.remove()
i +=1
fig.canvas.update()
# store the x value of the graph in the first element of the list
data = [x]
# properties of the legend labels
props = dict(boxstyle='round', edgecolor='black', facecolor='wheat', alpha=1.5)
for i in range(5):
if(i>0):
# all plots share the same x axes, thus during zooming and panning
# we will see always the same x section of each graph
ax = plt.subplot(5, 1, i+1, sharex=ax)
else:
ax = plt.subplot(5, 1, i+1)
axes_list.append(ax)
createRandomGraph(ax,x)
multi = MultiCursor(fig.canvas, axes_list, color='r', lw=1)
# function show_Legend is called while hovering over the graphs
fig.canvas.mpl_connect('motion_notify_event', show_Legend)
plt.show()
The output looks like this
Maybe you like it and find it useful

Shifting the origin in ruptures.display

I am trying to perform change point detection using the ruptures package. When I use the ruptures.display for plotting, the x axis starts of with 0 as the start point.
And here is how the plot looks like:
However, I would like to start with an offset. Therefore I have tried to create a custom display function using the ruptures.display source code. But, I am not able to figure out how to shift the origin.
Below is the main code:
data = pd.read_csv("test_flooding.csv")
Start_time = pd.to_datetime('81028520.26',unit='s')
End_time = pd.to_datetime('81113495.41',unit='s')
#Format the 'Date' column
data['Time']=data['Time'].astype(str)
#Convert the Date column into a date object
data['Time']=pd.to_datetime(data['Time'],unit='s')
#Selecting a specific range
data=data[(data['Time']<=End_time)]
data=data[(Start_time <=data['Time'])]
data = data.loc[data['ID'] == "id1"]
#Convert the time series values to a numpy 1D array
points=np.array(data['Signal1_of_ID'])
#RUPTURES PACKAGE
#Changepoint detection with the Pelt search method
start_timestamp = int(time.mktime(Start_time.timetuple()))
model="rbf"
algo = rpt.Pelt(model=model).fit(points)
result = algo.predict(pen=10)
display(points,start_timestamp , result, figsize=(10, 6))
plt.title('Change Point Detection: Pelt Search Method')
plt.show()
And here is the custom display code:
from itertools import cycle
import matplotlib.pyplot as plt
import numpy as np
from ruptures.utils import pairwise
COLOR_CYCLE = ["#4286f4", "#f44174"]
def display(signal,Start_time, true_chg_pts, computed_chg_pts=None, **kwargs):
"""
Display a signal and the change points provided in alternating colors. If another set of change
point is provided, they are displayed with dashed vertical dashed lines.
Args:
signal (array): signal array, shape (n_samples,) or (n_samples, n_features).
true_chg_pts (list): list of change point indexes.
computed_chg_pts (list, optional): list of change point indexes.
Returns:
tuple: (figure, axarr) with a :class:`matplotlib.figure.Figure` object and an array of Axes objects.
"""
if signal.ndim == 1:
signal = signal.reshape(-1, 1)
n_samples, n_features = signal.shape
# let's set all options
figsize = (10, 2 * n_features) # figure size
alpha = 0.2 # transparency of the colored background
color = "k" # color of the lines indicating the computed_chg_pts
linewidth = 3 # linewidth of the lines indicating the computed_chg_pts
linestyle = "--" # linestyle of the lines indicating the computed_chg_pts
if "figsize" in kwargs:
figsize = kwargs["figsize"]
if "alpha" in kwargs:
alpha = kwargs["alpha"]
if "color" in kwargs:
color = kwargs["color"]
if "linewidth" in kwargs:
linewidth = kwargs["linewidth"]
if "linestyle" in kwargs:
linestyle = kwargs["linestyle"]
fig, axarr = plt.subplots(n_features, figsize=figsize, sharex=True)
if n_features == 1:
axarr = [axarr]
for axe, sig in zip(axarr, signal.T):
color_cycle = cycle(COLOR_CYCLE)
# plot s
axe.plot(range(Start_time,Start_time+n_samples), sig)
# color each (true) regime
bkps = [0] + sorted(true_chg_pts)
for (start, end), col in zip(pairwise(bkps), color_cycle):
axe.axvspan(max(0, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)
# vertical lines to mark the computed_chg_pts
if computed_chg_pts is not None:
for bkp in computed_chg_pts:
if bkp != 0 and bkp < n_samples:
axe.axvline(x=bkp - 0.5,
color=color,
linewidth=linewidth,
linestyle=linestyle)
fig.tight_layout()
return fig, axarr
Here is how the image looks with my custom display trial, which still plots with the origin as 0:
Any help is highly appreciated.
It looks like your code might include zero, since you're prepending zero to the break points list:
axe.axvspan(max(0, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)
should probably use your Start_time instead of zero:
axe.axvspan(max(Start_time, start - 0.5),
end - 0.5,
facecolor=col, alpha=alpha)

Matplotlib: A bug in interactive zooming tool after updating figure

I am stuck again with interactive plotting with matplotlib.
Everything else works like a charm (hovering and clicking of objects in a figure) but if I zoom the shown figure and it will be updated, zooming rectangle will remain in the new figure. Probably I have to reset zooming settings somehow but I couldn't find out the correct method to do it from other StackOverflow questions (clearing the figure is not obviously enough).
I built a toy example to illustrate the problem. Four points are attached to four images and they are plotted to the figure. With interactive-mode by inserting cursor on top of chosen point, it shows related image in a imagebox. After one point is clicked, program waits 2 seconds and updates the view by rotating all the samples 15 degrees.
The problem occurs when current view is zoomed and then its updated. Zoom-to-rectangle will start automatically and after clicking once anywhere in the figure, the rectangle is gone without doing anything. This is shown in below image. I just want to have normal cursor after figure is updated.
Here is the code for the toy example:
import matplotlib.pyplot as plt
import matplotlib as mpl
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np
import copy
def initialize_figure(fignum):
plt.figure(fignum)
plt.clf()
def draw_interactive_figures(new_samples, images):
global new_samples_tmp, images_tmp, offset_image_tmp, image_box_tmp, fig_tmp, x_tmp, y_tmp
initialize_figure(1)
plt.ion()
fig_tmp = plt.gcf()
images_tmp = copy.deepcopy(images)
offset_image_tmp = OffsetImage(images_tmp[0,:,:,:])
image_box_tmp = (40., 40.)
x_tmp = new_samples[:,0]
y_tmp = new_samples[:,1]
new_samples_tmp = copy.deepcopy(new_samples)
update_plot()
fig_tmp.canvas.mpl_connect('motion_notify_event', hover)
fig_tmp.canvas.mpl_connect('button_press_event', click)
plt.show()
fig_tmp.canvas.start_event_loop()
plt.ioff()
def update_plot():
global points_tmp, annotationbox_tmp
ax = plt.gca()
points_tmp = plt.scatter(*new_samples_tmp.T, s=14, c='b', edgecolor='k')
annotationbox_tmp = AnnotationBbox(offset_image_tmp, (0,0), xybox=image_box_tmp, xycoords='data', boxcoords='offset points', pad=0.3, arrowprops=dict(arrowstyle='->'))
ax.add_artist(annotationbox_tmp)
annotationbox_tmp.set_visible(False)
def hover(event):
if points_tmp.contains(event)[0]:
inds = points_tmp.contains(event)[1]['ind']
ind = inds[0]
w,h = fig_tmp.get_size_inches()*fig_tmp.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
annotationbox_tmp.xybox = (image_box_tmp[0]*ws, image_box_tmp[1]*hs)
annotationbox_tmp.set_visible(True)
annotationbox_tmp.xy =(x_tmp[ind], y_tmp[ind])
offset_image_tmp.set_data(images_tmp[ind,:,:])
else:
annotationbox_tmp.set_visible(False)
fig_tmp.canvas.draw_idle()
def click(event):
if points_tmp.contains(event)[0]:
inds = points_tmp.contains(event)[1]['ind']
ind = inds[0]
initialize_figure(1)
update_plot()
plt.scatter(x_tmp[ind], y_tmp[ind], s=20, marker='*', c='y')
plt.pause(2)
fig_tmp.canvas.stop_event_loop()
fig_tmp.canvas.draw_idle()
def main():
fig, ax = plt.subplots(1, figsize=(7, 7))
points = np.array([[1,1],[1,-1],[-1,1],[-1,-1]])
zero_layer = np.zeros([28,28])
one_layer = np.ones([28,28])*255
images = np.array([np.array([zero_layer, zero_layer, one_layer]).astype(np.uint8),np.array([zero_layer, one_layer, zero_layer]).astype(np.uint8),np.array([one_layer, zero_layer, zero_layer]).astype(np.uint8),np.array([one_layer, zero_layer, one_layer]).astype(np.uint8)])
images = np.transpose(images, (0,3,2,1))
theta = 0
delta = 15 * (np.pi/180)
rotation_matrix = np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])
while True:
rotated_points = np.matmul(points, rotation_matrix)
draw_interactive_figures(rotated_points, images)
theta += delta
rotation_matrix = np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])
if __name__== "__main__":
main()
Thanks in advance!
I'm providing you with a starting point here. The following is a script that creates a plot and allows you to add new points by clicking on the axes. For each point one may mouse hover and show a respective image.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np
class MyInteractivePlotter():
def __init__(self):
self.fig, self.ax = plt.subplots()
self.ax.set(xlim=(0,1), ylim=(0,1))
self.points = np.array([[0.5,0.5]]) # will become N x 2 array
self.images = [np.random.rand(10,10)]
self.scatter = self.ax.scatter(*self.points.T)
self.im = OffsetImage(self.images[0], zoom=5)
self.ab = AnnotationBbox(self.im, (0,0), xybox=(50., 50.), xycoords='data',
boxcoords="offset points", pad=0.3,
arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
self.ax.add_artist(self.ab)
self.ab.set_visible(False)
self.cid = self.fig.canvas.mpl_connect("button_press_event", self.onclick)
self.hid = self.fig.canvas.mpl_connect("motion_notify_event", self.onhover)
def add_point(self):
# Update points (here, we just add a new random point)
self.points = np.concatenate((self.points, np.random.rand(1,2)), axis=0)
# For each points there is an image. (Here, we just add a random one)
self.images.append(np.random.rand(10,10))
# Update the scatter plot to show the new point
self.scatter.set_offsets(self.points)
def onclick(self, event):
self.add_point()
self.fig.canvas.draw_idle()
def onhover(self, event):
# if the mouse is over the scatter points
if self.scatter.contains(event)[0]:
# find out the index within the array from the event
ind, = self.scatter.contains(event)[1]["ind"]
# make annotation box visible
self.ab.set_visible(True)
# place it at the position of the hovered scatter point
self.ab.xy = self.points[ind,:]
# set the image corresponding to that point
self.im.set_data(self.images[ind])
else:
#if the mouse is not over a scatter point
self.ab.set_visible(False)
self.fig.canvas.draw_idle()
m = MyInteractivePlotter()
plt.show()
I would suggest you take this and add your functionality into it. Once you stumble upon a problem you can use it to ask for clarifications.

finding local maximum from fft of a signal

I'm trying to find a peak of an fft of a signal to be used for a further analysis of the signal. I'm using a SpanSelect of data and doing an fft, represented as a frequency spectrum. I really wanted to have the plot be interactive and the user click a point to be further analyzed, but I don't see a way to do that so would like a way to find local frequency peaks. The frequency spectrum may look like this:
So I would want a way to return the frequency that has a peak at 38 hz for example. Is there a way to do this?
use argrelextrema for finding local maxima:
import numpy as np
from scipy.signal import argrelextrema
from matplotlib.pyplot import *
np.random.seed()
x = np.random.random(50)
m = argrelextrema(x, np.greater) #array of indexes of the locals maxima
y = [x[m] for i in m]
plot(x)
plot(m, y, 'rs')
show()
You can do something like that using matplotlib widgets, for example check out the lasso method of selecting points.
You can then use the selected point in any form of analysis you need.
EDIT: Combined lasso and SpanSelect widget from matplotlib examples
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector, LassoSelector
from matplotlib.path import Path
import matplotlib.pyplot as plt
try:
raw_input
except NameError:
# Python 3
raw_input = input
class SelectFromCollection(object):
"""Select indices from a matplotlib collection using `LassoSelector`.
Selected indices are saved in the `ind` attribute. This tool highlights
selected points by fading them out (i.e., reducing their alpha values).
If your collection has alpha < 1, this tool will permanently alter them.
Note that this tool selects collection objects based on their *origins*
(i.e., `offsets`).
Parameters
----------
ax : :class:`~matplotlib.axes.Axes`
Axes to interact with.
collection : :class:`matplotlib.collections.Collection` subclass
Collection you want to select from.
alpha_other : 0 <= float <= 1
To highlight a selection, this tool sets all selected points to an
alpha value of 1 and non-selected points to `alpha_other`.
"""
def __init__(self, ax, collection, alpha_other=0.3):
self.canvas = ax.figure.canvas
self.collection = collection
self.alpha_other = alpha_other
self.xys = collection.get_offsets()
self.Npts = len(self.xys)
# Ensure that we have separate colors for each object
self.fc = collection.get_facecolors()
if len(self.fc) == 0:
raise ValueError('Collection must have a facecolor')
elif len(self.fc) == 1:
self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1)
self.lasso = LassoSelector(ax, onselect=self.onselect)
self.ind = []
def onselect(self, verts):
path = Path(verts)
self.ind = np.nonzero([path.contains_point(xy) for xy in self.xys])[0]
self.fc[:, -1] = self.alpha_other
self.fc[self.ind, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def disconnect(self):
self.lasso.disconnect_events()
self.fc[:, -1] = 1
self.collection.set_facecolors(self.fc)
self.canvas.draw_idle()
def onselect(xmin, xmax):
indmin, indmax = np.searchsorted(x, (xmin, xmax))
indmax = min(len(x)-1, indmax)
thisx = x[indmin:indmax]
thisy = y[indmin:indmax]
line2.set_data(thisx, thisy)
ax2.set_xlim(thisx[0], thisx[-1])
ax2.set_ylim(thisy.min(), thisy.max())
fig.canvas.draw()
if __name__ == '__main__':
plt.ion()
fig = plt.figure(figsize=(8,6))
ax = fig.add_subplot(211, axisbg='#FFFFCC')
x = np.arange(0.0, 5.0, 0.01)
y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x))
ax.plot(x, y, '-')
ax.set_ylim(-2,2)
ax.set_title('Press left mouse button and drag to test')
ax2 = fig.add_subplot(212, axisbg='#FFFFCC')
line2, = ax2.plot(x, y, '-')
pts = ax2.scatter(x, y)
# set useblit True on gtkagg for enhanced performance
span = SpanSelector(ax, onselect, 'horizontal', useblit=True,
rectprops=dict(alpha=0.5, facecolor='red') )
selector = SelectFromCollection(ax2, pts)
plt.draw()
raw_input('Press any key to accept selected points')
print("Selected points:")
print(selector.xys[selector.ind])
selector.disconnect()
# Block end of script so you can check that the lasso is disconnected.
raw_input('Press any key to quit')

Matplotlib/Pyplot: How to zoom subplots together AND x-scroll separately?

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!
-BobC
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[0]))
ax.set_ylim(*(lims[1]))
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][1] - oxy[i][j][0]
# Determine new axis scale factors in a way that correctly
# handles simultaneous zoom + scroll: Zoom from each end.
zoom[j] = [(nxy[j][0] - oxy[i][j][0]) / width, # lo-end zoom
(oxy[i][j][1] - nxy[j][1]) / 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][1] - oxy[i][j][0]
nxy[j] = [oxy[i][j][0] + (width*zoom[j][0]),
oxy[i][j][1] - (width*zoom[j][1])]
_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[1]-nx[0])/(ox[1]-ox[0])
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[0] + ox[1])/2.0
dif = zoom*(ox[1] - ox[0])/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()

Categories