Python結(jié)合matplotlib實現(xiàn)圖表的基本交互
前言
最近在使用pyqt結(jié)合matplotlib開發(fā)一款內(nèi)部使用的數(shù)據(jù)分析軟件,發(fā)現(xiàn)matplotlib庫在處理大數(shù)據(jù),出圖性能方面還是很不錯的,但是就是圖表的交互性上差了一點,比如說圖像的放大和縮小,移動的光標(biāo)線,顯示注釋等等,很多還是需要自己造輪子,本人通過五一假期的一番研究,從中也頗有收獲,現(xiàn)在把下面的這些研究成果分享給大家。
使用Matplotlib庫完成基本的圖表交互
初始化基本的曲線配置
import sys
import numpy as np
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
class MatplotlibWidget(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
self.compute_initial_figure()
FigureCanvas.__init__(self, fig)
self.setParent(parent)
self.lef_mouse_pressed = False # 鼠標(biāo)左鍵是否按下
self.connect_event()
def connect_event(self):
return #添加鼠標(biāo)事件,后續(xù)在這里添加
def compute_initial_figure(self):
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) * 2
self.line1, = self.axes.plot(x, y1, 'b-', label='sin(x)')
self.line2, = self.axes.plot(x, y2, 'r-', label='cos(x)')
self.line3, = self.axes.plot(x, y3, 'g-', label='2*sin(x)')
self.axes.legend() # 顯示右上角標(biāo)簽
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.widget = QWidget()
self.setMinimumHeight(600)
self.setMinimumWidth(800)
self.showMaximized() # 設(shè)置全屏
self.setCentralWidget(self.widget)
layout = QVBoxLayout(self.widget)
self.mpl_widget = MatplotlibWidget(self.widget, width=5, height=4, dpi=100)
layout.addWidget(self.mpl_widget)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWin = MainWindow()
sys.exit(app.exec_())
現(xiàn)在的圖像應(yīng)該是這個效果

添加鼠標(biāo)滾動放大和縮小效果,比例可以自己設(shè)置,我這里設(shè)置的是最大值和最小值差值的10分之1
def on_mouse_wheel(self, event):
if self.axes is not None:
x_min, x_max = self.axes.get_xlim()
x_delta = (x_max - x_min) / 10 # 控制縮放X軸的比例
y_min, y_max = self.axes.get_ylim()
y_delta = (y_max - y_min) / 10 # 控制縮放X軸的比例
if event.button == "up":
self.axes.set(xlim=(x_min + x_delta, x_max - x_delta))
self.axes.set(ylim=(y_min + y_delta, y_max - y_delta))
elif event.button == "down":
self.axes.set(xlim=(x_min - x_delta, x_max + x_delta))
self.axes.set(ylim=(y_min - y_delta, y_max + y_delta))
self.draw_idle()
添加鼠標(biāo)滾動事件到connect_event()函數(shù)里面
def connect_event(self):
self.mpl_connect("scroll_event", self.on_mouse_wheel) #鼠標(biāo)滾動事件
實現(xiàn)按住鼠標(biāo)向上下左右拖動的效果,拖動的距離同理可以自己控制
def on_button_press(self, event):
if event.inaxes is not None: # 判斷是否在坐標(biāo)軸內(nèi)
if event.button == 1:
self.lef_mouse_pressed = True
self.pre_x = event.xdata
self.pre_y = event.ydata
def on_button_release(self, event):
self.lef_mouse_pressed = False
def on_mouse_move(self, event):
if event.inaxes is not None and event.button == 1:
if self.lef_mouse_pressed: #鼠標(biāo)左鍵按下時才計算
x_delta = event.xdata - self.pre_x
y_delta = event.ydata - self.pre_y
# 獲取當(dāng)前原點和最大點的4個位置
x_min, x_max = self.axes.get_xlim()
y_min, y_max = self.axes.get_ylim()
# 控制一次移動鼠標(biāo)拖動的距離
x_min = x_min - x_delta
x_max = x_max - x_delta
y_min = y_min - y_delta
y_max = y_max - y_delta
self.axes.set_xlim(x_min, x_max)
self.axes.set_ylim(y_min, y_max)
self.draw_idle()
添加鼠標(biāo)按住和松開事件到connect_event()函數(shù)里面
def connect_event(self):
self.mpl_connect("scroll_event", self.on_mouse_wheel)
self.mpl_connect("button_press_event", self.on_button_press)
self.mpl_connect("button_release_event", self.on_button_release)
self.mpl_connect("motion_notify_event", self.on_mouse_move)
現(xiàn)在圖像可以實現(xiàn)如下效果

可以看到鼠標(biāo)按住后把圖像拖動了,同時鼠標(biāo)滾動也放大了圖像
實現(xiàn)圖表的高階交互
我們最終想達(dá)到的目標(biāo)是能夠?qū)崿F(xiàn)類似echarts庫的功能,能夠隨著鼠標(biāo)移動顯示一條豎的光標(biāo)線,光標(biāo)線旁邊能夠顯示詳細(xì)信息,效果跟下圖所示差不多

? 接下來我們使用matplotlib實現(xiàn)如圖效果
給圖表添加一個豎光標(biāo)線,在圖表中可以隨著鼠標(biāo)移動而移動,在光標(biāo)線旁邊顯示相應(yīng)的曲線信息
def init_annotation(self):
# 初始化光標(biāo)線和注釋
self.vertline, = self.ax.plot([], [], 'c-', lw=2)
# 預(yù)置一個空文本顯示橫坐標(biāo)值
hPackerList = [HPacker(children=[TextArea("", textprops=dict(size=10))])]
for line in self.axes.get_lines():
if line == self.vertline: # 跳過光標(biāo)線
continue
line_color = line.get_color() # 獲取每條曲線的顏色
text_area = TextArea(line.get_label(), textprops=dict(size=10, color=line_color)) #根據(jù)曲線顏色設(shè)置文字顏色
hPacker = HPacker(children=[text_area])
hPackerList.append(hPacker)
self.text_box = VPacker(children=hPackerList, pad=1, sep=3) # 豎值布局,設(shè)置padding和文字之間上下的間距
self.annotation_bbox = AnnotationBbox(self.text_box, (0, 0),
xybox=(100, 0),
xycoords='data',
boxcoords="offset points")
if self.axes is not None:
self.axes.add_artist(self.annotation_bbox)
將init_annotation函數(shù)放到初始化init函數(shù)里面
添加hover函數(shù),使豎光標(biāo)線和注釋隨著鼠標(biāo)的移動能夠動態(tài)地做出改變,這里使用了annocation_bbox,這個工具網(wǎng)上資料很少,我也是臨時翻英文文檔看的,gpt生成的錯誤代碼用不了,如果需要詳細(xì)了解可以翻閱matlibplot的官方文檔,hPacker和vPacker類似于qt的hBoxlayout和vBoxLayout,還是比較好理解的,還有textarea是可以設(shè)置顏色的
def hover(self, event):
if event.inaxes == self.axes:
x = event.xdata
if x is not None:
text = f"x: {x}" #顯示橫坐標(biāo)值
hPacker_list = self.text_box.get_children()
time_hPacker = hPacker_list[0]
time_text_area: TextArea = time_hPacker.get_children()[0]
time_text_area.set_text(text) # 更新橫坐標(biāo)值
for index,line in enumerate(self.axes.get_lines()):
if line == self.vertline: # 跳過光標(biāo)線
continue
x_data = line.get_xdata()
y_data = line.get_ydata()
y = np.interp(x, x_data, y_data)
# 更新光標(biāo)線的位置
self.vertline.set_xdata([x, x])
self.vertline.set_ydata([self.axes.get_ylim()[0], self.axes.get_ylim()[1]])
# 顯示每條曲線的詳細(xì)信息
line_text = f"{line.get_label()}: {y:.3f}"
hPacker = hPacker_list[index+1] # 因為橫坐標(biāo)值放在了第一個hPacker中,所以從第二個開始
text_area: TextArea = hPacker.get_children()[0]
text_area.set_text(line_text)
# 更新AnnotationBbox的位置
self.annotation_bbox.xy = (x, event.ydata)
self.annotation_bbox.set_visible(True)
self.draw_idle()
else:
# 隱藏AnnotationBbox和光標(biāo)線
self.annotation_bbox.set_visible(False)
self.vertline.set_xdata([])
self.vertline.set_ydata([])
self.draw_idle()
將hover函數(shù)添加到connect_event()函數(shù)中
def connect_event(self):
self.mpl_connect("scroll_event", self.on_mouse_wheel)
self.mpl_connect("button_press_event", self.on_button_press)
self.mpl_connect("button_release_event", self.on_button_release)
self.mpl_connect("motion_notify_event", self.on_mouse_move)
self.mpl_connect("motion_notify_event", self.hover)
最終實現(xiàn)效果

全部代碼
import sys
import numpy as np
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from matplotlib.offsetbox import HPacker, TextArea, VPacker, AnnotationBbox
class MatplotlibWidget(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
self.axes = fig.add_subplot(111)
self.lef_mouse_pressed = False # 鼠標(biāo)左鍵是否按下
self.compute_initial_figure()
FigureCanvas.__init__(self, fig)
self.setParent(parent)
self.connect_event()
self.init_annotation()
def connect_event(self):
self.mpl_connect("scroll_event", self.on_mouse_wheel)
self.mpl_connect("button_press_event", self.on_button_press)
self.mpl_connect("button_release_event", self.on_button_release)
self.mpl_connect("motion_notify_event", self.on_mouse_move)
self.mpl_connect("motion_notify_event", self.hover)
def compute_initial_figure(self):
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.sin(x) * 2
self.line1, = self.axes.plot(x, y1, 'b-', label='sin(x)')
self.line2, = self.axes.plot(x, y2, 'r-', label='cos(x)')
self.line3, = self.axes.plot(x, y3, 'g-', label='2*sin(x)')
self.axes.legend() # 顯示右上角標(biāo)簽
def init_annotation(self):
# 初始化光標(biāo)線和注釋
self.vertline, = self.axes.plot([], [], 'c-', lw=2)
hPackerList = [HPacker(children=[TextArea("", textprops=dict(size=10))])] # 預(yù)置一個空文本顯示橫坐標(biāo)值
for line in self.axes.get_lines():
if line == self.vertline: # 跳過光標(biāo)線
continue
line_color = line.get_color() # 獲取每條曲線的顏色
text_area = TextArea(line.get_label(), textprops=dict(size=10, color=line_color)) #根據(jù)曲線顏色設(shè)置文字顏色
hPacker = HPacker(children=[text_area])
hPackerList.append(hPacker)
self.text_box = VPacker(children=hPackerList, pad=1, sep=3) # 豎值布局,設(shè)置padding和文字之間上下的間距
self.annotation_bbox = AnnotationBbox(self.text_box, (0, 0),
xybox=(100, 0),
xycoords='data',
boxcoords="offset points")
if self.axes is not None:
self.axes.add_artist(self.annotation_bbox)
def on_mouse_wheel(self, event):
if self.axes is not None:
x_min, x_max = self.axes.get_xlim()
x_delta = (x_max - x_min) / 10
y_min, y_max = self.axes.get_ylim()
y_delta = (y_max - y_min) / 10
if event.button == "up":
self.axes.set(xlim=(x_min + x_delta, x_max - x_delta))
self.axes.set(ylim=(y_min + y_delta, y_max - y_delta))
elif event.button == "down":
self.axes.set(xlim=(x_min - x_delta, x_max + x_delta))
self.axes.set(ylim=(y_min - y_delta, y_max + y_delta))
self.draw_idle()
def on_button_press(self, event):
if event.inaxes is not None: # 判斷是否在坐標(biāo)軸內(nèi)
if event.button == 1:
self.lef_mouse_pressed = True
self.pre_x = event.xdata
self.pre_y = event.ydata
def on_button_release(self, event):
self.lef_mouse_pressed = False
def on_mouse_move(self, event):
if event.inaxes is not None and event.button == 1:
if self.lef_mouse_pressed:
x_delta = event.xdata - self.pre_x
y_delta = event.ydata - self.pre_y
# 獲取當(dāng)前原點和最大點的4個位置
x_min, x_max = self.axes.get_xlim()
y_min, y_max = self.axes.get_ylim()
x_min = x_min - x_delta
x_max = x_max - x_delta
y_min = y_min - y_delta
y_max = y_max - y_delta
self.axes.set_xlim(x_min, x_max)
self.axes.set_ylim(y_min, y_max)
self.draw_idle()
def hover(self, event):
if event.inaxes == self.axes:
x = event.xdata
if x is not None:
text = f"x: {x}" #顯示橫坐標(biāo)值
hPacker_list = self.text_box.get_children()
time_hPacker = hPacker_list[0]
time_text_area: TextArea = time_hPacker.get_children()[0]
time_text_area.set_text(text) # 更新橫坐標(biāo)值
for index,line in enumerate(self.axes.get_lines()):
if line == self.vertline: # 跳過光標(biāo)線
continue
x_data = line.get_xdata()
y_data = line.get_ydata()
y = np.interp(x, x_data, y_data)
# 更新光標(biāo)線的位置
self.vertline.set_xdata([x, x])
self.vertline.set_ydata([self.axes.get_ylim()[0], self.axes.get_ylim()[1]])
# 顯示每條曲線的詳細(xì)信息
line_text = f"{line.get_label()}: {y:.3f}"
hPacker = hPacker_list[index+1] # 因為橫坐標(biāo)值放在了第一個hPacker中,所以從第二個開始
text_area: TextArea = hPacker.get_children()[0]
text_area.set_text(line_text)
# 更新AnnotationBbox的位置
self.annotation_bbox.xy = (x, event.ydata)
self.annotation_bbox.set_visible(True)
self.draw_idle()
else:
# 隱藏AnnotationBbox和光標(biāo)線
self.annotation_bbox.set_visible(False)
self.vertline.set_xdata([])
self.vertline.set_ydata([])
self.draw_idle()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.widget = QWidget()
self.setMinimumHeight(600)
self.setMinimumWidth(800)
self.showMaximized() # 設(shè)置全屏
self.setCentralWidget(self.widget)
layout = QVBoxLayout(self.widget)
self.mpl_widget = MatplotlibWidget(self.widget, width=5, height=4, dpi=100)
layout.addWidget(self.mpl_widget)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWin = MainWindow()
sys.exit(app.exec_())
總結(jié)
感覺matplotlib庫更適用于靜態(tài)圖表的分析,即使自己造了這些輪子,但總感覺還是不如echats好用,不過echarts性能方面還是不如matplotlib的,畢竟兩者的應(yīng)用場景確實不一樣。
到此這篇關(guān)于Python結(jié)合matplotlib實現(xiàn)圖表的基本交互的文章就介紹到這了,更多相關(guān)Python matplotlib圖表交互內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python可視化單詞統(tǒng)計詞頻統(tǒng)計中文分詞的實現(xiàn)步驟
這篇文章主要介紹了Python可視化單詞統(tǒng)計詞頻統(tǒng)計中文分詞,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-11-11
Python中g(shù)etattr函數(shù)和hasattr函數(shù)作用詳解
這篇文章主要介紹了Python中g(shù)etattr函數(shù)和hasattr函數(shù)作用的相關(guān)知識,非常不錯具有參考借鑒價值,需要的朋友可以參考下2016-06-06

