カーソルがmatplotlibプロットの上にあるときにスクロールホイールをバインドしてズームイン/ズームアウトすることはできますか?
これは動作するはずです。スクロールすると、ポインターの位置を中心にグラフが再配置されます。
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
のコピーを保管してください。コールバックはweak-refを使用するため、f
のコピーを保持しないと、ガベージコレクションされる可能性があります。
この回答を書いた後、私はこれを実際に非常に便利であると判断し、それを Gist に入れました
みんなありがとう、例はとても役に立ちました。散布図を操作するにはいくつかの変更を加える必要があり、左ボタンのドラッグによるパンを追加しました。うまくいけば、誰かがこれが役に立つと思うでしょう。
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()
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()
このわずかに変更されたコードの目的は、新しいズームセンターを基準にしたカーソルの位置を追跡することです。このようにして、中心以外の点で画像を拡大または縮小しても、同じ点に留まります。
どうもありがとう。これはうまくいきました。ただし、スケールが線形ではなくなったプロット(たとえば、対数プロット)の場合、これは失敗します。私はこれのために新しいバージョンを書きました。誰かのお役に立てば幸いです。
基本的に、[0,1]に正規化された座標軸を拡大します。したがって、xで2倍にズームインすると、[。25、.75]の範囲になりたいと思います。また、x軸の真上または真下にいる場合はxだけを拡大し、y軸のすぐ左または右にいる場合はyだけを拡大する機能も追加しました。これが必要ない場合は、zoomx = True、zoomy = Trueに設定し、ifステートメントを無視してください。
このリファレンスは、matplotlibが異なる座標系間でどのように変換するかを理解したい場合に非常に役立ちます。 http://matplotlib.org/users/transforms_tutorial.html
この関数は、Axes(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()
数値プロットの「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
これは、上記のコードにわずかな変更を加えることをお勧めします。これにより、ズームの中心を維持しやすくなります。
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)
tacaswellの答えを「スムーズ」にする
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
ax.set_xlim()
およびax.set_ylim()
を使用した他の回答は、軸の設定が遅い図に対して満足できるユーザーエクスペリエンスを提供しませんでした。 (私にとっては、これはpcolormeshを使用したAxesでした)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)
TkAggバックエンドとpython 3.6を使用してmatplotlib 3.0.2でテスト