Можно ли привязать колесо прокрутки для увеличения/уменьшения масштаба, когда курсор зависает над графиком matplotlib?
Матрица масштабирования Matplotlib с колесом прокрутки
Ответ 1
Это должно сработать. Он повторно центрирует график по расположению указателя при прокрутке.
import matplotlib.pyplot as plt
def zoom_factory(ax,base_scale = 2.):
def zoom_fun(event):
# get the current x and y limits
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
cur_xrange = (cur_xlim[1] - cur_xlim[0])*.5
cur_yrange = (cur_ylim[1] - cur_ylim[0])*.5
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if event.button == 'up':
# deal with zoom in
scale_factor = 1/base_scale
elif event.button == 'down':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print event.button
# set new limits
ax.set_xlim([xdata - cur_xrange*scale_factor,
xdata + cur_xrange*scale_factor])
ax.set_ylim([ydata - cur_yrange*scale_factor,
ydata + cur_yrange*scale_factor])
plt.draw() # force re-draw
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('scroll_event',zoom_fun)
#return the function
return zoom_fun
Предполагая, что у вас есть объект оси ax
ax.plot(range(10))
scale = 1.5
f = zoom_factory(ax,base_scale = scale)
Необязательный аргумент base_scale
позволяет установить масштабный коэффициент как то, что вы хотите.
убедитесь, что вы сохранили копию f
. В обратном вызове используется слабый-ref, поэтому, если вы не сохраняете копию f
, это может быть сбор мусора.
После написания этого ответа я решил, что это действительно полезно и помещает его в gist
Ответ 2
Спасибо, ребята, примеры были очень полезными. Я должен был сделать несколько изменений для работы с диаграммой рассеяния, и я добавил панорамирование с помощью перетаскивания левой кнопкой. Надеюсь, кто-то найдет это полезным.
from matplotlib.pyplot import figure, show
import numpy
class ZoomPan:
def __init__(self):
self.press = None
self.cur_xlim = None
self.cur_ylim = None
self.x0 = None
self.y0 = None
self.x1 = None
self.y1 = None
self.xpress = None
self.ypress = None
def zoom_factory(self, ax, base_scale = 2.):
def zoom(event):
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if event.button == 'down':
# deal with zoom in
scale_factor = 1 / base_scale
elif event.button == 'up':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print event.button
new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])
ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
fig.canvas.mpl_connect('scroll_event', zoom)
return zoom
def pan_factory(self, ax):
def onPress(event):
if event.inaxes != ax: return
self.cur_xlim = ax.get_xlim()
self.cur_ylim = ax.get_ylim()
self.press = self.x0, self.y0, event.xdata, event.ydata
self.x0, self.y0, self.xpress, self.ypress = self.press
def onRelease(event):
self.press = None
ax.figure.canvas.draw()
def onMotion(event):
if self.press is None: return
if event.inaxes != ax: return
dx = event.xdata - self.xpress
dy = event.ydata - self.ypress
self.cur_xlim -= dx
self.cur_ylim -= dy
ax.set_xlim(self.cur_xlim)
ax.set_ylim(self.cur_ylim)
ax.figure.canvas.draw()
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('button_press_event',onPress)
fig.canvas.mpl_connect('button_release_event',onRelease)
fig.canvas.mpl_connect('motion_notify_event',onMotion)
#return the function
return onMotion
fig = figure()
ax = fig.add_subplot(111, xlim=(0,1), ylim=(0,1), autoscale_on=False)
ax.set_title('Click to zoom')
x,y,s,c = numpy.random.rand(4,200)
s *= 200
ax.scatter(x,y,s,c)
scale = 1.1
zp = ZoomPan()
figZoom = zp.zoom_factory(ax, base_scale = scale)
figPan = zp.pan_factory(ax)
show()
Ответ 3
def zoom(self, event, factor):
curr_xlim = self.ax.get_xlim()
curr_ylim = self.ax.get_ylim()
new_width = (curr_xlim[1]-curr_ylim[0])*factor
new_height= (curr_xlim[1]-curr_ylim[0])*factor
relx = (curr_xlim[1]-event.xdata)/(curr_xlim[1]-curr_xlim[0])
rely = (curr_ylim[1]-event.ydata)/(curr_ylim[1]-curr_ylim[0])
self.ax.set_xlim([event.xdata-new_width*(1-relx),
event.xdata+new_width*(relx)])
self.ax.set_ylim([event.ydata-new_width*(1-rely),
event.ydata+new_width*(rely)])
self.draw()
Цель этого слегка измененного кода - отслеживать положение курсора относительно нового центра масштабирования. Таким образом, если вы увеличиваете и уменьшаете изображение в точках, отличных от центра, вы остаетесь в одной и той же точке.
Ответ 4
Большое спасибо. Это отлично поработало. Однако для участков, где масштаб больше не является линейным (например, лог-графики), это разрушается. Я написал для этого новую версию. Надеюсь, это поможет кому-то.
В основном, я увеличиваю координаты осей, которые нормализуются как [0,1]. Итак, если я увеличиваю масштаб на два в x, я хочу теперь находиться в диапазоне [.25,.75]. Я также добавил функцию, чтобы увеличить масштаб x, если вы находитесь непосредственно выше или ниже оси x, и только в y, если вы прямо слева или справа от оси y. Если вам это не нужно, просто установите zoomx = True и zoomy = True и игнорируйте инструкции if.
Эта ссылка очень полезна для тех, кто хочет понять, как matplotlib трансформируется между разными системами координат: http://matplotlib.org/users/transforms_tutorial.html
Эта функция находится внутри объекта, который содержит указатель на оси (self.ax).
def zoom(self,event):
'''This function zooms the image upon scrolling the mouse wheel.
Scrolling it in the plot zooms the plot. Scrolling above or below the
plot scrolls the x axis. Scrolling to the left or the right of the plot
scrolls the y axis. Where it is ambiguous nothing happens.
NOTE: If expanding figure to subplots, you will need to add an extra
check to make sure you are not in any other plot. It is not clear how to
go about this.
Since we also want this to work in loglog plot, we work in axes
coordinates and use the proper scaling transform to convert to data
limits.'''
x = event.x
y = event.y
#convert pixels to axes
tranP2A = self.ax.transAxes.inverted().transform
#convert axes to data limits
tranA2D= self.ax.transLimits.inverted().transform
#convert the scale (for log plots)
tranSclA2D = self.ax.transScale.inverted().transform
if event.button == 'down':
# deal with zoom in
scale_factor = self.zoom_scale
elif event.button == 'up':
# deal with zoom out
scale_factor = 1 / self.zoom_scale
else:
# deal with something that should never happen
scale_factor = 1
#get my axes position to know where I am with respect to them
xa,ya = tranP2A((x,y))
zoomx = False
zoomy = False
if(ya < 0):
if(xa >= 0 and xa <= 1):
zoomx = True
zoomy = False
elif(ya <= 1):
if(xa <0):
zoomx = False
zoomy = True
elif(xa <= 1):
zoomx = True
zoomy = True
else:
zoomx = False
zoomy = True
else:
if(xa >=0 and xa <= 1):
zoomx = True
zoomy = False
new_alimx = (0,1)
new_alimy = (0,1)
if(zoomx):
new_alimx = (np.array([1,1]) + np.array([-1,1])*scale_factor)*.5
if(zoomy):
new_alimy = (np.array([1,1]) + np.array([-1,1])*scale_factor)*.5
#now convert axes to data
new_xlim0,new_ylim0 = tranSclA2D(tranA2D((new_alimx[0],new_alimy[0])))
new_xlim1,new_ylim1 = tranSclA2D(tranA2D((new_alimx[1],new_alimy[1])))
#and set limits
self.ax.set_xlim([new_xlim0,new_xlim1])
self.ax.set_ylim([new_ylim0,new_ylim1])
self.redraw()
Ответ 5
Мне действительно нравятся режимы "только x" или "y только" на графиках фигур. Вы можете привязать ключи x и y, чтобы масштабирование происходило только в одном направлении. Обратите внимание, что вам также может понадобиться поместить фокус обратно на холст, если вы нажмете на поле ввода или что-то еще -
canvas.mpl_connect('button_press_event', lambda event:canvas._tkcanvas.focus_set())
Остальная часть измененного кода ниже:
from matplotlib.pyplot import figure, show
import numpy
class ZoomPan:
def __init__(self):
self.press = None
self.cur_xlim = None
self.cur_ylim = None
self.x0 = None
self.y0 = None
self.x1 = None
self.y1 = None
self.xpress = None
self.ypress = None
self.xzoom = True
self.yzoom = True
self.cidBP = None
self.cidBR = None
self.cidBM = None
self.cidKeyP = None
self.cidKeyR = None
self.cidScroll = None
def zoom_factory(self, ax, base_scale = 2.):
def zoom(event):
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
if(xdata is None):
return()
if(ydata is None):
return()
if event.button == 'down':
# deal with zoom in
scale_factor = 1 / base_scale
elif event.button == 'up':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print(event.button)
new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0])
rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0])
if(self.xzoom):
ax.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)])
if(self.yzoom):
ax.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)])
ax.figure.canvas.draw()
ax.figure.canvas.flush_events()
def onKeyPress(event):
if event.key == 'x':
self.xzoom = True
self.yzoom = False
if event.key == 'y':
self.xzoom = False
self.yzoom = True
def onKeyRelease(event):
self.xzoom = True
self.yzoom = True
fig = ax.get_figure() # get the figure of interest
self.cidScroll = fig.canvas.mpl_connect('scroll_event', zoom)
self.cidKeyP = fig.canvas.mpl_connect('key_press_event',onKeyPress)
self.cidKeyR = fig.canvas.mpl_connect('key_release_event',onKeyRelease)
return zoom
def pan_factory(self, ax):
def onPress(event):
if event.inaxes != ax: return
self.cur_xlim = ax.get_xlim()
self.cur_ylim = ax.get_ylim()
self.press = self.x0, self.y0, event.xdata, event.ydata
self.x0, self.y0, self.xpress, self.ypress = self.press
def onRelease(event):
self.press = None
ax.figure.canvas.draw()
def onMotion(event):
if self.press is None: return
if event.inaxes != ax: return
dx = event.xdata - self.xpress
dy = event.ydata - self.ypress
self.cur_xlim -= dx
self.cur_ylim -= dy
ax.set_xlim(self.cur_xlim)
ax.set_ylim(self.cur_ylim)
ax.figure.canvas.draw()
ax.figure.canvas.flush_events()
fig = ax.get_figure() # get the figure of interest
self.cidBP = fig.canvas.mpl_connect('button_press_event',onPress)
self.cidBR = fig.canvas.mpl_connect('button_release_event',onRelease)
self.cidBM = fig.canvas.mpl_connect('motion_notify_event',onMotion)
# attach the call back
#return the function
return onMotion
Ответ 6
Это предложение для небольшой модификации вышеприведенного кода - это делает сохранение большего масштаба более управляемым.
cur_xrange = (cur_xlim[1] - cur_xlim[0])*.5
cur_yrange = (cur_ylim[1] - cur_ylim[0])*.5
xmouse = event.xdata # get event x location
ymouse = event.ydata # get event y location
cur_xcentre = (cur_xlim[1] + cur_xlim[0])*.5
cur_ycentre = (cur_ylim[1] + cur_ylim[0])*.5
xdata = cur_xcentre+ 0.25*(xmouse-cur_xcentre)
ydata = cur_ycentre+ 0.25*(ymouse-cur_ycentre)
Ответ 7
сделать так называемый "гладкий" ответ
def zoom_factory(ax, base_scale=2.):
prex = 0
prey = 0
prexdata = 0
preydata = 0
def zoom_fun(event):
nonlocal prex, prey, prexdata, preydata
curx = event.x
cury = event.y
# if not changed mouse position(or changed so little)
# remain the pre scale center
if abs(curx - prex) < 10 and abs(cury - prey) < 10:
# remain same
xdata = prexdata
ydata = preydata
# if changed mouse position ,also change the cur scale center
else:
# change
xdata = event.xdata # get event x location
ydata = event.ydata # get event y location
# update previous location data
prex = event.x
prey = event.y
prexdata = xdata
preydata = ydata
# get the current x and y limits
cur_xlim = ax.get_xlim()
cur_ylim = ax.get_ylim()
cur_xrange = (cur_xlim[1] - cur_xlim[0]) * .5
cur_yrange = (cur_ylim[1] - cur_ylim[0]) * .5
# log.debug((xdata, ydata))
if event.button == 'up':
# deal with zoom in
scale_factor = 1 / base_scale
elif event.button == 'down':
# deal with zoom out
scale_factor = base_scale
else:
# deal with something that should never happen
scale_factor = 1
print(event.button)
# set new limits
ax.set_xlim([
xdata - cur_xrange * scale_factor,
xdata + cur_xrange * scale_factor
])
ax.set_ylim([
ydata - cur_yrange * scale_factor,
ydata + cur_yrange * scale_factor
])
plt.draw() # force re-draw
fig = ax.get_figure() # get the figure of interest
# attach the call back
fig.canvas.mpl_connect('scroll_event', zoom_fun)
# return the function
return zoom_fun
Ответ 8
Другие ответы с использованием ax.set_xlim()
и ax.set_ylim()
не дали удовлетворительного пользовательского опыта для фигур, где установка осей медленная. (для меня это были оси с pcolormesh) Метод ax.drag_pan()
намного быстрее, и я считаю, что он больше подходит для большинства случаев:
def mousewheel_move( event):
ax=event.inaxes
ax._pan_start = types.SimpleNamespace(
lim=ax.viewLim.frozen(),
trans=ax.transData.frozen(),
trans_inverse=ax.transData.inverted().frozen(),
bbox=ax.bbox.frozen(),
x=event.x,
y=event.y)
if event.button == 'up':
ax.drag_pan(3, event.key, event.x+10, event.y+10)
else: #event.button == 'down':
ax.drag_pan(3, event.key, event.x-10, event.y-10)
fig=ax.get_figure()
fig.canvas.draw_idle()
Затем соедините свою фигуру с:
fig.canvas.mpl_connect('scroll_event',mousewheel_move)
Протестировано с matplotlib 3.0.2 с использованием бэкэнда TkAgg и python 3.6