7.1 动画制作基础

7.1.1 FuncAnimation 基础

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import pandas as pd
from matplotlib.patches import Circle, Rectangle
from matplotlib.collections import LineCollection
import time

class AnimationBasics:
    """动画制作基础演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
        
        # 生成示例数据
        self.generate_animation_data()
    
    def generate_animation_data(self):
        """生成动画演示数据"""
        np.random.seed(42)
        
        # 时间序列数据
        self.time_steps = 100
        self.x = np.linspace(0, 4*np.pi, self.time_steps)
        self.y_sin = np.sin(self.x)
        self.y_cos = np.cos(self.x)
        
        # 随机游走数据
        self.random_walk = np.cumsum(np.random.randn(200))
        
        # 散点动画数据
        self.n_points = 50
        self.scatter_x = np.random.randn(self.n_points)
        self.scatter_y = np.random.randn(self.n_points)
        self.scatter_colors = np.random.rand(self.n_points)
        
        # 柱状图动画数据
        self.categories = ['A', 'B', 'C', 'D', 'E']
        self.bar_data = np.random.rand(50, 5) * 100
    
    def simple_line_animation_demo(self):
        """简单线图动画演示"""
        fig, ax = plt.subplots(figsize=(10, 6))
        
        # 初始化空线图
        line, = ax.plot([], [], 'b-', linewidth=2)
        ax.set_xlim(0, 4*np.pi)
        ax.set_ylim(-1.5, 1.5)
        ax.set_title('正弦波动画演示')
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.grid(True, alpha=0.3)
        
        def animate(frame):
            """动画函数"""
            # 更新数据
            x_data = self.x[:frame+1]
            y_data = self.y_sin[:frame+1]
            
            # 更新线图
            line.set_data(x_data, y_data)
            
            # 更新标题显示当前帧
            ax.set_title(f'正弦波动画演示 - 帧: {frame+1}/{self.time_steps}')
            
            return line,
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=self.time_steps, 
            interval=50, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        # 保存动画(注释掉以避免文件生成)
        # anim.save('sine_wave_animation.gif', writer='pillow', fps=20)
        
        return anim
    
    def multi_line_animation_demo(self):
        """多线动画演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        # 初始化多条线
        line1, = ax.plot([], [], 'b-', linewidth=2, label='sin(x)')
        line2, = ax.plot([], [], 'r-', linewidth=2, label='cos(x)')
        line3, = ax.plot([], [], 'g-', linewidth=2, label='sin(2x)')
        
        ax.set_xlim(0, 4*np.pi)
        ax.set_ylim(-1.5, 1.5)
        ax.set_title('多函数动画演示')
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        def animate(frame):
            """多线动画函数"""
            # 计算当前帧的数据
            x_data = self.x[:frame+1]
            y1_data = self.y_sin[:frame+1]
            y2_data = self.y_cos[:frame+1]
            y3_data = np.sin(2 * x_data)
            
            # 更新所有线图
            line1.set_data(x_data, y1_data)
            line2.set_data(x_data, y2_data)
            line3.set_data(x_data, y3_data)
            
            # 更新标题
            ax.set_title(f'多函数动画演示 - 进度: {frame+1}/{self.time_steps}')
            
            return line1, line2, line3
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=self.time_steps,
            interval=80, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        return anim
    
    def scatter_animation_demo(self):
        """散点图动画演示"""
        fig, ax = plt.subplots(figsize=(10, 8))
        
        # 初始化散点图
        scat = ax.scatter([], [], c=[], s=[], cmap='viridis', alpha=0.7)
        
        ax.set_xlim(-3, 3)
        ax.set_ylim(-3, 3)
        ax.set_title('动态散点图演示')
        ax.set_xlabel('X 坐标')
        ax.set_ylabel('Y 坐标')
        ax.grid(True, alpha=0.3)
        
        # 添加颜色条
        cbar = plt.colorbar(scat, ax=ax)
        cbar.set_label('颜色值')
        
        def animate(frame):
            """散点动画函数"""
            # 更新散点位置(添加随机扰动)
            noise_scale = 0.1
            x_data = self.scatter_x + noise_scale * np.random.randn(self.n_points)
            y_data = self.scatter_y + noise_scale * np.random.randn(self.n_points)
            
            # 动态改变大小
            sizes = 50 + 100 * np.sin(frame * 0.1 + np.arange(self.n_points))
            
            # 动态改变颜色
            colors = (self.scatter_colors + frame * 0.01) % 1.0
            
            # 更新散点图
            scat.set_offsets(np.column_stack([x_data, y_data]))
            scat.set_sizes(sizes)
            scat.set_array(colors)
            
            # 更新标题
            ax.set_title(f'动态散点图演示 - 帧: {frame}')
            
            return scat,
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=200,
            interval=100, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        return anim
    
    def bar_chart_animation_demo(self):
        """柱状图动画演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        # 初始化柱状图
        bars = ax.bar(self.categories, self.bar_data[0], 
                     color=['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#ff99cc'])
        
        ax.set_ylim(0, 120)
        ax.set_title('动态柱状图演示')
        ax.set_xlabel('类别')
        ax.set_ylabel('数值')
        ax.grid(True, alpha=0.3, axis='y')
        
        # 添加数值标签
        value_labels = []
        for bar in bars:
            label = ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
                           f'{bar.get_height():.1f}', ha='center', va='bottom',
                           fontweight='bold')
            value_labels.append(label)
        
        def animate(frame):
            """柱状图动画函数"""
            # 获取当前帧的数据
            current_data = self.bar_data[frame % len(self.bar_data)]
            
            # 更新柱状图高度
            for bar, value in zip(bars, current_data):
                bar.set_height(value)
            
            # 更新数值标签
            for label, bar, value in zip(value_labels, bars, current_data):
                label.set_position((bar.get_x() + bar.get_width()/2, value + 1))
                label.set_text(f'{value:.1f}')
            
            # 更新标题
            ax.set_title(f'动态柱状图演示 - 数据集: {frame % len(self.bar_data) + 1}')
            
            return bars + value_labels
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=len(self.bar_data),
            interval=200, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        return anim

# 使用示例
anim_basics = AnimationBasics()

# 演示各种动画类型
print("1. 简单线图动画")
anim1 = anim_basics.simple_line_animation_demo()

print("2. 多线动画")
anim2 = anim_basics.multi_line_animation_demo()

print("3. 散点图动画")
anim3 = anim_basics.scatter_animation_demo()

print("4. 柱状图动画")
anim4 = anim_basics.bar_chart_animation_demo()

7.1.2 关键帧动画

class KeyframeAnimation:
    """关键帧动画演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
        
        # 生成关键帧数据
        self.generate_keyframe_data()
    
    def generate_keyframe_data(self):
        """生成关键帧数据"""
        # 定义关键帧时间点
        self.keyframes = {
            0: {'x': 0, 'y': 0, 'size': 50, 'color': 'blue'},
            25: {'x': 2, 'y': 1, 'size': 100, 'color': 'green'},
            50: {'x': 4, 'y': 0, 'size': 150, 'color': 'red'},
            75: {'x': 2, 'y': -1, 'size': 100, 'color': 'orange'},
            100: {'x': 0, 'y': 0, 'size': 50, 'color': 'blue'}
        }
        
        # 销售数据关键帧
        self.sales_keyframes = {
            0: [10, 15, 20, 25, 30],
            20: [15, 25, 35, 40, 45],
            40: [25, 40, 55, 60, 65],
            60: [35, 55, 75, 80, 85],
            80: [45, 70, 95, 100, 105],
            100: [55, 85, 115, 120, 125]
        }
    
    def interpolate_values(self, start_val, end_val, progress):
        """线性插值函数"""
        if isinstance(start_val, (int, float)):
            return start_val + (end_val - start_val) * progress
        elif isinstance(start_val, str):  # 颜色插值(简化版)
            return start_val if progress < 0.5 else end_val
        elif isinstance(start_val, list):
            return [self.interpolate_values(s, e, progress) 
                   for s, e in zip(start_val, end_val)]
        else:
            return start_val
    
    def get_interpolated_frame(self, frame, total_frames, keyframes):
        """获取插值后的帧数据"""
        # 计算当前进度
        progress = (frame / total_frames) * 100
        
        # 找到相邻的关键帧
        keyframe_times = sorted(keyframes.keys())
        
        # 找到当前帧所在的区间
        start_key = keyframe_times[0]
        end_key = keyframe_times[-1]
        
        for i in range(len(keyframe_times) - 1):
            if keyframe_times[i] <= progress <= keyframe_times[i + 1]:
                start_key = keyframe_times[i]
                end_key = keyframe_times[i + 1]
                break
        
        # 计算区间内的进度
        if end_key == start_key:
            local_progress = 0
        else:
            local_progress = (progress - start_key) / (end_key - start_key)
        
        # 插值计算
        start_frame = keyframes[start_key]
        end_frame = keyframes[end_key]
        
        interpolated = {}
        for key in start_frame:
            interpolated[key] = self.interpolate_values(
                start_frame[key], end_frame[key], local_progress
            )
        
        return interpolated
    
    def keyframe_scatter_animation_demo(self):
        """关键帧散点动画演示"""
        fig, ax = plt.subplots(figsize=(10, 8))
        
        # 初始化散点
        scat = ax.scatter([], [], s=[], c=[], alpha=0.8)
        
        ax.set_xlim(-1, 5)
        ax.set_ylim(-2, 2)
        ax.set_title('关键帧散点动画演示')
        ax.set_xlabel('X 坐标')
        ax.set_ylabel('Y 坐标')
        ax.grid(True, alpha=0.3)
        
        # 添加关键帧标记
        for frame_num, data in self.keyframes.items():
            ax.plot(data['x'], data['y'], 'ko', markersize=8, alpha=0.3)
            ax.annotate(f'关键帧{frame_num}', 
                       (data['x'], data['y']), 
                       xytext=(10, 10), textcoords='offset points',
                       fontsize=8, alpha=0.6)
        
        total_frames = 100
        
        def animate(frame):
            """关键帧动画函数"""
            # 获取插值后的帧数据
            current_data = self.get_interpolated_frame(frame, total_frames, self.keyframes)
            
            # 更新散点
            scat.set_offsets([[current_data['x'], current_data['y']]])
            scat.set_sizes([current_data['size']])
            scat.set_color([current_data['color']])
            
            # 更新标题
            progress = (frame / total_frames) * 100
            ax.set_title(f'关键帧散点动画演示 - 进度: {progress:.1f}%')
            
            return scat,
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=total_frames,
            interval=100, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        return anim
    
    def keyframe_bar_animation_demo(self):
        """关键帧柱状图动画演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        categories = ['产品A', '产品B', '产品C', '产品D', '产品E']
        colors = ['#ff9999', '#66b3ff', '#99ff99', '#ffcc99', '#ff99cc']
        
        # 初始化柱状图
        bars = ax.bar(categories, self.sales_keyframes[0], color=colors, alpha=0.8)
        
        ax.set_ylim(0, 140)
        ax.set_title('销售数据关键帧动画')
        ax.set_xlabel('产品类别')
        ax.set_ylabel('销售额 (万元)')
        ax.grid(True, alpha=0.3, axis='y')
        
        # 添加数值标签
        value_labels = []
        for bar in bars:
            label = ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 2,
                           f'{bar.get_height():.0f}', ha='center', va='bottom',
                           fontweight='bold', fontsize=10)
            value_labels.append(label)
        
        # 添加关键帧时间线
        timeline_ax = fig.add_axes([0.1, 0.02, 0.8, 0.03])
        timeline_ax.set_xlim(0, 100)
        timeline_ax.set_ylim(-0.5, 0.5)
        timeline_ax.set_xticks(list(self.sales_keyframes.keys()))
        timeline_ax.set_xticklabels([f'第{k}周' for k in self.sales_keyframes.keys()])
        timeline_ax.set_yticks([])
        timeline_ax.set_title('时间线', fontsize=8)
        
        # 时间线标记
        for key in self.sales_keyframes.keys():
            timeline_ax.axvline(key, color='red', alpha=0.5, linestyle='--')
        
        # 当前位置标记
        current_marker, = timeline_ax.plot([], [], 'ro', markersize=8)
        
        total_frames = 100
        
        def animate(frame):
            """关键帧柱状图动画函数"""
            # 获取插值后的数据
            current_data = self.get_interpolated_frame(
                frame, total_frames, self.sales_keyframes
            )
            
            # 更新柱状图
            for bar, value in zip(bars, current_data):
                bar.set_height(value)
            
            # 更新数值标签
            for label, bar, value in zip(value_labels, bars, current_data):
                label.set_position((bar.get_x() + bar.get_width()/2, value + 2))
                label.set_text(f'{value:.0f}')
            
            # 更新时间线标记
            progress = (frame / total_frames) * 100
            current_marker.set_data([progress], [0])
            
            # 更新标题
            week = int(progress / 100 * 25)  # 假设总共25周
            ax.set_title(f'销售数据关键帧动画 - 第{week}周')
            
            return bars + value_labels + [current_marker]
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=total_frames,
            interval=150, blit=True, repeat=True
        )
        
        plt.tight_layout()
        plt.show()
        
        return anim
    
    def easing_functions_demo(self):
        """缓动函数演示"""
        fig, axes = plt.subplots(2, 2, figsize=(16, 12))
        
        # 定义不同的缓动函数
        def linear(t):
            return t
        
        def ease_in_quad(t):
            return t * t
        
        def ease_out_quad(t):
            return 1 - (1 - t) * (1 - t)
        
        def ease_in_out_quad(t):
            if t < 0.5:
                return 2 * t * t
            else:
                return 1 - 2 * (1 - t) * (1 - t)
        
        easing_functions = [
            (linear, '线性', axes[0, 0]),
            (ease_in_quad, '缓入', axes[0, 1]),
            (ease_out_quad, '缓出', axes[1, 0]),
            (ease_in_out_quad, '缓入缓出', axes[1, 1])
        ]
        
        t_values = np.linspace(0, 1, 100)
        
        for easing_func, title, ax in easing_functions:
            # 绘制缓动曲线
            y_values = [easing_func(t) for t in t_values]
            ax.plot(t_values, y_values, 'b-', linewidth=3, label='缓动曲线')
            
            # 绘制线性参考线
            ax.plot([0, 1], [0, 1], 'r--', alpha=0.5, label='线性参考')
            
            # 演示动画效果(静态展示多个位置)
            demo_times = np.linspace(0, 1, 10)
            for i, t in enumerate(demo_times):
                eased_t = easing_func(t)
                ax.plot(t, eased_t, 'ro', markersize=6, alpha=0.7)
                
                # 在右侧显示实际动画效果
                x_pos = 1.1 + 0.3 * t
                y_pos = 0.1 + 0.8 * eased_t
                ax.plot(x_pos, y_pos, 'go', markersize=8, alpha=0.8)
            
            ax.set_xlim(-0.1, 1.5)
            ax.set_ylim(-0.1, 1.1)
            ax.set_title(f'{title}缓动函数')
            ax.set_xlabel('时间 (t)')
            ax.set_ylabel('进度')
            ax.legend()
            ax.grid(True, alpha=0.3)
            
            # 添加说明文字
            ax.text(1.2, 0.5, '动画\n效果', ha='center', va='center',
                   bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7))
        
        plt.tight_layout()
        plt.show()

# 使用示例
keyframe_anim = KeyframeAnimation()

print("1. 关键帧散点动画")
anim1 = keyframe_anim.keyframe_scatter_animation_demo()

print("2. 关键帧柱状图动画")
anim2 = keyframe_anim.keyframe_bar_animation_demo()

print("3. 缓动函数演示")
keyframe_anim.easing_functions_demo()

7.1.3 动画保存和导出

class AnimationExport:
    """动画保存和导出演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
    
    def create_sample_animation(self):
        """创建示例动画"""
        fig, ax = plt.subplots(figsize=(10, 6))
        
        # 生成数据
        x = np.linspace(0, 2*np.pi, 100)
        
        # 初始化线图
        line, = ax.plot([], [], 'b-', linewidth=2)
        ax.set_xlim(0, 2*np.pi)
        ax.set_ylim(-1.5, 1.5)
        ax.set_title('示例动画 - 正弦波')
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.grid(True, alpha=0.3)
        
        def animate(frame):
            """动画函数"""
            # 计算当前帧的数据
            phase = frame * 0.1
            y = np.sin(x + phase)
            
            # 更新线图
            line.set_data(x, y)
            
            # 更新标题
            ax.set_title(f'示例动画 - 正弦波 (相位: {phase:.2f})')
            
            return line,
        
        # 创建动画
        anim = animation.FuncAnimation(
            fig, animate, frames=100,
            interval=100, blit=True, repeat=True
        )
        
        return fig, anim
    
    def save_animation_examples(self):
        """动画保存示例"""
        print("创建示例动画...")
        fig, anim = self.create_sample_animation()
        
        # 1. 保存为 GIF
        print("\n1. 保存为 GIF 格式")
        print("代码示例:")
        print("anim.save('animation.gif', writer='pillow', fps=10)")
        print("参数说明:")
        print("- writer='pillow': 使用 Pillow 库作为 GIF 写入器")
        print("- fps=10: 每秒10帧")
        print("- dpi=100: 图像分辨率")
        
        # 2. 保存为 MP4
        print("\n2. 保存为 MP4 格式")
        print("代码示例:")
        print("anim.save('animation.mp4', writer='ffmpeg', fps=20, bitrate=1800)")
        print("参数说明:")
        print("- writer='ffmpeg': 使用 FFmpeg 作为视频编码器")
        print("- fps=20: 每秒20帧")
        print("- bitrate=1800: 视频比特率")
        
        # 3. 保存为 HTML
        print("\n3. 保存为 HTML 格式")
        print("代码示例:")
        print("anim.save('animation.html', writer='html')")
        print("参数说明:")
        print("- writer='html': 生成可在浏览器中播放的 HTML 文件")
        
        # 4. 自定义写入器设置
        print("\n4. 自定义写入器设置")
        print("代码示例:")
        print("""
# 自定义 FFmpeg 写入器
Writer = animation.writers['ffmpeg']
writer = Writer(fps=15, metadata=dict(artist='Me'), bitrate=1800)
anim.save('custom_animation.mp4', writer=writer)

# 自定义 Pillow 写入器
Writer = animation.writers['pillow']
writer = Writer(fps=10)
anim.save('custom_animation.gif', writer=writer)
        """)
        
        # 5. 保存单帧图像
        print("\n5. 保存单帧图像")
        print("代码示例:")
        print("""
# 保存特定帧
fig.savefig('frame_50.png', dpi=150, bbox_inches='tight')

# 批量保存所有帧
for i in range(100):
    animate(i)  # 更新到第i帧
    fig.savefig(f'frame_{i:03d}.png', dpi=150, bbox_inches='tight')
        """)
        
        plt.show()
        
        return anim
    
    def optimization_tips_demo(self):
        """动画优化技巧演示"""
        print("动画性能优化技巧:")
        
        print("\n1. 使用 blit 技术")
        print("代码示例:")
        print("""
# 启用 blit 可以显著提高动画性能
anim = animation.FuncAnimation(
    fig, animate, frames=100,
    interval=50, blit=True, repeat=True
)

# 注意:使用 blit 时,animate 函数必须返回所有更新的艺术家对象
def animate(frame):
    line.set_data(x_data, y_data)
    return line,  # 注意这里的逗号
        """)
        
        print("\n2. 减少不必要的重绘")
        print("代码示例:")
        print("""
# 只更新变化的部分
def animate(frame):
    # 只更新数据,不重新设置标签、标题等静态元素
    line.set_data(x_data, y_data)
    
    # 避免在每帧都调用这些函数
    # ax.set_title(...)  # 不要在动画函数中设置静态内容
    # ax.set_xlabel(...)  # 在动画创建前设置一次即可
    
    return line,
        """)
        
        print("\n3. 优化数据计算")
        print("代码示例:")
        print("""
# 预计算数据
class OptimizedAnimation:
    def __init__(self):
        # 预计算所有帧的数据
        self.precomputed_data = []
        for frame in range(100):
            x_data = np.linspace(0, 2*np.pi, 100)
            y_data = np.sin(x_data + frame * 0.1)
            self.precomputed_data.append((x_data, y_data))
    
    def animate(self, frame):
        # 直接使用预计算的数据
        x_data, y_data = self.precomputed_data[frame]
        line.set_data(x_data, y_data)
        return line,
        """)
        
        print("\n4. 内存管理")
        print("代码示例:")
        print("""
# 限制数据量
class MemoryEfficientAnimation:
    def __init__(self):
        self.max_points = 1000  # 限制最大点数
        self.data_buffer = []   # 使用循环缓冲区
    
    def animate(self, frame):
        # 添加新数据点
        new_point = calculate_new_point(frame)
        self.data_buffer.append(new_point)
        
        # 保持缓冲区大小
        if len(self.data_buffer) > self.max_points:
            self.data_buffer.pop(0)
        
        # 更新图表
        line.set_data(range(len(self.data_buffer)), self.data_buffer)
        return line,
        """)
        
        print("\n5. 选择合适的间隔时间")
        print("代码示例:")
        print("""
# 根据动画复杂度调整间隔
# 简单动画:interval=50 (20 FPS)
# 复杂动画:interval=100 (10 FPS)
# 实时数据:interval=1000 (1 FPS)

anim = animation.FuncAnimation(
    fig, animate, frames=100,
    interval=50,  # 调整这个值来平衡流畅度和性能
    blit=True, repeat=True
)
        """)
        
        # 创建性能对比演示
        self.performance_comparison_demo()
    
    def performance_comparison_demo(self):
        """性能对比演示"""
        fig, axes = plt.subplots(1, 2, figsize=(16, 6))
        
        # 模拟性能数据
        methods = ['无优化', '使用blit', '预计算数据', '内存优化', '全部优化']
        fps_values = [5, 15, 25, 20, 30]
        memory_usage = [100, 95, 120, 80, 85]  # MB
        
        # FPS 对比
        ax1 = axes[0]
        bars1 = ax1.bar(methods, fps_values, 
                       color=['red', 'orange', 'yellow', 'lightgreen', 'green'],
                       alpha=0.8)
        ax1.set_title('动画性能对比 - FPS')
        ax1.set_ylabel('帧率 (FPS)')
        ax1.set_ylim(0, 35)
        
        # 添加数值标签
        for bar, value in zip(bars1, fps_values):
            ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
                    f'{value}', ha='center', va='bottom', fontweight='bold')
        
        ax1.grid(True, alpha=0.3, axis='y')
        plt.setp(ax1.get_xticklabels(), rotation=45, ha='right')
        
        # 内存使用对比
        ax2 = axes[1]
        bars2 = ax2.bar(methods, memory_usage,
                       color=['red', 'orange', 'yellow', 'lightgreen', 'green'],
                       alpha=0.8)
        ax2.set_title('内存使用对比')
        ax2.set_ylabel('内存使用 (MB)')
        ax2.set_ylim(0, 140)
        
        # 添加数值标签
        for bar, value in zip(bars2, memory_usage):
            ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 2,
                    f'{value}MB', ha='center', va='bottom', fontweight='bold')
        
        ax2.grid(True, alpha=0.3, axis='y')
        plt.setp(ax2.get_xticklabels(), rotation=45, ha='right')
        
        plt.tight_layout()
        plt.show()
        
        # 显示优化建议
        print("\n动画优化建议总结:")
        print("1. 始终使用 blit=True 来提高性能")
        print("2. 预计算复杂的数学运算")
        print("3. 限制数据点数量,使用循环缓冲区")
        print("4. 避免在动画函数中进行重复的设置操作")
        print("5. 根据需求调整帧率和间隔时间")
        print("6. 使用适当的图像分辨率和压缩设置")
        print("7. 考虑使用多线程处理数据计算")

# 使用示例
export_demo = AnimationExport()

print("动画保存和导出演示")
anim = export_demo.save_animation_examples()

print("\n动画优化技巧")
export_demo.optimization_tips_demo()

7.2 交互式图表

7.2.1 鼠标事件处理

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.widgets import Button, Slider, CheckButtons, RadioButtons
import numpy as np

class InteractiveCharts:
    """交互式图表演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
        
        # 生成示例数据
        self.generate_interactive_data()
    
    def generate_interactive_data(self):
        """生成交互式演示数据"""
        np.random.seed(42)
        
        # 散点数据
        self.n_points = 100
        self.x_data = np.random.randn(self.n_points)
        self.y_data = np.random.randn(self.n_points)
        self.colors = np.random.rand(self.n_points)
        self.sizes = 50 + 100 * np.random.rand(self.n_points)
        
        # 线图数据
        self.x_line = np.linspace(0, 10, 100)
        self.y_line = np.sin(self.x_line)
        
        # 选中的点
        self.selected_points = []
    
    def mouse_events_demo(self):
        """鼠标事件演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        # 创建散点图
        scatter = ax.scatter(self.x_data, self.y_data, 
                           c=self.colors, s=self.sizes, 
                           alpha=0.7, cmap='viridis')
        
        ax.set_title('交互式散点图 - 点击、拖拽和悬停演示')
        ax.set_xlabel('X 坐标')
        ax.set_ylabel('Y 坐标')
        ax.grid(True, alpha=0.3)
        
        # 添加颜色条
        cbar = plt.colorbar(scatter, ax=ax)
        cbar.set_label('颜色值')
        
        # 信息显示文本
        info_text = ax.text(0.02, 0.98, '', transform=ax.transAxes, 
                           verticalalignment='top', fontsize=10,
                           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
        
        # 选中点的标记
        selected_scatter = ax.scatter([], [], s=200, facecolors='none', 
                                    edgecolors='red', linewidths=3)
        
        # 鼠标按下状态
        self.mouse_pressed = False
        self.drag_start = None
        
        def on_click(event):
            """鼠标点击事件"""
            if event.inaxes != ax:
                return
            
            self.mouse_pressed = True
            self.drag_start = (event.xdata, event.ydata)
            
            # 查找最近的点
            if event.xdata is not None and event.ydata is not None:
                distances = np.sqrt((self.x_data - event.xdata)**2 + 
                                  (self.y_data - event.ydata)**2)
                nearest_idx = np.argmin(distances)
                
                # 如果点击距离足够近,选中该点
                if distances[nearest_idx] < 0.5:
                    if nearest_idx in self.selected_points:
                        self.selected_points.remove(nearest_idx)
                    else:
                        self.selected_points.append(nearest_idx)
                    
                    # 更新选中点的显示
                    if self.selected_points:
                        selected_x = [self.x_data[i] for i in self.selected_points]
                        selected_y = [self.y_data[i] for i in self.selected_points]
                        selected_scatter.set_offsets(np.column_stack([selected_x, selected_y]))
                    else:
                        selected_scatter.set_offsets(np.empty((0, 2)))
                    
                    fig.canvas.draw()
        
        def on_release(event):
            """鼠标释放事件"""
            self.mouse_pressed = False
            self.drag_start = None
        
        def on_motion(event):
            """鼠标移动事件"""
            if event.inaxes != ax:
                info_text.set_text('')
                fig.canvas.draw()
                return
            
            # 显示鼠标位置信息
            if event.xdata is not None and event.ydata is not None:
                # 查找最近的点
                distances = np.sqrt((self.x_data - event.xdata)**2 + 
                                  (self.y_data - event.ydata)**2)
                nearest_idx = np.argmin(distances)
                
                if distances[nearest_idx] < 0.5:
                    # 显示点的详细信息
                    info = f"点 {nearest_idx}\n"
                    info += f"坐标: ({self.x_data[nearest_idx]:.2f}, {self.y_data[nearest_idx]:.2f})\n"
                    info += f"颜色值: {self.colors[nearest_idx]:.3f}\n"
                    info += f"大小: {self.sizes[nearest_idx]:.1f}\n"
                    info += "点击选择/取消选择"
                else:
                    info = f"鼠标位置: ({event.xdata:.2f}, {event.ydata:.2f})\n"
                    info += f"已选中 {len(self.selected_points)} 个点"
                
                info_text.set_text(info)
                
                # 拖拽功能
                if self.mouse_pressed and self.drag_start is not None:
                    dx = event.xdata - self.drag_start[0]
                    dy = event.ydata - self.drag_start[1]
                    
                    # 移动选中的点
                    for idx in self.selected_points:
                        self.x_data[idx] += dx * 0.1  # 减慢移动速度
                        self.y_data[idx] += dy * 0.1
                    
                    # 更新散点图
                    scatter.set_offsets(np.column_stack([self.x_data, self.y_data]))
                    
                    # 更新选中点标记
                    if self.selected_points:
                        selected_x = [self.x_data[i] for i in self.selected_points]
                        selected_y = [self.y_data[i] for i in self.selected_points]
                        selected_scatter.set_offsets(np.column_stack([selected_x, selected_y]))
                
                fig.canvas.draw()
        
        def on_key(event):
            """键盘事件"""
            if event.key == 'delete' or event.key == 'd':
                # 删除选中的点
                if self.selected_points:
                    # 创建新的数据数组(排除选中的点)
                    mask = np.ones(len(self.x_data), dtype=bool)
                    mask[self.selected_points] = False
                    
                    self.x_data = self.x_data[mask]
                    self.y_data = self.y_data[mask]
                    self.colors = self.colors[mask]
                    self.sizes = self.sizes[mask]
                    
                    # 更新散点图
                    scatter.set_offsets(np.column_stack([self.x_data, self.y_data]))
                    scatter.set_array(self.colors)
                    scatter.set_sizes(self.sizes)
                    
                    # 清空选中点
                    self.selected_points = []
                    selected_scatter.set_offsets(np.empty((0, 2)))
                    
                    fig.canvas.draw()
            
            elif event.key == 'r':
                # 重置数据
                self.generate_interactive_data()
                scatter.set_offsets(np.column_stack([self.x_data, self.y_data]))
                scatter.set_array(self.colors)
                scatter.set_sizes(self.sizes)
                self.selected_points = []
                selected_scatter.set_offsets(np.empty((0, 2)))
                fig.canvas.draw()
        
        # 连接事件
        fig.canvas.mpl_connect('button_press_event', on_click)
        fig.canvas.mpl_connect('button_release_event', on_release)
        fig.canvas.mpl_connect('motion_notify_event', on_motion)
        fig.canvas.mpl_connect('key_press_event', on_key)
        
        # 添加说明文字
        ax.text(0.02, 0.02, 
               '操作说明:\n• 悬停查看点信息\n• 点击选择/取消选择点\n• 拖拽移动选中的点\n• 按 D 键删除选中的点\n• 按 R 键重置数据',
               transform=ax.transAxes, fontsize=9,
               bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        return fig
    
    def zoom_pan_demo(self):
        """缩放和平移演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        # 创建复杂的数据
        x = np.linspace(-10, 10, 1000)
        y1 = np.sin(x) * np.exp(-x**2/20)
        y2 = np.cos(x) * np.exp(-x**2/30)
        y3 = np.sin(2*x) * np.exp(-x**2/40)
        
        # 绘制多条线
        line1, = ax.plot(x, y1, 'b-', linewidth=2, label='sin(x)·exp(-x²/20)')
        line2, = ax.plot(x, y2, 'r-', linewidth=2, label='cos(x)·exp(-x²/30)')
        line3, = ax.plot(x, y3, 'g-', linewidth=2, label='sin(2x)·exp(-x²/40)')
        
        ax.set_title('交互式缩放和平移演示')
        ax.set_xlabel('X')
        ax.set_ylabel('Y')
        ax.legend()
        ax.grid(True, alpha=0.3)
        
        # 缩放和平移状态
        self.zoom_factor = 1.0
        self.pan_start = None
        self.original_xlim = ax.get_xlim()
        self.original_ylim = ax.get_ylim()
        
        # 信息显示
        info_text = ax.text(0.02, 0.98, '', transform=ax.transAxes,
                           verticalalignment='top', fontsize=10,
                           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
        
        def on_scroll(event):
            """鼠标滚轮缩放"""
            if event.inaxes != ax:
                return
            
            # 缩放因子
            scale_factor = 1.1 if event.step > 0 else 1/1.1
            
            # 获取当前坐标范围
            xlim = ax.get_xlim()
            ylim = ax.get_ylim()
            
            # 计算缩放中心(鼠标位置)
            xdata, ydata = event.xdata, event.ydata
            
            # 计算新的坐标范围
            x_left = xdata - (xdata - xlim[0]) * scale_factor
            x_right = xdata + (xlim[1] - xdata) * scale_factor
            y_bottom = ydata - (ydata - ylim[0]) * scale_factor
            y_top = ydata + (ylim[1] - ydata) * scale_factor
            
            # 应用新的坐标范围
            ax.set_xlim(x_left, x_right)
            ax.set_ylim(y_bottom, y_top)
            
            # 更新缩放因子
            self.zoom_factor *= scale_factor
            
            # 更新信息显示
            info_text.set_text(f'缩放: {self.zoom_factor:.2f}x\n鼠标位置: ({xdata:.2f}, {ydata:.2f})')
            
            fig.canvas.draw()
        
        def on_press(event):
            """鼠标按下开始平移"""
            if event.inaxes != ax or event.button != 1:  # 只响应左键
                return
            
            self.pan_start = (event.xdata, event.ydata)
        
        def on_motion(event):
            """鼠标移动平移"""
            if event.inaxes != ax:
                return
            
            # 显示鼠标位置
            if event.xdata is not None and event.ydata is not None:
                info_text.set_text(f'缩放: {self.zoom_factor:.2f}x\n鼠标位置: ({event.xdata:.2f}, {event.ydata:.2f})')
            
            # 平移功能
            if self.pan_start is not None and event.xdata is not None and event.ydata is not None:
                dx = self.pan_start[0] - event.xdata
                dy = self.pan_start[1] - event.ydata
                
                # 获取当前坐标范围
                xlim = ax.get_xlim()
                ylim = ax.get_ylim()
                
                # 应用平移
                ax.set_xlim(xlim[0] + dx, xlim[1] + dx)
                ax.set_ylim(ylim[0] + dy, ylim[1] + dy)
                
                fig.canvas.draw()
        
        def on_release(event):
            """鼠标释放结束平移"""
            self.pan_start = None
        
        def on_key(event):
            """键盘快捷键"""
            if event.key == 'r':
                # 重置视图
                ax.set_xlim(self.original_xlim)
                ax.set_ylim(self.original_ylim)
                self.zoom_factor = 1.0
                info_text.set_text('视图已重置')
                fig.canvas.draw()
            elif event.key == 'f':
                # 适应数据范围
                ax.relim()
                ax.autoscale()
                self.zoom_factor = 1.0
                info_text.set_text('已适应数据范围')
                fig.canvas.draw()
        
        # 连接事件
        fig.canvas.mpl_connect('scroll_event', on_scroll)
        fig.canvas.mpl_connect('button_press_event', on_press)
        fig.canvas.mpl_connect('motion_notify_event', on_motion)
        fig.canvas.mpl_connect('button_release_event', on_release)
        fig.canvas.mpl_connect('key_press_event', on_key)
        
        # 添加说明
        ax.text(0.02, 0.02,
               '操作说明:\n• 滚轮缩放\n• 左键拖拽平移\n• R键重置视图\n• F键适应数据',
               transform=ax.transAxes, fontsize=9,
               bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        return fig
    
    def selection_tools_demo(self):
        """选择工具演示"""
        fig, ax = plt.subplots(figsize=(12, 8))
        
        # 创建散点图
        scatter = ax.scatter(self.x_data, self.y_data, 
                           c=self.colors, s=self.sizes, 
                           alpha=0.7, cmap='plasma')
        
        ax.set_title('交互式选择工具演示')
        ax.set_xlabel('X 坐标')
        ax.set_ylabel('Y 坐标')
        ax.grid(True, alpha=0.3)
        
        # 选择模式
        self.selection_mode = 'rectangle'  # 'rectangle', 'circle', 'lasso'
        self.selection_active = False
        self.selection_start = None
        self.selection_shape = None
        
        # 选中的点
        selected_scatter = ax.scatter([], [], s=200, facecolors='none',
                                    edgecolors='red', linewidths=3)
        
        # 信息显示
        info_text = ax.text(0.02, 0.98, '', transform=ax.transAxes,
                           verticalalignment='top', fontsize=10,
                           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
        
        def update_info():
            """更新信息显示"""
            info = f"选择模式: {self.selection_mode}\n"
            info += f"已选中: {len(self.selected_points)} 个点\n"
            if self.selected_points:
                selected_colors = [self.colors[i] for i in self.selected_points]
                selected_sizes = [self.sizes[i] for i in self.selected_points]
                info += f"平均颜色值: {np.mean(selected_colors):.3f}\n"
                info += f"平均大小: {np.mean(selected_sizes):.1f}"
            info_text.set_text(info)
        
        def points_in_rectangle(x1, y1, x2, y2):
            """获取矩形内的点"""
            x_min, x_max = min(x1, x2), max(x1, x2)
            y_min, y_max = min(y1, y2), max(y1, y2)
            
            mask = ((self.x_data >= x_min) & (self.x_data <= x_max) &
                   (self.y_data >= y_min) & (self.y_data <= y_max))
            return np.where(mask)[0].tolist()
        
        def points_in_circle(cx, cy, radius):
            """获取圆形内的点"""
            distances = np.sqrt((self.x_data - cx)**2 + (self.y_data - cy)**2)
            mask = distances <= radius
            return np.where(mask)[0].tolist()
        
        def on_press(event):
            """鼠标按下开始选择"""
            if event.inaxes != ax or event.button != 1:
                return
            
            self.selection_active = True
            self.selection_start = (event.xdata, event.ydata)
            
            # 清除之前的选择形状
            if self.selection_shape is not None:
                self.selection_shape.remove()
                self.selection_shape = None
        
        def on_motion(event):
            """鼠标移动更新选择区域"""
            if not self.selection_active or event.inaxes != ax:
                return
            
            if event.xdata is None or event.ydata is None:
                return
            
            # 清除之前的选择形状
            if self.selection_shape is not None:
                self.selection_shape.remove()
            
            x1, y1 = self.selection_start
            x2, y2 = event.xdata, event.ydata
            
            if self.selection_mode == 'rectangle':
                # 绘制矩形选择框
                width = x2 - x1
                height = y2 - y1
                self.selection_shape = patches.Rectangle(
                    (x1, y1), width, height,
                    linewidth=2, edgecolor='red', facecolor='red', alpha=0.2
                )
                ax.add_patch(self.selection_shape)
            
            elif self.selection_mode == 'circle':
                # 绘制圆形选择框
                radius = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
                self.selection_shape = patches.Circle(
                    (x1, y1), radius,
                    linewidth=2, edgecolor='blue', facecolor='blue', alpha=0.2
                )
                ax.add_patch(self.selection_shape)
            
            fig.canvas.draw()
        
        def on_release(event):
            """鼠标释放完成选择"""
            if not self.selection_active or event.inaxes != ax:
                return
            
            self.selection_active = False
            
            if event.xdata is None or event.ydata is None:
                return
            
            x1, y1 = self.selection_start
            x2, y2 = event.xdata, event.ydata
            
            # 根据选择模式获取选中的点
            if self.selection_mode == 'rectangle':
                new_selected = points_in_rectangle(x1, y1, x2, y2)
            elif self.selection_mode == 'circle':
                radius = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
                new_selected = points_in_circle(x1, y1, radius)
            else:
                new_selected = []
            
            # 更新选中的点(按住Ctrl键可以多选)
            if hasattr(event, 'key') and event.key == 'control':
                # 添加到现有选择
                for idx in new_selected:
                    if idx not in self.selected_points:
                        self.selected_points.append(idx)
            else:
                # 替换选择
                self.selected_points = new_selected
            
            # 更新选中点的显示
            if self.selected_points:
                selected_x = [self.x_data[i] for i in self.selected_points]
                selected_y = [self.y_data[i] for i in self.selected_points]
                selected_scatter.set_offsets(np.column_stack([selected_x, selected_y]))
            else:
                selected_scatter.set_offsets(np.empty((0, 2)))
            
            # 清除选择形状
            if self.selection_shape is not None:
                self.selection_shape.remove()
                self.selection_shape = None
            
            update_info()
            fig.canvas.draw()
        
        def on_key(event):
            """键盘事件"""
            if event.key == '1':
                self.selection_mode = 'rectangle'
                update_info()
            elif event.key == '2':
                self.selection_mode = 'circle'
                update_info()
            elif event.key == 'escape':
                # 清除选择
                self.selected_points = []
                selected_scatter.set_offsets(np.empty((0, 2)))
                update_info()
                fig.canvas.draw()
        
        # 连接事件
        fig.canvas.mpl_connect('button_press_event', on_press)
        fig.canvas.mpl_connect('motion_notify_event', on_motion)
        fig.canvas.mpl_connect('button_release_event', on_release)
        fig.canvas.mpl_connect('key_press_event', on_key)
        
        # 初始化信息显示
        update_info()
        
        # 添加说明
        ax.text(0.02, 0.02,
               '操作说明:\n• 1键: 矩形选择\n• 2键: 圆形选择\n• 拖拽选择区域\n• Ctrl+拖拽: 多选\n• Esc: 清除选择',
               transform=ax.transAxes, fontsize=9,
               bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        return fig

# 使用示例
interactive_charts = InteractiveCharts()

print("1. 鼠标事件演示")
fig1 = interactive_charts.mouse_events_demo()

print("2. 缩放和平移演示")
fig2 = interactive_charts.zoom_pan_demo()

print("3. 选择工具演示")
fig3 = interactive_charts.selection_tools_demo()

7.2.2 控件和小部件

class InteractiveWidgets:
    """交互式控件演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
    
    def slider_demo(self):
        """滑块控件演示"""
        # 创建图形和子图
        fig, ax = plt.subplots(figsize=(12, 8))
        plt.subplots_adjust(bottom=0.25)
        
        # 生成初始数据
        t = np.linspace(0, 2*np.pi, 1000)
        frequency = 1.0
        amplitude = 1.0
        phase = 0.0
        
        # 初始线图
        line, = ax.plot(t, amplitude * np.sin(frequency * t + phase), 'b-', linewidth=2)
        ax.set_xlim(0, 2*np.pi)
        ax.set_ylim(-3, 3)
        ax.set_title('交互式正弦波控制器')
        ax.set_xlabel('时间 (t)')
        ax.set_ylabel('幅度')
        ax.grid(True, alpha=0.3)
        
        # 创建滑块
        # 频率滑块
        ax_freq = plt.axes([0.1, 0.15, 0.65, 0.03])
        slider_freq = Slider(ax_freq, '频率', 0.1, 5.0, valinit=frequency, valfmt='%.1f Hz')
        
        # 幅度滑块
        ax_amp = plt.axes([0.1, 0.10, 0.65, 0.03])
        slider_amp = Slider(ax_amp, '幅度', 0.1, 3.0, valinit=amplitude, valfmt='%.1f')
        
        # 相位滑块
        ax_phase = plt.axes([0.1, 0.05, 0.65, 0.03])
        slider_phase = Slider(ax_phase, '相位', 0, 2*np.pi, valinit=phase, valfmt='%.2f rad')
        
        def update(val):
            """更新函数"""
            freq = slider_freq.val
            amp = slider_amp.val
            phase = slider_phase.val
            
            # 计算新的y值
            y = amp * np.sin(freq * t + phase)
            line.set_ydata(y)
            
            # 更新标题
            ax.set_title(f'y = {amp:.1f} × sin({freq:.1f}t + {phase:.2f})')
            
            fig.canvas.draw()
        
        # 连接滑块事件
        slider_freq.on_changed(update)
        slider_amp.on_changed(update)
        slider_phase.on_changed(update)
        
        # 重置按钮
        ax_reset = plt.axes([0.8, 0.10, 0.1, 0.05])
        button_reset = Button(ax_reset, '重置')
        
        def reset(event):
            """重置函数"""
            slider_freq.reset()
            slider_amp.reset()
            slider_phase.reset()
        
        button_reset.on_clicked(reset)
        
        plt.show()
        
        return fig
    
    def checkbox_radio_demo(self):
        """复选框和单选按钮演示"""
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
        
        # 生成数据
        x = np.linspace(0, 10, 100)
        
        # 左侧图:复选框控制多条线的显示
        lines = {}
        lines['sin'] = ax1.plot(x, np.sin(x), 'b-', linewidth=2, label='sin(x)')[0]
        lines['cos'] = ax1.plot(x, np.cos(x), 'r-', linewidth=2, label='cos(x)')[0]
        lines['tan'] = ax1.plot(x, np.tan(x), 'g-', linewidth=2, label='tan(x)')[0]
        lines['exp'] = ax1.plot(x, np.exp(-x/5), 'm-', linewidth=2, label='exp(-x/5)')[0]
        
        ax1.set_xlim(0, 10)
        ax1.set_ylim(-2, 2)
        ax1.set_title('复选框控制线条显示')
        ax1.set_xlabel('X')
        ax1.set_ylabel('Y')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # 复选框
        ax_check = plt.axes([0.02, 0.5, 0.15, 0.3])
        check_labels = ['sin(x)', 'cos(x)', 'tan(x)', 'exp(-x/5)']
        check_buttons = CheckButtons(ax_check, check_labels, [True, True, True, True])
        
        def toggle_lines(label):
            """切换线条显示"""
            line_map = {
                'sin(x)': 'sin',
                'cos(x)': 'cos', 
                'tan(x)': 'tan',
                'exp(-x/5)': 'exp'
            }
            
            line_key = line_map[label]
            lines[line_key].set_visible(not lines[line_key].get_visible())
            fig.canvas.draw()
        
        check_buttons.on_clicked(toggle_lines)
        
        # 右侧图:单选按钮控制函数类型
        line2, = ax2.plot(x, np.sin(x), 'b-', linewidth=3)
        ax2.set_xlim(0, 10)
        ax2.set_ylim(-2, 2)
        ax2.set_title('单选按钮控制函数类型')
        ax2.set_xlabel('X')
        ax2.set_ylabel('Y')
        ax2.grid(True, alpha=0.3)
        
        # 单选按钮
        ax_radio = plt.axes([0.52, 0.5, 0.15, 0.3])
        radio_labels = ['sin(x)', 'cos(x)', 'sin(2x)', 'cos(2x)', 'sin(x)cos(x)']
        radio_buttons = RadioButtons(ax_radio, radio_labels)
        
        def change_function(label):
            """改变函数"""
            if label == 'sin(x)':
                y = np.sin(x)
                color = 'blue'
            elif label == 'cos(x)':
                y = np.cos(x)
                color = 'red'
            elif label == 'sin(2x)':
                y = np.sin(2*x)
                color = 'green'
            elif label == 'cos(2x)':
                y = np.cos(2*x)
                color = 'orange'
            elif label == 'sin(x)cos(x)':
                y = np.sin(x) * np.cos(x)
                color = 'purple'
            
            line2.set_ydata(y)
            line2.set_color(color)
            ax2.set_title(f'函数: {label}')
            fig.canvas.draw()
        
        radio_buttons.on_clicked(change_function)
        
        plt.tight_layout()
        plt.show()
        
        return fig
    
    def comprehensive_dashboard_demo(self):
        """综合仪表板演示"""
        # 创建复杂的子图布局
        fig = plt.figure(figsize=(18, 12))
        
        # 主图区域
        ax_main = plt.subplot2grid((4, 4), (0, 1), colspan=3, rowspan=3)
        ax_hist = plt.subplot2grid((4, 4), (3, 1), colspan=3)
        ax_scatter = plt.subplot2grid((4, 4), (0, 0), rowspan=2)
        ax_pie = plt.subplot2grid((4, 4), (2, 0), rowspan=2)
        
        # 生成示例数据
        np.random.seed(42)
        n_points = 200
        x_data = np.random.randn(n_points)
        y_data = 2 * x_data + np.random.randn(n_points) * 0.5
        categories = np.random.choice(['A', 'B', 'C', 'D'], n_points)
        
        # 主散点图
        scatter_main = ax_main.scatter(x_data, y_data, c=y_data, 
                                     cmap='viridis', alpha=0.7, s=50)
        ax_main.set_title('主数据视图 - 散点图')
        ax_main.set_xlabel('X 变量')
        ax_main.set_ylabel('Y 变量')
        ax_main.grid(True, alpha=0.3)
        
        # 添加回归线
        z = np.polyfit(x_data, y_data, 1)
        p = np.poly1d(z)
        x_line = np.linspace(x_data.min(), x_data.max(), 100)
        line_reg, = ax_main.plot(x_line, p(x_line), 'r--', linewidth=2, 
                                label=f'回归线: y={z[0]:.2f}x+{z[1]:.2f}')
        ax_main.legend()
        
        # 直方图
        n_bins = 20
        hist_counts, hist_bins, hist_patches = ax_hist.hist(y_data, bins=n_bins, 
                                                           alpha=0.7, color='skyblue', 
                                                           edgecolor='black')
        ax_hist.set_title('Y 变量分布')
        ax_hist.set_xlabel('Y 值')
        ax_hist.set_ylabel('频数')
        ax_hist.grid(True, alpha=0.3)
        
        # 边际散点图
        scatter_side = ax_scatter.scatter(x_data, range(len(x_data)), 
                                        c=y_data, cmap='viridis', alpha=0.7, s=30)
        ax_scatter.set_title('X 变量分布')
        ax_scatter.set_xlabel('X 值')
        ax_scatter.set_ylabel('数据点索引')
        
        # 饼图
        category_counts = pd.Series(categories).value_counts()
        wedges, texts, autotexts = ax_pie.pie(category_counts.values, 
                                             labels=category_counts.index,
                                             autopct='%1.1f%%', startangle=90)
        ax_pie.set_title('类别分布')
        
        # 控制面板区域
        plt.subplots_adjust(bottom=0.15, right=0.85)
        
        # 滑块控制
        ax_alpha = plt.axes([0.1, 0.08, 0.3, 0.02])
        slider_alpha = Slider(ax_alpha, '透明度', 0.1, 1.0, valinit=0.7, valfmt='%.1f')
        
        ax_size = plt.axes([0.1, 0.05, 0.3, 0.02])
        slider_size = Slider(ax_size, '点大小', 10, 200, valinit=50, valfmt='%.0f')
        
        ax_bins = plt.axes([0.1, 0.02, 0.3, 0.02])
        slider_bins = Slider(ax_bins, '直方图箱数', 5, 50, valinit=20, valfmt='%.0f')
        
        # 按钮控制
        ax_colormap = plt.axes([0.45, 0.08, 0.1, 0.03])
        button_colormap = Button(ax_colormap, '切换色彩')
        
        ax_reset = plt.axes([0.45, 0.04, 0.1, 0.03])
        button_reset = Button(ax_reset, '重置视图')
        
        # 复选框
        ax_options = plt.axes([0.6, 0.02, 0.15, 0.08])
        option_labels = ['显示回归线', '显示网格', '显示图例']
        check_options = CheckButtons(ax_options, option_labels, [True, True, True])
        
        # 颜色映射列表
        colormaps = ['viridis', 'plasma', 'inferno', 'magma', 'coolwarm', 'RdYlBu']
        current_cmap_idx = 0
        
        def update_alpha(val):
            """更新透明度"""
            alpha = slider_alpha.val
            scatter_main.set_alpha(alpha)
            scatter_side.set_alpha(alpha)
            fig.canvas.draw()
        
        def update_size(val):
            """更新点大小"""
            size = slider_size.val
            scatter_main.set_sizes([size] * len(x_data))
            fig.canvas.draw()
        
        def update_bins(val):
            """更新直方图箱数"""
            bins = int(slider_bins.val)
            ax_hist.clear()
            ax_hist.hist(y_data, bins=bins, alpha=0.7, color='skyblue', edgecolor='black')
            ax_hist.set_title('Y 变量分布')
            ax_hist.set_xlabel('Y 值')
            ax_hist.set_ylabel('频数')
            if check_options.get_status()[1]:  # 网格选项
                ax_hist.grid(True, alpha=0.3)
            fig.canvas.draw()
        
        def change_colormap(event):
            """切换颜色映射"""
            nonlocal current_cmap_idx
            current_cmap_idx = (current_cmap_idx + 1) % len(colormaps)
            new_cmap = colormaps[current_cmap_idx]
            
            scatter_main.set_cmap(new_cmap)
            scatter_side.set_cmap(new_cmap)
            
            ax_main.set_title(f'主数据视图 - 散点图 (色彩: {new_cmap})')
            fig.canvas.draw()
        
        def reset_view(event):
            """重置视图"""
            slider_alpha.reset()
            slider_size.reset()
            slider_bins.reset()
            
            # 重置颜色映射
            nonlocal current_cmap_idx
            current_cmap_idx = 0
            scatter_main.set_cmap(colormaps[0])
            scatter_side.set_cmap(colormaps[0])
            
            ax_main.set_title('主数据视图 - 散点图')
            fig.canvas.draw()
        
        def toggle_options(label):
            """切换选项"""
            if label == '显示回归线':
                line_reg.set_visible(not line_reg.get_visible())
            elif label == '显示网格':
                for ax in [ax_main, ax_hist, ax_scatter]:
                    ax.grid(not ax.get_gridlines()[0].get_visible() if ax.get_gridlines() else True, alpha=0.3)
            elif label == '显示图例':
                legend = ax_main.get_legend()
                if legend:
                    legend.set_visible(not legend.get_visible())
            
            fig.canvas.draw()
        
        # 连接事件
        slider_alpha.on_changed(update_alpha)
        slider_size.on_changed(update_size)
        slider_bins.on_changed(update_bins)
        button_colormap.on_clicked(change_colormap)
        button_reset.on_clicked(reset_view)
        check_options.on_clicked(toggle_options)
        
        # 添加颜色条
        cbar = plt.colorbar(scatter_main, ax=ax_main, shrink=0.8)
        cbar.set_label('Y 值')
        
        # 添加统计信息文本
        stats_text = f"""数据统计:
点数: {len(x_data)}
X均值: {np.mean(x_data):.2f}
Y均值: {np.mean(y_data):.2f}
相关系数: {np.corrcoef(x_data, y_data)[0,1]:.3f}"""
        
        fig.text(0.87, 0.7, stats_text, fontsize=10, 
                bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))
        
        plt.show()
        
        return fig

# 使用示例
widgets_demo = InteractiveWidgets()

print("1. 滑块控件演示")
fig1 = widgets_demo.slider_demo()

print("2. 复选框和单选按钮演示")
fig2 = widgets_demo.checkbox_radio_demo()

print("3. 综合仪表板演示")
fig3 = widgets_demo.comprehensive_dashboard_demo()

7.2.3 实时数据更新

import threading
import time
from collections import deque
import pandas as pd

class RealTimeCharts:
    """实时数据更新图表演示类"""
    
    def __init__(self):
        plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
        plt.rcParams['axes.unicode_minus'] = False
        
        # 实时数据缓冲区
        self.max_points = 100
        self.time_data = deque(maxlen=self.max_points)
        self.value_data = deque(maxlen=self.max_points)
        self.running = False
        
        # 数据生成参数
        self.start_time = time.time()
        self.noise_level = 0.1
        self.trend = 0.01
    
    def generate_real_time_data(self):
        """生成实时数据"""
        current_time = time.time() - self.start_time
        
        # 基础信号:正弦波 + 趋势 + 噪声
        base_signal = np.sin(current_time * 0.5) * 2
        trend_component = current_time * self.trend
        noise_component = np.random.normal(0, self.noise_level)
        
        value = base_signal + trend_component + noise_component
        
        return current_time, value
    
    def real_time_line_chart(self):
        """实时线图演示"""
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
        
        # 初始化空数据
        line1, = ax1.plot([], [], 'b-', linewidth=2, label='实时数据')
        line2, = ax1.plot([], [], 'r--', linewidth=1, alpha=0.7, label='移动平均')
        
        ax1.set_title('实时数据监控')
        ax1.set_xlabel('时间 (秒)')
        ax1.set_ylabel('数值')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # 统计图表
        bars = ax2.bar(['最小值', '平均值', '最大值', '标准差'], [0, 0, 0, 0], 
                      color=['red', 'blue', 'green', 'orange'], alpha=0.7)
        ax2.set_title('实时统计信息')
        ax2.set_ylabel('数值')
        
        # 信息显示
        info_text = fig.text(0.02, 0.95, '', fontsize=10,
                           bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
        
        # 控制按钮
        plt.subplots_adjust(bottom=0.15)
        
        ax_start = plt.axes([0.1, 0.05, 0.1, 0.05])
        button_start = Button(ax_start, '开始')
        
        ax_stop = plt.axes([0.25, 0.05, 0.1, 0.05])
        button_stop = Button(ax_stop, '停止')
        
        ax_clear = plt.axes([0.4, 0.05, 0.1, 0.05])
        button_clear = Button(ax_clear, '清除')
        
        # 滑块控制
        ax_noise = plt.axes([0.6, 0.08, 0.3, 0.02])
        slider_noise = Slider(ax_noise, '噪声水平', 0.01, 0.5, valinit=0.1, valfmt='%.2f')
        
        ax_trend = plt.axes([0.6, 0.05, 0.3, 0.02])
        slider_trend = Slider(ax_trend, '趋势斜率', -0.05, 0.05, valinit=0.01, valfmt='%.3f')
        
        def update_plot():
            """更新图表"""
            if len(self.time_data) > 1:
                # 更新线图
                line1.set_data(list(self.time_data), list(self.value_data))
                
                # 计算移动平均
                if len(self.value_data) >= 10:
                    window_size = min(10, len(self.value_data))
                    moving_avg = pd.Series(list(self.value_data)).rolling(window=window_size).mean()
                    line2.set_data(list(self.time_data), moving_avg)
                
                # 自动调整坐标轴
                if self.time_data:
                    ax1.set_xlim(max(0, self.time_data[-1] - 30), self.time_data[-1] + 2)
                    
                    if self.value_data:
                        y_min, y_max = min(self.value_data), max(self.value_data)
                        y_range = y_max - y_min
                        ax1.set_ylim(y_min - y_range * 0.1, y_max + y_range * 0.1)
                
                # 更新统计信息
                if self.value_data:
                    values = list(self.value_data)
                    stats = [min(values), np.mean(values), max(values), np.std(values)]
                    
                    for bar, stat in zip(bars, stats):
                        bar.set_height(stat)
                    
                    # 自动调整统计图表的y轴
                    ax2.set_ylim(0, max(stats) * 1.1)
                    
                    # 更新信息文本
                    info = f"""实时监控状态:
数据点数: {len(self.value_data)}
当前值: {values[-1]:.3f}
最新时间: {self.time_data[-1]:.1f}s
运行状态: {'运行中' if self.running else '已停止'}"""
                    info_text.set_text(info)
                
                fig.canvas.draw()
        
        def data_generator():
            """数据生成线程"""
            while self.running:
                current_time, value = self.generate_real_time_data()
                self.time_data.append(current_time)
                self.value_data.append(value)
                
                # 在主线程中更新图表
                fig.canvas.draw_idle()
                
                time.sleep(0.1)  # 100ms更新间隔
        
        def start_monitoring(event):
            """开始监控"""
            if not self.running:
                self.running = True
                self.data_thread = threading.Thread(target=data_generator, daemon=True)
                self.data_thread.start()
                
                # 设置定时器更新图表
                self.timer = fig.canvas.new_timer(interval=100)
                self.timer.add_callback(update_plot)
                self.timer.start()
        
        def stop_monitoring(event):
            """停止监控"""
            self.running = False
            if hasattr(self, 'timer'):
                self.timer.stop()
        
        def clear_data(event):
            """清除数据"""
            self.time_data.clear()
            self.value_data.clear()
            self.start_time = time.time()
            
            # 清除图表
            line1.set_data([], [])
            line2.set_data([], [])
            
            for bar in bars:
                bar.set_height(0)
            
            info_text.set_text('数据已清除')
            fig.canvas.draw()
        
        def update_noise(val):
            """更新噪声水平"""
            self.noise_level = slider_noise.val
        
        def update_trend(val):
            """更新趋势斜率"""
            self.trend = slider_trend.val
        
        # 连接事件
        button_start.on_clicked(start_monitoring)
        button_stop.on_clicked(stop_monitoring)
        button_clear.on_clicked(clear_data)
        slider_noise.on_changed(update_noise)
        slider_trend.on_changed(update_trend)
        
        plt.show()
        
        return fig
    
    def real_time_dashboard(self):
        """实时仪表板演示"""
        fig = plt.figure(figsize=(16, 12))
        
        # 创建子图布局
        gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3)
        
        # 主时间序列图
        ax_main = fig.add_subplot(gs[0, :])
        # 直方图
        ax_hist = fig.add_subplot(gs[1, 0])
        # 散点图
        ax_scatter = fig.add_subplot(gs[1, 1])
        # 仪表盘
        ax_gauge = fig.add_subplot(gs[1, 2])
        # 状态指示器
        ax_status = fig.add_subplot(gs[2, :])
        
        # 初始化图表元素
        line_main, = ax_main.plot([], [], 'b-', linewidth=2)
        ax_main.set_title('实时数据流')
        ax_main.set_xlabel('时间')
        ax_main.set_ylabel('数值')
        ax_main.grid(True, alpha=0.3)
        
        # 直方图
        n_bins = 20
        hist_counts, hist_bins, hist_patches = ax_hist.hist([], bins=n_bins, alpha=0.7)
        ax_hist.set_title('数值分布')
        ax_hist.set_xlabel('数值')
        ax_hist.set_ylabel('频数')
        
        # 散点图(时间 vs 数值的相关性)
        scatter_points = ax_scatter.scatter([], [], alpha=0.6, s=30)
        ax_scatter.set_title('时间-数值散点图')
        ax_scatter.set_xlabel('时间')
        ax_scatter.set_ylabel('数值')
        
        # 仪表盘(当前值指示器)
        theta = np.linspace(0, np.pi, 100)
        ax_gauge.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=3)
        ax_gauge.set_xlim(-1.2, 1.2)
        ax_gauge.set_ylim(-0.2, 1.2)
        ax_gauge.set_aspect('equal')
        ax_gauge.set_title('当前值指示器')
        ax_gauge.axis('off')
        
        # 指针
        needle, = ax_gauge.plot([0, 0], [0, 1], 'r-', linewidth=4)
        
        # 状态指示器
        status_bars = ax_status.bar(['数据点数', '平均值', '标准差', '变化率'], 
                                  [0, 0, 0, 0], color=['blue', 'green', 'orange', 'red'])
        ax_status.set_title('实时统计指标')
        ax_status.set_ylabel('数值')
        
        # 数据缓冲区
        max_points = 200
        time_buffer = deque(maxlen=max_points)
        value_buffer = deque(maxlen=max_points)
        
        # 运行状态
        running = [False]
        start_time = [time.time()]
        
        def generate_data():
            """生成新数据点"""
            current_time = time.time() - start_time[0]
            
            # 复杂信号:多个正弦波叠加
            signal = (np.sin(current_time * 0.5) * 2 + 
                     np.sin(current_time * 1.2) * 0.5 + 
                     np.sin(current_time * 0.1) * 1.5 + 
                     np.random.normal(0, 0.2))
            
            return current_time, signal
        
        def update_dashboard():
            """更新仪表板"""
            if running[0]:
                # 生成新数据
                t, v = generate_data()
                time_buffer.append(t)
                value_buffer.append(v)
                
                if len(time_buffer) > 1:
                    # 更新主时间序列
                    line_main.set_data(list(time_buffer), list(value_buffer))
                    ax_main.set_xlim(max(0, t - 30), t + 2)
                    
                    if value_buffer:
                        y_min, y_max = min(value_buffer), max(value_buffer)
                        y_range = y_max - y_min
                        ax_main.set_ylim(y_min - y_range * 0.1, y_max + y_range * 0.1)
                    
                    # 更新直方图
                    if len(value_buffer) > 10:
                        ax_hist.clear()
                        ax_hist.hist(list(value_buffer), bins=20, alpha=0.7, color='skyblue')
                        ax_hist.set_title('数值分布')
                        ax_hist.set_xlabel('数值')
                        ax_hist.set_ylabel('频数')
                    
                    # 更新散点图
                    if len(time_buffer) > 1:
                        scatter_points.set_offsets(np.column_stack([list(time_buffer), list(value_buffer)]))
                        ax_scatter.set_xlim(max(0, t - 30), t + 2)
                        
                        if value_buffer:
                            ax_scatter.set_ylim(y_min - y_range * 0.1, y_max + y_range * 0.1)
                    
                    # 更新仪表盘指针
                    if value_buffer:
                        current_value = value_buffer[-1]
                        # 将数值映射到角度(-π/2 到 π/2)
                        normalized_value = np.clip(current_value / 5, -1, 1)  # 假设数值范围是-5到5
                        angle = normalized_value * np.pi / 2
                        
                        needle_x = [0, np.cos(angle + np.pi/2)]
                        needle_y = [0, np.sin(angle + np.pi/2)]
                        needle.set_data(needle_x, needle_y)
                        
                        # 添加数值标签
                        ax_gauge.clear()
                        theta = np.linspace(0, np.pi, 100)
                        ax_gauge.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=3)
                        ax_gauge.plot(needle_x, needle_y, 'r-', linewidth=4)
                        ax_gauge.text(0, -0.1, f'{current_value:.2f}', ha='center', va='center', 
                                    fontsize=14, fontweight='bold')
                        ax_gauge.set_xlim(-1.2, 1.2)
                        ax_gauge.set_ylim(-0.2, 1.2)
                        ax_gauge.set_aspect('equal')
                        ax_gauge.set_title('当前值指示器')
                        ax_gauge.axis('off')
                    
                    # 更新状态指标
                    if len(value_buffer) > 1:
                        values = list(value_buffer)
                        stats = [
                            len(values),
                            np.mean(values),
                            np.std(values),
                            abs(values[-1] - values[-2]) if len(values) > 1 else 0
                        ]
                        
                        for bar, stat in zip(status_bars, stats):
                            bar.set_height(abs(stat))
                        
                        # 自动调整y轴
                        max_stat = max(abs(s) for s in stats)
                        ax_status.set_ylim(0, max_stat * 1.1 if max_stat > 0 else 1)
                
                fig.canvas.draw_idle()
        
        # 控制面板
        plt.subplots_adjust(bottom=0.1)
        
        ax_start = plt.axes([0.1, 0.02, 0.1, 0.05])
        button_start = Button(ax_start, '开始')
        
        ax_stop = plt.axes([0.25, 0.02, 0.1, 0.05])
        button_stop = Button(ax_stop, '停止')
        
        ax_reset = plt.axes([0.4, 0.02, 0.1, 0.05])
        button_reset = Button(ax_reset, '重置')
        
        def start_dashboard(event):
            """开始仪表板"""
            running[0] = True
            timer = fig.canvas.new_timer(interval=100)
            timer.add_callback(update_dashboard)
            timer.start()
            
            # 保存timer引用以便停止
            fig._dashboard_timer = timer
        
        def stop_dashboard(event):
            """停止仪表板"""
            running[0] = False
            if hasattr(fig, '_dashboard_timer'):
                fig._dashboard_timer.stop()
        
        def reset_dashboard(event):
            """重置仪表板"""
            running[0] = False
            if hasattr(fig, '_dashboard_timer'):
                fig._dashboard_timer.stop()
            
            # 清除数据
            time_buffer.clear()
            value_buffer.clear()
            start_time[0] = time.time()
            
            # 重置图表
            line_main.set_data([], [])
            scatter_points.set_offsets(np.empty((0, 2)))
            
            ax_hist.clear()
            ax_hist.set_title('数值分布')
            ax_hist.set_xlabel('数值')
            ax_hist.set_ylabel('频数')
            
            for bar in status_bars:
                bar.set_height(0)
            
            fig.canvas.draw()
        
        # 连接事件
        button_start.on_clicked(start_dashboard)
        button_stop.on_clicked(stop_dashboard)
        button_reset.on_clicked(reset_dashboard)
        
        plt.show()
        
        return fig

# 使用示例
real_time_demo = RealTimeCharts()

print("1. 实时线图演示")
fig1 = real_time_demo.real_time_line_chart()

print("2. 实时仪表板演示")
fig2 = real_time_demo.real_time_dashboard()

7.3 本章总结

7.3.1 学习要点回顾

本章我们深入学习了 Matplotlib 中的动画和交互式图表制作技术:

动画制作基础 - FuncAnimation 的使用方法和参数配置 - 不同类型图表的动画实现(线图、散点图、柱状图等) - 关键帧动画和缓动函数的应用 - 动画保存和导出的多种格式支持

交互式图表开发 - 鼠标事件处理(点击、拖拽、悬停、滚轮) - 键盘事件响应和快捷键设置 - 缩放、平移和选择工具的实现 - 控件和小部件的集成使用

实时数据可视化 - 实时数据流的处理和显示 - 多线程数据生成和图表更新 - 综合仪表板的设计和实现 - 性能优化和内存管理

7.3.2 实践练习

练习1:股票价格动画

# 创建一个股票价格变化的动画图表
# 要求:
# 1. 显示K线图动画
# 2. 包含成交量柱状图
# 3. 添加移动平均线
# 4. 实现暂停/继续功能

def create_stock_animation():
    # 生成模拟股票数据
    dates = pd.date_range('2024-01-01', periods=100, freq='D')
    prices = 100 + np.cumsum(np.random.randn(100) * 0.5)
    volumes = np.random.randint(1000, 10000, 100)
    
    # 实现K线图动画
    # ...
    
    return fig

练习2:交互式数据探索工具

# 创建一个交互式数据探索工具
# 要求:
# 1. 支持多种图表类型切换
# 2. 可以选择不同的数据列
# 3. 包含数据过滤功能
# 4. 提供统计信息显示

def create_data_explorer(data):
    # 实现交互式数据探索界面
    # ...
    
    return fig

练习3:实时监控系统

# 创建一个实时系统监控界面
# 要求:
# 1. 监控CPU、内存使用率
# 2. 显示网络流量
# 3. 包含告警阈值设置
# 4. 支持历史数据查看

def create_system_monitor():
    # 实现实时系统监控
    # ...
    
    return fig

7.3.3 常见问题解答

Q1: 动画播放卡顿怎么办? A1: 可以尝试以下优化方法: - 使用 blit=True 参数 - 减少数据点数量 - 降低更新频率 - 优化绘图代码,避免重复计算

Q2: 如何在动画中添加用户交互? A2: 可以结合使用动画和事件处理:

def on_click(event):
    # 处理点击事件
    if event.inaxes:
        # 暂停/继续动画
        if anim.running:
            anim.pause()
        else:
            anim.resume()

fig.canvas.mpl_connect('button_press_event', on_click)

Q3: 实时数据更新时内存占用过高? A3: 使用固定大小的数据缓冲区:

from collections import deque

# 限制数据点数量
max_points = 1000
data_buffer = deque(maxlen=max_points)

Q4: 如何保存交互式图表? A4: 交互式图表通常保存为静态图像,如需保存交互功能,可以: - 使用 Plotly 等支持交互式导出的库 - 保存为 HTML 格式(需要额外工具) - 录制屏幕制作演示视频

7.3.4 进阶技巧

1. 自定义动画控制器

class AnimationController:
    def __init__(self, fig, anim):
        self.fig = fig
        self.anim = anim
        self.paused = False
    
    def toggle_pause(self):
        if self.paused:
            self.anim.resume()
        else:
            self.anim.pause()
        self.paused = not self.paused
    
    def set_speed(self, speed_factor):
        self.anim.interval = int(self.anim.interval / speed_factor)

2. 多图表同步动画

def create_synchronized_animation():
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
    
    def animate(frame):
        # 同时更新两个子图
        ax1.clear()
        ax2.clear()
        
        # 绘制相关联的数据
        # ...
    
    anim = FuncAnimation(fig, animate, frames=100, interval=50)
    return fig, anim

3. 响应式布局设计

def create_responsive_dashboard():
    fig = plt.figure(figsize=(16, 10))
    
    # 使用GridSpec创建响应式布局
    gs = fig.add_gridspec(3, 4, hspace=0.3, wspace=0.3)
    
    # 主图占据大部分空间
    ax_main = fig.add_subplot(gs[:2, :3])
    
    # 辅助图表
    ax_side1 = fig.add_subplot(gs[0, 3])
    ax_side2 = fig.add_subplot(gs[1, 3])
    ax_bottom = fig.add_subplot(gs[2, :])
    
    return fig

7.3.5 下章预告

在下一章《第八章:项目实战与案例分析》中,我们将:

  1. 综合项目实战

    • 数据分析报告自动化生成
    • 交互式数据仪表板开发
    • 科学计算结果可视化
  2. 行业应用案例

    • 金融数据分析与可视化
    • 科研数据图表制作
    • 商业智能报表设计
  3. 最佳实践总结

    • 代码组织和模块化
    • 性能优化策略
    • 团队协作规范
  4. 扩展学习方向

    • 与其他可视化库的集成
    • Web应用中的图表嵌入
    • 大数据可视化解决方案

通过本章的学习,你已经掌握了 Matplotlib 中动画和交互式图表的核心技术。这些技能将帮助你创建更加生动和用户友好的数据可视化应用,为数据分析和展示工作增添强大的交互性和动态效果。