Просмотр исходного кода

feat(渠道效果分析): 添加全表日期切换和排名动画效果

- 所有12个表格支持分天数据切换(3整体+5一级矩阵+4二级矩阵)
- 全局日期切换控制所有表格同步切换
- 每个表格独立日期切换控件(前一天/后一天/播放)
- 排名上升行绿色动画,下降行红色动画,渐变恢复
- 重构代码,使用 daily_html 预渲染所有日期数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
yangxiaohui 2 месяцев назад
Родитель
Сommit
100a8dad1a
1 измененных файлов с 580 добавлено и 369 удалено
  1. 580 369
      tasks/渠道效果分析/visualize.py

+ 580 - 369
tasks/渠道效果分析/visualize.py

@@ -34,67 +34,130 @@ print(f"分析文件: {latest_file.name}")
 print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")
 print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")
 
 
 # ============================================================
 # ============================================================
-# 数据准备
+# 日期列表
 # ============================================================
 # ============================================================
 
 
-# 渠道整体
-channel_stats = df.groupby('channel').agg({
-    '点击uv': 'sum',
-    '裂变uv': 'sum'
-}).reset_index()
-
-for ch in channel_stats['channel']:
-    ch_df = df[df['channel'] == ch]
-    uv = ch_df['点击uv'].sum()
-    channel_stats.loc[channel_stats['channel'] == ch, '进入分发率'] = (ch_df['进入分发率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '整体裂变率'] = (ch_df['整体裂变率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '头部裂变率'] = (ch_df['头部裂变率'] * ch_df['点击uv']).sum() / uv
-    channel_stats.loc[channel_stats['channel'] == ch, '推荐裂变率'] = (ch_df['推荐裂变率'] * ch_df['点击uv']).sum() / uv
-
-channel_stats = channel_stats.sort_values('点击uv', ascending=False)
-
-# 一级品类整体
-category_stats = df.groupby('merge一级品类').agg({
-    '点击uv': 'sum',
-    '裂变uv': 'sum'
-}).reset_index()
-
-for cat in category_stats['merge一级品类']:
-    cat_df = df[df['merge一级品类'] == cat]
-    uv = cat_df['点击uv'].sum()
-    category_stats.loc[category_stats['merge一级品类'] == cat, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
-    category_stats.loc[category_stats['merge一级品类'] == cat, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
-    category_stats.loc[category_stats['merge一级品类'] == cat, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
-    category_stats.loc[category_stats['merge一级品类'] == cat, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
-
-category_stats = category_stats.sort_values('点击uv', ascending=False)
-
-# 二级品类整体
-cat2_overall = df.groupby(['merge一级品类', 'merge二级品类']).agg({
-    '点击uv': 'sum',
-    '裂变uv': 'sum'
-}).reset_index()
-
-cat2_overall['cat2_label'] = cat2_overall.apply(
-    lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
-    if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
-)
-
-for idx in cat2_overall.index:
-    cat1 = cat2_overall.loc[idx, 'merge一级品类']
-    cat2 = cat2_overall.loc[idx, 'merge二级品类']
-    mask = (df['merge一级品类'] == cat1) & (df['merge二级品类'] == cat2) if pd.notna(cat2) else (df['merge一级品类'] == cat1) & (df['merge二级品类'].isna())
-    cat_df = df[mask]
-    uv = cat_df['点击uv'].sum()
-    if uv > 0:
-        cat2_overall.loc[idx, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
-        cat2_overall.loc[idx, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
-        cat2_overall.loc[idx, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
-        cat2_overall.loc[idx, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
-
-cat2_overall = cat2_overall.sort_values('点击uv', ascending=False)
-
-# 渠道×品类
+all_dates = sorted([str(d) for d in df['dt'].unique()])
+date_options = ['all'] + all_dates  # 'all' 表示汇总
+latest_date = all_dates[-1] if all_dates else 'all'
+
+print(f"日期数: {len(all_dates)}")
+
+# ============================================================
+# 数据准备函数
+# ============================================================
+
+def calc_channel_stats(data):
+    """计算渠道整体数据"""
+    stats = data.groupby('channel').agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
+    for ch in stats['channel']:
+        ch_df = data[data['channel'] == ch]
+        uv = ch_df['点击uv'].sum()
+        if uv > 0:
+            stats.loc[stats['channel'] == ch, '进入分发率'] = (ch_df['进入分发率'] * ch_df['点击uv']).sum() / uv
+            stats.loc[stats['channel'] == ch, '整体裂变率'] = (ch_df['整体裂变率'] * ch_df['点击uv']).sum() / uv
+            stats.loc[stats['channel'] == ch, '头部裂变率'] = (ch_df['头部裂变率'] * ch_df['点击uv']).sum() / uv
+            stats.loc[stats['channel'] == ch, '推荐裂变率'] = (ch_df['推荐裂变率'] * ch_df['点击uv']).sum() / uv
+    return stats.sort_values('点击uv', ascending=False)
+
+def calc_category_stats(data):
+    """计算一级品类整体数据"""
+    stats = data.groupby('merge一级品类').agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
+    for cat in stats['merge一级品类']:
+        cat_df = data[data['merge一级品类'] == cat]
+        uv = cat_df['点击uv'].sum()
+        if uv > 0:
+            stats.loc[stats['merge一级品类'] == cat, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[stats['merge一级品类'] == cat, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[stats['merge一级品类'] == cat, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[stats['merge一级品类'] == cat, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
+    return stats.sort_values('点击uv', ascending=False)
+
+def calc_cat2_stats(data):
+    """计算二级品类整体数据"""
+    stats = data.groupby(['merge一级品类', 'merge二级品类']).agg({'点击uv': 'sum', '裂变uv': 'sum'}).reset_index()
+    stats['cat2_label'] = stats.apply(
+        lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
+        if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
+    )
+    for idx in stats.index:
+        cat1, cat2 = stats.loc[idx, 'merge一级品类'], stats.loc[idx, 'merge二级品类']
+        mask = (data['merge一级品类'] == cat1) & (data['merge二级品类'] == cat2) if pd.notna(cat2) else (data['merge一级品类'] == cat1) & (data['merge二级品类'].isna())
+        cat_df = data[mask]
+        uv = cat_df['点击uv'].sum()
+        if uv > 0:
+            stats.loc[idx, '进入分发率'] = (cat_df['进入分发率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[idx, '整体裂变率'] = (cat_df['整体裂变率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[idx, '头部裂变率'] = (cat_df['头部裂变率'] * cat_df['点击uv']).sum() / uv
+            stats.loc[idx, '推荐裂变率'] = (cat_df['推荐裂变率'] * cat_df['点击uv']).sum() / uv
+    return stats.sort_values('点击uv', ascending=False)
+
+# ============================================================
+# 为每个日期计算数据
+# ============================================================
+
+daily_data = {}
+for dt in date_options:
+    data = df if dt == 'all' else df[df['dt'].astype(str) == dt]
+    daily_data[dt] = {
+        'channel_stats': calc_channel_stats(data),
+        'category_stats': calc_category_stats(data),
+        'cat2_stats': calc_cat2_stats(data),
+    }
+
+# 使用汇总数据作为默认
+channel_stats = daily_data['all']['channel_stats']
+category_stats = daily_data['all']['category_stats']
+cat2_overall = daily_data['all']['cat2_stats']
+
+# 矩阵数据计算函数
+def calc_channel_category(data):
+    """计算渠道×一级品类矩阵"""
+    cc = data.groupby(['channel', 'merge一级品类']).apply(
+        lambda x: pd.Series({
+            '点击uv': x['点击uv'].sum(),
+            '进入分发率': (x['进入分发率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+            '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+            '头部裂变率': (x['头部裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+            '推荐裂变率': (x['推荐裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
+        }), include_groups=False
+    ).reset_index()
+    return {
+        'ror': cc.pivot(index='merge一级品类', columns='channel', values='整体裂变率'),
+        'ror_orig': cc.pivot(index='merge一级品类', columns='channel', values='头部裂变率'),
+        'ror_rec': cc.pivot(index='merge一级品类', columns='channel', values='推荐裂变率'),
+        'uv': cc.pivot(index='merge一级品类', columns='channel', values='点击uv').fillna(0),
+        'recommend': cc.pivot(index='merge一级品类', columns='channel', values='进入分发率'),
+    }
+
+def calc_channel_cat2(data):
+    """计算渠道×二级品类矩阵"""
+    cc2 = data.groupby(['channel', 'merge一级品类', 'merge二级品类']).apply(
+        lambda x: pd.Series({
+            '点击uv': x['点击uv'].sum(),
+            '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+            '头部裂变率': (x['头部裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
+            '推荐裂变率': (x['推荐裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
+        }), include_groups=False
+    ).reset_index()
+    cc2['cat2_label'] = cc2.apply(
+        lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
+        if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
+    )
+    return {
+        'ror': cc2.pivot_table(index='cat2_label', columns='channel', values='整体裂变率'),
+        'ror_orig': cc2.pivot_table(index='cat2_label', columns='channel', values='头部裂变率'),
+        'ror_rec': cc2.pivot_table(index='cat2_label', columns='channel', values='推荐裂变率'),
+        'uv': cc2.pivot_table(index='cat2_label', columns='channel', values='点击uv', fill_value=0),
+    }
+
+# 为每个日期计算矩阵数据
+for dt in date_options:
+    data = df if dt == 'all' else df[df['dt'].astype(str) == dt]
+    daily_data[dt]['matrix'] = calc_channel_category(data)
+    daily_data[dt]['matrix2'] = calc_channel_cat2(data)
+
+# 使用汇总数据
 channel_category = df.groupby(['channel', 'merge一级品类']).apply(
 channel_category = df.groupby(['channel', 'merge一级品类']).apply(
     lambda x: pd.Series({
     lambda x: pd.Series({
         '点击uv': x['点击uv'].sum(),
         '点击uv': x['点击uv'].sum(),
@@ -109,267 +172,151 @@ pivot_ror_orig = channel_category.pivot(index='merge一级品类', columns='chan
 pivot_ror_rec = channel_category.pivot(index='merge一级品类', columns='channel', values='推荐裂变率')
 pivot_ror_rec = channel_category.pivot(index='merge一级品类', columns='channel', values='推荐裂变率')
 pivot_uv = channel_category.pivot(index='merge一级品类', columns='channel', values='点击uv').fillna(0)
 pivot_uv = channel_category.pivot(index='merge一级品类', columns='channel', values='点击uv').fillna(0)
 
 
-# 每日趋势
-daily = df.groupby(['dt', 'channel']).apply(
-    lambda x: pd.Series({
-        '点击uv': x['点击uv'].sum(),
-        '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
-    }), include_groups=False
-).reset_index()
-
 # ============================================================
 # ============================================================
 # 统一渐变颜色函数
 # 统一渐变颜色函数
 # ============================================================
 # ============================================================
 
 
 def get_gradient_color(val, max_val, min_val=0):
 def get_gradient_color(val, max_val, min_val=0):
-    """
-    统一的渐变颜色:白色(min) -> 绿色(max)
-    所有矩阵使用相同的渐变逻辑,统一黑色字体
-    """
+    """统一的渐变颜色:白色(min) -> 绿色(max)"""
     if val is None or pd.isna(val) or val <= min_val:
     if val is None or pd.isna(val) or val <= min_val:
         return "background: #f8f9fa"
         return "background: #f8f9fa"
     ratio = min((val - min_val) / (max_val - min_val), 1.0)
     ratio = min((val - min_val) / (max_val - min_val), 1.0)
-    # 白色 (255,255,255) -> 绿色 (40,167,69)
     r = int(255 - ratio * 215)
     r = int(255 - ratio * 215)
     g = int(255 - ratio * 88)
     g = int(255 - ratio * 88)
     b = int(255 - ratio * 186)
     b = int(255 - ratio * 186)
     return f"background: rgb({r},{g},{b})"
     return f"background: rgb({r},{g},{b})"
 
 
 # ============================================================
 # ============================================================
-# 准备图表数
+# 表格HTML生成函
 # ============================================================
 # ============================================================
 
 
-# 主要渠道(UV > 10000)
-main_channels = channel_stats[channel_stats['点击uv'] > 10000]['channel'].tolist()
-valid_categories = pivot_uv[pivot_uv.sum(axis=1) >= 1000].index.tolist()
-heatmap_cols = [c for c in main_channels if c in pivot_ror.columns]
-
-# 1. 渠道表格行(使用统一渐变)
-channel_rows = []
-max_channel_uv = channel_stats['点击uv'].max()
-for _, row in channel_stats.iterrows():
-    uv_style = get_gradient_color(row['点击uv'], max_channel_uv)  # 点击UV渐变
-    rec_style = get_gradient_color(row['进入分发率'], 1.0, 0.6)   # 进入分发率 0.6~1.0
-    ror_style = get_gradient_color(row['整体裂变率'], 0.8)        # 整体裂变率 0~0.8
-    orig_style = get_gradient_color(row['头部裂变率'], 0.3)       # 头部裂变率 0~0.3
-    rec_ror_style = get_gradient_color(row['推荐裂变率'], 0.5)    # 推荐裂变率 0~0.5
-    channel_rows.append(
-        f"<tr><td>{row['channel']}</td>"
-        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
-        f"<td style='{rec_style}; text-align:center'>{row['进入分发率']:.4f}</td>"
-        f"<td style='{ror_style}; text-align:center'>{row['整体裂变率']:.4f}</td>"
-        f"<td style='{orig_style}; text-align:center'>{row['头部裂变率']:.4f}</td>"
-        f"<td style='{rec_ror_style}; text-align:center'>{row['推荐裂变率']:.4f}</td></tr>"
-    )
-
-# 1.5 一级品类表格行(使用统一渐变)
-category_rows = []
-max_cat_uv = category_stats['点击uv'].max()
-for _, row in category_stats.iterrows():
-    cat_name = str(row['merge一级品类']) if pd.notna(row['merge一级品类']) else '-'
-    uv_style = get_gradient_color(row['点击uv'], max_cat_uv)
-    rec_style = get_gradient_color(row['进入分发率'], 1.0, 0.6)
-    ror_style = get_gradient_color(row['整体裂变率'], 0.8)
-    orig_style = get_gradient_color(row['头部裂变率'], 0.3)
-    rec_ror_style = get_gradient_color(row['推荐裂变率'], 0.5)
-    category_rows.append(
-        f"<tr><td>{cat_name}</td>"
-        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
-        f"<td style='{rec_style}; text-align:center'>{row['进入分发率']:.4f}</td>"
-        f"<td style='{ror_style}; text-align:center'>{row['整体裂变率']:.4f}</td>"
-        f"<td style='{orig_style}; text-align:center'>{row['头部裂变率']:.4f}</td>"
-        f"<td style='{rec_ror_style}; text-align:center'>{row['推荐裂变率']:.4f}</td></tr>"
-    )
-
-# 1.6 二级品类表格行(使用统一渐变)
-cat2_overall_rows = []
-max_cat2_uv = cat2_overall['点击uv'].max()
-for _, row in cat2_overall.iterrows():
-    uv_style = get_gradient_color(row['点击uv'], max_cat2_uv)
-    rec_style = get_gradient_color(row.get('进入分发率'), 1.0, 0.6)
-    ror_style = get_gradient_color(row.get('整体裂变率'), 0.8)
-    orig_style = get_gradient_color(row.get('头部裂变率'), 0.3)
-    rec_ror_style = get_gradient_color(row.get('推荐裂变率'), 0.5)
-    cat2_overall_rows.append(
-        f"<tr><td>{row['cat2_label']}</td>"
-        f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
-        f"<td style='{rec_style}; text-align:center'>{row.get('进入分发率', 0):.4f}</td>"
-        f"<td style='{ror_style}; text-align:center'>{row.get('整体裂变率', 0):.4f}</td>"
-        f"<td style='{orig_style}; text-align:center'>{row.get('头部裂变率', 0):.4f}</td>"
-        f"<td style='{rec_ror_style}; text-align:center'>{row.get('推荐裂变率', 0):.4f}</td></tr>"
-    )
-
-# 2. 回流率热力图(使用统一渐变)
-ror_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
-
-# 2.1 整体裂变率
-ror_rows = []
-for cat in valid_categories:
-    cells = [f"<td>{str(cat)}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_ror.columns and pd.notna(pivot_ror.loc[cat, ch]):
-            val = pivot_ror.loc[cat, ch]
-            style = get_gradient_color(val, 0.8)
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    ror_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 2.2 头部裂变率
-ror_orig_rows = []
-for cat in valid_categories:
-    cells = [f"<td>{str(cat)}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_ror_orig.columns and pd.notna(pivot_ror_orig.loc[cat, ch]):
-            val = pivot_ror_orig.loc[cat, ch]
-            style = get_gradient_color(val, 0.3)  # 原视频 max=0.3
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    ror_orig_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 2.3 推荐裂变率
-ror_rec_rows = []
-for cat in valid_categories:
-    cells = [f"<td>{str(cat)}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_ror_rec.columns and pd.notna(pivot_ror_rec.loc[cat, ch]):
-            val = pivot_ror_rec.loc[cat, ch]
-            style = get_gradient_color(val, 0.5)  # 推荐 max=0.5
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    ror_rec_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 3. UV分布热力图(使用统一渐变)
-uv_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
-uv_rows = []
-for cat in valid_categories:
-    cells = [f"<td>{str(cat)}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_uv.columns:
-            val = pivot_uv.loc[cat, ch]
-            if val > 0:
-                style = get_gradient_color(val, 100000)  # UV max=10万
-                cells.append(f'<td style="{style}">{int(val):,}</td>')
+def gen_channel_rows(stats):
+    """生成渠道表格行"""
+    rows = []
+    max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
+    for _, row in stats.iterrows():
+        uv_style = get_gradient_color(row['点击uv'], max_uv)
+        rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
+        ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
+        orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
+        rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
+        rows.append(
+            f"<tr><td>{row['channel']}</td>"
+            f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+            f"<td style='{rec_style}; text-align:center'>{row.get('进入分发率', 0):.4f}</td>"
+            f"<td style='{ror_style}; text-align:center'>{row.get('整体裂变率', 0):.4f}</td>"
+            f"<td style='{orig_style}; text-align:center'>{row.get('头部裂变率', 0):.4f}</td>"
+            f"<td style='{rec_ror_style}; text-align:center'>{row.get('推荐裂变率', 0):.4f}</td></tr>"
+        )
+    return "".join(rows)
+
+def gen_category_rows(stats):
+    """生成品类表格行"""
+    rows = []
+    max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
+    for _, row in stats.iterrows():
+        cat_name = str(row['merge一级品类']) if pd.notna(row['merge一级品类']) else '-'
+        uv_style = get_gradient_color(row['点击uv'], max_uv)
+        rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
+        ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
+        orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
+        rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
+        rows.append(
+            f"<tr><td>{cat_name}</td>"
+            f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+            f"<td style='{rec_style}; text-align:center'>{row.get('进入分发率', 0):.4f}</td>"
+            f"<td style='{ror_style}; text-align:center'>{row.get('整体裂变率', 0):.4f}</td>"
+            f"<td style='{orig_style}; text-align:center'>{row.get('头部裂变率', 0):.4f}</td>"
+            f"<td style='{rec_ror_style}; text-align:center'>{row.get('推荐裂变率', 0):.4f}</td></tr>"
+        )
+    return "".join(rows)
+
+def gen_cat2_rows(stats):
+    """生成二级品类表格行"""
+    rows = []
+    max_uv = stats['点击uv'].max() if len(stats) > 0 else 1
+    for _, row in stats.iterrows():
+        uv_style = get_gradient_color(row['点击uv'], max_uv)
+        rec_style = get_gradient_color(row.get('进入分发率', 0), 1.0, 0.6)
+        ror_style = get_gradient_color(row.get('整体裂变率', 0), 0.8)
+        orig_style = get_gradient_color(row.get('头部裂变率', 0), 0.3)
+        rec_ror_style = get_gradient_color(row.get('推荐裂变率', 0), 0.5)
+        rows.append(
+            f"<tr><td>{row['cat2_label']}</td>"
+            f"<td style='{uv_style}; text-align:right'>{int(row['点击uv']):,}</td>"
+            f"<td style='{rec_style}; text-align:center'>{row.get('进入分发率', 0):.4f}</td>"
+            f"<td style='{ror_style}; text-align:center'>{row.get('整体裂变率', 0):.4f}</td>"
+            f"<td style='{orig_style}; text-align:center'>{row.get('头部裂变率', 0):.4f}</td>"
+            f"<td style='{rec_ror_style}; text-align:center'>{row.get('推荐裂变率', 0):.4f}</td></tr>"
+        )
+    return "".join(rows)
+
+def gen_matrix_rows(pivot_df, valid_rows, cols, max_val, min_val=0, is_int=False):
+    """生成矩阵热力图行"""
+    rows = []
+    for cat in valid_rows:
+        if cat not in pivot_df.index:
+            continue
+        cells = [f"<td>{str(cat)}</td>"]
+        for ch in cols:
+            if ch in pivot_df.columns and pd.notna(pivot_df.loc[cat, ch]):
+                val = pivot_df.loc[cat, ch]
+                style = get_gradient_color(val, max_val, min_val)
+                if is_int:
+                    cells.append(f'<td style="{style}">{int(val):,}</td>' if val > 0 else '<td>-</td>')
+                else:
+                    cells.append(f'<td style="{style}">{val:.4f}</td>')
             else:
             else:
                 cells.append("<td>-</td>")
                 cells.append("<td>-</td>")
-        else:
-            cells.append("<td>-</td>")
-    uv_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 4. 进入分发率热力图(使用统一渐变)
-pivot_recommend = df.groupby(['merge一级品类', 'channel']).apply(
-    lambda x: (x['进入分发率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
-    include_groups=False
-).unstack()
-
-recommend_rows = []
-for cat in valid_categories:
-    if cat not in pivot_recommend.index:
-        continue
-    cells = [f"<td>{str(cat)}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_recommend.columns and pd.notna(pivot_recommend.loc[cat, ch]):
-            val = pivot_recommend.loc[cat, ch]
-            style = get_gradient_color(val, 1.0, 0.6)  # 推荐率 0.6~1.0
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    recommend_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 5. 二级品类热力图
-channel_cat2 = df.groupby(['channel', 'merge一级品类', 'merge二级品类']).apply(
-    lambda x: pd.Series({
-        '点击uv': x['点击uv'].sum(),
-        '整体裂变率': (x['整体裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
-        '头部裂变率': (x['头部裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0,
-        '推荐裂变率': (x['推荐裂变率'] * x['点击uv']).sum() / x['点击uv'].sum() if x['点击uv'].sum() > 0 else 0
-    }), include_groups=False
-).reset_index()
+        rows.append("<tr>" + "".join(cells) + "</tr>")
+    return "".join(rows)
 
 
-# 创建二级品类标签
-channel_cat2['cat2_label'] = channel_cat2.apply(
-    lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
-    if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
-)
+# ============================================================
+# 为每个日期生成表格HTML
+# ============================================================
 
 
-# 二级品类汇总
-cat2_stats = channel_cat2.groupby('cat2_label').agg({'点击uv': 'sum'}).reset_index()
-cat2_stats = cat2_stats[cat2_stats['点击uv'] >= 1000].sort_values('点击uv', ascending=False)
-valid_cat2_labels = cat2_stats.head(20)['cat2_label'].tolist()
+# 先计算汇总数据确定有效品类和渠道
+all_channel_stats = daily_data['all']['channel_stats']
+main_channels = all_channel_stats[all_channel_stats['点击uv'] > 10000]['channel'].tolist()
+all_matrix = daily_data['all']['matrix']
+valid_categories = all_matrix['uv'][all_matrix['uv'].sum(axis=1) >= 1000].index.tolist()
+heatmap_cols = [c for c in main_channels if c in all_matrix['ror'].columns]
+
+# 二级品类有效列表
+all_matrix2 = daily_data['all']['matrix2']
+cat2_uv_sum = all_matrix2['uv'].sum(axis=1)
+valid_cat2_labels = cat2_uv_sum[cat2_uv_sum >= 1000].sort_values(ascending=False).head(20).index.tolist()
+
+daily_html = {}
+for dt in date_options:
+    data = daily_data[dt]
+    matrix = data['matrix']
+    matrix2 = data['matrix2']
+
+    daily_html[dt] = {
+        'channel_rows': gen_channel_rows(data['channel_stats']),
+        'category_rows': gen_category_rows(data['category_stats']),
+        'cat2_rows': gen_cat2_rows(data['cat2_stats']),
+        # 渠道×一级品类矩阵
+        'matrix_ror': gen_matrix_rows(matrix['ror'], valid_categories, heatmap_cols, 0.8),
+        'matrix_ror_orig': gen_matrix_rows(matrix['ror_orig'], valid_categories, heatmap_cols, 0.3),
+        'matrix_ror_rec': gen_matrix_rows(matrix['ror_rec'], valid_categories, heatmap_cols, 0.5),
+        'matrix_uv': gen_matrix_rows(matrix['uv'], valid_categories, heatmap_cols, 100000, is_int=True),
+        'matrix_recommend': gen_matrix_rows(matrix['recommend'], valid_categories, heatmap_cols, 1.0, 0.6),
+        # 渠道×二级品类矩阵
+        'matrix2_ror': gen_matrix_rows(matrix2['ror'], valid_cat2_labels, heatmap_cols, 0.8),
+        'matrix2_ror_orig': gen_matrix_rows(matrix2['ror_orig'], valid_cat2_labels, heatmap_cols, 0.3),
+        'matrix2_ror_rec': gen_matrix_rows(matrix2['ror_rec'], valid_cat2_labels, heatmap_cols, 0.5),
+        'matrix2_uv': gen_matrix_rows(matrix2['uv'], valid_cat2_labels, heatmap_cols, 100000, is_int=True),
+    }
 
 
-pivot_cat2_ror = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='整体裂变率')
-pivot_cat2_ror_orig = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='头部裂变率')
-pivot_cat2_ror_rec = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='推荐裂变率')
-pivot_cat2_uv = channel_cat2.pivot_table(index='cat2_label', columns='channel', values='点击uv', fill_value=0)
+# ============================================================
+# 生成表头
+# ============================================================
 
 
-# 二级品类回流率热力图(使用统一渐变)
+ror_header = "<tr><th>品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
 cat2_ror_header = "<tr><th>二级品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
 cat2_ror_header = "<tr><th>二级品类</th>" + "".join([f"<th>{c}</th>" for c in heatmap_cols]) + "</tr>"
 
 
-# 整体裂变率
-cat2_ror_rows = []
-for cat2 in valid_cat2_labels:
-    if cat2 not in pivot_cat2_ror.index:
-        continue
-    cells = [f"<td>{cat2}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_cat2_ror.columns and pd.notna(pivot_cat2_ror.loc[cat2, ch]):
-            val = pivot_cat2_ror.loc[cat2, ch]
-            style = get_gradient_color(val, 0.8)
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    cat2_ror_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 头部裂变率
-cat2_ror_orig_rows = []
-for cat2 in valid_cat2_labels:
-    if cat2 not in pivot_cat2_ror_orig.index:
-        continue
-    cells = [f"<td>{cat2}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_cat2_ror_orig.columns and pd.notna(pivot_cat2_ror_orig.loc[cat2, ch]):
-            val = pivot_cat2_ror_orig.loc[cat2, ch]
-            style = get_gradient_color(val, 0.3)
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    cat2_ror_orig_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 推荐裂变率
-cat2_ror_rec_rows = []
-for cat2 in valid_cat2_labels:
-    if cat2 not in pivot_cat2_ror_rec.index:
-        continue
-    cells = [f"<td>{cat2}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_cat2_ror_rec.columns and pd.notna(pivot_cat2_ror_rec.loc[cat2, ch]):
-            val = pivot_cat2_ror_rec.loc[cat2, ch]
-            style = get_gradient_color(val, 0.5)
-            cells.append(f'<td style="{style}">{val:.4f}</td>')
-        else:
-            cells.append("<td>-</td>")
-    cat2_ror_rec_rows.append("<tr>" + "".join(cells) + "</tr>")
-
-# 二级品类UV热力图
-cat2_uv_rows = []
-for cat2 in valid_cat2_labels:
-    if cat2 not in pivot_cat2_uv.index:
-        continue
-    cells = [f"<td>{cat2}</td>"]
-    for ch in heatmap_cols:
-        if ch in pivot_cat2_uv.columns:
-            val = pivot_cat2_uv.loc[cat2, ch]
-            if val > 0:
-                style = get_gradient_color(val, 100000)
-                cells.append(f'<td style="{style}">{int(val):,}</td>')
-            else:
-                cells.append("<td>-</td>")
-        else:
-            cells.append("<td>-</td>")
-    cat2_uv_rows.append("<tr>" + "".join(cells) + "</tr>")
-
 # ============================================================
 # ============================================================
 # 生成 HTML
 # 生成 HTML
 # ============================================================
 # ============================================================
@@ -380,6 +327,14 @@ channel_count = df['channel'].nunique()
 category_count = df['merge一级品类'].nunique()
 category_count = df['merge一级品类'].nunique()
 date_range = f"{df['dt'].min()} ~ {df['dt'].max()}"
 date_range = f"{df['dt'].min()} ~ {df['dt'].max()}"
 
 
+# 日期选项HTML
+date_options_html = "".join([
+    f'<option value="{dt}" {"selected" if dt == latest_date else ""}>'
+    f'{"汇总" if dt == "all" else dt}</option>'
+    for dt in date_options
+])
+dates_json = json.dumps(date_options)
+
 html_content = f"""<!DOCTYPE html>
 html_content = f"""<!DOCTYPE html>
 <html>
 <html>
 <head>
 <head>
@@ -429,7 +384,37 @@ html_content = f"""<!DOCTYPE html>
         .reset-btn {{ font-size: 11px; padding: 2px 8px; margin-left: 10px; cursor: pointer;
         .reset-btn {{ font-size: 11px; padding: 2px 8px; margin-left: 10px; cursor: pointer;
                      background: #6c757d; color: white; border: none; border-radius: 3px; }}
                      background: #6c757d; color: white; border: none; border-radius: 3px; }}
         .reset-btn:hover {{ background: #5a6268; }}
         .reset-btn:hover {{ background: #5a6268; }}
-    </style>
+        .date-switcher {{ display: inline-flex; align-items: center; gap: 5px; margin-left: 15px; }}
+        .date-switcher select {{ padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }}
+        .date-switcher button {{ padding: 4px 10px; border: 1px solid #ccc; border-radius: 4px;
+                                background: white; cursor: pointer; font-size: 13px; }}
+        .date-switcher button:hover:not(:disabled) {{ background: #e9ecef; }}
+        .date-switcher button:disabled {{ opacity: 0.5; cursor: not-allowed; }}
+        .date-switcher .play-btn {{ background: #28a745; color: white; border-color: #28a745; }}
+        .date-switcher .play-btn:hover {{ background: #218838; }}
+        .date-switcher .play-btn.playing {{ background: #dc3545; border-color: #dc3545; }}
+        .global-date {{ background: #e3f2fd; padding: 10px 15px; border-radius: 8px; margin: 15px 0;
+                       display: flex; align-items: center; }}
+        .global-date label {{ font-weight: 600; margin-right: 10px; }}
+        .table-date {{ float: right; }}
+        .date-content {{ display: none; opacity: 0; }}
+        .date-content.active {{ display: block; opacity: 1; }}
+        .date-content table tr {{ transition: transform 0.5s ease, opacity 0.3s ease; }}
+        .date-content.animating table tr {{ transform: translateY(0); }}
+        @keyframes slideUpColor {{
+            0% {{ transform: translateY(20px); opacity: 0; color: #28a745; }}
+            40% {{ transform: translateY(0); opacity: 1; color: #28a745; }}
+            100% {{ transform: translateY(0); opacity: 1; color: inherit; }}
+        }}
+        @keyframes slideDownColor {{
+            0% {{ transform: translateY(-20px); opacity: 0; color: #dc3545; }}
+            40% {{ transform: translateY(0); opacity: 1; color: #dc3545; }}
+            100% {{ transform: translateY(0); opacity: 1; color: inherit; }}
+        }}
+        .row-up {{ animation: slideUpColor 1s ease forwards; }}
+        .row-down {{ animation: slideDownColor 1s ease forwards; }}
+        .row-same {{ animation: none; }}
+        </style>
 </head>
 </head>
 <body>
 <body>
     <nav class="sidebar">
     <nav class="sidebar">
@@ -476,118 +461,194 @@ html_content = f"""<!DOCTYPE html>
             </div>
             </div>
         </div>
         </div>
 
 
+        <div class="global-date">
+            <label>全局日期切换:</label>
+            <div class="date-switcher" id="global-switcher">
+                <button class="prev-btn" title="前一天">◀</button>
+                <select class="date-select">{date_options_html}</select>
+                <button class="next-btn" title="后一天">▶</button>
+                <button class="play-btn" title="播放">▶ 播放</button>
+            </div>
+        </div>
+
         <h2 id="sec1">一、整体表现</h2>
         <h2 id="sec1">一、整体表现</h2>
 
 
         <h3 id="sec1-1">1.1 渠道整体</h3>
         <h3 id="sec1-1">1.1 渠道整体</h3>
-        <div class="chart-container">
-            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-0">重置</button></div>
-            <table class="sortable">
-                <tr><th>渠道</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
-                {"".join(channel_rows)}
-            </table>
+        <div class="chart-container" data-section="channel">
+            <div class="legend">
+                颜色越深=数值越高 | 点击表头排序
+                <div class="date-switcher table-date" data-section="channel">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{'active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable"><tr><th>渠道</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
+                {daily_html[dt]['channel_rows']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec1-2">1.2 一级品类整体</h3>
         <h3 id="sec1-2">1.2 一级品类整体</h3>
-        <div class="chart-container">
-            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-1">重置</button></div>
-            <table class="sortable">
-                <tr><th>一级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
-                {"".join(category_rows)}
-            </table>
+        <div class="chart-container" data-section="category">
+            <div class="legend">
+                颜色越深=数值越高 | 点击表头排序
+                <div class="date-switcher table-date" data-section="category">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable"><tr><th>一级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
+                {daily_html[dt]['category_rows']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec1-3">1.3 二级品类整体</h3>
         <h3 id="sec1-3">1.3 二级品类整体</h3>
-        <div class="chart-container">
-            <div class="legend">颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" id="reset-2">重置</button></div>
-            <table class="sortable">
-                <tr><th>二级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
-                {"".join(cat2_overall_rows)}
-            </table>
+        <div class="chart-container" data-section="cat2">
+            <div class="legend">
+                颜色越深=数值越高 | 点击表头排序
+                <div class="date-switcher table-date" data-section="cat2">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable"><tr><th>二级品类</th><th>点击UV</th><th>进入分发率</th><th>整体裂变率</th><th>头部裂变率</th><th>推荐裂变率</th></tr>
+                {daily_html[dt]['cat2_rows']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h2 id="sec2">二、渠道×一级品类</h2>
         <h2 id="sec2">二、渠道×一级品类</h2>
 
 
         <h3 id="sec2-1">2.1 整体裂变率</h3>
         <h3 id="sec2-1">2.1 整体裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.80 | 点击表头/行名排序 <button class="reset-btn" id="reset-3">重置</button></div>
-            <table class="sortable">
-                {ror_header}
-                {"".join(ror_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m1_ror">
+            <div class="legend">max=0.80 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m1_ror">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec2-2">2.2 头部裂变率</h3>
         <h3 id="sec2-2">2.2 头部裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.30 | 点击表头/行名排序 <button class="reset-btn" id="reset-4">重置</button></div>
-            <table class="sortable">
-                {ror_header}
-                {"".join(ror_orig_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m1_orig">
+            <div class="legend">max=0.30 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m1_orig">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror_orig']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec2-3">2.3 推荐裂变率</h3>
         <h3 id="sec2-3">2.3 推荐裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.50 | 点击表头/行名排序 <button class="reset-btn" id="reset-5">重置</button></div>
-            <table class="sortable">
-                {ror_header}
-                {"".join(ror_rec_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m1_rec">
+            <div class="legend">max=0.50 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m1_rec">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{ror_header}{daily_html[dt]['matrix_ror_rec']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec2-4">2.4 点击UV</h3>
         <h3 id="sec2-4">2.4 点击UV</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=10万 | 点击表头/行名排序 <button class="reset-btn" id="reset-6">重置</button></div>
-            <table class="sortable">
-                {uv_header}
-                {"".join(uv_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m1_uv">
+            <div class="legend">max=10万 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m1_uv">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{ror_header}{daily_html[dt]['matrix_uv']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec2-5">2.5 进入分发率</h3>
         <h3 id="sec2-5">2.5 进入分发率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">min=0.60, max=1.00 | 点击表头/行名排序 <button class="reset-btn" id="reset-7">重置</button></div>
-            <table class="sortable">
-                {ror_header}
-                {"".join(recommend_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m1_recommend">
+            <div class="legend">min=0.60, max=1.00 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m1_recommend">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{ror_header}{daily_html[dt]['matrix_recommend']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h2 id="sec3">三、渠道×二级品类</h2>
         <h2 id="sec3">三、渠道×二级品类</h2>
 
 
         <h3 id="sec3-1">3.1 整体裂变率</h3>
         <h3 id="sec3-1">3.1 整体裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.80 | 点击表头/行名排序 <button class="reset-btn" id="reset-8">重置</button></div>
-            <table class="sortable">
-                {cat2_ror_header}
-                {"".join(cat2_ror_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m2_ror">
+            <div class="legend">max=0.80 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m2_ror">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec3-2">3.2 头部裂变率</h3>
         <h3 id="sec3-2">3.2 头部裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.30 | 点击表头/行名排序 <button class="reset-btn" id="reset-9">重置</button></div>
-            <table class="sortable">
-                {cat2_ror_header}
-                {"".join(cat2_ror_orig_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m2_orig">
+            <div class="legend">max=0.30 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m2_orig">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror_orig']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec3-3">3.3 推荐裂变率</h3>
         <h3 id="sec3-3">3.3 推荐裂变率</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=0.50 | 点击表头/行名排序 <button class="reset-btn" id="reset-10">重置</button></div>
-            <table class="sortable">
-                {cat2_ror_header}
-                {"".join(cat2_ror_rec_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m2_rec">
+            <div class="legend">max=0.50 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m2_rec">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_ror_rec']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
         <h3 id="sec3-4">3.4 点击UV</h3>
         <h3 id="sec3-4">3.4 点击UV</h3>
-        <div class="chart-container heatmap matrix-section">
-            <div class="legend">max=10万 | 点击表头/行名排序 <button class="reset-btn" id="reset-11">重置</button></div>
-            <table class="sortable">
-                {cat2_ror_header}
-                {"".join(cat2_uv_rows)}
-            </table>
+        <div class="chart-container heatmap matrix-section" data-section="m2_uv">
+            <div class="legend">max=10万 | 点击表头/行名排序
+                <div class="date-switcher table-date" data-section="m2_uv">
+                    <button class="prev-btn">◀</button>
+                    <select class="date-select">{date_options_html}</select>
+                    <button class="next-btn">▶</button>
+                    <button class="play-btn">▶</button>
+                </div>
+            </div>
+            {"".join([f'''<div class="date-content{' active' if dt == latest_date else ''}" data-date="{dt}">
+                <table class="sortable">{cat2_ror_header}{daily_html[dt]['matrix2_uv']}</table></div>''' for dt in date_options])}
         </div>
         </div>
 
 
     </div>
     </div>
@@ -684,6 +745,156 @@ html_content = f"""<!DOCTYPE html>
             }});
             }});
         }}
         }}
     }});
     }});
+
+    // ============================================================
+    // 日期切换功能
+    // ============================================================
+    const dates = {dates_json};
+    let playIntervals = {{}};
+
+    // 存储每个section当前的行顺序
+    const sectionRowOrder = {{}};
+
+    function getRowKeys(container) {{
+        const active = container.querySelector('.date-content.active');
+        if (!active) return [];
+        return Array.from(active.querySelectorAll('table tr')).slice(1).map(tr => tr.cells[0]?.textContent || '');
+    }}
+
+    function switchDate(section, date) {{
+        const container = document.querySelector(`.chart-container[data-section="${{section}}"]`);
+        if (!container) return;
+
+        // 记录切换前的行顺序
+        const oldOrder = getRowKeys(container);
+        const oldOrderMap = {{}};
+        oldOrder.forEach((key, idx) => oldOrderMap[key] = idx);
+
+        // 切换内容显示
+        container.querySelectorAll('.date-content').forEach(el => {{
+            el.classList.toggle('active', el.dataset.date === String(date));
+        }});
+
+        // 获取新的行顺序并应用动画
+        const newContent = container.querySelector('.date-content.active');
+        if (newContent && oldOrder.length > 0) {{
+            const rows = Array.from(newContent.querySelectorAll('table tr')).slice(1);
+            rows.forEach((tr, newIdx) => {{
+                const key = tr.cells[0]?.textContent || '';
+                const oldIdx = oldOrderMap[key];
+                tr.classList.remove('row-up', 'row-down', 'row-same');
+                if (oldIdx === undefined) {{
+                    tr.classList.add('row-up'); // 新出现的行
+                }} else if (newIdx < oldIdx) {{
+                    tr.classList.add('row-up'); // 排名上升
+                }} else if (newIdx > oldIdx) {{
+                    tr.classList.add('row-down'); // 排名下降
+                }} else {{
+                    tr.classList.add('row-same');
+                }}
+            }});
+        }}
+
+        // 更新下拉框
+        const switcher = document.querySelector(`.date-switcher[data-section="${{section}}"]`);
+        if (switcher) {{
+            switcher.querySelector('.date-select').value = date;
+            updateNavButtons(switcher);
+        }}
+    }}
+
+    function updateNavButtons(switcher) {{
+        const select = switcher.querySelector('.date-select');
+        const idx = dates.indexOf(select.value);
+        switcher.querySelector('.prev-btn').disabled = (idx <= 0);
+        switcher.querySelector('.next-btn').disabled = (idx >= dates.length - 1);
+    }}
+
+    function switchAll(date) {{
+        // 所有可切换的 section
+        const allSections = [
+            'channel', 'category', 'cat2',
+            'm1_ror', 'm1_orig', 'm1_rec', 'm1_uv', 'm1_recommend',
+            'm2_ror', 'm2_orig', 'm2_rec', 'm2_uv'
+        ];
+        allSections.forEach(section => switchDate(section, date));
+        document.querySelector('#global-switcher .date-select').value = date;
+        updateNavButtons(document.querySelector('#global-switcher'));
+    }}
+
+    function playSection(switcher, section) {{
+        const playBtn = switcher.querySelector('.play-btn');
+        const isGlobal = switcher.id === 'global-switcher';
+        const key = isGlobal ? 'global' : section;
+
+        if (playIntervals[key]) {{
+            clearInterval(playIntervals[key]);
+            playIntervals[key] = null;
+            playBtn.classList.remove('playing');
+            playBtn.textContent = isGlobal ? '▶ 播放' : '▶';
+            return;
+        }}
+
+        playBtn.classList.add('playing');
+        playBtn.textContent = isGlobal ? '⏸ 暂停' : '⏸';
+
+        let idx = 0;
+        const play = () => {{
+            if (idx >= dates.length) {{
+                clearInterval(playIntervals[key]);
+                playIntervals[key] = null;
+                playBtn.classList.remove('playing');
+                playBtn.textContent = isGlobal ? '▶ 播放' : '▶';
+                return;
+            }}
+            if (isGlobal) {{
+                switchAll(dates[idx]);
+            }} else {{
+                switchDate(section, dates[idx]);
+            }}
+            idx++;
+        }};
+        play();
+        playIntervals[key] = setInterval(play, 1500);
+    }}
+
+    // 绑定全局切换器
+    const globalSwitcher = document.querySelector('#global-switcher');
+    globalSwitcher.querySelector('.date-select').addEventListener('change', e => switchAll(e.target.value));
+    globalSwitcher.querySelector('.prev-btn').addEventListener('click', () => {{
+        const select = globalSwitcher.querySelector('.date-select');
+        const idx = dates.indexOf(select.value);
+        if (idx > 0) switchAll(dates[idx - 1]);
+    }});
+    globalSwitcher.querySelector('.next-btn').addEventListener('click', () => {{
+        const select = globalSwitcher.querySelector('.date-select');
+        const idx = dates.indexOf(select.value);
+        if (idx < dates.length - 1) switchAll(dates[idx + 1]);
+    }});
+    globalSwitcher.querySelector('.play-btn').addEventListener('click', () => playSection(globalSwitcher, null));
+    updateNavButtons(globalSwitcher);
+
+    // 绑定各表格切换器
+    document.querySelectorAll('.table-date').forEach(switcher => {{
+        const section = switcher.dataset.section;
+        switcher.querySelector('.date-select').addEventListener('change', e => switchDate(section, e.target.value));
+        switcher.querySelector('.prev-btn').addEventListener('click', () => {{
+            const select = switcher.querySelector('.date-select');
+            const idx = dates.indexOf(select.value);
+            if (idx > 0) {{
+                switchDate(section, dates[idx - 1]);
+            }}
+        }});
+        switcher.querySelector('.next-btn').addEventListener('click', () => {{
+            const select = switcher.querySelector('.date-select');
+            const idx = dates.indexOf(select.value);
+            if (idx < dates.length - 1) {{
+                switchDate(section, dates[idx + 1]);
+            }}
+        }});
+        switcher.querySelector('.play-btn').addEventListener('click', () => playSection(switcher, section));
+        updateNavButtons(switcher);
+    }});
     </script>
     </script>
 </body>
 </body>
 </html>
 </html>