Python Matplotlib 动画 交互
Python Matplotlib 动画 交互
Matplotlib 不仅支持静态图表,还能创建动态动画(如实时数据更新、运动轨迹展示)并保存为 GIF,同时提供交互功能(如鼠标点击、按键控制、悬停提示)。
一、Matplotlib 动画保存 GIF
Matplotlib 动画依赖 matplotlib.animation 模块,其中 FuncAnimation 是最常用的类(适合逐帧更新的动画)。保存 GIF 需依赖外部库 Pillow(用于图像编码)。
1. 环境准备
1.1 安装依赖库
pip install matplotlib numpy pillow # pillow 是保存 GIF 的必需库1.2 概念
FuncAnimation:通过反复调用“帧更新函数”生成动画,参数:fig:动画所属的画布(Figure)。func:帧更新函数(每帧执行一次,负责更新图形数据)。frames:动画的总帧数(或生成帧的迭代器)。interval:帧间隔时间(单位:毫秒,控制动画速度)。init_func:初始化函数(可选,用于绘制初始图形)。
- 保存 GIF:通过
animation.save()实现,指定writer='pillow'(Pillow 提供的 GIF 编码器)。
2. 基础动画:动态正弦波(保存为 GIF)
以“正弦波沿 x 轴移动”为例,演示动画制作与 GIF 保存的完整流程。
import matplotlib.pyplot as plt import numpy as np from matplotlib.animation import FuncAnimation # 1. 中文与负号支持 plt.rcParams['font.sans-serif']=['SimHei'] plt.rcParams['axes.unicode_minus']=False# 2. 准备画布与子图 fig, ax = plt.subplots(figsize=(8,4)) x = np.linspace(0,2*np.pi,100)# 固定 x 轴范围 line,= ax.plot(x, np.sin(x), color='blue', linewidth=2)# 初始正弦波(注意逗号:返回的是列表元素) ax.set_ylim(-1.2,1.2)# 固定 y 轴范围(避免动画抖动) ax.set_title('动态正弦波(沿 x 轴移动)') ax.set_xlabel('x') ax.set_ylabel('sin(x + t)') ax.grid(alpha=0.3)# 3. 定义初始化函数(可选:初始化图形状态)definit(): line.set_ydata(np.sin(x))# 初始 y 数据return line,# 必须返回更新的图形元素(元组形式)# 4. 定义帧更新函数(核心:每帧更新数据)defupdate(frame):# frame 是帧索引(从 0 到 frames-1),控制动画进度 y = np.sin(x + frame *0.1)# 每帧沿 x 轴偏移 0.1 line.set_ydata(y)# 更新线条的 y 数据return line,# 返回更新的图形元素# 5. 创建动画对象 ani = FuncAnimation( fig=fig, func=update, frames=120,# 总帧数(120 帧,按 30fps 算,动画时长 4 秒) init_func=init, interval=33,# 帧间隔(33ms ≈ 30fps) blit=True,# 只更新变化的部分,提升动画流畅度 repeat=True# 动画循环播放)# 6. 保存为 GIF(需安装 pillow) ani.save('sin_wave_animation.gif', writer='pillow',# 指定 GIF 编码器 fps=30,# 帧率(与 interval 匹配:1000/33 ≈ 30) dpi=100# 分辨率(过高会导致 GIF 体积过大))# 7. 显示动画(脚本中需加此句,Jupyter 中可省略) plt.show()print("动画已保存为 sin_wave_animation.gif")3. 进阶动画:多子图同步动画
演示 2x1 子图的同步动画(上方动态散点图,下方动态柱状图)。
import matplotlib.pyplot as plt import numpy as np from matplotlib.animation import FuncAnimation # 确保中文显示正常 plt.rcParams['font.sans-serif']=['SimHei'] plt.rcParams['axes.unicode_minus']=False# 1. 准备画布与子图 fig,(ax1, ax2)= plt.subplots(nrows=2, ncols=1, figsize=(8,6), height_ratios=[2,1])# 子图1:动态散点图(随机运动的点) n_points =50 x_scatter = np.random.rand(n_points) y_scatter = np.random.rand(n_points) scatter = ax1.scatter(x_scatter, y_scatter, color='red', alpha=0.7) ax1.set_xlim(0,1) ax1.set_ylim(0,1) ax1.set_title('动态散点图(随机运动)') ax1.grid(alpha=0.3)# 子图2:动态柱状图(数值波动) categories =['A','B','C'] y_bar = np.random.randint(1,10,3) bars = ax2.bar(categories, y_bar, color='blue', alpha=0.7) ax2.set_ylim(0,10) ax2.set_title('动态柱状图(数值波动)') ax2.grid(axis='y', alpha=0.3)# 2. 帧更新函数(同步更新两个子图)defupdate(frame):# 更新散点图(随机微小移动) x_new = x_scatter +(np.random.randn(n_points)*0.01) y_new = y_scatter +(np.random.randn(n_points)*0.01) x_new = np.clip(x_new,0,1)# 限制点在 [0,1] 范围内 y_new = np.clip(y_new,0,1) scatter.set_offsets(np.c_[x_new, y_new])# 更新柱状图(数值波动) y_bar_new = y_bar +(np.random.randint(-1,2,3)) y_bar_new = np.clip(y_bar_new,1,9)# 限制数值范围for bar, y inzip(bars, y_bar_new): bar.set_height(y)# 修复:将bars转换为元组,确保blit正确处理所有艺术家对象return(scatter,)+tuple(bars)# 3. 创建动画并保存 ani = FuncAnimation( fig=fig, func=update, frames=60,# 60帧,约2秒动画 interval=33,# 33ms/帧 ~ 30fps blit=True, repeat=True)# 保存动画(关键:保存后显式关闭画布避免资源泄漏) ani.save('multi_subplot_animation.gif', writer='pillow', fps=30, dpi=100) plt.close(fig)# 保存后关闭画布,避免残留资源导致错误print("多子图动画已保存为 multi_subplot_animation.gif")4. 保存 GIF
- 帧率匹配:
interval(毫秒)与fps(帧率)需匹配,公式:fps ≈ 1000 / interval(如 33ms 对应 30fps)。 - 体积控制:
dpi不宜过高(建议 100-150),帧数不宜过多(单 GIF 建议 < 300 帧),避免体积过大。 - 编码器依赖:必须安装
pillow,否则保存时会报错(No MovieWriter available for format 'gif')。 - 循环播放:
repeat=True控制动画是否循环,repeat_delay可设置循环间隔(如repeat_delay=1000表示暂停 1 秒)。
二、Matplotlib 交互功能
Matplotlib 提供两种交互模式:脚本交互模式(适合 Python 脚本)和 Notebook 交互模式(适合 Jupyter 环境),核心通过“事件绑定”实现鼠标/键盘交互。
import matplotlib.pyplot as plt import numpy as np from matplotlib.animation import FuncAnimation # -------------------------- 1. 初始化配置 --------------------------# 中文与负号显示支持 plt.rcParams['font.sans-serif']=['SimHei'] plt.rcParams['axes.unicode_minus']=False# 固定随机种子,确保结果可复现 np.random.seed(42)# 开启交互模式 plt.ion()# -------------------------- 2. 创建画布与初始图形 -------------------------- fig,(ax1, ax2)= plt.subplots(nrows=2, ncols=1, figsize=(10,8), height_ratios=[3,2])# -------------------------- 2.1 上方子图:动态折线图 -------------------------- x_line = np.linspace(0,10,100) y_line = np.sin(x_line) line,= ax1.plot( x_line, y_line, color='royalblue', linewidth=2, label='动态正弦波') ax1.set_xlim(0,10) ax1.set_ylim(-1.5,1.5) ax1.set_title('交互+动画演示:上方折线图(自动移动) | 下方散点图(点击添加)', fontsize=14) ax1.set_xlabel('x轴', fontsize=12) ax1.set_ylabel('sin(x + t)', fontsize=12) ax1.legend(loc='upper right') ax1.grid(alpha=0.3)# 交互提示文本 hint_text1 = ax1.text(0.5,-1.3,'按键控制:C=切换颜色 | W=变线宽 | R=重置 | ESC=退出', ha='center', va='center', fontsize=10, transform=ax1.transAxes )# -------------------------- 2.2 下方子图:交互散点图 -------------------------- click_points_x =[] click_points_y =[] scatter = ax2.scatter( click_points_x, click_points_y, color='crimson', s=80, alpha=0.7, label='点击添加的点')# 鼠标悬停注释 hover_annot = ax2.annotate('', xy=(0,0), xytext=(10,10), textcoords='offset points', bbox=dict(boxstyle='round,pad=0.3', fc='yellow', alpha=0.8), arrowprops=dict(arrowstyle='->', color='black')) hover_annot.set_visible(False) ax2.set_xlim(0,10) ax2.set_ylim(0,10) ax2.set_xlabel('x轴(点击添加点)', fontsize=12) ax2.set_ylabel('y轴', fontsize=12) ax2.legend(loc='upper left') ax2.grid(alpha=0.3) hint_text2 = ax2.text(0.5,-0.2,'鼠标操作:左键添加点 | 右键清除 | 悬停显示坐标', ha='center', va='center', fontsize=10, transform=ax2.transAxes )# -------------------------- 3. 动画与交互逻辑 --------------------------defanimate(frame): new_y = np.sin(x_line + frame *0.05) line.set_ydata(new_y)return line,# 关键修复1:frames使用大整数(避免float),并显式保存动画对象到变量 anim = FuncAnimation( fig=fig, func=animate, frames=10000,# 大整数替代浮点数,确保range()可用 interval=50, blit=True, repeat=True# 循环播放,实现"无限"效果)# 鼠标点击事件defon_mouse_click(event):if event.inaxes != ax2:returnif event.button ==1: click_points_x.append(event.xdata) click_points_y.append(event.ydata) scatter.set_offsets(np.c_[click_points_x, click_points_y])elif event.button ==3: click_points_x.clear() click_points_y.clear() scatter.set_offsets(np.c_[click_points_x, click_points_y]) fig.canvas.draw()# 键盘事件 initial_color = line.get_color() initial_linewidth = line.get_linewidth()defon_key_press(event):if event.key.lower()=='c': line.set_color('crimson'if line.get_color()=='royalblue'else'royalblue')elif event.key.lower()=='w': line.set_linewidth(4if line.get_linewidth()==2else2)elif event.key.lower()=='r': line.set_color(initial_color) line.set_linewidth(initial_linewidth) click_points_x.clear() click_points_y.clear() scatter.set_offsets(np.c_[click_points_x, click_points_y]) hover_annot.set_visible(False)elif event.key =='escape': plt.ioff() plt.close(fig)print("程序已退出") exit() fig.canvas.draw()# 鼠标悬停事件defon_mouse_hover(event):if event.inaxes != ax2:if hover_annot.get_visible(): hover_annot.set_visible(False) fig.canvas.draw_idle()return contains, info = scatter.contains(event)if contains: point_idx = info['ind'][0] x_pos = click_points_x[point_idx] y_pos = click_points_y[point_idx] hover_annot.xy =(x_pos, y_pos) hover_annot.set_text(f'坐标:({x_pos:.2f}, {y_pos:.2f})') hover_annot.set_visible(True) fig.canvas.draw_idle()else:if hover_annot.get_visible(): hover_annot.set_visible(False) fig.canvas.draw_idle()# 绑定事件 click_cid = fig.canvas.mpl_connect('button_press_event', on_mouse_click) key_cid = fig.canvas.mpl_connect('key_press_event', on_key_press) hover_cid = fig.canvas.mpl_connect('motion_notify_event', on_mouse_hover)# -------------------------- 4. 保持程序运行 --------------------------print("程序已启动!交互说明:")print("1. 上方折线图:自动移动(动画)")print("2. 键盘控制:C=切换颜色 | W=变线宽 | R=重置 | ESC=退出")print("3. 鼠标操作:左键添加点 | 右键清除 | 悬停显示坐标")# 关键修复2:显式引用动画对象(anim),避免被垃圾回收while plt.fignum_exists(fig.number): plt.pause(0.1)# 资源清理 fig.canvas.mpl_disconnect(click_cid) fig.canvas.mpl_disconnect(key_cid) fig.canvas.mpl_disconnect(hover_cid) plt.ioff() plt.close(fig)print("资源已释放")用 3D 散点图展示分布
将素数映射到3D空间
核心逻辑是将每个素数(一维数值)通过三角函数转换为三维坐标(x,y,z),具体公式为:
- x=p⋅sin(p)⋅cos(p)x = p \cdot \sin(p) \cdot \cos(p)x=p⋅sin(p)⋅cos(p)
- y=p⋅(sin(p))2y = p \cdot (\sin(p))^2y=p⋅(sin(p))2
- z=p⋅cos(p)z = p \cdot \cos(p)z=p⋅cos(p)
在图形窗口中拖动鼠标,可从不同角度查看3D图形
import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np # 判断单个数字是否为质数defis_prime(n):if n <=1:returnFalseif n <=3:returnTrueif n %2==0or n %3==0:returnFalse i =5while i * i <= n:if n % i ==0or n %(i +2)==0:returnFalse i +=6returnTrue# 生成前 N 个质数defgenerate_primes(count): primes =[] num =2# 从2开始(最小的质数)whilelen(primes)< count:if is_prime(num): primes.append(num) num +=1return primes # 生成质数(可调整数量) primes = generate_primes(count=10000) p = np.array(primes, dtype=np.float64)# 转换为浮点数数组# 计算3D坐标 x = p * np.sin(p)* np.cos(p) y = p *(np.sin(p))**2 z = p * np.cos(p)# 创建3D图形 fig = plt.figure(figsize=(10,8)) ax = fig.add_subplot(111, projection='3d')# 绘制散点图(使用质数大小作为颜色映射的依据,彩虹色增强视觉效果)# 修正:使用cmap参数指定颜色映射,c参数使用数值序列 sc = ax.scatter(x, y, z, c=p, cmap='rainbow', s=5, alpha=0.8)# 添加颜色条,显示颜色与质数大小的对应关系 cbar = plt.colorbar(sc, ax=ax) cbar.set_label('Prime Number Value')# 设置标题与坐标轴标签 ax.set_title("Prime Numbers in 3D Space") ax.set_xlabel("X Axis") ax.set_ylabel("Y Axis") ax.set_zlabel("Z Axis")# 提示用户操作方式print("提示:在图形窗口中拖动鼠标,可从不同角度查看3D图形~")# 显示图形 plt.show()