如何为Matplotlib的Barh横向条形图竞赛添加平滑过渡动画效果
Matplotlib条形竞赛平滑过渡实现方案
问题根源
你当前代码过渡生硬的核心原因有两个:
- 每帧直接对应一个真实年份,没有生成相邻年份之间的过渡中间帧
- 每次渲染都调用
ax.clear()清空重绘所有元素,没有复用已有组件做属性插值
实现逻辑
核心方案是在相邻两个真实年份之间插入若干过渡帧,对每个城市的两个核心属性做插值计算:
- 条形宽度:对应数值的平滑增长/下降
- 条形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
相关产品推荐
相关产品推荐

