You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Matplotlib的Barh横向条形图竞赛添加平滑过渡动画效果

Matplotlib条形竞赛平滑过渡实现方案

问题根源

你当前代码过渡生硬的核心原因有两个:

  • 每帧直接对应一个真实年份,没有生成相邻年份之间的过渡中间帧
  • 每次渲染都调用ax.clear()清空重绘所有元素,没有复用已有组件做属性插值

实现逻辑

核心方案是在相邻两个真实年份之间插入若干过渡帧,对每个城市的两个核心属性做插值计算:

  1. 条形宽度:对应数值的平滑增长/下降
  2. 条形y轴坐标:对应排名变化的上下交错移动
    渲染时不要每次清空画布,提前初始化所有条形、文本对象,每帧仅更新对象的位置、数值、文本内容等属性即可。

可直接运行的修改后代码

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.animation as animation
import numpy as np

# 基础配置
N_FRAMES_PER_YEAR = 10 # 每两个真实年份之间的过渡帧数,数值越大越平滑
TOP_N = 10 # 每次展示的排名数量

# 数据读取和预处理
url = 'https://gist.githubusercontent.com/johnburnmurdoch/4199dbe55095c3e13de8d5b2e5e5307a/raw/fa018b25c24b7b5f47fd0568937ff6c04e384786/city_populations'
df = pd.read_csv(url, usecols=['name', 'group', 'year', 'value'])
years = sorted(df['year'].unique())
min_year, max_year = years[0], years[-1]

colors = dict(zip(
    ["India", "Europe", "Asia", "Latin America", "Middle East", "North America", "Africa"],
    ["#adb0ff", "#ffb3ff", "#90d595", "#e48381", "#aafbff", "#f7bb5f", "#eafb50"]
))
group_lk = df.set_index('name')['group'].to_dict()

# 预计算每个年份每个城市的排名和数值
year_data = {}
for y in years:
    dff = df[df['year'] == y].sort_values(by='value', ascending=False).head(TOP_N)
    dff['rank'] = np.arange(len(dff))
    year_data[y] = dff.set_index('name')[['value', 'rank']].to_dict('index')
all_cities = df['name'].unique()

# 初始化画布和元素
fig, ax = plt.subplots(figsize=(15, 8))
# 初始化条形、文本对象
bars = ax.barh([], [], color=[])
name_labels = [ax.text(0, 0, '', size=12, color='#777777', weight=600, ha='left', va='center') for _ in range(TOP_N)]
year_text = ax.text(1, 0.4, '', transform=ax.transAxes, color='#777777', size=46, ha='right', weight=800)

# 画布固定配置
ax.xaxis.set_major_formatter(ticker.StrMethodFormatter('{x:,.0f}'))
ax.xaxis.set_ticks_position('bottom')
ax.tick_params(axis='x', colors='#777777', labelsize=12)
ax.set_yticks([])
ax.margins(0, 0.01)
ax.grid(which='major', axis='x', linestyle='-')
ax.set_axisbelow(True)
ax.text(0, 1.15, 'This is a title',
        transform=ax.transAxes, size=24, weight=600, ha='left', va='top', color="#777")
ax.text(1, 0, "My description here", transform=ax.transAxes, color='#777777', ha='right',
        bbox=dict(facecolor='white', alpha=0.8, edgecolor='white'))
plt.box(False)
plt.subplots_adjust(left=0.15, right=0.80, bottom=0.20, top=0.86)
ax.set_ylim(-0.5, TOP_N - 0.5)

def animate(frame):
    # 计算当前帧对应的年份和过渡进度
    year_idx = frame // N_FRAMES_PER_YEAR
    progress = (frame % N_FRAMES_PER_YEAR) / N_FRAMES_PER_YEAR
    current_base_year = years[year_idx]
    next_base_year = years[year_idx + 1] if year_idx + 1 < len(years) else years[-1]
    
    # 插值计算每个城市当前帧的数值和排名
    current_vals = []
    current_ranks = []
    current_names = []
    current_colors = []
    for city in all_cities:
        if city not in year_data[current_base_year] or city not in year_data[next_base_year]:
            continue
        val1, rank1 = year_data[current_base_year][city]['value'], year_data[current_base_year][city]['rank']
        val2, rank2 = year_data[next_base_year][city]['value'], year_data[next_base_year][city]['rank']
        interp_val = val1 + (val2 - val1) * progress
        interp_rank = rank1 + (rank2 - rank1) * progress
        current_vals.append(interp_val)
        current_ranks.append(interp_rank)
        current_names.append(city)
        current_colors.append(colors[group_lk[city]])
    
    # 按插值后的排名排序,取前TOP_N
    frame_df = pd.DataFrame({
        'name': current_names,
        'value': current_vals,
        'rank': current_ranks,
        'color': current_colors
    }).sort_values('rank', ascending=True).head(TOP_N)
    frame_df = frame_df.sort_values('rank', ascending=False) # 倒序适配barh的y轴顺序
    
    # 更新条形属性
    for bar, w, c, y in zip(bars, frame_df['value'], frame_df['color'], range(TOP_N)):
        bar.set_width(w)
        bar.set_color(c)
        bar.set_y(y - 0.4)
        bar.set_height(0.8)
    # 更新文本属性
    dx = frame_df['value'].max() / 200
    for label, val, name, y in zip(name_labels, frame_df['value'], frame_df['name'], range(TOP_N)):
        label.set_x(val - dx)
        label.set_y(y)
        label.set_text(f"   {name} ({round(val, 2)})")
    # 更新年份文本
    display_year = current_base_year + (next_base_year - current_base_year) * progress
    year_text.set_text(f"{int(display_year)}")
    # 更新x轴范围
    ax.set_xlim(0, frame_df['value'].max() * 1.1)
    return bars + name_labels + [year_text]

# 生成总帧数
total_frames = (len(years) - 1) * N_FRAMES_PER_YEAR
animator = animation.FuncAnimation(fig, animate, frames=total_frames, interval=50, blit=True)
# 保存gif,可调整fps控制播放速度
animator.save('smooth_barchart_race.gif', dpi=180, writer='imagemagick', fps=30)
plt.show()

参数调整说明

  • 调整N_FRAMES_PER_YEAR数值:数值越大过渡越平滑,对应渲染时间也越长,10~20区间是性价比最高的选择
  • 可将线性插值替换为缓动函数(如progress = np.sin(progress * np.pi / 2)),过渡效果会更自然,避免匀速运动的机械感
  • 保存时调整fps参数可控制整体播放速度,25~30fps视觉体验最优

内容的提问来源于stack exchange,提问作者Alexa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.29 16:06:05